本节咱们来说说引用:
C++添加了"引用",与指针成了两兄弟------这两兄弟对我们今后写C++代码可谓各有特点,缺一不可。
何谓引用?
引用:就是取别名
不知诸位可有别名?这里不妨举一本耳熟能详的小说《水浒传》,这别名可是一百零八好汉的标配。好比,景阳冈打虎的武松,别号:武二郎、行者、武行者、武都头;倒拔垂杨柳的鲁智深,别号:花和尚。
那鲁智深倒拔垂杨柳,花和尚有没有拔杨柳呢? 那必须是当然的。花和尚就是鲁智深,这俩是一个人。
同理,武松打虎,也意味着武二郎打虎。------这就是取别名。
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间, 它和它引⽤的变量共⽤同⼀块内存空间。
你叫花和尚,既然和鲁智深是一个人,那只是多个名字,并不是多个人。
在C++里面,给变量取别名,就是引用(此处为动词),这个别名我们也叫做该变量的引用(此处为名词)。
那如何创建引用呢?便是用:变量类型& 别名 = 要引用的对象。
cpp
int main()
{
int a = 10;
int& b = a;
//给a 取了个别名 b,即b是a的引用,也可叫b是a的别名
return 0;
}
那我们来验证一下,按照取别名的逻辑,此时的b的值应该也是10,且++b,意味着++a------以此来检验b确实是a的别名(引用)。
cpp
int main()
{
int a = 10;
int& b = a;
cout << b << endl;//应该打印10
++b;
cout << b << endl;//应该打印11
cout << a << endl;//应该打印11
return 0;
}
所料不错:
引用的特性:
引⽤在定义时必须初始化
cpp
int main()
{
//引⽤在定义时必须初始化
int a = 10;
int& ra;//这里没初始化,报错
return 0;
}
正确的写法:别名一创建就有与其对应的"本名" ------变量类型& 别名 = 要引用的对象
cpp
int main()
{
//引⽤在定义时必须初始化
int a = 10;
int& ra = a;//这里的r,refer:引用
return 0;
}
⼀个变量可以有多个引⽤
cpp
int main()
{
//⼀个变量可以有多个引⽤
int a = 10;
int& b = a;
int& c = a;
cout << b << " " << c << endl;//给a 取了两个别名 b ,c。它们的值都是10。
return 0;
}
正如武松的别名就有4个,都是武松。
引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
cpp
int main()
{
int a = 10;
int& b = a;
int& c = a;
//引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
int x = 100;
b = x;//这⾥让b引⽤c?
cout << &b << " 值为:" << b << endl;//此处想打印b,a,c的地址和它们的值
cout << &a << " 值为:" << a << endl;//如果b变成x的别名,地址和a,c应该不一样
cout << &c << " 值为:" << c << endl;//如果只是赋值,那么,b,a地址相同,值全部变成100
return 0;
}
提示:别名的地址和"本名"的地址应该是一样的,因为取别名不会为别名另开空间。
运行截图:
所以,得到结论:C++ 引⽤不能改变指向。
引用的本质:加深理解引用
那引用在内存空间中,长什么样呢? 洒家就给诸位涂鸦一番。
内存里,我们定义了一个变量a,类型为int,值为100。
现在我们给a变量取两个别名,分别是:b和c。
我们又把10这个值赋给b,意味着10这个值,会被b存起来,那自然存到b引用的空间。巧了不是,a和c的空间,b的空间都一样。
那此时,访问abc三者的值,是不是都是一个空间里存着的10,三个变量的地址是不是都是一样的?那必须是当然的。
引用的使用:
不知诸位,还记得初学指针时,要求写的Swap函数吗?
cpp
void Swap(int* px,int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}//此函数想要实现两个数的交换
int main()
{
int a = 1,b = 2;
cout << a << " " << b << endl;//依次打印 a,b: 1 2
Swap(&a,&b);//交换
cout << a << " " << b << endl;//依次打印 a,b: 2 1
return 0;
}
现在学了引用,我们若用引用替换这里的指针,又该怎么做呢?
首先,指针出现在了 函数的定义中 (int* int*),接着函数体里面也有对指针的解引用。
若要实现引用替代指针,首先得把指针改成引用。改是能改,但是是否应该改呢?传地址过去,无非就是不再对复印件修改,而是通过地址,再解引用,最后才对正主进行操作。
引用,虽然是取别名。哥们儿,可没指针那么偷偷摸摸:偷偷开个空间,存下目标的地址,再去登门拜访。引用,直接是本人,何须解引用,直接贴脸开大。------对引用操作,就是对正主操作。
引用传参:
cpp
void swap(int& rx, int& ry)//传参就传引用
{
int tmp = rx;
rx = ry;
ry = tmp;
}
主函数里,用指针就是传a,b的地址,这里不用了,直接传 a,b;
cpp
int main()
{
//引用传参
int x = 10,y = 20;
cout << x << " " << y << endl;
swap(x,y);//直接传x,y;你可能会问,为什么不传引用类型,而直接传int类型
cout << x << " " << y << endl;
return 0;
}
传过去不管是数据类型还是引用,走到函数第一步接收参数时, 此时函数形参就是为数据创建的别名。对别名操作后,函数结束,栈帧销毁,局部变量也不复存在(这里丢失的只是别名而已),但是,本名还在,人还在(空间还在),被修改的数据也放在空间好好的。
cpp
void swap(int& rx, int& ry)//走到这一步:创建两个变量的引用接收
至于为什么引用类型接收int ,和我们刚开始说的
cpp
int& b = a;
是一个道理,int&从语法上也是个int,不过它只是个别号。
通过Swap函数,我们可以看到引用传参更加方便,当然现在没习惯引用,可能颇觉不自在,细水长流慢慢来。
引⽤在实践中主要是于引⽤传参和引⽤做返回值中减少拷⻉提⾼效率和改变引⽤对象时同时改变被 引⽤对象。
没错。引用还有个做返回值的用途。
这就不得不拿我们的数据结构篇的------栈出来了。
忆往昔,以前要修改栈,总是传栈的地址,再在函数体里面进行解引用。现在,我们会使用引用了。
cpp
//引用作返回值
typedef struct Stack
{
int* a;
int top;
int capacity;
}ST;
void STInit(ST& rs,int n = 4)//意在初始化栈
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc");
return;
}
rs.a = tmp;
rs.capacity = n;
rs.top = 0;
}
void STDestroy(ST& rs)//意在销毁栈
{
free(rs.a);
rs.a = NULL;
rs.top = rs.capacity = 0;
}
void STPush(ST& rs,int x)//意在插入数据
{
if (rs.top == rs.capacity)
{
int newcapacity = 2 * rs.capacity;
int* tmp = (int*)realloc(rs.a,sizeof(int) * newcapacity);
if (tmp == NULL)
{
perror("realloc");
return;
}
rs.a = tmp;
rs.capacity = newcapacity;
}
//增容完毕
rs.a[rs.top] = x;
++rs.top;
}
重头戏就在这儿:
引用作返回值:
cpp
int& STTop(ST& rs)//意在返回栈顶数据
{
assert(rs.top);
return rs.a[rs.top - 1];
}
哥们儿现在返回的不只是栈顶数据,还有栈顶数据的别名。
我们以前若要修改栈顶数据,需要再写个Modify函数。现在不用了:
cpp
int main()
{
ST st;
STInit(st);
STPush(st,1);
STPush(st, 2);
STPush(st, 3);
//插入3个数据
cout << STTop(st) << endl;//打印栈顶数据
//STTop()现在返回的是栈顶数据的引用,而不是单单一个数据
//通过引用,直接修改栈顶数据
STTop(st) = 4;//这就类似前面的操作,给引用赋值,改变引用的同时也改变被引用的对象
cout << STTop(st) << endl;
STDestroy(st);//堆上申请的资源,记得主动释放
}
运行截图:
3是原先的栈顶数据,后来我们又给栈顶数据赋值为4,再次打印栈顶数据就变成了4------ 就这么浅显地改了栈顶数据,谁不来一句:"家人们谁懂啊~"。
有人会说了:"直接返回int不就好了嘛,然后也像这样在主调函数里面赋值改变栈顶数据。"
不说不知道,简简单单传个int回来,因为这份数据是在函数栈帧里,一旦函数函数栈帧销毁,结果也会跟着不复存在。所以编译器会在它销毁之前拷贝一份,临时创建一个未命名的空间里,把结果存进去。------ 我们称这个结果为临时对象。
临时对象具有常性
临时对象返回在主调函数里,它就已经无法被简单赋值更改了。
当然,没接触过数据结构的同学,安啦~这一段是为了说明引用在事件中的第二大用:作返回值。
后续,关于引用的大戏几乎天天上演,不用担心这个例子不够。
const引用:
const咱们学过的,先来看瞧瞧最原始的模样:
cpp
const int x = 30;//const 一修饰,x的值再也不能改变
x++;//你猜会报错嘛
报错,那必须是当然的。
两句话就说了const的作用,现在来看const引用------ 无非就是const修饰一个引用。
可以引⽤⼀个const对象,但是必须⽤const引⽤。
假设哥们我一身反骨。我们偏不用const是不是会报错:
cpp
const int x = 30;
int& rx = x;//可以引⽤⼀个const对象,这里使用rx引用x
看好了,我们没用const 修饰int&。运行截图:
无疑是说右边本是const int(无法被修改的整型) 反而取了个别名,可以通过别名可以改变值了。(通过别名对值进行操作,咱又不是前面没学(狗头😎))------ 这种情况,我们又称为权限的放大。
所以啊,之所以引用前加const,就是为了类型匹配。
cpp
const int x = 30;
const int& rx = x;//可以引⽤⼀个const对象,这里使用rx引用x
接着走,我们企图通过别名对x的值进行改变:
确实,const确实不是吃素的,说不变就不变。
const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
cpp
//const引⽤ 引⽤普通对象
int a = 10;
const int& ra = a;//此时不能通过别名对a进行操作了------权限的缩小
确实,一个坑咱们不踩两次:改变a不非得通过ra; a不还是一个int嘛,一个没有被const修饰的int。
cpp
a++;
cout << ra << endl;//此时打印ra的值,果然验证了别名和本名的关系:11
碰到临时对象,const包有的
没有修炼遗忘大法的同学,应该记得我们刚刚才就谈到了临时对象--- 函数返回值而非引用时,会被拷贝成为临时对象。而临时对象具有常性,不容赋值修改。
什么情况下产生临时对象:
1.运算表达式结果具有常性,因为产生了临时对象:
cpp
int main()
{
int a = 10;
const int b = 20;
//被引用对象具有常性,因为产生了临时对象
int& rc = (a + b)* 30;//右边结果无法被修改,左边企图取个别名,实现修改------ 妥妥的权限放大
return 0;
}
(a + b)* 30 典型的运算表达式,其结果会被拷贝,成为临时对象。
cpp
int main()
{
int a = 10;
const int b = 20;
//被引用对象具有常性,因为产生了临时对象
const int& rc = (a + b)* 30;//右边结果无法被修改,左边企图取个别名,实现修改------ 妥妥的权限放大
return 0;
}
2.类型转换
cpp
double d = 3.14;
const int& rd = d;//此处有类型的转换
在类型转换中会产⽣临时对象存储中间值,你猜d变没?
cpp
double d = 3.14;
const int& rd = d;
cout << d << endl;//
cout << rd << endl;//
:
为什么d不变:你以为rd引用的是d。 非也非也,rd
引用的是一个由 d
的值转换成的临时 int
对象。 而d不变,还是double,还是3.14。从始至终,发生类型转变的,是一个拷贝d值的临时对象。
碰到临时对象,你说const该不该加,那必须是当然的。
3.常量值
cpp
//引用常量
const int& r = 30;
30不是临时对象,也不会似类型转换那般中间值作为临时对象,那为什么要加const。因为30,它""常"。
临时对象具有常性,常量之所以叫常量,也是因为具有常性:不容改变。
虽说函数返回值的时候,也会产生临时对象,但是这里我们讨论的是const和临时对象搭配的场景,理解这三种就好。
引用和指针:相见恨晚
引用和指针的结合打法,可谓所向披靡。当二者各司其职,各显神通,这C++的性能不就提上去了。
掉书袋&总结time:
语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间
引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的
引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象
引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象
sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下 占4个字节,64位下是8byte)
指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些
引用相对安全:
因为引用定义时就必须初始化一个实体,很少出现空引用。但不是不出现,咱们现在就举一个:
cpp
int& fun(int a = 30)
{
return a;// 这里尝试返回函数参数a的引用
}
int main()
{
int a = 0;
fun(a);
cout << a << endl;//打印结果是什么
}
首先得搞清楚,返回的是实参a的引用还是形参a的引用------ 形参a的引用,形参会在函数结束后随着栈帧销毁而被跟着销毁。接下来,就会导致返回的别名,它的空间被系统回收,一个指针存的地址不合法,成为野指针;如果一个引用所指向的空间变得无效,那么这个引用就变成了野引用
上面那6条,推荐理解+记忆,考过真题