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

1#include<iostream> 2using namespace std; 3int main() 4{ 5 int a = 0; 6 int& b = a; 7 int& c = a; 8 int& d = b; 9 ++d; //++d就相当于++b,也就是++a 10 cout << &a << endl;//这里的&是取地址 11 cout << &b << endl; 12 cout << &c << endl; 13 cout << &d << endl; 14 return 0; 15} 16

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

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

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