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

385

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

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

PMG
источник
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() дает размер, занятый массивом, а не размер указателя.
einpoklum
источник
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 ++ является критически важной функцией языка.

Шеф-повар Гладиатор
источник