1.引用概念
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共同使用同一块内存空间。
例如,你小时候家里人叫你的小名“二宝”,上学了同学们叫你的绰号“菠萝头”,“二宝”和“菠萝头”都是你的别名。
用法:类型& 引用变量名(对象名) = 引用实体
void TestRef()
{
int a = 10;
int& ra = a;//定义引用类型
printf("%p\n", &a);
printf("%p\n", &ra);
}
在VS2013下的运行结果表明,a与ra共用同一块内存空间。
2.引用特性
- 引用在定义时必须初始化。引用变量在定义时就必须要确定是给谁取的别名,否则其所在的内存空间是无法确定的,也就无法创建出来。
- 一个变量可以有多个引用。例如人也可以有多个别名(绰号)。
- 引用一旦引用了一个实体,再不能引用其他实体。
void TestRef()
{
int a = 10;
// int& ra; // 该条语句编译时会出错
int& ra = a;
int& rra = a;
printf("%p %p %p\n", &a, &ra, &rra);
}
运行结果表明a的引用ra和rra与a共用同一块内存空间
3.常引用
在C++中,const修饰的变量具有宏替换的特性,在编译时会替换成对应的常量。
例1:
void TestConstRef()
{
const int a = 10;
int& ra = a; // 该语句编译时会出错,a为常量
const int& ra = a;//通过
}
例2:
void TestConstRef()
{
int& b = 10; // 该语句编译时会出错,b为常量
const int& b = 10;//通过
}
结论1:在引用常量时,必须在类型前用const修饰**
例3:
void TestConstRef()
{
double d = 12.34;
int& rd = d; // 该语句编译时会出错,类型不同
const int& rd = d;//而这样却可以通过?
}
在例3中,当使用const int类型引用double的实体时,编译器能通过,并且提示:从“double”转换到“const int”,可能丢失数据。说明这样的做法是支持的,只是不推荐。
于是我想,如果rd既然是d的引用,那么他们应该共用同一块内存空间,打印的地址应该是相同的,而结果却大失所望。
rd是const int类型的,其只取了d类型的整数部分12,并存储在另一个空间内,而不是指向d所在的空间,那不就与我们之前所接触到的概念相违背了吗,要理解这点我们就要知道C++的编译器在底层是如何处理引用的。参考本文第7点(引用和指针的区别)
结论2:引用类型必须与实体类型相同
4.使用场景
4.1 引用做函数参数
可以起到传指针的作用,对外部实参进行操作。注意在不需要修改实参时可以传值或在参数类型前加上const修饰
调用如下函数可以对外部实参起到交换作用。
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
4.2 引用做函数返回值
例一:
int& Count()
{
static int n = 0;
n++;
// ...
return n;
}
例二:下面的代码输出结果是什么?
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
输出的结果并不是第一次调用得到的返回值:3,而是最后一次调用Add(3,4)的返回值。
这是因为函数在调用时,系统会给每个正在调用的函数分配一个栈环境:栈帧,栈帧内部保存函数调用所需的参数和返回值等信息。
在反汇编中可以看到,在执行语句 int& ret = Add(1, 2); 时,引用类型的ret已经指向了调用Add(1, 2)的那块栈帧空间的返回值部分,在完成这条语句后,ret的值毋庸置疑是3.
但在第一次调用结束后,系统会自动回收第一次函数调用所使用过的栈帧,此时由于调用约定是__cdecl,该栈帧内部的数据并不会被清除,而是由下一次被调用的函数来清除之前的数据,这称为手动清栈。再执行语句 Add(3, 4),系统又在上次调用Add函数的位置给本次调用申请了栈环境,此时要注意,ret之前指向的那块栈空间正好是Add函数存储返回值的部分,因此会被Add(3, 4)修改成7。因此ret最后输出的结果会是7。
需要注意的是,在这里我们重复调用的是同一函数,因此在栈空间上申请出的栈帧大小一般是相同的,所以第一次调用时ret指向的栈帧中的返回值的部分,第二次指向的还是返回值,因此能得到返回值的结果。若调用不同的函数,该位置可能存储着不知道是什么类型的数据,因此可能会是随机值。
结论: 如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
5.传值、传引用的效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝。当实参很大时,如含有10000个int元素的结构体,用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include <iosteam>
#include <time.h>
using namespace std;
struct A
{
int a[10000];
};
void TestFunc1(A a)
{}
void TestFunc2(A& a)
{}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 10000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 分别计算两个函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
结论:传递的实参如果很小时,传值和传引用的效率很接近,但如果实参很大,传值的效率是远低于传引用的。
6.值和引用作为返回值类型的性能比较
#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;
}
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
7.引用和指针的区别
在语法层面上,引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main()
{
int a = 10;
int& ra = a;
cout<<"&a = "<<&a<<endl;
cout<<"&ra = "<<&ra<<endl;
return 0;
}
由前面的结论,输出的结果地址应该是相同的。
在底层实现上实际是有空间的,因为在底层下,引用是按照指针方式来实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
引用与指针的汇编代码对比如下:可见在底层,引用与指针的实现方式是一样的,只是编译器在背后帮我们取到了实体的地址,并在使用时帮我们解了引用,为我们使用引用提供了便利。
引用和指针的不同点:
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全