Как оценивается оператор sizeof

Мой проект требует полного понимания того, как работает оператор sizeof. Спецификация стандарта C в этом отношении расплывчата, и полагаться на ее интерпретацию будет опасно. Меня особенно интересует, когда и как следует обрабатывать sizeof.

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

    Однако:

    int size = 0;
    scanf("%i", &size);
    printf("%i\n", sizeof(int[size]));
    

    Это, например, не может быть оценено во время компиляции каким-либо образом.

    char c = '\0';
    char*p = &c;
    printf("%i\n", sizeof(*p));
    

    Я не помню точный код, который создает U/B, но здесь *p — это фактическое выражение (унарное разыменование RTL). Предположительно, означает ли это, что sizeof(c+c) — это способ принудительной оценки времени компиляции с помощью выражения или он будет оптимизирован компилятором?

  2. Возвращает ли sizeof значение типа int, это size_t (ULL на моей платформе) или оно определяется реализацией?

  3. В этой статье говорится, что «Операнд sizeof не может быть преобразованием типа», что неверно. Приведение типов имеет тот же приоритет, что и оператор sizeof, то есть в ситуации, когда используются оба, они просто оцениваются справа налево. sizeof(int) * p, вероятно, не работает, потому что если операнд является типом в фигурных скобках, он обрабатывается первым, но sizeof((int)*p) работает нормально.

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

Ваш второй фрагмент можно легко оценить во время компиляции. Тип *p фиксируется как char, который всегда будет иметь размер 1. Кстати: результат оператора sizeof имеет тип size_t, который печатается с использованием спецификатора формата %zu, а не %i.

Gerhardh 02.02.2023 13:44

@Gerhardh Это правда (относительно вашего первого замечания). Я думаю, это был неправильный пример.

Edenia 02.02.2023 13:45

Что касается вашего пункта (3), статья верна: в вашем примере sizeof((int)*p) операнд sizeof не является приведением типа; это ((int)*p), то есть тип, заключенный в круглые скобки. Это разница, которая очень важна для того, как sizeof интерпретирует свои аргументы.

Konrad Rudolph 02.02.2023 13:48
sizeof всегда оценивается во время компиляции, за исключением массивов переменной длины, даже для c + c, где результатом является int: sizeof(c + c) == sizeof(int) в то время как sizeof(3.14 * 2) == sizeof(double)
David Ranieri 02.02.2023 13:54

Что бы это ни стоило, этот вопрос зависит от, IMO, гигантской разницы между VLA и любым другим типом в C. Именно поэтому они не были в языке сначала; Я считаю, что это также объясняет, почему они необязательны и до сих пор не так популярны среди всех программистов на C.

Steve Summit 02.02.2023 14:05

@KonradRudolph Они должны дать понять, что такое поведение связано только с синтаксисом, специфичным для sizeof, который приведет к синтаксису с другим значением. В общем, поскольку sizeof NULL возвращает 8, я предполагаю, что это происходит, когда вы используете дополнительные () в определениях макросов. #define NULL ((void* )0x0)

Edenia 02.02.2023 14:08

@DavidRanieri Это правда, если один из операндов является double (также может быть переменной), то возвращаемое значение также будет double

Edenia 02.02.2023 14:10

@Edenia Примечание, которое вы цитируете, делает (попытка) прояснить это. Однако его не следует понимать изолированно. Вам также необходимо прочитать и понять en.cppreference.com/w/c/language/sizeof. В частности, знаете ли вы, что существует две формы sizeof и что sizeof Xsizeof (X) (по крайней мере, в некоторых случаях)?

Konrad Rudolph 02.02.2023 14:41

@KonradRudolph Я просто предполагаю, что sizeof сначала ожидает (type-name), а если его нет, он ожидает выражение или один объект, как и везде.

Edenia 02.02.2023 14:51

@Edenia, обратите внимание, что sizeof NULL может быть действительным и оцениваться для вас как 8, а макрос NULL может расширяться до ((void *)0) для вас, но язык C не гарантирует ничего из этого. Это не гарантирует даже, что sizeof(NULL) оценивается как размер указателя любого типа. Похоже, вы, возможно, читали системные заголовки, но если вы хотите писать переносимое программное обеспечение, не делайте этого. Вместо этого полагайтесь на документацию, особенно на спецификацию языка.

John Bollinger 02.02.2023 16:43
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
8
10
255
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Семантика sizeof()согласно (черновому) стандарту C11:

Оператор sizeof возвращает размер (в байтах) своего операнда, который может быть выражением или именем типа в скобках. Размер определяется типом операнда. Результат — целое число. Если тип операнда является типом массива переменной длины, вычисляется операнд; в противном случае операнд не оценивается и результатом является целочисленная константа.

Примечание: «Если тип операнда является типом массива переменной длины, операнд оценивается». Это означает, что размер VLA вычисляется во время выполнения.

«в противном случае операнд не оценивается, а результат является целочисленной константой» означает, что результат оценивается во время компиляции.

Тип возвращаемого значения — size_t. Точка:

Значение результата обоих операторов (sizeof() и _Alignof()) определяется реализацией, а его тип (целочисленный тип без знака) — size_t, определенный в <stddef.h> (и других заголовках).

Обратите внимание, что тип size_t. Не используйте ни unsigned long, ни unsigned long long, ни что-либо еще. Всегда используйте size_t.

Ответ принят как подходящий

1. Мои предыдущие знания предполагали, что это оператор времени компиляции, который я никогда не подвергал сомнению, потому что я никогда не злоупотреблял sizeof слишком сильно…

C 2018 6.5.3.4 2 определяет поведение sizeof и говорит:

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

В вашем примере с sizeof(int[size]) тип int[size] является типом массива переменной длины, поэтому операнд оценивается1, эффективно вычисляя размер во время выполнения программы.

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

2. Возвращает ли sizeof значение типа int, это size_t (ULL на моей платформе) или оно определяется реализацией.

C 2018 6.5.3.4 5 говорит: «Значение результата обоих операторов [sizeof и _Alignof] определяется реализацией, а его тип (целочисленный тип без знака) — size_t, определенный в <stddef.h> (и других заголовках) ».

3. В этой статье говорится, что «Операнд для sizeof не может быть преобразованием типа», что неверно. Приведение типов имеет тот же приоритет, что и оператор sizeof, то есть в ситуации, когда используются оба, они просто оцениваются справа налево. sizeof(int) * p, вероятно, не работает, потому что если операнд является типом в фигурных скобках, он обрабатывается первым, но sizeof((int)*p) работает нормально.

Статья означает, что операнд не может быть непосредственно приведенным выражением (C 2018 6.5.4) в форме ( type-name ) cast-expression из-за того, как устроена формальная грамматика C. Формально операнд выражения для sizeof является унарным-выражением (6.5.3) в грамматике, а унарное-выражение может через цепочку грамматических производств быть приведенным-выражением внутри круглых скобок.

Сноска

1 Мы часто думаем об имени типа (спецификации типа, например int [size]) как о пассивном объявлении, а не об исполняемой инструкции или выражении, но C 2018 6.8 4 говорит нам: «Существует также неявное полное выражение, в котором вычисляются выражения непостоянного размера для изменяемого типа...»

Кстати, можно ли сказать, что значение, возвращаемое операцией, имеет тип большего из двух операндов, вместо того, чтобы везде отображать таблицы истинности?

Edenia 02.02.2023 14:47

Нет, @Edenia, это небезопасно воспринимать как правило. Например, это не относится к арифметическим операциям, когда оба операнда имеют целые типы с рангом преобразования меньше, чем у int (в основном, типы меньше, чем int). Это не всегда верно для операций сдвига, это никогда не верно для допустимых операций разности указателей, и неясно, как это вообще применимо к сложению указателя и оператору индексации ([]).

John Bollinger 02.02.2023 16:29

Я только что проверил, char + short возвращает int в основном. Вы правы, он не может дать значение меньше 4 байтов. Просто забавно, я могу проверить это с помощью sizeof.

Edenia 02.02.2023 16:39

Вы немного переоцениваете вещи.

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

printf("%i\n", sizeof(*p));

Я не помню точный код, который создает U/B, но здесь *p — это фактическое выражение (унарное разыменование RTL).

Не имеет значения — выражение *p не вычисляется как часть операции sizeof. Имеет значение только тип *p, который известен при переводе. Это совершенно верная идиома для динамического выделения памяти:

size_t size = some_value();
int *p = malloc( sizeof *p * size );

Предположительно, означает ли это, что sizeof(c+c) — это способ принудительной оценки времени компиляции с помощью выражения или он будет оптимизирован компилятором?

Опять же, выражение c+c не будет оцениваться — важен только тип.

Возвращает ли sizeof значение типа int, это size_t (ULL на моей платформе) или оно определяется реализацией.

size_t. Это прямо указано в определении языка:

6.5.3.4 Операторы sizeof и _Alignof
...
5 Значение результата обоих операторов определяется реализацией, а его тип ( целочисленный тип без знака) — это size_t, определенный в <stddef.h> (и других заголовках).
C 2011 Online Draft

В этой статье говорится, что «Операнд для sizeof не может быть преобразованием типа», что неверно. Приведение типов имеет тот же приоритет, что и оператор sizeof, то есть в ситуации, когда используются оба, они просто оцениваются справа налево. sizeof(int) * p, вероятно, не работает, потому что если операнд является типом в фигурных скобках, он обрабатывается первым, но sizeof((int)*p) работает нормально.

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

unary-expression:
    ...
    sizeofunary-expressionsizeof ( type-name)

и синтаксис для приведенного выражения

cast-expression:
    unary-expression(type-name)cast-expression

Если написать выражение типа

sizeof (int) *p;

это не будет проанализировано как

sizeof ((int) *p);

Вместо этого он будет проанализирован как

(sizeof (int)) *p;

и интерпретируется как мультипликативное выражение:

multiplicative-expression*cast-expression

IOW, компилятор подумает, что вы пытаетесь умножить результат sizeof (int) на значение p (что должно привести к диагностике). Если вы заключаете выражение приведения в круглые скобки, то оно анализируется правильно.

Приведение типов имеет тот же приоритет, что и оператор sizeof.

Это неправильно. Унарные выражения (включая выражения sizeof) имеют более высокий приоритет, чем выражения приведения. Вот почему sizeof (int) *p анализируется как (sizeof (int)) *p.

Согласно статье они находятся в одной приоритетной группе. Приведение типов также является унарным. Что мне не хватает?

Edenia 02.02.2023 17:38

@Edenia: выражения приведения не являются унарными выражениями - они описаны в другом подразделе (6.5.4) от унарных выражений (6.5.3) и имеют отдельное правило производства в грамматике. Подпункты 6.5 перечислены в порядке убывания приоритета (т. е. первичные выражения в 6.5.1 имеют более высокий приоритет, чем постфиксные операторы в 6.5.2, которые имеют более высокий приоритет, чем унарные операторы в 6.5.3 и т. д.).

John Bode 02.02.2023 18:38

Вот попытка предоставить полное руководство по оператору sizeof и его многочисленным особенностям. Предупреждение: этот пост может содержать сильную «языковую юриспруденцию».


Формальный синтаксис и допустимые формы

sizeof является ключевым словом в C, а синтаксис определен в C17 6.5.3 как:

sizeof унарное выражение
sizeof( название типа )

Это означает, что есть два возможных способа его использования: sizeof op или sizeof(op). В первом случае операнд должен быть выражением (например, sizeof my_variable), а во втором — типом (например, sizeof(int)).

Когда мы используем sizeof, мы почти всегда используем скобки. Всегда использовать скобки считается хорошей практикой (и, как известно, у Линуса Торвальдса однажды была одна из его обычных детских истерик по этому поводу). Но какую форму sizeof мы используем, зависит от того, передаем ли мы выражение или тип. Таким образом, даже когда мы используем круглые скобки вокруг выражения, мы на самом деле используем не вторую версию, а первую. Пример:

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

В этом случае мы передаем выражение в sizeof. Выражение — это (x), а скобка — это обычная скобка («первичное выражение»), которую мы можем использовать вокруг любого выражения в C — в данном случае она не принадлежит оператору sizeof.


«Операнд sizeof не может быть типизирован» - приоритет и ассоциативность или ...?

Следуя приведенному выше объяснению, всякий раз, когда мы пишем sizeof (int) * p, это интерпретируется как вторая форма с именем типа. Почему?

Почему вообще не очень очевидно, это на самом деле чертовски тонко. Легко быть обманутым «таблицами приоритета операторов», такими как та, которую вы связываете. В нем говорится, что оператор приведения типа sizeof является унарным оператором с ассоциативностью справа налево. Но на самом деле это не так, если копаться в грязных деталях грамматики C.

На самом деле в стандарте C нет такой вещи, как таблица приоритетов, и он не определяет ассоциативность явно. Вместо этого приоритет операций определяется (настолько сложно, насколько это возможно) длинной цепочкой определений синтаксиса в главе 6.5. В каждой подглаве группа операторов ссылается на предыдущую, а иногда и на следующую группу операторов в формальном синтаксисе, тем самым заявляя, что текущая группа имеет более низкий приоритет, чем предыдущая. Для унарных операторов 6.5.3 это выглядит так:

унарное выражение:

постфиксное выражение
++ унарное выражение
-- унарное выражение
унарное операторное выражение приведения
sizeof унарное выражение
sizeof( название типа )
_Alignof( название типа )

унарный оператор: один из
& * + - ˜ !

В переводе со стандартного на английский эта грамматическая каша читается примерно так:

«Вот группа унарных выражений. Это префиксные операторы ++ и --, или один из унарных операторов (перечисленных отдельно), или sizeof в двух разных формах, или _Alignof. Они могут следовать за постфиксным выражением, что означает, что любой постфиксное выражение (или группы операторов еще выше по синтаксической цепочке) имеет более высокий приоритет, чем унарные операторы. За ними может следовать выражение приведения, которое, таким образом, имеет более низкий приоритет, чем унарные операторы».

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

Так или иначе, оператор sizeof(имя типа) является унарным выражением и имеет приоритет в грамматике над оператором приведения. И именно поэтому компилятор будет рассматривать это не как два оператора sizeof и (cast), а как оператор sizeof(type), за которым следует оператор двоичного умножения.

Итак, sizeof (int) * p превращается в эквивалент (sizeof(int)) * p, sizeof с двоичным умножением, что, вероятно, является ерундой, и, возможно, фактическое намерение здесь состояло в том, чтобы разыменовать указатель p, привести и затем получить размер.

Однако мы могли бы написать что-то вроде sizeof ((int)*p)), и тогда порядок синтаксического анализа будет таким: круглые скобки, затем (из-за ассоциативности унарного оператора справа налево) разыменование, затем приведение, затем sizeof.


Какой тип возвращает sizeof?

Он возвращает особый большой целочисленный тип без знака size_t (C17 6.5.3.4/5), который обычно считается «достаточно большим», чтобы содержать самый большой объект, разрешенный в системе. Этот тип обычно используется всякий раз, когда мы хотим получить размер чего-либо, например, при переборе массива.

Например, вы можете увидеть некоторый код на SO в форме for(size_t i=0; i<n; i++) при переборе массива, поскольку это наиболее правильный тип «достаточно большой», чтобы содержать размер массива. (int может быть слишком маленьким, и, кроме того, он тоже подписан, и у нас не может быть отрицательных размеров.)

size_t находится в stddef.h, который, в свою очередь, включен во многие другие стандартные заголовки, такие как stdio.h. Он может содержать значения до SIZE_MAX, определенные в stdint.h.

size_t печатается с printf с помощью спецификатора преобразования %zu, отсюда и мой предыдущий пример printf("%zu\n", sizeof(x));.


Время компиляции или время выполнения?

sizeof обычно является оператором времени компиляции, что означает, что операнд не оценивается. За одним исключением, это массивы переменной длины (VLA), размер которых просто неизвестен во время компиляции.

С17 6.5.3.4/2:

Оператор sizeof возвращает размер (в байтах) своего операнда, который может быть выражением или имя типа в скобках. Размер определяется типом операнда. Результат является целым числом. Если тип операнда является типом массива переменной длины, вычисляется операнд; в противном случае операнд не оценивается и результатом является целочисленная константа.

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

#include <stdio.h>

int main (void)
{
  int size;
  scanf("%d",&size); // enter 2
  int arr[5][size];

  printf("%zu ", sizeof(size++)); // size++ not executed
  printf("%d ", size); // print 2

  printf("%zu ", sizeof(arr[size++])); // size++ is executed
  printf("%d ", size);
}

Когда я пробую это и ввожу 2, он печатает 4 2 8 3:

  • 4, потому что это размер int в этой системе.
  • 2, потому что операнд size++ не был выполнен/оценен.
  • 8, потому что 2 * sizeof(int) равно 8.
  • 3, потому что операнд arr[size++] был выполнен/оценен, так как arr[n] приводит к операнду VLA.

Это поведение того, какой операнд оценивается или нет, четко определено и гарантировано.

Отсюда популярный трюк int* ptr = malloc(n * sizeof *ptr);. В случае, если *ptr будет оцениваться, это неинициализированный указатель, который мы определенно не можем разыменовать, и это было бы неопределенным поведением. Но поскольку он гарантированно не будет оценен, трюк безопасен.


Исключение из "распада массива"

sizeof — один из немногих операндов, который является исключением из правила «затухания массива»:

С17 6.3.2.1/3

За исключением случаев, когда это операнд оператора sizeof, унарного оператора & или строкового литерала, используемого для инициализации массива, выражение типа «массив типа» преобразуется в выражение типа «указатель на тип». который указывает на начальный элемент объекта массива и не является lvalue.


sizeof используется в определении байта в C

Размер байта в C определяется согласно C17 3.6.

3.6
байт
адресная единица хранения данных, достаточно большая, чтобы вместить любой элемент базового набора символов среды выполнения

а затем 6.5.3.4/4:

Когда sizeof применяется к операнду, имеющему тип char, unsigned char или signed char (или его уточненную версию), результатом будет 1.

По этой причине не имеет особого смысла писать такие вещи, как malloc(n * sizeof(char), потому что sizeof(char) по определению гарантировано всегда равно 1.

(Однако количество битов в char не обязательно равно 8.)

очень хорошо сделано для тщательного раскрытия этого иначе не сразу понятного предмета. На самом деле я не знал, что выражения, переданные в sizeof, позже не оцениваются (не включая VLA). Я не могу сказать, что понимаю, почему они не могут быть, но я могу сказать, что это по крайней мере один пример, который доказывает, что sizeof НЕ следует рассматривать как функцию (это распространенное заблуждение среди новичков, и я не согласен с Linux в этой части). С другой стороны, какое правило определяет тип, возвращаемый выражением, переданным в качестве операнда в sizeof?

Edenia 03.02.2023 02:27

Кроме того, для sizeof(*p) ему все еще нужно оценить выражение deref, чтобы отобразить размер объекта, на который указывает p, поэтому я подозреваю, что мое понимание «оценки» также запутано.

Edenia 03.02.2023 02:55

«В первом случае операнд должен быть выражением (например, sizeof my_variable)». Я думаю, также стоит добавить, что он ожидает UNARY выражение (либо один операнд, либо унарное подвыражение), если фигурные скобки не присутствуют. Потому что sizeof 1+1 это 5, а не 4

Edenia 03.02.2023 03:18

@Edenia Любое подвыражение, которое является операндом для sizeof (или любое другое целочисленное константное выражение, оцениваемое во время компиляции), обрабатывается так же, как любое другое выражение C с точки зрения повышения типа или разрешенных типов для определенных операторов и т. д. Что касается «оценки " обычно это означает "будет выполнен" - sizeof не нужно "оценивать" операнд, например, при определении того, имеет ли он какие-либо побочные эффекты и т. д., поскольку он не будет выполнен (за исключением случая VLA). Компилятор знает размеры всех типов и объектов во время компиляции, поэтому, если он знает, какой тип имеет что-то, он может определить размер.

Lundin 03.02.2023 08:23

@Edenia sizeof 1+1 — это простая ситуация приоритета оператора, похожая на -1+1 или *ptr + 1. Единственное, что делает sizeof странным оператором, — это две допустимые формы, а также нестандартные концепции таблиц приоритетов и ассоциативности, где он не совсем подходит, по отношению к оператору приведения.

Lundin 03.02.2023 08:26

И, кстати, формальное определение оценки соответствует 5.1.2.3: «Вычисление выражения в целом включает в себя как вычисление значения, так и инициирование побочных эффектов. Вычисление значения для выражения lvalue включает определение идентификатора назначенного объекта». Концепция вычисления значения C11, возможно, здесь бесполезна (также), но я думаю, вы могли бы сказать, что sizeof выполняет вычисление значения, но не вызывает побочных эффектов.

Lundin 03.02.2023 08:30

Другие вопросы по теме