1. 引用的概念和定义
引用就是为一个变量取一个别名,无论是通过原名还是别名进行访问,访问到的都是同一块空间。
从语法上来讲,引用没有开辟额外的空间,逻辑上类似于只有同类成员的联合体(引用类型和引用对象不对应的情况,详见“4. const引用”部分)。
进行引用的方式是:
类型& 引用别名 = 引用对象;
int a = 0;
int& ra = a;// 进行引用
C++中为了避免引入太多的运算符,会复用C语言的一些符号,比如前面的>>,这里引用也和取地址使用了同一个符号&,大家注意使用方法角度区分就可以。
int main()
{
int a = 0;
// 引⽤:b和c是a的别名
int& b = a;
int& c = a;
// 也可以给别名b取别名,d相当于还是a的别名
int& d = b;
++d;
// 这⾥取地址我们看到是⼀样的
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
这里的a,b,c,d共用一块空间,所以输出的结果是4个1。
2. 引用的特性
• 引用在定义时必须初始化
• 一个变量可以有多个引用
• 引用一旦引用一个实体,再不能引用其他实体
其中,第一点和第三点是区别于指针来说的,指针在定义时可以不初始化,并且可以改变指向的空间,而引用则与之不同。
int a = 10;
// 编译报错:“ra”: 必须初始化引⽤
int& ra1;
ra1 = a;
// 正确用法
int& ra2 = a;
执行下面这段代码,不会使得ra2指向b,而只是将b的值赋值给了ra2(a),因为C++引用不能改变指向。
int b = 10;
ra2 = b;
3. 引用的使用
• 引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。
• 引用传参跟指针传参功能是类似的,引用传参相对更方便一些。
void Swap(int* px, int* py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
void Swap(int& rx, int& ry)
{
int tmp = rx;
rx = ry;
ry = tmp;
}
指针变量也可取别名,这里LTNode*& phead就是给指针变量取别名。
这样就不需要二级指针了,相对而言简化了程序。
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTNode, *PNode;
void ListPushBack(LTNode** phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (*phead == NULL)
{
*phead = newnode;
}
else
{
//...
}
}
void ListPushBack(LTNode*& phead, int x)
{
PNode newnode = (PNode)malloc(sizeof(LTNode));
newnode->val = x;
newnode->next = NULL;
if (phead == NULL)
{
phead = newnode;
}
else
{
//...
}
}
• 引用返回值的场景相对比较复杂,以下是用栈的实现来展示的一个简单场景。
int& STTop(ST& rs)
{
assert(rs.top > 0);
return rs.a[rs.top];
}
void STInit(ST& rs, int n = 4)
{
rs.data = (STDataType*)malloc(n * sizeof(STDataType));
rs.top = 0;
rs.capacity = n;
}
int main()
{
ST st;
STInit(st);
STTop(st) += 10;
return 0;
}
将STTop函数的返回值改为引用,可以方便我们在需要时直接对栈顶数据进行修改。
• 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向, Java的引用可以改变指向。
4. const引用
• 可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以缩小,但是不能放大。
// 这里的引用是对a访问权限的放大
const int a = 0;
int& ra1 = a;
这是错误的引用。
a和ra1对应同一块空间,但a的类型为const int,而ra1的类型为int。a对于这块空间只读不写,但ra1对这块空间可读可写,放大了访问权限。
这种情况下,必须使用const引用:
const int& ra2 = a;
在引用时缩小了访问权限,例如下面的代码。
这样的引用是可行的,其中b对对应空间可读可写,但rb只读不可写,否则会报错。
// 这⾥的引⽤是对b访问权限的缩⼩
int b = 20;
const int& rb = b;
• 对于字面量也可进行const引用,这些字面量在被引用时会临时存入某个临时对象(具有常性)中,然后别名对这个临时对象进行引用,该引用的生命周期与别名的生命周期相同,相当于是声明了一个变量来存储该字面量。
const int& ra = 10;
在其他某些情况下,声明的对象也可能会先被放入临时对象中再被引用,例如对表达式进行引用,引用类型与引用对象类型不对应(发生隐式类型转换)等。
const int& rb = a * 3;// 计算结果存放到临时对象
double d = 12.34;
const int& rd = d;// 发生隐式类型转换,结果存放到临时对象
5. 指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有自己的特点,互相不可替代。
• 语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。
• 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
• 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。
• 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。
• sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4byte,64位下是8byte)。
• 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。
空引用的例子:
int& func()
{
int a = 0;
return a;
}
在函数结束时,a的空间就被释放,返还给操作系统了,此时返回的是一个空引用。在函数返回指针时也有类似的野指针的例子。
还有一个例子:
int* pb = NULL;
int& rb = *pb;
在编译时是能通过的,但绝对禁止对rb进行访问,否则会报错。
指针和引用在语法和逻辑上有所不同,但是在被翻译为汇编语言之后我们会发现,二者本质是相同的。
int a = 0;
0077211F mov dword ptr [a],0
//指针
int* pa = &a;
00772126 lea eax,[a]
00772129 mov dword ptr [pa],eax
*pa = 10;
0077212C mov eax,dword ptr [pa]
0077212F mov dword ptr [eax],0Ah
//引用
int& ra = a;
00772135 lea eax,[a]
00772138 mov dword ptr [ra],eax
ra = 10;
0077213B mov eax,dword ptr [ra]
0077213E mov dword ptr [eax],0Ah