为啥数组不能作为函数参数传递?

Posted

技术标签:

【中文标题】为啥数组不能作为函数参数传递?【英文标题】:Why can't arrays be passed as function arguments?为什么数组不能作为函数参数传递? 【发布时间】:2011-10-27 17:48:17 【问题描述】:

为什么不能将数组作为函数参数传递?

我一直在阅读这本 C++ 书,上面说“你不能将数组作为函数参数传递”,但它从未解释过原因。此外,当我在网上查找时,我发现了类似“你为什么要这样做?”之类的 cmets。不是我会做,我只是想知道你为什么做不到。

【问题讨论】:

请注意他的正文中写着“by value”。 可能是因为复制整个数组只是为了通过值作为函数参数传递它很容易导致堆栈溢出和/或非常慢? @In silico:那为什么允许创建数组作为局部变量,它们具有相同的自动生命周期? @Ben Voigt:我看不出这与按值传递数组的问题有什么关系。我想要得到的是,按值传递数组肯定会涉及将其内容复制到堆栈帧中,这是浪费时间和内存,因为将指针传递给第一个元素可以让您获得相同的访问它的能力但是以一种更简单、更快且占用内存更少的方式。 @In:但做出这样的决定与 C 非常不同。我们几乎可以做任何其他事情,如果我想复制我的数组怎么办?为什么仅将数组放入结构中就可以消除此规则?您的论点将适用于这样的结构。 【参考方案1】:

为什么不能将数组作为函数参数传递?

他们可以:

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

用技术术语来说,foo 的参数类型是“引用数组 5 constints”;通过引用,我们可以在 周围传递 实际 对象(免责声明:术语因抽象级别而异)

你不能做的是按值传递,因为由于历史原因我们不会复制数组。相反,尝试将数组按值传递给函数(或者,传递数组的副本)会导致其名称衰减为指针。 (some resources get this wrong!)


数组名称衰减为按值传递的指针

这意味着:

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 demo.)


但你可以解决它!

You can hack around this 通过将数组包装在structclass 中,因为默认的复制运算符复制数组:

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 支持在主流工具链中得到很好的支持,您可以使用继承自 Boost 的可爱的std::array!我将把研究留给读者作为练习。

【讨论】:

伙计,你真快!不过,您能否详细介绍一下指针衰减的内容? 我认为反对无偿按值复制数组的原因仍然非常相关...... 好吧,严格来说,您传递的是对数组的引用。 很高兴看到你涵盖了所有的基础。 +1 很好,但我们不是已经在数组上有一个FAQ 吗? ;)【参考方案2】:

所以我看到答案解释,“为什么编译器不允许我这样做?”而不是“是什么导致标准指定这种行为?”答案就在 C 的历史中。本文摘自 Dennis Ritchie 的“The Development of the C Language”(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++ 的另一个问题。

【讨论】:

我不相信这能解释为什么数组不能被复制。 c# 中的 params 关键字几乎可以做到这一点! @Tomalak:无法复制的原因在于标准。这是历史信息,而非规范信息。 @Kiran:是的,其他语言在引用类型和值类型之间有更仔细的架构差异,不需要容纳半个世纪的遗留代码。 @Dietrich:我知道这是历史性的;我的意思是在这种情况下。可以肯定,这是一个有趣的答案,但我不太明白它之间的联系以及为什么它在历史上意味着不复制数组。很可能就是我;你能帮我吗? :)【参考方案3】:

数组在某种意义上是二等类型,C++ 继承自 C。

在the C99 standard 中引用 6.3.2.1p3:

除非它是 sizeof 运算符的操作数或一元 & 运算符,或者是用于初始化数组的字符串文字, 类型为“type 的数组”的表达式被转换为 类型为“pointer to type”的表达式,指向初始 数组对象的元素并且不是左值。如果数组对象 有注册存储类,行为未定义。

C11 standard 中的相同段落基本相同,只是添加了新的_Alignof 运算符。 (两个链接都指向非常接近官方标准的草案。(更新:这实际上是 N1570 草案中的一个错误,已在已发布的 C11 标准中更正。_Alignof 无法应用到一个表达式,只到一个带括号的类型名称,所以 C11 只有与 C99 和 C90 相同的 3 个例外。(但我离题了。)))

我手边没有相应的 C++ 引用,但我相信它非常相似。

所以如果arr 是一个数组对象,并且你调用了一个函数func(arr),那么func 将收到一个指向arr 的第一个元素的指针。

到目前为止,这或多或少是“它是这样工作的,因为它是这样定义的”,但它有历史和技术原因。

允许数组参数不会带来太大的灵活性(无需进一步更改语言),因为例如,char[5]char[6] 是不同的类型。即使通过引用传递数组也无济于事(除非我缺少一些 C++ 功能,否则总是有可能的)。传递指针给了你极大的灵活性(也许太多了!)。指针可以指向任意大小数组的第一个元素——但您必须使用自己的机制来告诉函数数组有多大。

设计一种语言以使不同长度的数组在某种程度上兼容但仍然不同实际上是相当棘手的。例如,在 Ada 中,char[5]char[6] 的等价物是相同的类型,但不同的子类型。更多动态语言使长度成为数组对象值的一部分,而不是其类型。 C 仍然与显式指针和长度,或指针和终止符一起混淆。 C++ 继承了 C 的所有包袱。它主要关注整个数组并引入向量,因此没有太多需要将数组设为一等类型。

TL;DR:这是 C++,无论如何你都应该使用向量! (嗯,有时。)

【讨论】:

"TL;DR:这是 C++,无论如何你都应该使用向量!"我不同意。为什么我要动态分配一个我知道具有静态小尺寸的数组?另外,您知道这些历史和技术原因是什么吗? 这是 C++;你应该使用std::array。 :) @Tomalak:这是 C++,你应该使用 Perl! (跑得非常快) 我不希望对反对票做出解释,但如果有办法改进这个答案,我很乐意听到。【参考方案4】:

数组不是按值传递的,因为数组本质上是连续的内存块。如果您有一个想要按值传递的数组,您可以在一个结构中声明它,然后通过该结构访问它。

这本身会对性能产生影响,因为这意味着您将在堆栈上锁定更多空间。传递指针更快,因为要复制到堆栈的数据包络要少得多。

【讨论】:

"数组不是按值传递的,因为数组本质上是连续的内存块。"嗯,我不能对结构说同样的话吗? @Hudson:请等待 7 分钟以上,然后再接受答复。让人们找出正确的方法;至少一天。 @ccozad:我不明白同质性有多重要。 @Hudson:数组元素的连续集合。给定int a[10];,名称a 的类型为int[10],是十个整数的连续集合。 @ccozad:不,数组不是指向第一个元素的指针。数组不是指针;指针不是数组。请参阅我的回答中引用的 C99 标准。【参考方案5】:

我相信 C++ 这样做的原因是,当它被创建时,它可能占用了太多资源来发送整个数组而不是内存中的地址。这只是我对此事的想法和假设。

【讨论】:

【参考方案6】:

这是由于技术原因。参数在堆栈上传递;数组可以有很大的大小,兆字节等等。在每次调用时将数据复制到堆栈不仅会更慢,而且会很快耗尽堆栈。

您可以通过将数组放入结构体(或使用 Boost::Array)来克服该限制:

struct Array

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

尝试对该函数进行嵌套调用,看看会发生多少堆栈溢出!

【讨论】:

您也可以将数组放入 C 中的结构中。

以上是关于为啥数组不能作为函数参数传递?的主要内容,如果未能解决你的问题,请参考以下文章

C语言中,数组名作为函数参数,属于啥传递,为啥?

数组名作为函数的参数属于啥传递为啥

为啥我们在传递二维数组作为参数时需要指定列大小?

C中 传递 数组 ,参数不能传递进去。

spring MVC 怎么获取前端传递的数组参数

如果我尝试在不同的公共类中将数组作为参数传递,为啥我的构造函数无法在 Java 中编译?