Hi!我是Duoni!
目录
🍊引用
引用概念:引用并不是一个独立定义的变量或其他值,而是一个对已存在的变量或其他值取了一个别名。这一期间编译器不会为他开辟空间(语法角度),它与其引用对象共同使用内存空间,对别名进行操作会影响引用对象。
引用用法:类型& 引用变量名 = 引用对象名
int num_a = 6;
int& num_b = num_a;//num_b是num_a的别名
为了便于理解,我们将取别名再理解为取外号。
tips:”小狗“被取了别名叫”修勾“。
🍊引用与取地址符的区分
&符在类型名后的是引用
int num_a = 6;
int& num_b = num_a;//给变量num_a定义了一个名为num_b的别名
&符在变量名前是取地址
int num_a = 6;
int* num_b = &num_a;//取num_a的地址给num_b
附:定义别名真的不会另开空间吗
int main()
{
int num_a = 6;
int& num_b = num_a;//num_b是num_a的别名
std::cout << "变量num_a的地址:" << & num_a << std::endl;
std::cout << "别名num_b的地址:" << & num_b << std::endl;
return 0;
}
小结:从语法角度上,定义别名,其别名是和引用对象共占一块空间的。
tips:底层上,引用实际是依靠指针实现的,具体后期深入。
附:对别名的修改会影响到引用对象吗?
int main()
{
int num_a = 6;
int& num_b = num_a;
std::cout << "引用对象修改前:" << num_a << std::endl;
std::cout << "别名修改前:" << num_b << std::endl;
num_b--;
std::cout << std::endl;
std::cout << "引用对象修改后:" << num_a << std::endl;
std::cout << "别名修改后:" << num_b << std::endl;
return 0;
}
小结:对别名的操作会直接对引用对象进行影响。
🍊引用的特性
1.引用在定义时必须被初始化
int main()
{
int& val;//错误,引用的定义(使用)必须指明引用对象
return 0;
}
2.一个变量可以有多个引用,而一个别名也可以有多个别名
int main()
{
int num_a = 2;
int& num_1 = num_a;//一个变量可以拥有多个别名
int& num_2 = num_a;
int& num_3 = num_a;
int& num_cp1 = num_1;//一个别名可以拥有多个别名
int& num_cp2 = num_1;
int& num_cp3 = num_1;
std::cout << "我们都是变量num_a的别名:" << num_1 << " " << num_2 << " " << num_3 << std::endl;
std::cout << "我们都是别名num_1的别名:" << num_cp1 << " " << num_cp2 << " " << num_cp3 << std::endl;
return 0;
}
3.引用一旦引用某一实体,就不能够再引用其他实体。
int main()
{
int val_a = 2;
int val_b = 6;
int& num_cp = val_a;
num_cp = val_b;//num_cp不能够再改变实体,在这只能进行赋值动作
std::cout << &val_a << std::endl;//观察num_cp十分还是val_a的别名
std::cout << &num_cp << std::endl;
return 0;
}
🍊引用的应用场景
1.可用于做参数(输出型参数)
void Change_Num(int& num_1, int& num_2)//用引用接收参数
{
num_1 = 60;//是否会对原值进行改变?
num_2 = 80;
}
int main()
{
int val_1 = 6;
int val_2 = 8;
Change_Num(val_1, val_2);//传参
std::cout << "val_1:" << val_1 << std::endl;
std::cout << "val_2:" << val_2 << std::endl;
return 0;
}
优点:提高了传参的效率,相对于指针的传址解引用修改。引用做参数能更加直观、便利的完成操作。
tips:输入型参数与输出型参数是什么?
(1)、输入型参数是指:传参处传递的是普通变量,且在函数执行后,不会对外部的实体产生影响。
(2)、输出型参数是指:传参处传递的是地址,或者在接收参数时使用别名接收,运行后会对外部实体产生影响。
2.大型对象的传参,提高效率。(节省了参数拷贝的环节)
3、做返回值(输出型返回值,调用者可通过返回值修改引用对象。减少返回时的一次临时拷贝,提高效率)
🍊传值返回与传引用返回
🍊传值返回
传值返回的意义:函数结束后,通过临时拷贝带回所需要的值。
int test_return()
{
int num = 6;
return num;//第三步对值进行拷贝,产生值的临时拷贝//返回的只是num的一份临时拷贝
}
int main()//第一步栈空间开辟
{
int val = test_return();//第二步调用函数//第三步接收返回值
return 0;
}
🍊传值返回的实现
进入主函数,main函数在栈帧创建空间,再调用test_return函数,test_return函数在栈帧开辟空间,走到(return num值)的那一步,编译器做了两个动作:
局部变量的生命周期取决于所存储的物理空间,栈空间在函数执行完毕后就会销毁,这也说明函数内的一切局部变量将随着栈空间的销毁而被销毁。
而如果直接将值进行返回,那将是错误的,因为在函数结束后,所有的局部变量空间都被销毁,此时的返回值并不存在,最终形成了一次越界访问的错误,该值也会是一个随机值。
为了返回有效的值,编译器是这么处理的:第一步,当进行到(return num值)时,会对num进行一次临时拷贝,存储它的数据。第二步,再将这一份临时拷贝的数据进行返回拷贝。
附:若函数中所返回值不存放在栈区,那编译器会怎么做?
int test_return()
{
static int num = 6;//我存放在静态区,栈帧销毁与我无关
//int num = 6;
return num;
}
确实,静态变量存放在静态区,栈区的销毁动作也并不能影响到它。但编译器同样还是会进行临时拷贝再返回的动作,因为这么做是最安全的。
小结:只要是传值返回,都会形成临时拷贝。
🍊传引用返回
int& test_return()//返回num的别名
{
int num = 6;//局部变量在函数结束后会被销毁
return num;
}
int main()
{
int val = test_return();
return 0;
}
🍊传引用返回的实现
num成为引用对象,别名被返回。但函数结束意味着栈帧中局部变量的销毁,而别名一旦被定义引用对象便无法再修改。此时别名被返回到主函数中,若对别名进行访问,其结果是未被定义且不确定的。(可能是原值,也可能是随机值)
小结;如果引用对象出了作用域一定被销毁,那么便一定不能使用引用返回,只能使用传值返回。若要使用传引用返回,那么前提一定要保证引用对象出作用域不被销毁。
附:引用对象被销毁后,再次用别名访问,得到的一定是随机值。因为栈空间销毁后,所有地址都保存着一个随机值,虽然引用对象被销毁,但空间一直存在!
tips:引用对象是全局变量或静态变量或是存于堆空间,就可使用引用返回。
🍊传值返回与传引用返回的优缺点
传值返回的优缺点
优点:安全、稳定,应用范围广
缺点:需要进行一次临时拷贝,效率低、速度慢
传引用返回的优缺点
优点:不用临时拷贝,直接返回别名、对别名的修改可以直接影响实体、高效
缺点:要满足使用条件,才可使用(出作用域不被销毁)
🍊常引用
常引用的意义:const对引用进行修饰,限制权限。
🍊权限规则
1.权限无法被放大
int main()
{
const int val = 6;//val由const修饰,只可读,不可写
int& num = val;//num是变量val的别名,权限发生改变:可读可写。错误!权限被放大
return 0;
}
2.权限可以平移
int main()
{
const int val = 6;//权限:可读,不可写
const int& num = val;//权限:可读,不可写。正确,权限平移
return 0;
}
3.权限可以被缩小
int main()
{
int val = 6;//权限:可读,可写
const int& num = val;//权限:可读,不可写。正确,权限缩小
return 0;
}
🍊隐式类型转换
隐式类型转换的意义:不同类型间的转换,通常情况下,小类型会隐式转换成大类型。
int main()
{
int a = 6;
double b = a;
//整形变量会向双精度浮点值进行转换,形成一个临时变量进行提升,不会对本体进行改变
return 0;
}
tips:不同类型间的运算或赋值,变量会进行隐式转换。小类型会向大类型进行提升、截断。
这一过程是在临时变量上进行实现,因为小类型本身的体量过小,没有办法在本体进行提升,所以本体不会发生改变,但也不会被使用,使用的是提升后的那份临时变量。
附:为什么没有发生权限问题?
因为在类型转化中将整形a提升为双精度浮点值后,虽然临时变量具有常性,但表达式本身也只是赋值功能,将临时变量赋值给b,并没有发生权限的改变。
因为权限规则只对指针和引用有效。
int main()
{
int a = 6;
double& b = a;//定义整形a为双精度浮点值b的引用对象,错误!
//整形变量会向双精度浮点值进行转换,在临时变量中进行二进制的提升,所以此时临时变量具有常性,权限不能被扩大。错误!
return 0;
}
tips:进行隐式类型转换后,临时变量具有常性,所以不能够成为b的引用对象,因为如此的话,具有常性的临时变量权限将受到放大,这是不符合规则的。
若要正确的对其进行引用,应该加上const修饰别名,起到权限平移的功能。
const double& b = a;
附:引用的作用在于形成别名,并且对别名具有可访问、可操作的功能,所以会引发类型转化后的权限问题。所以今后在函数参数接收上或具有类型转换的表达式中,最好使用const修饰,提高接收度。
🍊指针与引用的区别
1.在定义时,引用必须初始化,而指针不要求必须初始化
int main() { int& pre;//error:未初始化引用 int* ret; return 0; }
2.没有空引用,但有空指针
int main() { int& pre = nullptr;//不存在对空的引用 int* ret = nullptr; return 0; }
3.引用在初始化阶段引用一个实体后,就不能再引用其他实体。而指针可以在任何情况下改变指向。(同类型实体)
引用举例
using namespace std; int main() { int num_1 = 5; int num_2 = 10; int& pre = num_1; pre = num_2; cout << "pre别名的值:" << pre << " " << "pre引用对象地址:" << &pre << endl; cout << "num_1的地址" << &num_1 << endl; cout << "num_2的地址" << &num_2 << endl; return 0; }
tips:初始化时别名的引用对象为num_1,尽管后面又被“貌似”的引用num_2,但其只是赋值操作。究其根本就是,别名的赋值操作改变了num_1的值,但并不能改变别名的引用对象。
指针举例
int main() { int num_1 = 10; int num_2 = 20; cout << "num_1:" << &num_1 << " " << "num_2:" << &num_2 << endl; int* pre = &num_1; cout << "pre初始化指向的实体地址:" << " " << pre << endl; pre = &num_2; cout << "pre改变指向的实体地址:" << " " << pre << endl; return 0; }
tips:指针确实可以任意修改指向,而引用则不可以随意改变实体。
4.在sizeof操作符中的含义不同,引用的大小取决于引用对象的类型大小,指针的大小取决于系统环境,32位平台下指针大小为:4字节,64位平台下指针大小为:8字节。
int main() { short num = 2; short& pre = num;//引用的大小取决于引用对象的类型大小 short* p = #//指针的大小取决于环境 cout << "pre:" << sizeof(pre) << " " << "p:" << sizeof(p) << endl; return 0; }
5.引用进行自加的效果会让引用对象的值增加1,指针的自加会让指针向后偏移一个类型大小。
int main() { int num = 5; int& pre = num; int* p = # cout << "pre:" << ++pre << " " << "p:" << ++p << endl; //pre++表示引用对象值自加1,指针自加则表示向后访问一个自身类型大小的地址 return 0; }
6.有多级指针,但没有多级引用。
int main() { int num = 5; int* p = # int** pp = &p;//有多级指针 int& pre = num; int& ppre = pre;//不能形成多级引用,在这只表示:给别名取一个别名 return 0; }
7.访问方式不同:指针需要显式的解引用访问,引用则可以直接使用别名访问。(编译器自己处理)
int main() { int num = 2; int& pre = num; int* p = # cout << "pre:" << pre << " " << "p:" << *p << endl; //引用可直接使用别名访问实体对象,指针必须使用解引用才可访问实体对象 return 0; }
8.引用的使用比指针要安全。
🍊引用与指针底层刨析
从语法角度来看,引用自身是不开辟空间,是与引用对象共用一块空间。但在底层,引用的实现却并不是如此。
先说结论:引用的实现需要开辟空间,并且底层是依靠指针实现,之所以使用方法不同,在于等于引用进行了封装。
🍊底层刨析
代码:
int main()
{
int num = 20;
int& pre = num;
pre = 30;
int* p = #
*p = 40;
return 0;
}
汇编:
int num = 20;
005F1FDF mov dword ptr [num],14h
int& pre = num;
005F1FE6 lea eax,[num]
005F1FE9 mov dword ptr [pre],eax
pre = 30;
005F1FEC mov eax,dword ptr [pre]
005F1FEF mov dword ptr [eax],1Eh
int* p = #
005F1FF5 lea eax,[num]
005F1FF8 mov dword ptr [p],eax
*p = 40;
005F1FFB mov eax,dword ptr [p]
005F1FFE mov dword ptr [eax],28h
🍊汇编解读
int num = 20; 005F1FDF mov dword ptr [num],14h
首先创建一个四个字节的空间用于存储整形。dword表示:d指的是double(双倍),word表示两个字节,共四个字节,num是变量名,将八进制的20存进变量中。(mov)代表移动。
🍊引用实现部分
int& pre = num; 005F1FE6 lea eax,[num]
将num的地址存入到寄存器eax中。
lea表示:装入有效地址,操作数必须为地址。
005F1FE9 mov dword ptr [pre],eax
将寄存器eax中的值移动到pre中。
pre = 30; 005F1FEC mov eax,dword ptr [pre]
将pre的值移动至寄存器eax中。
005F1FEF mov dword ptr [eax],1Eh
将八进制的30移动到存放pre值的寄存器eax中,相当于赋值操作。
小结:从汇编代码中,可以证实使用引用是必须开辟空间的。
🍊指针实现部分
int* p = # 005F1FF5 lea eax,[num]
将变量num的地址存进寄存器eax中。
005F1FF8 mov dword ptr [p],eax
开辟一个四个字节的指针,将寄存器eax的值存进整形指针p中。
*p = 40; 005F1FFB mov eax,dword ptr [p]
将p的值移动到寄存器eax中。
005F1FFE mov dword ptr [eax],28h
将八进制的40赋值给eax。
小结:指针与引用的底层实现相同!
附:指针与引用的相似处
都可以用作函数参数或返回值(输出型参数、输出型返回值)。
tips:引用的不可多级引用是其的缺点,体现在单链表的实现,不可以使用引用,指针更有优势。
小结:指针更为复杂,功能更为强大,可适用的场景更广,但也更为的危险。引用更加的便捷与安全,但适用性还是比指针要狭小一些。
文章到这就结束啦!如果喜欢就关注Duoni叭!