
目录
问题引入
生活中我们或多或少都有一些外号,比如《水浒传》中,及时雨宋江、九纹龙史进……而C++中也可以给同一个变量起外号,这就是引用。
1. 引用的概念
引用不是新定义一个变量,而是为已经有变量起一个别名。系统并不会为其开辟一个新的空间,而是与原来的变量共用一块内存。
1.1. 引用的表示方法
类型 & 引用变量名(对象名) 引用实体
这里熟悉C语言的同学就会发现,&是C语言中的取值符,同时也是按位运算符,这其实是运算符重载例子。类似于函数重载,运算符重载也是的同一个运算符有着不同的功能,编译器能够根据上下文来理解运算符到底是什么意思。除了这里提到的,C++中还有一些运算符重载的例子:<<可以是插入运算符,也可以是左移运算符。
代码示例:
#include <iostream> using namespace std; int main() { int a = 10; int& b = a; int& c = b; }由本段代码可知,有一个是实体变量a,有两个别名b、c,我们可以通过调试来看一下这几个值的内存地址:
通过调取内存不难发现,a、b、c指向的是同一块内存空间。
【注意】:引用类型必须和引用实体是同种类型的。
1.2. 引用的特性
引用的三个特性:
- 引用必须在定义时进行初始化
- 一个变量可以有多个引用
- 引用一旦引用了一个实体,就不能再引用别的实体
1.2.1. 引用在定义时必须进行初始化
引用时对已经存在的变量起别名,所以必须为引用指定变量(初始化)
int& b;//错误,未初始化
1.2.2. 变量可以有多个引用
就像人可以有多个别名:
int main() { int a = 10; int& b = a; int& c = b; }
1.2.3. 引用一旦引用一个实体,再不能引用其他实体
这个也比较好理解,因为引用一旦引用了一个已经存在的实体,就是这个实体的别名,当然不能再成为其他实体的别名。
1.3. 常引用、引用权限
大家看一下这段代码能不能编译成功?
int main() { const int x = 1; int& y = x; return 0; }会出现错误警告:
【注意】
引用原则:对原变量的引用,权限不能放大。
再来看一下这段代码能否编译成功?
int main() { const int x = 1; //int& y = x; const int& z = x; //权限不变 int a = 2; const int& b = a; //权限变小 return 0; }
是可以成功通过编译的,很明显这段代码是符合引用原则的,也就是说权限不变或者权限缩小都是符合引用原则的,唯一注意的就是:权限不能放大。
1.4. 引用的使用场景
1.4.1 做参数
void Swap(int& x, int& y) { int tmp = x; x = y; y = tmp; } int main() { int a = 1, b = 2; Swap(a, b); cout << a << " " << b << endl; }将引用作为函数的参数,也就是说x是a的别名、y是b的别名,使用引用比较方便理解。
那么既然值可以作为函数参数,引用也可以作为函数参数,那么为什么要使用引用来作为参数呢?或者说使用引用作为函数参数的好处是什么?
这里使用这样一个测试代码,能够更加直接地看出使用引用作为函数参数的高效性:
#include <time.h> #include <iostream> 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; }运行结果:
可以很明显的看出,引用作为参数的效率是更加高效的。因为当值作为函数参数或者返回值类型时,函数并不会直接传递实参或者返回变量本身,而是传递参数或者返回变量的一份临时拷贝,这个效率是十分低效的。
所以使用引用作函数参数的意义:
输出型参数(允许修改外部变量)
减少拷贝,提高效率
1.4.2. 做返回值
先来看一下这段代码的运行结果:
int Count() { static int n = 0; n++; return n; } int main() { int ret1 = Count(); int ret2 = Count(); int ret3 = Count(); cout << ret1 << endl; cout << ret2 << endl; cout << ret3 << endl; return 0; }运行结果:
n虽然是定义在函数内部的静态局部变量,但是生命周期是全局的,所以我们能够看见结果的输出。但是实际生活中,静态的变量是少见的,大多还应该是局部变量。局部变量就会存在一个问题,我们学过函数栈帧的知识:返回值会储存在寄存器当中,当退出了函数(函数栈帧被销毁),这个返回值就会随之销毁ret就接收不到返回值,但是我们这里确实接收到了返回值的结果,这是为什么?
这个涉及到编译器的底层运作机制,为了解决这个问题,会自动产生一个临时变量,将寄存器中的返回值拷贝一份,赋值给临时变量,然后将临时变量返回(临时变量不会受函数栈帧销毁的影响)
传值返回:
也就是上面提到的,会产生一个临时变量,然后将返回值拷贝给它,最后返回临时变量,然后ret接收(也就是拷贝给ret)。
怎么证明确实产生了一个临时变量?
这个地方报错了,我们思考一下,如果没有产生临时变量,那么我使用一个int&来接收一个返回类型int,这是肯定没有问题的。但是出现了报错,就可以证明中间确实产生了一个临时变量,因为临时变量是有常性的,而这样接收的话,相当于是权限的放大。而我只要加上一个const就可以了。
传引用返回:
//在返回值类型上面加上& int& Count() { int n = 0; n++; cout << "n:" << &n << endl; return n; } int main() { int& ret = Count(); cout << "ret:" << &ret << endl; return 0; }PS:这里把const去掉了(并不影响我们得出的结论)
这段代码我们怎么理解?其实我们同样可以这样理解:同样产生了一个临时变量,只不过这个临时临时变量类型是int&,也就是n的别名,所以返回的是n的别名,所以接收的ret也应该是int&类型。
我们可以打印一下n和ret的地址:
可以发现是一样的,也就说明ret就是n的一个别名。
1.5. 引用导致野指针
回到上面提到的代码片段:
int& Count() { int n = 0; n++; cout << "n:" << &n << endl; return n; } int main() { int& ret = Count(); cout << "ret:" << &ret << endl; return 0; }大家思考一下,这段代码有没有问题?似乎我们看结果来说,代码是没有问题的,但是我们加上一句代码试试:
int& Count() { int n = 0; n++; cout << "&n:" << &n << endl; return n; } int main() { const int& ret = Count(); cout << ret << endl; //第一次打印ret cout << "&ret:" << &ret << endl; cout << ret << endl; //第二次打印ret return 0; }运行结果:
oh~mygod!!!会惊奇地发现,ret的值居然在变化,所以毫无疑问这段代码肯定是有问题的。但是问题出在哪?
是因为出了函数的作用域,Count函数就被销毁了,所以再去访问时就会造成非法访问,也就是引用搞出来的野指针。
我们可以在举一个更直观例子说明:
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):" << ret << endl; return 0; }运行结果:
1.6. 值和引用作为返回类型的性能比较
#include <time.h> #include <iostream> 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; }
1.7. 引用和指针的区别
引用的底层还是使用指针实现的,可以使用反汇编查看一下定义引用和指针的区别:
引用和指针的不同点:
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全












22万+

被折叠的 条评论
为什么被折叠?



