前言
C 语言的指针让我们有了直接操控内存的强大能力, 同时指针也是使用 C 语言时最容易出问题的地方. C++ 在继承了 C 语言的指针的同时又给我们提供了另外一个武器: 引用. 今天我们来探讨一下指针与引用的异同以及两者之间的性能差异.
指针与引用的区别
引用与指针的区别也算是 C++ 中老生常谈的话题了, 无论是在期末考试的试卷上还是找工作时的笔试面试上, 这个问题都是 "常客". 对于这个问题更加详细的解答请参考More Effective C++的条款 1, 本文主要提一下以下三个区别:
引用必须初始化, 而指针可以不初始化.
我们在定义一个引用的时候必须为其指定一个初始值, 但是指针却不需要.
- int &r; // 不合法, 没有初始化引用
- int *p; // 合法, 但 p 为野指针, 使用需要小心
引用不能为空, 而指针可以为空.
由于引用不能为空, 所以我们在使用引用的时候不需要测试其合法性, 而在使用指针的时候需要首先判断指针是否为空指针, 否则可能会引起程序崩溃.
- void test_p(int* p)
- {
- if(p != null_ptr) // 对 p 所指对象赋值时需先判断 p 是否为空指针
- *p = 3;
- return;
- }
- void test_r(int& r)
- {
- r = 3; // 由于引用不能为空, 所以此处无需判断 r 的有效性就可以对 r 直接赋值
- return;
- }
引用不能更换目标
指针可以随时改变指向, 但是引用只能指向初始化时指向的对象, 无法改变.
- int a = 1;
- int b = 2;
- int &r = a; // 初始化引用 r 指向变量 a
- int *p = &a; // 初始化指针 p 指向变量 a
- p = &b; // 指针 p 指向了变量 b
- r = b; // 引用 r 依然指向 a, 但 a 的值变成了 b
引用的使用场景
只看两者区别的话, 我们发现引用可以完成的任务都可以使用指针完成, 并且在使用引用时限制条件更多, 那么 C++ 为什么要引入 "引用" 呢?
限制条件多不一定是缺点, C++ 的引用在减少了程序员自由度的同时提升了内存操作的安全性和语义的优美性. 比如引用强制要求必须初始化, 可以让我们在使用引用的时候不用再去判断引用是否为空, 让代码更加简洁优美, 避免了指针满天飞的情形. 除了这种场景之外引用还用于如下两个场景:
引用型参数
一般我们使用 const reference 参数作为只读形参, 这种情况下既可以避免参数拷贝还可以获得与传值参数一样的调用方式.
- void test(const vector<int> &data)
- {
- //...
- }
- int main()
- {
- vector<int> data{1,2,3,4,5,6,7,8};
- test(data);
- }
引用型返回值
C++ 提供了重载运算符的功能, 我们在重载某些操作符的时候, 使用引用型返回值可以获得跟该操作符原来语法相同的调用方式, 保持了操作符语义的一致性. 一个例子就是 operator []操作符, 这个操作符一般需要返回一个引用对象, 才能正确的被修改.
- vector<int> v(10);
- v[5] = 10; //[]操作符返回引用, 然后 vector 对应元素才能被修改
- // 如果 [] 操作符不返回引用而是指针的话, 赋值语句则需要这样写
- *v[5] = 10; // 这种书写方式, 完全不符合我们对 [] 调用的认知, 容易产生误解
指针与引用的性能差距
指针与引用之间有没有性能差距呢? 这种问题就需要进入汇编层面去看一下. 我们先写一个 test1 函数, 参数传递使用指针:
- void test1(int* p)
- {
- *p = 3; // 此处应该首先判断 p 是否为空, 为了测试的需要, 此处我们没加.
- return;
- }
该代码段对应的汇编代码如下:
- pushq %rbp
- movq %rsp, %rbp
- movq %rdi, -8(%rbp)
- movq -8(%rbp), %rax
- movl $3, (%rax)
- nop
- popq %rbp
- ret
上述代码 1,2 行是参数调用保存现场操作; 第 3 行是参数传递, 函数调用第一个参数一般放在 rdi 寄存器, 此行代码把 rdi 寄存器值 (指针 p 的值) 写入栈中; 第 4 行是把栈中 p 的值写入 rax 寄存器; 第 5 行是把立即数 3 写入到 rax 寄存器值所指向的内存中, 此处要注意 (%rax) 两边的括号, 这个括号并并不是可有可无的,(%rax)和 %rax 完全是两种意义,(%rax)代表 rax 寄存器中值所代表地址部分的内存, 即相当于 C++ 代码中的 * p, 而 %rax 代表 rax 寄存器, 相当于 C++ 代码中的 p 值, 所以汇编这里使用了 (%rax) 而不是 %rax.
我们再写出参数传递使用引用的 C++ 代码段 test2:
- void test2(int& r)
- {
- r = 3; // 赋值前无需判断 reference 是否为空
- return;
- }
这段代码对应的汇编代码如下:
- pushq %rbp
- movq %rsp, %rbp
- movq %rdi, -8(%rbp)
- movq -8(%rbp), %rax
- movl $3, (%rax)
- nop
- popq %rbp
- ret
我们发现 test2 对应的汇编代码和 test1 对应的汇编代码完全相同, 这说明 C++ 编译器在编译程序的时候将指针和引用编译成了完全一样的机器码. 所以 C++ 中的引用只是 C++ 对指针操作的一个 "语法糖", 在底层实现时 C++ 编译器实现这两种操作的方法完全相同.
总结
C++ 中引入了引用操作, 在对引用的使用加了更多限制条件的情况下, 保证了引用使用的安全性和便捷性, 还可以保持代码的优雅性. 在适合的情况使用适合的操作, 引用的使用可以一定程度避免 "指针满天飞" 的情况, 对于提升程序鲁棒性也有一定的积极意义. 最后, 指针与引用底层实现都是一样的, 不用担心两者的性能差距.
来源: https://juejin.im/entry/5b14f560f265da6e053ac0c8