数组参数传递时为什么会退化成指针以及怎么解决?

3,516 阅读4分钟

数组退化为指针的现象

在C++传递函数参数时经常都有这样子的一个现象:

把一个数组传给子函数的时候,我们经常不得不传一个参数n代表数组中含有多少个元素。这样子在子函数中我们才能知道这个数组的大小。

我们先做个实验看看,为什么在这种情况下,我们不得不传参数n:

#include <iostream>
void fun(int arr[10])
{
    std::cout << sizeof(arr)<<std::endl;
}
 
int main()
{
    int array[] = {1,2,3,4,5,6,7,8,9,10};
    fun(array);  //在64位机器下cout:8,在64位机器下cout:4
    return 0;
}

可以看到,数组在传进子函数的时候,变成了一个指针,正所谓数组转化为指针。所以sizeof(arr)的结果就是指针的大小,在64位机器下地址的长度是64位8个字节,在32位机器下地址的长度是32位4个字节。对应了程序的输出。可是为什么数组会转化为指针呢,数组传参的时候到底是怎么传的?

数组传参的时候到底是怎么传的?

我们通过下面的代码,增加输出,来探索一下指针的本质是什么:

#include <iostream>
void fun(int arr[10])
{
    std::cout<<&arr<<std::endl; //0x61fdb0
    std::cout<<arr<<std::endl; //0x61fdf0
}

int main()
{

    int array[] = {1,2,3,4,5,6,7,8,9,10};
    std::cout<<array<<std::endl;  //0x61fdf0
    fun(array);  
    return 0;
}

可以看到,我们将数组array传到子函数fun()里面去,然后我们做了两个输出:

  1. 查看子函数中arr指针这个变量本身的地址是多少:&arr
  2. 查看子函数中arr指针这个变量本身的内容是多少:arr

两个结果是不相同的,具体的内容我们可以通过下面这张图来讲解:

在0x61db0这个地址里面,存了一个变量,他是一个指针变量叫arr,而这个变量它的值(即内容)是一个地址0x61fdf0。在0x61fdf0地址是一个长度为10的数组的第一个元素,之后的元素以此类推+4个字节(因为int类型占4个字节)

回到我们一开始的问题:数组传参的时候到底是怎么传的。

在传参的时候,把这个数组头部的地址(0x61fdf0)按值传递,拷贝给子函数。子函数新建了一个变量(例子中的fun()中的arr,地址为0x61fdb0),是一个指针,里面存放了我们接收到的数组首地址(0x61fdf0)。

由此可以得出:函数在传数组参数的时候,实际上是创建了一个新的指针来指向数组,(相当于找了一个中间人,中间人知道数组在哪里,每次子函数访问和修改数组的时候,都是通过中间人来做到的)因而指针和原本的数组已经不是一回事情。

那么我们有没有解决方法呢?

数组参数传递时如何防止数组退化成指针?

既然我们知道了,在传参的时候,实际上是经过了一轮拷贝,产生了中间人。但是我们想要本来的数组该怎么办呢? 这个思想跟引用是一致的:引用的作用就在于阻止拷贝的发生,像我们想要修改一个普通类型变量的值,我们就会传递引用。

所以这里我们想要原本的数组地址,我们也可以传递引用

void fun2(int (&arr)[10])
{
    std::cout<<&arr<<std::endl;  //0x61fdf0
    std::cout<<arr<<std::endl;   //0x61fdf0
    std::cout<<sizeof(arr)<<std::endl;  //40
    
}
int main()
{

    int array[] = {1,2,3,4,5,6,7,8,9,10};
    std::cout<<array<<std::endl;  //0x61fdf0
    fun2(array); 
    
    return 0;
}

通过传递引用,我们让形参得到和数组名同样的地址。消除了拷贝的发生,可以看到sizeof(arr)=40,数组仍然是数组。

但是这样子又有一个新的问题:这种情况下,我们给函数传数组的时候,只能传和形参一样固定大小的数组。如果不一致的话,就会报错。

这个问题可以用模板来解决:

#include <iostream>

template<int T>
void print(int (&arr)[T])
{
    for(auto i:arr)
    {
        std::cout<<i<<" ";
    }
}

int main()
{
    int a[]={1,2,3,4,5};
    int b[]={1,3,4};
    print(a); //1,2,3,4,5
    std::cout<<std::endl;
    print(b); //1,3,4
    return 0;
}

通过模板,我们引进了一个int的非类型参数(一个非类型参数表示一个值而非一个类型),当调用print函数时,编译器就会从实参中推断出参数数组的大小T,并且实例化对应的函数模板。

文章灵感由来:

C++ Primer Edition 5th p195、p580