C указатели: указатель на массив фиксированного размера - PullRequest
104 голосов
/ 27 ноября 2009

Этот вопрос задается гуру C:

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

char (* p)[10];

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

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

void foo(char * p, int plen);

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

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

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

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

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

Ответы [ 9 ]

155 голосов
/ 27 ноября 2009

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

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

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

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

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

).

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

typedef int Vector3d[3];

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

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

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

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

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

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

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

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

9 голосов
/ 02 декабря 2009

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

По мере того, как я начинаю больше играть с этими объявлениями, я понимаю, что с ними связан серьезный недостаток (очевидно, не в 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. Другая причина в том, что большинство людей даже не знают, что вы можете объявить указатель как это указал Андрей Т.

4 голосов
/ 27 ноября 2009

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

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

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

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

1 голос
/ 21 сентября 2016

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

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

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

  • Для доступа к массиву необходимо использовать (*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;
    }
    

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

  • Следует помнить об использовании оператора address-of при назначении адреса массива указателю на массив:

    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];
    
1 голос
/ 28 ноября 2009

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

typedef int Vector3d[3];

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

void foo(Vector3d a) {
   Vector3D b;
}

где размер a! = Размер b

1 голос
/ 27 ноября 2009

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

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*
1 голос
/ 27 ноября 2009

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

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

*dst++ = *src++

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

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

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

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

0 голосов
/ 14 июня 2012

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

0 голосов
/ 27 ноября 2009

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...