Наши проекты:
Журнал · Discuz!ML · Wiki · DRKB · Помощь проекту |
||
ПРАВИЛА | FAQ | Помощь | Поиск | Участники | Календарь | Избранное | RSS |
[3.144.249.247] |
|
Сообщ.
#1
,
|
|
|
Народ, я себя отношу к сугубо прикладным программистам.
(Даже не к программистам вовсе, а к научно-техническим работникам, использующим ПК как мощнейший калькулятор для проведения исследований). Так вот, вроде до последнего времени все было отлично как в Linux, так и в Windows. Но тут решил поставить на виртуальную машину Debian 11 с суперновейшими компиляторами и начались проблемы. 1. Еле-еле завел Qt creator, так как в Debian 11 (да и в Ubuntu последних версий) убрали пакет Qt5-default, дающий в том числе профили Qt. Полдня соображал, что надо вручную указать путь расположения qmake... 2. Дальше - больше. Стал собирать свои старые программы с глобальными переменными (или типами переменных), подключаемыми в разных модулях. Чтобы не было двойного объявления переменных, я использовал подсказанный мне опытными программистами старый прием - условную компиляцию с объявлением макроса. Например, в модуле glvar.hpp объявляю глобальные переменные/типы: #ifndef GLVAR #define GLVAR // Секция глобальных переменных typedef double vXYZ[3]; // Слэш в зависимости от ОС (Linux '/', Windows '\') char cSl; #endif А непосредственно в нескольких модулях *.c использую эти переменные/типы: #ifndef GLVAR #include "glvar.hpp" #endif // Функция поиска максимума void MAX2() { // ... cSl = '/'; return; } Раньше, до Debian 11, все работало просто отлично! А сейчас сыплет ошибками и пишет, что переменные объявляются несколько раз. Подскажите, где копать? P.S. Думал, что это в Qt начудили. Но в командной строке тоже самое! И с clang, и с gcc. |
Сообщ.
#2
,
|
|
|
Цитата mkudritsky @ Это были не очень опытные программисты.Чтобы не было двойного объявления переменных, я использовал подсказанный мне опытными программистами старый прием Судя по тексту, есть путаница в терминах. Есть понятие "определение", и есть "объявление". Это не одно и то же. Определение вводит в контекст некую сущность, тогда как объявление лишь информирует о ней. Например, вон, во втором фрагменте кода определена функция MAX2(). Она введена в контекст, компилятор проверил её синтаксис и грамматику и скомпилировал в объектный код. А вот известное понятие "прототип функции" является самым настоящим объявлением. Компилятор информирован о её наличии, параметрах и возвращаемом результате, и этого ему достаточно, чтобы в объектном коде правильно генерировать её вызовы и использоваль её результаты, но самой функции он не видел, ничего там естественно не мог проверить и никакого кода для неё не сгенерировал. Определений некой сущности во всей программе должно быть только одно, ибо не может быть так, чтобы у неё было несколько экземпляров, даже если они идентичны. А вот объявлений может быть сколько угодно, потому как компилятору всё равно, сколько раз его уверят в её наличии где-то там. Заголовки предназначены как раз для размещения в них объявлений. Их роль заключается в документировании внешнего интерфейса некой подсистемы, а для этого определений не требуется. Скажем <stdio.h> публикует интерфейс подсистемы ввода/вывода, так что в нём будут все необходимые типы, прототипы функций и объявления переменных stdin, stdout и stderr. А тот факт, что определения всех этих сущностей лежат в стандартных библиотеках, знать в общем-то необязательно, т.к. это знание (или его отсутствие) абсолютно никак не влияет на возможность ими свободно пользоваться везде, где подключён <stdio.h>. Опытные программисты не говорят, как избежать ошибок дублирования сущностей. Вместо этого они говорят, что определения не должны располагаться в заголовках, там должны быть только объявления. Потому что объявления являются частью интерфейса подсистемы, а определения – частью её реализации. Детали реализации выносить в публичный доступ – это всегда моветон. Правильно оформленный пример выше будет выглядеть примерно так: // glvar.hpp #ifndef GLVAR // имена для стражей включения лучше делать более уникальными, чем просто имя заголовка #define GLVAR // я, например, предпочитаю добавлять в их конец GUID проекта, сгенерированный спецом для него // Секция глобальных переменных typedef double vXYZ[3]; // Слэш в зависимости от ОС (Linux '/', Windows '\') extern char cSl; // объявление #endif // glvar.cpp #include "glvar.hpp" char cSl; // определение // main.cpp #include "glvar.hpp" // Функция поиска максимума void MAX2() { // ... cSl = '/'; return; } |
Сообщ.
#3
,
|
|
|
Однако, с формальной т.з., должно же было происходить так:
1. Зайдя в первый xxx.c-файл, компилятор (препроцессор) не знал о GLVAR, включил тот блок, откомпилировал и сказал, что норм. 2. Зайдя во второй yyy.c-файл, компилятор (препроцессор) знал о GLVAR, не включил тот блок, и не должен был компилировать (т.к. неизвестная переменная), выдав ошибку компиляции, а не компоновки. Отчего же шаг 2 не произошёл? |
Сообщ.
#4
,
|
|
|
Спасибо за информацию к размышлению!
Вроде я начинаю понимать, почему так все происходит. Почему-то в Debian 11 компиляторы собирают все глобальные переменные из всех модулей в одну большую общую кучу. То есть, если в модуле 1.c объявлена переменная int iVar и в модуле 2.c объявлена int iVar, то при сборке выдается сообщение о двойном объявлении переменной iVar. А я-то рассчитывал, что в модулях глобальные переменные - только для этих модулей. Получается, что в "до Debian 11" все так и было, а в Debian 11 все глобальное объединяется вместе. Может надо какую-то опцию компилятору задать? (Ну и ее же в Qt прописать). |
Сообщ.
#5
,
|
|
|
Цитата mkudritsky @ Например, в модуле glvar.hpp объявляю глобальные переменные/типы: Попробуй так: // -------------------------------------------------------------------------- // FILE PROJECTVAR.H 2022.03.20 // -------------------------------------------------------------------------- #ifndef __PROJECT_VAR_H__ #define __PROJECT_VAR_H__ #ifdef THIS_IS_MAIN_MODULE // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv int SomeVar; // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #else // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv extern int SomeVar; // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #endif // .......................................................................... #endif Или так: // -------------------------------------------------------------------------- // FILE PROJECTVAR.H 2022.03.20 // -------------------------------------------------------------------------- #ifndef __PROJECT_VAR_H__ #define __PROJECT_VAR_H__ #ifdef THIS_IS_MAIN_MODULE // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv #define PLACE_var // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #else // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv #define PLACE_var extern // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #endif PLACE_var int SomeVar; // .......................................................................... #endif В одном единственном модуле: // -------------------------------------------------------------------------- // 2022.03.20 // file main.c // // -------------------------------------------------------------------------- #define THIS_IS_MAIN_MODULE // -------------------------------------------------------------------------- #include "project.h" // -------------------------------------------------------------------------- int main (...) { .... |
Сообщ.
#6
,
|
|
|
Цитата Славян @ Раздельная компиляция. Компилируя yyy.c компилятор точно так же ничего не знает о xxx.c, а ошибки повторного определения выдает редактор связей (линкер) на следующем за компиляцией этапе.2. Зайдя во второй yyy.c-файл, компилятор (препроцессор) знал о GLVAR, не включил тот блок, и не должен был компилировать (т.к. неизвестная переменная), выдав ошибку компиляции, а не компоновки. Отчего же шаг 2 не произошёл? Цитата mkudritsky @ Они всегда так делали. Только вот в "голом" C в отличие от C++ для совместимости со старым кодом допускалось двойное определение переменных (не могу быстро найти описание, приведу его чуть позже), возможно, в новых версиях эту возможность отключили по-умолчанию.Почему-то в Debian 11 компиляторы собирают все глобальные переменные из всех модулей в одну большую общую кучу. Цитата mkudritsky @ Тогда надо было писать statiic int iVar; А я-то рассчитывал, что в модулях глобальные переменные - только для этих модулей. |
Сообщ.
#7
,
|
|
|
Цитата Dushevny @ В общем искать надо tentative definitionТолько вот в "голом" C в отличие от C++ для совместимости со старым кодом допускалось двойное определение переменных (не могу быстро найти описание, приведу его чуть позже) Вот две ссылки: https://blogs.oracle.com/solaris/post/what-...ntative-symbols - объяснение, как это работет в "голом" C https://stackoverflow.com/questions/6220881...e-common-symbol - о том, что в GCC10 по умолчанию включена опция -fno-common, которая такое поведение запрещает. |
Сообщ.
#8
,
|
|
|
Цитата Dushevny @ Они всегда так делали. Только вот в "голом" C в отличие от C++ для совместимости со старым кодом допускалось двойное определение переменных (не могу быстро найти описание, приведу его чуть позже), возможно, в новых версиях эту возможность отключили по-умолчанию. Dushevny, в С-компиляторах GCC по этому вопросу просто ..опа, специально проверял. (в Fedora) Определение переменных с одним именем в разных модулях, приводит к тому, что компилер считает их одной и той же (физической) переменной. (Без ключевого слова extern !) И никаких предупреждений при этом не делает. Это продолжается уже давно - лет 20, а может и от начала времён. --- Могу лишь посоветовать - никогда не использовать С-компилятор из набора GCC. Использовать С++ - у него такого эффекта нет. |
Сообщ.
#9
,
|
|
|
Цитата ЫукпШ @ Это не ..опа, а так и должно быть по стандврту. И не только в GCC. Но это можно отключить ключами командной строки. в С-компиляторах GCC по этому вопросу просто ..опа, специально проверял. (в Fedora) Определение переменных с одним именем в разных модулях, приводит к тому, что компилер считает их одной и той же (физической) переменной. (Без ключевого слова extern !) |
Сообщ.
#10
,
|
|
|
Цитата mkudritsky @ Глобальные переменные являются частью состояния приложения в целом. Конечно они должны быть доступны изо всех модулей, в которых объявлены. Поэтому дублирование их определений является ошибкой, причём скорее архитектурной, чем программной. Но модули могут хранит и свои локальные состояния. Как уже написал Dushevny, для этого определение должно быть предварено static:А я-то рассчитывал, что в модулях глобальные переменные - только для этих модулей. Получается, что в "до Debian 11" все так и было, а в Debian 11 все глобальное объединяется вместе. Может надо какую-то опцию компилятору задать? (Ну и ее же в Qt прописать). // glvar.hpp #ifndef GLVAR // имена для стражей включения лучше делать более уникальными, чем просто имя заголовка #define GLVAR // я, например, предпочитаю добавлять в их конец GUID проекта, сгенерированный спецом для него // Секция глобальных переменных typedef double vXYZ[3]; // Слэш в зависимости от ОС (Linux '/', Windows '\') static char cSl; // определение #endif // main.cpp #include "glvar.hpp" // Функция поиска максимума void MAX2() { // ... cSl = '/'; return; } P.S. Несмотря на то, что нарушается принцип размещать в заголовках только объявления, по факту такое размещение не публикует сущности наружу, так что можно считать, что это нарушает другой принцип: предназначения заголовков для публикаций интерфейсов. "Идеальный код" такого содержать не должен, но обычно всем пофик. Опытные программисты не просто знают принципы, они понимают, почему они такие. И поэтому знают, когда можно отойти от буквы закона, не ломая дух закона. Добавлено P.P.S. Да, Dushevny, в C допускаются множественные определения, если они идентичны. Но не всё так просто. Чуть позже подниму Стандарт. |
Сообщ.
#11
,
|
|
|
Вот. Цитата из Стандарт C89:
Цитата A.6.5.11 Multiple external definitions Т.е. позволяются полностью идентичные определения при условии, что не более одного из них будет содержать инициализатор. There may be more than one external definition for the identifier of an object, with or without the explicit use of the keyword extern, If the definitions disagree, or more than one is initialized, the behavior is undefined ($3.7.2). Добавлено Ну и немного примеров ещё нашёл в 3.7.2 "External object definitions" int i1 = 1; /* definition, external linkage */ static int i2 = 2; /* definition, internal linkage */ extern int i3 = 3; /* definition, external linkage */ int i4; /* tentative definition, external linkage */ static int i5; /* tentative definition, internal linkage */ int i1; /* valid tentative definition, refers to previous */ int i2; /* $3.1.2.2 renders undefined, linkage disagreement */ int i3; /* valid tentative definition, refers to previous */ int i4; /* valid tentative definition, refers to previous */ int i5; /* $3.1.2.2 renders undefined, linkage disagreement */ extern int i1; /* refers to previous, whose linkage is external */ extern int i2; /* refers to previous, whose linkage is internal */ extern int i3; /* refers to previous, whose linkage is external */ extern int i4; /* refers to previous, whose linkage is external */ extern int i5; /* refers to previous, whose linkage is internal */ |
Сообщ.
#12
,
|
|
|
Цитата Qraizer @ // я, например, предпочитаю добавлять в их конец GUID проекта, сгенерированный спецом для него #pragma once ещё не везде что ли работает? |
Сообщ.
#13
,
|
|
|
По поводу прагм в Стандарте всё плохо. Всё, что о них там написано, помимо определения синтаксиса, это что они предназначены для управления процессом трансляции специфическим и зависящим от реализации способом. Ну и что нераспознанные реализацией прагмы должны молча игнорироваться. Что же касается конкретных прагм, там вообще полный ноль. Т.е. просто нет ни одной Стандартной прагмы. Ситуация ровно как с asm: есть определение синтаксиса ассемблерных вставок и на этом всё, Стандарт оставляет их грамматику полностью на усмотрение реализаций.
Так что если нужен кроссплатформенный страж включения, то это не #pragma once. |
Сообщ.
#14
,
|
|
|
Цитата Dushevny @ о том, что в GCC10 по умолчанию включена опция -fno-common, которая такое поведение запрещает Для решения проблемы в стиле "скорой помощи" этот совет оказался самым эффективным! Компиляция прошла отлично следующей строкой: clang -m64 -std=c11 -fcommon -O3 main.c src/*.c -lm (тут все мои модули, где без опции -fcommon было двойное объявление переменных, лежат в каталоге src) Для работы среды Qt пришлось опцию -fcommon добавить в проект (файл *.pro): QMAKE_CFLAGS += -std=c11 -fcommon Но, конечно, в глобальном плане надо со всем разбираться, чтобы не использовать #define GLOBALVAR Будем разбираться! P.S. Уже 7 лет осваиваю C/C++ как научно-технический работник и никак не могу привыкнуть к его особенностям! Разумеется, в моих программах везде используются функции sin, cos, sqrt, pow... и мне никак не понятно, почему не достаточно объявить #include <math.h> Фантастика! В сборке в командной строке для этого обязателен ключ -lm Причем, этот ключ надо писать в самом конце строки компиляции (посередине он не работает - не может найти математические функции). А теперь еще и этот -fcommon как гром среди ясного неба. P.P.S. Сдается мне, что приближаюсь к типовым программам на C/C++, когда ключи компиляции и сборки занимают по половине экрана текста! |
Сообщ.
#15
,
|
|
|
Цитата mkudritsky @ Компилятору достаточно. Ключ -lm передаётся линкеру. Мне тоже непонятно, почему нельзя было просто сканировать все стандартные библиотеки. MSный линкер, к примеру, такой фигнёй не страдает.и мне никак не понятно, почему не достаточно объявить #include <math.h> Что же до -fcommon... Я всегда предпочитал именно такое поведение. Не надо позволять множественные определения, это чревато глубоко зарытыми проблемами. Однако Qt тот ещё тролль: вот так нагло идти поперёк Стандарта и собирать все глобальные сущности в одну кучу, наплевав на требование раздельной компиляции... |