目录
引用的概念和基本用法
在C++中,引用是一种特殊的变量类型,它提供了一种简单而有效的方法来操作其他变量的值。引用是一个别名,它引用了另一个变量的地址。引用在定义时必须初始化,并且一旦初始化后就不能再引用其他变量。
引用的定义方式是在变量名前面加上&符号,例如:
int a = 10;
int &b = a;
这里的&符号表示b是a的引用,也就是b和a指向同一块内存地址,对b的操作会直接影响到a的值。
引用特性
- 引用在定义时必须初始化
- 一个变量可以有多个引用
- 引用一旦引用一个实体,再不能引用其他实体
左值引用
左值引用(Lvalue reference)是最常见的引用类型,它是一种指向左值的引用,可以绑定到具有名称的对象,例如变量、数组元素、成员变量等。
左值引用的定义方式是在变量名前面加上&符号,例如:
int a = 10;
int& b = a;
这里的&符号表示b是a的左值引用,也就是b和a指向同一块内存地址,对b的操作会直接影响到a的值。
左值引用的主要用途有以下几个方面:
-
作为函数参数传递,可以避免复制大型对象而带来的性能开销。在函数中使用左值引用参数,可以直接修改实参的值,从而达到修改函数外部变量的目的。
-
在函数返回值中使用左值引用,可以使函数返回一个左值,从而可以继续对其进行操作。例如:
int& func(int& a) { return a; } int b = 10; func(b) = 20; // b的值变为20
这里的func函数返回b的左值引用,因此可以对其进行赋值操作。
需要注意的是,左值引用只能绑定到左值,不能绑定到右值。如果试图将左值引用绑定到右值,编译器会报错。此外,左值引用在使用时需要注意避免悬空引用的问题,即引用的对象已经被销毁而引用仍然存在的情况。
常引用
常引用(const reference)是一种特殊的引用类型,它可以绑定到一个常量对象或表达式的结果,并且不能修改所引用的对象的值。常引用的定义方式是在变量名前面加上const关键字和&符号,例如:
const int& a = 10;
这里的const关键字表示a是一个常量引用,不能通过a修改其所引用的对象的值。常引用主要用于函数参数传递和返回值类型,可以避免复制对象而带来的性能开销。
常引用的主要用途有以下几个方面:
-
作为函数参数传递,可以避免复制大型对象而带来的性能开销,并且可以保证函数不会修改实参的值。在函数中使用常引用参数,可以直接访问实参的值,但不能修改实参的值。
-
在函数返回值中使用常引用,可以避免复制大型对象而带来的性能开销,并且可以保证函数返回的对象不会被修改。在函数中返回常引用,可以直接返回函数外部变量的引用,但不能修改其值。
需要注意的是,常引用只能绑定到常量对象或表达式的结果,不能绑定到非常量对象。如果试图将常引用绑定到非常量对象,编译器会报错。此外,常引用在使用时需要注意避免悬空引用的问题,即引用的对象已经被销毁而引用仍然存在的情况。
右值引用
在C++11中,引入了右值引用(Rvalue reference)的概念。右值引用是一种特殊的引用类型,它主要用于移动语义和完美转发。
右值引用的定义方式是在变量名前面加上&&符号,例如:
int&& a = 10;
这里的&&符号表示a是一个右值引用,它可以绑定到一个临时对象或表达式的结果。与左值引用不同,右值引用不能绑定到左值,例如:
int b = 20;
int&& c = b; // 错误,不能将右值引用绑定到左值
右值引用的主要用途有以下几个方面:
-
移动语义:右值引用可以用于实现移动构造函数和移动赋值运算符,从而避免不必要的内存拷贝操作,提高程序的性能。
-
完美转发:右值引用可以用于实现完美转发,即在函数中将参数按原样转发给其他函数,从而避免不必要的拷贝操作,提高程序的性能。
需要注意的是,右值引用的生命周期通常很短,只在表达式求值期间存在,因此不能将其用于存储对象的地址或引用。此外,右值引用在使用时需要注意避免悬空引用的问题,即引用的对象已经被销毁而引用仍然存在的情况。
传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
以下是值和引用的作为返回值类型的性能比较,传参比较也是类似的
#include<iostream>
using namespace std;
#include <time.h>
struct A { int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a; }
// 引用返回
A& TestFunc2() { return a; }
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}
int main()
{
TestReturnByRefOrValue();
return 0;
}
可以看到,返回值和返回引用的效率差别在返回对象越大区别越明显
引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。
我们可以把以下代码反汇编看一下
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
以下是反汇编代码,
int& ra = a;
006C201C lea eax,[a]
006C201F mov dword ptr [ra],eax
ra = 20;
006C2022 mov eax,dword ptr [ra]
006C2025 mov dword ptr [eax],14h
int* pa = &a;
006C202B lea eax,[a]
006C202E mov dword ptr [pa],eax
*pa = 20;
006C2031 mov eax,dword ptr [pa]
006C2034 mov dword ptr [eax],14h
可以看到,引用底层还是开辟了一块空间来存储地址,使用引用来改值时,还是通过定义引用时存储的地址来对a的内存空间进行修改,和指针的实现方式类似。