文章目录
- [1. 引用](#1. 引用)
-
- [1.1 引用的概念和定义](#1.1 引用的概念和定义)
- [1.2 引用的特性](#1.2 引用的特性)
- [1.3 引用的使用](#1.3 引用的使用)
- 小小的延伸
- [1.4 const引用](#1.4 const引用)
- [1.5 指针和引用的关系(面试常考)](#1.5 指针和引用的关系(面试常考))
1. 引用
1.1 引用的概念和定义
引⽤不是新定义⼀个变量,⽽是给已存在变量取了⼀个别名,编译器不会为引⽤变量开辟内存空间(指针会开辟空间), ++它和它引⽤的变量共⽤同⼀块内存空间++。比如:水浒传中林冲,外号豹⼦头
类型&引⽤别名=引⽤对象;
C++中为了避免引⼊太多的运算符,会复⽤C语⾔的⼀些符号,⽐如前⾯的<<和>>,这⾥引⽤也和取地址使⽤了同⼀个符号&。
如何区分取地址和引用呢?
&i->在变量之前是取地址
int&->在类型之后是引用
cpp
#include<iostream>
using namespace std;
int main()
{
int a = 0;
// 引⽤:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
// 这⾥取地址我们看到是⼀样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}

cpp
#include<iostream>
using namespace std;
int main()
{
int a = 0;
int& b = a;
int& c = a;
int& d = b;
++d; //++d就相当于++b,也就是++a
cout << &a << endl;//这里的&是取地址
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}

1.2 引用的特性
• 引⽤在定义时必须初始化
int& b=a;//必须初始化
int& c;//编译报错
• ⼀个变量可以有多个引⽤
int& b=a;
int& c=a;
• 引⽤⼀旦引⽤⼀个实体,再不能引⽤其他实体
cpp
#include<iosteam>
using namespace std;
int main()
{
int a = 10;
int& b = a;
int c = 20;
// 这⾥并⾮让b引⽤c,因为C++引⽤不能改变指向
// 这⾥是⼀个赋值
b = c;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
c
#include<iostream>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST& rs, int n = 4)
{
rs.a = (STDataType*)malloc(n * sizeof(STDataType));
rs.top = 0;
rs.capacity = n;
}
// 栈顶
void STPush(ST& rs, STDataType x)
{
assert(ps);
// 满了, 扩容
if (rs.top == rs.capacity)
{ printf("扩容\n");
int newcapacity = rs.capacity == 0 ? 4 : rs.capacity * 2;
STDataType* tmp = (STDataType*)realloc(rs.a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
rs.a = tmp;
rs.capacity = newcapacity;
}
rs.a[rs.top] = x;
rs.top++;
}
// int STTop(ST& rs)
int& STTop(ST& rs)
{
assert(rs.top);
return rs.a[rs.top];
}
int main()
{
// 调⽤全局的
ST st1;
STInit(st1);
STPush(st1, 1);
STPush(st1, 2);
cout << STTop(st1) << endl;
STTop(st1) += 10;
cout << STTop(st1) << endl;
return 0;
}
c
#include<iostream>
using namespace std;
typedef struct SeqList
{
int a[10];
int size;
}SLT;
// ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。
void SeqPushBack(SLT& sl, int x)
{}
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, *PNode;
// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
// 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
//...
}
}
int main()
{
PNode plist = NULL;
ListPushBack(plist, 1);
return 0;
}
延伸

1.3 引用的使用
1.引⽤在实践中主要是于引⽤传参和引⽤做返回值中++减少拷⻉提⾼效率++ 和++改变引⽤对象时同时改变被引⽤对象++。
1) 引⽤传参跟指针传参功能是类似的,引⽤传参相对更⽅便⼀些。
2) 传值返回会产生临时变量,传引用返回本质是返回返回对象的引用(别名),从而提高了效率,同时可以改变返回对象(不是所有场景都可以传引用返回,如果是一个返回值是当前函数的局部对象,传引用返回是有风险的,这个后面会深入讲解)
2.指针传地址的时候可能出现二级指针,并且两数交换要在函数内部进行解引用,引用可以很好的提高效率,不过大部分场景可以替代指针,部分场景还是离不开指针,比如++链表,树,结点定义位置++,原因是c++的引用无法改变指向,结点一定存在改变指向,Java可以
3.C++的引⽤跟其他语⾔的引⽤(如Java)是有很⼤的区别的,除了⽤法,最⼤的点,C++引⽤定义后不能改变指向, Java的引⽤可以改变指向。
cpp
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
void Swap(int* rx,int* ry)
{
int tmp=rx;
rx=ry;
ry=tmp;
}
//这两个函数可以同时存在
int main()
{
int x = 0, y = 1;
Swap(x,y);
cout << x <<" " << y << endl;
Swap(&x, &y);
cout << x << " " << y << endl;
return 0;
}
cpp
//指针
void Swap(int*& rx, int*& ry)
{
int* tmp = *rx;
*rx = *ry;
*ry = *tmp;
}
void Swap(int** rx,int** ry)
{
int* tmp=*rpx;
*rpx=*rpy;
*rpy=*tmp;
}
//这两个函数不可以同时存在,会出现调用不明确的风险
int main()
{
int* x = &a;
int* y = &b;
Swap(x,y);
cout << x <<" " << y << endl;
Swap(&x, &y);
cout << x << " " << y << endl;
return 0;
}
cpp
#include<iosteam>
using namespace std;
typedef struct SeqList
{
int a[10];
int size;
}SLT;
// ⼀些主要⽤C代码实现版本数据结构教材中,使⽤C++引⽤替代指针传参,⽬的是简化程序,避开复杂的指针,但是很多同学没学过引⽤,导致⼀头雾⽔。
void SeqPushBack(SLT& sl, int x)
{
}
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, *PNode;
//相当于typedef struct ListNode *PNode;(重命名)
//typedef struct ListNodeLTNode;
// 指针变量也可以取别名,这⾥LTNode*& phead就是给指针变量取别名
// 这样就不需要⽤⼆级指针了,相对⽽⾔简化了程序
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
//找到尾结点,newnode链接到尾结点
}
}
int main()
{
PNode plist = NULL;
ListPushBack(plist, 1);
return 0;
}
传值返回
func栈帧局部结束后销毁,ret的空间释放,传值返回,返回的是拷贝的临时变量
cpp
int func()
{
int ret=0;
return ret;
}
int main()
{
int a=func();
return 0;
}
传引用返回
这个代码很危险,访问的是野指针(与拷贝无关),结果可能是随机值或0
但越界访问为什么不报错呢
因为越界不一定报错,越界是抽查
cpp
int& func()
{
int ret=0;
return ret;
}
//相当于我们返回了ret的别名
int main()
{
int a=func();
cout<<a<<endl;
return 0;
}
小小的延伸
有个很神奇的现象
cpp
int& func1()
{
int ret=0;
return ret;
}
int& func2()
{
int y=123;
return y;
}
int main()
{
int& a=func1();
cout<<a<<endl;
func2();
cout<<a<<endl;
return 0;
}
运行结果

为什么a的值变成了123?
可以这样理解:func1的栈帧销毁后,系统又将func1的空间分配给了func2,a又是ret的别名,ret的内存空间释放后,a还指向那块空间,而那块空间又变成了y,所以a输出的是y的值
- 那怎么修改呢
c
int& func1()
{
static int ret=0;
return ret;
}
int& func2()
{
int y=123;
return y;
}
int main()
{
int& a=func1();
cout<<a<<endl;
func2();
cout<<a<<endl;
return 0;
}
1.4 const引用
• 可以引⽤⼀个const对象,但是必须⽤const引⽤。const引⽤也可以引⽤普通对象,因为对象的访问权限在引⽤过程中可以缩⼩,但是不能放⼤。
++权限的放大和缩小只存在于指针和引用++
• 不需要注意的是类似 int& rb = a* 3; double d = 12.34; int& rd = d; 这样⼀些场景下a*3的和结果保存在⼀个临时对象中, int& rd = d 也是类似,在类型转换中会产⽣临时对象存储中间值,也就是rb和rd引⽤的都是临时对象,⽽C++规定临时对象具有常性,所以这⾥就触发了权限放⼤,必须要⽤常引⽤才可以。
• 所谓临时对象就是编译器需要⼀个空间暂存表达式的求值结果时临时创建的⼀个未命名的对象, C++中把这个未命名对象叫做临时对象。
cpp
int main()
{
const int a = 10;
// 编译报错:error C2440: "初始化": ⽆法从"const int"转换为"int &"
// 这⾥的引⽤是对a访问权限的放⼤
int& ra = a;
// 这样才可以
const int& ra = a;
// 编译报错:error C3892: "ra": 不能给常量赋值
ra++;
// 这⾥的引⽤是对b访问权限的缩⼩,可以
int b = 20;
const int& rb = b;
// 编译报错:error C3892: "rb": 不能给常量赋值
//rb++;
return 0;
}
c
//指针
const int* p1=&a;
//报错:不能权限放大
int* p2=p1;
//可以权限缩小
int* p3=&e;
const int* p4=p3;
权限缩小的应用
c
//如果不希望形参的改变影响实参,最好给形参加上const
void func(const int& x)
{
}
int main()
{
int y=0;
func(y);
//形参加上const才会成功传参
const int z=1;
func(z);
func(1);
return 0;
}
再讲第二个例子之前,先理解下面的例子
c
int i=1;
double d=i;
int p=(int)&i;
实际上i先给了一个临时变量,临时变量再给d,&i先强制转成int型,再给一个临时变量,临时变量再给p。临时变量具有常性,就像被const修饰一样
cpp
#include<iosteam>
using namespace std;
int main()
{
int a = 10;
const int& ra = 30;
// 编译报错: "初始化": ⽆法从"int"转换为"int &"
int& rb = a * 3;
//可以
const int& rb = a*3;
double d = 12.34;
// 编译报错:"初始化": ⽆法从"double"转换为"int &"
int& rd = d;
const int& rd = d;
return 0;
}
1.5 指针和引用的关系(面试常考)
C++中指针和引⽤在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。
• 语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。
• 引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。
• 引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。
• sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8个字节)
• 指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。