【问题标题】:Pointer vs Reference指针与参考
【发布时间】:2021-08-03 03:42:19
【问题描述】:

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

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

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

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

【问题讨论】:

  • 你的问题实际上是几个不同问题的组合,每个问题都是重复的。职业培训中心。
  • 听起来你可以使用good C++ book。他们应该深入研究这一点
  • 指针和引用如何在底层工作是一个实现细节。通常,它们都通过存储内存地址来工作。您的数组参考代码给出不同大小的原因与这些细节无关。该行为是标准规定的,并且无论实施如何都将以这种方式工作。您看到的实际值可能会有所不同,但带有数组参数的示例将始终返回指针的大小,而引用数组的示例将始终返回实际数组的大小。
  • int arr[4] 这样的函数参数是语言告诉你的一个大谎言。它是从 C 继承的一种行为,为了向后兼容而保留。数组类型参数始终只是变相的指针。它与int * arr 相同,这就是为什么您获得指针大小而不是数组大小的原因。这会引起很多混乱,特别是导致新用户认为数组只是指针。
  • 旁注:C++ 不需要堆栈。它们只是另一个实现细节,尽管是迄今为止最常见的实现细节。

标签: c++ pointers reference


【解决方案1】:

旧的 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++ 的优势:如果你调用一个函数,它需要一个数组的引用(而不仅仅是一个数组或一个指针),数组的大小实际上是经过验证的!

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

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

【讨论】:

  • “用不同大小的数组调用 g() 或 h()”我认为你的意思是 farrharr。而且我认为通过传递不同大小的数组是不可能导致UB的,尽管我对数组了解得越多,对数组的了解就越少。
  • 是的,感谢您指出该错误 - 我的意思是 farr2() 和 farr3()。我会纠正的。如果传递不同的数组大小,很容易导致段错误和其他未定义的行为:如果一个函数需要一个大小为 10 的数组并修改该数组的所有元素,但只传入一个大小为 5 的数组,那么您将损坏堆栈或堆。
  • 啊好吧,现在我明白你的意思了。我以为你的意思是仅仅传递参数可能会导致 ub。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-01-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多