Подтвердить что ты не робот

Почему массивы не могут передаваться как аргументы функции?

Почему вы не можете передавать массивы в качестве аргументов функции?

Я читал эту книгу на С++, в которой говорится: "Вы не можете передавать массивы в качестве аргументов функции", но это никогда не объясняет, почему. Кроме того, когда я просматривал его в Интернете, я нашел комментарии, например: "Почему бы вам так поступить?" Не то, чтобы я это сделал, я просто хочу знать, почему вы не можете.

4b9b3361

Ответ 1

Почему массивы не могут передаваться как аргументы функции?

Они могут:

void foo(const int (&myArray)[5]) {
   // `myArray` is the original array of five integers
}

В технических терминах тип аргумента foo - это "ссылка на массив из 5 const int s"; со ссылками, мы можем передать фактический объект вокруг ( отказ от ответственности: терминология зависит от уровня абстракции).

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


Имена массивов распадаются на указатели для пропущенных значений

Это означает:

void foo(int* ptr);

int ar[10]; // an array
foo(ar);    // automatically passing ptr to first element of ar (i.e. &ar[0])

Там также очень вводящий в заблуждение "синтаксический сахар", который выглядит так, будто вы можете передать массив произвольной длины по значению:

void foo(int ptr[]);

int ar[10]; // an array
foo(ar);

Но, на самом деле, вы все еще просто передаете указатель (к первому элементу ar). foo - это то же самое, что было выше!

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

void foo(int ar[5]);
int main() {
   int ar[5];
   foo(ar);
}

// error: undefined reference to `func(int*)'

Итак, foo принимает int* фактически, а не int[5]!

(демо-версия.


Но вы можете обойти это!

Вы можете взломать это, обернув массив в struct или class, потому что оператор копирования по умолчанию скопирует массив:

struct Array_by_val
{
  int my_array[10];
};

void func (Array_by_val x) {}

int main() {
   Array_by_val x;
   func(x);
}

Это несколько запутывающее поведение.


Или, что лучше, общий подход по ссылке

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

template <typename T, size_t N>
void foo(const T (&myArray)[N]) {
   // `myArray` is the original array of N Ts
}

Но мы по-прежнему не можем передавать один по значению. Что-то вспомнить.


Будущее...

И так как С++ 11 находится за горизонтом, а поддержка С++ 0x прекрасно сочетается с основными инструментами, вы можете использовать прекрасный std::array, унаследованный от Boost! Я оставлю исследование в качестве упражнения для читателя.

Ответ 2

Итак, я вижу ответы, объясняющие: "Почему компилятор не позволяет мне это делать?" Вместо "Что заставило стандарт указать это поведение?" Ответ лежит в истории C. Это взято из "Развитие языка C" (источник) Деннисом Ричи.

В языках прото-C память делилась на "ячейки", каждая из которых содержала слово. Они могут быть разыменованы с помощью конечного унарного оператора * - да, это были, по сути, бесхитростные языки, например, некоторые из сегодняшних игрушечных языков, таких как Brainf_ck. Синтаксический сахар позволял притворяться, что указателем был массив:

a[5]; // equivalent to *(a + 5)

Затем было добавлено автоматическое распределение:

auto a[10]; // allocate 10 cells, assign pointer to a
            // note that we are still typeless
a += 1;     // remember that a is a pointer

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

Ответ 3

Массивы в некотором смысле являются типами второго класса, то, что С++ унаследовано от C.

Цитата 6.3.2.1p3 в стандарт C99:

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

Тот же абзац в C11 standard по сути тот же, с добавлением нового оператора _Alignof. (Оба ссылки относятся к черновикам, которые очень близки к официальным стандартам. ( UPDATE: Это была фактически ошибка в черновике N1570, исправленная в выпущенном стандарте C11. _Alignof не может применяться к выражению, только к имени типа в скобках, поэтому C11 имеет только те же 3 исключения, что и C99 и C90. (Но я отвлекаюсь.)))

У меня нет соответствующей ссылки на С++, но я считаю, что это очень похоже.

Итак, если arr - объект массива, и вы вызываете функцию func(arr), то func получит указатель на первый элемент arr.

До сих пор это более или менее "работает так, потому что оно определено именно так", но есть исторические и технические причины для этого.

Разрешающие параметры массива не позволят обеспечить большую гибкость (без дополнительных изменений в языке), поскольку, например, char[5] и char[6] являются различными типами. Даже прохождение массивов по ссылке не помогает с этим (если нет какой-либо функции С++, которую я пропускаю, всегда есть возможность). Передающие указатели дают вам огромную гибкость (возможно, слишком много!). Указатель может указывать на первый элемент массива любого размера - но вам нужно перевернуть свой собственный механизм, чтобы сообщить функции, насколько велик массив.

Разработка языка, так что массивы разной длины в некоторой степени совместимы, хотя они все еще различны, на самом деле довольно сложны. Например, в Аде эквиваленты char[5] и char[6] являются одним и тем же типом, но разными подтипами. Более динамические языки составляют длину объекта массива, а не его тип. C все еще довольно много путается вместе с явными указателями и длинами, или указателями и терминаторами. С++ унаследовал весь этот багаж от C. Он в основном проигрывал всю вещь массива и вводил векторы, поэтому было не так много необходимости создавать массивы первоклассных типов.

TL; DR: Это С++, вы все равно должны использовать векторы! (Ну, иногда.)

Ответ 4

Массивы не передаются по значению, потому что массивы являются по существу непрерывными блоками memmory. Если у вас есть массив, который вы хотите передать по значению, вы можете объявить его внутри структуры, а затем получить доступ к ней через структуру.

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

Ответ 5

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

Ответ 6

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

Вы можете преодолеть это ограничение, поместив массив в структуру (или используя Boost:: Array):

struct Array
{
    int data[512*1024];
    int& operator[](int i) { return data[i]; }
};
void foo(Array byValueArray) { .......... }

Попробуйте сделать вложенные вызовы этой функции и посмотреть, сколько переполнений стека вы получите!