C одна и та же глобальная переменная, определенная в разных файлах

Я читаю этот код(на китайском языке). Есть один кусок кода о тестировании глобальной переменной в переменную a было определено в файле t.h, который был включен дважды. В файле foo.c определен struct b С некоторым значением и

6 ответов


вы нарушаете "правило одного определения" C, и результатом является неопределенное поведение. "Правило одного определения" формально в стандарте как таковом не указано. Мы рассматриваем объекты в разных исходных файлах (ака, единицы перевода), поэтому мы касаемся "внешних определений". Семантика "одного внешнего определения" прописана (C11 6.9 p5):

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

что в основном означает, что вам разрешено только определение an объект в лучшем случае после. (Предложение otherwise позволяет вообще не определять внешний объект, если он никогда не используется нигде в программе.)

обратите внимание, что у вас есть два внешних определений b. Одна из них-это структура, которую вы инициализируете в foo.c, а другой предварительное определение на main.c, (C11 6.9.2 p1-2):

если объявление идентификатора для объекта, объем файла и инициализатор, этот объявление-Это внешнее определение идентификатора.

объявление идентификатора для объекта, который имеет область файла без инициализатора и без спецификатора класса хранения или со спецификатором класса хранения static, представляет собой предварительное определение. Если единица перевода содержит одно или несколько предварительных определений для идентификатора, а единица перевода не содержит внешнего определения для этого идентификатора, то поведение точно так же, как если бы блок перевода содержал объявление области файла этого идентификатора с составным типом по состоянию на конец блока перевода с инициализатором, равным 0.

таким образом, у вас есть несколько определений b. Однако есть еще одна ошибка, в том, что вы определили b С разными типами. Сначала обратите внимание, что разрешено несколько объявлений для одного объекта с внешней связью. Однако, когда одно и то же имя используется в двух разных источниках файлы, это имя относится к тому же объекту (C11 6.2.2 p2):

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

C накладывает строгое ограничение на объявления одного и того же объекта (C11 6.2.7 p2):

все объявления, ссылающиеся на один и тот же объект или функцию, должны быть совместимы тип; в противном случае поведение не определено.

так как типы для b в каждом из ваших исходных файлов на самом деле не совпадают, поведение не определено. (То, что составляет совместимый тип, подробно описано во всех C11 6.2.7, но в основном сводится к тому, что типы должны совпадать.)

Итак, у вас есть два недостатка для b:

  • несколько определений.
  • несколько объявлений с несовместимыми типы.

технически, ваша декларация int a в обоих ваших исходных файлах также нарушается "правило одного определения". Обратите внимание, что a имеет внешнюю связь (C11 6.2.2 p5):

если объявление идентификатора для объекта имеет область файла и нет спецификатора класса хранения, его связь является внешней.

но, из цитаты из C11 6.9.2 ранее, те int a предварительные определения являются внешними определениями, и вам разрешен только один из них из цитаты из C11 6.9 вверху.

обычные оговорки применяются для неопределенного поведения. Может случиться все, что угодно, включая поведение, которое вы наблюдали.


общее расширение до C должно допускать несколько внешних определений и описано в стандарте C в информационном приложении J. 5 (C11 J. 5.11):

для идентификатора объект, с или без явного использования ключевого слова extern; если определения не согласен, или инициализируется более одного,поведение не определено (6.9.2).

(выделено мной.) Так как определения для a согласитесь, там нет никакого вреда, но определения для b не согласен. Это расширение объясняет, почему компилятор не жалуется на наличие нескольких определений. От цитата из С11 6.2.2, компоновщик попытается согласовать несколько ссылок на один и тот же объект.

линкеры обычно используют одну из двух моделей для совмещения нескольких определений одного и того же символа в различных единиц перевода. Это "общая модель"и" модель Ref/Def". В" общей модели " несколько объектов с одинаковым именем складываются в один объект в union стиль так, чтобы объект принимал размер самого большого определения. В" модели Ref/Def " каждый внешнее имя должно иметь ровно одно определение.

цепочка инструментов GNU по умолчанию использует "общую модель "и" расслабленную модель Ref/Def", где она применяет строго одно правило определения для одной единицы перевода, но не жалуется на нарушения в нескольких единицах перевода.

"общая модель" может быть подавлена в компиляторе GNU с помощью . Когда я тестировал это в своей системе, это вызвало поведение "строгой модели Ref / Def" для кода похож на твой:

$ cat a.c
#include <stdio.h>
int a;
struct { char a; int b; } b = { 2, 4 };
void foo () { printf("%zu\n", sizeof(b)); }
$ cat b.c
#include <stdio.h>
extern void foo();
int a, b;
int main () { printf("%zu\n", sizeof(b)); foo(); }
$ gcc -fno-common a.c b.c
/tmp/ccd4fSOL.o:(.bss+0x0): multiple definition of `a'
/tmp/ccMoQ72v.o:(.bss+0x0): first defined here
/tmp/ccd4fSOL.o:(.bss+0x4): multiple definition of `b'
/tmp/ccMoQ72v.o:(.data+0x0): first defined here
/usr/bin/ld: Warning: size of symbol `b' changed from 8 in /tmp/ccMoQ72v.o to 4 in /tmp/ccd4fSOL.o
collect2: ld returned 1 exit status
$

я лично считаю, что последнее предупреждение, выданное компоновщиком, всегда должно предоставляться независимо от модели разрешения для нескольких определений объектов, но это ни здесь, ни там.


ссылки:
к сожалению, я не могу дать вам ссылку на мою копию стандарта С11
каковы extern переменные в C?
"руководство для начинающих линкеров"
документация SAS по моделям внешних переменных


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

практически, позволяя несколько определений одной и той же переменной с внешней связью является популярным расширением компилятора (общее расширение, упомянутое как таковое в спецификации языка). Однако для надлежащего использования каждое определение должно объявлять его с тем же типом. И нет более одного определения должно включать инициализатор.

ваш случай не соответствует общему описанию расширения. Ваш код компилируется как побочный эффект этого общего расширения, но его поведение все еще не определено.


кусок кода, похоже, специально нарушает правило одного определения. Это вызовет неопределенное поведение, не делайте этого.

о глобальной переменной a: не помещайте определение глобальной переменной в файл заголовка, так как оно будет включено в несколько .c файлы, и приводит к множественному определению. Просто поместите объявления в заголовок и поместите определение в одно из них .c файлов.

в т. h:

extern int a;

In foo.c

int a;

о глобальной переменной b: не определяйте его несколько раз, используйте static чтобы ограничить переменную в файле.

In foo.c:

static struct {
   char a;
   int b;
} b = { 2, 4 };

в Main.c

static int b;

b имеет тот же адрес, потому что линкер решил разрешить конфликт за вас.

sizeof показывает разные значения, потому что sizeof оценивается в время компиляции. На этом этапе компилятор знает только об одном b (тот, который определен в текущем файле).


во время компиляции foo b то есть в области действия-это два вектора ints {2, 4} или 8 байт, когда sizeof(int) равен 4. Когда main компилируется, b только что был объявлен как int таким образом, размер 4 имеет смысл. Также, вероятно, в структуру после" a "добавлены" байты заполнения", так что следующий слот (int) выровнен по границе 4 байта.


A и b имеют одинаковые адреса, потому что они происходят в одних и тех же точках файла. Тот факт, что b имеет другой размер, не имеет значения, где начинается переменная. Если вы добавили переменную c между a и b в одном из файлов, адрес bs будет отличаться.