突破编程_C++_基础教程(引用)

1 引用是什么

引用是变量的别名。对于变量名而言,C++ 实际上对其是不作存储的,在汇编以后不会出现变量名,变量名作用只是用于方便编译器成汇编代码,是给编译器看的,同时也是方便人编写与阅读代码。作为变量名的别名,引用自然也不会在内存中存储,它只是提供了另一种访问已分配内存的方式。另外,引用也并没有自己的内存地址,即使对引用进行取地址操作,返回来的结果也是原变量地址,如下为样例代码:

#include <iostream>  

int main() {

	int val = 1;
	int& refVal = val;

	printf("val address = %p\n", &val);
	printf("refVal address = %p\n", &refVal);

	return 0;
}

上面代码输出为:

val address = 00000025324FF724
refVal address = 00000025324FF724

从上面的输出可以看到,引用指向的地址(00000025324FF724)就是原变量的地址(00000025324FF724),因此,对引用的任何操作实际上修改的就是原有变量:

#include <iostream>  

int main() {

	int val = 1;
	int& refVal = val;
	refVal = 2;

	printf("val = %d\n", val);
	
	return 0;
}

上面代码输出为:

val = 2

由于引用是变量的别名,所以其生命周期应该严格与变量一致。所以如果在变量被销毁的情况下,仍然访问其引用,有可能会造成程序崩溃,这种现象被称为是悬挂引用。如下为样例代码:

#include <iostream>  

int& getVaRef() {
	int val = 1;
	return val;
}

int main() 
{
	int& val = getVaRef();		//错误:在变量被销毁的情况下,仍然访问其引用
	return 0;
}

上面代码会报出警告: warning C4172: returning address of local variable or temporary: val

2 引用与函数

在C++中,引用与函数之间的关系主要体现在函数参数传递和函数返回值两个方面。
当使用引用传递函数参数时,这意味着函数接收的是实际参数的别名,而不是它的副本。通过引用传递参数,函数能够直接修改传递进来的变量的值。
引用也可以作为函数的返回值,但这种用法要谨慎使用,防止出现前面所提到的悬挂引用的问题(返回局部变量的引用有可能会导致程序的崩溃,该引用将指向无效的内存)。一般情况下,在返回值方面的应用,引用一般是作为类成员函数的返回值,这样只要是该类对应对象的生命周期还存在,则引用所指向的内存地址是有效的。

2.1 引用作为入参

使用引用作为入参,一方面可以避免按值传递时所造成的数据拷贝,另外一方面也提供了函数对入参做修改的支持,比如经常可以看到一个数据交换的例子:

#include <iostream>

//按值传递
void swapByValue(int a, int b)
{
	int c=a;
	a=b;
	b=c;
}

//按引用传递
void swapByRef(int& a, int& b)
{
	int c=a;
	a=b;
	b=c;
}

int main()
{
	int a=1,b=2;
	swapByValue(a,b);				//a,b 交换值失败
	printf("swapByValue: a=%d, b=%d\n",a,b);
	swapByRef(a,b);					//a,b 交换值成功
	printf("swapByRef: a=%d, b=%d\n",a,b);
	return 0;
}

上面代码的输出为:

swapByValue: a=1, b=2
swapByRef: a=2, b=1

所以有一些技术文档会把按引用传参认为成一种多返回值模式(对于上面代码而言,void swapByRef(int& a, int& b) 中的 a 、b 也被认为成是返回值),这种说法虽然不严谨,但是很形象的表明了使用引用作为入参所带来的功能特点。

2.2 引用作为返回值

引用作为返回值大多是用于类的成员函数,主要目的是减少无谓的数据复制过程,比如如下代码:

class A
{

public:
	vector<int> getVals()		//按值返回
	{
		return m_vals;
	}

private:
	vector<int> m_vals;
};

上面代码中成员函数 vector<int> getVals() 会返回成员变量 m_vals 的一份拷贝,如果此时的 m_vals 有巨量的元素(比如超百万),则只是这个拷贝过程就需要耗费大量的时间,并且由于多了一份拷贝,也会占用很多内存空间。所以建议使用引用作为返回值:

class A
{

public:
	vector<int>& getVals()		//按引用返回
	{
		return m_vals;
	}

private:
	vector<int> m_vals;
};

不过按照上面引用返回的方式,调用者是可以通过引用来修改成员变量 m_vals 的值的,如果想避免这种情况,可以使用 const :

const vector<int>& getVals()

3 引用与指针的比较

(1)基本概念
引用是变量的别名,是一个已经存在的变量的另一个名字,不占用存储空间。一旦引用被定义并初始化,就不能被重新指向另一个变量。引用总是指向在初始化时被指定的变量,直到该引用和变量都超出作用域。
指针是一个变量,占用存储空间( 32 位平台编译是 4 个字节, 64 位位平台编译是 8 个字节),其值为另一个变量的地址。非 const 的指针可以重新指向另一个变量,即可以改变指针的值,使其指向另一个地址。
(2)访问所指向的变量
使用引用就像使用它所引用的变量一样。例如:int val=1; int& valRef=val; valRef=2;, 该段代码将 val 的值由 1 修改为 2。
使用指针访问其指向的变量需要使用解引用操作符(*)。例如:int val=1; int* prt=&val; *prt=2; , 该段代码将 val 的值由 1 修改为 2。
(3)初始值
引用在定义时必须要同时初始化指向有效的变量,没有空引用的概念。
指针可以是空( nullptr ),指向不确定的内存,或者指向已经被释放的内存。
(4)运算
引用没有自己的地址,不可以进行指针算术。
指针有自己的地址和值,可以进行指针算术(如递增、递减、比较等)。
(5)用途
引用通常用于函数参数和函数返回值,以确保传递的是变量的别名而非副本,从而可以避免值拷贝的过程并且能够修改实际参数的值。
指针除了能够用于函数参数和函数返回值,还可以用于动态内存分配、构建数据结构(如链表、树、图等)以及以函数指针的形式用于回调等过程。
(6)安全性
引用总是指向有效的对象,并且不能被重新指向。所以其类型是固定的,更为安全。
指针可以强制类型转换、可以为空、可以指向无效的地址,所以在使用不当的情况下容易引起程序崩溃。

4 右值引用

注:该部分内容涉及到 C++11 新特性。
右值引用是 C++11 引入的新特性,用于支持移动语义(move semantics)和完美转发(perfect forwarding)。右值引用的主要目的是识别临时对象(即右值),从而将其资源安全的转移给指定的变量,避免了耗时的拷贝过程,提高了程序的效率。

4.1 右值引用的基本概念

C++ 中的表达式可以分为左值(lvalue)和右值(rvalue)。左值是指表达式结束后仍然存储位置的对象,允许我们通过其名称来访问它(例如变量)。相反,右值是一个临时对象,通常作为表达式的结果出现,并且在其所在的表达式结束后就不再存在。例如,字面量、临时变量和表达式的返回值都是右值。
C++ 中的右值又可以分为两个子类别:纯右值(prvalue)和将亡值(xvalue)。
纯右值(prvalue)是一个表达式的结果,该值在其表达式结束后就立即销毁,并且没有办法获取其地址。字面量(如 1 或 “abc” )、临时对象以及某些表达式的返回值都是纯右值的例子。如下是一个纯右值的例子:

int val = 1;	// 1 是一个纯右值

将亡值(xvalue)是 C++11 新增的概念,该值与右值引用相关,它表示一个对象的资源可以被安全地移动到另一个对象中。
右值引用允许我们识别并绑定到将亡值。右值引用的语法是在类型后面加上两个与号( && ),例如 int&& 是一个指向整型的右值引用。右值引用本质上还是一个引用,其还是作为某个变量的别名,所以在定义一个右值引用后要立即进行初始化。通过右值引用的定义,将亡值的生命周期将会延长,从而避免了拷贝所带来的性能损失。

4.2 引用折叠

上面章节中使用符号 && 来定义一个右值引用,但是当这个符号在模板中使用时,则其不一定表示右值,它绑定的类型是不确定的,可能是右值也可能是左值引用。经过类型推导后的 T&& 类型,相比于确定类型的右值引用 &&,会发生类型变化,这种变化被称为引用折叠,如下为样例代码:

#include <iostream>  

using namespace std;

template<class T>
void testFunc(T&& val) {}

int main() 
{
	testFunc(1);		//1 是右值
	int val = 1;
	testFunc(val);		//val 是左值

	return 0;
}

上面代码说明, T&& 是左值还是右值引用取决于它被赋值的类型,如果被一个右值初始化,则它是一个右值;如果被一个左值初始化,则它是一个左值。
引用折叠的规则如下:
(1)所有右值引用折叠到右值引用上仍然是一个右值引用。(T&& && 折叠为 T&&
针对一个类型为 T 的右值引用,尝试创建一个这种类型的右值引用,它仍然会保持为 T 的右值引用。
(2)所有的其他引用类型之间的折叠都将变成左值引用。(T& &T& &&T&& & 折叠为 T&
针对一个类型为 T 的左值引用,尝试创建一个这种类型的引用的引用(无论是左值引用还是右值引用),它都会折叠成一个简单的 T 的左值引用。

4.3 move (移动语义)

move (移动语义)也是 C++11 引入的新特性,允许对象在某些情况下将其资源(如动态分配的内存)从一个对象转移到另一个对象,而不是进行深拷贝。这个移动过程是通过右值引用、移动构造函数以及移动赋值运算符来实现的。
move 的本质是将左值强制转换为右值引用,避免拷贝带来的性能损失,该函数对具有移动构造函数的类类型有效,但是对于一些基本类型(比如 int 、 float 等)使用时,仍然会发生拷贝( C++ 中所有容器都支持 move 操作)。
移动构造函数与移动赋值函数(重载 operator= 实现)都接受一个右值引用作为参数,并从该参数中移动资源(而非复制资源)。从而是将资源的所有权(如动态内存)从源对象转移到目标对象。
重点注意:移动语义并不直接避免析构函数的调用。
析构函数是C++对象生命周期的一部分,当一个对象离开其作用域或者被显式地销毁时,它的析构函数会被自动调用。析构函数用于释放对象在生命周期中可能获取的资源,比如动态分配的内存、打开的文件句柄等。
移动语义的目的是优化资源转移,而不是避免析构。当对象被移动时,资源(如内存指针)从源对象转移到目标对象,然后源对象通常会被置于一个有效但未定义的状态(通常是将其资源指针设置为 nullptr )。这意味着源对象在移动之后仍然会调用其析构函数,但析构函数不会释放任何资源,因为资源已经被转移走了。
如下是一个使用 move 、移动构造函数以及移动赋值运算提高程序性能的实现过程:

#include <iostream>  

using namespace std;

class A
{
public:
	A()
	{
		printf("execute constructor function\n");
		m_val = new int(0);
	}
	A(A&& a) noexcept		//移动构造函数 
	{
		if (this != &a) {	// 防止自赋值  
			printf("execute move constructor function\n");
			m_val = a.getVal();	//内存转移
			a.initVal();	//源对象的内存做 nullptr 处理,防止其在析构时出现二次释放内存的行为
		}
	}
	A& operator=(A&& a) noexcept		//移动赋值函数 
	{
		if (this != &a) // 防止自赋值  
		{
			printf("execute operator=() function\n");
			m_val = a.getVal();	//内存转移
			a.initVal();	//源对象的内存做 nullptr 处理,防止其在析构时出现二次释放内存的行为
		}
		return *this;
	}
	~A()
	{
		printf("execute destructor function\n");
		if (nullptr != m_val)
		{
			delete m_val;
			m_val = nullptr;
		}
	}

public:
	int* getVal() const
	{
		return m_val;
	}

	void initVal()
	{
		m_val = nullptr;
	}

private:
	int* m_val = nullptr;
};

int main()
{
	A a1;
	A a2(move(a1));		//调用移动构造函数
	A a3;
	A a4;
	a4 = move(a3);		//调用移动赋值函数,注意:如果写成 A a4 = move(a3); 依然会调用移动构造运算符
	return 0;
}

上面代码输出为:

execute constructor function
execute move constructor function
execute constructor function
execute constructor function
execute operator=() function
execute destructor function
execute destructor function
execute destructor function
execute destructor function

上面代码中的移动构造函数以及移动赋值函数实现了将堆上的内存 m_val 转移到目标对象的功能。

4.4 forward 和完美转发

forward 和完美转发(Perfect Forwarding)与上面提到的类型折叠相关。首先,forward 是一个模板函数,用于在模板函数中保持参数的类型(左值或右值)不变,并将其转发给另一个函数。这是通过引用折叠规则实现的,该规则允许在模板参数推导过程中保持参数的原始类型。
其次,完美转发是指在函数模板中,将参数完全按照其原始类型(左值或右值)转发给另一个函数。这是通过使用 forward 结合模板的通用引用(或称为转发引用)实现的。通用引用是一种特殊的模板参数类型,它可以接受左值或右值作为参数,并将其类型保持为原始类型。如下为样例代码:

#include <iostream>

using namespace std;

class A
{
public:
	A() {}
	~A() {}

};

template<class T>
void revVal(T&& val)
{
	printf("call revVal(T&& val)\n");
}

template<class T>
void revVal(T& val)
{
	printf("call revVal(T& val)\n");
}

template<class T>
void sendValNormal(T&& val) {
	// 入参val 可以接受左值或右值  
	// 将 val 转发给另一个函数  
	revVal(val);
}

template<class T>
void sendValPerfect(T&& val) {
	// 入参val 可以接受左值或右值  
	// 使用 forward 将 val 完美转发给另一个函数  
	revVal(forward<T>(val));
}

int main()
{
	A a;
	printf("normal sending\n");
	sendValNormal(move(a));
	printf("perfect sending\n");
	sendValPerfect(move(a));
	return 0;
}

上面代码输出为:

normal sending
call revVal(T& val)
perfect sending
call revVal(T&& val)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值