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

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

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

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

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

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

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

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

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

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

Ответы

Ответ 1

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

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

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

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

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

Ответ 2

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

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

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

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

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


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

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

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

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

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

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

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

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

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

Ответ 3

Прошло некоторое время с тех пор, как я запрограммировал в 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. p.99 описывает это довольно хорошо:

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

Ответ 4

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:

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

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

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


Подводя итог:

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

  • scanf("%s", a); - good
  • scanf("%s", &a); - bad
  • scanf("%s", (char *)&a); - должно быть нормально

Ответ 5

Извините, крошечный отрывок от темы:

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

Ответ 6

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

При нажатии на указатель ((void*) a) эта переменная дает адрес первого char.

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