Что такое распадающийся массив?

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

11 ответов

Решение

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

Массивы в основном такие же, как указатели в C/C++, но не совсем. Как только вы конвертируете массив:

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

в указатель (который работает без приведения и, следовательно, в некоторых случаях может произойти неожиданно):

const int* p = a;

вы теряете способность sizeof Оператор для подсчета элементов в массиве:

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

Эта потерянная способность называется "распадом".

Для более подробной информации, ознакомьтесь с этой статьей о распаде массива.

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

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

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

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

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

Значение l или значение типа "массив N T" или "массив неизвестной границы T" может быть преобразовано в значение типа "указатель на T".

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

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

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

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 простой указатель на символ

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

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

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

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

Попробуйте этот код

      
void f(double a[10]) {
    printf("in function: %d", sizeof(a));
    printf("pointer size: %d\n", sizeof(double *));
}

int main() {
    double a[10];
    printf("in main: %d", sizeof(a));
    f(a);
}

и вы увидите, что размер массива внутри функции не равен размеру массива в main, а равен размеру указателя.

Вы, наверное, слышали, что «массивы — это указатели», но это не совсем так (т. sizeofвнутри mainпечатает правильный размер). Однако при передаче массив распадается на указатель. То есть независимо от того, что показывает синтаксис, вы фактически передаете указатель, и функция фактически получает указатель.

В этом случае определение void f(double a[10]неявно преобразуется компилятором в void f(double *a). Вы могли бы эквивалентно объявить аргумент функции непосредственно как *a. Вы могли бы даже написать a[100]или же a[1], вместо a[10], так как на самом деле он никогда не компилируется таким образом (однако, вы не должны этого делать, это может запутать читателя).

Затухание массива означает, что когда массив передается в качестве параметра функции, он обрабатывается идентично ("распадается на") указателю.

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

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

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

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

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

  • Вы можете передавать массивы фиксированной длины в функции внутри struct,
  • sizeof() дает размер, занятый массивом, а не размер указателя.

Массивы автоматически передаются указателем в C. Обоснование этого можно только предполагать.

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

Например, при первом разыменовании будет получено (то есть тот же адрес, только другой тип, а не заметка) и арифметика указателя на ie a+1 или же *(a+1)будет в терминах размера массива из 5 целых чисел (который является типом данных, на который он указывает), а второе разыменование приведет к созданию файла. Однако при первом разыменовании будет произведено арифметическое вычисление указателя в единицах размера int.

В функцию можно передать только и int (*)[5], и функция преобразует его в любой тип параметра, поэтому внутри функции у вас есть выбор, рассматривать ли передаваемый адрес как распавшийся массив или как указатель на массив (где функция должна указывать размер передаваемый массив). Если вы переходите к функции и определяется, то как aразрешается в адрес, вы передаете адрес, и адрес может быть только типом указателя. В функции параметр, к которому она обращается, тогда является адресом в стеке или в регистре, который может быть только типом указателя, а не типом массива - это потому, что это фактический адрес в стеке и, следовательно, явно не является сам массив.

Вы теряете размер массива, потому что тип параметра, являющийся адресом, является указателем, а не массивом, который не имеет размера массива, как это можно увидеть при использовании sizeof, который работает с типом передаваемого ему значения. Тип параметра int a[5] вместо int *a разрешено, но рассматривается как int * вместо того, чтобы полностью запретить это, хотя это должно быть запрещено, потому что это вводит в заблуждение, потому что заставляет вас думать, что информацию о размере можно использовать, но вы можете сделать это, только преобразовав ее в int (*a)[5], и, конечно же, функция должна указать размер массива, потому что нет возможности передать размер массива, потому что размер массива должен быть константой времени компиляции.

Я мог бы быть настолько смелым, чтобы думать, что существует четыре (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++ - критически важная функция языка.

Другие вопросы по тегам