Що таке масив для розпаду вказівника?


384

Що таке масив для розпаду вказівника? Чи є якесь відношення до покажчиків масиву?


73
маловідоме: Оператор unary plus може використовуватися як "оператор розпаду": Дано int a[10]; int b(void);, тоді +aце int pointer та +bє покажчиком функції. Корисно, якщо ви хочете передати його шаблону, що приймає посилання.
Йоханнес Шауб - ліб

3
@litb - parenns зробив би те саме (наприклад, (a) має бути виразом, що оцінює покажчик), правда ?.
Майкл Берр

21
std::decayвід C ++ 14 був би менш незрозумілим способом розпаду масиву над unry +.
legends2k

21
@ JohannesSchaub-litb, оскільки це питання позначено як C і C ++, я хотів би уточнити, що хоча +aі +bє легальним в C ++, він є незаконним в C (C11 6.5.3.3/1 "Операнд унарного +або -оператора повинен мати арифметичний тип ")
ММ

5
@lege Справа. Але я вважаю, що це не так маловідомо, як трюк з унарним +. Тому я говорив про це не тільки тому , що він розпадається , але тому , що це якийсь - то кумедний матеріал , щоб грати с;)
Johannes Schaub - LITB

Відповіді:


283

Кажуть, що масиви "розпадаються" на покажчики. Масив C ++, оголошений таким, що int numbers [5]не можна повторно вказати, тобто ви не можете сказати numbers = 0x5a5aff23. Більш важливо, що термін розпад означає втрату типу і розміру; numbersзанепадаючи int*, втрачаючи інформацію про розмірність (рахуйте 5), і тип вже не int [5]є. Шукайте тут випадки, коли гниття не відбувається .

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

Три шляхи проходження в масиві 1 :

void by_value(const T* array)   // const T array[] means the same
void by_pointer(const T (*array)[U])
void by_reference(const T (&array)[U])

Останні два даватимуть належну sizeofінформацію, тоді як перша не буде, оскільки аргумент масиву відкладений для присвоєння параметру.

1 Константа U повинна бути відома під час компіляції.


8
Як перший прохідний за значенням?
rlbond

10
by_value передає вказівник на перший елемент масиву; в контексті параметрів функції T a[]ідентичний T *a. by_pointer передає те саме, за винятком того, що значення покажчика зараз кваліфіковане const. Якщо ви хочете передати вказівник на масив (на відміну від вказівника на перший елемент масиву), синтаксис є T (*array)[U].
Джон Боде

4
"з явним вказівником на цей масив" - це неправильно. Якщо aце масив char, то він aмає тип char[N]і розпадається на char*; але &aмає тип char(*)[N]і не загниває.
Павло Мінаєв

5
@FredOverflow: Тож якщо Uзміни вам не потрібно пам’ятати, змінювати їх у двох місцях, або ризикувати тихими помилками ... Автономія!
Гонки легкості в орбіті

4
"Якщо ви передаєте масив за значенням, те, що ви дійсно робите, - це копіювання покажчика" Це не має сенсу, оскільки масиви не можуть передаватися за значенням, періодом.
juanchopanza

103

Масиви в основному такі ж, як покажчики в C / C ++, але не зовсім. Після перетворення масиву:

const int a[] = { 2, 3, 5, 7, 11 };

на покажчик (який працює без кастингу, а тому може траплятися несподівано в деяких випадках):

const int* p = a;

ви втрачаєте здатність sizeofоператора рахувати елементи в масиві:

assert( sizeof(p) != sizeof(a) );  // sizes are not equal

Цю втрачену здатність називають «занепадом».

Щоб отримати детальнішу інформацію, перегляньте цю статтю про розпад масиву .


51
Масиви в основному не збігаються з покажчиками; вони зовсім різні тварини. У більшості контекстів масив може трактуватися як би вказівником, а вказівник може трактуватися так, ніби це масив, але це так близько, як вони наближаються.
Джон Боде

20
@John, вибачте за мою неточну мову. Я намагався дійти до відповіді, не занурившись у тривалу історію, і "в основному ... але не зовсім" є настільки ж хорошим поясненням, як я коли-небудь потрапляв до коледжу. Я впевнений, що кожен, хто зацікавився, може отримати більш точну картину з вашого схваленого коментаря.
система ПАУЗА

"працює без лиття" означає те саме, що "трапляється неявно", коли йдеться про перетворення типів
ММ

47

Ось що говорить стандарт (C99 6.3.2.1/3 - Інші операнди - Значення, масиви та позначення функцій):

За винятком випадків, коли це операнд оператора sizeof або unary & operator, або рядковий літерал, який використовується для ініціалізації масиву, вираз, який має тип '' тип масиву '', перетворюється на вираз з типом '' вказівник на тип '', який вказує на початковий елемент об'єкта масиву і не є значенням.

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

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

Стандарт C ++ (конверсія Array-to-pointer 4.2) послаблює вимогу перетворення до (міна акценту):

Рівне значення або ревальвер типу "масив NT" або "масив невідомої межі T" може бути перетворений в ревальне значення типу "вказівник на T."

Таким чином, перетворення не повинно відбуватися так, як це майже завжди відбувається в C (це дозволяє функції перевантаження або шаблони збігаються для типу масиву).

Це також, чому в C вам слід уникати використання параметрів масиву у прототипах / визначеннях функцій (на мою думку - я не впевнений, чи існує якась загальна згода). Вони викликають плутанину і так чи інакше є вигадкою - використовуйте параметри вказівника, і плутанина може не відійти повністю, але принаймні декларація параметра не бреше.


2
Що таке приклад рядка коду, де "вираз, у якого тип" масив типу "", є "рядковим літералом, який використовується для ініціалізації масиву"?
Гарретт

4
@Garrett char x[] = "Hello";. Масив із 6 елементів "Hello"не розпадається; натомість xнабуває розмір 6і його елементи ініціалізуються з елементів "Hello".
ММ

30

"Розпад" відноситься до неявного перетворення виразу з типу масиву в тип вказівника. У більшості контекстів, коли компілятор бачить вираз масиву, він перетворює тип виразу з "N-елементного масиву T" в "покажчик на T" і встановлює значення виразу на адресу першого елемента масиву . Винятки з цього правила - це коли масив є операндом sizeofабо &операторів, або масив - це рядковий літерал, який використовується як ініціалізатор у декларації.

Припустимо наступний код:

char a[80];
strcpy(a, "This is a test");

Вираз aмає тип "80-елементний масив char", а вираз "Це тест" має тип "16-елементний масив char" (в C; у C ++ рядкові літерали - це масиви const char). Однак у виклику до strcpy()жоден вираз не є операндом sizeofабо &, тому їх типи неявно перетворюються на "покажчик на char", а їх значення встановлюються на адресу першого елемента в кожному. Що strcpy()отримує не масиви, а покажчики, як видно з його прототипу:

char *strcpy(char *dest, const char *src);

Це не те саме, що вказівник масиву. Наприклад:

char a[80];
char *ptr_to_first_element = a;
char (*ptr_to_array)[80] = &a;

Обидва ptr_to_first_elementі ptr_to_arrayмають однакове значення ; базова адреса a. Однак вони бувають різних типів і трактуються по-різному, як показано нижче:

a[i] == ptr_to_first_element[i] == (*ptr_to_array)[i] != *ptr_to_array[i] != ptr_to_array[i]

Пам’ятайте, що вираз a[i]інтерпретується як *(a+i)(що працює лише в тому випадку, якщо тип масиву перетворений у тип вказівника), тому обидва a[i]і ptr_to_first_element[i]працюють однаково. Вираз (*ptr_to_array)[i]трактується як *(*a+i). Вирази *ptr_to_array[i]та ptr_to_array[i]можуть призвести до попереджень або помилок компілятора залежно від контексту; вони напевно зроблять неправильну справу, якщо ви очікуєте їх оцінки a[i].

sizeof a == sizeof *ptr_to_array == 80

Знову ж таки, коли масив є операндом sizeof, він не перетворюється на тип покажчика.

sizeof *ptr_to_first_element == sizeof (char) == 1
sizeof ptr_to_first_element == sizeof (char *) == whatever the pointer size
                                                  is on your platform

ptr_to_first_element є простим покажчиком на char.


1
Чи не "This is a test" is of type "16-element array of char"є "15-element array of char"? (довжина 14 + 1 для \ 0)
chux - Відновіть Моніку

16

Масиви в С не мають значення.

Там, де очікується значення об’єкта, але об'єкт є масивом, використовується адреса його першого елемента замість типу pointer to (type of array elements).

У функції всі параметри передаються за значенням (масиви не є винятком). При передачі масиву у функції він "розпадається на покажчик" (sic); коли ви порівнюєте масив із чимось іншим, він знову "розпадається на покажчик" (sic); ...

void foo(int arr[]);

Функція foo очікує значення масиву. Але, в С, масиви не мають значення! Так fooотримує натомість адресу першого елемента масиву.

int arr[5];
int *ip = &(arr[1]);
if (arr == ip) { /* something; */ }

У порівнянні вище arrзначення не має значення, тому воно стає вказівником. Він стає вказівником на int. Цей покажчик можна порівняти зі змінною ip.

У синтаксисі індексації масиву, який ви звикли бачити, знову ж таки, arr "розкладений на покажчик"

arr[42];
/* same as *(arr + 42); */
/* same as *(&(arr[0]) + 42); */

Єдиний раз, коли масив не занепадає в покажчик, коли це операнд оператора sizeof, або оператора & (оператор «адреса»), або як рядкового літералу, який використовується для ініціалізації масиву символів.


5
"Масиви не мають значення" - що це повинно означати? Звичайно масиви мають значення ... вони є об'єктами, ви можете мати вказівники, а в C ++ - посилання на них тощо
Павло Мінаєв,

2
Я вважаю, строго "Значення" визначається в С як інтерпретація бітів об'єкта відповідно до типу. Мені важко з'ясувати корисне значення цього типу з масивом. Натомість ви можете сказати, що ви перетворюєте на покажчик, але це не інтерпретує вміст масиву, він просто отримує його місцезнаходження. Ви отримуєте значення вказівника (і це адреса), а не значення масиву (це була б "послідовність значень елементів, що містяться", як використовується у визначенні "рядок"). З цього приводу я думаю, що справедливо сказати "значення масиву", коли означає, що вказівник отримує.
Йоханнес Шауб - ліб

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

1
+1 для фрази з невеликим виправленням; для масивів це навіть не триплет, а лише куплет [розташування, тип]. Чи було у вас щось на увазі щодо третього місця у випадку масиву? Я не можу придумати жодного.
legends2k

1
@ legends2k: Я думаю, що я використав третє місце в масивах, щоб уникнути їх особливого випадку лише з куплетом. Можливо, [розташування, тип, порожнеча ] було б краще.
pmg

8

Це коли масив гниє і на нього вказують ;-)

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


Приємно сказано. Що було б приємним масивом, який не розпадається на покажчик чи той, який не завадить? Чи можете ви навести приклад в С? Дякую.
Unheilig

@Unheilig, звичайно, можна вакуумувати пакет масиву в struct і передавати struct.
Майкл Крелін - хакер

Я не впевнений, що ви маєте на увазі під «роботою». Доступ до повного масиву заборонено, хоча він працює так, як очікувалося, якщо ви очікуєте, що насправді станеться. Така поведінка (хоча, знову ж таки, офіційно невизначена) збережена.
Майкл Крелін - хакер

Занепад також трапляється у багатьох ситуаціях, які не передають масив нікуди (як описано в інших відповідях). Наприклад, a + 1.
ММ

3

Розпад масиву означає, що, коли масив передається як параметр функції, він трактується ідентично ("відпадає до") вказівника.

void do_something(int *array) {
  // We don't know how big array is here, because it's decayed to a pointer.
  printf("%i\n", sizeof(array));  // always prints 4 on a 32-bit machine
}

int main (int argc, char **argv) {
    int a[10];
    int b[20];
    int *c;
    printf("%zu\n", sizeof(a)); //prints 40 on a 32-bit machine
    printf("%zu\n", sizeof(b)); //prints 80 on a 32-bit machine
    printf("%zu\n", sizeof(c)); //prints 4 on a 32-bit machine
    do_something(a);
    do_something(b);
    do_something(c);
}

З вищезазначених є два ускладнення або винятки.

По-перше, при роботі з багатовимірними масивами в C і C ++ втрачається лише перший вимір. Це відбувається тому, що масиви безперервно розміщуються в пам'яті, тому компілятор повинен знати всі, крім першого виміру, щоб мати можливість обчислювати компенсації в цьому блоці пам'яті.

void do_something(int array[][10])
{
    // We don't know how big the first dimension is.
}

int main(int argc, char *argv[]) {
    int a[5][10];
    int b[20][10];
    do_something(a);
    do_something(b);
    return 0;
}

По-друге, в C ++ ви можете використовувати шаблони для виведення розміру масивів. Microsoft використовує це для C ++ версій функцій Secure CRT, таких як strcpy_s , і ви можете використовувати аналогічний трюк, щоб надійно отримати кількість елементів у масиві .


1
розпад трапляється в багатьох інших ситуаціях, не просто передаючи масив функції.
ММ

0

tl; dr: Коли ви використовуєте визначений масив, ви фактично використовуєте вказівник на його перший елемент.

Таким чином:

  • Коли пишеш arr[idx], ти насправді просто кажеш*(arr + idx) .
  • функції насправді ніколи не приймають масиви як параметри, а лише вказівники, навіть коли ви вказуєте параметр масиву.

Сортування винятків із цього правила:

  • Ви можете передавати масиви фіксованої довжини функціям в межах a struct.
  • sizeof() дає розмір, який приймає масив, а не розмір вказівника.

0

Мені може бути настільки сміливо думати, що існує чотири (4) способи передачі масиву як аргумент функції. Також ось короткий, але робочий код для вашої уваги.

#include <iostream>
#include <string>
#include <vector>
#include <cassert>

using namespace std;

// test data
// notice native array init with no copy aka "="
// not possible in C
 const char* specimen[]{ __TIME__, __DATE__, __TIMESTAMP__ };

// ONE
// simple, dangerous and useless
template<typename T>
void as_pointer(const T* array) { 
    // a pointer
    assert(array != nullptr); 
} ;

// TWO
// for above const T array[] means the same
// but and also , minimum array size indication might be given too
// this also does not stop the array decay into T *
// thus size information is lost
template<typename T>
void by_value_no_size(const T array[0xFF]) { 
    // decayed to a pointer
    assert( array != nullptr ); 
}

// THREE
// size information is preserved
// but pointer is asked for
template<typename T, size_t N>
void pointer_to_array(const T (*array)[N])
{
   // dealing with native pointer 
    assert( array != nullptr ); 
}

// FOUR
// no C equivalent
// array by reference
// size is preserved
template<typename T, size_t N>
void reference_to_array(const T (&array)[N])
{
    // array is not a pointer here
    // it is (almost) a container
    // most of the std:: lib algorithms 
    // do work on array reference, for example
    // range for requires std::begin() and std::end()
    // on the type passed as range to iterate over
    for (auto && elem : array )
    {
        cout << endl << elem ;
    }
}

int main()
{
     // ONE
     as_pointer(specimen);
     // TWO
     by_value_no_size(specimen);
     // THREE
     pointer_to_array(&specimen);
     // FOUR
     reference_to_array( specimen ) ;
}

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

Звичайно, є надзвичайно суворі проекти без розподілу купи, без винятків і без std :: lib. Можна сказати, що обробка нативного масиву C ++ є важливою мовою місії.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.