【C++11 面试】左值/右值引用全解:10+个代码示例带你玩转C++11移动语义


前言

本篇文档将介绍 C++ 11中比较重要的一部分 - 右值引用和移动语义, 这同时也是我们需要掌握的 !


一、左右值介绍

在 C++ 11 更新以后引出了右值的概念 , 那这里就得理解一下什么是左值了 .

左值

左值其实在 C 语言中就提出了 , 只不过不常提起罢了 .

  • 介绍

左值 (lvalue)是C/C++等编程语言中的一个重要概念,通常指可以取地址具有持久存储位置的对象或表达式

右值

  • 介绍

‌右值 (Right Value,简称rvalue)‌是指那些不具有持久存储位置的临时对象不能取地址 , 通常用于表达式的结果‌ .


重要

总结


二、左值引用和右值引用(核心内容)

前提 , 看到题目 , 左值引用和右值引用 ? 所以 , 这里的左右值引用的本质还是引用 , 和之前笔者讲的引用是一个东西 , 这里不必慌张 !

● 左值引用

  • 符号 : &
  • 引用 : 取别名 , 写法 , int & a = b ; , a 是 b 的别名 .
  • C++ 98 一直在用的引用就是左值引用 . 以下给出一个代码回顾一下左值引用 .

● 为什么要有左值引用 ?

C++的引用的发明只能说特别牛逼 ! 笔者从一下方面简单带大家理解一下 , 左值引用的价值所在 .

  • 引用 : 起别名 , 虽然底层是指针 ,但理解层面上还是理解为起别名 .

  • 讨论

笔者从以下讲起 : 这里给出一个代码

代码1 :

cpp 复制代码
// 为什么要有左值引用 ? 讲解代码
class Date
{
public:


	Date(int year = 2025, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date 全缺省 --- 构造函数" << endl;
	}


	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date  --- 拷贝构造函数" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};


//这里不传引用 , 正常走
void Print(const Date d)
{
	// .....
	cout << "Print ....." << endl;
}



int main()
{

	//为什么要有左值引用  ? 
	//这里是拷贝构造 + 构造 , 因为编译器优化原因会直接优化为直接构造
	Date d0 = { 2025,3,18 };
	//仔细观察结果你会发现什么 ? 
	Print(d0);

	return 0;
}

运行结果 :

Date 全缺省 --- 构造函数
Date  --- 拷贝构造函数
Print .....

代码2 :

cpp 复制代码
//  Print 加上引用后 , 结果会大变 ? 
void Print(const Date& d)
{
	// .....
	cout << "Print ....." << endl;
}


运行结果 :

Date 全缺省 --- 构造函数
Print .....

看出了什么 ? 嗯 ? 发现加了引用后竟然少了一次拷贝构造 . 我举的例子呢 , 还不具备足够的说明性 , 这里可以试想一下 , 如果我传的参数是个自定义类型 , 并且这个类型内容指向的资源很大呢 ? 如果成员资源指向了一个1000个大小的数组呢 ? 况且还是自定义类型的深拷贝 , 那就要一个字节的拷贝 , 仔细想想 , 传个参数还进行拷贝 , 这样的效率是不是也太低下了 .那么 , 如果这种情况下 ,用了引用那是不是效率高了很多呢 , 所以这就是引用最吸引人的地方所在了 .

  • 引用目的

C++ 中规定 , 传值传参要走拷贝构造 , 那么走了引用就是为了减少拷贝 , 提高效率 !

  • 左值引用场景

C++ 中传值传参 , 传值返回要调用拷贝构造的 , 所以一般用于这两个场景 .并且传值返回引用还可以达到修改的功能 , 岂不是很好 .

复制代码
传值返回调用拷贝构造

所以 , 这里更能体现出了引用的价值 , 当传引用返回时不会调用拷贝构造 !

讲到这里 , 可以看到引用的发明可谓是真的对实践有帮助 , 可大大提高效率 . 那么既然左值引用这么好 , 这么强 , 为什么还要发明右值引用呢 ?

  • 左值引用的缺陷

虽然左值引用可以减少拷贝 , 提高效率 , 但仍存在一定的缺陷 .

复制代码
如以下场景 
cpp 复制代码
class Solution {
public:
	// 这里的传值返回拷⻉代价就太⼤了
	vector<vector<int>> generate(int numRows) {
		//这里的 vv 是个局部对象
		vector<vector<int>> vv(numRows);
		for (int i = 0; i < numRows; ++i)
		{
			vv[i].resize(i + 1, 1);
		}
		for (int i = 2; i < numRows; ++i)
		{
			for (int j = 1; j < i; ++j)
			{
				vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
			}
		}
		return vv;
	}
}

对以上这个场景 , 进行详细分析 .

a. 首先这里 , 是一个 vector - vector , 是一个二维数组 , 这里直接进行传值返回 , 显然效率是低下的 .

b. 那传左值引用返回 ? 仔细观察可以发现 , 返回的对象是个局部对象呀 , 要是返回了会发生什么 ? 局部对象出了这个函数的作用域就销毁了 , 那还返回其引用 , 就成了野引用了 (野指针) .

故 : 针对局部对象的场景 , 左值引用就会变成野引用(空引用) , 这就是左值引用的缺陷 .

那右值引用可以了吗 ? ............. 其实左值引用也可以弥补缺陷 .

  • 左值引用的缺陷的弥补

针对上面的问题 , 在 C++ 11 没有提出之前 , 难道就没有解决办法了吗 ? 兄弟 , 有的 , 有的 !

针对上面问题 , 左值引用不能返回的 , C++ 98 解决办法 : 用输出型参数

cpp 复制代码
class Solution {
public:
	// 这里的传值返回拷⻉代价就太大了 , 用传统方法解决 , 直接传参解决
	void generate(int numRows , vector<vector<int>>& vv) {
		for (int i = 0; i < numRows; ++i)
		{
			vv[i].resize(i + 1, 1);
		}
		for (int i = 2; i < numRows; ++i)
		{
			for (int j = 1; j < i; ++j)
			{
				vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
			}
		}
	}
};

这样也是可以解决问题的 , 但这样写我们在主函数中传参还有在传一个 vector , 一定程度上牺牲了可读性 . , 于是 C++ 11 出来了 .......


● 右值引用

C++ 11 提出了右值引用 , 解决了很大问题 .

  • 符号 : &&
  • 还是引用 , 起别名 .
  • 写法 : int&& a = b ; a 是 b 的别名 .

● 左右值引用交叉问题


● 右值引用的本质属性(重要)

  • ⼀个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值 .

简单来讲 : 右值引用的本质属性是左值 . (常考哦 ~)

第一次了解的伙伴一定会楞一下 , 为什么 ? 为什么这样设计 ? 有用吗 ? 其实是为了移动语义做了铺垫 ...


三、引用场景和移动语义(重要 ! 重要 ! 重要 !)

● 左值引用场景

左值引用场景 , 笔者在前面的左值部分已经介绍 , 总之 : 就是为了减少拷贝 , 提高效率 .

那么在之前笔者的例子中 , vv 是局部对象 , 用左值引用会出现野引用 , 那有的人会想了 , 那用右值引用 ? 仔细想想 , 不论是左值还是右值的引用 ,它们的本质还是指针啊 , 所以单纯的右值引用肯定解决不了问题 .....

那 ? 怎么解决 ? 所以提出了移动语义的概念 . 这可谓是神一样的存在啊 !


● 初见移动语义

  • 介绍

‌移动语义‌ 是C++11引入的一项重要特性,它允许对象的资源(如堆上分配的内存)在不进行深度复制的情况下进行转移。通过移动语义,可以将对象的资源从一个对象转移到另一个对象,从而避免不必要的内存拷贝,提高程序性能和效率‌ .

  • 理解

简单来讲不就是 ' 掠夺资源 ' 嘛 , 不走深拷贝 , 还能拿到想要的资源 . 很香 ~

  • 符号

参数引用用 ------ 右值引用 ---- &&

  • 分类

1. 移动构造

2. 移动赋值

  • 移动构造

移动构造还是构造函数 , 类似拷贝构造函数, 只不过构造函数内部实现方式不同 .

erlang 复制代码
a . 函数名与类名相同 .
b . 无返回值 .
c . 函数第一个参数必须是该类类型的右值引用 , 可以有其余参数必须有缺省值 .
  • 移动赋值

移动赋值是一个运算符重载 , 与赋值重载类似 .

css 复制代码
a . operator= 
b . 函数第一个参数必须是该类类型的右值引用 , 可以有其余参数必须有缺省值 .
  • 代码展示
cpp 复制代码
#include <iostream>
using namespace std;

class my_class
{
public:

	
	my_class(const string str = "")
		:_str(str)
	{
		cout << " 构造函数 " << endl;
	}


	//拷贝构造 , 左值会走拷贝构造
	my_class(my_class& mcl)
	{
		
		cout << " & -- 拷贝构造函数 " << endl;
	}

	void swap(my_class& mcl)
	{
		std::swap(_str, mcl._str);
	}

	
	//移动构造 , 右值会走移动构造
	my_class(my_class&& mcl)
	{
		cout << " && - 移动构造函数 " << endl;
		swap(mcl);
	}

private:
	string _str = "0";
};

int main()
{
	cout << "左值 :" << endl;
	//构造左值 
	my_class str("1111"); // 这里调用构造函数
	//拷贝左值
	my_class str1(str); // str 拷贝给 str1

	cout << "-----------" << endl;

	cout << "右值 :" << endl;
	//构造右值 + 拷贝右值
	my_class str2(my_class("2222")); // 构造 + 移动构造

	return 0;
}

因为编译器会对此进行相应的优化 , 这里的运行是在 Linux 关闭优化环境下执行的 :

  • 重要区分

● 右值引用和传值返回场景理解

这里给出一个整体示例代码 , 一一讲解 :

cpp 复制代码
namespace GJG
{
	class string
	{
	public:
		typedef char* iterator;
		typedef const char* const_iterator;

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}

		const_iterator end() const
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(char* str)-构造" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 拷贝构造" << endl;
			reserve(s._capacity);
			for (auto ch : s)
			{
				push_back(ch);
			}
		}

		void swap(string& ss)
		{
			::swap(_str, ss._str);
			::swap(_size, ss._size);
			::swap(_capacity, ss._capacity);
		}

		// 移动构造
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			// 转移掠夺你的资源
			swap(s);
		}

		string& operator=(const string& s)
		{
			cout << "string& operator=(const string& s) -- 拷贝赋值" <<
				endl;
			if (this != &s)
			{
				_str[0] = '\0';
				_size = 0;
				reserve(s._capacity);
				for (auto ch : s)
				{
					push_back(ch);
				}
			}
			return *this;
		}

		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}

		~string()
		{
			//cout << "~string() -- 析构" << endl;
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				if (_str)
				{
					strcpy(tmp, _str);
					delete[] _str;
				}
				_str = tmp;
				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity *
					2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}

		size_t size() const
		{
			return _size;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
}


namespace GJG
{
	string addStrings(string num1, string num2)
	{
		string str;
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;
			str += ('0' + ret);
		}
		if (next == 1)
			str += '1';
		reverse(str.begin(), str.end());
		cout << "******************************" << endl;
		return str;
	}
}

int main()
{

	GJG::string ret = GJG::addStrings("11111", "22222");
	return 0;
}

以上代码进行拆解

  • 只有拷贝构造 , 没有移动构造时

观察允许结果的变化 :

因为编译器在其中做了优化 , 所以不能直接看到真正结果 , 这里笔者建议到 Linux 中关闭优化 , 可查看 ~

cpp 复制代码
GJG::string str = GJG::addStrings("11111", "22222");

Linux 关闭优化 :

正常 VS2022 Debug 下运行 :

VS2022 优化还是比较狠的 .


  • 右值对象构造,有拷贝构造,也有移动构造时

Linux 关闭优化 :       正常 VS2022 Debug 下运行 :   

嗯 ? 发现了什么 ? 有移动构造就不会走拷贝构造了 , 右值一定就走移动构造了 , 代价小 , 效率高 .


  • 移动赋值

还有赋值的情况 , 笔者就不展示了 , 这里只想通过以上的讲解了解 , 编译器原来是可以优化的 , 一些步骤编译器会优化为它认为可行的步骤 , 知道编译器优化即可 ~


四、面试重要题

● 什么是左值 ? 什么是右值 ? 如何区分 ?

  • 左值

左值是一个表示数据的表达式 , 具有持久状态 , 它可以取地址 . 比如 : 变量名 , 解引用的指针等 .

  • 右值

右值是一个数据表达式 , 它不可以取地址 . 比如 : 字面值常量 , 临时对象 , 匿名对象 , 这些具有常性的都是右值 .

  • 区分

二者通过是否能取地址进行区分 , 这是区分的关键 .


● 左值引用和右值引用有什么价值 ? 为什么还要有右值引用 ?

问题 1 :

答 :

1. 左值引用和右值引用的最终目的是为了 : 减少拷贝 , 提高效率 .

  1. 左值引用可以达到修改参数 / 返回值的效果 , 方便好用 .    3. 在插入时 , 右值引用可以提高效率 , 如 : STL 中C++11 list 的 insert 中引入了右值引用 .

问题 2 :

答 :

因为左值引用的场景也受限 , 对于传值返回的情况 , 如果返回的值是一个局部变量    但, 局部变量出了该函数所在的作用域就会销毁 , 如果这样的话就导致引用就是空引用了(左值引用的缺陷)    拿底层来讲就是野指针了 , 面临风险较大 . 所以为了解决这样的场景 C++ 11提出了右值引用    和移动语义可以很好的解决这一点 .


● 讲一下移动语义 ; 它的提出能解决什么? 怎么解决的 ?

问题 1 :

答 :

移动语义是 C++11 提出来的 , 具有很大意义 , 可以提供效率 . 移动语义分为 , 移动构造和移动赋值   如果参数是右值 ,并且存在移动构造和移动赋值 , 那么就会调用它们 , 但在 C++98 中会调用拷贝构   和拷贝赋值 , 移动语义和移动赋值简单来讲就是来 '掠夺资源 ' 不会存在资源复制对于自定义类型的   深拷贝调用二者会大大提高效率 .

问题 2 : (了解)

答 :

移动语义和右值引用可以很好的解决传值返回的问题 , 对于返回的是局部变量或对象时 , 通过移动语   义就不会走拷贝构造了 , 就不存在资源是深层复制了 , 即使 , 局部变量作用域销毁 , 也不会影响接受者   比如 : 有一个字符串相加的函数 , 在其内部创建一个局部对象 自定义类型的string str ; 然后进行系列操   作后 , 把其返回 , 用 ret 接受返回值 , 这里面存在移动构造 , 即 : 本来 str 是临时对象 , ret 与 str 进行   资源交换 , 那么 str 就会空 , 为空当进行析构时就不会释放 str 指向的资源 , 这时 ret 就得到了资源 , 可   以正产管理了 .

cpp 复制代码
// 这里是 string 自定义类 , 里面包含 : 移动语义

.....
.....
.....

// ....

namespace GJG
{
	string addStrings(string num1, string num2)
	{
		string str;
		// ..... 
		return str;
	}
}
int main()
{
	GJG::string ret = GJG::addStrings("11111", "22222");
	return 0;
}

总结

以上本章节所有内容 , 希望对学者有所帮助 ! 觉得有帮助的还不忘点赞哦 ~ 抱拳 ~ 抱拳 ~ .

相关推荐
杨DaB2 小时前
【SpringMVC】拦截器,实现小型登录验证
java·开发语言·后端·servlet·mvc
努力的小雨8 小时前
还在为调试提示词头疼?一个案例教你轻松上手!
后端
魔都吴所谓8 小时前
【go】语言的匿名变量如何定义与使用
开发语言·后端·golang
陈佬昔没带相机9 小时前
围观前后端对接的 TypeScript 最佳实践,我们缺什么?
前端·后端·api
Livingbody10 小时前
大模型微调数据集加载和分析
后端
Livingbody11 小时前
第一次免费使用A800显卡80GB显存微调Ernie大模型
后端
Goboy11 小时前
Java 使用 FileOutputStream 写 Excel 文件不落盘?
后端·面试·架构
Goboy12 小时前
讲了八百遍,你还是没有理解CAS
后端·面试·架构
麦兜*12 小时前
大模型时代,Transformer 架构中的核心注意力机制算法详解与优化实践
jvm·后端·深度学习·算法·spring·spring cloud·transformer
树獭叔叔12 小时前
Python 多进程与多线程:深入理解与实践指南
后端·python