Почему c и c ++ по-разному трактуют переопределения неинициализированных переменных? - PullRequest
4 голосов
/ 30 марта 2020
int a;
int a=3; //error as cpp compiled with clang++-7 compiler but not as C compiled with clang-7;

int main() {

}

Для C компилятор, кажется, объединяет эти символы в один глобальный символ, но для C ++ это ошибка.

Демо


file1:

int a = 2;

file2:

#include<stdio.h>
int a; 

int main() {
    printf("%d", a); //2
}

Поскольку C файлы, скомпилированные с помощью clang-7, компоновщик не выдает ошибку, и я предполагаю, что он преобразует неинициализированный глобальный символ 'a' к внешнему символу (рассматривая его, как если бы он был скомпилирован как внешнее объявление). Так как файлы C ++ скомпилированы с помощью clang ++ - 7, компоновщик выдает ошибку множественного определения.


Обновление: связанный вопрос действительно отвечает на первый пример в моем вопросе, а именно: «В C, если фактический внешнее определение находится раньше или позже в той же самой единице перевода, тогда предварительное определение просто действует как декларация. ' и «C ++ не имеет« предварительных определений ».

Что касается второго сценария, если я печатаю a, то он печатает 2, так что, очевидно, компоновщик связал его правильно (но я ранее предполагал, что предварительное определение будет инициализировано компилятором в 0) как глобальное определение и может вызвать ошибку связи.

Оказывается, что int i[]; предварительное определение в обоих файлах также связано с одним определением. int i[5]; также является предварительным определением в .common, только с другим размером, выраженным ассемблеру. Первое известно как предварительное определение с неполным типом, тогда как второе является предварительным определением с полным типом.

Что происходит с компилятором C, так это то, что int a становится сильным слабый глобальный в .common и оставленный неинициализированным (где .common подразумевает слабый глобальный) в таблице символов (тогда как extern int a будет внешним символом), и компоновщик принимает необходимое решение , то есть он игнорирует все глобалы со слабой связью, определенные с помощью #pragma weak, если в единице перевода существует глобал со строгой связью с тем же идентификатором, где 2 сильные границы будут ошибкой множественного определения (но если он не найдет сильную -bounds и 1 слабой границей, выходной файл представляет собой одну слабую границу, и если он не находит сильных границ, но две слабых границы, он выбирает определение в первом файле в командной строке и выводит одну слабую границу Хотя две слабые границы - это два определения для компоновщика (потому что они инициализируются компилятором в 0) , это не множественная ошибка определения, потому что они оба являются слабосвязанными), а затем разрешает все символы .common, чтобы указывать на сильный / слабый связанный сильный глобал. https://godbolt.org/z/Xu_8tY https://docs.oracle.com/cd/E19120-01/open.solaris/819-0690/chapter2-93321/index.html Поскольку baz объявлено с #pragma слабым, оно является слабой границей и обнуляется компилятором и вставляется. bss (даже если это слабый глобал, он не go в .common, потому что он слабосвязан; все слабые переменные go в .bss, если неинициализированы и инициализируются компилятором, или. данные, если они инициализированы). Если он не был объявлен с #pragma weak, baz будет go общим, и компоновщик обнулит его, если не найден сильный глобальный символ со слабой / сильной связью.

Компилятор C ++ делает int a сильный глобал в .bss и инициализирует его 0 : https://godbolt.org/z/aGT2-o, поэтому компоновщик обрабатывает его как множественное определение.

1 Ответ

2 голосов
/ 12 апреля 2020

Я остановлюсь на C конце вопроса, так как я более знаком с этим языком, и вы, похоже, уже достаточно ясно понимаете, почему сторона C ++ работает так, как она работает. Кто-то другой может добавить подробный ответ C ++.

Как вы заметили, в вашем первом примере C обрабатывает строку int a; как предварительное определение (см. 6.9.2). в N2176 ). Позднее int a = 3; является объявлением с инициализатором, поэтому это внешнее определение. Таким образом, предыдущее предварительное определение int a; рассматривается как просто декларация. Итак, задним числом вы сначала объявили переменную в области видимости файла, а затем определили ее (с помощью инициализатора). Нет проблем.

Во втором примере file2 также имеет предварительное определение a. В этом модуле перевода нет внешнего определения, поэтому

поведение точно такое, как если бы модуль перевода содержал объявление области файла этого идентификатора с составным типом на конец модуля перевода с инициализатором, равным 0. [6.9.2 (1)]

То есть, как если бы вы написали int a = 0; в file2. Теперь у вас есть два внешних определения a в вашей программе, одно в file1 и другое в file2. Это нарушает 6.9 (5):

Если в выражении используется идентификатор, объявленный с внешней связью (кроме как как часть операнда оператора sizeof или _Alignof, результатом которого является целочисленная константа), где-то во всей программе должно быть ровно одно внешнее определение для идентификатора; в противном случае их должно быть не более одного.

Таким образом, в соответствии со стандартом C поведение вашей программы не определено, и компилятор может делать то, что ему нравится. (Но учтите, что диагностика c не требуется.) В вашей конкретной реализации вместо вызова носовых демонов ваш компилятор решит то, что вы описали: используйте функцию common вашего формата объектных файлов и получите Линкер объединяет определения в одно. Хотя это и не требуется стандартом, это поведение является традиционным по крайней мере для Unix и упоминается стандартом как «общее расширение» (без каламбура) в J.5.11.

Эта функция, на мой взгляд, весьма удобна, но, поскольку она возможна только в том случае, если ваш объектный формат файла поддерживает ее, мы не можем ожидать, что стандартные авторы C будут ее использовать.

* 1035 Насколько я понимаю, *clang не очень четко документирует это поведение, но gcc с таким же поведением описывает его в опции -fcommon. В любом компиляторе вы можете отключить его с помощью -fno-common, и тогда ваша программа не сможет связать с ошибкой множественного определения.
...