Семантика char a [] - PullRequest
       22

Семантика char a []

14 голосов
/ 30 января 2009

Я недавно смутился, объясняя коллеге, почему

char a[100];
scanf("%s", &a); // notice a & in front of 'a'

очень плохо, и что немного лучший способ сделать это:

char a[100];
scanf("%s", a); // notice no & in front of 'a'  

Хорошо. Для всех, кто готов сказать мне, почему scanf не должен использоваться в любом случае по соображениям безопасности: упростите. Этот вопрос на самом деле о значении «& a» против «a».

Дело в том, что после того, как я объяснил, почему это не должно работать, мы попробовали это (с gcc), и это работает =)). Я быстро пробежал

printf("%p %p", a, &a);

и он печатает один и тот же адрес дважды.

Кто-нибудь может мне объяснить, что происходит?

Ответы [ 6 ]

18 голосов
/ 30 января 2009

Ну, случай &a должен быть очевиден. Вы берете адрес массива, точно так, как ожидалось. a немного более тонкий, но ответ таков: a - это массив. И, как известно любому программисту на Си, массивы имеют тенденцию вырождаться в указатель при малейшей провокации, , например , при передаче его в качестве параметра функции.

То есть scanf("%s", a) ожидает указатель, а не массив, поэтому массив вырождается в указатель на первый элемент массива.

Конечно, scanf("%s", &a) тоже работает, потому что это явно адрес массива.

Редактировать: Упс, похоже, я полностью не смог понять, какие типы аргументов ожидает scanf. В обоих случаях указатель на один и тот же адрес, но разных типов. (указатель на символ, в отличие от указателя на массив символов).

И я с радостью признаю, что недостаточно знаю о семантике для многоточия (...), которого я всегда избегал, как чумы, так что похоже, что преобразование в тот тип Scanf, который заканчивается использованием, может быть неопределенное поведение. Прочитайте комментарии и ответ Литба. Обычно вы можете доверять ему, чтобы все было правильно. ;)

11 голосов
/ 30 января 2009

Что ж, scanf ожидает указатель char * в качестве следующего аргумента при просмотре "% s". Но то, что вы даете, это указатель на символ [100]. Вы даете ему char(*)[100]. Это совсем не гарантировано, потому что компилятор может использовать другое представление для указателей массива. Если вы включите предупреждения для gcc, вы также увидите соответствующее предупреждение.

Когда вы предоставляете объект аргумента, который является аргументом, не имеющим перечисленного параметра в функции (так, как в случае с scanf, когда аргументы в стиле vararg "..." после строки формата), массив будет выродиться в указатель на свой первый элемент. То есть компилятор создаст char* и передаст его в printf.

Итак, никогда не делайте это с &a и передавайте его в scanf, используя "% s". Хорошие компиляторы, как и все, будут предупреждать вас правильно:

предупреждение: аргумент несовместим с соответствующим преобразованием строки формата

Конечно, &a и (char*)a имеют один и тот же сохраненный адрес. Но это не значит, что вы можете использовать &a и (char*)a взаимозаменяемо.


Некоторые стандартные кавычки специально показывают, как аргументы указателя не автоматически преобразуются в void*, и как все это неопределенное поведение.

За исключением случаев, когда он является операндом оператора sizeof или унарного оператора &, или является строковый литерал, используемый для инициализации массива; выражение с типом '' массив типа '' преобразуется в выражение с типом 'указатель на тип' ', указывающий на начальный элемент объекта массива. (6.3.2.1/3)

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

Многоточие в объявлении прототипа функции останавливает преобразование типа аргумента после последнего объявленного параметра. Повышение аргументов по умолчанию выполняется на конечных аргументах. (6.5.2.2/7)

О том, как va_arg ведет себя, извлекая аргументы, передаваемые в printf, которая является функцией vararg, выделение мной добавлено (7.15.1.1/2):

Каждый вызов макроса va_arg изменяет ap так, что Значения последовательных аргументов возвращаются по очереди. Тип параметра должен быть типом имя указывается так, что тип указателя на объект, имеющий указанный тип, может быть получен простым постфиксом * для ввода. Если фактический следующий аргумент отсутствует или тип не совместим с типом фактического следующего аргумента (как продвигается в соответствии с продвижением аргумента по умолчанию), поведение не определено , за исключением следующих случаев:

  • один тип является целым числом со знаком, другой тип является соответствующим целым числом без знака тип, а значение представимо в обоих типах;
  • один тип является указателем на void , а другой является указателем на тип символа .

Ну, вот что такое продвижение аргумента по умолчанию :

Если выражение, обозначающее вызываемую функцию, имеет тип, который не включает прототип, целочисленные продвижения выполняются для каждого аргумента, и аргументы, которые У типа float повышаются до двойного. Они называются аргументом по умолчанию промо акции. (6.5.2.2/6)

6 голосов
/ 30 января 2009

Я давно программировал на C, но вот мой 2c:

char a[100] не выделяет отдельную переменную для адреса массива, поэтому выделение памяти выглядит следующим образом:

 ---+-----+---
 ...|0..99|...
 ---+-----+---
    ^
    a == &a

Для сравнения, если массив был malloc'd, то для указателя есть отдельная переменная, и a != &a.

char *a;
a = malloc(100);

В этом случае память выглядит так:

 ---+---+---+-----+---
 ...| a |...|0..99|...
 ---+---+---+-----+---
    ^       ^
    &a  !=  a

K & R 2nd Ed. стр.99 описывает это довольно хорошо:

Соответствие между индексированием и арифметика указателя очень близка. По определению, значение переменной или выражение типа массив является адрес нулевого элемента массива. Таким образом, после назначения pa=&a[0]; pa и a имеют одинаковые значения. поскольку имя массива является синонимом расположение исходного элемента, присвоение pa=&a[0] также может быть записывается как pa=a;

5 голосов
/ 30 января 2009
Массив

AC может быть неявно преобразован в указатель на его первый элемент (C99: TC3 6.3.2.1 §3), т.е. во многих случаях a (тип char [100]) будет вести себя одинаково путь как &a[0] (который имеет тип char *). Это объясняет, почему передача a в качестве аргумента будет работать.

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

&a на самом деле является одной из этих ловушек: это создаст указатель на массив, т.е. он имеет тип char (*) [100]не char **). Это означает, что &a и &a[0] будут указывать на одну и ту же область памяти, но будут иметь разные типы.

Насколько я знаю, не существует неявного преобразования между этими типами, и они также не гарантируют совместимое представление. Все, что я мог найти, это C99: TC3 6.2.5 §27, который мало говорит о указателях на массивы:

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

Но есть также 6.3.2.3 §7:

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

Таким образом, приведение (char *)&a должно работать как положено. На самом деле, я предполагаю, что младший адресуемый байт массива будет самым младшим адресуемым байтом его первого элемента - не уверен, гарантировано ли это, или если компилятор может добавить произвольный отступ перед массивом, но если это так, это было бы серьезно странно ...

В любом случае, чтобы это работало, &a все еще необходимо привести к char * (или void * - стандарт гарантирует, что эти типы имеют совместимые представления). Проблема в том, что не будет никаких преобразований, применяемых к переменным аргументам, кроме продвижения аргументов по умолчанию, то есть вы должны выполнять приведение в явном виде самостоятельно.


Подведем итог:

&a имеет тип char (*) [100], который может иметь битовое представление, отличное от char *. Следовательно, программист должен выполнить явное приведение, потому что для переменных аргументов компилятор не может знать, во что он должен преобразовать значение. Это означает, что будет выполняться только продвижение аргумента по умолчанию, которое, как указывалось litb , не включает преобразование в void *. Отсюда следует:

  • scanf("%s", a); - хорошо
  • scanf("%s", &a); - плохо
  • scanf("%s", (char *)&a); - должно быть в порядке
4 голосов
/ 30 января 2009

Извините, немного не по теме:

Это напомнило мне статью, которую я прочитал около 8 лет назад, когда я занимался программированием на С полный рабочий день. Я не могу найти статью, но я думаю, что она называлась «массивы не указатели» или что-то в этом роде. Во всяком случае, я сталкивался с этим C массивами и указателями FAQ , который интересно читать.

0 голосов
/ 30 января 2009

char [100] - сложный тип из 100 смежных char, чей sizeof равен 100.

Будучи приведенным к указателю ((void*) a), эта переменная возвращает адрес первой char.

Ссылка на переменную этого типа (&a) дает адрес всей переменной, который, в свою очередь, также является адресом первой char

...