
◆ 博主名称: 晓此方-CSDN博客
大家好,欢迎来到晓此方的博客。
⭐️个人专栏:
◆数据结构系列
◆C语言系列
◆C++系列
⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰
目录
0.1前言&概述
C++对C语言中的许多方面做出了重要修改,让语言变得易写且高效。如增加引用分担指针的工作,优化了代码的复杂性,增加了内联函数取代宏函数避免了很多宏替换问题同时提升运行效率。本期将接续上篇继续为大家带来C++基础部分更加深入的内容。讲解深入骨髓,细节无微不至。以真诚换真心,倾尽全力做到最好现在,让我们开始吧。
一、引用
1.1引用的概念和定义
引用不是新定义一个变量 ,而是给已存在变量取了一个别名 ,编译器不会 为引用变量开辟内存空间 ,它和它引用的变量共用 同一块内存空间。比如:水壶传中李逵,宋江叫"铁牛",江湖上人称"黑旋风";林冲,外号豹子头;一句话:引用就是取别名
1.2引用的使用方式
"类型&引用别名=引用对象"
和 C 语言也公用了一个符号: &( 取地址)
|------------------------|-------|
| 引用在对象前面(例:int* b=&a) | 还是取地址 |
| 引用在类型后面(例:int&rb=b) | 变成了引用 |
1.2.3例
cpp
int& b=a;
1.3取别名的方式
1.3.1一个变量可以有多个别名
引用就是取别名

一个整型a开辟4个字节空间,这四个字节的空间的别名可以是b,c也可以是d。
cpp
int a=0;
int& b=a;
int& c=a;
int& d=a;
d++;
此时a变量所指向的整型空间有三个别名 。由于别名都指向同一块空间 ,**改变别名就可以改变原值,**对d++,同时会让a,b,c,d同时改变。
1.3.2别名的别名
可以给别名取别名。
cpp
int a=0;
int& b=a;
int& d=b;
此时:b是a的别名,d是b的别名,d相当于a的别名。
1.4引用的应用场景
引用在实践中主要是于引用传参 和引用做返回值 中减少拷贝 提高效率和改变引用对象时同时改变被引用对象。
1.4.1引用传参
1.4.1.1部分情况下代替一级指针
cpp
//指针传递法
void swap(int* x,int* y)
{
int swp=*x;
*x=*y;
*y=swp;
}
//引用传递法
void swap(int& rx,int& ry)
{
int swp=rx;
rx=ry;
ry=swp;
}
int main()
{
int a = 5;
int b = 6;
swap ( &a , &b ) ;//指针传递
swap ( a , b ) ;//引用传递
}
C语言传递指针的方法弊端:
- 1、取地址和解引用的繁琐步骤
- 2、使用不透明性------感受上并非操作变量本身
- C++引用传参方法的优势:
- 1、传递即取别名,直接操作别名不需要解引用
- 2、使用透明性------感受直接操作变量本身
1.4.1.2与指针交错使用取代二级指针
cpp
//链表结构体
typedef struct ListNode
{
int val;
struct ListNode* next;
} LTNode, * PNode;
cpp
void ListPushBack(LTNode** pphead, int x)//最初的写法
{
assert(pphead);
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
if (!newNode)
{
perror("malloc:fail");
exit(1);
}
newNode->val = x;
newNode->next = NULL;
if (*ppHead == NULL)
{
*ppHead = newNode;
}
else
{
ListNode* tail = *ppHead;
while (tail->next)
{
tail = tail->next;
}
tail->next = newNode;
}
}
C语言二级指针的弊端:
1,不安全,需要assert断言。
2,解引用复杂。新手不易理解。
C++优化方法:
直接对链表的指针引用取别名:修改链表指针的别名就是修改链表指针本身
cpp
void ListPushBack(PNode& head, int x)//纯引用
{
void ListPushBack_v3(PNode& pHead, int x) {
PNode newNode = new ListNode;
newNode->val = x;
newNode->next = NULL;
if (pHead == NULL) {
pHead = newNode;
} else {
PNode tail = pHead;
while (tail->next) {
tail = tail->next;
}
tail->next = newNode;
}
}
}
其实还有一种介于两者之间的方法:
- 二级指针法是指针的指针;
- 纯引用的法是指针的引用;
- 该方法时引用的指针;
既没有完全脱离指针使用的复杂,又不能更好的发挥引用的优势,不建议使用。
cpp
void ListPushBack(LTNode*& phead, int x)//指针-引用混合
1.4.2引用传返回值
引用返回值的场景相对比较复杂 ,我们在这里简单讲了一下场景,还有一些内容后续类和对象章节中会继续深入讲解。
1.4.2.1引用返回的优势
传值返回:STTop返回值不可直接被修改
原理:
传值返回返回值不会直接给到STTop调用点,而是先创建并传递给一个中间变量 ,这个中间变量具有常性质------不可修改,然后再传递给调用函数作为返回值。
cpp
int STTop(ST& rs)
{
assert(rs.top > 0);
return rs.a[rs.top-1];
}
STTop(st1) = 3;
传引用返回:STTop返回值可直接被修改
原理:
传引用返回不经过中间变量,直接传递别名,可以直接修改别名
cpp
int& STTop(ST& rs)
{
assert(rs.top > 0);
return rs.a[rs.top-1];
}
STTop(st1) = 3;
1.4.2.2引用返回的注意事项
引用虽好,但是不是什么时候都可以用引用
cpp
int& func()
{
int a-0;
int& ra=a;
return ra;
}
使用ra必然引发报错,a的栈帧已经被销毁。可以参考野指针。
: warning C4172: 返回局部变量或临时变量的地址
总结:
- 引用返回返回值可以直接被修改
- 直接返回引用同时还可以减少拷贝的支出
- 返回的值在堆上的时候可以用引用
- 返回值在栈上的时候不可以用引用
1.5引用的特性
1.5.1不开辟空间
引用是不开辟空间的,只对现有的一块空间给他一个别名

1.5.2引用的初始化
由于引用不开辟空间的特性,与指针不同,引用必须初始化。
1.5.3引用的专一性
引用一旦引用一个实体,再不能引用其他任何实体
cpp
int& b = a;
int c = 20;
// 这里并非让b引用c,因为C++引用不能改变指向,
// 这里是一个赋值
b = c;
b始终是a的外号,就像江湖人称豹子头就是指林冲。不指别人
1.6扩展
C++的引用和java的引用是完全不同的。
|-----------------------------|-----------------------------|
| C++ | JAVA |
| C++的引用不能改变指向,而是"一块空间有多个名字"。 | java的引用更像指针,引用可以初始化也可以不初始化。 |
| C++的引用是要和指针相辅相成使用 | JAVA的引用是C++的指针+引用的结合 |
在C++中如链表的指针结点next,仍然必须要用指针解决,因为指针可以改变指向
总结
对C++ :可以改变引用引用的东西但不能改变引用引用什么。
对JAVA:可以改变****引用引用 的东西又可以改变****引用引用 什么。
二,const引用
2.1概念补充
在理解const引用之前,首先需要补充一些官方或非官方概念:
2.1.1权限
非正式术语 ,指一个类成员基于其访问说明符 所在类的继承关系以及访问上下文所拥有的可访问性 (accessibility) 。它决定了程序的其他部分(如类的成员函数、派生类、友元或外部代码)是否能够访问 该成员,以及访问时的范围 (例如,是完全无法访问、仅能读取,还是可以读取和修改)。
(现在看不懂没关系,只需要知道最后一句话)
2.1.1.1权限放大与权限缩小
权限放大:
非正式术语, 指在特定上下文通过特定机制扩大或提升了继承自基类的成员 的可访问范围 ,使得原本访问受限的成员变得更容易被访问。
例如:拥有只读权限限制的变量被使用某种方式使其使其看起来像是权限从只读变成了可读可写。
权限缩小:
非正式术语 ,指在特定上下文通过特定机制限制或降低了继承自基类的成员 的可访问范围 ,使得原本可以访问的成员变得难以或无法被访问。
例如:拥有可读可写权限的变量被使用某种方式使其变得看起来像是从可读可写变成只读。
2.1.2临时对象和中间变量
中间变量:
非正式术语, 通常指在代码执行过程中,为了计算最终结果或完成一系列操作而创建的、具有名称 的变量。它可以是用户显式声明的局部变量,也可以是编译器优化过程中引入的临时存储。
临时对象:正式术语, 这是 C++ 标准中的正式术语。临时对象是不具有名称 的、生命周期短暂的对象,通常在表达式求值过程中为了保存中间结果而创建。
2.1.2.1临时对象的常见创建情况
根据C++的底层需求来考量:
- 1、隐式类型转换
- 2、表达式赋值
- 3、调用函数的参数传递
- 4、函数传值返回
2.1.3常量与常化
常量:
值在初始化后不能被修改的对象或表达式。
常化:将某个对象、引用、指针或函数参数标记为const或
constexpr的过程,以表明其不可变性 或使其成为编译时常量。
2.2const引用的使用方式
2.2.1从常见的问题开始
cpp
const int a = 10;
int& ra = a;
发生错误 :error C2440: "初始化": 无法从"const int"转换为"int &"
这是经典的权限放大错误:const设置了a只能读不能写。所以不能用一个可以读可以写的别名来引用a,不然a权限就放大了。
2.2.1.1解决方法
权限不可以放大:对于一个const限制的对象,需要一个const(相同权限)的别名来引用。
cpp
const int a = 10;
const int& ra = a;
**权限可以缩小:**别名ra的权限相对于a缩小了,但是a本身的权限不变,ra++会报错。
cpp
int a = 10;
const int& ra = a;
a++;
ra++;
2.2.2const引用与常量
不可以对常量进行引用,常量具有常性 ,储存在内存中的只读区域 。对常量进行引用同样会导致权限扩大。
const引用可以解决这个问题
cpp
const int& ra = 30;
2.2.3const与临时对象
取别名是对临时对象取别名
注意,临时对象在被引用后它的声明周期会跟着引用走 。不会销毁,只有引用的别名被销毁它才同时销毁 。(声明周期绑定)
2.2.3.1例一:隐式类型转换
cpp
int a = 10;
int b = 20;
const int& ra = a+b;
这里的a由于隐式类型转换会先给一个临时对象 ,这里的ra引用的本质是引用一个临时对象。
临时对象具有常性 ,只能读不能写。如果给它一个别名,会导致权限放大。必须使用const。
2.2.3.2例二:表达式
cpp
int a = 10;
int b = 20;
const int& rc = a+b;
同理,表达式的运算结果会先放在一个临时对象中,临时对象不能被权限扩大。
2.2.3.3可能存在的误解
cpp
const int a=10;
int ra =a;
这只是单纯的拷贝。不是权限扩大。
2.3const引用的应用价值------传参
以后我们会学一种东西叫模板------函数模板。
cpp
template <class T>
void func (const T& val){
}
首先,模板的体量较大 ,传值传参拷贝消耗大,所以采用引用 ,但是引用又存在权限放大限制即------能传进来的东西非常有限 。(常数、表达式、隐式类型转换的变量等都不能传递),引入const引用就变得格外重要**------扩大了传递参数的范围**。
在C++的STL库中也有这样的设计;

顺序表的插入函数同样采用const引用 来扩展参数传递的范围,提升函数的通用性。
三,指针和引用的区别
指针和引用的区别非常容易在面试中考到!
3.1是否开辟空间
引用不开辟空间(语法层面上我们应该这么认为)引用指向一个空间,而指针开辟一块空间并拷贝入指向对象的地址。
因此会导致第二个区别 :引用在初始化时引用一个对象后就不能再引用 其他对象;而指针可以改变指向。
3.2错误使用
引用比指针更加安全,不需要assert断言。指针很容易出现空指针和野指针的问题,引用相对更加安全。
但是不代表引用绝对安全 ,引用也有"野引用 "和"空引用 "的情况,但是不多见**:** **"野引用"**的问题在前文中有所提及------函数返回栈空间引用
此外,"空引用"也会出现:
cpp
int* ptr =NULL;
int& rb =*ptr;
rb++;
3.3底层
虽然引用和指针有很多的区别 ,但是这些区别全都是在语法层面上的。
实际上:引用的底层 和语法存在巨大的割裂------引用的底层和指针完全一致。
3.3.1底层汇编代码逐句分析:
cpp
//测试代码
int main()
{
int a = 0;
int* pa = &a;
(*pa)=1;
int& ra = a;
ra=2;
return 0;
}
转反汇编:
3.3.1.1为函数建立栈帧:(不用管)
cpp
00007FF7168B14D0 push rdi
00007FF7168B14D2 sub rsp,60h
00007FF7168B14D6 lea rdi,[rsp+20h]
00007FF7168B14DB mov ecx,10h
00007FF7168B14E0 mov eax,0CCCCCCCCh
00007FF7168B14E5 rep stos dword ptr [rdi]
00007FF7168B14E7 mov rax,qword ptr [__security_cookie (07FF7168BC080h)]
00007FF7168B14EE xor rax,rsp
00007FF7168B14F1 mov qword ptr [rsp+50h],rax
00007FF7168B14F6 lea rcx,[__DE5F0DB2_第一个输入输出@cpp (07FF7168C0076h)]
00007FF7168B14FD call __CheckForDebuggerJustMyCode (07FF7168B122Bh)
3.3.1.3创建a变量
- 00007FF7168B1502 是创建的a变量所在地址。
- []符号 的作用类似于解引用 ------不直接访问[]内的对象名,而是访问[]内的对象地址指向的东西。
- dword ptr开辟 一个64位的空间创建变量a
- 指令:mov(move)移动。将值0移动给[a](a所指向的对象)赋值操作。
cpp
int a = 0;
00007FF7168B1502 mov dword ptr [a],0
3.3.1.4指针操作
- lea:把[a]的值取地址传给寄存器rax
- mov:把rax寄存器中的值传给指针指向的值[pa]
- mov:把指针pa的值传递给rax寄存器
- mov:把1赋值给[rax]------实现解引用赋值
cpp
int* pa = &a;
00007FF7168B150A lea rax,[a]
00007FF7168B150F mov qword ptr [pa],rax
(*pa)=1;
00007FF7168B1514 mov rax,qword ptr [pa]
00007FF7168B1519 mov dword ptr [rax],1
3.3.1.5引用操作
可见------与指针操作完全一致:
cpp
int& ra = a;
00007FF7168B151F lea rax,[a]
00007FF7168B1524 mov qword ptr [ra],rax
ra=2;
00007FF7168B1529 mov rax,qword ptr [ra]
00007FF7168B152E mov dword ptr [rax],2
3.3.1.6主函数返回(不用管)
cpp
return 0;
00007FF7168B1534 xor eax,eax
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成 ,功能有重叠性 ,但是各有自己的特点,互相不可替代。