【C++】C++11 新特性

本篇文章会讲解C++11中添加的各种新特性,包括 initializer_list、右值引用与移动语义、可变模板参数等等


目录

[1 C++ 的发展历史](#1 C++ 的发展历史)

[2 列表初始化](#2 列表初始化)

[2.1 任何对象都可列表初始化](#2.1 任何对象都可列表初始化)

[2.2 C++11 中的 initializer_list](#2.2 C++11 中的 initializer_list)

[3 右值引用与移动语义](#3 右值引用与移动语义)

[3.1 左值与右值](#3.1 左值与右值)

[3.2 左值引用与右值引用](#3.2 左值引用与右值引用)

[3.3 左值引用与右值引用的参数匹配](#3.3 左值引用与右值引用的参数匹配)

[3.4 右值引用与移动语义](#3.4 右值引用与移动语义)

[3.5 移动构造与移动赋值](#3.5 移动构造与移动赋值)

[3.6 引用折叠](#3.6 引用折叠)

[3.7 完美转发](#3.7 完美转发)

[4 可变参数模板](#4 可变参数模板)

[4.1 可变参数模板语法](#4.1 可变参数模板语法)

[4.2 可变参数模板原理](#4.2 可变参数模板原理)

[4.3 包扩展](#4.3 包扩展)

[4.4 emplace 系列接口](#4.4 emplace 系列接口)

[5 类的新功能](#5 类的新功能)

[5.1 默认移动构造与默认移动赋值](#5.1 默认移动构造与默认移动赋值)

[5.2 delete 与 default](#5.2 delete 与 default)

[5.3 其他新特性](#5.3 其他新特性)

[6 lambda 匿名函数](#6 lambda 匿名函数)

[6.1 lambda 语法格式](#6.1 lambda 语法格式)

[6.2 捕捉列表](#6.2 捕捉列表)

[6.3 lambda 的使用场景](#6.3 lambda 的使用场景)

[6.4 lambda 的原理](#6.4 lambda 的原理)

[7 包装器](#7 包装器)

[7.1 function](#7.1 function)

[7.2 bind](#7.2 bind)

[8 其他新特性](#8 其他新特性)

[8.1 STL 中的一些变化](#8.1 STL 中的一些变化)

[8.2 智能指针](#8.2 智能指针)

[8.3 线程库](#8.3 线程库)

总结


1 C++ 的发展历史

C++ 经历几个版本的迭代已经发展为了一门较为完整的面向对象变成语言了。从最开始的C++98,到后来的 C++11 大版本更新,因为一些历史的缘故,中间相隔了 13 年才迎来了第一个大版本更新。后面就是三年稳定更新一个版本,所以后面的新标准就包括 C++14、C++17、C++20、C++23、C++26,每次更新都会增加一些新特性,其中C++11、C++20、C++26 是 C++ 发展中比较重要的几个版本,添加的特性也很多,而 C++11 更是被称为现代 C++ 的起点,许多公司用的 C++ 标准也是 C++11,所以我们有必要学习一下 C++11 新增的各种特性。


2 列表初始化

2.1 任何对象都可列表初始化

在 C++98 中,只有数组和结构体可以使用 {} 来进行初始化,这点与 C 语言中相同:

cpp 复制代码
//C++98
#include <iostream>

using namespace std;

struct A
{
    int _a1;
    int _a2;
};

int main()
{
    int a[10] = {1, 2, 3, 4};
    A a = {1, 1};

    return 0;
}

但是在 C++11 中想提供一种统一初始化的方式,所以就将C++98 中的 {} 初始化功能上升到了所有对象,即一切皆可 {} 初始化,{} 初始化又成为列表初始化

不仅内置类型支持列表初始化,自定义类型也支持列表初始化。但是自定义类型列表初始化其实是隐式类型转换,就是利用 {} 去调用对象的构造函数,构造一个临时对象,再将临时对象拷贝构造到真正对象上,但是现在的编译器都会进行优化,将构造 + 拷贝构造优化为直接构造。另外列表初始化可以忽略掉赋值符号(=)

cpp 复制代码
#include <iostream>

using namespace std;

class A
{
public:
    A(int a1, int a2)
        :_a1(a1)
        ,_a2(a2)
    {
        cout << "A(int a1, int a2)" << endl;
    }

private:
    int _a1;
    int _a2;
};

int main()
{
    A a1 = {1, 2}; // 构造 + 拷贝构造 -> 直接构造

    A a2{2, 3}; // 可以忽略掉 =

    return 0;
}

这种统一的列表初始化的方式不管是在构造对象还是在函数传参时都很方便:

cpp 复制代码
#include <iostream>
#include <vector>

using namespace std;

class A
{
public:
    A(int a1, int a2)
        :_a1(a1)
        , _a2(a2)
    {
        cout << "A(int a1, int a2)" << endl;
    }

private:
    int _a1;
    int _a2;
};

int main()
{
    vector<A> v;
    //在C++98中只能构造有名对象,然后插入
    A a1(1, 2);
    v.push_back(a1);

    //C++11 中可以直接传入 {},进行隐式类型转换
    v.push_back({ 1, 3 });

    return 0;
}

2.2 C++11 中的 initializer_list

有了上面的列表初始化还不够,因为如果使用上面的列表初始化,那么各个容器的构造函数就需要写很多版本。因为 {} 中的元素个数是不确定的,而 {} 又是去调用容器的构造函数去完成初始化,所以我们就需要写很多版本的构造函数,而这些构造函数又是类似的,只是参数个数不同。为了解决这一问题,C++ 中引入了 initializer_list类。

initializer_list 本质就是在底层开辟一个数组,然后将 {} 的元素拷贝到数组内部,然后在 initializer_list 内部有一个开始指针 _start,还有一个元素大小字段 _size。所以 initializer_list 的 begin() 迭代器就是返回 _start, end() 就是返回 _start + _size。

cpp 复制代码
template<class T>
class initializer_list
{
public:
    initializer_list() {}

    size_t size() const
    {
        return _size;
    }

    const T* begin() const
    {
        return _start;
    }

    const T* end() const
    {
        return _start + _size;
    }

private:
    T* _start;
    size_t _size;

    //编译器会对 {} 特殊处理,{} 会自动调用这个私有构造
    initializer_list(const T* begin, size_t size)
        :_start(begin)
        ,_size(size)
    {}
};

所以只要有了 initializer_list 这个类,只要在所有的容器中添加一个用 initializer_list 初始化的构造函数,容器也可以使用 {} 进行初始化了。


3 右值引用与移动语义

C++ 98 中的引用都是左值引用,也就是之前的 '&' 都是左值引用,而在 C++11 及之后又添加了右值引用,也就是 '&&',但是不管是左值引用还是右值引用,本质上都是引用,都是给一个对象起别名。

3.1 左值与右值

左值就是我们平常见到过的那些值,通常是一个数据的表达式,比如变量名、指针等等;而右值就是那些通常是我们无法直接操作的那些变量或者表达式,比如字面量常量(1、2、3等等)、匿名对象、临时对象、1+1 等等。区分左值与右值最简单的办法就是看一个变量有没有办法取地址,能取地址就叫做左值,不能取地址的就叫做右值。需要注意的是,const 变量是左值,因为 const 变量虽然没法修改,但是可以取地址。

cpp 复制代码
int main()
{
	int a = 10; //变量名是左值
	int* p = &a; //指针变量是左值
	const int b = 10; //const 变量是左值
	string s("aaaaaa"); // s 变量是左值
	s[0] = 'x'; // s[0] 也是左值

	cout << "&a: " << hex << &a << endl;
	cout << "&p: " << hex << &p << endl;
	cout << "&b: " << hex << &b << endl;
	cout << "&s: " << hex << &s << endl;
	cout << "&s[0]: " << hex << &s[0] << endl;

	//以下的变量+-表达式、字面量常量、匿名对象都不能取地址
	cout << "&(a + b): " << hex << &(a + b) << endl;
	cout << "&10: " << hex << &10 << endl;
	cout << "&string(): " << hex << &string() << endl;

	return 0;
}

其实区分左值还是右值还有一个方法,那就是有名字、在内存中有固定位置、可以长期存在的值就成为左值,而没有名字、临时值、即将要销毁的值,那就是右值。所以左值又称为 loactor value,右值又称为 read value,不过传统上也将左值叫做 left value,右值叫做 right value。

其实在 C++ 中还进一步将右值区分为纯右值和将亡值:

其中 lvalue 就是左值,xvalue 为将亡值,prvalue 为纯右值。其中左值就是有名字、有内存、能取地址的变量;将亡值就是有名字、但是即将被销毁的值,比如 std::move(a),move 是将一个左值变为右值的函数;纯右值就是没有名字、即将销毁的值,比如匿名对象、临时对象都是纯右值。


3.2 左值引用与右值引用

我们之前学到过的引用,T& r = a,这个称之为左值引用;而如果我们使用 T&& rr = move(a),这个就称之为右值引用。其中 move 是 C++ 中提供的一个函数(其参数 T&& 并不是右值引用,而是万能引用,会在引用折叠中讲解),作用就是可以将一个左值变为右值:

左值引用不能引用右值,但是可以通过 const 左值引用来引用右值;右值引用不能引用左值,但是可以通过 move() 函数将左值变为右值进行引用。但是不管是左值引用还是右值引用,其本质还是给变量取别名,所以左值引用和右值引用都可以延长对象的生命周期

cpp 复制代码
#include <iostream>

using namespace std;

class A
{
public:
	A(int a = 10)
		:_a1(a)
	{
		cout << "A(int a = 10)" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}

private:
	int _a1;
};

int main()
{
	//左值引用不可以引用右值
	//A& r1 = A();
	const A& r2 = A();

	A a;
	//右值引用不能引用左值
	//A&& rr1 = a;
	//可以通过 move 来引用左值
	A&& rr2 = move(a);

	cout << "***********************************" << endl;
	//引用可以延长声明周期
	//匿名对象声明周期只在这一行
	A();
	const A& r3 = A();
	A&& rr3 = A();
	cout << "***********************************" << endl;
	//左值引用可以引用右值引用
	A& r4 = rr3;

	return 0;
}

但是需要注意的是右值引用的表达式,也就是变量名依然是左值,并不是右值,这一点是 C++ 故意设计的,目的就是为了后面的移动语义可以实现。

虽然左值引用右值引用在语法层上是给变量起别名,但是在底层其实都是使用的指针

所以说左值引用与右值引用其实在底层都是一样的,只是作用的对象类型不一样罢了。


3.3 左值引用与右值引用的参数匹配

之前我们在函数重载时说过,编译器对于重载的函数是会根根据传入的参数去匹配最适合的函数。之前对于右值,我们写的都是 const T& 版本的函数,因为没有右值引用,所以编译器只能去匹配 const T& 版本的函数;但是有了右值引用之后,编译器肯定是回去匹配最合适的函数,也就是右值引用参数的函数:

cpp 复制代码
#include <iostream>

using namespace std;

void Func(int& x)
{
	cout << "void Func(int& x)" << endl;
}

void Func(const int& x)
{
	cout << "void Func(const int& x)" << endl;
}

void Func(int&& x)
{
	cout << "void Func(int&& x)" << endl;
}

int main()
{
	int x1 = 1;
	const int x2 = 10;

	Func(x1);//匹配左值引用版本
	Func(x2);//匹配const左值引用版本
	//如果有右值引用,那就匹配右值引用版本;没有就匹配 const 左值引用
	Func(10);

	return 0;
}

如果我们将右值引用版本注释掉,其实 Func(10) 会去调用 const T& 版本:

所以说有了右值引用之后,编译器对于右值参数,就可以去匹配最合适的右值引用的函数版本了。


3.4 右值引用与移动语义

在这我们可能会思考一个问题,那就是有了左值引用,为什么还要有右值引用呢?在回答这个问题之前,我们先来回顾一下左值引用的使用场景。左值引用主要有以下三个作用或者使用场景:

(1)传参过程中减少拷贝(本质就是指针传参),提高效率

(2)传递引用参数,可以改变被引用对象,比如 swap 函数

(3)返回值返回引用,可以修改返回的被引用对象

虽然左值引用解决了大部分问题,但是传值返回的场景是左值引用解决不了的,而且必须传值返回,不能传引用返回:

cpp 复制代码
string Addstr(string num1, string num2)
{
    string ret;

    //完成 ret = num1 + num2;
    //...

    return ret;
}

int main()
{
    string num1 = "123456", num2 = "124567";
    string addret = Addstr(num1, num2);

    return 0;
}

上面这段代码是使用 addret 字符串来接收 Addstr 函数的返回值。在 Addstr 函数中,是不能够将返回值写为 string& 的,因为一旦 Addstr 函数调用结束,那么整个函数栈帧就销毁了,而 ret 又是在 Addstr 函数内部创建的函数,也就是一个局部对象,其会跟随 Addstr 的函数栈帧一起被销毁,所以如果返回 string&,返回的其实是已经被销毁的 ret 对象的引用,那么 addret 其实就是野引用了。所以这里只能传值返回。而这里传值返回时,需要先用 ret 去拷贝构造一个 string 的临时对象,再用临时对象去拷贝构造 addret(现在的编译器一般都会优化为一次拷贝构造):

左边是没有优化的场景,右边是优化后的场景。所以在编译器不优化的场景下,其实传值返回的消耗是很大的,因为要经历两次拷贝才能完成。但是你可能会有疑问,这里只是拷贝两次 string 啊,消耗能有多大?那么如果返回值是 vector<vector<int>>,或者 map<string, string> 呢?所以不管返回值是什么,传值返回的消耗始终还是比较大的。

既然传值返回消耗很大,我们来分析一下为什么消耗很大呢?就是因为一个函数内部的局部对象在函数调用结束之后会销毁,所以必须借用临时对象来完成拷贝。其实临时对象在这里只是起到一个中间人的作用,就是在 ret 和 addret 中间作为一个拷贝的 tmp 对象,当拷贝完成之后,临时对象也要销毁了,那么我们可不可以将临时对象的资源转移到 addret 内部,而减少一次拷贝呢?这就是移动语义


3.5 移动构造与移动赋值

所谓的移动语义,就是将右值的资源移动到要其他对象内部。这里的移动其实就是将右值对象和其他对象的资源进行互换。就拿我们之前实现的 string 来举例的话,其实实现移动语义,就是实现移动构造与移动赋值函数,使得参数是右值时,可以去去调用移动构造和移动赋值函数,而不去调用拷贝构造和拷贝赋值函数,从而减少了拷贝:

cpp 复制代码
#pragma once
#include<iostream>
#include<string.h>
#include<assert.h>

using namespace std;

//为了区别于库中的 string,这里选择使用命名空间进行分隔
namespace L
{
	class string
	{
		friend ostream& operator<<(ostream& out, const string& s);
		friend istream& operator>>(istream& in, string& s);
	public:
		//实现迭代器
		typedef char* iterator;
		typedef const char* const_iterator;

		//构造函数
		string(const char* str = "")
			:_size(strlen(str))
		{
			_str = new char[_size + 1];
			strcpy(_str, str);
			_capacity = _size;
		}

		//拷贝构造
		string(const string& s)
		{
			cout << "string(const string& s)" << endl;
			//先利用 s 中的 _str 拷贝构造一个 tmp 对象
			string tmp(s._str);
			//交换 tmp 与 this,tmp 变为 nullptr,this 会管理 tmp 的空间
			swap(tmp);
		}

		//移动构造
		string(string&& s)
		{
			cout << "string(string&& s)" << endl;
			swap(s);
		}

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

		//拷贝赋值
		string& operator=(const string& s)
		{
			cout << "operator=(string s)" << endl;
            
            if (this != &s)
            {
                _str = new char[s._capacity + 1];
                strcpy(_str, s._str);
                _size = s._size;
                _capacity = s._capacity;
            }

            return *this;
		}

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

		~string()
		{
			delete[] _str;
			_size = _capacity = 0;
		}

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		const_iterator begin() const
		{
			return _str;
		}

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

		size_t size()
		{
			return _size;
		}

		size_t capacity()
		{
			return _capacity;
		}

		size_t size() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _capacity;
		}

		void clear()
		{
			_str[0] = '\0';
			_size = 0;
		}

		//扩容
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				//strcpy(tmp, _str);
				for (size_t i = 0; i <= _size; i++)
				{
					tmp[i] = _str[i];
				}
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}

		//改变 size 
		void resize(size_t n, char ch = '\0')
		{
			if (n < _size)
			{
				_size = n;
				_str[_size] = '\0';
			}
			else if (n > _capacity)
			{
				//扩容
				int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
				reserve(newCapacity);

				//填充数据
				size_t start = _size;
				_size = n;
				while (start < _size)
					_str[start++] = ch;
				_str[_size] = '\0';
			}
			else
			{
				//只填充数据
				//填充数据
				size_t start = _size;
				_size = n;
				while (start < _size)
					_str[start++] = ch;
				_str[_size] = '\0';
			}
		}

		//尾插
		void push_back(char ch)
		{
			//满了先扩容 -- 这里采取两倍扩容
			if (_size == _capacity)
			{
				int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
				reserve(newCapacity);
			}

			_str[_size++] = ch;
			//不要忘记'\0'
			_str[_size] = '\0';
		}

		//追加
		void append(const char* str)
		{
			//按需扩容
			size_t len = strlen(str);
			if (_size + len > _capacity)
			{
				int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
				if (_size + len > newCapacity)
					newCapacity = _size + len + 1;
				reserve(newCapacity);
			}
			size_t end = _size;
			for (size_t i = 0; i <= len; i++)
			{
				_str[end] = str[i];
				++end;
			}
			_size += len;
		}

		//+=运算符重载
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		string& operator+=(const char* str)
		{
			append(str);
			return *this;
		}

		//插入与删除
		void insert(size_t pos, size_t n, char ch)
		{
			assert(pos <= _size);
			//先判断是否需要扩容
			if (_size + n > _capacity)
			{
				int newCapacity = _capacity == 0 ? 4 : 2 * _capacity;
				if (_size + n > newCapacity)
					newCapacity = _size + n;
				reserve(newCapacity);
			}

			//挪动数据
			size_t end = _size + n;
			while (end > pos + n - 1)
			{
				_str[end] = _str[end - n];
				--end;
			}

			//插入数据
			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = ch;
			}

			_size += n;
		}

		void insert(size_t pos, const char* str)
		{
			assert(pos <= _size);

			size_t n = strlen(str);
			//复用插入 n 个 ch 版本的 insert
			insert(pos, n, 'x');
			//再将这 n 个 'x' 改为 str 中的字符
			for (size_t i = 0; i < n; i++)
			{
				_str[pos + i] = str[i];
			}
		}

		void erase(size_t pos = 0, size_t n = npos)
		{
			assert(pos < _size);

			if (n == npos || n == _size - pos)
			{
				//将 pos 下标及之后的字符全都删去的情况
				_str[pos] = '\0';
				_size = pos;
			}
			else
			{
				size_t start = pos + n;
				while (start <= _size)
				{
					_str[start - n] = _str[start];
					++start;
				}

				_size -= n;
			}
		}

		//[]运算符重载
		char& operator[](size_t pos)
		{
			assert(pos < _size);

			return _str[pos];
		}

		const char& operator[](size_t pos) const
		{
			assert(pos < _size);

			return _str[pos];
		}

		//查找
		size_t find(char ch, size_t pos = 0)
		{
			for (size_t i = pos; i < _size; ++i)
			{
				if (_str[i] == ch)
					return i;
			}

			return npos;
		}

		size_t find(const char* str, size_t pos = 0)
		{
			//利用 strstr 函数
			//char* strstr (char * str1, const char * str2 )
			char* ptr = strstr(_str + pos, str);

			if (ptr == nullptr)
				return npos;

			return ptr - _str;
		}


		//生成子串
		string substr(size_t pos, size_t len = npos)
		{
			assert(pos < _size);

			//首先判断 len 个字符是否超过了剩下的字符个数
			size_t end = pos + len;
			if (len > _size - pos)
				end = _size;

			string s;
			//先扩容,避免因为增容而降低效率
			s.reserve(end - pos);
			for (size_t i = pos; i < end; i++)
			{
				s += _str[i];
			}

			return s;
		}


		//比较大小运算符重载
		bool operator==(const string& s) const
		{
			return strcmp(_str, s._str) == 0;
		}
		bool operator!=(const string& s) const
		{
			return !(*this == s);
		}
		bool operator>=(const string& s) const
		{
			return (*this > s) || (*this == s);
		}
		bool operator<=(const string& s) const
		{
			return !(*this > s);
		}
		bool operator>(const string& s) const
		{
			return strcmp(_str, s._str) > 0;
		}
		bool operator<(const string& s) const
		{
			return !(*this >= s);
		}


	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;

		const static size_t npos;
	};

	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto& ch : s)
		{
			cout << ch;
		}

		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		s.clear();

		//创建一个字符数组,避免因多次增容降低效率
		char buf[1024];
		//通过 get 函数获取一个输入的字符
		char ch = in.get();
		//用来记录 buf 数组中的字符个数
		size_t i = 0;
		while (ch != ' ' && ch != '\n')
		{
			buf[i] = ch;
			++i;
			if (i == 1023)
			{
				//将 buf 数组中的内容放到 s 中
				buf[i] = '\0';
				s += buf;
				i = 0;
			}

			ch = in.get();
		}

		//如果 buf 中还有数据,全都放到 s 中
		if (i > 0)
		{
			buf[i] = '\0';
			s += buf;
		}

		return in;
	}

	istream& getline(istream& in, string& s, char delim)
	{
		s.clear();

		//与 operator>> 实现逻辑相同
		char buf[1024];
		char ch = in.get();
		int i = 0;
		while (ch != delim)
		{
			buf[i] = ch;
			++i;
			if (i == 1023)
			{
				buf[i] = '\0';
				s += buf;
				i = 0;
			}

			ch = in.get();
		}

		if (i > 0)
		{
			buf[i] = '\0';
			s += buf;
		}

		return in;
	}

}
cpp 复制代码
//main.cc
#include "String.hpp"

L::string Addstr(L::string num1, L::string num2)
{
    L::string ret;

    //完成 ret = num1 + num2;
    //...
    cout << "***********************" << endl;
    return ret;
}

int main()
{
    L::string num1("123456"), num2("1234567");
    L::string addret1 = Addstr(num1, num2);
    cout << "**********************" << endl;
    L::string addret2;
    addret2 = Addstr(num1, num2);

    return 0;
}

上面模拟实现 string 类中的移动构造和移动赋值就是移动语义,本质就是通过 swap 函数将右值对象与 this 对象底层的 _str、_size、_capacity 互换,这样就完成了资源的移动,所以才叫做移动语义。这里也可以看出 C++ 为什么要将右值的变量表达式设计为左值,就是因为在移动语义中我们需要移动右值对象的资源,而如果将右值变量表达式设计为右值,那么右值对象本身就是不可变的,所以右值变量表达式必须设计为左值才可以

为了方便我们观察拷贝语义和移动语义的差别,这里我们使用 Linux 系统的 g++ 编译器来完成编译工作,因为 vs 编译器会进行代码的极致优化,将一些行为给优化掉。我们可以使用 -fno-elide-constructors来关闭构造相关的优化:

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

首先我们看一下没有移动语义的场景:

可以看到,在 Addstr 的返回过程中确实是调用了两次拷贝构造函数。而对于拷贝赋值来说,是先用返回值拷贝构造一个临时对象,然后使用临时对象拷贝赋值给 addret2:

在拷贝赋值中,编译器优化时会将 Addstr 函数内部的局部对象变为临时对象的引用,这样在构造局部对象时,实际上是在构造临时对象,这样就少了一次从局部对象到临时对象的拷贝了。

上面是没有移动语义的场景,那么添加了移动语义呢?请看如下的执行过程:

在将拷贝构造变为移动构造之后,由于临时对象是右值,所以这里会去调用其移动构造函数,而局部对象作为返回值,也会被编译器识别为是右值对象,因为局部对象会随函数栈帧一起销毁,不如将其变为右值直接转移资源,可以提高效率;编译器对于这种场景也会进行极致优化,实际上 ret 对象是 addret2 对象的引用,我们可以打印他们呢两个的地址看一下:

所以编译器会对局部对象作为返回值情况进行极致优化,省去拷贝和移动的消耗。值得注意的是,右边的优化场景并不仅仅是对于移动构造会优化,现在的编译器只要遇到局部对象作为返回值的情况,基本上就会采用这种优化来提高效率。

在赋值这里,原理是一样的,因为临时对象为右值,所以拷贝构造与拷贝赋值都会变为移动构造和移动赋值;优化场景下,是先用 ret 来构造临时对象,再用临时对象去移动赋值 addret2 对象。

移动构造和移动赋值,不管是在编译器优化还是不优化的场景下,都会比拷贝构造和拷贝赋值效率更高(当然前提必须是在参数为右值的场景下),因为移动语义只需要移动资源,也就是 swap 函数交换资源,但是拷贝语义还需要新开辟空间,然后将数据一个一个拷贝过去。所以说,右值引用和移动语义可以大大提高右值场景下的运行效率。

正是因为移动语义可以大幅提高效率,所以在 C++11 之后,各种容器都在相关接口中添加了右值引用版本:

以后如果传入右值,比如以前的隐式类型转换:

cpp 复制代码
#include <iostream>
#include <vector>

using namespace std;

int main()
{
    vector<pair<int, int>> v;
    v.push_back({ 1, 1 });
    return 0;
}

这里的 {1, 1} 会发生隐式类型转换,去构造一个 pair 的临时对象,然后调用 push_back 的右值引用版本,只需要转移资源,不需要拷贝了,大幅提高了效率。


3.6 引用折叠

我们在使用引用的时候,是不能直接给引用再叠加引用的:

cpp 复制代码
#include <iostream>

using namespace std;

int main()
{
	int&&& r = 10;

	return 0;
}

但是我们可以却通过 typedef 来给引用取别名,再在上面叠加引用:

cpp 复制代码
#include <iostream>

using namespace std;

typedef int&& irr;

int main()
{
	int a = 10;
	irr& ref = a;

	return 0;
}

那么这里的 ref 是左值引用还是右值引用呢?很显然,这里的 ref 引用了左值 a,所以这里的 ref 是左值引用。在 C++ 中,新出了一个引用折叠的特性,那就是:右值引用的右值引用折叠为为右值引用,其他引用组合皆为左值引用。按照这条规则,上面的 ref 是右值引用叠加左值引用,所以很显然会被折叠成左值引用。

但是引用折叠用在上面的场景不免显得有点鸡肋,而且代码的可读性还很差。其实引用折叠的真正用途是用在模板中以实现万能引用:

cpp 复制代码
#include <iostream>

using namespace std;

template<class T>
//此为万能引用,并不是右值引用
void Func(T&& a)
{
	T x = a;
	x = 1;
}

int main()
{
	int a1 = 10;
	//a1 为左值,T 被实例化为 int&
	Func(a1);
	//x 改变,a1 也被改变
	cout << a1 << endl;

	int a2 = 10;
	//move(a2) 为右值,T 被实例化为 int, 那么 x 改变,a2 不改变
	Func(move(a2));
	cout << a2 << endl;

	return 0;
}

如果一个函数模板参数写为 T&& a,那么这个参数称之为万能引用,并不是右值引用,因为这个函数模板会根据你传入的参数自动推导参数类型,如果你传入的是左值,比如上面的代码,那么 T 就会实例化为 int& 类型,那么通过引用折叠,参数就变成了 int& a,变为了左值引用,此时 x 就是 a 的别名,x 改变,a 也改变;如果传入的是右值,那么 T 还是会实例化为 int,那么参数就是 int&& a,就变为了右值引用,此时 x 就是 a 的拷贝,x 改变,a 不改变。

那么如果传入的参数带有 const,那么万能引用是会携带 const 关键字的,传入的是 const 左值,那么 T 就实例化为 const int&,引用折叠之后参数就变为 const int& a,此时 x 就是 const int& 类型,是不允许改变的;如果传入的是右值,那么 T 就实例化为 const int,引用折叠之后参数就变为 const int&& a,此时 x 就是 const int 类型,也是不允许改变的。

cpp 复制代码
#include <iostream>

using namespace std;

template<class T>
//此为万能引用,并不是右值引用
void Func(T&& a)
{
	T x = a;
	x = 1;
}

int main()
{
	const int a3 = 10;
	//a3 为 const int 左值,T被实话为 const int&,x 不可改变
	Func(a3);
	cout << a3 << endl;

	const int a4 = 10;
	//move(a4) 为 const int 右值,T 被实例化为 const int,x 不可改变
	Func(move(a4));
	cout << a4 << endl;

	return 0;
}

3.7 完美转发

我们先来看下面这段代码:

cpp 复制代码
#include <iostream>

using namespace std;

void F(int& x) { cout << "左值引用" << endl; }
void F(const int& x) { cout << "左值引用" << endl; }

void F(int&& x) { cout << "右值引用" << endl; }
void F(const int&& x) { cout << "右值引用" << endl; }

template<class T>
//此为万能引用,并不是右值引用
void Func(T&& a)
{
	F(a);
}

int main()
{
	int a1 = 10;
	Func(a1);
	Func(move(a1));

	const int a2 = 10;
	Func(a2);
	Func(move(a2));

	return 0;
}

在上面这段代码中,Func 是一个万能引用函数模板,我们本来的目的是想要如果 a 是左值,那就调用左值版本的 F 函数,如果是右值,那就调用右值版本的 F 函数,但是现在调用的却全部都是左值版本的 F 函数,这是为什么呢?

原因就是所有的变量表达式属性都是左值,即使是右值引用的变量表达式,所以这里就全部去调用左值引用版本的 F 函数了。如果我们想要保持变量表达式本来的属性,我们就需要完美转发。

cpp 复制代码
_EXPORT_STD template <class _Ty>
struct remove_reference 
{
    using type                 = _Ty;
    using _Const_thru_ref_type = const _Ty;
};


_EXPORT_STD template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;


_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept 
{
    return static_cast<_Ty&&>(_Arg);
}

_EXPORT_STD template <class _Ty>
_NODISCARD _MSVC_INTRINSIC constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept 
{
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
    return static_cast<_Ty&&>(_Arg);
}

上面的是源码,可能比较难看,简化一下就是下面的代码:

cpp 复制代码
template <class T>
constexpr T&& forward(T& _Arg) noexcept 
{
    return static_cast<T&&>(_Arg);
}

template <class T>
constexpr Ty&& forward(T&& _Arg) noexcept 
{
    static_assert(!is_lvalue_reference_v<T>, "bad forward call");
    return static_cast<T&&>(_Arg);
}

使用完美转发实现调用不同版本的 F 函数代码为:

cpp 复制代码
#include <iostream>

using namespace std;

void F(int& x) { cout << "左值引用" << endl; }
void F(const int& x) { cout << "左值引用" << endl; }

void F(int&& x) { cout << "右值引用" << endl; }
void F(const int&& x) { cout << "右值引用" << endl; }

template<class T>
//此为万能引用,并不是右值引用
void Func(T&& a)
{
	F(forward<T>(a));
}

int main()
{
	int a1 = 10;
	Func(a1);
	Func(move(a1));

	const int a2 = 10;
	Func(a2);
	Func(move(a2));

	return 0;
}

forward 中的 static_cast 就是一个实现强转的函数,会将参数强转为 <> 里面的类型。所以其实 forward 就是一个实现强转的函数。结合上面的场景,当传入 Func 函数的参数为左值时,T 会被实例化为 int&,那么 forward 的模板参数也会被实例化为 int&,那么第二个版本的 forward 就会因为 static_assert 而报错,第一个 forward 函数会实例化成功,变为:

cpp 复制代码
constexpr int& forward(int& _Arg) noexcept 
{
    return static_cast<int&>(_Arg);
}

所以 a 就被成功强转为了左值引用。

如果传入 Func 函数的参数为右值,那么 T 就会实例化为 int,那么 forward 就会实例化出两个版本:

cpp 复制代码
constexpr int&& forward(int& _Arg) noexcept 
{
    return static_cast<int&&>(_Arg);
}

constexpr int&& forward(int&& _Arg) noexcept 
{
    static_assert(!is_lvalue_reference_v<int>, "bad forward call");
    return static_cast<int&&>(_Arg);
}

所以 a 就会成功强转为右值引用。


4 可变参数模板

以前使用过的模板参数都只能固定接收一个类型,如果想要接收多个类型,就必须写多个模板参数,C++11 为了解决这个问题,就提出了可变参数模板的语法。

4.1 可变参数模板语法

可变参数模板本质上也是模板,所以可变参数模板也与普通模板一样分为两种:函数可变参数模板与类可变参数模板。其中可变模板参数称为参数包,参数包也分为两种:(1)模板参数包,表示 0 或多个模板参数(2)函数参数包,表示 0 或多个函数参数。

在可变参数模板中,我们使用省略号(...)来表示一个参数包。在模板参数参数中,使用 **class ...**或者 **typename ...**表示 0 或多个类型列表;在函数参数中,我们使用 类型名... 表示接下来的 0 或多个参数列表。也就是在模板参数中,省略号(...)是跟在 class 或者 typename 关键字后面,而在函数参数中,省略号(...)是跟在类型名后面。以下是可变参数模板的使用示例:

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

//模板参数中 ... 写在 class 或者 typename 后面
template<class ...Args>
class A
{
	A(Args... args)
	{}
};

template<class ...Args>
//函数参数中,... 写在类型名后面
void Func(Args... args)
{
	cout << sizeof...(args) << endl;
}

int main()
{
	Func();
	Func(1, 'x');
	Func(1, 'x', 2.1);
	Func(1, 'x', 2.1, string("123456"));

	return 0;
}

在 C++11 之后,添加了一个新的运算符 sizeof...,这个就是专门计算参数包中的参数个数的。

函数参数包和普通的函数参数一样,也可以叠加左值引用或者万能引用:

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

//模板参数中 ... 写在 class 或者 typename 后面
template<class ...Args>
class A
{
	A(Args... args)
	{}
};

template<class ...Args>
//函数参数中,... 写在类型名后面
void Func(Args... args)
{
	cout << sizeof...(args) << endl;
}

int main()
{
	Func();
	Func(1);
	Func(1, 'x');
	Func(1, 'x', 2.1);
	Func(1, 'x', 2.1, string("123456"));

	return 0;
}

4.2 可变参数模板原理

可变参数模板的原理其实与普通模板是类似的。普通模板是根据传入的参数类型来推演出一个具体的类或者函数;可变参数模板也一样,会根据传入的参数个数与类型来推演出真正的类或者函数:

所以可变参数模板是比普通模板更进一步的泛型,不仅实现了类型上的泛型,还实现了个数上的泛型,让编译器去做了更多重复的工作,就不用我们去做了,提升了我们开发的效率。

4.3 包扩展

在参数包中,我们除了能够计算参数包的元素个数,我们还可以对其进行访问,这时候就需要用到包扩展的新功能了。

我们可以采用两种方式来进行包扩展。第一种就是递归推导的方式进行包扩展:

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

void ShowList()
{
    cout << endl;
}

template<class T, class ...Args>
void ShowList(T x, Args... args)
{
    cout << x << " ";
    ShowList(args...);
}

template<class ...Args>
void Print(Args... args)
{
    //将参数包一个一个函数的向下传递
    //编译器会在编译阶段递归推导 ShowList 函数
    ShowList(args...);
}

int main()
{
    Print(1, 'x', string("123456"));

    return 0;
}

这种进行包扩展的方式就是将参数包一步一步向下传递,然后编译器会在编译阶段递归推导参数,进行参数包解析:

包扩展的第二种方式就是将参数包展开依次作为一个函数的形参,以此种方式进行包扩展:

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

template<class T>
const T& GetArgs(const T& x)
{
    cout << x << ' ';
    return x;
}

template<class... Args>
void Arguments(Args... args)
{}

template<class ...Args>
void Print(Args... args)
{
    //GetArgs 必须返回一个值,将 GetArgs 的返回值继续构成参数包传递给 Arguments
    Arguments(GetArgs(args)...);
}

int main()
{
    Print(1, 'x', string("123456"));

    return 0;
}

这种包扩展的方式其实就是在将参数包向下传递的过程中添加了一层函数处理层,将处理后的结果重新构成参数包继续向下传递,之所以打印结果是相反的,是因为函数传递参数时,是从右向左传递的。

4.4 emplace 系列接口

由于可变参数模板的产生,各个容器中又新增了 emplace 系列接口:

可以看到 emplace 系列接口都是用可变参数模板实现的,函数参数部分又是 Args&& 万能引用。需要注意的是各个容器中也添加了 push_back 的右值引用版本:

push_back 的参数 value_type&& 是右值引用,并不是万能引用,因为 value_type 是各个容器的模板参数,并不是 push_back 函数的模板参数,所以在类进行实例化后,这里的 value_type 就确定了。比如:vector<int> v,这里的 value_type 就是 int,所以就不是万能引用了。

emplace 系列接口一般都会比 push_back 系列接口更高效,这里我们使用我们当时模拟实现的 string 来进行举例(我在上面的 string 实现中为构造、拷贝/移动构造、拷贝/移动赋值重载添加了一句打印函数):

cpp 复制代码
#include <iostream>
#include <list>
#include "String.hpp"

using namespace std;

int main()
{
	list<L::string> ls;
	//传左值,emplace_back 与 push_back 相同,都是拷贝构造
	L::string s1 = "1111111";
	ls.push_back(s1);
	ls.emplace_back(s1);
	cout << "************************" << endl;

	//传右值,emplace_back 与 push_back 相同,走移动构造
	L::string s2 = "2222222";
	ls.push_back(move(s1));
	ls.emplace_back(move(s2));
	cout << "************************" << endl;

	//但是传递匿名对象,push_back 是构造 + 移动构造,emplace_back 是构造
	ls.push_back("3333333");
	ls.emplace_back("3333333");
	cout << "************************" << endl;

	return 0;
}

在第一个 "****************" 之前拷贝构造之后还有一次构造是因为之前实现 string 时在拷贝构造中调用了一次构造函数构造了 tmp 对象。可以看到有名的左值和右值对象,emplace 系列与 push 系列都是一样,要么拷贝构造,要么移动构造;但是对于直接传递参数,push 系列是构造 + 移动构造,emplace 系列是直接构造,所以 emplace 系列会比 push 系列更加高效,少了一次移动。下面这个图就可以看出来他们两个的区别:

可以看到 push_back 由于参数被推演为 L::string&& data,所以会先用 "33333333" 构造一个临时对象,然后再将临时对象向下传递,最终在 list_node 节点内进行移动构造;而 emplace_back 系列接口由于是函数模板,所以 Args 会被推演为 const char*&,参数就变为了 const char*&... args,然后将 args 参数包一步一步向下传递,知道传递到 list_node 节点处,节点直接使用 const char* 来进行构造。

所以以后更加推荐使用 emplace 系列接口,emplace 系列接口确实会比 push_back 系列接口更加高效。

list 添加 emplace 系列接口

cpp 复制代码
//list.h
#pragma once
#include <iostream>
#include <assert.h>

using namespace std;

namespace LTL
{
	//创建链表的节点
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T& data = T())
			:_data(data)
			, _next(nullptr)
			, _prev(nullptr)
		{}

		list_node(T&& data)
			:_data(move(data))
			, _next(nullptr)
			, _prev(nullptr)
		{}

		template<class... Args>
		list_node(Args&&... args)
			: _data(forward<Args>(args)...)
			, _next(nullptr)
			, _prev(nullptr)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct Iterator
	{
		typedef list_node<T> Node;
		typedef Iterator<T, Ref, Ptr> Self;
		
		//成员变量
		Node* _node;

		//构造函数
		Iterator(Node* node)
			:_node(node)
		{}

		Ref operator*()
		{
			return _node->_data;
		}

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

		//重载 ++ 运算符
		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Self operator++(int)
		{
			Node* tmp = _node;
			_node = _node->_next;

			return Iterator(tmp);
		}

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

		Self operator--(int)
		{
			Node* tmp = _node;
			_node = _node->_prev;

			return Iterator(tmp);
		}

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

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

	//链表
	template<class T>
	class list
	{
		typedef list_node<T> Node;
	public:
		typedef Iterator<T, T&, T*> iterator;
		typedef Iterator<T, const T&, const T*> const_iterator;
		
		//创建头节点的函数
		void empty_init()
		{
			//先创建一个哨兵位
			_head = new Node;
			//需要让自己循环起来
			_head->_next = _head->_prev = _head;

			_size = 0;
		}

		//创建迭代器
		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);
		}

		//无参默认构造
		list()
		{
			empty_init();
		}

		//n个 val 构造
		list(size_t n, const T& val)
		{
			empty_init();
			for (size_t i = 0; i < n; i++)
				push_back(val);
		}

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


		//initializer_list 构造
		list(const initializer_list<T>& il)
		{
			empty_init();
			for (auto& x : il)
				push_back(x);
		}


		//迭代器区间构造
		template<class InputIterator>
		list(InputIterator first, InputIterator last)
		{
			empty_init();

			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		//拷贝构造
		list(const list<T>& ls)
		{
			empty_init();

			for (auto& x : ls)
				push_back(x);
		}

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

		//赋值运算符重载
		list<T>& operator=(list<T> ls)
		{
			swap(ls);

			return *this;
		}

		//析构函数
		~list()
		{
			Node* pcur = _head->_next;
			while (pcur != _head)
			{
				Node* next = pcur->_next;
				delete pcur;
				pcur = next;
			}

			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			Node* pcur = _head->_next;
			while (pcur != _head)
			{
				Node* next = pcur->_next;
				Node* prev = pcur->_prev;
				prev->_next = next;
				next->_prev = prev;
				delete pcur;
				pcur = next;
			}

			_size = 0;
		}

		size_t size() const
		{
			return _size;
		}

		bool empty() const
		{
			return _head->_next == _head;
		}

		T& back()
		{
			return *(--end());
		}

		T& front()
		{
			return *(begin());
		}

		const T& back() const
		{
			return *(--end());
		}

		const T& front() const
		{
			return *(begin());
		}

		//尾插
		void push_back(const T& data)
		{
			//复用 insert
			insert(end(), data);
		}

		void push_back(T&& data)
		{
			insert(end(), move(data));
		}

		//头插
		void push_front(const T& data)
		{
			insert(iterator(_head->_next), data);
		}

		//尾删
		void pop_back()
		{
			assert(_head->_next != _head);

			//复用 erase
			erase(iterator(_head->_prev));
		}

		//头删
		void pop_front()
		{
			assert(_head->_next != _head);

			erase(iterator(_head->_next));
		}

		template<class... Args>
		void emplace_back(Args&&... args)
		{
            //参数包继续向下传
			emplace(end(), forward<Args>(args)...);
		}

		template<class... Args>
		void emplace(iterator pos, Args&&... args)
		{
			//继续将参数包向下传
			Node* newnode = new Node(forward<Args>(args)...);
			Node* node = pos._node;

			//改变指针指向
			Node* prev = node->_prev;
			//prev newnode node
			newnode->_next = node;
			newnode->_prev = prev;
			prev->_next = newnode;
			node->_prev = newnode;

			//不要忘记让 _size++
			++_size;
		}

		void insert(iterator pos, const T& val)
		{
			//先创建一个节点
			Node* newnode = new Node(val);
			Node* node = pos._node;

			//改变指针指向
			Node* prev = node->_prev;
			//prev newnode node
			newnode->_next = node;
			newnode->_prev = prev;
			prev->_next = newnode;
			node->_prev = newnode;

			//不要忘记让 _size++
			++_size;
		}

		void insert(iterator pos, T&& val)
		{
			//先创建一个节点
			Node* newnode = new Node(move(val));
			Node* node = pos._node;

			//改变指针指向
			Node* prev = node->_prev;
			//prev newnode node
			newnode->_next = node;
			newnode->_prev = prev;
			prev->_next = newnode;
			node->_prev = newnode;

			//不要忘记让 _size++
			++_size;
		}

		iterator erase(iterator pos)
		{
			assert(_head->_next != _head);
			Node* del = pos._node;

			//改变指针指向
			Node* prev = del->_prev;
			Node* next = del->_next;

			//prev del next
			prev->_next = next;
			next->_prev = prev;

			delete del;

			//最后不要忘记让 _size--
			--_size;

			return iterator(next);
		}

	private:
		Node* _head = nullptr;
		size_t _size;
	};
}

//main.cc
#include <iostream>
#include <list>
#include "String.hpp"
#include "list.h"

using namespace std;

int main()
{
	LTL::list<L::string> ls;
	//传左值,emplace_back 与 push_back 相同,都是拷贝构造
	L::string s1 = "1111111";
	ls.push_back(s1);
	ls.emplace_back(s1);
	cout << "************************" << endl;

	//传右值,emplace_back 与 push_back 相同,走移动构造
	L::string s2 = "2222222";
	ls.push_back(move(s1));
	ls.emplace_back(move(s2));
	cout << "************************" << endl;

	//但是传递匿名对象,push_back 是构造 + 移动构造,emplace_back 是构造
	ls.push_back("3333333");
	ls.emplace_back("3333333");
	cout << "************************" << endl;

	return 0;
}

最开始比库中的 list 多了构造 + 拷贝构造 + 构造是因为我们自己实现的 list_node 默认构造为:

cpp 复制代码
list_node(const T& data = T())
	:_data(data)
	, _next(nullptr)
	, _prev(nullptr)
{}

其中 T() 会调用一下 string 的默认构造,_data(data) 会进行 string 的拷贝构造,而拷贝构造中又会调用构造,所以就多了两次构造 + 一次拷贝构造,别的与库中的行为都是相同的。在实现 emplace 系列函数时,一定要注意,因为右值变量表达式都是左值,所以向下传递参数包时,需要完美转发,要不然最后都会调到左值版本。


5 类的新功能

在 C++ 新增了许多新特性之后,类也与之匹配的新增很多新功能,下面我们就来看一下。

5.1 默认移动构造与默认移动赋值

在 C++11 新增了右值引用与移动语义之后,由于移动构造与移动赋值可以大幅提高效率,尤其是在传值返回场景中,所以 C++ 就在前六大默认成员函数的基础上新增了两个默认成员函数:移动构造与移动赋值运算符重载函数

对于六大默认成员函数,显示实现与编译器默认生成是一一对应的,也就是如果没有显示写出构造函数,那么编译器就会实现构造函数。但是对于移动构造和移动赋值重载函数比较苛刻。对于移动构造函数来说,如果你没有显示实现移动构造函数,而且还没有实现析构、拷贝构造与拷贝赋值重载中的任意一个,那么编译器就会自动生成一个移动构造函数。默认的移动构造函数****对于内置类型成员会采用逐字节浅拷贝,对于自定义类型成员,如果实现了移动构造就调用移动构造,没有实现就调用拷贝构造

移动赋值重载函数与移动构造函数类似,如果你没有实现移动赋值重载,而且没有实现析构、拷贝构造、拷贝赋值重载函数的任意一个,编译器才会自动生成一个移动赋值重载函数。默认的移动赋值重载函数对于内置类型成员会采用逐字节浅拷贝,对于自定义类型成员,如果实现了移动赋值重载函数,就调用移动赋值重载函数,没实现就去调用拷贝赋值重载函数

但是还有一个语法,如果你显示写出了移动构造和移动赋值,那么编译器就不会默认生成拷贝构造和拷贝赋值了,这一点要注意。

cpp 复制代码
#include <iostream>
#include <utility>
using namespace std;

class A
{
public:
    A()
    {
        cout << "A()" << endl;
    }

    A(const A& a)
    {
        cout << "A(const A& a) 拷贝构造" << endl;
    }

    A& operator=(const A& a)
    {
        cout << "A& operator=(const A& a) 拷贝赋值" << endl;

        return *this;
    }

    A(A&& a)
    {
        cout << "A(A&& a) 移动构造" << endl;
    }

    A& operator=(A&& a)
    {
        cout << "A& operator=(A&& a) 移动赋值" << endl;

        return *this;
    }
};

// B 没有写析构、拷贝构造、拷贝赋值、移动构造、移动赋值
// 所以编译器会自动生成移动构造和移动赋值
class B
{
public:
    int x;
    A a;
};

// C 显式写了移动构造和移动赋值
// 所以编译器会删除默认拷贝构造和默认拷贝赋值
class C
{
public:
    int x;

    C(int x = 0)
        : x(x)
    {}

    C(C&& c)
    {
        cout << "C(C&&) 移动构造" << endl;
        x = c.x;
        c.x = 0;
    }

    C& operator=(C&& c)
    {
        cout << "C& operator=(C&&) 移动赋值" << endl;

        if (this != &c)
        {
            x = c.x;
            c.x = 0;
        }

        return *this;
    }
};

int main()
{
    cout << "----- 默认移动构造和移动赋值 -----" << endl;

    B b1;

    cout << "移动构造 b2:" << endl;
    B b2(std::move(b1));   // 调用 B 的默认移动构造,内部调用 A 的移动构造

    cout << "移动赋值 b3:" << endl;
    B b3;
    b3 = std::move(b2);    // 调用 B 的默认移动赋值,内部调用 A 的移动赋值


    cout << endl;
    cout << "----- 显式写移动后,拷贝被删除 -----" << endl;

    C c1(10);

    C c2(std::move(c1));   // 可以,调用移动构造

    C c3(20);
    c3 = std::move(c2);    // 可以,调用移动赋值

    // 下面两句会报错,因为 C 写了移动构造/移动赋值后,
    // 编译器会删除拷贝构造和拷贝赋值

    // C c4(c3);           // error:拷贝构造被删除
    // c1 = c3;            // error:拷贝赋值被删除

    return 0;
}

5.2 delete 与 default

C++11 之后为了让我们更好的控制默认成员函数,新增了 deletedefault 关键字。如果想要限制一个默认成员函数的生成,我们可以直接在函数声明后面加上 = delete,那么编译器就不会生成对应的成员函数了,我们称这个函数为删除函数;但是如果想要编译器默认生成一个成员函数,我们可以在函数声明后加上 = default。比如:写了拷贝构造之后我们还想要编译器默认生成移动构造,就可以在移动构造函数声明后面加上 = default,那么编译器就会默认生成移动构造了。

cpp 复制代码
#include <iostream>
using namespace std;

class Person
{
public:
    Person() = default;   // 让编译器生成默认构造函数

    Person(int age)
        : _age(age)
    {}

    // 让编译器生成默认拷贝构造
    Person(const Person& p) = default;

    // 禁止拷贝赋值
    Person& operator=(const Person& p) = delete;

    void Print() const
    {
        cout << "_age = " << _age << endl;
    }

private:
    int _age = 0;
};

int main()
{
    Person p1;        // 调用默认构造
    p1.Print();

    Person p2(18);    // 调用带参构造
    p2.Print();

    Person p3(p2);    // 可以,拷贝构造被 default
    p3.Print();

    // p1 = p2;       // error:拷贝赋值被 delete,禁止赋值

    return 0;
}

5.3 其他新特性

C++11 中其他跟类相关的新特性我们之前都讲解过,一个是成员变量在声明时给缺省值;另一个是 overridefinal 关键字,override 关键字用来让编译器检查是否完成了虚函数重写,final 关键字用来表明一个类是最终类,不可被继承或者表示一个虚函数不可被重写。由于之前讲解过,这里就不过多赘述了。


6 lambda 匿名函数

C++11 之后,新增了一个 lambda 匿名函数的新语法。lambda 本质上是一个函数对象。既然lambda 叫做匿名函数,所以 lambda 在语法层上是没有具体类型的,我们一般都是使用 auto 关键字来接收这个匿名函数对象。

6.1 lambda 语法格式

lambda 语法格式为:

cpp 复制代码
[capture-list](arguments-list)->return-type { function-body };

capture-list 为捕捉列表,(arguments-list) 为参数列表,return-type 为返回值类型,function-body 为函数体,除了捕捉列表之外,其余的与我们平常写的函数一模一样。该 lambda 表达是会返回一个具体对象,我们只要使用 auto 关键字定义变量接收即可。比如我们定义一个简单的加法函数:

cpp 复制代码
#include <iostream>

using namespace std;

int main()
{
	//定义 lambda 匿名函数对象
	auto Add = [](int x, int y)->int { return x + y; };

	cout << Add(1, 2) << endl;
	int x = 10, y = 20;
	cout << Add(x, y) << endl;

	return 0;
}

而在 lambda 中,如果该函数没有参数,可以将 () 省略;对于返回值也是可以省略的,省略之后编译器会自动推导返回类型;捕捉列表和函数体是不可以省略的。比如实现一个普通的打印函数:

cpp 复制代码
#include <iostream>

using namespace std;

int main()
{
	//定义仿函数对象
	auto Print = [] { cout << "hello world" << endl; };

	Print();

	return 0;
}

6.2 捕捉列表

在 lambda 中,我们默认只能使用 lambda 参数列表和函数体中的变量,如果想要使用 lambda 之外的变量,就要在捕捉列表中对变量进行捕捉,可以捕捉多个变量,用 ',' 隔开。捕捉的方式有三种:

(1)显示捕捉。显示捕捉分为显示值捕捉和显示引用捕捉。比如:a, b, \&c,a,b 就是值捕捉,c 就是引用捕捉。

(2)隐式捕捉。隐式捕捉分为隐式值捕捉与隐式引用捕捉。使用 = 就代表对全部变量采用值捕捉;使用 \& 就代表对全部变量进行引用捕捉。

(3) 混合捕捉。混合捕捉是指隐式捕捉 + 显示捕捉。比如 =, \&a, \&b 就是指对 a,b 采用引用捕捉,其他变量采用值捕捉;\&, a, b 指对 a,b 进行值捕捉,其余变量采用引用捕捉。注意在混合捕捉中,=/& 必须在其他变量前面,而且 = 之后必须是引用捕捉,& 之后必须是值捕捉。

需要注意的是,上面的三种捕捉方式值捕捉默认都携带 const 属性,不可改变;引用捕捉 lambda 内部改变,会影响外部。要是想改变值捕捉的 const 属性,可以在参数列表后面添加 mutable,但是内部改变依然是改变形参,不会影响外部。另外,静态变量和全部变量不能再捕捉列表中进行捕捉,因为在内部可以直接使用。

cpp 复制代码
#include <iostream>

using namespace std;

int m = 20;

int main()
{
	int a = 1, b = 2, c = 3;
	//显示捕捉
	auto Func1 = [a, b, &c] {
		//++a; //值捕捉不可改变
		++c; //引用捕捉可以改变
		cout << a << ' ' << b << ' ' << c << ' ' << endl;
	};

	//隐式捕捉 -- 值捕捉
	auto Func2 = [=] {
		cout << a << ' ' << b << ' ' << c << endl;
	};

	//隐式捕捉 -- 引用捕捉
	auto Func3 = [&] {
		++a, ++b, ++c;
		cout << a << ' ' << b << ' ' << c << endl;
	};

	//混合捕捉
	auto Func4 = [=, &a] {
		++a;
		cout << a << ' ' << b << ' ' << c << endl;
	};

	auto Func5 = [&, a] {
		++b, ++c;
		cout << a << ' ' << b << ' ' << c << endl;
	};

	static int z = 10;
	//不可捕捉全局和静态变量
	/*auto Func6 = [&m, &z] {
		cout << m << ' ' << z << endl;
	};*/

	auto Func6 = [] {
		cout << m << ' ' << z << endl;
	};

	Func1();
	cout << c << endl;
	Func2();
	Func3();
	Func4();
	Func5();
	Func6();

	return 0;
}

6.3 lambda 的使用场景

除了 lambda,我们之前接触过的可调用对象有两个:函数指针与仿函数。对于一些轻量化场景,函数指针类型很复杂,仿函数又需要定义一个类,写起来很麻烦。所以这时候我们就可以使用lambda 匿名函数。比如实现 sort 的比较逻辑函数:

cpp 复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;

// 1. 函数指针
bool Greater(int a, int b)
{
    return a > b;
}

// 2. 仿函数
class CompareByDistance
{
private:
    int target;
public:
    CompareByDistance(int x) : target(x) {}
    bool operator()(int a, int b)
    {
        return abs(a - target) < abs(b - target);
    }
};

int main()
{
    vector<int> v1 = {3, 1, 5, 2, 4};
    vector<int> v2 = v1; // 复制数组
    vector<int> v3 = v1; // 复制数组

    //使用函数指针排序
    sort(v1.begin(), v1.end(), Greater);
    cout << "函数指针排序结果: ";
    for (auto e : v1) cout << e << " ";
    cout << endl;

    //使用仿函数排序
    sort(v2.begin(), v2.end(), CompareByDistance(3)); // 按距离 3 排序
    cout << "仿函数排序结果(按距离3): ";
    for (auto e : v2) cout << e << " ";
    cout << endl;

    //使用 lambda 排序
    int target = 3;
    sort(v3.begin(), v3.end(), [target](int a, int b) {
        return abs(a - target) < abs(b - target);
    });
    cout << "Lambda排序结果(按距离3): ";
    for (auto e : v3) cout << e << " ";
    cout << endl;

    return 0;
}

上面的代码是分别使用函数指针、仿函数、lambda 实现了 sort 的比较逻辑函数,可以看到函数指针与仿函数还需要实现独立的函数和类,lambda 直接写就可以了,更加轻量化,所以以后这种轻量化场景更推荐大家使用 lambda 匿名函数。

6.4 lambda 的原理

lambda 的原理其实很简单,就是替换为仿函数。捕捉列表中的变量就是仿函数的成员变量,() 就是 operator() 里面的参数,->return-type 就是 operator() 的返回值,{} 就是 operator() 的函数体。我们可以实现一个加法函数的 lambda 匿名函数看一下:

cpp 复制代码
#include <iostream>

using namespace std;

int main()
{
	auto add = [](int x, int y)->int { return x + y; };
	add(1, 2);

	return 0;
}

可以看到匿名函数其实汇编也是去调用了 operator() 函数,所以说明 lambda 底层还是变成了仿函数。


7 包装器

7.1 function

在 C++11 中提供了可以封装所有可调用对象的包装器,也就是 function

使用之前需要先包含头文件 functional。function 可以包装一切可调用对象,包括函数指针、仿函数、lambda 匿名函数,被 function 包装的可调用对象称为 function 的目标,如果 function 不含任何目标,则称该 function 为空,空 function 不可被调用,调用则会抛出 std::bad_function_call 异常。

function 的用法为 function<Ret (Args...)>,比如要包装返回值为 int,参数为 int, int 的可调用对象就可以写为:

cpp 复制代码
function<int (int, int)>;

该表达式会返回一个具体类型,所以一般我们都会使用 typedef 或者 using 来重命名该类型:

cpp 复制代码
//typedef function<int (int, int)> func_t;
using func_t = function<int (int, int)>;

之后就可以利用该类型来包装所有的该类型的可调用对象了,比如包装加法功能的可调用对象:

cpp 复制代码
#include <iostream>
#include <functional>

using namespace std;

using func_t = function<int(int, int)>;

//函数指针
int Add(int x, int y)
{
	return x + y;
}

//仿函数
struct Plus
{
	int operator()(int x, int y)
	{
		return x + y;
	}
};

int main()
{
	//lambda
	auto add = [](int x, int y)->int { return x + y; };
	
	//使用 function 统一包装
	func_t f1 = Add;
	func_t f2 = Plus();
	func_t f3 = add;

	cout << f1(1, 2) << endl;
	cout << f2(1, 2) << endl;
	cout << f3(1, 2) << endl;

	return 0;
}

function 的优势就是将所有的可调用对象包装为统一类型,这样上层不管传入什么都可以调用了,有了包装器,就可以大幅增加代码之间的解耦。

7.2 bind

bind 是一个函数模板,同时也是一个包装器,他可以为可调用对象绑定参数后重新返回一个可调用对象:

其使用的一般形式是:

cpp 复制代码
auto newCallback = std::bind(callback, argument_list)

callback 是一个可调用对象,argument_list 是一个用逗号分割的参数列表,bind 会将 argument_list 的参数一个一个传给 callback,然后返回一个新的可调用对象 newCallback。

在 argument_list 中我们可以使用 _n 来指定返回的新的可调用对象的参数位置。_n 本质上是一个占位符,n 可以是 1、2、3、4....,_1 就代表新返回的可调用对象的第一个参数,_2 就代表第二个参数,这些占位符都被封到了 placeholders 的命名空间中,我们可以使用这些占位符实现改变参数顺序和改变参数个数的效果:

cpp 复制代码
#include <iostream>
#include <functional>

using namespace std;

using placeholders::_1;
using placeholders::_2;

using func_t = function<int(int, int)>;

//函数指针
int Sub(int x, int y)
{
	return x - y;
}

//仿函数
struct Minus
{
	int operator()(int x, int y)
	{
		return x - y;
	}
};

int main()
{
	//lambda
	auto sub = [](int x, int y)->int { return x - y; };
	
	//bind 改变参数位置
	auto cb1 = bind(Sub, _1, _2);
	auto cb2 = bind(Sub, _2, _1);
	cout << cb1(10, 5) << endl; //5
	cout << cb2(10, 5) << endl; //-5

	//bind 改变参数个数
	auto cb3 = bind(Minus(), 10, _1); //5
	auto cb4 = bind(Minus(), _1, 10); //-5
	cout << cb3(5) << endl;
	cout << cb4(5) << endl;

	return 0;
}

在 bind 中的 _1、_2 就代表你后续调用时的参数位置,比如 cb2(10, 5),_1 就代表 cb2 中的第一个参数,也就是 10;_2 就代表 cb2 的第二个参数,也就是 5。但是向上传递给 Sub 时,还是按照后面的参数顺序传递的,也就是 _2 传递给 Sub 的第一个参数,_1 传递给 Sub 的第二个参数,借此来达到改变参数顺序的目的。但是最常用的其实是改变参数个数,而不是改变参数顺序。

bind 有一个最常用的场景,那就是为类的成员函数绑定 this 指针参数,这样就不用每次都传递 this 指针了:

cpp 复制代码
#include <iostream>
#include <functional>

using namespace std;

using placeholders::_1;
using placeholders::_2;

class Plus
{
public:
	int Add(int x, int y)
	{
		return x + y;
	}
};

using func_t = function<int(Plus*, int, int)>;
using callback_t = function<int(int, int)>;

int main()
{
	func_t f1 = &Plus::Add;

	//bind 为成员函数绑定 this
	Plus ps;
	callback_t f2 = bind(f1, &ps, _1, _2);

	cout << f2(1, 2) << endl; //3

	return 0;
}

下面是一个使用 function 和 bind 包装器实现计算器的例子:

cpp 复制代码
#include <iostream>
#include <functional>
#include <map>
#include <string>
using namespace std;

class Calculator
{
public:
    void Add(int a, int b)
    {
        cout << "Add: " << a << " + " << b << " = " << a + b << endl;
    }

    void Sub(int a, int b)
    {
        cout << "Sub: " << a << " - " << b << " = " << a - b << endl;
    }

    void Mul(int a, int b)
    {
        cout << "Mul: " << a << " * " << b << " = " << a * b << endl;
    }
};

void PrintHello()
{
    cout << "Hello, this is a normal function!" << endl;
}

int main()
{
    Calculator cal;

    // function<void()> 表示保存一个:无参数、无返回值的可调用对象
    map<int, function<void()>> menu;

    // 1. 绑定普通函数
    menu[1] = PrintHello;

    // 2. 使用 bind 绑定成员函数,并提前固定参数
    menu[2] = bind(&Calculator::Add, &cal, 10, 20);

    // 3. 使用 bind 绑定成员函数,并提前固定参数
    menu[3] = bind(&Calculator::Sub, &cal, 30, 15);

    // 4. 使用 lambda
    menu[4] = [&cal]() {
        cal.Mul(6, 7);
        };

    int choice = 0;

    cout << "------ Menu ------" << endl;
    cout << "1. Print Hello" << endl;
    cout << "2. Add 10 and 20" << endl;
    cout << "3. Sub 30 and 15" << endl;
    cout << "4. Mul 6 and 7" << endl;
    cout << "Please choose: ";

    cin >> choice;

    if (menu.find(choice) != menu.end())
    {
        menu[choice]();
    }
    else
    {
        cout << "Invalid choice!" << endl;
    }

    return 0;
}

8 其他新特性

8.1 STL 中的一些变化

C++ 11 之后 STL 中也增加了一些新容器:

array 是定长数组,其实就是 C 语言中的 T arrN,但是会比普通的数组多出一些 STL 的统一接口;forward_list 就是单链表,比起 list 来说,其只能前向遍历,不能反向遍历;unordered_map 和 unordered_set 就是哈希表,之前已经了解过了,这里就不再赘述了。

除了一些新容器,各个容器中也添加了一些新接口,比如右值引用版本的 push_back、emplace 系列函数、initializer_list 版本的构造函数,添加了这些新接口之后,容器变得效率更高而且更方便使用了。

另外,范围 for 也是 C++11 及之后提供的,底层就是替换成迭代器,这里就不再赘述了。

8.2 智能指针

C++11 之后为了更好的管理内存,提供了智能指针:

智能指针对于管理内存来说很重要,但是内容较多,我们后面会进行讲解。

8.3 线程库

C++11 中还增加了各种线程控制库,包括条件变量、互斥锁、线程库:

这里可以大家自行学习,这里就不再赘述了。

总结

C++11 是 C++ 发展过程中一次非常重要的标准更新,它在语法、性能和工程开发能力上都进行了大量增强。语法方面,C++11 引入了auto、范围 for、nullptr、列表初始化、decltype、lambda 表达式等特性,使代码更加简洁、安全和现代化。性能方面,C++11 引入了右值引用、移动语义和完美转发,配合移动构造函数与移动赋值运算符,可以减少不必要的深拷贝,提高程序运行效率。类和对象方面,C++11 增加了 delete、default 关键字、override、final 等语法,使类的设计更加清晰、可控。标准库方面,C++11 新增了 array、forward_list、unordered_map、unordered_set、智能指针、function、bind 等工具,增强了容器、回调和资源管理能力。此外,C++11 还正式支持多线程编程,提供了 thread、mutex、automic、condition_variable 等库组件。总体来说,C++11 让 C++ 从传统面向对象语言进一步发展为兼具高性能、泛型编程、函数式编程和现代工程能力的语言,是现代 C++ 的重要起点。

相关推荐
方也_arkling1 小时前
【Java-Day15】API篇-ArrayList集合
java·开发语言
我是一颗柠檬1 小时前
【Java后端技术亮点】动态路由权限(按钮级权限),细粒度控制到按钮级别
java·开发语言·后端·状态模式
Fanfanaas1 小时前
C++ 继承
java·开发语言·jvm·c++·学习·算法
在繁华处1 小时前
Java从零到熟练(十一):Spring框架入门
java·开发语言·spring
十五年专注C++开发1 小时前
cereal 库:C++ 序列化的轻量之选
开发语言·c++·序列化·反序列化·cereal
lqqjuly2 小时前
设计模式:理论、架构与 C++ 实现—SOLID原则到23 种经典模式
c++·设计模式·架构
BestOrNothing_20152 小时前
C++零基础到工程实战(5.2.8)多文件声明定义函数和全局变量
c++·c++多文件编译·.h头文件·.cpp·函数声明定义
星卯教育tony2 小时前
2026年全国青少年信息素养大赛主题应用 数字守艺人 丝路新城 星火征程 智传民韵 c++ python scratch 所有真题免费分享
开发语言·c++
z落落2 小时前
C# 继承:父子构造函数 + base 关键字 +五大访问修饰符(同项目+跨项目 全覆盖)
开发语言·c#