在函数中返回数组

293

我有一个数组 int arr[5],它被传递给一个函数 fillarr(int arr[])

int fillarr(int arr[])
{
    for(...);
    return arr;
}
  1. 我如何返回这个数组?
  2. 如果我返回一个指针,我将如何访问它?

66
严格来说,在这个上下文中,你不需要返回数组,因为数组是通过引用传递的,所以对于“arr”内部元素的任何更改都会在函数外部可见。 - BuggerMe
15
返回数组方便链式调用函数。 - seand
7
只要你不犯在栈上创建数组并返回指向它的指针的错误。 - detly
7
@BuggerMe说:数组并不是按引用传递的(除非你使用更加有趣的语法来请求),在代码中,数组会退化成指向第一个元素的指针并被传递给函数。函数签名中的5会被编译器丢弃。 - David Rodríguez - dribeas
7
@BuggerMe:不,其实不是。我很精确是因为我已经习惯了人们误解C ++中数组按值传递语法的语意。通过引用传递数组是这样的:void foo( int (&array)[5] );(将一个包含5个整数的数组作为引用传递)。当你通过引用传递时,在函数内部得到的是对实际类型的引用。另一方面,void foo(int array[5])在函数定义期间被编译器转换为void foo(int*)。调用foo(myarray)会导致数组衰变为指向第一个元素的指针。 - David Rodríguez - dribeas
显示剩余5条评论
21个回答

253
在这种情况下,你的数组变量 arr 实际上也可以被视为一个指向内存中数组块开头的指针,通过隐式转换。你使用的语法如下:

在这种情况下,你的数组变量 arr 实际上也可以被视为一个指向内存中数组块开头的指针,通过隐式转换。

int fillarr(int arr[])

这只是一种语法糖。如果用下面的代码替换,也能正常工作:

int fillarr(int* arr)

所以在同样的意义上,你想从函数返回的实际上是指向数组第一个元素的指针:

int* fillarr(int arr[])

而且您仍然可以像使用普通数组一样使用它:

int main()
{
  int y[10];
  int *a = fillarr(y);
  cout << a[0] << endl;
}

51
澄清一下,“经典的C++语句”是错误的;数组不是指针。 - GManNickG
35
记住 a[i] == *(a + i) 规则。 - seand
9
@Brent Nash,不是的。数组就是数组,指向数组开头的指针就是指针。编译器只是在某些情况下为您提供了一些语法糖来帮助您进行转换。在许多情况下,“array”和“&array”是可以互换的。请注意,这并不改变它们的本质。 - Carl Norum
27
@Brent:不是的,数组是自己的类型,它不是指针的一种特殊类型。在“int a[10]”中,“a”的类型是“int[10]”。你可以说数组“衰变”为指向它们第一个元素的指针(这是隐式的从数组到指针的转换)。如果你修改你的回答来区分数组、从数组到指针的转换和指针之间的区别,那么我会删除我的回答,因为它们包含相同的核心信息且你先回答了。 - GManNickG
8
@seand 记住 a[i] == *(a + sizeof(a)*i) 规则。 - Amir
显示剩余13条评论

137

C++函数无法按值返回C语言风格的数组。最接近的方法是返回一个指针。此外,参数列表中的数组类型会被简单转换为指针。

int *fillarr( int arr[] ) { // arr "decays" to type int *
    return arr;
}

你可以通过对参数和返回值使用数组引用来改善它,这可以防止衰减:

int ( &fillarr( int (&arr)[5] ) )[5] { // no decay; argument must be size 5
    return arr;
}

使用Boost或C++11时,按引用传递只是可选的,并且语法更加易懂:

array< int, 5 > &fillarr( array< int, 5 > &arr ) {
    return arr; // "array" being boost::array or std::array
}

array 模板只是生成一个包含 C 风格数组的 struct,因此您可以应用面向对象的语义并保留数组的原始简单性。


5
赞成提供一个如何通过引用传递数组的示例。但是你错了,不能通过引用返回数组。最简单的语法是使用typedef: typedef int array[5]; array& foo(); 但是,如果你愿意编写以下内容,则不需要typedef:int (&foo())[5] { static int a[5] = {}; return a; },问题中的示例将是:int (&foo( int (&a)[5] ))[5] { return a; }。很简单,不是吗? - David Rodríguez - dribeas
2
chubsdad的回答引用了标准中的正确说法:你不能返回一个数组,但是你可以返回一个数组的引用或指针。数组作为一种类型是不可复制的,因此它们不能被返回——这将意味着复制——当这种语法出现时,编译器会将参数转换为指针。 - David Rodríguez - dribeas
3
@David:是的,确实如此。这个页面变得异常冗长。从来没有那么多人自愿在一个地方写那么多返回数组的琐碎函数。 - Potatoswatter
@Potatoswatter,我是cpp的新手,你能详细解释一下第二段代码吗?我无法将其分解成易于理解的部分。 - ajaysinghnegi
@AjaySinghNegi 这与 typedef int arri5[5]; arri5 &fillarr( arri5 & arr ) { return arr; } 相同。最好在这里使用typedef。 - Potatoswatter
显示剩余6条评论

42

在C++11中,您可以返回std::array

#include <array>
using namespace std;

array<int, 5> fillarr(int arr[])
{
    array<int, 5> arr2;
    for(int i=0; i<5; ++i) {
        arr2[i]=arr[i]*2;
    }
    return arr2;
}

3
引述原文作者:“(...)你可以认为返回的数组 arr2 是完全不同的另一个数组(...)”。 - cubuspl42

25

$8.3.5/8 规定-

"函数的返回类型不能是数组或函数,但可以是指向这些类型的指针或引用。不允许定义函数的数组,但可以定义指向函数的指针的数组。"

int (&fn1(int (&arr)[5]))[5]{     // declare fn1 as returning refernce to array
   return arr;
}

int *fn2(int arr[]){              // declare fn2 as returning pointer to array
   return arr;
}


int main(){
   int buf[5];
   fn1(buf);
   fn2(buf);
}

10
你的第二个函数返回一个指向int类型的指针,而不是一个数组。 - GManNickG
再说一遍,为什么函数内部实际的数组已经被更新了,还要返回类型呢?这是最佳实践的问题吗? - Dan

15

答案可能会有所不同,具体取决于您计划如何使用该函数。为了给出最简单的答案,让我们假设您真正需要的是一个向量而不是数组。向量很好用,因为它们看起来就像普通的、无聊的值,可以存储在常规指针中。我们将在之后查看其他选项及其原因:

std::vector<int> fillarr( std::vector<int> arr ) {
    // do something
    return arr;
}

这将完全按照您的期望执行。好处在于 std::vector 确保一切处理得干净利落。坏处在于,如果您的数组很大,这将复制大量数据。实际上,它会两次复制每个数组元素。首先,它复制向量以便函数可以将其用作参数。然后它再次复制它以将其返回给调用方。如果您能够自己管理向量,那么您可以更轻松地完成某些操作。(如果调用方需要将其存储在某种变量中以进行更多计算,则可能需要第三次复制)
看起来,您真正想做的是填充集合。如果您没有特定原因要返回集合的新实例,则不要这样做。我们可以这样做:
void fillarr(std::vector<int> &  arr) {
    // modify arr
    // don't return anything
}

这样,您可以获得传递给函数的数组的引用,而不是它的私有副本。您对参数所做的任何更改都会被调用者看到。如果您想要,可以返回对它的引用,但这并不是一个好主意,因为它有点暗示您得到了与传递的内容不同的东西。
如果您确实需要集合的新实例,但想避免将其放在堆栈上(以及涉及的所有复制),则需要创建某种处理该实例的契约。最简单的方法是使用智能指针,它将保留引用的实例,只要有人持有它。如果超出范围,则干净地消失。代码如下:
std::auto_ptr<std::vector<int> > fillarr( const std::vector<int> & arr) {
    std::auto_ptr<std::vector<int> > myArr(new std::vector<int>);
    // do stuff with arr and *myArr
    return myArr;
}

大多数情况下,使用*myArr与使用普通的vector相同。此示例还通过添加const关键字来修改参数列表。现在,您可以获得一个引用而不是复制它,但您无法修改它,因此调用者知道它将与函数到达之前相同。
所有这些都很好,但惯用的c++很少将集合作为整体处理。更正常的情况是,您将使用迭代器遍历这些集合。看起来会像这样:
template <class Iterator>
Iterator fillarr(Iterator arrStart, Iterator arrEnd) {
    Iterator arrIter = arrStart;
    for(;arrIter <= arrEnd; arrIter++)
       ;// do something
    return arrStart;
}

如果你不习惯这种风格,使用它看起来有点奇怪。

vector<int> arr;
vector<int>::iterator foo = fillarr(arr.begin(), arr.end());

现在,foo指向修改后的arr开头。

这个方法同样适用于vector和普通的C数组以及其他许多类型的集合。

int arr[100];
int *foo = fillarr(arr, arr+100);

现在它看起来非常像在这个问题的其他地方给出的普通指针示例。


语法错误,& 符号必须出现在类型后面:void fillarr(std::vector<int> & arr) - David Rodríguez - dribeas

11

这个:

int fillarr(int arr[])

实际上被视为相同:

int fillarr(int *arr)

如果你真的想要返回一个数组,你可以将那一行改为:

int * fillarr(int arr[]){
    // do something to arr
    return arr;
}

它实际上并没有返回一个数组,而是返回指向数组地址开始的指针。

但请记住,当你传入数组时,你只是传入指针。所以当你修改数组数据时,你实际上是在修改指针指向的数据。因此,在传入数组之前,你必须意识到你已经在外部获得了修改后的结果。

例如:

int fillarr(int arr[]){
   array[0] = 10;
   array[1] = 5;
}

int main(int argc, char* argv[]){
   int arr[] = { 1,2,3,4,5 };

   // arr[0] == 1
   // arr[1] == 2 etc
   int result = fillarr(arr);
   // arr[0] == 10
   // arr[1] == 5    
   return 0;
}

我建议你考虑在 fillarr 函数中加入类似于这样的长度限制。

int * fillarr(int arr[], int length)

这样你可以使用 length 将数组填充到它的长度,无论它是多少。

要正确地使用它,可以像这样做:

int * fillarr(int arr[], int length){
   for (int i = 0; i < length; ++i){
      // arr[i] = ? // do what you want to do here
   }
   return arr;
}

// then where you want to use it.
int arr[5];
int *arr2;

arr2 = fillarr(arr, 5);

// at this point, arr & arr2 are basically the same, just slightly
// different types.  You can cast arr to a (char*) and it'll be the same.

如果您只是想设置数组的默认值,请考虑使用内置的memset函数。

例如: memset((int*)&arr, 5, sizeof(int));

顺便说一下,您说您正在使用C ++。看一下使用STL向量的方法。您的代码可能更加健壮。

有很多教程。这里有一个可以让您了解如何使用它们。 http://www.yolinux.com/TUTORIALS/LinuxTutorialC++STL.html


使用std::copy而不是memset,它更安全、更容易。(而且如果不是更快的话,也同样快。) - GManNickG

8
这是一个相当老的问题,但我想要发表一下我的意见。虽然有很多答案,但没有一个清晰简明地展现所有可能的方法(不确定是否简明,因为这个问题有点失控了。TL;DR)。
我假设OP希望返回传入的数组而不进行复制,以便直接将其传递给调用者,从而使代码显得更加漂亮。
但是,使用这样的数组就等于让它衰变成指针,并让编译器像处理数组一样来处理它。如果你传递了一个类似这样的数组,函数期望它将有5个元素,但是你的调用者实际上传递了其他数字,那么这可能会导致微妙的错误。
有几种更好的处理方法。可以传递一个std::vector或std::array(不确定2010年时是否有std::array)。然后,您可以将该对象作为引用传递,而无需复制/移动该对象。
std::array<int, 5>& fillarr(std::array<int, 5>& arr)
{
    // (before c++11)
    for(auto it = arr.begin(); it != arr.end(); ++it)
    { /* do stuff */ }

    // Note the following are for c++11 and higher.  They will work for all
    // the other examples below except for the stuff after the Edit.

    // (c++11 and up)
    for(auto it = std::begin(arr); it != std::end(arr); ++it)
    { /* do stuff */ }

    // range for loop (c++11 and up)
    for(auto& element : arr)
    { /* do stuff */ }

    return arr;
}

std::vector<int>& fillarr(std::vector<int>& arr)
{
    for(auto it = arr.begin(); it != arr.end(); ++it)
    { /* do stuff */ }
    return arr;
}

但是,如果您坚持使用C数组,请使用一个模板来保留数组中有多少项的信息。

template <size_t N>
int(&fillarr(int(&arr)[N]))[N]
{
    // N is easier and cleaner than specifying sizeof(arr)/sizeof(arr[0])
    for(int* it = arr; it != arr + N; ++it)
    { /* do stuff */ }
    return arr;
}

除此之外,那看起来非常难看,而且很难读。我现在使用一些辅助工具来解决这个问题,这些工具在2010年还不存在,我也用于函数指针:
template <typename T>
using type_t = T;

template <size_t N>
type_t<int(&)[N]> fillarr(type_t<int(&)[N]> arr)
{
    // N is easier and cleaner than specifying sizeof(arr)/sizeof(arr[0])
    for(int* it = arr; it != arr + N; ++it)
    { /* do stuff */ }
    return arr;
}

这将把类型移动到人们期望的位置,使其更易读。当然,如果您只使用5个元素,则使用模板是多余的,因此您可以硬编码它:

type_t<int(&)[5]> fillarr(type_t<int(&)[5]> arr)
{
    // Prefer using the compiler to figure out how many elements there are
    // as it reduces the number of locations where you have to change if needed.
    for(int* it = arr; it != arr + sizeof(arr)/sizeof(arr[0]); ++it)
    { /* do stuff */ }
    return arr;
}

正如我所说,当时这个问题被问出来的时候,我的type_t<>技巧是行不通的。那时候你最好能做到的就是在结构体中使用一个类型:
template<typename T>
struct type
{
  typedef T type;
};

typename type<int(&)[5]>::type fillarr(typename type<int(&)[5]>::type arr)
{
    // Prefer using the compiler to figure out how many elements there are
    // as it reduces the number of locations where you have to change if needed.
    for(int* it = arr; it != arr + sizeof(arr)/sizeof(arr[0]); ++it)
    { /* do stuff */ }
    return arr;
}

这看起来又有些难看了,但至少还是比较可读的,尽管在那个时候,依赖于编译器,typename可能是可选的,导致以下结果:

type<int(&)[5]>::type fillarr(type<int(&)[5]>::type arr)
{
    // Prefer using the compiler to figure out how many elements there are
    // as it reduces the number of locations where you have to change if needed.
    for(int* it = arr; it != arr + sizeof(arr)/sizeof(arr[0]); ++it)
    { /* do stuff */ }
    return arr;
}

当然,您可以指定特定的类型,而不是使用我的帮助程序。
typedef int(&array5)[5];

array5 fillarr(array5 arr)
{
    // Prefer using the compiler to figure out how many elements there are
    // as it reduces the number of locations where you have to change if needed.
    for(int* it = arr; it != arr + sizeof(arr)/sizeof(arr[0]); ++it)
    { /* do stuff */ }
    return arr;
}

过去,免费函数std::begin()std::end()并不存在,尽管很容易实现。这将使得在C数组上迭代更加安全,但在指针上却不合适。

至于访问该数组,您可以将其传递给另一个接受相同参数类型的函数,或对其进行别名处理(这似乎没有太大意义,因为你已经在该作用域中有原始数组)。访问数组引用就像访问原始数组一样。

void other_function(type_t<int(&)[5]> x) { /* do something else */ }

void fn()
{
    int array[5];
    other_function(fillarr(array));
}

或者

void fn()
{
    int array[5];
    auto& array2 = fillarr(array); // alias. But why bother.
    int forth_entry = array[4];
    int forth_entry2 = array2[4]; // same value as forth_entry
}

总之,如果您打算迭代数组,则最好不要将其降解为指针。这只会让编译器无法保护您免受自己的错误,并使您的代码更难阅读。始终尝试通过尽可能长时间地保留类型来帮助编译器帮助您,除非您有非常充分的理由不这样做。
另外,为了完整起见,您可以允许它退化为指针,但这会使数组与其所包含的元素数量分离。在C/C++中经常使用此方法,并且通常通过传递数组中元素的数量来减轻影响。但是,如果您犯了错误并向元素数量传递了错误的值,则编译器无法帮助您。
// separate size value
int* fillarr(int* arr, size_t size)
{
    for(int* it = arr; it != arr + size; ++it)
    { /* do stuff */ }
    return arr;
}

不需要传递数组的大小,而是可以传递一个指向数组末尾后一位的指针。这样做有助于使代码更接近于标准算法中使用的begin和end指针,但返回的只是一个必须记住的内容。

// separate end pointer
int* fillarr(int* arr, int* end)
{
    for(int* it = arr; it != end; ++it)
    { /* do stuff */ }
    return arr;
}

另外一种方法是记录下该函数只能够使用5个元素,并希望调用该函数的用户不会做出任何愚蠢的操作。

// I document that this function will ONLY take 5 elements and 
// return the same array of 5 elements.  If you pass in anything
// else, may nazal demons exit thine nose!
int* fillarr(int* arr)
{
    for(int* it = arr; it != arr + 5; ++it)
    { /* do stuff */ }
    return arr;
}

请注意,返回值已经失去了其原始类型,并降级为指针。因此,您现在必须自己确保不会超出数组范围。
您可以传递一个 std::pair<int*,int*>,用于开始和结束,并在周围传递它,但这样看起来就不像一个数组了。
std::pair<int*, int*> fillarr(std::pair<int*, int*> arr)
{
    for(int* it = arr.first; it != arr.second; ++it)
    { /* do stuff */ }
    return arr; // if you change arr, then return the original arr value.
}

void fn()
{
    int array[5];
    auto array2 = fillarr(std::make_pair(&array[0], &array[5]));

    // Can be done, but you have the original array in scope, so why bother.
    int fourth_element = array2.first[4];
}

或者

void other_function(std::pair<int*, int*> array)
{
    // Can be done, but you have the original array in scope, so why bother.
    int fourth_element = array2.first[4];
}

void fn()
{
    int array[5];
    other_function(fillarr(std::make_pair(&array[0], &array[5])));
}

有趣的是,这与 std :: initializer_list 的工作方式非常相似(c ++ 11),但它们在此上下文中不起作用。

我不知道你怎么看,但对我来说,下面的代码看起来要好看得多,而且在现代C++中是可行的,这也是你回答的前提条件:template <size_t N> auto fillarr(int (&arr)[N]) -> int(&)[N]。虽然你的技巧很巧妙,但在这种情况下是不必要的,而且在我看来,它再次降低了代码的可读性,尤其是在表达这些类型时。 - undefined
1
是的@0xC0000022L,使用尾返回类型语法实际上是我没有考虑到的,这是一种使定义更清晰的好方法。但是它确实将arr放在参数类型规范的中间。这是一个个人偏好的问题。 - undefined
反正不管怎样,它不就是在中间吗?但是是的,我觉得这是个偏好的问题。 - undefined
@0xC0000022L,中间的 int (&arr)[N] 和最后的 type_t<int(&)[N]> arr 有所不同。 - undefined
1
我明白了,所以你特别指的是名字和它的位置。 - undefined

5
为了从一个函数中返回一个数组,我们可以定义一个包含该数组的结构体;这样看起来就像这样:
struct Marks{
   int list[5];
}

现在让我们创建结构类型的变量。
typedef struct Marks marks;
marks marks_list;

我们可以按照以下方式将数组传递给函数并为其赋值:
void setMarks(int marks_array[]){
   for(int i=0;i<sizeof(marks_array)/sizeof(int);i++)
       marks_list.list[i]=marks_array[i];
}

我们也可以返回数组。为了返回数组,函数的返回类型应该是结构体类型,即marks。这是因为实际上我们传递的是包含数组的结构体。因此,最终代码可能如下所示。

marks getMarks(){
 return marks_list;
}

2

最简单的方法是通过引用返回它,即使你不写'&'符号,它也会自动被引用返回。

     void fillarr(int arr[5])
  {
       for(...);

  }

那不是一个引用。那是一个指针。试着将不同大小的数组传递给fillarr。它会起作用的。 - undefined

2
int *fillarr(int arr[])

你仍然可以像以前一样使用结果。
int *returned_array = fillarr(some_other_array);
if(returned_array[0] == 3)
    do_important_cool_stuff();

我认为“int [] fillarr ...”是不合法的。由于数组指针等价性,应使用“int *fillarr”。 - seand

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