首页 > 解决方案 > 指针与参考

问题描述

我知道这已经是一个常见问题,但我更好奇指针和引用在较低级别的行为(例如编译器如何处理它们,以及它们在内存中的样子),我没有找到解决方案,所以我在这里。

起初我想知道是否可以将数组作为参数传递而不被强制转换(或衰减)到指针中。进一步来说。我想要以下代码:

void func(?? arr) {
    cout << sizeof(arr) << "\n";
}

int main() {
    int arr[4];
    func(arr);
    return 0;
}

输出16而不是8,它是指针的大小。

首先我试过

void func(int arr[4]);

希望指定大小可以保留数组的属性,但arr仍然被视为指针。

然后我发现了一些有用的东西:

void func(int (&arr)[4]);

但这让我很困惑。

过去我的印象是,虽然指针和引用有不同的含义,但在代码实际执行时它们的行为是相同的。

我从自己的实验中得到了这个想法:

void swap(int* a, int* b) {
    int c = *a;
    *a = *b;
    *b = c;
}
int main() {
    int a = 3, b = 5;
    swap(&a, &b);
}

void swap(int& a, int& b) {
    int c = a;
    a = b;
    b = c;
}
int main() {
    int a = 3, b = 5;
    swap(a, b);
}

被编译成相同的汇编代码,也是如此

int main() {
    int a = 3;
    int& b = a;
    b = 127;
    return 0;
}

int main() {
    int a = 3;
    int* b = &a;
    *b = 127;
    return 0;
}

我关闭了优化,g++ 和 clang++ 都显示了这个结果。

还有我对第一个实验的想法:

在考虑内存时,swap应该有自己的堆栈帧和局部变量。将ainswap直接映射到ainmain对我来说没有多大意义。就好像ainmain神奇地出现在swap它不应该属于的堆栈帧中。所以它被编译成与指针版本相同的程序集并不让我感到惊讶。也许引用的魔力是通过引擎盖下的指针实现的。但现在我不确定。

那么编译器如何处理引用以及引用在内存中的样子呢?它们是占据空间的变量吗?我该如何解释我的实验结果和数组问题?

标签: c++pointersreference

解决方案


旧的 C 习惯用法是,当调用函数时,数组会变成指向数组类型的指针。对于 C++ 来说,这在很大程度上仍然是正确的。

所以对于它的细节,让我们看两个接受整数参数的函数,首先:

void f(int x) { x = 42; }              // function called with x by value
void g(int& x) { x = 42; }             // function called with x by reference

f()按值调用时,会生成参数的副本,并且x = 42;函数内部的赋值实际上对函数的调用者没有影响。但是对于,调用者可以看到g()的赋值:x

int a = 0, b = 0;                        // initialize both a and b to zero
f(a);                                    // a is not changed, because f is called by value
g(b);                                    // b is changed, because g is called by reference
std::cout << a << " " << b << std::endl; // prints 0 42

对于数组,同样的规则应该成立,但不成立。所以让我们尝试一下:

void farr(int x[4]) { x[0] = 42; }       // hypothetical call by value
void garr(int (&x)[4]) { x[0] = 42; }    // call by reference

让我们调用这些函数:

int c[4] = { 1, 2, 3, 4 }, d[4] = { 5, 6, 7, 8 };
farr(c);                        // call by value?
garr(d);                        // call by reference
for(unsigned i = 0; i < 4; i++)
    std::cout << c[i] << (i < 3 ? ", " : "\n");
for(unsigned i = 0; i < 4; i++)
    std::cout << d[i] << (i < 3 ? ", " : "\n");

出乎意料的结果是,该farr()函数(与f之前的函数不同)也确实修改了它的数组。原因是一个古老的 C 习惯用法:因为数组不能通过赋值直接复制,所以不能用数组按值调用函数。因此,参数列表中的数组声明会自动转换为指向数组第一个元素的指针。因此,以下四个函数声明在 C 中的语法是相同的:

void farr1(int a[]) {}
void farr2(int a[4]) {}
void farr3(int a[40]) {}
void farr4(int *a) {}

由于与 C 兼容,C++ 接管了该属性。因此,调用farr2()farr3()使用不同大小的数组不是语法错误(但可能导致未定义的行为!)。此外,尽管如此,引用也出现在 C++ 中。是的,正如您已经怀疑的那样,对数组的引用在内部表示为指向第一个数组元素的指针。但是,这就是 C++ 的优势:如果你调用一个函数,它需要一个数组的引用(而不仅仅是一个数组或指针),数组的大小实际上是经过验证的!

因此,farr()使用大小为 5 的 int-Array 进行调用是可能的,garr()使用相同的调用会导致编译器错误。这为您提供了更好的类型检查,因此您应该尽可能使用它。它甚至允许您使用模板将数组大小传递给函数:

template<std::size_t N>
void harr(int (&x)[N]) { for(std::size_t i = 0; i < N; i++) x[i] = i*i; }

推荐阅读