为什么C++不支持返回数组的函数?

55

有些语言允许您像普通函数一样声明返回数组的函数,例如Java:

public String[] funcarray() {
   String[] test = new String[]{"hi", "hello"};
   return test;
}
为什么C++不支持像int[] funcarray(){}这样的东西? 你可以返回一个数组,但是创建这样一个函数真的很麻烦。而且,我听说字符串只是char数组。那么如果在C++中可以返回一个字符串,为什么不能返回一个数组呢?

1
为什么不使用指针创建数组,然后返回指针呢? - RageD
在函数中,你不返回一个字符串,而是返回指向该字符串的指针。如果你返回一个std::string()对象,那么你应该意识到会有一些复制开销。 - boatcoder
1
@MisterSir:我会说这更像是一种特性——它可以保证一致性。如果你使用指针创建一个数组,那么你就在堆上动态分配了内存——也就是说,你可以通过引用返回并消除任何复制开销(因此数组的大小不会影响效率)。但是,你需要记得释放你已经分配的内存。 - RageD
8
@MisterSir - 同时,这并不会“打扰程序员”。C和C++不是应用程序设计语言。它们是系统编程语言。因此,在这些语言中有反映所需工作类型的设计决策。不要考虑高级别。考虑低级别。深入到底层,接触硬件。回顾我们在汇编语言、计算机组成原理和操作系统课程中学习的内容。这样当涉及到C和C++时,一切将变得更加清晰明了。 - luis.espinal
2
@luis.espinal:“C和C ++不是应用程序编程语言。它们是系统编程语言。[...]不要想得太高级。”-它们非常适合并广泛用于两者(当然,C显示出其年龄)。您关于历史和在系统编程中的使用帮助理解的观点是正确的,但不能建议这两种语言不适合或无法用于高级/应用程序编程。 - Tony Delroy
显示剩余6条评论
10个回答

74

我猜测这只是一个设计决策,为了简洁明了。更具体地说,如果你真的想知道为什么,你需要从最基础的东西开始学习。

让我们先考虑C语言。在C语言中,“按引用传递”和“按值传递”的区别是非常明显的。简单来说,在C语言中,数组的名称实际上就是一个指针。就大部分情况而言,它们之间的区别在于内存分配。下面的代码

int array[n];

在32位系统上,这将创建4*n字节的内存(在声明所属的代码块的范围内)。

int* array = (int*) malloc(sizeof(int)*n);

会在堆上创建同样大小的内存。在这种情况下,内存中的内容与作用域无关,只有对内存的引用受到作用域的限制。这就是传值和传引用的区别。传值意味着当将某个东西传递给函数或从函数返回时,被传递的"东西"是变量求值的结果。换句话说,

int n = 4;
printf("%d", n);

代码将打印数字4,因为构造n的值为4(如果这太基础了,我只是想涵盖所有情况)。这个4与程序的内存空间没有任何关系或联系,它只是一个字面上的值,一旦你离开拥有上下文的作用域,你就失去了它。那传递引用呢?在函数的上下文中,传递引用并没有什么不同;你只需评估被传递的构造。唯一的区别是,在评估传递的“东西”之后,你将评估结果作为内存地址使用。我曾经有一个特别愤世嫉俗的计算机科学教师,他喜欢说不存在传递引用,只有传递巧妙的值的方法。实际上,他是对的。所以现在我们从函数的角度来思考作用域。假设你可以有一个数组返回类型:

int[] foo(args){
    result[n];
    // Some code
    return result;
}

这里的问题在于result被评估为数组的第0个元素的地址。但当你试图从该函数之外访问这个内存时(通过返回值),你会遇到一个问题,因为你正在尝试访问不在当前作用域中的内存(函数调用的堆栈)。所以我们通过标准的“按引用传递”技巧来解决这个问题:

int* foo(args){
    int* result = (int*) malloc(sizeof(int)*n));
    // Some code
    return result;
}

在C++中,如果你想返回一个数组,你必须通过使用指针来手动管理内存。相比之下,在Java中,虽然语言会将数组或字符串版本转换为指针版本并自动处理内存,但这往往效率不高,因为它需要大量的内存分配和垃圾回收操作。

事实上,尽管Java中所有东西都是按值传递的,但几乎所有的值实际上都是内存地址。这意味着在Java中,即使你能够返回数组或字符串,它们仍然是被转换成指针版本返回的。C++之所以没有实现类似于Java的自动内存管理或垃圾回收机制,是因为其设计者Bjarne Stroustrup认为这样做会导致程序运行速度变慢。因此,在C++中,如果你想返回一个数组,你必须手动管理内存,而不能像Java那样让语言自动处理。


1
此外,针对“字符串是字符数组”的评论;这基本上是正确的。在C语言中,没有String类型;你必须自己处理它。它们存储在以null结尾的字符数组中,虽然存在一个String库来执行诸如查找长度等操作,但是它是通过解析字符串来完成的。在C++或Java中,可以将String视为包含字符数组的类,但还包含其他成员字段,如长度等信息,以便更轻松地操作。所以回到按引用传递。 - Doug Stephen
1
这正是我在寻找的答案!这极大地提高了我的内存理解。谢谢你! - Lockhead
4
不要再来了...“数组”和“指针”是不同的东西。即使加上“轻描淡写地处理它”的限定词,这种类型的答案只会增加混乱。 - David Rodríguez - dribeas
2
我从未说过数组是指针。我说的是数组的名称是指针。虽然这在语义上是错误的,但这只是一种简短而非技术性的说法,意思是除非在非常特殊的情况下,类型为T的数组的名称将会衰变成指向第一个元素的T类型指针,尽管不言而喻,数组的名称是不可修改的左值。但还是很抱歉。我理解你的担忧。 - Doug Stephen
2
这应该被提名为某种令人惊叹的回答奖。我学到了很多东西,因为它重新排列了我一直以来所知道和想当然的东西。 - Mad Physicist
显示剩余2条评论

32

C++确实支持它——嗯,有点像:

vector< string> func()
{
   vector<string> res;
   res.push_back( "hello" );
   res.push_back( "world" );
   return res;
}

甚至C也有一定程度的支持:

struct somearray
{
  struct somestruct d[50];
};

struct somearray func()
{
   struct somearray res;
   for( int i = 0; i < 50; ++i )
   {
      res.d[i] = whatever;
   }
   // fill them all in
   return res;
}

std::string是一个类,但当你说“字符串”时,你可能是指字面量。你可以从函数安全地返回字面量,但事实上,你可以静态创建任何数组并将其从函数返回。如果这是一个const(只读)数组,那么它就是线程安全的,这也是字符串字面量的情况。

但是,你返回的数组会退化成指针,所以你无法仅从返回值中得知它的大小。

如果可能的话,返回一个数组首先必须是固定长度的,因为编译器需要创建调用堆栈,然后存在这样的问题:数组不是L-value,所以在调用函数中接收它的方式是使用具有初始化的新变量,这是不切实际的。由于相同的原因,返回一个数组也可能是不切实际的,尽管它们可能已经使用了一种特殊的符号表示法来表示返回值。

请记住,在C的早期,所有变量都必须在函数顶部声明,你不能只在使用时声明。因此,当时是行不通的。

他们给出了将数组放入结构体中的解决方法,而这正是在C++中必须保持的方式,因为它使用相同的调用约定。

注意:在像Java这样的语言中,数组是一个类。你可以使用new来创建它们,也可以重新分配它们(它们是L-value)。


3
如果数组的大小在编译时固定,您可以使用 std::array<X,N>(或 std::tr1::array<X,N> 或 boost::array<X,N>)来处理。 - ysdx
1
一个std::vector不是一个数组,也不是包含一个数组的结构体。它们只是解决返回数组(实际本地类型,而不是其结构或对象包装器)的限制的机制。我理解你的意思,并且这些都是可行的例子。然而,这些既不是C++(或C)支持的特性(返回本地类型数组)的示例,也没有解释为什么在C++中存在这种限制。 - luis.espinal
1
@luis C++使用与C相同的调用约定。在C或C++中,数组不是l-value,这是主要问题。 - CashCow
你返回的数组只会退化成指针,如果返回类型是指针或者可以从指针进行隐式转换的类型。 - juanchopanza
1
@v.oddou 但是数组不能从指针隐式构造。"数组"函数中的参数并不是一个数组, 而是一个指针。它被允许看起来像一个数组来混淆人们的视听(有人可能在60年代后期认为这是个好主意)。 - juanchopanza
显示剩余4条评论

27
在C中(为了向后兼容,在C++中也是如此),数组具有与其他类型不同的特殊语义。特别地,尽管对于其他类型,C仅具有传值语义,但在数组的情况下,传值语法的效果以奇怪的方式模拟了传引用:
在函数签名中,类型为“T类型的N个元素的数组”的参数将转换为“T类型的指针”。在函数调用中,将数组作为参数传递给函数将使数组“衰减”为“第一个元素的指针”,并且该指针被复制到函数中。
由于数组的这种特殊处理——它们不能按值传递——因此它们也不能按值返回。在C中,您可以返回指针,在C++中,您还可以返回引用,但数组本身无法分配在堆栈中。
如果您考虑一下,这与您在问题中使用的语言没有区别,因为数组是动态分配的,您只返回指针/引用。
另一方面,C++语言提供了不同的解决方法,例如在当前标准中使用std::vector(内容是动态分配的)或在即将推出的标准中使用std::array(内容可以在堆栈中分配,但可能会有更高的成本,因为每个元素都必须在编译器无法省略副本的情况下进行复制)。实际上,您可以使用现有的库(如boost::array)来使用当前标准的相同类型的方法。

关于“在函数签名中,[数组->指针]” “[因此]它们不能按值返回”的问题。8.3.5.5确实要求将“任何类型为‘T的数组’的参数”调整为使用指针,但没有声明说该处理适用于返回类型,因为它们是不允许的。你的解释让它听起来像是将参数的处理应用于返回类型,并产生了一个无效的签名。事实并非如此——简单地说,数组返回类型是不允许的:8.3.5.8“函数不得具有类型为数组或函数的返回类型”。 - Tony Delroy
@TonyD:我认为他的解释很好,比被采纳的答案更好。但是最后关于std::vector/array的内容有些离题。(因为使用RVO/复制省略和返回值语义的东西与返回指向C数组的指针不是相同的语义,这是由于每个初学者在C中都已经熟悉了“衰减为指针”的概念,因为这是学习的最初阶段之一) - v.oddou

9

无法从函数返回数组,因为该数组将在函数内部声明,并且其位置将是堆栈帧。但是,当函数退出时,堆栈帧会被擦除。函数必须将返回值从堆栈帧复制到返回位置,而对于数组来说这是不可能的。

来自这里的讨论:

http://forum.codecall.net/c-c/32457-function-return-array-c.html


1
对于从你引用的链接中抄袭的内容进行负面评价。此外,这个答案是误导性的。特别是“函数必须复制返回值[sic]”在技术上是错误的,因为函数可以返回引用和指针。 - phooji
7
我认为这个引用没有问题,已经附上了参考资料。 - Brandon Frohbieter
1
@phooji:引用和指针都是指针,它们本身也是值。如果你理解指针的含义,那么这并不会有任何误导性。 - Inverse
10
我不能同意这个答案。对于大多数其他类型,您可以通过值返回,而且返回的对象在函数内部没有问题:会生成一份副本(或如果编译器能够这样做,则省略)。这是一种常见的行为,事实上,与数组无法完成相同的操作更多是 C 语言中的设计决策——在 C++ 中继承。实际上,如果将数组封装在结构体中,就会发生这种情况:结构体(包括内部数组)将在返回语句中被复制。 - David Rodríguez - dribeas
这根本没有回答问题。如果 C 语言的设计者决定数组是可分配和可复制的,那么它们就可以从函数中返回。 - juanchopanza
显示剩余2条评论

7

有人说在C++中,使用vector<>而不是从C继承的数组。

那么为什么C++不允许返回C数组呢?因为C语言也不允许。

为什么C语言不允许呢?因为C语言是从一种无类型语言B演变而来的,而在B语言中返回一个数组根本没有意义。在给B语言添加类型时,可以使返回数组成为可能,但这并没有做到,以保持某些B语言习惯用法的有效性,并简化从B到C的程序转换。自那时以来,使C数组更易于使用的可能性一直被拒绝(甚至没有考虑),因为它会破坏太多现有的代码。


“使C数组更易用...会破坏太多现有的代码” - 这是不正确的。如果现有程序包括返回数组的函数,则它们将无法编译,因此这些功能只与选择使用这些函数的新代码相关,并且绝不会使现有代码失效。换句话说,您并没有假设改变现有行为,而是提出了新的独立行为。 - Tony Delroy
@TonyD,你需要删除数组自动衰减为指针的功能,这将破坏很多代码,或者做出很多特殊情况,这样C数组就没有更多的可用性了,或者改变很少的东西,这样做是不值得的。 - AProgrammer
有趣的断言。请帮我了解您的具体关注点。为了更好地理解,请考虑以下上下文中的示例代码:int[4] f() { int x[4]; ...populate x...; return x; }为了使其以直观的方式有用,让我们添加一个新的支持要求,即在返回值和 int x[4] = f(); 中都需要对数组进行赋值。我不认为这需要指针衰减,也不需要更改其他代码来防止指针衰减。您看到哪些代码与此冲突? - Tony Delroy
@tonyd,如果您不更改当前规则,则f()的结果将会衰减为指针(就像int (*p)[4]一样,*p会衰减为指针)。 - AProgrammer
但是它什么时候会衰变呢? - 只有在原始类型无法进行赋值时才会发生衰变。就像 long x = get_char(); 一样 - 转换为 long 仅在赋值的右操作数不是 long 时尝试。因此,我们所讨论的不是指针衰变的抑制,而是在考虑指针衰变之前使某些新东西起作用。"(就像 int (p)[4],p 衰变为指针)" - 不是这样的,*p 仍然是 int[4] - 通过传递给 template <int N> void f(int (&a)[N]) { std::cout << N << '\n'; } 进行确认。衰变是最后的手段。 - Tony Delroy
它并不是从B语言演变而来,而是从BCPL语言演变而来。 - Skiller Dz

3
您可以返回指向数组的指针。只需小心后续释放内存即可。
public std::string* funcarray() {
    std::string* test = new std::string[2];
    test[0] = "hi";
    test[1] = "hello";
    return test;
}

// somewhere else:
std::string* arr = funcarray();
std::cout << arr[0] << " MisterSir" << std::endl;
delete[] arr;

或者您可以直接使用std命名空间中的容器,如std::vector。


我也应该删除 std::string* test 吗? - Lockhead
1
@MisterSir - 不需要。test是一个存储在堆栈上的变量,在函数返回时会超出作用域。然而,test指向的位置位于堆/自由存储器上,并返回给arr。因此,如果您删除arr,就足够了。 - Mahesh

2
为什么C++不支持类似于这样的功能?因为这没有任何意义。在基于引用的语言(如JAVA或PHP)中,内存管理基于垃圾回收。那些没有被引用(即程序中没有变量指向它)的内存部分会自动释放。在这种情况下,你可以轻松地分配内存并传递引用。
C++代码将被翻译成机器代码,并且其中没有定义垃圾回收。因此,在C和C++中具有内存块的强烈所有权感。你必须知道指针是否属于你,在任何时候都要释放它(实际上你应该在使用后释放它),或者你拥有指向共享内存区域的指针,绝不能释放它。
在这种环境中,每次函数传递数组时创建无尽的副本是没有意义的。在类似于C的语言中,管理数据数组更加复杂。没有一种通用的解决方案,你需要知道何时释放内存。
一个由函数返回的数组总是会是一个副本(你需要释放),还是你需要复制它们?获得数组而不是指向数组的指针,你能赢得什么?

为什么返回数组没有意义?C++ 发明了 std::array 部分是为了克服这种古怪的限制。这与 GC 或引用无关。C++ 允许按值返回对象(事实上 C 也是如此)。只是不允许返回普通数组。你的回答毫无意义。 - juanchopanza
我认为根本问题在于,如果一个方法要通过值返回某些东西,则必须在调用该方法之前为该对象保留空间。由于固定大小的数组可以封装在结构中以实现此目的,并且由于这种结构的行为比数组类型更一致和有用,因此返回固定大小的数组类型几乎没有任何好处。在某些情况下,可变大小的数组可能很好,但是调用者无法提供它们的空间的合理机制。 - supercat

1

应该返回一个 std::vector<> 而不是一个数组。一般来说,数组在 C++ 中表现不佳,应尽量避免使用。

string 数据类型不仅仅是字符数组,虽然 "quoted string" 是。 string 管理一个字符数组,并且您可以使用 .c_str() 访问它,但是 string 不止于此。



0

这些答案都没有抓住重点。C++根本不支持它。甚至在std::array<T, N>之前,它甚至不支持返回静态大小的数组的方法。C++可以支持返回动态大小的数组,但它们没有。我相信有可辩解的原因,但他们可以。

你需要做的就是在堆栈上分配动态数组,返回其地址和大小,并确保调用者将堆栈指针提升到返回的数组的末尾。可能需要一些堆栈帧修复,但绝不是不可能的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接