Указатели C: указывающие на массив фиксированного размера

120

Этот вопрос уходит к гуру Си:

В C можно объявить указатель следующим образом:

char (* p)[10];

.. что в основном означает, что этот указатель указывает на массив из 10 символов. При объявлении подобного указателя замечательно то, что вы получите ошибку времени компиляции, если попытаетесь назначить указатель на массив другого размера для p. Это также даст вам ошибку времени компиляции, если вы попытаетесь присвоить значение простого указателя char для p. Я пробовал это с помощью gcc, и, похоже, он работает с ANSI, C89 и C99.

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

void foo(char * p, int plen);

Если бы вы ожидали буфер определенного размера, вы бы просто проверили значение plen. Однако вы не можете гарантировать, что человек, который передаст вам p, действительно предоставит вам много действительных ячеек памяти в этом буфере. Вы должны верить, что человек, вызвавший эту функцию, поступает правильно. С другой стороны:

void foo(char (*p)[10]);

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

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

Мой вопрос: есть ли причина, по которой люди не объявляют такие указатели? Я не вижу очевидной ловушки?

figurassa
источник
3
примечание: начиная с C99, массив не обязательно должен иметь фиксированный размер, как указано в заголовке, 10может быть заменен любой переменной в области видимости
MM

Ответы:

174

То, что вы говорите в своем посте, абсолютно правильно. Я бы сказал, что каждый разработчик C приходит к одному и тому же открытию и к одному и тому же выводу, когда (если) они достигают определенного уровня владения языком C.

Когда специфика области вашего приложения вызывает массив определенного фиксированного размера (размер массива является константой времени компиляции), единственный правильный способ передать такой массив в функцию - использовать параметр указателя на массив

void foo(char (*p)[10]);

(на языке C ++ это тоже делается со ссылками

void foo(char (&p)[10]);

).

Это включит проверку типов на уровне языка, что обеспечит предоставление массива точно правильного размера в качестве аргумента. Фактически, во многих случаях люди используют эту технику неявно, даже не осознавая этого, скрывая тип массива за именем typedef.

typedef int Vector3d[3];

void transform(Vector3d *vector);
/* equivalent to `void transform(int (*vector)[3])` */
...
Vector3d vec;
...
transform(&vec);

Кроме того, обратите внимание, что приведенный выше код инвариантен по отношению к Vector3dтипу, являющемуся массивом или struct. Вы можете Vector3dв любой момент переключить определение с массива на a structи обратно, и вам не придется менять объявление функции. В любом случае функции получат агрегированный объект «по ссылке» (есть исключения, но в контексте этого обсуждения это правда).

Однако вы не увидите, что этот метод передачи массива используется явно слишком часто, просто потому, что слишком много людей запутываются из-за довольно запутанного синтаксиса и просто недостаточно знакомы с такими функциями языка C, чтобы использовать их должным образом. По этой причине в обычной реальной жизни передача массива в качестве указателя на его первый элемент является более популярным подходом. Это просто выглядит «проще».

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

void foo(char p[], unsigned plen);

На самом деле, во многих случаях очень полезно иметь возможность обрабатывать массивы размера во время выполнения, что также способствует популярности метода. Многие разработчики C просто никогда не сталкиваются (или никогда не осознают) необходимости обрабатывать массив фиксированного размера, поэтому не обращают внимания на правильную технику фиксированного размера.

Тем не менее, если размер массива фиксированный, передача его как указателя на элемент

void foo(char p[])

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

Другая причина, которая может помешать внедрению техники передачи массивов фиксированного размера, - это преобладание наивного подхода к типизации динамически выделяемых массивов. Например, если программа вызывает фиксированные массивы типа char[10](как в вашем примере), средний разработчик будет mallocтакие массивы, как

char *p = malloc(10 * sizeof *p);

Этот массив нельзя передать функции, объявленной как

void foo(char (*p)[10]);

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

char (*p)[10] = malloc(sizeof *p);

Это, конечно, легко передается заявленному выше foo

foo(p);

и компилятор выполнит правильную проверку типа. Но опять же, это слишком сбивает с толку неподготовленного разработчика C, поэтому вы не будете часто видеть это в "типичном" среднем повседневном коде.

Муравей
источник
2
Ответ предоставляет очень краткое и информативное описание того, как sizeof () работает успешно, как часто и как всегда терпит неудачу. Ваши наблюдения за тем, что большинство инженеров C / C ++ не понимают, и поэтому делать то, что, по их мнению, они понимают, - это одна из самых пророческих вещей, которые я видел некоторое время, и завеса ничто по сравнению с точностью, которую она описывает. серьезно, сэр. отличный ответ.
WhozCraig
Я только что отредактировал код на основе этого ответа, браво и спасибо как за Q, так и за A.
Перри
1
Мне любопытно узнать, как вы обрабатываете constсобственность с помощью этой техники. const char (*p)[N]Аргумент не кажется совместимым с указателем char table[N];В противоположность этому , простой char*PTR остаются совместимыми с const char*аргументом.
Cyan
4
Было бы полезно отметить, что для доступа к элементу вашего массива вам нужно делать, (*p)[i]а не делать *p[i]. Последний будет прыгать на размер массива, что почти наверняка не то, что вам нужно. По крайней мере, для меня изучение этого синтаксиса вызвало, а не предотвратило ошибку; Я бы быстрее получил правильный код, просто передав ему float *.
Эндрю Вагнер
1
Да, @mickey, вы предложили constуказатель на массив изменяемых элементов. И да, это полностью отличается от указателя на массив неизменяемых элементов.
Cyan
11

Я хотел бы добавить к ответу AndreyT (если кто-то наткнется на эту страницу в поисках дополнительной информации по этой теме):

По мере того как я начинаю больше играть с этими объявлениями, я понимаю, что с ними связаны серьезные препятствия в C (очевидно, не в C ++). Довольно часто возникает ситуация, когда вы хотите дать вызывающей стороне константный указатель на буфер, в который вы записали. К сожалению, это невозможно при объявлении такого указателя в C. Другими словами, стандарт C (6.7.3 - Параграф 8) расходится с чем-то вроде этого:


   int array[9];

   const int (* p2)[9] = &array;  /* Not legal unless array is const as well */

Это ограничение, похоже, отсутствует в C ++, что делает такие объявления гораздо более полезными. Но в случае C необходимо вернуться к объявлению обычного указателя всякий раз, когда вам нужен указатель const на буфер фиксированного размера (если сам буфер не был объявлен как const с самого начала). Вы можете найти больше информации в этой цепочке писем: текст ссылки

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

figurassa
источник
2
Похоже, это проблема компилятора. Я смог продублировать с помощью gcc 4.9.1, но clang 3.4.2 смог без проблем перейти с неконстантной на константную версию. Я прочитал спецификацию C11 (стр. 9 в моей версии ... часть, в которой говорится о совместимости двух квалифицированных типов) и согласен с тем, что, похоже, эти преобразования являются незаконными. Однако на практике мы знаем, что вы всегда можете автоматически преобразовать char * в char const * без предупреждения. ИМО, clang более согласован в разрешении этого, чем gcc, хотя я согласен с вами, что спецификация, похоже, запрещает любое из этих автоматических преобразований.
Дуг Ричардсон
4

Очевидная причина в том, что этот код не компилируется:

extern void foo(char (*p)[10]);
void bar() {
  char p[10];
  foo(p);
}

По умолчанию для массива используется неквалифицированный указатель.

Также см. Этот вопрос , использование foo(&p)должно работать.

Кейт Рэндалл
источник
3
Конечно, foo (p) не будет работать, foo запрашивает указатель на массив из 10 элементов, поэтому вам нужно передать адрес вашего массива ...
Брайан Р. Бонди
9
Как это "очевидная причина"? Очевидно, что правильным способом вызова функции является foo(&p).
AnT
3
Я полагаю, «очевидный» - неправильное слово. Я имел в виду «самый простой». Различие между p и & p в этом случае довольно неясно для среднего программиста на C. Кто-то, пытающийся сделать то, что предлагал плакат, напишет то, что написал я, получит ошибку времени компиляции и сдастся.
Кейт Рэндалл
2

Я также хочу использовать этот синтаксис, чтобы включить дополнительную проверку типов.

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

Вот еще несколько препятствий, с которыми я столкнулся.

  • Для доступа к массиву необходимо использовать (*p)[]:

    void foo(char (*p)[10])
    {
        char c = (*p)[3];
        (*p)[0] = 1;
    }

    Заманчиво использовать вместо этого локальный указатель на символ:

    void foo(char (*p)[10])
    {
        char *cp = (char *)p;
        char c = cp[3];
        cp[0] = 1;
    }

    Но это частично лишило бы цели использования правильного типа.

  • Следует не забывать использовать оператор адресации при присвоении адреса массива указателю на массив:

    char a[10];
    char (*p)[10] = &a;

    Оператор address-of получает адрес всего массива &aс правильным типом для его присвоения p. Без оператора aавтоматически преобразуется в адрес первого элемента массива, такого же, как в &a[0], но имеющего другой тип.

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

    Одна из причин моей проблемы может заключаться в том, что я изучил K&R C еще в 80-х годах, который еще не позволял использовать &оператор для целых массивов (хотя некоторые компиляторы игнорировали это или допускали синтаксис). Что, кстати, может быть еще одной причиной того, почему указатели на массивы трудно внедрить: они работают должным образом только после ANSI C, и &ограничение оператора могло быть еще одной причиной, по которой они считались слишком неудобными.

  • Когда typedefэто не используется для создания типа для массива указателей на-(в общем заголовочном файле), то глобальный массив указатель на потребности более сложное externзаявление , чтобы разделить его по файлам:

    fileA:
    char (*p)[10];
    
    fileB:
    extern char (*p)[10];
Orafu
источник
1

Проще говоря, C так не поступает. Массив типа Tпередается как указатель на первый Tв массиве, и это все, что вы получаете.

Это позволяет использовать некоторые классные и элегантные алгоритмы, такие как цикл по массиву с такими выражениями, как

*dst++ = *src++

Обратной стороной является то, что управление размером зависит от вас. К сожалению, неспособность сделать это сознательно также привело к миллионам ошибок в коде C и / или возможностям злонамеренного использования.

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

Ваша структура может содержать любые данные, которые вы хотите; он может содержать ваш массив четко определенного размера.

Тем не менее, ничто не мешает вам или некомпетентному или злонамеренному кодировщику использовать приведение типов, чтобы обмануть компилятор и заставить его рассматривать вашу структуру как структуру другого размера. Практически неограниченная способность делать такие вещи - часть дизайна C.

Карл Смотрич
источник
1

Вы можете объявить массив символов несколькими способами:

char p[10];
char* p = (char*)malloc(10 * sizeof(char));

Прототип функции, которая принимает массив по значению:

void foo(char* p); //cannot modify p

или по ссылке:

void foo(char** p); //can modify p, derefernce by *p[0] = 'f';

или по синтаксису массива:

void foo(char p[]); //same as char*
S1N
источник
2
Не забывайте, что массив фиксированного размера также может быть динамически выделен как char (*p)[10] = malloc(sizeof *p).
AnT
См. Здесь более подробное обсуждение различий char array [] и char * ptr здесь. stackoverflow.com/questions/1807530/…
t0mm13b
1

Я бы не рекомендовал это решение

typedef int Vector3d[3];

поскольку он скрывает тот факт, что Vector3D имеет тип, о котором вы должны знать. Программисты обычно не ожидают, что переменные одного и того же типа будут иметь разные размеры. Рассматривать :

void foo(Vector3d a) {
   Vector3D b;
}

где sizeof a! = sizeof b

Per Knytt
источник
Он не предлагал это как решение. Он просто использовал это как пример.
figurassa
Гектометр Почему sizeof(a)не то же самое sizeof(b)?
sherrellbc
0

Может быть, я что-то упускаю, но ... поскольку массивы являются постоянными указателями, в основном это означает, что нет смысла передавать на них указатели.

Не могли бы вы просто использовать void foo(char p[10], int plen);?

Фортран
источник
4
Массивы НЕ являются постоянными указателями. Прочтите, пожалуйста, FAQ по массивам.
AnT
2
Что здесь важно (одномерные массивы как параметры), дело в том, что они распадаются на постоянные указатели. Прочтите, пожалуйста, FAQ о том, как быть менее педантичным.
fortran
-2

В моем компиляторе (vs2008) он рассматривается char (*p)[10]как массив символьных указателей, как если бы не было скобок, даже если я компилирую как файл C. Поддерживает ли компилятор эту «переменную»? Если это так, то это основная причина не использовать его.

Тайсон Джейкобс
источник
1
-1 Неправильно. Он отлично работает на vs2008, vs2010, gcc. В частности, этот пример отлично работает: stackoverflow.com/a/19208364/2333290
kotlomoy