C - При каких обстоятельствах внешнее объявление становится определением?

Из стандарта C99 6.2.3:

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

и 6,7

Объявление определяет интерпретацию и атрибуты набора идентификаторов. Определение идентификатора — это объявление для этого идентификатора, которое:

— for an object, causes storage to be reserved for that object;
— for a function, includes the function body;99)
— for an enumeration constant or typedef name, is the (only) declaration of the identifier.

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

Поэтому я провел несколько экспериментов. Сначала я заметил, что:

struct A a;
int main() {
}

недействителен, gcc говорит, что тип A неполный и не знает, как выделить память для a. Однако, что интересно, у нас есть следующий правильный код:

struct A a;
int main() {
}
struct A {int x;};

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

Однако объявление массива является исключительным. Измененный код больше не действителен:

struct A a[1];
int main() {
}
struct A {int x;};

И стандарт C99 говорит об этом, он говорит, что элементы массива должны быть завершенного типа. Итак, возникает вопрос: является ли struct A a[1] определением или декларацией? Не спешите отвечать на него. Проверьте следующие примеры.

Здесь у нас есть два файла: a.c и b.c. В a.c:

#include <stdio.h>
int arr[10];
void a_arr_info() {
    printf("%lu at %lx\n", sizeof arr, (size_t)arr);
}

в то время как в b.c:

#include <stdio.h>
int arr[20];
void b_arr_info() {
    printf("%lu at %lx\n", sizeof arr, (size_t)arr);
}
int main() {
    a_arr_info();
    b_arr_info();
}

Результат потрясающий. Вывод показывает, что arr в обоих файлах относится к одному и тому же адресу. Это можно понять, потому что arr оба находятся в области действия файла, поэтому они являются внешней связью. Проблема в том, что они имеют разный размер. В каком файле компилятор принял объявление как определение и выделил память?

Почему я спрашиваю об этом? Потому что я работаю над проектом упрощенного компилятора C (домашняя работа по курсу). Так что мне может быть важно это выяснить. Хотя домашнее задание не заходит так далеко, мне очень любопытно, и я хотел бы узнать больше. Спасибо!


person Determinant    schedule 08.04.2014    source источник
comment
В каком файле компилятор принял объявление как определение и выделил память? -- Какой бы он ни захотел, так как это неопределенное поведение. Выделение памяти фактически выполняется компоновщиком.   -  person Jim Balter    schedule 08.04.2014
comment
Обратите внимание, что во втором примере вы не можете получить доступ к a.x из main().   -  person M.M    schedule 08.04.2014
comment
Примеры a.c и b.c представляют собой совершенно отдельную проблему от предварительных определений ранее, возможно, измените это на два разных сообщения с вопросами. В a.c и b.c поведение не определено, потому что у вас есть два внешне видимых объекта с одинаковыми именами.   -  person M.M    schedule 08.04.2014
comment
@Matt, я полагаю, то же имя подойдет из-за внешней связи.   -  person Determinant    schedule 08.04.2014


Ответы (1)


Это называется предварительным определением

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

Таким образом, любая единица компиляции (файл .o), которая имеет такое предварительное определение, реализует объект. Связывание двух таких блоков вместе имеет неопределенное поведение, обычно вы должны сталкиваться с ошибкой «множественно определенный символ». Некоторые компиляторы/компоновщики просто делают это, вы должны убедиться, что такие символы имеют одинаковый размер и тип.

person Jens Gustedt    schedule 08.04.2014
comment
Не могли бы вы объяснить это подробно: и единица перевода не содержит внешнего определения для этого идентификатора, тогда поведение точно такое же, как если бы единица перевода содержала объявление области файла этого идентификатора с составным типом в конце перевода единица измерения? Что такое композитный тип? - person Determinant; 08.04.2014
comment
..и нет внешнего определения для этого идентификатора, что это? - person Determinant; 08.04.2014
comment
Внешнее определение — это внешнее объявление, которое также является определением. Внешнее объявление — это объявление, которое не находится внутри тела функции. Для составного типа здесь вы можете просто прочитать тип. - person M.M; 08.04.2014
comment
@Matt, я все еще не понимаю предварительного определения. не могли бы вы объяснить это в двух словах? так как иногда слова и выражения в документе немного абстрактны.. - person Determinant; 08.04.2014
comment
Переменная ведет себя как ее окончательное определение во время выполнения, однако во время компиляции код, предшествующий окончательному определению, может видеть только то, что было доступно в предварительном определении. Например int x[]; int x[] ; int x[]; int main() { printf("%p\n", (void *)x); } int x[10]; . Этот код хорош, но main() не может выполнить sizeof(x), так как на тот момент это неполный тип. - person M.M; 08.04.2014
comment
У вас может быть несколько идентичных предварительных определений, но только одно объявление (т. е. определение плюс инициализатор). Если вы никогда не предоставляете инициализатор, предполагается, что вы написали int x[1]; для массива, или int foo = 0;, или struct A a = { 0 }; - person M.M; 08.04.2014
comment
В исходном случае, поскольку A — неполный тип, struct A a = { 0 }; будет ошибкой. Таким образом, вы должны предоставить определение struct A до конца файла, если у вас есть предварительное определение с неполным типом ранее, чтобы struct A не было неполным типом в конце файла. - person M.M; 08.04.2014