C++ | 引用与指针





一、引用和指针的区别

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;

二、引用的特点

  1. 引用必须初始化、指针可以不初始化
  2. 引用只存在一级引用,指针存在多级指向
  3. 左值引用与右值引用

针对前两个特点不需要做过多的解释,这里主要说明一下左值引用与右值引用。

  • 首先左值顾名思义就是等号左边的值,它可以接收赋值,即左值是可以修改的。右值就是等号右边的值,一本来说等号右边的值是对其他对象进行赋值的,自身是不用修改,引申一下就是右值是不可修改的
  • 对于字面值常量,如 数字1234、字符“ABCD”等都是右值,对于一些将亡值、临时量、无名对象等也是右值。

左值与右值:
在C++11中可以取地址的、有名字的是左值;不能取地址的、没有名字的就是右值。比如我们自己定义的变量int a中的a就是左值,而常量10、字符'A'等都是右值。总的来说可以总结有如下几点:

  1. 左值可以寻址,而右值不可以。&a
  2. 左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。a = 10
  3. 左值可变,右值不可变。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节:右值引用:移动语义和完美转发。

附: C++左值引用和右值引用
引用类型 可以引用的值类型 使用场景
非常量左值 常量左值 非常量右值 常量右值
非常量左值引用
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 引用接收返回值
  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;
}

注:上面函数使用引用的方式返回,我们在主函数内分别使用普通方式和引用方式接收返回值,最终的输出结果显示,只有使用普通方式接收的返回值输出了正确的结果。

然而需要注意的是,不论在主函数中使用怎样的方式接收返回值都是错误的,因为在函数定义时就已经留下了隐患。

分析:

  1. int a = func();的结果是正确的。
    • 在调用func时,使用引用进行了返回值的处理,我们用int a = func();的方式得到了正确的结果。需要注意的是,这种情况并不是真正安全的方式,因为在单线程中执行了func()后,val虽然已经被销毁但是在val中的数据在并没有被其他程序篡改之前就已经被 a 所接收,因此我们执行结果总是正确。
    • 而在多线程中,我们在刚调用完func()后,a 还没来得及接收 val的数据时,该片内存空间可能就会被其他线程使用,从而修改了原val所在空间的值,而我们拿到了数据就会是一个无效值。
  2. 为什么以引用接收的返回值不正确。
    • 之前分析过了,在单线程中我们在调用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 调用之前就提前保存好的值,因此使用这种方式输出会造成一种结果正确的假象。

三、cosnt、一级指针、引用的结合使用

1. 指针与引用结合
	int* p = (int*)0x0018ff44;			//  指针
	int* &&pa = (int*)0x0018ff44;		// 右值引用
	int *const&pb = (int*)0x0018ff44;	//	常引用

	cout << p << " " << pa << " " << pb << endl;	// 输出:0x0018ff44

引用与指针结合,可以看做是对指针指向处的内存取别名。如上例中的 ppapb 都可以表示内存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
	*/
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我叫RT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值