C: указатель на массив фиксированного размера

Этот вопрос выходит на C-гуру:

В 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]);

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

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

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

Ответ 1

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

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


   int array[9];

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

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

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

Ответ 2

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

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

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

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

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 из массива в 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, поэтому вы не увидите его слишком часто в "типичном" среднесуточном коде.

Ответ 3

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

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

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

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

Ответ 4

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

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

*dst++ = *src++

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

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

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

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

Ответ 5

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

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*

Ответ 6

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

typedef int Vector3d[3];

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

void foo(Vector3d a) {
   Vector3D b;
}

где sizeof a!= sizeof b

Ответ 7

Я также хочу использовать этот синтаксис для большей проверки типов.

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

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

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

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

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

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

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

  • Нельзя забывать использовать адрес-оператор при назначении адреса массива указателю на массив:

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

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

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

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

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

    fileA:
    char (*p)[10];
    
    fileB:
    extern char (*p)[10];
    

Ответ 8

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

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

Ответ 9

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