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



этот вопрос выходит к гуру C там:



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



char (* p)[10];


.. который гласит, что этот указатель указывает на массив из 10 символов. Аккуратная вещь об объявлении такого указателя заключается в том, что вы получите ошибку времени компиляции, если вы попытаетесь назначить указатель массива разного размера p. это также даст вам ошибку времени компиляции, если вы попытаетесь присвоить значение простого указателя char С. Я попытался это с помощью GCC, и это, кажется, работает с ANSI, С89 и С99.



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



void foo(char * p, int plen);


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



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


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



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



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

634   9  

9 ответов:

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

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

когда я начинаю играть больше с этими объявлениями, я понимаю, что есть главный недостаток, связанный с ними в C (по-видимому, не в C++). Довольно часто возникает ситуация, когда вы хотите дать вызывающему объекту указатель const на буфер, в который вы записали. К сожалению, это невозможно при объявлении указателя, как это на других слова, стандарт 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.

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

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

по умолчанию продвижение массива осуществляется к неквалифицированному указателю.

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

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

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

*dst++ = *src++

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

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

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

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

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

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*

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

typedef int Vector3d[3];

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

void foo(Vector3d a) {
   Vector3D b;
}

где sizeof a != sizeof b

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

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

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

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

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

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

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

Comments

    Ничего не найдено.