Страницы

Поиск по вопросам

пятница, 15 февраля 2019 г.

Язык C, локальные автоматические массивы

Никогда не думал, что для локального автоматического массива справедливо следующее:
void func(void) { char arr[10]; if (arr == &arr) { // Всегда истина, по крайней мере, при использовании компилятора MinGW. } }
Подскажите, Стандарт что-нибудь говорит об этом, или это не более, чем совпадение?
Самое интересное во всей этой ситуации, что следующий код корректно работает:
char arr[10]; scanf("%10s", &arr);
И я уже было предположил, что компилятор тихо опускает операцию взятия адреса (&), и &arr преобразуется в arr или, если точнее, в &arr[0].
Если бы не одно но:
char arr[10]; char *p = &arr;// Warning, несоответствие типов char **pp = &arr;// Warning, несоответствие типов
Так что происходит?


Ответ

Операция взятия адреса &, примененная к массиву, дает вам адрес всего массива. Нет ничего удивительно в том, что адрес всего массива &arr численно совпадает с адресом нулевого элемента массива &arr[0] (выражение arr в этом контексте эквивалентно &arr[0]). Точно так же адрес первого элемента структуры совпадает с адресом всей структуры. В этом тоже нет ничего удивительного.
Вот в таком примере вы увидите одинаковые адреса в каждом printf
#include
struct S { int a, b, c; };
int main() { int a[10]; printf("%p %p
", (void *) &a, (void *) a);
struct S s; printf("%p %p
", (void *) &s, (void *) &s.a); }
Причины одинаковости адресов в каждом printf - совершенно идентичные. Почему вас это удивило - не ясно.

Однако:
Первый вопрос бессмыслен ибо код некорректен с точки зрения языка С.
Ограничения на операнды оператора ==
Constraints 2 One of the following shall hold: — both operands have arithmetic type; — both operands are pointers to qualified or unqualified versions of compatible types; — one operand is a pointer to an object or incomplete type and the other is a pointer to a qualified or unqualified version of void; or — one operand is a pointer and the other is a null pointer constant.
Ваши операнды имеют типы char * и char (*)[10] и не удовлетворяют вышеприведенным требованиям. Язык С не допускает прямого сравнения указателей типа char * и типа char (*)[10]
Вы могли бы сравнить if ((void *) arr == (void *) &arr) и действительно получить равенство. Но в этом не ничего удивительного, как сказано выше.

Код
char arr[10]; scanf("%10s", &arr);
не "корректно работает", а порождает неопределенное поведение. Спецификатор формата %s требует аргумента типа char *, а вы передаете char (*)[10]. Поведение не определено.
Практическую "работоспособность" этот код имеет из-за названной выше причины: адрес первого элемента массива совпадает с адресом всего массива.

Также по теме скопирую свой ответ отсюда
char mass[N]; scanf("%s",mass); // так правильно scanf("%s",&mass); // почему-то тоже срабатывает
Такой вопрос обычно возникает у людей, которые считают, что массив физически представлен неким указателем на некий блок памяти. Соответственно ожидается , что &mas даст нам указатель на этот указатель и не будет работать в scanf. Поэтому работоспособность такого scanf зачастую вызывает удивление.
На самом деле массивы в языке С не являются и никогда не являлись указателями. Это очень популярная легенда, которая никак не хочет умирать, несмотря на то, что материалов, объясняющих семантику С массивов написано очень много на всех языках.
Массив - это просто непрерывный блок памяти, содержащий элементы массива. Т.е. например int a[20] - это блок памяти из 20 объектов типа int. И все. Имя a, как объект - это объект типа int [20], который соответствует именно и только этому блок памяти. Никакого указателя там нигде нет.
Иллюзия того, что массив "является указателем" возникает из-за того, что когда вы используете имя массива в выражениях, то почти всегда к массиву применяется неявное преобразование типа, которое автоматически "на лету" преобразует объект типа "массив" к временному значению "указатель на элемент". Этот указатель является rvalue и нигде не хранится в памяти. Он существует только концептуально - как результат вышеупомянутого неявного преобразования. Это чисто воображаемый указатель.
По-английски явление "превращения" массива в указатель известно под устойчивым названием "array type decay". В стандарте языка С++ (где ситуация с массивами точно такая же) это неявное преобразование официально называется "array-to-pointer conversion".
Но это неявное преобразование применяется не всегда. В языке С существует три контекста-исключения, в которых это неявное преобразование не делается:
Оператор sizeof. Как известно sizeof(array) возвращает размер всего массива, а не размер указателя на элемент. Унарный оператор &. Возвращает указатель типа "указатель-на-весь-массив", а не "указатель-на-указатель" Инициализация массива строковым литералом - char a[] = "Hello";. Литерал "Hello" является массивом , но не деградирует до указателя в данном контексте.
Таким образом, применение оператора & к объекту типа "массив" дает вам указатель "на весь массив" (с точки зрения типа указателя). Т.е. для int a[20] выражение &a имеет тип int (*)[20]. В памяти адрес "всего массива" - это адрес того самого непрерывного блока, о котором я говорил выше. Т.е. численно этот адрес совпадает с адресом нулевого элемента массива. Именно поэтому выражения &a, &a[0] и просто a численно дают один и тот же адрес.
Именно поэтому, к примеру, в функцию memcpy для двух массивов (т.е. именно объектов типа "массив", а не "указатель") можно вызывать и как memcpy(dst, src, sizeof src), и как memcpy(&dst, &src, sizeof src) и как memcpy(&dst[0], &src[0], sizeof src). Результат будет один и тот же во всех случаях.
Именно поэтому правильно работает scanf при использовании mass или &mass, ибо оба выражения дают одно и то же физическое численное значение. (Второй вариант формально не верен, т.к. формат %s требует указателя именно типа char *, а не char (*)[N], но на практике это не важно.)
Кстати, Денис Ритчи в своей статье об истории происхождения С (http://cm.bell-labs.com/who/dmr/chist.html) пишет, что изначально он собирался реализовать массивы в С точно так же, как они были реализованы в языках B и BCPL. В B и BCPL массивы были реализованы именно как самостоятельные указатели, указывающие на отдельные посторонние блоки памяти. Но Ритчи также хотел ввести в язык такой новый тип, как структуры. И тут сразу возникла проблема: если массив будет реализован через указатель, то структуру, содержащую в себе массивы, невозможно будет тривиально копировать. Т.е. обычный memcpy будет тупо копировать указатели из одной структуры в другую. А это, разумеется, неприемлемо. По этой причине он отказался от идеи реализовывать массивы через физические указатели. Массивы в С стали просто блоками памяти. Никаких указателей. Но для сохранения похожей на B и BCPL семантики в язык С было введено неявное преобразование из типа "массив" в тип "указатель". Большинство операций с массивами работает именно через это преобразование, т.е. через временный указатель. Но физически этого указателя не существует.

Комментариев нет:

Отправить комментарий