C++11 核心特性解析(一):从初始化列表到移动语义,解锁高效对象构造

目录

一、列表初始化

[1.1 C++98传统的{ }](#1.1 C++98传统的{ })

[1.2 C++11中的{ }](#1.2 C++11中的{ })

[1.3 C++11中的std: :initializer_list](#1.3 C++11中的std: :initializer_list)

二、左/右值以及引用

[2.1 什么是左/右值](#2.1 什么是左/右值)

[2.2 左/右值引用](#2.2 左/右值引用)

[3.3 引用延长生命周期](#3.3 引用延长生命周期)

[3.4 左/右值的参数匹配](#3.4 左/右值的参数匹配)

三、移动语义

[3.1 左值引用的局限性](#3.1 左值引用的局限性)

[3.2 移动构造和移动赋值](#3.2 移动构造和移动赋值)

[3.2.1 移动构造](#3.2.1 移动构造)

[3.2.2 移动赋值](#3.2.2 移动赋值)

一、列表初始化

1.1 C++98传统的{ }

C++98中一般数组和结构体可以用{ }初始化:

cpp 复制代码
struct Point
{
	int a;
	int b;
};

int mian()
{
	int arr[] = {1,2,3,4,5,6};
	Point d1 = {32,64};
}

1.2 C++11中的{ }

C++11后想统一初始化方法,试图实现一切对象都可利用{ }来进行初始化,{ }也叫做列表初始化。

这里分为两种情况,一种是内置类型的列表初始化一种是自定义类型的列表初始化:

cpp 复制代码
class Data
{
public:
	Data(int x,int y,int z)
		:_x(x)
		,_y(y)
		,_z(z)
	{
		std::cout << "Data()" << std::endl;
	}
	Data(const Data& data)
	{
		_x = data._x;
		_y = data._y;
		_z = data._z;
		std::cout << "Data(const Data& data)" << std::endl;
	}
private:
	int _x;
	int _y;
	int _z;
};

int main()
{
	int x = { 5 };
	Data mydata = {5,6,7};
	return 0;
}

需要注意的是,在这里自定义类型使用列表初始化的时候本质上是首先调用构造函数利用5,6,7三个参数构造了一个临时对象,然后利用这个临时对象调用拷贝构造初始化mydata。

但是在这里编译器做了优化,直接将5,6,7三个参数用来构造并初始化mydata了:

怎么确定编译器本质上是利用三个参数先构造了一个临时对象呢?我们可以执行下面的代码:

cpp 复制代码
Data& mydata = {5,6,7};

发现这是语法直接报错了,因为临时对象具有常型所以我们只能使用const引用:

cpp 复制代码
const Data& mydata = {5,6,7};

C++11中还规定利用列表初始化的时候可以省略掉=,所以以下的代码也是可以运行的:

cpp 复制代码
Data mydata  {5,6,7};

1.3 C++11中的std: :initializer_list

initializer_list是 C++11 引入的标准库模板,专门用来接收花括号 {} 包裹的初始化列表,让我们可以用统一的简洁语法初始化容器、自定义类型,也能轻松传递一组同类型数据。

他的成员接口主要包含迭代器,迭代器的底层主要是原生指针:

他具有以下特点:

  • 只能存放同类型数据(比如全是 int、全是 string
  • 只读不可修改:只能遍历读取,不能增删改元素
  • 自动构造:编译器看到 {} 列表,会自动转成 initializer_list
  • 标准头文件:需要包含 <initializer_list>(很多容器头文件已间接包含)
cpp 复制代码
int main()
{
	//这里本质上首先构造临时对象->拷贝构造v1
	//编译器优化为直接构造v1
	std::vector<int> v1= {5,8,9,8,7};
	//所以也可以直接省略=
	std::vector<int> v2 { 5,8,9,8,7 };
	//也可以将{ 5,8,9,8,7 }直接作为一个initializer_list构造v3
	std::vector<int> v3({ 5,8,9,8,7 });
	return 0;
}

二、左/右值以及引用

2.1 什么是左/右值

左值是⼀个表示数据的表达式(如变量名或解引用的指针),⼀般是有持久状态,存储在内存中,我 们可以获取它的地址。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址。

cpp 复制代码
int main()
{
	//常见的左值
	int* p = new int(0);
	int b = 1;
	const int c = b;
	*p = 10;
	std::string s("111111");
	s[0] = 'x';


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

右值也是⼀个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象 或匿名对象等右值不能取地址。

所以一般的右值主要有三种:字面值常量,临时对象,匿名对象。

它区别于左值的核心特征是不能取地址。

cpp 复制代码
int main()
{
	// 右值:不能取地址
	double x = 1.1, y = 2.2;
	10;
	x + y;
	fmin(x, y);
	std::string("11111");


	cout << &10 << endl;
	cout << &(x+y) << endl;
	cout << &(fmin(x, y)) << endl;
	cout << &string("11111") << endl;
	return 0;
}

2.2 左/右值引用

左值引用、右值引用是 C++ 的复合类型 ,均为对象的别名(无独立内存,底层由指针实现);

  • 左值引用(左值别名):T&,C++98 原生支持
  • 右值引用(右值别名):T&&,C++11 新增,为移动语义设计

需要注意的是,左值引用不能直接引用右值但是const左值引用一般可以引用右值(上面我们说过右值主要有三种:字面值常量,临时对象,匿名对象他们都一般具有常型);右值引用不能直接引用左值但是右值引用可以引用move(左值),关于move的介绍我们后续会详细讲解。

cpp 复制代码
// 左值:可以取地址
// 以下的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];
cpp 复制代码
// 右值引⽤给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
cpp 复制代码
// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);

需要注意的是,变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。语法层面看,左值引用和右值引用都是取别名,不开空间。

3.3 引用延长生命周期

前面我们讲过右值大都分为三种:字面值常量,临时对象,匿名对象

这些对象都有一个特点就是生命周期并不是持久的或者说它们的生命周期大都只在当前一行(比如匿名对象)。右值引用有一个显著的特点就是可以延长右值的生命周期,同理const左值引用也可以延长右值的生命周期。

cpp 复制代码
int main()
{
	std::string s1 = "Test";
	//const左值引用延长临时对象的生命周期
	const std::string& r2 = s1 + s1;
	//  r2 += "Test";    错误不能修改const引用引用的对象
	//右值引用延长临时对象的生命周期
	std::string&& r3 = s1 + s1;
	r3 += "Test";
	std::cout << r3 << '\n';
	return 0;
}

3.4 左/右值的参数匹配

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

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

cpp 复制代码
#include<iostream>
using namespace std;
void f(int& x)
{
	std::cout << "左值引⽤重载f(" << x << ")\n";
}
void f(const int& x)
{
	std::cout << "到const的左值引⽤重载f(" << x << ")\n";
}
void f(int&& x)
{
	std::cout << "右值引⽤重载f(" << x << ")\n";
}
int main()
{
	int i = 1;
	const int ci = 2;
	f(i);  // 调⽤f(int&)
	f(ci); // 调⽤f(const int&)
	f(3);  // 调⽤f(int&&),如果没有f(int&&)重载则会调⽤f(const int&)
	f(std::move(i)); // 调⽤f(int&&)
	// 右值引⽤变量在⽤于表达式时是左值
	int&& x = 1;
	f(x);            // 调⽤f(int& x)
	f(std::move(x)); // 调⽤f(int&& x)
	return 0;
}

三、移动语义

3.1 左值引用的局限性

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

cpp 复制代码
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;
        }
}

这里我们来详细分析一下addStrings函数,函数传参可以使用左值引用这样可以减少拷贝。但是传返回值不能使用左值引用返回,因为str变量在函数的栈帧内部定义。当函数调用结束,栈帧被销毁该变量的生命周期也就随之结束了,如果使用左值引用返回则会导致未定义行为(悬空引用)就等于返回了一个指向已销毁内存的野引用,程序会崩溃、乱码、逻辑错误。在这种情况下我们只能使用传值返回调用拷贝构造。

这就暴露了左值引用的一个弊端,因为有时候拷贝构造尤其是频繁的深拷贝是一个非常消耗性能的一个过程(如vector<vector>二维数组)。那么有什么办法可以解决这种问题呢?

1. 现代编译器已经做出了优化

在老标准(C++98)中如果传值返回编译器会一步步执行创建临时对象,然后调用拷贝构造。传值传参也是一样的,单参数非explicit构造函数,支持隐式类型转换这个过程也会走创建临时对象再调用拷贝构造。

cpp 复制代码
namespace yue
{
	class string
	{
	public:
		typedef char *iterator;
		iterator begin() const
		{
			return _str;
		}
		iterator end() const
		{
			return _str + _size;
		}
		void reserve(int n)
		{
			if (n > _capacity)
			{
				char *temp = new char[n + 1];
				strcpy(temp, _str);
				delete[] _str;
				_str = temp;
				_capacity = n;
			}
		}
		// 构造
		string()
		{
			_str = new char[1]{'\0'};
			_size = _capacity = 0;
			std::cout << "string()构造" << std::endl;
		}
		string(const char *str)
		{
			int len = strlen(str);
			_str = new char[len + 1];
			strcpy(_str, str);
			_size = _capacity = len;
			std::cout << "string(const char *str)构造" << std::endl;
		}
		string(const string &ch)
		{
			_str = new char[ch._capacity + 1];
			strcpy(_str, ch._str);
			_size = ch._size;
			_capacity = ch._capacity;
			std::cout << "string(const string &ch) 拷贝构造" << std::endl;
		}
        void Swap(string &str)
		{
			std::swap(_str, str._str);
			std::swap(_size, str._size);
			std::swap(_capacity, str._capacity);
		}
		// 尾插一个字符
		void PushBack(const char ch)
		{

			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			_str[_size++] = ch;
			_str[_size] = 0;
		}
		~string()
		{
			delete[] _str;
			_str = NULL;
			_size = _capacity = 0;
		}

	private:
		char *_str;
		int _size;
		int _capacity;

		static const size_t npos;
	};

	std::ostream &operator<<(std::ostream &out, const string &s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}
}
yue::string addStrings(yue::string num1, yue::string num2)
{
	yue::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';
	std::reverse(str.begin(), str.end());
	return str;
}

int main()
{
	yue::string s3 = addStrings("11111", "22222");
	std::cout << s3 << std::endl;
}

如上述代码我们利用自定义实现的string调用addStrings接口并使用传值传参并使用命令行

bash 复制代码
g++ -std=c++14 -O0 -fno-elide-constructors main.cc -o main

关闭g++的优化会出现以下的运行结果:

这里我们利用两张图来理解一下:

为了减少过多的拷贝降低性能,现代 C++(C++11 及以后)基本不会创建临时对象 + 拷贝构造,而是直接初始化。相同的代码我们放在VS2019中的Debug环境中会有以下的运行结果:

在Release环境下编译器的优化会更加激进,编译器会直接优化掉最后一步的拷贝构造:

此时,addStrings函数内部的str变量就是s3,他们虽然名称不同但是用的是同一块物理空间。如果我们对其取地址会发现两者的地址是一样的,对str的修改就是对s3的修改,这样编译器便优化掉了最后一个拷贝构造。

2.使用移动语义减少拷贝

下面我们就来介绍一下移动语义以及移动语义是如何减少拷贝提高性能的

3.2 移动构造和移动赋值

移动语义 是 C++11 引入的核心类型系统与对象生命周期特性,基于右值引用实现,核心语义为:对可转移资源的对象,执行资源所有权的转移,而非资源的拷贝复制,从语言层面消除临时对象、纯右值的冗余深拷贝开销,同时支撑仅可移动类型的设计。

移动语义通过两个类成员函数实现,编译器会依据重载决议优先绑定右值:

3.2.1 移动构造

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

在移动构造的内部,我们并不需要执行类似拷贝构造函数类似的深拷贝而是进行资源的转移。比如在我们上述示例的yue::string这个类而言它的移动构造应该这样定义:

cpp 复制代码
void Swap(string& str)
{
	std::swap(_str, str._str);
	std::swap(_size, str._size);
	std::swap(_capacity, str._capacity);
}
//移动构造
string(string&& s)
{
	Swap(s);
}

这里或许我们会有这样一个疑问:右值不是不可移动的吗,为什么可以直接传递给Swap的左值引用?前面我们讲过,右值是不可移动的但是当它被右值引用引用后右值引用本身的属性是左值。

那右值引用应该怎么用呢,他能给我们带来哪些便捷呢?我们保持main函数内部的逻辑不变,为yue::string类添加移动构造接口并关闭所有优化并运行会得到一下结果:

下面我们来画图解释一下:

在实际情况下,编译器对代码的一定优化加上移动构造便可以大幅度的降低性能的损耗,如下图在VS2019的Debug与release环境下的运行结果:

注意:

如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意⼀ 个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执 行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤用移动构造,没有实现就调用拷贝构造。

3.2.2 移动赋值

除了构造函数会涉及到拷贝操作之外,赋值也可能会涉及到拷贝操作。例如,我们将我们的示例代码该成下面这种情况:

cpp 复制代码
#include <assert.h>
#include <cstring>
#include <algorithm>
namespace yue
{
	class string
	{
	public:
		typedef char *iterator;
		iterator begin() const
		{
			return _str;
		}
		iterator end() const
		{
			return _str + _size;
		}
		void reserve(int n)
		{
			if (n > _capacity)
			{
				char *temp = new char[n + 1];
				strcpy(temp, _str);
				delete[] _str;
				_str = temp;
				_capacity = n;
			}
		}
		// 构造
		string()
		{
			_str = new char[1]{'\0'};
			_size = _capacity = 0;
			std::cout << "string()构造" << std::endl;
		}
		string(const char *str)
		{
			int len = strlen(str);
			_str = new char[len + 1];
			strcpy(_str, str);
			_size = _capacity = len;
			std::cout << "string(const char *str)构造" << std::endl;
		}
		string(const string &ch)
		{
			_str = new char[ch._capacity + 1];
			strcpy(_str, ch._str);
			_size = ch._size;
			_capacity = ch._capacity;
			std::cout << "string(const string &ch) 拷贝构造" << std::endl;
		}
		void Swap(string &str)
		{
			std::swap(_str, str._str);
			std::swap(_size, str._size);
			std::swap(_capacity, str._capacity);
		}
		string(string&& s)
		{
			Swap(s);
			std::cout << "string(string&& s) 移动构造" << std::endl;
		}
		//赋值运算符重载
		string& operator=(const string& str)
		{
			if (*this == str)
			{
				return *this;
			}
			reserve(str._capacity);
			strcpy(_str,str._str);
			_size = str._size;
			std::cout << "string &operator=(string str) 拷贝赋值" << std::endl;
			return *this;
		}
		// 尾插一个字符
		void PushBack(const char ch)
		{

			if (_size == _capacity)
			{
				reserve(_capacity == 0 ? 4 : 2 * _capacity);
			}
			_str[_size++] = ch;
			_str[_size] = 0;
		}
		void operator+=(const char ch)
		{
			PushBack(ch);
		}
		char operator[](int pos)
		{
			assert(pos >= 0 && pos < _size);
			return _str[pos];
		}
		int size()
		{
			return _size;
		}
		int capacity()
		{
			return _capacity;
		}
		~string()
		{
			delete[] _str;
			_str = NULL;
			_size = _capacity = 0;
		}
		bool operator==(const string &s2)
		{
			return strcmp((*this).c_str(), s2.c_str()) == 0;
		}
	private:
		char *_str;
		int _size;
		int _capacity;

		static const size_t npos;
	};
	
	std::ostream &operator<<(std::ostream &out, const string &s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}
}

yue::string addStrings(yue::string num1, yue::string num2)
{
	yue::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';
	std::reverse(str.begin(), str.end());
	return str;
}

int main()
{
	yue::string s3 ;
	s3= addStrings("22222","11111");

	std::cout << s3<< std::endl;
}

此时的运行结果会是:

我们来画图解释一下:

为什么这里会调用一次移动构造和一次移动赋值呢?关键在于main函数中的这处区别:

cpp 复制代码
//在构造时使用=符号调用的是构造函数(移动构造)
yue::string s3 = addStrings("11111", "22222");

//构造完成后在使用=进行的是赋值操作(拷贝/移动赋值)
yue::string s3;
s3=addStrings("11111", "22222");

因为代码中我们没有实现以右值为参数的赋值操作接口,所以临时对象给s3进行赋值操作的时候调用的是拷贝赋值用于将临时对象拷贝初始化s3。

与移动构造引入的原因一样,C++11标准也为我们带来了移动赋值接口,用于把一个右值对象赋值给已存在的对象时调用,先释放自身旧资源,再窃取右值资源。在我们的yue::string类中它应该这样实现:

cpp 复制代码
void Swap(string &str)
{
	std::swap(_str, str._str);
	std::swap(_size, str._size);
	std::swap(_capacity, str._capacity);
}
//移动赋值接口
string& operator=(string&& str)
{
	Swap(str);
	return *this;
}

此时,临时对象会被传递给移动赋值直接完成资源的置换不用进行拷贝。提高了代码的运行效率:

注意:

如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意 一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会 执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调 ⽤移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上⾯移动构造完全类似)

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

相关推荐
郝学胜-神的一滴2 小时前
冷却时间下的任务调度最优解:从原理到实现
数据结构·c++·算法·面试
大鹏说大话2 小时前
Java 并发基石:CAS 原理深度解析与 ABA 问题终极解决方案
开发语言·python
啊董dong2 小时前
noi-2026年3月24号作业
数据结构·c++·算法
zhixingheyi_tian2 小时前
Velox 之 libhdfs
c++
ALex_zry2 小时前
C++ MQTT物联网通信实战:从入门到生产环境
java·c++·物联网
bjxiaxueliang2 小时前
一文掌握Python aiohttp:异步Web开发从入门到部署
开发语言·前端·python
想搞艺术的程序员2 小时前
Go RWMutex 源码分析:一个计数器,如何把“读多写少”做得又快又稳
开发语言·redis·golang
吴声子夜歌2 小时前
JavaScript——JSON序列化和反序列化
开发语言·javascript·json
汉克老师2 小时前
GESP5级C++考试语法知识(十、二分算法(二))
c++·算法·二分算法·gesp5级·gesp五级·找答案