首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >C++11引入了哪些资源管理新规则?右值引用与移动语义为何是性能优化的关键?

C++11引入了哪些资源管理新规则?右值引用与移动语义为何是性能优化的关键?

作者头像
用户11831438
发布2025-12-30 14:26:23
发布2025-12-30 14:26:23
1440
举报

一、C++11的发展历史

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

ok,接下来,我们就开始学习C++11的相关知识~

二、列表初始化

也许会有很多UU看到这个标题之后,会想到我们之前学的初始化列表,ok,初始化列表和列表初始化列表是没有任何关系的,这两个不是一回事儿~

2.1 C++98传统的{ }

在C++98中,我们通常使用 { } 进行数组或者结构体的初始化——

代码语言:javascript
复制
struct A
{
	int _a1;
	int _a2;
};
void test1()
{
	//使用{ }对数组初始化
	int a[] = { 1,2,3,4,5,6,7 };
	//使用{ } 对结构初始化
	A b={ 1,2};
}

但是在C++11中使用{ } 的方法更高级点——

2.2 C++11中的 { }
  • C++11以后想统一初始化方式,试图实现一切对象皆可使用 { } 初始化,{ } 初始化也叫做列表初始化
  • 内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,最后优化了以后变成直接构造
  • { }初始化的过程中,可以省略=

ok,我们通过代码来看一下——

其实我们看到这个 { } 初始化对于内置类型的便捷之处好像没有那么明显,那我们接着看对于自定义类型——

代码语言:javascript
复制
class Date
{
public:
    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,16 };
    return d;*/

    //return { 2025,11,16 };

    //返回默认构造
    Date d;
    return d;

    //return {};
}

int main()
{
    int array1[] = { 1, 2, 3, 4, 5 };
    int array2[5] = { 0 };

    Date d2 = 1025;//单参数的隐式类型转换
    Date d2 = { 2025,11,17 };
    Date d3{ 2025,11,17 };
    Date d4{};//调用默认构造,用缺省值
    Date d5;  //也是调用默认构造,用缺省值

    Insert(2025);
    Insert({ 2025,11,17 });


    // C++11⽀持的
    // 内置类型⽀持
    int x1 = { 2 };
    // ⾃定义类型⽀持
    // 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象
    // 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化d1
    // 运⾏⼀下,我们可以验证上⾯的理论,发现是没调⽤拷⻉构造的
    Date d1 = { 2025, 1, 1 };

    // 这⾥d2引⽤的是{ 2024, 7, 25 }构造的临时对象
    const Date& d2 = { 2024, 7, 25 };

    // 需要注意的是C++98⽀持单参数时类型转换,也可以不⽤{}
    Date d3 = { 2025 };
    Date d4 = 2025;

    // 可以省略掉=
    Point p1{ 1, 2 };
    int x2{ 2 };
    Date d6{ 2024, 7, 25 };
    const Date& d7{ 2024, 7, 25 };

    // 不⽀持,只有{}初始化,才能省略=
    // Date d8 2025;

    vector<Date> v;
    v.push_back(d1);
    v.push_back(Date(2025, 1, 1));

    // ⽐起有名对象和匿名对象传参,这⾥{}更有性价⽐
    v.push_back({ 2025, 1, 1 });

    return 0;
}
  • 可以将 = 省略——

我们可以从图中看到,内置类型同样也可以用{ }初始化,但是我们看习惯了直接赋值,那么这里还要用{ }初始化的意义是什么呢?

  • C++就是要让一切皆可用{}初始化
2.3 C++11中的std::initializer_list

上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个 vector 对象,我想用 N 个值去构造初始化,那么我们得实现很多个构造函数才能支持,vector<int> v1 = {1,2,3};vector<int> v2 = {1,2,3,4,5};

initializer_list - C++ Reference

  • C++11 库中提出了一个std::initializer_list的类,auto il = { 10, 20, 30 }; // the type of il is an initializer_list,这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束
  • 容器支持一个std::initializer_list的构造函数,也就支持任意多个值构成的 {x1,x2,x3...} 进行初始化。STL 中的容器支持任意多个值构成的 {x1,x2,x3...} 进行初始化,就是通过std::initializer_list的构造函数支持的
代码语言:javascript
复制
int main()
{
	vector<int> v1 = { 1,2,3,4,5,6 };
	vector<int> v2{ 7,8,9 };
 
	//vector(initializer_list<T> l) vector传给initializer_list<T>
	//{
	//	for (auto e : l)
	//		push_back(e)
	//}
 
	map<string, string>dict = { {"sort","排序"},{"string","字符串"} };
 
	v1 = { 10,20,30 };
 
	auto il = { 10,20,30 };
	cout << typeid(il).name() << endl;
 
	std::initializer_list<int> mylist;
	mylist = { 10, 20, 30 };
	cout << sizeof(mylist) << endl;
 
	// 这⾥begin和end返回的值initializer_list对象中存的两个指针
	// 这两个指针的值跟i的地址跟接近,说明数组存在栈上
	int i = 0;
	cout << mylist.begin() << endl;
	cout << mylist.end() << endl;
	cout << &i << endl;
 
	return 0;
}

三、右值引用和移动语义

在C++98中的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,C++11之后我们就将之前学习的引用叫做左值引用。但是无论是左值引用还是右值引用,都是给对象去别名。(并且底层都是由指针实现的)

ok,那接下来,我们就来看看什么是左值,什么是右值?

3.1 左值和右值

左值是一个数据的表达式:

  • 变量名
  • 解引用的指针
  • 函数调用
  • ……

一般是有持久状态,存储在内存中,我们就可以获取它的地址。

左值可以出现在赋值符号的左边,也可以出现在赋值符号的右边。定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址

左值通常具有一个相对持久的一个状态,比如说是一个局部变量,他至少在当前的函数的栈帧里面是持续存在的

总结:左值的关建在于:可以取地址的都是左值

代码语言:javascript
复制
int main()
{
	//左值:可以取地址
	int* p = new int(0);
	int b = 1;
	const int c = b;
	//c = 2; //定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址
	*p = 10;
	string s("11111");
	s[0] = 'x';

	cout << &p << endl;
	cout << &b << endl;
	cout << &c << endl;
	cout << &s << endl;
	cout <<(void*) & s[0] << endl;
	return 0;
}

右值也是一个表示数据的表达式:

  • 要么是字面量常量(10,0.01,nullptr)
  • 要么是表达式求值过程中创建的临时变量(类型转换产生的临时对象,匿名对象)
  • ……

右值可以出现在赋值符号的右边,但是一般不能出现在赋值符号的左边,右值是不能取地址的

总结:右值是不能取地址的!!!

代码语言:javascript
复制
int main()
{
    //右值是不能取地址的
    double x = 1.1, y = 2.2;
    x + y;//表达式
    10;//常量
    string("11111");//类型转换
    //取地址会报错
    //cout << &10 << endl;
    //cout << &(x+y) << endl;
    //cout << &string("11111") << endl;
}

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

也就是说左值和右值的核心区别就是能否取地址(可以取地址的就是左值,不能取地址的就是右值)

通过上面,我们知道,所谓引用无非就是取别名:

  • 左值引用是给左值取别名
  • 右值引用是给右值取别名

那左值引用是否可以引用右值,右值引用是否可以引用左值呢?我们接着往下学——

3.2 左值引用和右值引用

左值引用和右值引用相对应的写法:

  • 左值引用的写法:Type& r1=x;
  • 右值引用的写法:Type&& r2=y;

左值引用就是给左值取别名,右值引用就是给右值取别名。

那左值引用是否可以引用右值,右值引用是否可以引用左值呢?

其实是可以的,只是需要在限定的条件下——

  • 左值引用不能直接引用右值,但是const左值引用可以引用右值
  • 右值引用不能直接引用左值,但是右值引用可以引用move(左值)

为什么左值引用不能直接引用右值,但是const左值引用可以引用右值?

这里就涉及权限问题,我们知道右值是一些常量、临时变量……,常量和临时变量都是不能被修改的,如果左值引用可以直接引用右值,那就导致右值的权限被放大(这是被允许的)

move是库里面的一个函数模板,本质是进行强制类型转换,move(左值)就是将属性从左值转换成右值

代码语言:javascript
复制
int main()
{
	//左值:可以取地址
	int* p = new int(0);
	int b = 1;
	const int c = b;
	*p = 10;
	string s("11111");
	s[0] = 'x';

	//右值是不能取地址的
	double x = 1.1, y = 2.2;
	x + y;//表达式
	10;//常量
	string("11111");//类型转换

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

	//右值引用给右值取别名
	double&& rr1 = x + y;
	int&& r2 = 10;
	string&& r3 = string("11111");//引用类型转换中间产生临时对象

	//左值引用不能直接引用右值,但是const左值引用可以引用右值
	const double& p1 = x + y;
	const int& p2 = 10;
	const string& p3 = string("11111");

	//右值引用不能直接引用左值,但是右值引用可以引用move(左值)
	int*&& pp1 = move(p);
	int&& pp2 = move(b);
	const int&& pp3 = move(c);
	int&& pp4 = move(*p);
	string&& pp5 = move(s);
	char&& pp6 = move(s[0]);

}

注意:(后面会有大用处!!!)

  • 变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值
3.3 引用延长生命周期

右值引用可用于为临时对象延长生命周期,const的左值引用也可以延长临时对象生存期,但这些临时对象无法被修改

上面所说修改的是s1+s1临时变量,通过s1+s1的别名——r3 来修改!!!

我们通过代码来验证一下:

代码语言:javascript
复制
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
};
int main()
{
	A aa1;
	//延长匿名对象A()的生命周期,延长到和ref1,ref2一样
	const A& ref1 = A();
	A&& ref2 = A();
	cout << "main end" << endl;
    return 0;
}
3.4 左值和右值的参数匹配

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

代码语言:javascript
复制
void f(const int& x)
{
	cout << "const int& x" << endl;
}
int main()
{
	int a = 20;//左值
	f(a);
	f(10);//实参传递右值
    return 0;
}

运行结果:

只有const 左值引用作为参数的函数,那么实参传递左值和右值都是可以匹配的

但是C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的函数,会是什么情况

代码语言:javascript
复制
//形参左值引用
void f(int& x)
{
	cout << "左值引用重载" << endl;
}
//形参const左值引用
void f(const int& x)
{
	cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
//形参右值引用
void f(int&& x)
{
	cout << "右值引⽤重载 f(" << x << ")\n";
}
int main()
{
	int a = 20;//左值
	const int b = 30;
	f(a);//调用f(int&)
	f(b);//调用f(const int&)
	f(10);//调用f(int&&),如果没有f(int&&)重载则会调用f(const int&)
}

运行结果——

通过上面的代码,我们可以看出当分别重载左值引用、const左值引用、右值引用作为形参的函数,左值会去调用左值引用的,const左值会去调用const左值引用的,右值会去调用右值引用的

ok,通过上面的代码我们还能验证上面的:右值引用本身的属性是左值

学到这里,不知道有没有uu会有这种想法:为什么要有右值引用?我们左值引用不是用的挺好的嘛,还要搞右值引用干嘛?

ok,既然搞了右值引用,那肯定就说明有些东西仅靠左值引用是无法完成的,必须要靠右值引用才能完成,左值引用和右值引用可以提高效率

3.5 右值引用和移动语义的使用场景
3.5.1 左值引用主要使用场景回顾

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。

左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如下面的addStrings和generate函数,返回值是一个临时对象,出了函数就销毁了,这种情况下C++98中的解决方案只能是被迫使用传值返回(但是代价有点太大了)

代码语言:javascript
复制
class Solution {
public:
	// 传值返回需要拷⻉
	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;
	}
};
class Solution {
public:
	// 这⾥的传值返回拷⻉代价就太⼤了
	vector<vector<int>> generate(int numRows) {
		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;
	}
};

那么C++11以后这里可以使用右值引用作为返回值解决问题吗?显然是不能的。

这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用也无法实现访问销毁的对象

虽然在编译器的优化下,代价不是那么大。

但是编译器优不优化不是标准规定的,在一些比较新的编译器中会有优化,在一些老的编译器中就没有优化,那我们该咋搞呢?那就要靠下面的利器——

3.5.2 移动构造和移动赋值

移动构造函数是一种构造函数,类似拷贝构造函数移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用如果还有其他参数,额外的参数必须有缺省值

也就是说:构造是对左值引用(&),移动构造是对右值引用(&&)

移动赋值是一个赋值运算符的重载,它跟拷贝复制构成函数重载,类似拷贝赋值函数移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用

也就是说:移动赋值是对右值拷贝,拷贝赋值是对左值赋值拷贝

现在呢?如果是右值呢,他就不想去拷贝了,而是想去完成移动。这是啥意思?

对于自定义类型,以前只有左值时,对于函数返回值为局部对象,我们只能去调用拷贝构造——

但是呢,好像有点浪费~

大佬们是这样搞的——

既然你是一个右值,并且马上就要析构了,我就不去执行拷贝了,直接把你的资源转移给我,然后我再去转移给其他人,这样就不浪费了~

在上面参数匹配的时候——

所以右值引用的核心意义是为了区分出左值和右值!!!

对于左值,我们就执行拷贝那一套;对于右值,我们就不拷贝,而是进行资源的转移

对于像string/vector这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义。

因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提高效率。

3.6 右值引用和移动语义解决传值返回问题
右值对象构造,只有拷贝构造,没有移动构造的场景
  • 图 1 展示了 vs2019 debug 环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
  • 需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release,下面代码优化为非常恐怖,会直接将 str 对象的构造,str 拷贝构造临时对象,临时对象拷贝构造 ret 对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图 3 所示。
  • linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造优化,运行结果可以看到图 1 左边没有优化的两次拷贝

图1

右值对象构造,有拷贝构造,也有移动构造的场景
  • 图2展示了 vs2019 debug 环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。
  • 需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release,下面代码优化为非常恐怖,会直接将 str 对象的构造,str 拷贝构造临时对象,临时对象拷贝构造 ret 对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如图 3 所示。
  • linux 下可以将下面代码拷贝到 test.cpp 文件,编译时用 g++ test.cpp -fno-elide-constructors 的方式关闭构造优化,运行结果可以看到图 1 左边没有优化的两次移动

图2

右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
  • 图 4 左边展示了 vs2019 debug 和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
  • 需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到 str 的析构是在赋值以后,说明 str 就是临时对象的别名
右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
  • 图 5 左边展示了 vs2019 debug 和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次移动构造,一次移动赋值。
  • 需要注意的是在 vs2019 的 release 和 vs2022 的 debug 和 release,下面代码会进一步优化,直接构造要返回的临时对象,str 本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到 str 的析构是在赋值以后,说明 str 就是临时对象的别名

C++11有了移动构造和移动赋值,有了移动构造和移动赋值的代价足够的低!!!

ok,也许会有uu会有这种想法:

传值返回在编译器的优化下已经没有拷贝了,是否意味上面的右值引用和移动语义就没意义了?

  1. 拷贝优化不是标准规定的,只有比较新的编译器会做这样的优化
  2. 其他场景做不到这样的优化(不进行拷贝的场景)
3.7 右值引用和移动语义在传参中的提效
  • 查看 STL 文档我们发现 C++11 以后容器的push和insert系列的接口都增加了右值引用版本
  • 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象
  • 当实参是一个右值,容器内部则调用移动构造,将右值对象的资源转移到容器空间的对象上
  • 把我们之前模拟实现的bit::list拷贝过来,支持右值引用参数版本的push_back和insert

但是当我们运行上面代码的时候,会发现调不到右值引用的insert。

传右值时可以调用右值的push_back,但是右值的push_back复用右值的insert时,却调不到右值引用的insert,这是什么原因?

也就是说——

x的本身属性是左值,那么调用的insert还是左值的那个

我们先来看看为什么要这么设计?

解决方案:

代码语言:javascript
复制
#include"List.h"
int main()
{
	bit::list<bit::string> lt;
	cout << "*************************" << endl;
	bit::string s1("111111111111111111111");
	lt.push_back(s1);
	cout << "*************************" << endl;
	lt.push_back(bit::string("22222222222222222222222222222"));
	cout << "*************************" << endl;
	lt.push_back("3333333333333333333333333333");
	cout << "*************************" << endl;
	lt.push_back(move(s1));
	cout << "*************************" << endl;
	return 0;
}
结尾

都看到这里啦!那请大佬不要忘记给博主来个“一键三连”哦!

૮₍ ˶ ˊ ᴥ ˋ˶₎ა

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、C++11的发展历史
  • 二、列表初始化
    • 2.1 C++98传统的{ }
    • 2.2 C++11中的 { }
    • 2.3 C++11中的std::initializer_list
  • 三、右值引用和移动语义
    • 3.1 左值和右值
    • 3.2 左值引用和右值引用
    • 3.3 引用延长生命周期
    • 3.4 左值和右值的参数匹配
    • 3.5 右值引用和移动语义的使用场景
      • 3.5.1 左值引用主要使用场景回顾
      • 3.5.2 移动构造和移动赋值
    • 3.6 右值引用和移动语义解决传值返回问题
      • 右值对象构造,只有拷贝构造,没有移动构造的场景
      • 右值对象构造,有拷贝构造,也有移动构造的场景
      • 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
      • 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
    • 3.7 右值引用和移动语义在传参中的提效
    • 结尾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档