приведение указателя к массиву в указатель

Рассмотрим следующий код C:

int arr[2] = {0, 0};
int *ptr = (int*)&arr;
ptr[0] = 5;
printf("%d\n", arr[0]);

Теперь ясно, что код печатает 5 на обычных компиляторах. Однако может ли кто-нибудь найти соответствующие разделы в стандарте C, в которых указано, что код действительно работает? Или поведение кода неопределенное?

По сути, я спрашиваю, почему &arr при преобразовании в void * совпадает с arr при преобразовании в void *? Потому что я считаю, что код эквивалентен:

int arr[2] = {0, 0};
int *ptr = (int*)(void*)&arr;
ptr[0] = 5;
printf("%d\n", arr[0]);

Я придумал этот пример, размышляя над вопросом здесь: Указатель на массив перекрывающийся конец массива ... но это явно отдельный вопрос.


person juhist    schedule 24.03.2015    source источник
comment
Позвольте мне посмотреть это для вас. Общая идея заключается в том, что указатель на составной тип (объединение/структура/массив) может быть приведен к указателю на его первый элемент.   -  person fuz    schedule 25.03.2015
comment
Отличный вопрос. Я задавался вопросом о том же самом в более чем одном случае в прошлом. Можно подумать, что &arr должен давать указатель **.   -  person clearlight    schedule 25.03.2015
comment
Ну, &arr дает тип данных int(*)[2], и вам действительно нужно приведение, иначе компилятор предупредит.   -  person juhist    schedule 25.03.2015
comment
@ 1sand0s: Нет, &arr не имеет смысла давать int**, поскольку нет объекта int*, на который он мог бы указывать.   -  person Keith Thompson    schedule 25.03.2015
comment
@juhist: На самом деле int *ptr = &arr; является нарушением ограничения. Соответствующий компилятор должен выдать диагностику. Эта диагностика может быть либо предупреждением, либо фатальной ошибкой. (ИМХО, это должно быть фатальной ошибкой по умолчанию, но, например, авторы gcc, похоже, не согласны.)   -  person Keith Thompson    schedule 25.03.2015
comment
@KeithThompson И все же это адрес адреса :-) Иначе что это? Это не похоже на то, что у компилятора нет места для хранения адреса адреса, хотя он непрозрачен и реализуем. Просто мысли вслух. Может быть не на базе.   -  person clearlight    schedule 25.03.2015
comment
Я не вижу в стандарте гарантии, что (int*)&arr == &arr[0]. В частности, ссылаясь на проект N1570, семантика преобразования указателя в 6.3.2.3p7 не распространяется на этот случай.   -  person Keith Thompson    schedule 25.03.2015
comment
@ 1sand0s: Нет, &arr абсолютно не является адресом адреса. Это адрес объекта массива. (Операнд унарного & — это один из трех контекстов, в которых выражения массива не преобразуются неявным образом в указатель.)   -  person Keith Thompson    schedule 25.03.2015
comment
@ 1sand0s, у массивов такое странное поведение. Использование имени массива в большинстве контекстов приводит к тому, что оно превращается в указатель на первый элемент массива. Один контекст, в котором этого не происходит, — это когда имя массива является аргументом унарного оператора &.   -  person Carl Norum    schedule 25.03.2015
comment
@1sand0s: Потому что arr это не адрес. Массивы не являются указателями. Указатели не являются массивами. arr — это имя объекта массива. Когда имя arr появляется как выражение, оно (в большинстве, но не во всех контекстах) неявно преобразуется в указатель на первый элемент объекта массива. Рекомендуемая литература: раздел 6 часто задаваемых вопросов о comp.lang.c.   -  person Keith Thompson    schedule 25.03.2015
comment
@Keith Thompson: Не могли бы вы взглянуть на stackoverflow.com/questions/29243116/ тогда? Правильно ли я понимаю, что вы не нашли в стандарте гарантии того, что (int*)&arr == &arr[0] будет означать, что этот вопрос будет неопределенным поведением? См. мой ответ на этот вопрос, который пытается объяснить, почему код работает.   -  person juhist    schedule 25.03.2015
comment
@KeithThompson, перечитывая ваш ответ (часть в скобках), теперь я понимаю, что вы говорите, и предполагаю, что вы гораздо лучше знакомы со спецификацией, чем я. Я не мог отредактировать свой ответ до того, как вы ответили, но понял, что вы сказали постфактум.   -  person clearlight    schedule 25.03.2015
comment
@juhist: То, что я не нашел гарантии, может очень легко означать, что я что-то упускаю. Я ожидаю, что код будет работать так, как ожидалось, для любой реальной реализации. Я просто не смог доказать, что стандарт требует его работы.   -  person Keith Thompson    schedule 25.03.2015
comment
Думаю, я собираюсь сделать запрос на интерпретацию по этому поводу.   -  person fuz    schedule 25.03.2015


Ответы (3)


О союзах и структурах см. ISO 9899:2011§6.7.2.1/16f:

16 Размер союза достаточен для того, чтобы в него входил самый крупный из его членов. Значение не более чем одного из членов может быть сохранено в объекте объединения в любое время. Указатель на объект объединения, соответствующим образом преобразованный, указывает на каждый из его членов (или, если член является битовым полем, то на единицу, в которой он находится), и наоборот.

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

Для типов массивов ситуация немного сложнее. Во-первых, обратите внимание на то, что такое массив, из ISO 9899:2011§6.2.5/20:

Тип массива описывает непрерывно размещенный непустой набор объектов с определенным типом объекта-члена, называемым типом элемента. Тип элемента должен быть полным всякий раз, когда указывается тип массива. Типы массивов характеризуются типом их элементов и количеством элементов в массиве. Говорят, что тип массива является производным от его типа элемента, и если его тип элемента — T, тип массива иногда называют «массивом из T». Построение типа массива из типа элемента называется «порождением типа массива».

Формулировка «непрерывно выделенный» подразумевает, что между членами массива нет заполнения. Это понятие подтверждается сноской 109:

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

Использование оператора sizeof в §6.5.3.5, пример 2 выражает намерение, что также нет заполнения до или после массивов:

ПРИМЕР 2

Другое использование оператора sizeof — вычисление количества элементов в массиве:

sizeof array / sizeof array[0]

Поэтому я делаю вывод, что указатель на массив, преобразованный в указатель на опечатку элемента этого массива, указывает на первый элемент в массиве. Кроме того, обратите внимание на то, что определение равенства говорит об указателях (§6.5.9/6f.):

6 Два указателя сравниваются равными тогда и только тогда, когда оба являются нулевыми указателями, оба являются указателями на один и тот же объект (включая указатель на объект и подобъект в его начале) или функцию, оба являются указателями на один после последнего элемента одного и того же объект массива, или один является указателем на один после конца одного объекта массива, а другой является указателем на начало другого объекта массива, который сразу же следует за первым объектом массива в адресном пространстве.109)

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

Поскольку первый элемент массива является «подобъектом в его начале», указатель на первый элемент массива и указатель на массив равны.

person fuz    schedule 24.03.2015
comment
Что ж, надеюсь, вы что-нибудь найдете; @Кейт Томпсон ничего не смог найти. Кажется, что ваши рассуждения разумны, и теперь у меня нет сомнений, почему обычные компиляторы рассматривают указатель на массив как указатель на его начальный член (как в случае с одним и тем же адресом, а не как с одним и тем же типом). - person juhist; 25.03.2015
comment
опечатка элемента -- chuckle - person Keith Thompson; 25.03.2015
comment
Я согласен с вашими рассуждениями о представлении массивов. Я не согласен с тем, что ваш вывод строго следует из представления. - person Keith Thompson; 25.03.2015
comment
@KeithThompson Это было просто промежуточное сохранение, потому что кто-то кхе-кхе подумал, что было бы неплохо редактировать ответ одновременно. - person fuz; 25.03.2015
comment
Извините за одновременное редактирование. Я думаю, что могу описать заведомо извращенную реализацию, которая соответствует стандарту, но нарушает ваши предположения. Подробности позже. - person Keith Thompson; 25.03.2015
comment
Да, я тоже так думаю. В настоящее время я подаю отчет о дефекте, так что следите за обновлениями. - person fuz; 25.03.2015
comment
Я подумал, что мог бы описать извращенную, но подходящую реализацию, в которой указатель представлен как адрес последнего байта объекта, на который он указывает. Преобразования в и из char* или void* изменят смещение. Но я не мог согласовать указатели на неполные типы. - person Keith Thompson; 25.03.2015

Вот немного рефакторинговая версия вашего кода для удобства:

int arr[2] = { 0, 0 };
int *p1 = &arr[0];
int *p2 = (int *)&arr;

с вопросом: верно ли p1 == p2, или не указано, или UB?


Во-первых: я думаю, что авторы абстрактной модели памяти C предполагают, что p1 == p2 верно; и если Стандарт на самом деле не разъясняет это, то это будет дефектом Стандарта.

Двигаемся дальше; единственный соответствующий фрагмент текста, по-видимому, C11 6.3.2.3/7 (ненужный текст удален):

Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. [...] При обратном преобразовании результат будет равен исходному указателю.

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

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

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

void *v1 = malloc( sizeof(int) );
int  *i1 = (int *)v1;

Если мы не примем «и указатель указывает на один и тот же адрес», тогда i1 может фактически не указывать на пространство malloc, что было бы нелепо.

Мой вывод состоит в том, что мы должны читать 6.3.2.3/7 как говорящий о том, что приведение указателя не изменяет адрес, на который он указывает. Часть об использовании указателей на тип символа, похоже, подтверждает это.

Следовательно, поскольку p1 и p2 имеют одинаковый тип и указывают на один и тот же адрес, они сравниваются равными.

person M.M    schedule 25.03.2015
comment
Обратите внимание, что согласно §6.2 преобразования между совместимыми типами не изменяют значение, хранящееся в типе. - person fuz; 25.03.2015
comment
@FUZxxl Вы имеете в виду 6.3/2 Преобразование значения операнда в совместимый тип не приводит к изменению значения или представления? Термин совместимый тип определен в 6.2.7 и в основном означает, что типы одинаковы. Два указателя на разные типы несовместимы. (На самом деле, поскольку указатели на разные типы могут иметь разный размер и представление, в общем случае приведение указателей может изменить представление). - person M.M; 25.03.2015

Чтобы ответить прямо:

Может ли кто-нибудь найти соответствующие разделы в стандарте C, в которых указано, что код действительно работает?

  • 6.3.2.1 Значения L, массивы и указатели функций, параграф 1
  • 6.3.2.3 Указатели, пункты 1,5 и 6
  • 6.5.3.2 Операторы адреса и косвенности, параграф 3

Или поведение кода не определено?

Код, который вы опубликовали, не является неопределенным, но может зависеть от компилятора/реализации (согласно разделу 6.3.2.3 p5/6)

По сути, я спрашиваю, почему &arr при преобразовании в void * совпадает с arr при преобразовании в void *?

Это означало бы, что вы спрашиваете, почему int *ptr = (int*)(void*)&arr дает те же результаты, что и int *ptr = (int*)(void*)arr;, но согласно вашему опубликованному коду вы на самом деле спрашиваете, почему int *ptr = (int*)(void*)&arr дает то же самое, что и int *ptr = (int*)&arr.

В любом случае я расскажу о том, что на самом деле делает ваш код, чтобы уточнить:

Согласно 6.3.2.1p3:

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

и согласно 6.5.3.2p3:

Унарный оператор & возвращает адрес своего операнда. Если операнд имеет тип «тип», результат имеет тип «указатель на тип».

Итак, в вашем первом объявлении

int arr[2] = {0, 0};

arr инициализируется типом массива, содержащим 2 элемента типа int, оба равны 0. Затем для 6.3.2.1p3 он распадается на тип указателя, указывающий на первый элемент в любом месте, где он вызывается в области видимости (кроме случаев, когда он используется как sizeof(arr), &arr, ++arr или --arr).

Итак, в следующей строке вы можете просто сделать следующее:

int *ptr = arr; or int *ptr = &*arr; or int *ptr = &arr[0];

а ptr теперь является указателем на тип int, который указывает на первый элемент массива arr (т.е. &arr[0]).

Вместо этого вы объявляете это как таковое:

int *ptr = (int*)&arr;

Разобьем это на части:

  1. &arr -> вызывает исключение для 6.3.2.1p3, поэтому вместо получения &arr[0] вы получаете адрес arr, который является типом int(*)[2] (а не типом int*), поэтому вы получаете не pointer to an int, вы получаете pointer to an int array

  2. (int*)&arr, (т. е. преобразование в int*) -> согласно 6.5.3.2p3, &arr берет адрес переменной arr, возвращая указатель на ее тип, поэтому просто произнесение int* ptr = &arr выдаст предупреждение о несовместимых типах указателей< /b> (поскольку ptr имеет тип int*, а &arr имеет тип int(*)[2]), поэтому вам нужно привести к int*.

Далее согласно 6.3.2.3p1: указатель на void может быть преобразован в указатель на любой тип объекта или из него. Указатель на объект любого типа может быть преобразован в указатель на void и обратно; результат будет равен исходному указателю.

Таким образом, ваше объявление int* ptr = (int*)(void*)&arr; даст те же результаты, что и int* ptr = (int*)&arr;, из-за типов, которые вы используете и конвертируете в/из. Также в качестве примечания: ptr[0] = 5; совпадает с *ptr = 5, где ptr[1] = 5; также совпадает с *++ptr = 5;.

Некоторые ссылки:

6.3.2.1 Lvalues, массивы и указатели функций

1. An lvalue is an expression (with an object type other than void) that potentially designates an object (*see note); if an lvalue does not designate an object when it is evaluated, the behavior is undefined. When an object is said to have a particular type, the type is specified by the lvalue used to designate the object. A modifiable lvalue is an lvalue that does not have array type, does not have an incomplete type, does not have a constqualified type, and if it is a structure or union, does not have any member (including, recursively, any member or element of all contained aggregates or unions) with a constqualified type.

*The name ‘‘lvalue’’ comes originally from the assignment expression E1 = E2, in which the left operand E1 is required to be a (modifiable) lvalue. It is perhaps better considered as representing an object ‘‘locator value’’. What is sometimes called ‘‘rvalue’’ is in this International Standard described as the ‘‘value of an expression’’. An obvious example of an lvalue is an identifier of an object. As a further example, if E is a unary expression that is a pointer to an object, *E is an lvalue that designates the object to which E points.

2. Except when it is the operand of the sizeof operator, the _Alignof operator, the unary & operator, the ++ operator, the -- operator, or the left operand of the . operator or an assignment operator, an lvalue that does not have array type is converted to the value stored in the designated object (and is no longer an lvalue); this is called lvalue conversion. If the lvalue has qualified type, the value has the unqualified version of the type of the lvalue; additionally, if the lvalue has atomic type, the value has the non-atomic version of the type of the lvalue; otherwise, the value has the type of the lvalue. If the lvalue has an incomplete type and does not have array type, the behavior is undefined. If the lvalue designates an object of automatic storage duration that could have been declared with the register storage class (never had its address taken), and that object is uninitialized (not declared with an initializer and no assignment to it has been performed prior to use), the behavior is undefined.

3. Except when it is the operand of the sizeof operator, the _Alignof operator, or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

6.3.2.3 Указатели

1. A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.

5. An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation (the mapping functions for converting a pointer to an integer or an integer to a pointer are intended to be consistent with the addressing structure of the execution environment).

6. Any pointer type may be converted to an integer type. Except as previously specified, the result is implementation-defined. If the result cannot be represented in the integer type, the behavior is undefined. The result need not be in the range of values of any integer type.

6.5.3.2 Операторы адреса и косвенного обращения

1. The operand of the unary & operator shall be either a function designator, the result of a [] or unary * operator, or an lvalue that designates an object that is not a bit-field and is not declared with the register storage-class specifier.

3. The unary & operator yields the address of its operand. If the operand has type ‘‘type’’, the result has type ‘‘pointer to type’’. If the operand is the result of a unary * operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted, except that the constraints on the operators still apply and the result is not an lvalue. Similarly, if the operand is the result of a [] operator, neither the & operator nor the unary * that is implied by the [] is evaluated and the result is as if the & operator were removed and the [] operator were changed to a + operator. Otherwise, the result is a pointer to the object or function designated by its operand.

4. The unary * operator denotes indirection. If the operand points to a function, the result is a function designator; if it points to an object, the result is an lvalue designating the object. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’. If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined (*see note).

*Thus, &*E is equivalent to E (even if E is a null pointer), and &(E1[E2]) to ((E1)+(E2)). It is always true that if E is a function designator or an lvalue that is a valid operand of the unary & operator, *&E is a function designator or an lvalue equal to E. If *P is an lvalue and T is the name of an object pointer type, *(T)P is an lvalue that has a type compatible with that to which T points. Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer, an address inappropriately aligned for the type of object pointed to, and the address of an object after the end of its lifetime.

6.5.4 Операторы приведения

5. Preceding an expression by a parenthesized type name converts the value of the expression to the named type. This construction is called a cast (a cast does not yield an lvalue; thus, a cast to a qualified type has the same effect as a cast to the unqualified version of the type). A cast that specifies no conversion has no effect on the type or value of an expression.

6. If the value of the expression is represented with greater range or precision than required by the type named by the cast (6.3.1.8), then the cast specifies a conversion even if the type of the expression is the same as the named type and removes any extra range and precision.

6.5.16.1 Простое назначение

2. In simple assignment (=), the value of the right operand is converted to the type of the assignment expression and replaces the value stored in the object designated by the left operand.

6.7.6.2 Деклараторы массивов

1. In addition to optional type qualifiers and the keyword static, the [ and ] may delimit an expression or *. If they delimit an expression (which specifies the size of an array), the expression shall have an integer type. If the expression is a constant expression, it shall have a value greater than zero. The element type shall not be an incomplete or function type. The optional type qualifiers and the keyword static shall appear only in a declaration of a function parameter with an array type, and then only in the outermost array type derivation.

3. If, in the declaration ‘‘T D1’’, D1 has one of the forms:

D[ type-qualifier-listopt assignment-expressionopt ]
D[ static type-qualifier-listopt assignment-expression ]
D[ type-qualifier-list static assignment-expression ]
D[ type-qualifier-listopt * ]


and the type specified for ident in the declaration ‘‘T D’’ is ‘‘derived-declarator-type-list T’’, then the type specified for ident is ‘‘derived-declarator-type-list array of T’’.142) (See 6.7.6.3 for the meaning of the optional type qualifiers and the keyword static.)

4. If the size is not present, the array type is an incomplete type. If the size is * instead of being an expression, the array type is a variable length array type of unspecified size, which can only be used in declarations or type names with function prototype scope;143) such arrays are nonetheless complete types. If the size is an integer constant expression and the element type has a known constant size, the array type is not a variable length array type; otherwise, the array type is a variable length array type. (Variable length arrays are a conditional feature that implementations need not support; see 6.10.8.3.)

5. If the size is an expression that is not an integer constant expression: if it occurs in a declaration at function prototype scope, it is treated as if it were replaced by *; otherwise, each time it is evaluated it shall have a value greater than zero. The size of each instance of a variable length array type does not change during its lifetime. Where a size expression is part of the operand of a sizeof operator and changing the value of the size expression would not affect the result of the operator, it is unspecified whether or not the size expression is evaluated.

6. For two array types to be compatible, both shall have compatible element types, and if both size specifiers are present, and are integer constant expressions, then both size specifiers shall have the same constant value. If the two array types are used in a context which requires them to be compatible, it is undefined behavior if the two size specifiers evaluate to unequal values.

P.S. В качестве примечания, учитывая следующий код:

#include <stdio.h>

int main(int argc, char** argv)
{
    int arr[2] = {10, 20};
    X
    Y
    printf("%d,%d\n", arr[0],arr[1]);
    return 0;
}

где X был одним из следующих:

int *ptr = (int*)(void*)&arr;
int *ptr = (int*)&arr;
int *ptr = &arr[0];

и Y был одним из следующих:

ptr[0] = 15;
*ptr = 15;

При компиляции в OpenBSD с gcc версии 4.2.1 20070719 и с указанием флага -S вывод ассемблера для всех файлов был абсолютно одинаковым.

person txtechhelp    schedule 25.03.2015
comment
Спасибо за подробный ответ! Тем не менее, вы говорите, что Обратите внимание, что приведение &arr к int* может не дать правильных результатов в зависимости от компилятора/реализации (для расширения см. 6.3.2.3p5/6), но в случае этого кода более чем вероятно будет, так как вы имеете дело с типами int. -- на мой взгляд, 6.3.2.3p5/6 говорит о преобразовании указателей в целые числа и наоборот, тогда как я преобразовываю указатель одного типа в указатель другого типа. - person juhist; 25.03.2015
comment
@juhist, после некоторого перечитывания я согласен, я убрал это примечание, но оставил абзацы для справки - person txtechhelp; 25.03.2015