C++11 核心特性全解:列表初始化、右值引用与移动语义实战

目录

  • 前言
  • 一、C++11的发展历史
  • 二、列表初始化
    • [2.1 C++98传统的{}](#2.1 C++98传统的{})
    • [2.2 C++11中的{}](#2.2 C++11中的{})
      • [2.1.1 什么是这里的隐式类型转换?](#2.1.1 什么是这里的隐式类型转换?)
    • [2.3 C++11中的std::initializer_list](#2.3 C++11中的std::initializer_list)
  • 三、右值引用和移动语义
    • [3.1 左值和右值](#3.1 左值和右值)
    • [3.2 左值引用和右值引用](#3.2 左值引用和右值引用)
    • [3.3 引用延长生命周期](#3.3 引用延长生命周期)
    • [3.4 左值和右值的参数匹配](#3.4 左值和右值的参数匹配)
    • [3.5 右值引用和移动语义的使用场景](#3.5 右值引用和移动语义的使用场景)
      • [3.5.1 左值引用主要使用场景回顾](#3.5.1 左值引用主要使用场景回顾)
      • [3.5.2 移动构造和移动赋值](#3.5.2 移动构造和移动赋值)
      • [3.5.3 右值引用和移动语义解决传值返回问题](#3.5.3 右值引用和移动语义解决传值返回问题)
  • 四、实现移动语义list的完整源码
  • 结语

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列》《笔试算法

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、C++11的发展历史

C++11 是 C++ 的第二个主要版本,并且是从 C++98 起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对 C++ 程序员可用的抽象。在它最终由 ISO 在 2011 年 8 月 12 日采纳前,人们曾使用名称 "C++0x",因为它曾被期待在 2010 年之前发布。C++03 与 C++11 期间花了 8 年时间,故而这是迄今为止最长的版本间隔。从那时起,C++ 有规律地每 3 年更新一次。

二、列表初始化

2.1 C++98传统的{}

C++98中的列表初始化主要是从C语言延续过来的对结构体和数组初始化

cpp 复制代码
struct Point
{
	int _x;
	int _y;
};

int main()
{
	//C++98
	int array1[] = { 1, 2, 3, 4, 5 };
	int array2[5] = { 0 };
	Point p = { 1, 2 };
	return 0;
}

2.2 C++11中的{}

  • C++11 以后想统一初始化方式,试图实现一切对象皆可用 {} 初始化,{} 初始化也叫做列表初始化,注意和构造函数那里的初始化进行区分,构造函数那里的{}叫做初始化列表,和这里是没有关联关系的。
  • 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造。
  • {} 初始化的过程中,可以省略掉 =
  • C++11 列表初始化的本意是想实现一个大统一的初始化方式,其次他在有些场景下带来的不少便利,如容器 push/inset 多参数构造的对象时,{} 初始化会很方便
cpp 复制代码
class Date
{
public:
	//该类的「默认构造函数」
	//C++ 里,默认构造函数的定义是「可以被无参调用的构造函数」
	Date(int year = 1, int month = 1, int day = 1)
		//成员初始化列表
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date(const Date& d)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

//引用构造的临时对象,没有优化了
void Insert(const Date& d)
{

}

Date func()
{
	////以前返回对象要定义一个有名对象进行返回
	//Date d(2025, 11, 15);
	//return d;

	////现在可以通过列表初始化构造了一个临时(匿名)的Date对象,作为函数的返回值
	////现代编译器(尤其是 C++17 标准强制优化)会直接把这个临时对象构造在调用方接收变量的内存地址中
	////全程只调用 1 次构造函数,没有任何拷贝 / 移动构造
	//return { 2025, 11, 15 };

	////返回默认构造的对象的两种写法
	//Date d;
	//return d;

	return {};
}


int main()
{
	//以前的初始化方法,直接构造,无隐式转换、无临时对象、无拷贝
	Date d1(2025, 11, 15);

	//下面二者和上面的初始化方法底层的语法意义是不一样的
	//C++11,自定义类型支持
	//拷贝初始化语法 = 要求:右侧必须是一个 Date 类型的值
	//这里本质是用{2025, 1, 1}构造一个Date临时对象(这一步调用构造函数)
	//临时对象再去拷贝构造d1(这一步调用拷贝构造函数),编译器优化后合二为一变成{2025, 1, 1}直接构造初始化
	//现代编译器(C++17 前是优化,C++17 标准强制)会直接消除中间的临时对象和拷贝构造:
	//可以调试验证上面的理论,发现是没调用拷贝构造的
	//单参数的隐式类型转换
	Date d2 = 2025;
	//多参数的隐式类型转换
	//同理先做隐式类型转换生成临时对象 → 再用临时对象拷贝构造 → 编译器优化
	Date d5 = { 2025, 11, 15 };
	
	//可以省略 = 
	Date d6{ 2025,11,15 };
	//直接调用默认构造,两者效果完全一样,都会调用同一个构造函数
	Date d7{};
	Date d8;
	
	//有了列表初始化之后就不用再构造有名或匿名对象了,可以直接插入
	Insert(2025);
	Insert({ 2025, 11, 15 });

	int i = 0;
	//C++11支持的
	//内置类型支持
	int j = { 1 };
	int k{ 2 };
	//int默认构造是0
	int m{};

	//不支持,只有{}初始化,才能省略 = 
	//Date d9 2025;
	return 0;
}

2.1.1 什么是这里的隐式类型转换?

1. 定义
拷贝初始化 中,编译器自动将非目标类型的值 / 列表 ,转换为类类型(Date) 的过程,就是隐式类型转换

2. 底层本质

单参数:内置类型(int) → 自定义类型(Date)

多参数 (C++11):初始化列表{...} → 自定义类型(Date)

这个转换不是强制的 ,是编译器根据构造函数自动完成的隐式行为

3. 禁止隐式转换的情况

给构造函数加 explicit 关键字(显式构造,禁止隐式转换):

cpp 复制代码
explicit Date(int year = 1, int month = 1, int day = 1)
cpp 复制代码
Date d5 = {2025,11,15};  // 编译报错!
Date d2 = 2025;          // 编译报错!

报错原因:拷贝初始化的隐式类型转换被 explicit 禁止了

而直接初始化依然正常:

cpp 复制代码
Date d5(2025,11,15);  // 正常运行

这直接证明:= {} / = 值 写法必然触发隐式类型转换

4、这种隐式类型转换的严格条件
条件 1:语法必须是「拷贝初始化」

只有这两种写法触发:

cpp 复制代码
T obj = 值;
T obj = {初始化列表};  // C++11

直接初始化 T obj(...) 不会触发任何隐式转换

条件 2:构造函数不能被 explicit 修饰

explicit 是 C++ 专门用来禁止隐式类型转换的关键字;只要构造函数加了 explicit,拷贝初始化的隐式转换直接失效。

条件 3:右侧值 / 列表能匹配构造函数参数

  1. 单参数转换:
    构造函数只有 1 个参数(或其余参数有默认值),且源类型可转换为参数类型(如 int → int)。
  2. 多参数列表转换(C++11):
    初始化列表的数量、类型严格匹配多参数构造函数(支持默认参数)。

条件 4:类不是聚合类

聚合类(无自定义构造、无私有成员、无基类)的 ={} 是聚合初始化,不调用构造函数,也不触发隐式转换。这里的 Date 类有自定义构造 + 私有成员,不是聚合类,因此触发隐式转换。

补充一个可能容易忘记的点:

C++ 里,默认构造函数的定义是「可以被无参调用的构造函数」,有两种情况:

  1. 显式的无参构造函数:Date() { ... }
  2. 所有参数都带默认值的构造函数

只有当你完全不提供任何用户定义的构造函数(包括带参、拷贝构造等)时,编译器才会自动生成一个默认的无参构造函数,它的逻辑是空的,只会按成员声明顺序初始化内置类型(但不会赋值)。

而现在提供了带参构造函数,所以编译器不会生成默认构造,带默认参数的构造函数就是唯一的默认构造。

2.3 C++11中的std::initializer_list


一、分清两个「大括号」场景

二、std::initializer_list 到底是什么?

「本质是底层开一个数组,两个指针指向首尾」
标准定义:

cpp 复制代码
template <class T>
class initializer_list {
public:
    // 迭代器接口(返回指向数组的指针)
    const T* begin() const noexcept { return _begin; }
    const T* end() const noexcept { return _end; }
    // 元素个数(等于_end - _begin)
    size_t size() const noexcept { return static_cast<size_t>(_end - _begin); }

private:
    const T* _begin; // 指向底层数组的首元素
    const T* _end;   // 指向底层数组的尾后元素(和STL迭代器一致)
};

1. 它是一个「轻量级视图」,不拥有数据

它本身不存储元素,只是用两个指针指向编译器自动生成的临时数组

  • 当写 auto il = {10,20,30}; 时,编译器会在栈上生成一个 int[3] 数组 {10,20,30};
  • 然后让 il._begin 指向数组首地址,il._end 指向数组尾后地址(il._begin + 3)。

2. 大小固定为「两个指针的大小」

代码里 cout << sizeof(mylist) << endl;

32 位系统:指针占 4 字节,所以 sizeof(mylist) = 4 + 4 = 8 字节;

64 位系统:指针占 8 字节,所以 sizeof(mylist) = 8 + 8 = 16 字节。

3. 为什么 begin()/end() 和栈变量 i 的地址接近?

因为编译器生成的临时数组是存在栈上的,和局部变量 int i 在同一个栈帧里,所以地址会非常接近,这就是代码里 cout << mylist.begin() << endl;cout << &i << endl; 输出地址相近的原因。

三、vector/map 为什么能用 {} 初始化?

  • 如果没有 initializer_list,要支持 vector< int > v(1)、vector< int > v(1,2)、vector< int > v(1,2,3)... 就得写无数个重载;
  • 有了 initializer_list,只要一个构造函数,就能支持任意数量的元素初始化,完美解决这个问题。

STL 容器(vector/map/list等)从 C++11 开始,都添加了接受 std::initializer_list< T > 的构造函数和赋值运算符,这就是它们支持 {x1,x2,x3...} 初始化的根本原因。

代码里的例子:
1. vector< int > v1 = {1,2,3,4}; / vector< int > v2{1,2,3,4,5,6,6,7,7};

编译器看到 vector< int >{1,2,3,4},优先匹配容器的 initializer_list 构造函数;

生成一个 std::initializer_list< int > 对象,指向栈上的 int[4] 数组 {1,2,3,4};

调用 vector(std::initializer_list< int >) 构造函数,遍历 initializer_list 里的元素,把它们拷贝到 vector 的动态数组里。

2. map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };

这是 initializer_list 的嵌套用法,我们拆成两层看:

  • 外层的 {} :是 std::initializer_list<std::pair<const string, string>>,因为 map 的构造函数是 map(std::initializer_list<std::pair<const K,V>>);
  • 内层的 {"sort", "排序"} :是普通列表初始化,用来隐式构造一个 std::pair<string, string> 对象(因为 pair 支持 pair(const T1&, const T2&) 构造,所以 {a,b} 会匹配这个构造)。

编译器先把每个内层的 {k,v} 转换成 pair 对象,再把这些 pair 对象组成一个 initializer_list< pair >,最后调用 map 的 initializer_list 构造函数,把元素插入到 map 里。

3. v1 = {10, 20, 30};(容器赋值)

和构造的逻辑完全一样:

vector 重载了 operator=(std::initializer_list< int >);

编译器生成 initializer_list< int > 对象,指向 {10,20,30} 数组;

调用赋值运算符,把 vector 的元素替换成列表里的元素。

四、auto il = {1, 20, 30}; 到底是什么?

这里的 {} 不是初始化 il,而是隐式构造 initializer_list 对象,再赋值给 il(这里的 il 的类型就是 std::initializer_list< int >),和之前的 Date d = {2025,11,15} 逻辑类似,只是这次生成的是 initializer_list 而不是 Date。

五、std::initializer_list 的关键特性与要点

  1. 不拥有数据,生命周期绑定临时数组,编译器生成的临时数组是栈上的局部变量,生命周期只在当前表达式结束前,所以可以用 initializer_list 作为函数参数(比如容器构造);但绝对不要返回 initializer_list 指向局部数组的对象,比如:
cpp 复制代码
// 错误!返回的initializer_list指向的数组在函数结束后就销毁了
std::initializer_list<int> bad_func() {
    return {1,2,3}; 
}
  1. 只能用 const 迭代器访问元素

    begin() 和 end() 返回的是 const T*,所以不能修改 initializer_list 里的元素,它是一个只读视图。

  2. 优先匹配 initializer_list 构造

    如果一个类同时有 initializer_list 构造和普通多参数构造,用 {} 初始化时会优先匹配 initializer_list:

cpp 复制代码
class A {
public:
    A(std::initializer_list<int>) { cout << "initializer_list构造" << endl; }
    A(int, int, int) { cout << "普通多参数构造" << endl; }
};

int main() {
    A a{1,2,3}; // 输出:initializer_list构造,优先匹配
    A b(1,2,3); // 输出:普通多参数构造,显式匹配
}

三、右值引用和移动语义

C++98 的语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,C++11 之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

3.1 左值和右值

  • 左值是一个表示数据的表达式 (如变量名或解引用的指针,甚至可以是一个函数调用,所指就是函数调用的返回值),一般是有持久状态,存储在内存中,我们可以获取它的地址 ,左值可以出现在赋值符号的左边,也可以出现在赋值符号右边。定义时 const 修饰符后的左值,不能给他赋值(不严谨),但是可以取它的地址。

  • 右值也是一个表示数据的表达式,要么是字面值常量(如10,0.0,空指针)、要么是表达式求值过程中创建的临时对象(或匿名对象,二者是一个东西)等,右值可以出现在赋值符号的右边,但是通常是不能(有极特殊情况)出现在赋值符号的左边,右值不能取地址

  • 值得一提的是,左值的英文简写为 lvalue,右值的英文简写为 rvalue。传统认为它们分别是 left value、right value 的缩写。现代 C++ 中,lvalue 被解释为 loactor value 的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面值常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别就是能否取地址。

3.2 左值引用和右值引用

  • Type& r1 = x; Type&& rr1 = y; 第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。
  • 左值引用不能直接引用右值,但是const左值引用可以引用右值
  • 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
  • template < class T > typename remove_reference< T >::type&& move (T&& arg);
  • move是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及一些引用折叠的知识,这个我们后面会细讲。
  • 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值
cpp 复制代码
int main()
{
	// 左值:可以取地址
	// 以下的p、b、c、*p、s、s[0]就是常见的左值
	int* p = new int(0);
	int b = 1;
	const int c = b;
	*p = 10;
	string s("111111");
	s[0] = 'x';
	double x = 1.1, y = 2.2;

	// 左值引用给左值取别名
	int& r1 = b;
	int*& r2 = p;
	int& r3 = *p;
	string& r4 = s;
	char& r5 = s[0];

	// 右值引用给右值取别名
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	string&& rr4 = string("11111");

	// 左值引用不能直接引用右值,但是const左值引用可以引用右值
	const int& rx1 = 10;
	const double& rx2 = x + y;
	const double& rx3 = fmin(x, y);
	const string& rx4 = string("11111");

	//右值引用不能直接引用左值,但是右值引用可以引用move(左值)
	//move可以将属性从左值转换为右值
	//move的本质就相当于强转
	int&& rrx1 = move(b);
	int*&& rrx2 = move(p);
	int&& rrx3 = move(*p);
	string&& rrx4 = move(s);
	//s是个左值,这里强转为右值
	string&& rrx5 = (string&&)s;

	//这里无法引用rrx1,右值引用本身的属性是左值
	//也就是说move(b)是右值,但是rrx1本身的属性是左值
	//int&& rrrx1 = rrx1;
	int&& rrrx1 = move(rrx1);

	return 0;
}

3.3 引用延长生命周期

C++ 里,const 左值引用 和 右值引用,都能把临时对象的生命周期,从 "当前语句" 延长到 "引用的作用域结束"。

区别只在于:能不能修改这个对象。

像 s1 + s1 这种表达式,会返回一个临时的 std::string 对象,其是一个纯右值(rvalue),用完即毁,默认生命周期只到当前语句结束,默认情况下,不能直接修改它(比如 (s1 + s1) += "Test" 会报错)。

cpp 复制代码
int main()
{
    std::string s1 = "Test";

    // 1. 错误:非const的左值引用,不能绑定到临时对象(右值)
    // std::string& r1 = s1 + s1; 
    // 原因:临时对象是右值,不能绑定到非const左值引用(C++的安全规则,防止修改临时对象)

    // 2. 正确:const左值引用绑定临时对象,延长生命周期
    const std::string& r2 = s1 + s1; 
    // 底层:s1 + s1 生成临时对象,r2绑定它,临时对象生命周期延长到r2作用域结束
    // 限制:r2是const引用,所以不能修改它指向的对象
    // r2 += "Test"; // 错误:不能通过const引用修改

    // 3. 正确:右值引用绑定临时对象,延长生命周期
    std::string&& r3 = s1 + s1; 
    // 底层:r3是右值引用,绑定临时对象,同样延长生命周期到r3作用域结束
    // 关键区别:r3是非const的右值引用,**它本身是一个左值**!
    // 所以可以通过它修改绑定的临时对象
    r3 += "Test"; // 正确:可以通过非const的右值引用修改

    std::cout << r3 << '\n';
    return 0;
}

r3 是一个右值引用 ,右值对象是没办法直接修改的,但它是一个有名字的变量 ;C++ 里,有名字的变量都是左值,不管它是什么类型的引用;所以 r3 本身是左值,可以被赋值、被修改,也可以被取地址;而 r3 绑定的是临时对象,所以通过 r3 就能修改这个原本无法直接修改的临时对象。

生命周期的概念也可以通过下面的程序来验证

通过调试也可以看出其是即用即销毁的

但是const左值引用就可以延长其生命周期

3.4 左值和右值的参数匹配

  • C++98 中,我们实现一个 const 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。

  • C++11 以后,分别重载左值引用、const 左值引用、右值引用作为形参的 f 函数,那么实参是左值会匹配 f (左值引用),实参是 const 左值会匹配 f (const 左值引用),实参是右值会匹配 f (右值引用)。

  • 右值引用变量在用于表达式时属性是左值,这个设计这里会感觉很怪,下面我们讲右值引用的使用场景时,就能体会这样设计的价值了

3.5 右值引用和移动语义的使用场景

3.5.1 左值引用主要使用场景回顾

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如 addStrings 和 generate 函数,C++98 中的解决方案只能是被迫使用输出型参数解决。

如图若是编译器不做任何返回值优化,也没有移动语义(C++98 场景),代码会发生 2 次拷贝构造 ,过程如下:
步骤 1:函数内部构造局部对象

addStrings 函数内,string str; 会调用 std::string 的默认构造函数,在函数栈帧上创建一个局部对象 str。

步骤 2:return str → 第一次拷贝构造(生成临时对象)

return str; 时,编译器会用 str 调用 std::string 的拷贝构造函数 ,构造一个「返回值临时对象」(也就是图里标的那个临时对象)。

拷贝完成后,函数结束,局部对象 str 会被析构。

步骤 3:main 中接收返回值 → 第二次拷贝构造

string ret = addStrings(...); 时,会用刚才的「返回值临时对象」,再调用一次 std::string 的拷贝构造函数,构造 ret。

拷贝完成后,临时对象被析构。

现代主流编译器(GCC 4.x+、Clang、MSVC 2015+)在开启优化(如 -O2)时,会对这种「返回局部具名对象」的场景做 NRVO(Named Return Value Optimization,具名返回值优化),直接把拷贝次数降到 0 次

NRVO 的核心逻辑 :编译器会直接把局部对象 str 的内存,分配到调用者(main 函数)为 ret 分配的栈空间里 ------ 也就是说,str 和 ret 本质上是同一个对象

优化后的过程:addStrings 函数内,string str; 不再在函数栈帧上创建新对象,而是直接在 ret 的内存地址上构造 str。后续所有对 str 的操作(比如 str += ...、reverse),其实都是直接操作 ret 本身,相当于str是ret的别名。return str; 时,不需要任何拷贝,直接把这个对象 "交出去" 即可。

对于有名对象和匿名对象来说,这里的优化规则也略有不同

  1. 匿名对象返回(RVO)
    C++17 之前是可选优化,C++17 之后变成强制消除临时对象 (直接记匿名对象是规定优化的),编译器必须直接在目标内存中构造对象,不调用任何拷贝。
    比如 string ret = string("123");,C++17 下不会产生临时对象,直接构造 ret。
  2. 有名对象返回(NRVO)
    不管 C++11/14/17/20,标准都没有强制要求 NRVO ,所以有名对象的返回是否优化了不好说,只是允许编译器这么做。
    现代主流编译器在开启优化时,几乎都会做 NRVO,但是不排除在工作中会使用老的编译器,这也是为什么要从C++98开始学,而不是学现在最新的C++23甚至26,像一些很成熟的项目,就比如说十几年前的一个很火的游戏如果现在还有不少人玩,里面的东西都是很老的,或者说微信支付的一个底层的组件,这里面的东西绝对是用C++98写的很老的东西,如果你进入该公司负责维护该项目,必定会接触更多C++98的语法,而不是C++14,17。所以这些看似不太先进很老的底层知识是必须要懂的

C++11也有应对这种拷贝损耗问题时不依赖编译器优化的兜底解决方案,因为天下没有免费的午餐,像上面这种优化有时候是有代价的,优化的不好反而可能存在bug,编译器厂商在做这种极致的优化也会很谨慎,这个过程需要进行语义分析等等一系列操作,是否在所有的场景都不会有副作用,这种标准未规定的东西一般来说都是很不可控的

那么 C++11 以后这里可以使用右值引用做返回值解决吗?显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法解决对象出作用域已经析构销毁的事实。

C++11引入了一个叫右值引用的移动语义来解决问题,这里的移动语义就叫做移动构造移动赋值

3.5.2 移动构造和移动赋值

  • 移动构造函数是一种构造函数,类似拷贝构造函数,它是之前拷贝构造的函数重载,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
  • 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
  • 对于像 string/vector 这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要 "窃取" 引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的 bit::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。

3.5.3 右值引用和移动语义解决传值返回问题

cpp 复制代码
#include<iostream>
#include<assert.h>
#include<string.h>
#include<algorithm>
using namespace std;

namespace yunze
{
    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);
        }

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

        // 拷贝构造
        string(const string& s)
        {
            cout << "string(const string& s) -- 拷贝构造" << endl;

            reserve(s._capacity);
            for (auto ch : s)
            {
                push_back(ch);
            }
        }

        // 移动构造
        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)
        {
            assert(pos < _size);
            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;
    };

    // 传值返回需要拷贝
    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());

        return str;
    }
}

int main()
{
	yunze::string ret = yunze::addStrings("11111", "2222");
}

结合上面的代码说一下所使用移动语义的原理:

阶段 1:return str; → 第一次移动构造(构造临时对象)

addStrings 里的 str 是局部对象,本身是左值 (有名字、可寻址、后续在函数内不会再被修改),但在 return str; 这个场景中,函数结束后会被销毁,编译器自动将其视为亡值(xvalue,属于右值的一种) ,优先匹配移动构造函数 string(string&& s)

这里的 s 是 string&&(右值引用),绑定到 str 上。注意:右值引用变量 s 本身是左值 (有名字、可寻址),所以可以正常调用 swap(s)(swap 参数是 string&,左值引用)。

移动构造函数调用 swap(s),也就是「临时对象」和 str 交换成员:

cpp 复制代码
void swap(string& s)
{
    ::swap(_str, s._str);       // 交换堆内存指针
    ::swap(_size, s._size);     // 交换有效长度
    ::swap(_capacity, s._capacity); // 交换容量
}

假设 addStrings 里的 str 已经处理完数据,此时:
str._str = 0x100(指向堆上的字符串 "13333"),str._size = 5,str._capacity = 4。

移动构造的目标对象(临时对象 /ret)初始状态:_str = nullptr,_size = 0,_capacity = 0(类内初始化)。

  • 临时对象拿到了 str 的堆内存、大小、容量。
  • str 变成空壳对象(_str = nullptr)。

addStrings 函数结束,str 析构,安全释放空指针,无内存问题。

阶段 2:ret = addStrings(...) → 第二次移动构造(构造 ret)

addStrings 返回的临时对象是纯右值,再次匹配移动构造函数,构造 main 里的 ret。

移动构造函数调用 swap(s),也就是 ret 和临时对象交换成员:

  • ret 拿到了临时对象的堆内存、大小、容量。
  • 临时对象变成空壳对象(_str = nullptr)。

临时对象析构,同样安全释放空指针,当 addStrings 函数结束,局部对象 str 被析构时,它的 _str 已经是 nullptr 了。C++ 标准规定:delete[] nullptr 是安全的空操作,不会触发任何内存释放,完美避免了double free问题。

两次移动构造都只做了指针和整数的交换 ,完全没有拷贝堆上的字符串数据,开销是 O (1),和字符串长度无关。这就是移动构造的底层本质:资源所有权的转移,而非数据的复制

也可以看到现代编译器的极致优化就相当于str是ret的别名了,二者地址都指向的是一块空间

左值 vs 右值减少拷贝的区别

很多人会把「左值引用减少拷贝」和「右值移动减少拷贝」搞混,这里结合 string 类做对比:

左值引用就像你把家门钥匙给朋友,让他来你家做客,你家房子还是你的,后续你还能正常住、修改家里的东西。

右值移动就像你马上要搬家了,直接把房子的钥匙给别人,你自己的房子清空了,别人直接住进去,你后续也不会再住了。

再说一下前面说的一些编译器优化覆盖不到的场景(用已经存在的值ret来接收),如图代码注释了移动构造和移动赋值

我这是最新版本的VS2022的C++编译器,编译器虽然靠着自身的优化,把整个拷贝构造拷贝赋值的过程优化为一次拷贝赋值,但我若是使用移动赋值,编译器也会优先调用移动赋值,相比拷贝赋值效率肯定是更高的

所以移动语义这一块是绝对有意义的

C++11 通过给容器接口增加右值引用版本,让临时对象 / 可转移对象的push_back/insert 从深拷贝变成资源转移,用极低的开销完成对象插入,是移动语义最典型的应用场景之一。

  • 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
  • 当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上
  • 其实这里还有一个 emplace 系列的接口,比push_back更高效:它直接在容器内部构造对象,连临时对象的创建都省去了,但是这个涉及可变参数模板,我后面会把可变参数模板讲解以后再讲解 emplace 系列的接口。

四、实现移动语义list的完整源码

List.h

cpp 复制代码
#pragma once
#include<iostream>
#include<assert.h>
#include<string.h>
#include<algorithm>
using namespace std;

namespace yunze
{
	// 泛型
	// 定义链表节点结构
	template<class T>
	struct list_node
	{
		list_node<T>* _prev;
		list_node<T>* _next;
		T _data;
		//节点的构造函数
		//左值版本的构造,若是左值,自定义类型走对应的拷贝构造
		list_node(const T& x = T())
			:_prev(nullptr)
			, _next(nullptr)
			, _data(x)
		{
		}

		//提供右值版本的构造,若是右值,自定义类型走对应的移动构造
		list_node(T&& x)
			:_prev(nullptr)
			, _next(nullptr)
			//这里若是string就调用string对应的移动构造
			, _data(move(x))
		{
		}
	};

	//Ref(引用)
	template<class T, class Ref, class Ptr>
	//用类封装迭代器
	//list_iterator 本身是一个类模板,list_iterator<T> 是这个类模板的一个实例化
	struct list_iterator
	{
		//自身类型别名 
		using Self = list_iterator<T, Ref, Ptr>;
		//链表节点类型别名
		using Node = list_node<T>;
		//指向链表节点的指针(迭代器的抓手)
		Node* _node;

		//构造函数接收一个Node*类型的指针,通过初始化列表将_node指向该节点
		//作用是确定迭代器在链表中的初始位置(比如指向第一个有效节点、哨兵节点等)
		list_iterator(Node* node)
			:_node(node)
		{
		}

		//*it = 1
		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		//前置++
		Self& operator++()
		{
			_node = _node->_next;
			//返回迭代器对象自身
			return *this;
		}

		//后置++
		Self operator++(int)
		{
			Self tmp(*this);
			_node = _node->_next;
			//返回迭代器对象自身
			return tmp;
		}

		//--it
		Self& operator--()
		{
			_node = _node->_prev;
			return *this;
		}

		//it--
		Self operator--(int)
		{
			Self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		bool operator!=(const Self& s) const
		{
			return _node != s._node;
		}

		bool operator==(const Self& s) const
		{
			return _node == s._node;
		}

	};

	//template<class T>
	////用类封装迭代器
	//struct list_const_iterator
	//{
	//	//自身类型别名
	//	using Self = list_const_iterator<T>;
	//	//链表节点类型别名
	//	using Node = list_node<T>;
	//	//指向链表节点的指针(迭代器的抓手)
	//	Node* _node;

	//	//构造函数接收一个Node*类型的指针,通过初始化列表将_node指向该节点
	//	//作用是确定迭代器在链表中的初始位置(比如指向第一个有效节点、哨兵节点等)
	//	list_const_iterator(Node* node)
	//		:_node(node)
	//	{
	//	}

	//	//*it
	//	const T& operator*()
	//	{
	//		return _node->_data;
	//	}

	//	//前置++
	//	Self& operator++()
	//	{
	//		_node = _node->_next;
	//		//返回迭代器对象自身
	//		return *this;
	//	}

	//	//后置++
	//	Self operator++(int)
	//	{
	//		Self tmp(*this);
	//		_node = _node->_next;
	//		//返回迭代器对象自身
	//		return tmp;
	//	}

	//	//--it
	//	Self& operator--()
	//	{
	//		_node = _node->_prev;
	//		return *this;
	//	}

	//	//it--
	//	Self operator--(int)
	//	{
	//		Self tmp(*this);
	//		_node = _node->_prev;
	//		return tmp;
	//	}

	//	bool operator!=(const Self& s) const
	//	{
	//		return _node != s._node;
	//	}

	//	bool operator==(const Self& s) const
	//	{
	//		return _node == s._node;
	//	}

	//};

	//定义链表类本身
	template<class T>
	class list
	{
		using Node = list_node<T>;
	public:
		using iterator = list_iterator<T, T&, T*>;
		using const_iterator = list_iterator<T, const T&, const T*>;
		//using const_iterator = list_const_iterator<T>;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

		void empty_init()
		{
			//创建一个哨兵节点(头节点),不存储任何有效数据
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
		}

		// list类的默认构造函数 
		list()
		{
			empty_init();
		}

		//{ }初始化
		list(initializer_list<T> il)
		{
			empty_init();
			for (auto& e : il)
			{
				push_back(e);
			}
		}

		template <class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			empty_init();
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		list(size_t n, T val = T())
		{
			empty_init();
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		list(int n, T val = T())
		{
			empty_init();
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		// 传统写法
		// lt2(lt1)
		list(const list<T>& lt)
		{
			empty_init();
			for (auto& e : lt)
			{
				push_back(e);
			}
		}

		//支持连续赋值
		list<T>& operator=(const list<T>& lt)
		{
			//判断是不是自己给自己赋值
			if (this != &lt)
			{
				//释放lt1
				clear();
				for (auto& e : lt)
				{
					push_back(e);
				}
			}
			return *this;
		}

		//// 现代写法
		//// lt2(lt1)
		////list(list<T>& lt)
		//list(const list& lt)
		//{
		//	empty_init();
		//	//list<T> tmp(lt.begin(), lt.end());
		//	list tmp(lt.begin(), lt.end());
		//	swap(tmp);
		//}

		//// 现代写法
		//// lt1 = lt3
		////list<T>& operator=(list<T> tmp)
		//list& operator=(list tmp)
		//{
		//	swap(tmp);
		//	return *this;
		//}

		//void swap(list<T>& lt)
		void swap(list& lt)
		{
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
			_size = 0;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				it = erase(it);
			}
		}

		////尾插一个新元素
		//void push_back(const T& x)
		//{
		//	Node* newnode = new Node(x);
		//	// 之前的尾节点
		//	Node* tail = _head->_prev;
		//	tail->_next = newnode;
		//	newnode->_prev = tail;
		//	newnode->_next = _head;
		//	_head->_prev = newnode;
		//}

		void push_back(const T& x)
		{
			//在end前面插入一个值
			insert(end(), x);
		}

		void push_back(T&& x)
		{
			//在end前面插入一个值
			//这里右值引用x的本身属性是左值,为了匹配右值引用版本的insert,需要move为右值
			insert(end(), move(x));
		}

		void push_front(const T& x)
		{
			//在begin前面插入一个值
			insert(begin(), x);
		}

		void pop_back()
		{
			//找到end的前一个节点
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		void insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);
			//prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
		}

		void insert(iterator pos, T&& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			//若是右值调用右值版本的构造
			Node* newnode = new Node(move(x));
			//prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
		}

		iterator erase(iterator pos)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			//prev next
			prev->_next = next;
			next->_prev = prev;
			delete cur;
			--_size;
			//return iterator(next);
			return next;
		}

		size_t size() const
		{
			//size_t n = 0;
			////不知道数据类型,用引用
			//for (auto& e : *this)
			//{
			//	++n;
			//}
			//return n;
			return _size;
		}
	private:
		//链表的唯一私有成员,指向哨兵节点(头节点)的指针
		//通过该指针可访问整个链表
		Node* _head;
		size_t _size = 0;
	};
}

test.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 666
#include"List.h"

namespace yunze
{
    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);
        }

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

        // 拷贝构造
        string(const string& s)
        {
            cout << "string(const string& s) -- 拷贝构造" << endl;

            reserve(s._capacity);
            for (auto ch : s)
            {
                push_back(ch);
            }
        }

        // 移动构造
        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)
        {
            assert(pos < _size);
            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;
    };

    // 传值返回需要拷贝
    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());

        return str;
    }
}

int main()
{
    yunze::list<yunze::string> lt;
    cout << "*************************" << endl;

    yunze::string s1("111111111111111111111");
    lt.push_back(s1);
    cout << "*************************" << endl;

    lt.push_back(yunze::string("22222222222222222222222222222"));
    cout << "*************************" << endl;

    lt.push_back("3333333333333333333333333333");
    cout << "*************************" << endl;

    // 左值move本质授予别人转移你数据资源权限,所以要谨慎
    lt.push_back(move(s1));
    cout << "*************************" << endl;

    return 0;
}

结语

相关推荐
froginwe112 小时前
DOM 加载函数
开发语言
Hello eveybody2 小时前
介绍一下背包DP(Python)
开发语言·python·动态规划·dp·背包dp
AI进化营-智能译站2 小时前
ROS2 C++开发系列12-用多态与虚函数构建可扩展的ROS2机器人行为模块
开发语言·c++·ai·机器人
iCxhust2 小时前
微机原理实践教程(C语言篇)---A002流水灯
c语言·开发语言·单片机·嵌入式硬件·51单片机·课程设计·微机原理
Morwit2 小时前
QML组件之间的通信方案(暴露子组件)
c++·qt·职场和发展
qeen872 小时前
【数据结构】建堆的时间复杂度讨论与TOP-K问题
c语言·数据结构·c++·学习·
莎士比亚的文学花园2 小时前
Linux驱动开发(3)——设备树
开发语言·javascript·ecmascript
图码3 小时前
如何用多种方法判断字符串是否为回文?
开发语言·数据结构·c++·算法·阿里云·线性回归·数字雕刻
U盘失踪了3 小时前
python curl转python脚本
开发语言·chrome·python