Допустимо ли разыменование нулевого указателя в операции sizeof

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

typedef struct {
  double length;
  unsigned char nPlaced;
  unsigned char path[0];
}


RouteDefinition* Alloc_RouteDefinition()
{
  // NB: The +nBags*sizeof.. trick "expands" the path[0] array in RouteDefinition
  // to the path[nBags] array
  RouteDefinition *def = NULL;
  return (RouteDefinition*) malloc(sizeof(RouteDefinition) + nBags * sizeof(def->path[0]));
}

Почему это работает? Я полагаю, что sizeof для char* будет соответствовать размеру указателя в данной архитектуре, но не должен ли он падать и гореть при разыменовании NULL-указателя?


person Veeg    schedule 05.11.2013    source источник
comment
comment
Иоахим прав (+1). Хотя sizeof, скорее всего, является внутренним для компилятора, вы часто можете наблюдать такое поведение языка в интересной и осязаемой форме, взглянув на реализацию offsetof вашей стандартной библиотеки: вероятно, она принимает адрес члена данных фиктивного объекта, созданного приведением Указатель 0/NULL... это даже ближе к пропасти, чем sizeof, но вполне законно.   -  person Tony Delroy    schedule 05.11.2013
comment
sizeof(def->path[0]) это 1 по определению, поэтому оператор return сворачивается до гораздо более читаемого: return malloc(sizeof(RouteDefinition) + nBags);   -  person Klas Lindbäck    schedule 05.11.2013


Ответы (3)


Почему это работает?

Это работает, потому что sizeof является конструкцией времени компиляции, за исключением переменной длины. массивы вообще не оцениваются. Если мы посмотрим на раздел проект стандарта C99 6.5.3.4 Оператор sizeof в абзаце 2 говорит (выделено мной):

[...] Размер определяется типом операнда. Результат — целое число. Если тип операнда является типом массива переменной длины, вычисляется операнд; в противном случае операнд не вычисляется и результатом является целочисленная константа.

мы также видим следующий пример в параграфе 5, который подтверждает это:

double *dp = alloc(sizeof *dp);
       ^^^                ^
                          |                                 
                          This is not the use of uninitialized pointer 

Во время компиляции определяется тип выражения для вычисления результата. Мы можем дополнительно продемонстрировать это на следующем примере:

int x = 0 ;
printf("%zu\n", sizeof( x++ ));

который не будет увеличивать x, что довольно удобно.

Обновить

Как я заметил в мой ответ на Почему sizeof(x++) не увеличивает x? существует исключение для sizeof, являющееся операцией времени компиляции, и это когда ее операнд представляет собой массив переменной длины (VLA). Хотя я ранее не указывал на это, цитата из 6.5.3.4 выше говорит об этом.

Хотя в C11, в отличие от C99, не указано, оценивается ли sizeof в этом случае или нет.

Кроме того, обратите внимание, что существует версия этого вопроса на С++: Не вычисление выражения, к которому применяется sizeof, делает допустимым разыменование нулевого или недопустимый указатель внутри sizeof в C++?.

person Shafik Yaghmour    schedule 05.11.2013

Оператор sizeof является чистой операцией времени компиляции. Во время выполнения ничего не делается, поэтому все работает нормально.

Кстати, член path на самом деле не является указателем, поэтому технически он не может быть NULL.

person Some programmer dude    schedule 05.11.2013
comment
Исключением для sizeof времени компиляции являются VLA. - person Shafik Yaghmour; 06.11.2013

Заявление о том, что sizeof является чисто конструкцией времени компиляции (как это делают существующие ответы), не совсем точно. Начиная с C99, sizeof не является чисто конструкцией времени компиляции. Операнд sizeof оценивается во время выполнения типа операнда VLA. Ответы, опубликованные до сих пор, похоже, игнорируют эту возможность.

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

unsigned n = 10;
int (*a)[n] = NULL; // `a` is a pointer to a VLA 

unsigned i = 0;
sizeof a[i++];      // applying `sizeof` to a VLA

Согласно стандарту C99, аргумент sizeof должен оцениваться (т.е. i должен увеличиваться, см. https://ideone.com/9Fv6xC). Однако я не совсем уверен, что разыменование нулевой точки в a[0] должно привести к неопределенному поведению.

person AnT    schedule 10.06.2015
comment
Существуют ли случаи, когда требование какого-либо наблюдаемого поведения из аргумента sizeof улучшает выразительность языка? Единственная ситуация, в которой значения, вычисленные в sizeof, могут повлиять на его результат, — это когда он применяется к типу VLA, созданному внутри выражения sizeof, и я не могу придумать причины, по которой это позволяет повысить выразительность языка. Обратите внимание, что даже операторы typedef могут генерировать исполняемый код! - person supercat; 11.06.2015
comment
@суперкот even typedef statements can generate executable code!. Не могли бы вы привести пример? Спасибо! - person a3f; 20.02.2016