假设:
int i = 42;
int j = 43;
int k = 44;
通过查看变量地址,我们知道每个地址占用4个字节(在大多数平台上)。
然而,考虑:
int i = 42;
int& j = i;
int k = 44;
我们将看到变量i
确实需要4个字节,但j
需要 none 而k
再次占用堆栈上的4个字节。
这里发生了什么?看起来j
在运行时根本就不存在。那么我作为函数参数收到的引用呢? 必须在堆栈上占用一些空间......
虽然我们在这里 - 为什么我不能定义数组或引用?
int&[] arr = new int&[SIZE]; // compiler error! array of references is illegal
答案 0 :(得分:43)
在遇到引用 j 的任何地方,都会将其替换为 i的地址。所以基本上参考内容地址在编译时解析,并且不需要像运行时的指针那样取消引用它。
只是为了澄清我的意思是我的地址:
void function(int& x)
{
x = 10;
}
int main()
{
int i = 5;
int& j = i;
function(j);
}
在上面的代码中, j 不应占用 主堆栈 的空间,但引用 x 功能将在其堆栈中占据一席之地。这意味着当使用 j 作为参数调用函数时, 函数的堆栈上将推送的地址 。编译器可以而且不应该在主要堆栈上为 j 预留空间。
对于数组部分,标准说::
C ++标准8.3.2 / 4:
不应引用引用,也不引用引用数组, 并没有指向参考的指针。
答案 1 :(得分:37)
C ++参考如何看, 存储器明智?
没有。 C ++标准只说明它应该如何表现,而不是它应该如何实现。
在一般情况下,编译器通常将引用实现为指针。但是它们通常有更多关于引用可能指向的信息,并将其用于优化。
请记住,引用的唯一要求是它表现为引用对象的别名。因此,如果编译器遇到此代码:
int i = 42;
int& j = i;
int k = 44;
它看到的不是“创建指向变量i
的指针”(虽然这是编译器在某些情况下可能选择实现它的方式),而是“在符号表中记下{ {1}}现在是j
的别名。“
编译器不必为i
创建一个新变量,它只需要记住,从现在开始引用j
时,它应该真正交换它并使用{{1相反。
至于创建一个引用数组,你不能这样做,因为它没用,毫无意义。
创建数组时,所有元素都是默认构造的。默认构造引用意味着什么?它指向什么?引用中的重点是它们初始化以引用另一个对象,之后它们无法重新定位。
因此,如果可以完成,最终会得到一个对 nothing 的引用数组。并且您无法更改它们以引用某些内容,因为它们已经初始化。
答案 2 :(得分:9)
实际上,引用等同于指针,除了允许使用引用的额外约束可以允许编译器在更多情况下“优化它”(取决于编译器的智能程度,它的优化设置等当然)。
答案 3 :(得分:9)
很抱歉使用程序集来解释这一点,但我认为这是了解参考资料的最佳方法。
#include <iostream>
using namespace std;
int main()
{
int i = 10;
int *ptrToI = &i;
int &refToI = i;
cout << "i = " << i << "\n";
cout << "&i = " << &i << "\n";
cout << "ptrToI = " << ptrToI << "\n";
cout << "*ptrToI = " << *ptrToI << "\n";
cout << "&ptrToI = " << &ptrToI << "\n";
cout << "refToI = " << refToI << "\n";
//cout << "*refToI = " << *refToI << "\n";
cout << "&refToI = " << &refToI << "\n";
return 0;
}
此代码的输出就像这样
i = 10
&i = 0xbf9e52f8
ptrToI = 0xbf9e52f8
*ptrToI = 10
&ptrToI = 0xbf9e52f4
refToI = 10
&refToI = 0xbf9e52f8
让我们看一下反汇编(我为此使用了GDB。这里的8,9和10是代码行号)
8 int i = 10;
0x08048698 <main()+18>: movl $0xa,-0x10(%ebp)
此处$0xa
是我们分配给i
的10(十进制)。 -0x10(%ebp)
此处表示ebp register
-16(十进制)的内容。
-0x10(%ebp)
指向堆栈上i
的地址。
9 int *ptrToI = &i;
0x0804869f <main()+25>: lea -0x10(%ebp),%eax
0x080486a2 <main()+28>: mov %eax,-0x14(%ebp)
将i
的地址指定给ptrToI
。 ptrToI
位于地址-0x14(%ebp)
的堆栈上,即ebp
- 20(十进制)。
10 int &refToI = i;
0x080486a5 <main()+31>: lea -0x10(%ebp),%eax
0x080486a8 <main()+34>: mov %eax,-0xc(%ebp)
现在抓住了!比较第9行和第10行的反汇编,您将观察到,-0x14(%ebp)
被第10行中的-0xc(%ebp)
替换。-0xc(%ebp)
是refToI
的地址。它在堆栈上分配。但是,您永远无法从代码中获取此地址,因为您无需知道地址。
因此;引用确实占用了内存。在这种情况下,它是堆栈内存,因为我们已将其分配为局部变量。 它占用了多少内存? 指针占据了很多。
现在让我们看看我们如何访问引用和指针。为简单起见,我只展示了汇编代码段的一部分
16 cout << "*ptrToI = " << *ptrToI << "\n";
0x08048746 <main()+192>: mov -0x14(%ebp),%eax
0x08048749 <main()+195>: mov (%eax),%ebx
19 cout << "refToI = " << refToI << "\n";
0x080487b0 <main()+298>: mov -0xc(%ebp),%eax
0x080487b3 <main()+301>: mov (%eax),%ebx
现在比较以上两行,你会看到惊人的相似性。 -0xc(%ebp)
是refToI
的实际地址,您永远无法访问该地址。
简单来说,如果您将引用视为普通指针,那么访问引用就像获取引用指向的地址处的值一样。这意味着以下两行代码将为您提供相同的结果
cout << "Value if i = " << *ptrToI << "\n";
cout << " Value if i = " << refToI << "\n";
现在比较一下
15 cout << "ptrToI = " << ptrToI << "\n";
0x08048713 <main()+141>: mov -0x14(%ebp),%ebx
21 cout << "&refToI = " << &refToI << "\n";
0x080487fb <main()+373>: mov -0xc(%ebp),%eax
我想你能发现这里发生了什么。
如果您要求&refToI
,则会返回-0xc(%ebp)
地址位置的内容,-0xc(%ebp)
位于refToi
所在的位置,其内容只是i
的地址。
最后一件事,为什么这一行被评论了?
//cout << "*refToI = " << *refToI << "\n";
因为不允许*refToI
,它会给你一个编译时错误。
答案 4 :(得分:8)
您无法定义引用数组,因为没有语法来初始化它们。 C ++不允许未初始化的引用。至于你的第一个问题,编译器没有义务为不必要的变量分配空间。没有办法让j指向另一个变量,因此它实际上只是函数范围内i的别名,而这就是编译器对待它的方式。
答案 5 :(得分:6)
在其他地方提到的东西 - 如何让编译器将一些存储空间用于引用:
class HasRef
{
int &r;
public:
HasRef(int &n)
: r(n) { }
};
这使编译器无法简单地将其视为编译时别名(同一存储的替代名称)。
答案 6 :(得分:3)
在他们需要物理表现形式(即作为聚合的成员)之前,参考文献实际上并不存在。
由于上述原因,有一系列参考文献是非法的。但没有什么能阻止你创建一个具有引用成员的结构/类数组。
我确信有人会指出提及这一切的标准条款。
答案 7 :(得分:3)
它没有修复 - 编译器在如何根据具体情况实现引用方面有很大的自由度。所以在你的第二个例子中,它将j视为i的别名,不需要其他任何东西。传递ref参数时,它也可以使用堆栈偏移量,同样没有开销。但在其他情况下,它可以使用指针。