Страницы

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

четверг, 9 января 2020 г.

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

#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, несоответствие типов


Так что происходит?
    


Ответы

Ответ 1



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

Ответ 2



Стандарт C99 6.3.2.1/3 Except when it is the operand of the sizeof 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. Вольный перевод: Если выражение не является аргументом оператора sizeof или унарного оператора &, то выражение типа «массив элементов типа» преобразуется к типу «указатель на тип», который указывает на начальный элемент данного массива, и не является lvalue. Т.е. согластно стандарту, левая часть (arr == &arr) раскрывается в указатель на первый аргумент arr (тип char *), а вторая по обычным правилам оператора & в указатель на массив (тип char (*)[10]). Учитывая то что представляют из себя Си'шные массивы, они всегда будут указывать на один участок памяти, и в любой реализации должны быть равны. Но эти выражения не являются синонимами в следствии того, что у них различаются типы. А компиляторы Си в отличие от плюсов допускает такие вольности как сравнения разнотипных указателей, хотя любой компилятор должен был бы чихнуть на манер: warning: comparison of distinct pointer types lacks a cast

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

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