一、指针和引用的区别

1. 概念

指针 引用
指针是一个变量,它保存了另一个变量的内存地址 引用是另一个变量的别名,与原变量共享内存地址.
指针需要独立存储内存地址,可能占用额外空间(如32位系统通常4字节,64位系统8字节) 引用不会额外分配内存空间,直接映射到原变量的内存地址
指针(除指针常量)可以被重新赋值,指向不同的变量 引用在初始化后不能更改,始终指向同一个变量
指针可以为 nullptr,表示不指向任何变量; 引用必须绑定到一个变量,不能为 nullptr
使用指针需要对其进行解引用以获取或修改其指向的变量的值 引用可以直接使用,无需解引用

2. 汇编看引用和指针**

C++引用和指针只是在编译阶段有区别,底层实现还是指针。所以C++引用你可以理解为有着各种限制的指针。

3. 引用和指针在C++中各有用途

引用提供了更简洁的语法和更安全的操作(不可为NULL、不可重新绑定),适用于函数参数和返回值传递、实现多态性等场景。
指针则提供了更灵活的操作(可以重新指向、可以为NULL),适用于动态内存分配、复杂数据结构(如链表、树等)的操作。

二、左值和右值

详解 C++ 左值、右值、左值引用以及右值引用
总结: 区分左值和右值,终究还是要看能否取地址。

左值 右值
左值是一个表示数据的表达式,比如:变量名、解引用的指针变量 右值也是一个表示数据的表达式,比如:字面常量、表达式返回值,传值返回函数的返回值(是传值返回,而非传引用返回)
可以获取它的地址和对它赋值 右值不能出现在赋值符号的左边且不能取地址
// 以下的a、p、*p、b都是左值
int a = 3;
int* p = &a;
*p;
const int b = 2;
double x = 1.3, y = 3.8;
// 以下几个都是常见的右值
10;                 // 字面常量
x + y;             // 表达式返回值
fmin(x, y);        // 传值返回函数的返回值

以下写法均不能通过编译:
    10 = 4;、x + y = 4;fmin(x, y) = 4;
    //S2015 编译报错:error C2106: “=”: 左操作数必须为左值。原因:右值不能出现在赋值符号的左边。
    &10;&(x + y);&fmin(x, y);
    //VS2015 编译报错:error C2102: “&” 要求左值。原因:右值不能取地址。

三、左值引用和右值引用

在这里插入图片描述

1. 概念

传统的 C++ 语法中就存在引用语法,而 C++11标准中新增了右值引用的语法特性,因此为了区分两者,将C++11标准出现之前的引用称为左值引用。
无论左值引用还是右值引用,都是给对象取别名。

// 以下几个是对上面左值的左值引用
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;

// 以下几个是对上面右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

注意:
右值引用引用右值,会使右值被存储到特定的位置。也就是说,右值引用变量其实是左值,可以对它取地址和赋值(const右值引用变量可以取地址但不可以赋值,因为 const 在起作用)。
当然,取地址是指取变量空间的地址(右值是不能取地址的)。比如:
1. double&& rr2 = x + y;
&rr2;
rr2 = 9.4;
右值引用 rr2 引用右值 x + y 后,该表达式的返回值被存储到特定的位置,不能取表达式返回值 x + y 的地址,但是可以取 rr2 的地址,也可以修改 rr2 。
2. const double&& rr4 = x + y;
&rr4;
可以对 rr4 取地址,但不能修改 rr4,即写成rr4 = 5.3;会编译报错。

2. const左值引用既可以引用左值,也可以引用右值。

在 C++11标准产生之前,是没有右值引用这个概念的,当时如果想要一个类型既能接收左值也能接收右值的话,需要用const左值引用,比如标准容器的 push_back 接口:void push_back (const T& val)。
也就是说,如果const左值引用不能引用右值的话,有些接口就不好支持了。

// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;
//int& rt2 = 8;  // 编译报错,因为8是右值,不能直接引用右值

// 2.但是const左值引用既可以引用左值
const int& rt3 = t;
const int& rt4 = 8;  // 也可以引用右值
const double& r1 = x + y;
const double& r2 = fmin(x, y);

3. 右值引用可以引用被move的左值。

move,本文指std::move(C++11),作用是将一个左值强制转化为右值,以实现移动语义。
左值被 move 后变为右值,于是右值引用可以引用。

// 1.右值引用只能引用右值
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = x + y;

int t = 10;
//int&& rrt = t;  // 编译报错,不能直接引用左值


// 2.但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
int*&& rr4 = std::move(p);
int&& rr5 = std::move(*p);
const int&& rr6 = std::move(b);

4. 引用的使用场景及实际意义

  1. 左值引用
    传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率。
  2. 右值引用

当对象出了函数作用域以后仍然存在时,可以使用左值引用返回,这是没问题的。
但当对象(对象是函数内的局部对象)出了函数作用域以后不存在时,就不可以使用左值引用返回了。

为了解决上述传值返回的拷贝问题,C++11标准就增加了右值引用和移动语义。

四、 移动语义

C++11中的移动语义

1.为什么需要移动语义

对象作为函数返回值或右值时,避免频繁触发拷贝。

在C++11标准之前,没有直接的方法移动对象,即使某些情况不需要拷贝对象,也不得不拷贝。例如,函数返回非引用的返回值时,会先构造一个临时对象作为返回值,函数调用时将该临时对象赋值给接收对象,调用结束后会销毁该临时对象。这里的临时对象是肯定会被销毁的,如果能够将临时对象的资源直接移交给接收对象,那么就可以减少一次拷贝。旧标准则只能拷贝,然后再销毁临时对象的资源。
在这里插入图片描述
C++11支持将即将被销毁的对象移动到接收对象,而不是复制。如下图,临时对象的资源被移动到接收对象,减少一次复制。
在这里插入图片描述

2. 移动语义的触发场景

(1) 移动赋值运算符:出现在右值,初始化对象。

MyVector v3{ 4,5,6 }; // 构造v3
MyVector v4 = std::move(v3); // 触发移动构造函数

(2)移动构造函数:出现在函数入参,避免拷贝

std::vector<MyVector> vt;
MyVector myvec{1,1,1};
vt.push_back(std::move(myvec));

(3)移动构造函数:出现在函数返回值,避免拷贝

MyVector CreateVector()
{
    MyVector vec{1,2,3};
    return vec;  // 可能触发移动构造(若 RVO 未生效)
}

在这里插入图片描述

3.移动构造函数(move constructor)

移动构造函数(move constructor),是C++11标准增加的在创建对象时移动旧对象资源的构造函数。
移动构造函数的几个特点:
1、函数名和类名相同,没有返回值,因为它也是构造函数的一种;
2、第一个参数必须是一个自身类类型的右值引用(&&),且其他参数都有默认值。
3、第一个参数不能声明为 const 右值引用的原因是该引用在函数内会被修改(移动资源)。
4、移动构造函数执行后,需要保证右值引用的对象能够被正常销毁。

类的移动构造函数原型通常是这样的:类名(类类型 &&);。以CDate类为例,其移动构造函数声明如下:

CDate(CDate &&date) noexcept;	// 声明
CDate::CDate(CDate&& date) noexcept // 实现
{
	m_year = date.m_year;
	m_mon = date.m_mon;
	m_day = date.m_day;
	str = date.str;
	date.str = NULL;
	cout << "Calling Move Constructor" << ", this=" << this <<endl;
}

移动构造函数解析:
1、处理普通数据成员,直接使用旧对象的进行赋值;
2、处理指向堆内存的指针,直接堆内存地址给新对象的指针,旧对象指针指向NULL。
语句str = date.str;将旧对象的str直接给到正在创建的对象,而不重新new、复制。
语句date.str = NULL;将旧对象的str赋值为NULL,保证其可以被正常销毁,delete NULL;、delete[] NULL不会造成任何问题。
3、由于移动操作“ 窃取” 资源, 它通常不分配任何资源。 因此, 移动操作通常不会抛出任何异常。不抛出异常的函数应该使用 noexcept 通知标准库,避免编译器为了处理异常而作一些额外的工作。

例:使用移动构造函数

  1. 怎样的类需要定义移动构造函数?
    如果该类的成员存在指针指向new分配的堆内存,则可以跟进需要定义移动构造函数。
// g++ 14_Move_Constructor_Date.cpp -std=c++11
#include <iostream>
#include <stdio.h>
#include <string.h>

using namespace std;

#define MAX_NEW_MEM		(64*1000*1000)

class CDate
{
public:
	CDate(int year, int mon, int day);	// 构造函数声明
	CDate(const CDate& date);			// 拷贝构造函数声明
	CDate(CDate&& date) noexcept;		// 移动构造函数声明
	~CDate();							// 析构函数声明
	
	CDate operator+(int day);			// 加号运算符声明

	void show()
	{
		cout << "Date: " << m_year << "." << m_mon << "." << m_day << ", this=" << this << endl;
		//cout << "Date: " << str << endl;
	}

private:
	int m_year;
	int m_mon;
	int m_day;
	char *str;
};

// 构造函数定义
CDate::CDate(int year, int mon, int day)
{
	m_year = year;
	m_mon = mon;
	m_day = day;
	str = new char[MAX_NEW_MEM];
	sprintf(str, "%4d.%02d.%02d", year,mon,day);
	cout << "Calling Constructor" << ", this=" << this <<endl;
}

// 拷贝构造函数定义
CDate::CDate(const CDate& date)
{
	m_year = date.m_year;
	m_mon = date.m_mon;
	m_day = date.m_day;
	str = new char[MAX_NEW_MEM];
	memcpy(str, date.str, MAX_NEW_MEM);
	cout << "Calling Copy Constructor" << ", this=" << this << ", Copy Data" <<endl;
}

CDate::CDate(CDate&& date) noexcept
{
	m_year = date.m_year;
	m_mon = date.m_mon;
	m_day = date.m_day;
	str = date.str;
	date.str = NULL;
	cout << "Calling Move Constructor" << ", this=" << this <<endl;
}

// 析构函数定义
CDate::~CDate()
{
	cout << "Calling Destructor" << ", this=" << this <<endl;
	delete [] str;
}

CDate CDate::operator+(int day)
{
	CDate temp = *this;
	temp.m_day += day;
	cout << "Calling operator+" << ", this=" << &temp << endl;
	return temp;
}

int main()
{
	CDate date(2024,06,07);
	cout << endl;
	
	CDate date1 = std::move(date+1);// std::move 强制将 date+1 的求值结果转为右值
	date1.show();
	
	cout << endl;
	
	return 0;
}

4.移动赋值运算符

参考
移动赋值运算符(move assignment operator),是C++11标准增加的在给对象赋值时移动旧对象资源的成员运算符函数。

移动赋值运算符的几个特点:
1、函数名为operator=,返回值为该类类型的引用,因为需要返回当前对象,以支持连续赋值a=b=c;
2、第一个参数必须是一个自身类类型的右值引用(&&),且其他参数都有默认值。
3、第一个参数不能声明为 const 右值引用的原因是该引用在函数内会被修改(移动资源)。
4、移动赋值运算符执行后,需要保证右值引用的对象能够被正常销毁。

CDate(CDate &&date) noexcept;	// 声明
CDate& CDate::operator=(CDate &&date) noexcept
{
	if(this == &date)	// 赋值给自身
		return *this;
	delete [] str;		// 释放旧的数据
	m_year = date.m_year;
	m_mon = date.m_mon;
	m_day = date.m_day;
	str = date.str;
	date.str = NULL;
	sprintf(str, "%4d.%02d.%02d", m_year,m_mon,m_day);
	cout << "Move operator=" << ", this=" << this <<endl;
	return *this;
}	

移动赋值运算符解析:
1、处理普通数据成员,直接使用旧对象的进行赋值;
2、处理指向堆内存的指针,直接堆内存地址给新对象的指针,旧对象指针指向NULL。
语句str = date.str;将旧对象的str直接给到正在创建的对象,而不重新new、复制。
语句date.str = NULL;将旧对象的str赋值为NULL,保证其可以被正常销毁,delete NULL;、delete[] NULL不会造成任何问题。
3、由于移动操作“ 窃取” 资源, 它通常不分配任何资源。 因此, 移动操作通常不会抛出任何异常。不抛出异常的函数应该使用 noexcept 通知标准库,避免编译器为了处理异常而作一些额外的工作。
例:

// g++ 15_Move_operator=.cpp -std=c++11
#include <iostream>
#include <stdio.h>
#include <string.h>

using namespace std;

#define MAX_NEW_MEM		(64*1000*1000)

class CDate
{
public:
	CDate(int year, int mon, int day);	// 构造函数声明
	CDate(const CDate& date);			// 拷贝构造函数声明
	~CDate();							// 析构函数声明
	
	CDate operator+(int day);			// 加号运算符声明
	CDate& operator=(const CDate &date);// 拷贝赋值运算符声明
	CDate& operator=(CDate &&date) noexcept;// 移动赋值运算符声明

	void show()
	{
		cout << "Date: " << m_year << "." << m_mon << "." << m_day << ", this=" << this << endl;
		//cout << "Date: " << str << endl;
	}

private:
	int m_year;
	int m_mon;
	int m_day;
	char *str;
};

// 构造函数定义
CDate::CDate(int year, int mon, int day)
{
	m_year = year;
	m_mon = mon;
	m_day = day;
	str = new char[MAX_NEW_MEM];
	sprintf(str, "%4d.%02d.%02d", year,mon,day);
	cout << "Calling Constructor" << ", this=" << this <<endl;
}

// 拷贝构造函数定义
CDate::CDate(const CDate& date)
{
	m_year = date.m_year;
	m_mon = date.m_mon;
	m_day = date.m_day;
	str = new char[MAX_NEW_MEM];
	memcpy(str, date.str, MAX_NEW_MEM);
	cout << "Calling Copy Constructor" << ", this=" << this << ", Copy Data" <<endl;
}

// 析构函数定义
CDate::~CDate()
{
	cout << "Calling Destructor" << ", this=" << this <<endl;
	delete [] str;
}

// 加号运算符函数定义
CDate CDate::operator+(int day)
{
	CDate temp = *this;
	temp.m_day += day;
	cout << "Calling operator+" << ", this=" << &temp << endl;
	return temp;
}

// 赋值运算符函数定义
CDate& CDate::operator=(const CDate& date)
{
	if(this == &date)	// 赋值给自身
		return *this;
	delete [] str;		// 释放旧的数据
	m_year = date.m_year;
	m_mon = date.m_mon;
	m_day = date.m_day;
	str = new char[64];
	sprintf(str, "%4d.%02d.%02d", m_year,m_mon,m_day);
	cout << "Calling operator=" << ", this=" << this << ", Copy Data" <<endl;
	return *this;
}

// 移动赋值运算符定义
CDate& CDate::operator=(CDate &&date) noexcept
{
	if(this == &date)	// 赋值给自身
		return *this;
	delete [] str;		// 释放旧的数据
	m_year = date.m_year;
	m_mon = date.m_mon;
	m_day = date.m_day;
	str = date.str;
	date.str = NULL;
	sprintf(str, "%4d.%02d.%02d", m_year,m_mon,m_day);
	cout << "Move operator=" << ", this=" << this <<endl;
	return *this;
}	

int main()
{
	CDate date(2024,06,14);
	CDate date1(2024,06,14);
	cout << endl;
	
	date1 = std::move(date+1);// std::move 强制将 date+1 的求值结果转为右值
	date1.show();
	
	cout << endl;
	
	return 0;
}

五、完美转发深度解析

1.1 完美转发的概念

完美转发是C++中一种高级的技术,用于在函数模板中转发参数至另一个函数,同时保持所有参数的值类别(左值、右值)和其他属性(如const修饰符)不变。这一技术主要通过模板和std::forward实现,并在泛型编程中尤为重要,因为它允许函数模板在不丢失任何参数信息的前提下传递参数。

std::forward

std::forward是C++11引入的一个模板函数,主要用于实现参数的完美转发。它的核心作用是在模板函数中保持参数的原始值类别(左值或右值)。std::forward通常与通用引用(Universal References,形式为T&&)一起使用,这种引用可以绑定到左值或右值上。通过使用std::forward,可以确保在函数模板中转发参数时,保持其左值或右值属性不变。

完美转发的实际应用案例

1 用完美转发实现委托构造函数

委托构造函数允许一个构造函数调用同一个类的其他构造函数,从而避免代码重复。通过使用完美转发,我们可以更高效地在构造函数间传递参数。例如:

class MyString {
public:
    template <typename... Args>
    MyString(Args&&... args) : _data(std::forward<Args>(args)...) {
    }

private:
    std::string _data;
};

int main() {
    MyString s1("Hello, world!"); // 调用 std::string 的构造函数
    MyString s2(s1); // 调用 std::string 的拷贝构造函数
    MyString s3(std::move(s2)); // 调用 std::string 的移动构造函数
}


.2 用完美转发实现可变参数模板函数

可变参数模板函数可以接受任意数量和类型的参数,通过使用完美转发,我们可以实现一个通用的元组或 bind 函数。例如:

template <typename Func, typename... Args>
auto bind_and_call(Func&& func, Args&&... args) -> decltype(func(std::forward<Args>(args)...)) {
    return func(std::forward<Args>(args)...);
}

int sum(int a, int b, int c) {
    return a + b + c;
}

int main() {
    int result = bind_and_call(sum, 1, 2, 3); // 完美转发参数给 sum 函数
}


3 用完美转发实现智能指针

智能指针是一种自动管理内存生命周期的对象,它可以确保在离开作用域时自动释放内存。通过使用完美转发,我们可以在智能指针的构造函数和 make 函数中避免不必要的拷贝操作。例如:

template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

class MyClass {
public:
    MyClass(int x, double y) : _x(x), _y(y) {
    }

private:
    int _x;
    double _y;
};

int main() {
    auto ptr = make_unique<MyClass>(42, 3.14); // 完美转发参数给 MyClass 的构造函数
}


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐