Почему это утверждение создает ошибку компоновщика с gcc?

У меня есть этот чрезвычайно тривиальный кусок кода C:

static int arr[];
int main(void) {
    *arr = 4;
    return 0;
}

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

/usr/bin/ld: /tmp/cch9lPwA.o: in function 'main':
unit.c:(.text+0xd): undefined reference to 'arr'
collect2: error: ld returned 1 exit status

Разве компилятор не сможет поймать это перед компоновщиком?

Мне также странно, что, если я опускаю static класс хранения, компилятор просто предполагает, что массив имеет длину 1 и не выдает ошибок, кроме этого:

int arr[];
int main(void) {
    *arr = 4;
    return 0;
}

Результаты в:

unit.c:5:5: warning: array 'arr' assumed to have one element
 int arr[];

Почему пропуск класса хранилища приводит к другому поведению и почему первый фрагмент кода вызывает ошибку компоновщика? Благодарю.

Ответ 1

Пустые массивы static int arr[]; и массивы нулевой длины static int arr[0]; были gcc нестандартные расширения.

Цель этих расширений заключалась в том, чтобы действовать как исправление для старого "структурного взлома". Вернувшись в дни C90, люди написали код, например:

typedef struct
{
  header stuff;
  ...
  int data[1]; // the "struct hack"
} protocol;

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

gcc исправил эту проблему, добавив пустые/нулевые массивы в качестве расширения компилятора, заставляя код вести себя без ошибок, хотя он больше не был переносимым.

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

Как признано в связанной документации gcc:

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

И это то, что делает ваш код.

Обратите внимание, что gcc без параметров компилятора передается по умолчанию -std=gnu90 (gcc <5.0) или -std=gnu11 (gcc> 5.0). Это дает вам все нестандартные расширения, поэтому программа компилируется, но не связывается.

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

gcc -std=c11 -pedantic-errors

Флаг -pedantic отключает расширения gcc, и ошибка компоновщика переключается на ошибку компилятора, как ожидалось. Для пустого массива, как в вашем случае, вы получаете:

ошибка: размер массива отсутствует в 'arr'

А для массива нулевой длины вы получаете:

ошибка: ISO C запрещает массив нулевого размера 'arr' [-Wpedantic]


Причина, по которой работает int arr[], заключается в том, что это объявление массива предварительного определения с внешней связью (см. C17 6.9.2). Он действителен C и может рассматриваться как декларация. Это означает, что в другом месте кода компилятор (или, скорее, компоновщик) должен ожидать найти, например, int arr[10], который затем ссылается на одну и ту же переменную. Таким образом, arr может использоваться в коде до того, как размер будет известен. (Я бы не рекомендовал использовать эту функцию языка, так как это форма программирования спагетти.)

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

Ответ 2

Возможно, одна из причин такого поведения заключается в том, что компилятор выдает предупреждение, приводящее к не доступной static переменной, и оптимизирует его - компоновщик будет жаловаться!

Если он не является статичным, его нельзя просто игнорировать, поскольку другие модули могут ссылаться на него - поэтому компоновщик может по крайней мере найти этот символ arr.