文章目录
一、引用和指针的区别
1. 引用是变量的别名。
int main()
{
int a = 10;
int* p = &a;
int& b = a;
*p = 20;
cout << &a << " " << p << " " << &b << endl;
cout << a << " " << *p << " " << b << endl;
b = 30;
cout << &a << " " << p << " " << &b << endl;
cout << a << " " << *p << " " << b << endl;
return 0;
}
输出
00D3F780 00D3F780 00D3F780
20 20 20
00D3F780 00D3F780 00D3F780
30 30 30
我们可以看到,通过指针和引用都可以达到简介操作变量的目的。接下来我们通过反汇编观察指针和引用的汇编代码,查看他们具体的实现:
int a = 10;
00C91FB2 mov dword ptr[a], 0Ah
int* p = &a;
00C91FB9 lea eax, [a] // 把a的地址拷贝到eax寄存器
00C91FBC mov dword ptr[p], eax // 将eax中保存的地址放入p中(将a的地址装入p中)
int& b = a;
00C91FBF lea eax, [a] // ...
00C91FC2 mov dword ptr[b], eax // ...
* p = 20;
00C91FC5 mov eax, dword ptr[p] // 将p中的值装入eax寄存器中(将a的地址装入eax寄存器中)
00C91FC8 mov dword ptr[eax], 14h // 将14h写入地址为eax的内存中(将20写入a的地址中)
b = 30;
00C91FCE mov eax, dword ptr[b] // ...
00C91FD1 mov dword ptr[eax], 1Eh // ...
可以看到引用的底层实现和指针的底层实现是相同的,只是在代码层面赋值时引用省略了一步解引用的操作,使得操作上更加安全方便。因此说我们说引用是一种更安全的指针。
2. 引用比指针更加安全
引用比指针更加安全具体体现在以下几个方面,比如我们知道有空指针,悬挂指针、逃逸指针等不安全的指针问题导致程序出现错误,但是我们可曾听说过有空引用、悬挂引用等的引用出现过问题?
-
指针的强大功能赋予C/C++语言以较高的灵活性,通过指针我们可以直接操作对象的内存单元,无疑这是非常强大的功能,C/C++语言的许多特性都与其紧密相关。而正是由于它功能上的强大,任何的失误都会使程序出现难以预料的结果。
-
比如使用了野指针(未初始化指针)造成内存的非法访问、使用了已释放资源的空悬指针(悬挂指针)致使程序发生错误、使用了空指针引发程序的崩溃、改变了指针指向时造成的资源的丢失,重复释放了同一片空间造成的未定义行为等。
基于以上种种,C++中引入了“引用”这一概念(后来又引入了智能指针,这里先不做讨论)。 而引用的设计之初就是为了提供一种更安全的指针类型、它能避免了一些常见的错误发生,同时也赋予了一种比之指针更为直观的意义——变量的别名(或对象的别名)。
同时引用具备更好的可读性和实用性,而根据之前的汇编代码分析,引用的底层是通过指针来实现的,准确的来说是 const type * p
类型的指针。
-
这种类型的指针在定义时必须初始化,但是指针终究还是不够安全,比如
int* const p = nullptr;
我们无法杜绝这种情况的发生,因此在使用指针时,特别是在函数的形参中存在指针时,记得给指针判空。 -
比如我们的Swap()函数,此函数用于两个值的交换,如果在设计函数时不进行指针判空的操作,那么程序就会崩溃。
void Swap(int* pa, int* pb)
{
if (pa != nullptr && pb != nullptr)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
}
int main()
{
int a = 10, b = 20;
int* const p = nullptr;
Swap(&a, p); // 注意判空
return 0;
}
- 除此之外,对于
const type * p
类型指针而言,一旦初始化之后就不能再随意的改变指针的指向。
对于以上两点,在引用中都有很好的体现,引用在定义时必须初始化,同时引用定义一旦完成变不可更改所引用的对象。
但是,千万不要写出如下的代码。
int* p = nullptr;
int& a = *p;
二、引用的特点
- 引用必须初始化、指针可以不初始化
- 引用只存在一级引用,指针存在多级指向
- 左值引用与右值引用
针对前两个特点不需要做过多的解释,这里主要说明一下左值引用与右值引用。
- 首先左值顾名思义就是等号左边的值,它可以接收被赋值,即左值是可以修改的。右值就是等号右边的值,一本来说等号右边的值是对其他对象进行赋值的,自身是不用修改,引申一下就是右值是不可修改的。
- 对于字面值常量,如 数字1234、字符“ABCD”等都是右值,对于一些将亡值、临时量、无名对象等也是右值。
左值与右值:
在C++11中可以取地址的、有名字的是左值;不能取地址的、没有名字的就是右值。比如我们自己定义的变量int a
中的a
就是左值,而常量10
、字符'A'
等都是右值。总的来说可以总结有如下几点:
- 左值可以寻址,而右值不可以。
&a
- 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
a = 10
- 左值可变,右值不可变。
a = 10; a = 20;
&
符号与&&
符号:
- 其中
&
作为位运算符是“按位与”,作用于变量名前是“取地址”。前者a & b
,后者&a
。 - 对于
&&
作为逻辑运算符是“逻辑与”。如:a>0 && a<10
。
但在引用中他们被赋予了新的意义,用 &
声明的引用类型被称作 lvalue(左值)引用,而用 &&
声明的引用类型被称作 rvalue(右值)引用。
1. 左值引用
对于一个变量,一个左值量来说,我们可以使用引用为其添加一个别名,例如:
int a = 10;
int &b = a;
int &c = b;
int &d = c;
此时b为a的别名,c也是a的别名,a、b、c、d 他们都表示同一块内存空间。
而对于一个常量,右值量来说,引用也可以为其添加一个别名,例如:
const int a = 10;
const int& b = a;
const int& c = a + 10;
a是一个常量,是右值,a+10
是一个临时量,也是右值,我们使用常量引用可以引用这些右值。
注:const type&
类型的引用——常量左值引用,是一种万能引用。它可以引用任何类型,具体的有非常量左值、 常量左值、 非常量右值、 常量右值 这几种。
2. 右值引用
右值引用与左值引用类似,使用(&&)的方式进行右值引用。
右值引用主要在C++11中实现移动语义、完美转发等功能。比如我们的移动构造函数、移动赋值函数的参数都使用的时右值应用的方式。
有关右值引用,这里不再做详细赘述,具体可参考《深入理解C++11:C++11新持性解析与应用》 一书,第三章3.3节:右值引用:移动语义和完美转发。
引用类型 | 可以引用的值类型 | 使用场景 | |||
---|---|---|---|---|---|
非常量左值 | 常量左值 | 非常量右值 | 常量右值 | ||
非常量左值引用 Type& | Y | N | N | N | 无 |
常量左值引用 const Type& | Y | Y | Y | Y | 常用于类中构建拷贝构造函数 |
非常量右值引用 Type&& | N | N | Y | N | 移动语义、完美转发 |
常量右值引用 const Type&& | N | N | Y | Y | 无实际用途 |
3. 左值引用与右值引用示例
3.1 引用接收返回值
- func函数在调用完成时,通过寄存器 eax 带出函数内 val 的值。 这里采用不同的方式对函数返回值进行接收。
int func()
{
int val = 10;
return val;
}
int main()
{
int ia = func(); // ok
//int& ib = func(); // error ,不能引用将亡值
const int& ic = func(); // ok, 万能引用
// int tmp = *eax
// const int& ic = tmp
int&& id = func(); // ok , 右值引用,可以提升对象生命周期
// int tmp = *eax
// int&& id = tmp;
const int&& ie = func(); // 常右值引用
// int tmp = *eax
// int&& id = tmp;
return 0;
}
我们使用普通类型接收时,eax将自身的值赋值到 ia 变量中,而使用引用接收时,却会出现错误。
因为引用是变量的别名,现在 ib 想引用一块临时量(这里把寄存器看做是一片临时空间)为别名,试想如果引用成功,会发生什么
- 当该赋值语句结束,此临时变量的生存周期结束,临时量被销毁(寄存器改变、栈帧回退等),那么此时 ib 引用的是一片非法地址空间
- 此空间已被系统回收,任何其他程序都可以申请该地址空间的使用,而 ib 一直引用的该空间已经失去了原有的意义,里面的值很可能已经被修改,该空间内存储的值对我们而言也是无效值。
需要说明的是,当函数的返回值在4字节,8字节,大于8字节时,分别会使用 eax、eax+edx、提前在在主函数开辟栈帧。在此示例中,寄存器中的值如果被引用,那么 ib 将永远指向 eax寄存器,而eax寄存器中的值一直都在改变,已经不是我们想要的func()函数的返回值了。
因此,C++从语法上限制了这种引用的使用方式(非常量的左值引用只能引用非常量的左值)。
而对于其他几种引用方式,都具有提升临时量的生存周期的能力。具体表现在引用临时量时,通过申请一个专用空间保存该临时量,然后对该空间进行引用。(可以通过反汇编查看具体步骤)
3.2 引用返回返回值
注:以下这种方式返回引用的函数定义也是正确的。函数能返回引用的条件是,返回值的生存期不受函数影响,否则不能以引用方式返回。
int& func()
{
int val = 10;
return val;
}
int main()
{
int a = func();
int& b = func();
//cout << a << " " << b << endl;
cout << a << endl; // 10
cout << b << endl; // 2035202640
return 0;
}
注:上面函数使用引用的方式返回,我们在主函数内分别使用普通方式和引用方式接收返回值,最终的输出结果显示,只有使用普通方式接收的返回值输出了正确的结果。
然而需要注意的是,不论在主函数中使用怎样的方式接收返回值都是错误的,因为在函数定义时就已经留下了隐患。
分析:
int a = func();
的结果是正确的。- 在调用func时,使用引用进行了返回值的处理,我们用
int a = func();
的方式得到了正确的结果。需要注意的是,这种情况并不是真正安全的方式,因为在单线程中执行了func()后,val虽然已经被销毁但是在val中的数据在并没有被其他程序篡改之前就已经被 a 所接收,因此我们执行结果总是正确。 - 而在多线程中,我们在刚调用完func()后,a 还没来得及接收 val的数据时,该片内存空间可能就会被其他线程使用,从而修改了原val所在空间的值,而我们拿到了数据就会是一个无效值。
- 在调用func时,使用引用进行了返回值的处理,我们用
- 为什么以引用接收的返回值不正确。
- 之前分析过了,在单线程中我们在调用func()后能够及时的拿到原val所在空间的数据,而
int& b = func();
实际上也及时的引用到了那块儿内存,因此我们在使用cout << a << " " << b << endl;
这种方式输出的时候,b 的结果“好像”就是正确的结果。 - 而使用先输出 a ,在输出 b 的方式时,b 的值显示不正确是因为,cout 本身就是一个函数,在调用cout时会开辟栈帧,此栈帧空间内的数据会覆盖掉原val所在空间的数据,而由于 b 是对那块儿内存的引用,所以 b 输出了无效值。
- 而在使用连续输出 a、b的值时,会先将实参数据入栈(将a的值与b的值压入栈中充当函数调用的形参),在调用cout函数后,实际上原val所在数据的值已经被修改,但我们输出的 b 的值是在cout 调用之前就提前保存好的值,因此使用这种方式输出会造成一种结果正确的假象。
- 之前分析过了,在单线程中我们在调用func()后能够及时的拿到原val所在空间的数据,而
三、cosnt、一级指针、引用的结合使用
1. 指针与引用结合
int* p = (int*)0x0018ff44; // 指针
int* &&pa = (int*)0x0018ff44; // 右值引用
int *const&pb = (int*)0x0018ff44; // 常引用
cout << p << " " << pa << " " << pb << endl; // 输出:0x0018ff44
引用与指针结合,可以看做是对指针指向处的内存取别名。如上例中的 p
、pa
、pb
都可以表示内存0x0018ff44
。
2. 指针与引用的转换
int a = 10;
int* p = &a;
int*& q = p; // int **q = &p;
针对 int*& q = p;
语句,把等号左边的&
换成 *
,等号右边加上一个 &
,就变成了 int **q = &p;
。
对于指针与引用相互结合使用时,不方便判断语句是否正确,我们可以将其转换为纯指针的形式。如下例:
请判断以下语句是否正确?
int a = 10;
int* p = &a;
const int*& q = p;
如果我们简单的按一级指针判断,如以上代码为 cosnt int* ⇐ int*
的类型赋值,按照指针语法规则是没有问题的,那么我们就会误认为该语句没有问题。
分析:
我们将该语句转换为纯指针的形式为 const int ** q = &p
,即 cosnt int ** ⇐ int**
类型赋值 。
很明显该语句有问题。因为当const修饰二级指针时,等式两边需同时有const 或 左边等式有两个cosnt 。
int* p1 = &a;
const int* const* q1 = &p1;
/* 等式左边有两个cosnt
const int* cosnt* <== int**
*/
cout << typeid(q1).name() << " <== " << typeid(&p1).name() << endl;
const int* p2 = &a;
const int** q2 = &p2;
/* 等式两边同时有cosnt
const int** <== const int**
*/
cout << typeid(q2).name() << " <== " << typeid(&p2).name() << endl;
- 其中根据第一个
const int* cosnt* <== int**
修改得
const int* const& q11 = p1; // 常 int const * <= int *
- 其中根据第二个
const int* cosnt* <== int**
修改得
const int*& q22 = p2; // 常 int const * <== int const *
且以上两个引用都为常引用,只读不可写。
3. 指针引用练习
指针引用,即对指针的引用。形如 int *& q = p; //p是指针
。
注:不存在指向引用的指针。(为什么不能定义指向引用的指针?)引用不是一个对象,所以不存在指针去指向一个引用。
以下代码错误的有?
// A.
int a = 10;
int* p = &a;
int*& q = p;
// B.
int a = 10;
int* const p = &a;
int*& q = p;
// C.
int a = 10;
const int* p = &a;
int*& q = p;
// D.
int a = 10;
int* p = &a;
const int*& q = p;
答案:(鼠标选中查看)
👉 错误:B、C、D,正确:A👈 |
思路:将指针的引用表示成纯指针的形式进行比较。有关指针的比较方法请参考博文:练习题:C++指针练习
解析:内含注释
// A.
int a = 10;
int* p = &a;
int*& q = p;
/*
int** <== int**
*/
// B.
int a = 10;
int* const p = &a;
int*& q = p;
/*
int** <== int* const*
int* * int* cosnt* // 取消前缀(int*)继续比较
int* <== int const* // error
*/
// C.
int a = 10;
const int* p = &a;
int*& q = p;
/*
int** <== cosnt int**
int* * cosnt int* * // 取消后缀(*)先比较前缀
int* <== const int* // error
*/
// D.
int a = 10;
int* p = &a;
const int*& q = p;
/*
cosnt int** <== int**
cosnt修饰二级指针:
1.两边都有cosnt ✖
2.左边有两个cosnt ✖
// error
*/