C покажчики: вказівка ​​на масив фіксованого розміру


119

Це питання стосується гуру С:

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

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

Це здається дуже корисним, але я жодного разу не бачив вказівника, заявленого таким чином, в жодному коді, який я коли-небудь стикався.

Моє запитання: чи є якась причина, чому люди не оголошують вказівники так? Хіба я не бачу явної явної пастки?


3
Примітка: Оскільки масив C99 не повинен мати фіксований розмір, як пропонується в заголовку, 10його можна замінити будь-якою змінною за обсягом
MM

Відповіді:


173

Те, що ви говорите у своєму дописі, абсолютно коректно. Я б сказав, що кожен розробник 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типу, який є масивом або a struct. Ви можете Vector3dбудь-коли переключити визначення масиву з масиву на a structі назад, і вам не доведеться змінювати декларацію функції. У будь-якому випадку функції отримають сукупний об’єкт "за посиланням" (є винятки з цього, але в контексті цього обговорення це правда).

Однак ви не побачите цей метод передачі масиву, який використовується явно занадто часто, просто через те, що занадто багато людей плутають доволі зведений синтаксис і просто недостатньо комфортні з такими особливостями мови С, щоб правильно їх використовувати. З цієї причини в середньому реальному житті передача масиву як вказівника на його перший елемент є більш популярним підходом. Це просто виглядає "простіше".

Але насправді використання вказівника на перший елемент для проходження масиву - це дуже ніша техніка, трюк, який служить дуже конкретній цілі: його єдиною метою є полегшення передачі масивів різної величини (тобто розміру часу виконання) . Якщо вам дійсно потрібно мати можливість обробляти масиви розміру часу виконання, то правильний спосіб передавати такий масив - це вказівник на його перший елемент із розміром конкретного поданого додатковим параметром

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
Відповідь дає дуже стислий та інформативний опис того, наскільки розмір () досягає успіху, як він часто виходить з ладу та способи його завжди виходить з ладу. ваші спостереження більшості інженерів C / C ++, які не розуміють, і тому робити щось, що, на їхню думку, розуміють, це одна з найбільш пророчих речей, яку я бачив за деякий час, і завіса - це ніщо в порівнянні з точністю, яку вона описує. серйозно, сер. чудова відповідь.
WhozCraig

Я просто відремонтував деякий код на основі цієї відповіді, браво і спасибі і за Q, і за А.
Перрі,

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 ++). Досить часто трапляється ситуація, коли ви хочете дати абоненту const вказівник на буфер, який ви записали. На жаль, це неможливо при оголошенні такого вказівника у 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. Інша справа в тому, що більшість людей навіть не знає, що можна вказати такий покажчик як АндрейТ зазначив.


2
Це, мабуть, є специфічним для компілятора. Мені вдалося скопіювати за допомогою gcc 4.9.1, але clang 3.4.2 не міг перейти від non-const до const версії. Я читав специфікацію C11 (п. 9 у моїй версії ... частина, яка говорить про сумісність двох кваліфікованих типів), і погоджуюся, що, здається, ці конверсії є незаконними. Однак ми на практиці знаємо, що ви завжди можете автоматично перетворювати з char * в char const * без попередження. IMO, 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;

    Оператор адреси адреси отримує адресу всього масиву &a, з правильним типом, для якого він може призначити його p. Без оператора aавтоматично перетворюється на адресу першого елемента масиву, такий же, як у &a[0], який має інший тип.

    Оскільки це автоматичне перетворення вже відбувається, я завжди здивований, що &це необхідно. Це відповідає використанню &змінних інших типів, але я маю пам’ятати, що масив є спеціальним і мені потрібен, &щоб отримати правильний тип адреси, навіть якщо значення адреси однакове.

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

  • Коли typedefце НЕ використовується для створення типу для масиву покажчиків на- (в загальному заголовки), то глобальний масив покажчик на потреби більш складне externзаяву , щоб розділити його по файлах:

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

1

Ну просто кажучи, C не робить так. Масив типу Tпередається навколо як вказівник на першийT в масиві, і це все, що ви отримуєте.

Це дозволяє створити круті та елегантні алгоритми, такі як циклічний перегляд масиву з виразами типу

*dst++ = *src++

Мінусом є те, що управління розміром залежить від вас. На жаль, невиконання цього сумлінно призвело також до мільйонів помилок у кодуванні C та / або можливостей зловмисної експлуатації.

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

Ваша структура може містити будь-які потрібні вам дані; він може містити ваш масив чітко визначеного розміру.

І все-таки ніщо не заважає вам або некомпетентному чи зловмисному кодеру використовувати касти, щоб обдурити компілятор у трактуванні вашої структури як до іншого розміру. Майже неперевершена здатність робити подібні речі є частиною дизайну С.


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*

2
Не забувайте, що масив фіксованого розміру також може бути динамічно виділений як char (*p)[10] = malloc(sizeof *p).
ANT

Дивіться тут для більш детальної дискусії між відмінностями масиву char [] та char * ptr тут. stackoverflow.com/questions/1807530 / ...
t0mm13b

1

Я б не рекомендував це рішення

typedef int Vector3d[3];

оскільки це затьмарює той факт, що Vector3D має тип, про який потрібно знати. Зазвичай програмісти не очікують, що змінні одного типу матимуть різні розміри. Поміркуйте:

void foo(Vector3d a) {
   Vector3D b;
}

де розмір a! = розмір b


Він не пропонував це як рішення. Він просто використовував це як приклад.
figurassa

Гм. Чому це sizeof(a)не те саме, що sizeof(b)?
sherrellbc

0

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

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


4
Масиви НЕ є постійними покажчиками. Прочитайте, будь ласка, деякі питання щодо масивів.
ANT

2
Що тут важливо (одномірні масиви як параметри), факт полягає в тому, що вони розпадаються на постійні покажчики. Прочитайте поширені запитання про те, як бути менш педантичним.
fortran

-2

У моєму компіляторі (vs2008) він трактує char (*p)[10]як масив символьних вказівників, як ніби немає дужок, навіть якщо я компілюю як файл C. Чи підтримується компілятор для цієї "змінної"? Якщо так, то це головна причина не використовувати його.


1
-1 Неправильно. Відмінно працює на vs2008, vs2010, gcc. Зокрема , цей приклад працює відмінно: stackoverflow.com/a/19208364/2333290
kotlomoy
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.