1. 前言
-
以往学习过程中将
指针和引用
作为平行的概念,很容易混淆。本文第一件事需要搞懂他们属于什么维度的东西。 -
提出
指针和引用有什么区别?
本身就是一个很糟糕的提问方式。应该先将指针这个概念剥离出命题,得到正确的问法:方法签名中,变量声明为普通类型和引用类型有什么区别?
。解决了第一个问题后,如果已经对结构体、指针、引用不陌生了,那么研究结构体指针的引用的作用
就好理解了。 -
&
作为操作符的用法,不是本文重点,将不做介绍。本文仅梳理&
在方法签名中的意义。 -
文末联系Java中方法除了基本类型都为引用传递的设计,加深对引用的理解。
2. 概念解释
2.1 结构体定义
typedef struct Goods {
int code;
Goods *next;
} Goods;
2.2 指针和引用属于不同维度
- 方法内部的形参声明,合法性验证
void f() {
Goods g1; // 合法
Goods* g2; // 合法
Goods& g3; // 不合法, 编译器报错
}
- 方法签名的形参声明,合法性验证
// 注意Good& g3 也可以写成 Good &g3,《数据结构》书中大多采用第二种写法
void f(Goods g1, Goods* g2, Goods& g3) { // 均合法
}
综上,Goods& 是引用
能定义在方法签名上而不能在方法内部定义。
基本数据类型、结构体类型、结构体指针类型属于一个范畴A;
引用类型属于另外一个范畴B;
通过方法签名,两个范畴得以联系,A能被声明成B;
换言之,指针也有引用类型,并且有实际作用,结构体指针也是指针,方法签名中能声明为引用。
2.3 结构体指针类型的引用
void f(Goods g1, Goods* g2, Goods& g3, Goods*& g4) { // 均合法, Goods*&就是结构体指针类型的引用
}
3. 使用的场景
下题摘自广东工业大学考研真题:
3.1 题目
某仓库用一个带头节点的循环链表L存储各种货物的代码,链表的定义如下:看不懂以下的链表定义点这里
typedef struct Goods {
int code;
Goods *next;
} Goods, *GoodsList;
试写一个算法 void f(GoodsList L, GoodsList &Lc, int c)
,将其中代码code大于c的货物从链表L删除,并将删除的货物组成的一个新的带头节点的循环链表。
3.2 题解
3.2.1 审题
题目要求传入L链表,修改L链表,组成一个新的链表
3.2.1 观察函数签名
/* GoodsList 都为指针变量 L */
void f(GoodsList L, GoodsList &Lc, int c) {
}
- 将函数签名翻译一下:
void f(Goods* L, Goods*& Lc, int c) {
}
Goods*& Lc
的作用:作为算法最终要得到的链表的指针,不用显式return。
如果用return,等价于:
Goods* f(Goods* L, int c) {
Goods* p; // 省略初始化及业务逻辑
return p;
}
- 使用引用相比于用返回值,有什么特点
-
使用返回值,调用层写法:
Goods* request; Goods* response = f(request, 3);
-
使用引用,调用层写法:
Goods* request; Goods* response; f(request, response, 3); // 此时的方法签名,需要有Goods*&的类型,也就是结构体指针的引用
这题使用引用,需要我们在业务逻辑中
注意到方法签名中的引用是算法需要返回的指针
,就足够解题了。为了更加深刻的理解引用,下文将讨论一个话题:使用引用的必要性是什么?。
4. 方法签名上使用引用的必要性
4.1 引例
牧童用绳子牵着牛去吃草。分类讨论牛在方法签名上的应用场景
牧童 == main方法
牛 == main方法定义的变量
绳子 == main方法定义的变量的指针
typedef struct Cow {
bool wantEat;
int times;
} Cow ;
typedef struct Visitor {
bool saw;
Cow *ownCow;
} Visitor ;
-
结构体类型
牧童放牛,目的是取悦游客。如果牛吃草了,游客就会看。本质是读牛的数据但是不修改。void f(Cow cow, Visitor& vistor) { if (cow.wantEat) { vistor.saw = true; } } main方法: Cow cow; Visitor vistor; cow.wantEat = randonBool(); // 为新创建的牛随机设置想不想吃草的属性 f(cow, vistor);
-
结构体引用
牧童放牛,取悦游客的同时,还不能让牛吃太多,牛每次吃草都要记录。本质上是读牛的数据但是且进行修改。void f(Cow& cow, Visitor& vistor) { if (cow.eated && cow.times < 5) { cow.times++; vistor.saw = 1; } } main方法: Cow cow; Visitor vistor; cow.wantEat = randonBool(); // 为新创建的牛随机设置想不想吃草的属性 cow.times = randonInt(); // 为新创建的牛随机设置放牧前吃过几次草的属性 f(cow, vistor);
-
结构体指针
牧童放牛,游客发现后,牧童和游客同时握着绑住牛的绳子。绳子就是牛的指针。void f(Cow* cow, Visitor& vistor) { vistor.ownCow = cow; } main方法: Cow cow; Visitor vistor; f(&cow, vistor); // & 这里做取地址符,也就是取到了cow的指针
-
结构体指针的引用
牧童放牛,游客也带了一头牛,他们两个交换牛的绳子。本质上,调用方的变量的指针也被改变了void f(Cow&* cow, Visitor& vistor) { Cow* temp = vistor.cow; vistor.ownCow = cow; cow = temp; } main方法: Cow cow; Visitor vistor; f(&cow, vistor); // & 这里做取地址符,也就是取到了cow的指针
4.2 结论
如果要对结构体入参用写操作,那么就需要使用引用类型的入参。如果main方法中定义的变量A,传入方法中后将得到新的变量覆盖A,那么方法签名就需要使用结构体指针的引用。
5. Java Web 中对象为引用传递
基于Spring框架的Web 处理层级结构见图
所有request
和response
都是引用传递,能层层封装包装request
和response
都是同一个对象,但是有不会传指针的引用,程序无法交换掉或者篡改request
和response
。