Почему нельзя передавать массивы в качестве аргументов функции? - PullRequest
34 голосов
/ 16 августа 2011

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

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

Ответы [ 6 ]

45 голосов
/ 16 августа 2011

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

Они могут:

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]!

( Live демо. )


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

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

struct Array_by_val
{
  int my_array[10];
};

void func (Array_by_val x) {}

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

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


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

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

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

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


Будущее ...

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

14 голосов
/ 16 августа 2011

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

В языках прото-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 / C ++.

5 голосов
/ 16 августа 2011

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

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

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

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

У меня нет соответствующей цитаты C ++ под рукой, ноЯ считаю, что это очень похоже.

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

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

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

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

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

2 голосов
/ 16 августа 2011

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

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

1 голос
/ 16 августа 2011

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

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

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

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

1 голос
/ 16 августа 2011

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

...