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

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

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

/* t.h */
#ifndef _H_
#define _H_
int a;
#endif

/* foo.c */
#include <stdio.h>
#include "t.h"

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

int main();

void foo()
{
    printf("foo:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
        \tsizeof(b)=%d\n\tb.a=%d\n\tb.b=%d\n\tmain:0x%08x\n",
        &a, &b, sizeof b, b.a, b.b, main);
}

/* main.c */
#include <stdio.h>
#include "t.h"

int b;
int c;

int main()
{
    foo();
    printf("main:\t(&a)=0x%08x\n\t(&b)=0x%08x\n
        \t(&c)=0x%08x\n\tsize(b)=%d\n\tb=%d\n\tc=%d\n",
        &a, &b, &c, sizeof b, b, c);
    return 0;
}

После использования компиляции Ubuntu GCC 4.4.3 результат выглядит следующим образом:

foo:    (&a)=0x0804a024
    (&b)=0x0804a014
    sizeof(b)=8
    b.a=2
    b.b=4
    main:0x080483e4
main:   (&a)=0x0804a024
    (&b)=0x0804a014
    (&c)=0x0804a028
    size(b)=4
    b=2
    c=0

Переменные a и b имеют один и тот же адрес в двух функциях, но размер b изменился. Я не понимаю, как это работает!

4b9b3361

Ответ 1

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

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

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

Обратите внимание, что у вас есть два внешних определения для 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):

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

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

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

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

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

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

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

Обычные заявления об отказе применяются для поведения undefined. Все может случиться, и это будет включать поведение, которое вы наблюдали.


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

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

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

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

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

"Общая модель" может быть подавлена ​​в компиляторе GNU с помощью параметра -fno-common. Когда я тестировал это в своей системе, это вызвало поведение "Строгое изображение Ref/Def Model" для кода, подобного вашему:

$ 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
$

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


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

Ответ 2

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

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

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

Ответ 3

Кусок кода, по-видимому, прервет одно правило определения. Он будет ссылаться на поведение undefined, не делайте этого.

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

В t.h:

extern int a;

В foo.c

int a;

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

В foo.c:

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

В main.c

static int b;

Ответ 4

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

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

Ответ 5

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

Ответ 6

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