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


385

Что такое массив для распада указателя? Есть ли какое-либо отношение к указателям на массивы?


73
малоизвестно: унарный оператор плюс можно использовать как «оператор затухания»: если задан int a[10]; int b(void);, то +aэто указатель на int и указатель +bна функцию. Полезно, если вы хотите передать его шаблону, принимающему ссылку.
Йоханнес Шауб - лит

3
@litb - parens будет делать то же самое (например, (a) должно быть выражением, которое оценивает указатель), верно?
Майкл Берр

21
std::decayиз C ++ 14 будет менее неясным способом распада массива по сравнению с унарным +.
legends2k

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

5
@ Право. Но я полагаю, что это не так мало известно, как трюк с унарным +. Причина, по которой я это упомянул, была не просто потому, что она разлагается, а потому, что с ней интересно играть;)
Йоханнес Шауб - 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
@ Джон, прошу прощения за мой неточный язык. Я пытался найти ответ, не увязнув в длительной предыстории, и «в основном ... но не совсем» - такое же хорошее объяснение, как я когда-либо получал в колледже. Я уверен, что любой заинтересованный может получить более точную картину из вашего комментария.
системная пауза

«работает без приведения» означает то же самое, что «происходит неявно», когда речь идет о преобразованиях типов
ММ,

47

Вот что говорит стандарт (C99 6.3.2.1/3 - Другие операнды - L-значения, массивы и обозначения функций):

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

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

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

Стандарт C ++ (4.2 преобразование массива в указатель) ослабляет требование преобразования к (выделение мое):

Значение l или значение типа «массив NT» или «массив неизвестных границ T» может быть преобразовано в значение типа «указатель на T».

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

Вот почему в C вы должны избегать использования параметров массива в прототипах / определениях функций (на мой взгляд - я не уверен, есть ли общее согласие). Они вызывают путаницу и в любом случае являются фикцией - используйте параметры указателя, и путаница может не исчезнуть полностью, но, по крайней мере, объявление параметра не лжёт.


2
Что такое пример строки кода, где «выражение с типом« массив типа »» является «строковым литералом, используемым для инициализации массива»?
Гарретт

4
@ Гарретт 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или &, поэтому их типы неявно преобразуются в «указатель на символ», а их значения устанавливаются по адресу первого элемента в каждом. То, что 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[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 простой указатель на символ


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 или оператора & (оператор 'address of'), или как строковый литерал, используемый для инициализации массива символов.


5
«Массивы не имеют значения» - что это должно означать? Конечно, массивы имеют значение ... это объекты, вы можете иметь указатели и, в C ++, ссылки на них и т. Д.
Павел Минаев

2
Я считаю, что строгое «Значение» определяется в С как интерпретация битов объекта в соответствии с типом. Мне трудно понять, как это можно понять с помощью типа массива. Вместо этого вы можете сказать, что вы конвертируете в указатель, но он не интерпретирует содержимое массива, он просто получает его местоположение. То, что вы получите, это значение указателя (и это адрес), а не значение массива (это будет «последовательность значений содержащихся элементов», как используется в определении «строка»). Тем не менее, я думаю, что было бы справедливо сказать «значение массива», когда один означает, что указатель получен.
Йоханнес Шауб - лит

во всяком случае, я думаю, что есть небольшая двусмысленность: значение объекта и значение выражения (как в «rvalue»). Если интерпретировать последний способ, то выражение массива, безусловно, имеет значение: оно является результатом его преобразования в значение r и является указателем. Но если интерпретировать первый способ, то, конечно, нет никакого полезного значения для объекта массива.
Йоханнес Шауб - лит

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

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

8

Это когда массив гниет и на него указывают ;-)

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


Красиво сказано. Какой будет хороший массив, который не распадается на указатель или тот, который не может распадаться? Можете ли вы привести пример в C? Спасибо.
Unheilig

@Unheilig, конечно, можно упаковать массив в 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 использует это для версий Secure CRT на C ++, таких как strcpy_s , и вы можете использовать аналогичный прием для надежного получения количества элементов в массиве .


1
Распад происходит во многих других ситуациях, а не просто при передаче массива в функцию.
ММ

0

tl; dr: когда вы используете определенный вами массив, вы фактически будете использовать указатель на его первый элемент.

Таким образом:

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

Сортировка исключений из этого правила:

  • Вы можете передавать массивы фиксированной длины в функции внутри 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.