上一期C++教程的博客链接:超级详细的C++教程2:缺省参数,函数重载
1.前言
C++对C语言改进最大其实就是引用,引用不仅难懂,同时它还非常的重要。刚开始学的时候可能会觉得指针能解决的东西为什么还出一个引用,这不显得C++有些杂乱吗?其实这是因为我们接触的少,后面代码写多了之后就会发现引用还是挺香的。
2.引用
2.1引用的概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
我们可以把引用简单的理解为给变量起外号,比如孙悟空,在猪八戒眼里弼马温,天上的神仙称呼它为大圣,弼马温和大圣都是孙悟空。
2.2引用的定义
类型& 引用变量名(对象名) = 引用实体;
注意:上面&这个符号表示它是一个变量的引用,不是取地址操作符。
我们可以给任何类型的变量取别名,例如:
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& c = a;
return 0;
}
上面代码中,c就是a别名,因为编译器不会为引用变量开辟内存空间,所以c和a共用同一块空间,c的改变也会影响a,同时a的改变也会影响c,例如:
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& c = a;
a += 20;
cout << a << " " << c << endl;
c-= 20;
cout << a << " " << c << endl;
return 0;
}
我们将a和c的地址打印出来,就会看到它们两个的地址是一样的,例如:
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& c = a;
cout << &a << endl;
cout << &c << endl;
return 0;
}
既然它们的地址都是一样的,我们就可以简单的理解为a和c是同一个变量。
一个变量可以有多个别名,也可以给别名取别名,例如:
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& b = a;
int& c = a;
int& d = c;
return 0;
}
它们都是同一个变量,也就是最初的a变量。
2.3引用的使用场景
1.做参数
1.交换两个变量的值
还记得在C语言中所学的swap函数吗?它的功能是交换两个变量的值,因为形参是实参的临时拷贝,形参的改变不会影响到实参,如果要改变实参,只能传地址过去,所以swap函数的参数必须是指针,例如:
#include <iostream>
using namespace std;
void Swap(int* left, int* right)
{
int temp = *left;
*left = *right;
*right = temp;
}
int main()
{
int a = 3;
int b = 5;
Swap(&a,&b);
return 0;
}
指针虽然好用,但是每次调用传参的时候都要取地址,有些麻烦,有了引用之后,我们就可以不用指针了,例如:
#include <iostream>
using namespace std;
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
int main()
{
int a = 3;
int b = 5;
Swap(a,b);
return 0;
}
因为left是a的别名,right是b的别名,left和right的改变会影响到a和b。所以a和b的值就被交换了,使用引用的swap函数,不用写*(解引用操作符),还有传参的时候不用写&(取地址操作符),所以引用使用上还是很爽的。
2.解决二级指针所带来的麻烦
C语言中要改变实参的值需要使用一级指针,那如果要改变实参为一级指针所指向的地址呢?
C语言要使用二级指针才行,例如:
#include <iostream>
#include <stdlib.h>
using namespace std;
void test(int** ppa)
{
*ppa = (int*)malloc(sizeof(int));
**ppa = 10;
}
int main()
{
int a = 20;
int *pa = &a;
cout << *pa << endl;
test(&pa);
cout << *pa << endl;
free(pa);
}
C++可以不用二级指针,直接使用引用,例如:
#include <iostream>
#include <stdlib.h>
using namespace std;
void test(int* &ra)
{
ra = (int*)malloc(sizeof(int));
*ra = 10;
}
int main()
{
int a = 20;
int *pa = &a;
cout << *pa << endl;
test(pa);
cout << *pa << endl;
free(pa);
}
上面写到:我们可以给任何类型的变量取别名;所以我们可以给指针取别名,ra是pa的别名,ra的改变也会影响到pa。虽然看上去只少写了一个*(解引用操作符)和&(取地址操作符),等到了后面我写了C++数据结构:AVL树和类和对象后你就知道使用引用的好处了。
3.减少传参时间消耗
如果函数参数是一个很大的对象,除了使用指针外,使用引用也能提高效率。指针的大小也就4/8个字节,引用传的对象的别名,这样能减少不必要的时间消耗,提高程序的运行效率,例如:
struct test
{
int a[1000000];
};
void func(struct test* t)
{
//......
}
void func(struct test& t)
{
//......
}
2.做返回值
引用做返回值的时候需要注意了,不能返回局部变量,局部变量一但离开它所在的作用域,它的生命周期就结束了,例如:
#include <iostream>
using namespace std;
//错误代码演示:
int& func()
{
int n = 0;
n++;
return n;
}
int main()
{
int& a = func();
return 0;
}
上面代码中,main函数中func函数调用完毕,它的栈帧就会销毁,局部变量n也就不在了,func函数最后返回了n的引用,又用引用来接收,如果我们去访问a就是在访问野指针,Linux平台上使用G++编译运行后直接报错(在VS上它会做一次保留)。所以引用做返回值的时候,我们需要注意一下返回对象的生命周期!!!
我们需要将上面代码中func函数里的局部变量n修改为静态的局部变量或者全局变量,所以引用做返回值正确的写法,例如:
#include <iostream>
using namespace std;
int& func()
{
static int n = 0;
n++;
return n;
}
int main()
{
int& a = func();
return 0;
}
1.提高效率
如果函数最后返回的是一个很大的对象,除了使用指针,用引用做返回也能提高效率,例如:
struct test
{
int a[1000000];
};
struct test& func()
{
static struct test t;
return t;
}
int main()
{
struct test& a = func();
return 0;
}
后面我们学到类和对象后,就会经常使用引用做返回值。
2.修改返回对象
在C语言中,如果我们想修改返回对象,必须返回的是指针,如下面代码,我们要修改的是func函数里的静态局部数组arr下标位置为pos的元素,所以我们返回了该元素的指针后就能在外部进行修改。
#include <iostream>
using namespace std;
int* func(int pos)
{
static int arr[10] = {1};
return &arr[pos];
}
int main()
{
*(func(9)) = 180230;
cout << *(func(9)) << endl;
// int *pa = func(9);
// cout << *pa << endl;
return 0;
}
C++中使用引用也能做到,例如:
#include <iostream>
using namespace std;
int& func(int pos)
{
static int arr[10] = {1};
return arr[pos];
}
int main()
{
func(9) = 180230;
cout << func(9) << endl;
return 0;
}
指针能修改返回对象是因为返回的是对象地址,通过地址间接修改了返回对象的值,引用能修改返回对象是因为返回的是对象的引用(别名),通过别名进行修改返回对象的值。
这一部分理论知识不扎实的同学会难以理解,需要不断的去练习才能加深印象,因为后面的类和对象会遇到类似于上面部分的代码。
2.4常引用
1.权限部分
前面我们都是给变量取别名,我们能不能给常变量取别名呢?答案是可以的,但是引用的前面必须加上const才行,例如:
int main()
{
const int a = 10;
//int& r = a;//错误代码,因为a是常变量,所以引用前面必须加上const
const int& r = a;
return 0;
}
上面代码中,如果引用的前面不加上const编译器会报错,因为出现了权限的放大。a加上了const后就应该不能被修改了,但是我们给它取了个别名后不加const后面可能会间接或直接的修改了a,这样做显然不合理。
所以使用引用的原则是:权限不能被放大,权限可以平移,权限可以缩小,例如:
int main()
{
//权限的平移
const int a = 10;
const int& ra = a;
//权限的缩小
int b = 20;
const int& rb = b;
return 0;
}
只要不涉及权限的放大,我们还可以给常量取别名(一般只会出现在传参),例如:
int main()
{
const int& e = 10;
return 0;
}
2.隐式类型转换部分
引用可不可以引用不是同类型的对象(如int类型的引用,去引用double类型)?答案是不可以,例如:
int main()
{
double a = 3.14;
int& re = a;//错误代码,编译器直接报错
return 0;
}
但是如果给上面代码中的引用re加上const以后编译就能通过了,例如:
int main()
{
double a = 3.14;
const int& re = a;
return 0;
}
原因是因为re引用的不是a,double类型的a转成int类型,并不是直接拿a去转成int类型,而是中间会生成一个临时变量(隐式类型转换/显示的强制类型转换中间都会生成一个临时变量),所以re引用的其实是中间生成的临时变量,临时变量具有常属性,如果不加上const会造成权限的放大,这就是加上const后编译通过的原因,如下图所示:
2.5引用需要注意的点(重点)
1.引用必须初始化,例如:
int main()
{
int& a;//错误代码,引用必须初始化
int& b = NULL;//错误代码,NULL是一个常量标识符,必须要常引用才行
return 0;
}
2.引用初始化不能改变指向,例如:
int main()
{
int a = 10;
int b = 20;
int& ra = a;
ra = b;
return 0;
}
上面代码中,ra = b的意思是将b的值赋值给ra,不是将ra的指向修改为b。
3.引用不能完全替换掉指针,因为引用不能改变指向,所以引用是无法替换掉指针,我们可以去思考链表这一部分,如果将指针替换成引用会导致节点丢失。
2.6引用的底层(了解)
引用的底层其实就是指针,我们可以通过VS上的调式里转到反汇编中就可以看出来,例如:
可以看见,指针的汇编代码和引用的汇编代码是一样的。
引用在语法上不开空间(我们可以用sizeof来计算(计算出来的结果是引用的对象的大小),最好不要用int类型),引用在底层开了4/8字节的空间,因为引用的底层是指针。
2.7引用和指针的区别
1. 引用概念上定义一个变量的别名,指针存储一个变量地址;2. 引用在定义时必须初始化,指针没有要求;3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体;4. 没有NULL引用,但有NULL指针;5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 位平台下占4个字节);6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小;7. 有多级指针,但是没有多级引用;8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理;9. 引用比指针使用起来相对更安全;
前情回顾:
看到这里同学,如果感兴趣还可以去看一下我所写的数据结构的博客,手把手带你手撕代码,可以关注一下我的C++和C数据结构,它们都还在持续更新:
超级详细的C++教程1:命名空间,输入输出
C语言数据结构:顺序表
C语言数据结构:链表
C语言数据结构:带头双向循环链表
C语言数据结构:栈
C语言数据结构:队列
C语言数据结构:树和二叉树的基本概念
C语言数据结构:堆