前言
大家好,今天我们一起来探究一下C++中的引用。
🐱🐱🐱
文章目录
引用
1.引用概念
引用其实就是给已经存在的变量取一个别名,编译器不会为引用变量开辟内存空间。
他们共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;
注意:引用类型必须和引用实体是同种类型的
有了引用,再写交换函数就不用传地址,直接传引用。看起来岂不是舒服很多?
#include <iostream>
using namespace std;
void Swap(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
int main()
{
int x = 1, y = 2;
cout << x << " " << y << endl;
Swap(x, y);
cout << x << " " << y << endl;
return 0;
}
2.引用性质
1. 一个变量可以有多个别名
int main(int argc, char const *argv[])
{
int a = 10;
// b是a的引用(别名)
int &b = a;
b = 20;
int* p = &b; // p指向b,也就是p里面是b的地址,也就是a的地址。
//还可以继续取别名
int &c = b;
c = 30;
return 0;
}
// a b c地址相同的,也就是改变b同时也就改变了a,c
2. 引用必须初始化
//正确示例
int a = 10;
int& b = a;//引用在定义时必须初始化
//错误示例
int a = 10;
int &b;//定义时未初始化
b = a;
3. 引用一旦引用一个实体,再不能引用其他实体
也就是别名一旦被定义,后面就不能再改了。
但指针不同,指针的指向是可以改变的。
int a = 10;
int& b = a; // b是a的引用
int e = 20;
b = e;//你的想法:让b转而引用e ‘错误’ 这里只是把e的值赋值给b
4. 引用与重载
void Swap(int &r1, int &r2);
和void Swap(int r1, int r2);
不构成重载
void Swap(int &r1, int &r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
int main(int argc, char const *argv[])
{
int a = 10;
int b = 20;
Swap(a, b);
return 0;
}
//int &r1 类型是int
//void Swap(int r1, int r2); 这样不构成重载了
3.常引用
指针有被const修饰的版本,那引用也应该有吧?当然有!
取别名的原则:
对原引用变量,读写权限只能缩小,不能放大。
1.权限放大
const int a = 10;
// int& ra = a; // 该语句编译时会出错,a为常量 ra引用a属于权限放大
// a被const修饰,表示a是只读的,如果 int& ra = a; 修饰,就变成可读可写的了。
const int& ra = a; //常引用 权限不变
2.权限缩小
int b = 10;
int& rb = b; // 权限不变
const int& crb = b;// crb引用b属于权限缩小,本来可读可写变成只读的了,编译可以通过。
3.隐式类型转换
C++沿袭了C语言的部分特性,因此在相似类型发生操作时会发生隐式类型转换或整型提升。也就是大给小会截断,小给大会提升。
给常量取别名
注意:不能直接给常量取别名,需要用const修饰,常量是只读的。
//int& b = 20; // err
const int& b = 20; // 这样才行
临时变量具有常性
double d = 2.2;
//int& e = d; //err: double 不能转换成 int
// 加个const为什么就可以了呢?
const int& e = d;
double转int
double d = 2.2;
int f = d; // 这里也是把d的整数部分截断,赋值给一个临时变量,临时变量再赋值给f
const int& e = d;
// 发生了隐式类型转换,把d的整数部分截出来给一个临时变量,临时变量再赋给f,而临时变量具有常性
// 也就是,e其实引用的是临时变量,所以加const才能正常引用
// 也就是说,e其实不是d的引用,e只是那个临时变量的别名,e 和 d 地址不一样的。
再举个例子,int 转 double:
int c = 10;
double d = 1.11;
d = c; //会发生隐式类型转换
//借助一个double的临时变量 临时变量具有常性
//double& rc = c; //错误
const double& rc = c; //rc引用的是 c的临时变量
//同理,rc并不是c的别名。
应用于传参
传值传参会有拷贝,因此最好都用传引用传参。
传引用传参可以减少拷贝,关于引用传参我们下面再细说。
void func(int& x)
{
}
int main()
{
int a = 10;
int& b = a;
const int& c = 20;
double d = 2.2;
const int& e = d;
func(a);
// func(10);
// func(c);
// func(d); // 隐式类型转换
// func(e);
// 统统传不过去,常量是只读的,而形参只是int& x 会放大权限,形参得用 const int& x
return 0;
}
形参加个const就能正常传参了。
4.使用场景
1.做参数
1.1 做输出型参数
所谓输出型参数,也就是函数内部返回给函数外部的。形参的传值调用是输入型参数。
传址调用有可能是输出型参数,也有可能是输入型参数。作为输入型参数使用时,一般会用const关键字修饰,表明是只读,不能修改。可参考这篇文章:👉戳我
以一道OJ题(LeetCode144)举例。
C语言版本下:int* preorderTraversal(struct TreeNode* root, int* returnSize){ }
如果是C++则可以这么写:
int* preorderTraversal(struct TreeNode* root, int& returnSize){ }
再举个例子:为什么
scanf("%d",&a);
需要取a的地址呢?
scanf是去缓冲区提取我们输入的变量然后赋值给a,而a是传给形参的,要让实参和形参一起改变就得传地址了。如果C语言有了引用,就不需要再取a的地址了。
一个经典例子:Swap函数。
void Swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
// 配合函数重载使用更爽
void Swap(double& a, double& b)
{
double tmp = a;
a = b;
b = tmp;
}
int main()
{
int x = 1, y = 2;
double c = 1.1, d = 2.2;
cout << x << " " << y << endl;
cout << c << " " << d << endl;
Swap(x, y); // a是x的别名,b是y的别名
Swap(c, d); // 实际上调用的不是同一个函数
cout << x << " " << y << endl;
cout << c << " " << d << endl;
return 0;
}
1.2 减少拷贝,提高效率
传值传引用传指针效率比较
#include <time.h>
#include <iostream>
using namespace std;
void Swap(int& r1, int& r2)
{
int tmp = r1;
r1 = r2;
r2 = tmp;
}
struct A
{
int a[10000];
};
void TestFunc1(A a)
{
}
void TestFunc2(A& aa)
{
}
void TestFunc3(A* paa)
{
}
void TestRefAndValue()
{
A a;
// 以值作为函数参数
size_t begin1 = clock();
// 把实参拷贝给形参,10000Byte拷贝100000次,
for (size_t i = 0; i < 100000; ++i)
TestFunc1(a);
size_t end1 = clock();
// 以引用作为函数参数
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2(a);
size_t end2 = clock();
// 传地址
size_t begin3 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc3(&a);
size_t end3 = clock();
// 分别计算函数运行结束后的时间
cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
cout << "TestFunc3(A*)-time:" << end3 - begin3 << endl;
}
int main()
{
TestRefAndValue();
return 0;
}
2.做返回值
特殊场景下才行。
传值返回
例1
先举个静态变量的例子👇
// 输出结果是什么呢?
int Count()
{
static int n = 0; // 静态变量只会初始化一次。
n++;
// ...
return n;
}
// 静态的局部变量,虽然作用域只在Count里面,但生命周期是全局的。
// n的地址初始化之后地址就不变了。
int main()
{
cout << Count() << endl;
cout << Count() << endl;
cout << Count() << endl;
// 结果为1 1 1 静态变量只会初始化一次。
return 0;
}
传值返回的过程中会产生一个临时变量,也就是会把返回值拷贝给一个临时对象。
为什么要借助一个临时变量来操作呢?
更一般的情况下,局部变量 n 出了函数作用域就被销毁了,就不能直接把n给ret。
- 如果对象比较小(4/8byte),那么一般会使用寄存器存储对象的拷贝。
- 如果对象比较大,这个拷贝通常会存在上一个函数的栈帧。
// 更一般的情况:
int& Count()
{
int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
// int& ret2 = Count(); // err 证明传回来的是临时变量。
const int& ret2 = Count();
return 0;
}
传引用返回
那加个引用又是啥意思呢?
表示临时变量的类型不是 int 而是 int&
int& Count()
{
int n = 0;
n++;
// ...
return n;
}
int main()
{
int ret = Count();
return 0;
}
int& Count()
{
int n = 0;
n++;
// 证明地址都一样
cout << "&n:" << &n << endl;
// ...
return n;
}
int main()
{
int& ret = Count();
// 此时ret是临时对象的别名,也就是n的别名。
// 他们地址都一样
cout << "ret:" << &ret << endl;
return 0;
}
注意:
这个代码是不正确的。间接的一个野指针访问。
猜一猜下面的程序输出结果是什么?👇
是1吗?还是随机值?
int& Count()
{
int n = 0;
n++;
cout << "&n:" << &n << endl;
// ...
return n;
}
int main()
{
int& ret = Count();
cout << "ret:" << &ret << endl;
cout << ret << endl;
return 0;
}
答案:一切皆有可能,VS编译器下现在是随机值。
有些编译器在函数结束后会清空函数所在的那片栈帧,而有些编译器不会清理,我们所用的MSVC编译器就没有清理。
关于函数栈帧,详情可参考这篇文章:👉戳我
第一次输出是1,第二次输出是随机值。
主要看那块空间有没有被覆盖,我们知道,局部变量的销毁意味着把原来的空间的操作权限还给操作系统,原来的空间还是存在的,最初的数据也是不变的,直到被覆盖。
这也就告诉我们,只有特定场景下才用传引用返回。
如果函数返回时,出了函数作用域,如果返回对象还未还给系统,则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。
也就是:我们返回的数据必须是被 static 修饰或者是动态开辟的或者是全局变量等不会随着函数调用的结束而被销毁的数据。
例2
再举个例子加深理解。👇
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << "Add(1, 2) is :" << ret << endl;
Add(3, 4);
cout << "Add(3, 4) is :" << ret << endl;
return 0;
}
// 输出结果是多少呢?
分别是3 和 7
第一次调用Add,ret是c的别名,c为3,因此输出3。
第二次调用Add,c 的值被改为7,ret仍然是c的别名,因此输出7。当然这样写是不对的。
再来:
int& Add(int a, int b)
{
static int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << "Add(1, 2) is :" << ret << endl;
Add(3, 4);
cout << "Add(3, 4) is :" << ret << endl;
return 0;
}
// 输出结果是多少呢?
c被static修饰,跑到静态区去了,只会初始化一次。
每一次调用Add函数就直接return c了
梅开三度:
int& Add(int a, int b)
{
static int c = 0;
c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
cout << "Add(1, 2) is :" << ret << endl;
Add(3, 4);
cout << "Add(3, 4) is :" << ret << endl;
return 0;
}
// 输出结果是多少呢?
不过这样也保证了,c不会因为调用输出语句而被刷新掉。
静态区中的变量不会因为函数结束而被销毁。例如👇
int& Add(int a, int b)
{
int c = 0;
c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(3, 4) is :" << ret << endl;
cout << "Add(3, 4) is :" << ret << endl;
cout << "Add(3, 4) is :" << ret << endl;
cout << "Add(3, 4) is :" << ret << endl;
cout << "Add(3, 4) is :" << ret << endl;
return 0;
}
// 输出结果是多少呢?
那为什么cout << "Add(3, 4) is :" << ret << endl;
就会把空间覆盖了呢?
cout << 流操作
<< 相当于operator<<(ret)
调用函数前要先传参。也就是ret要做这里的参数。ret就是c的别名
传值传引用返回效率比较
struct A
{
int a[100000];
};
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;
}
3.总结:
传引用做参数既能充当输出型参数,也能减少拷贝提高效率。
传值返回,会生成一个tmp对象的拷贝。
- 如果对象比较小(4/8byte),那么一般会使用寄存器存储对象的拷贝。
- 如果对象比较大,这个拷贝通常会存在上一个函数的栈帧。
传引用返回,返回的是对象的引用,也就是返回变量的别名。
但要特别注意,返回的数据不能是函数内部创建的普通局部变量,因为在函数内部定义的普通的局部变量会随着函数调用的结束而被销毁。
我们返回的数据必须是被 static 修饰或者是动态开辟的或者是全局变量等不会随着函数调用的结束而被销毁的数据。
5.指针&引用的区别
汇编底层
int main(int argc, char const *argv[])
{
int a = 10;
//语法上,这里给a这块空间取了个别名,没有新开空间
int& ra = a;
ra = 20;
//语法上,这里定义一个pa指针变量,开辟4个字节空间,存储a的地址
int* pa = &a; // int* 表示解引用时访问4个字节的空间
*pa = 20;
// 但从底层的角度而言:他们是一样的实现,也就是他们会转换成一样的汇编代码。
return 0;
}
实际从汇编实现角度,引用的底层也是类似指针存地址的方式处理的。
语法和底层是隔离开的。
指针和引用不同点:
从使用的角度进行对比
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 没有NULL引用,但有NULL指针。
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
- 引用比指针使用起来相对更安全。
int a = 10;
int *pa = &a; //pa指向a
*pa = 20;
int b = 30;
int *&rpa = pa; //rpa是pa的别名
rpa = &b; //pa 指向了b
尾声
🌹🌹🌹
写文不易,如果有帮助烦请点个赞~ 👍👍👍
Thanks♪(・ω・)ノ🌹🌹🌹
😘😘😘
👀👀👀由于笔者水平有限,在今后的博文中难免会出现错误之处,本人非常希望您如果发现错误,恳请留言批评斧正,希望和大家一起学习,一起进步ヽ( ̄ω ̄( ̄ω ̄〃)ゝ,期待您的留言评论。
附GitHub仓库链接