目录
[1. { } 初始化](#1. { } 初始化)
[2. std::initializer_list](#2. std::initializer_list)
[1. auto](#1. auto)
[2. decltype](#2. decltype)
[3. nullptr](#3. nullptr)
[1. 左值引用和右值引用](#1. 左值引用和右值引用)
[2. 移动语义(移动构造和移动赋值)](#2. 移动语义(移动构造和移动赋值))
[3. move的作用](#3. move的作用)
[4. 应用场景](#4. 应用场景)
[5. 万能引用](#5. 万能引用)
[6. 完美转发](#6. 完美转发)
[1. 默认成员函数](#1. 默认成员函数)
[2. default、delete关键字](#2. default、delete关键字)
[1. 概念](#1. 概念)
[2. STL中的emplace系列](#2. STL中的emplace系列)
一、列表初始化
1. { } 初始化
C++98 中的 { } 只能用于数组 和C 风格结构体 ;而 C++11 引入了统一初始化,几乎可以用在任何地方,包括普通变量、类对象和 STL 容器。
在C++98中,花括号 { } 被称为聚合初始化 ,有许多限制,只能用于数组,简单的 C 风格结构体 (不能有构造函数、不能有 private 成员、不能有虚函数),STL 容器无法使用,如下所示:
cpp
// 数组
int arr1[5] = { 1, 2, 3, 4, 5 }; // 合法:定义数组并初始化
int arr2[] = { 1, 2, 3 }; // 合法:自动推断大小
int arr3[10] = { 0 }; // 合法:将第一个元素设为0,其余自动初始化为0
// int arr[3] {1, 2, 3}; // Error: C++98 不允许省略等号
// C风格结构体(即:不能有构造函数、不能有 private 成员、不能有虚函数)
struct Point { int x, y; };
Point p = { 1, 2 }; // OK
// 容器
// std::vector<int> vec = {1, 2, 3}; // Error: C++98 没有 initializer_list
std::vector<int> vec;
vec.push_back(1); // 只能这样繁琐地添加
vec.push_back(2);
而,在C++11中,就将 { } 提升为统一初始化 ,扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。简单来说就是在C++11中一切都可以通过{ } 进行初始化。而C++11中的变化可以体现在一些几个方面:
基本数据类型也可以使用 { } ,并且可以省略 = 。
cpp// C++11 int a{ 10 }; // 替代 int a = 10; double d{ 3.14 }; // 替代 double d = 3.14; // 数组初始化(等号可省略) int arr[]{ 1, 2, 3, 4, 5 }; // 自动推断大小为 5 // 动态数组初始化 int* pa = new int[3] {1, 2, 3}; // C++11 允许 new 表达式使用 {}
结构体与类对象
没有构造函数的简单结构体(聚合体)
C++11 允许直接按成员声明顺序赋值,无需写构造函数
cppstruct Point { int x; int y; }; // C++11:直接初始化成员 Point p{ 10, 20 }; // x=10, y=20 // C++98 :必须带等号 = Point p = { 10, 20 }; // 如果只初始化部分成员,后面的成员会被自动初始化为 0 Point p2 = { 5 }; // p2.x = 5, p2.y = 0有构造函数的类
创建对象时也可以使用列表初始化方式会调用构造函数初始化,没有构造函数就会报错。
cppclass Student { public: // 构造函数 Student(std::string n, int i) : _name(n) , _id(i) { } private: std::string _name; int _id; }; // C++11:直接使用 { } Student s1 = { "张三", 20251117 }; // C++98中只有在 Student 是"聚合类"时才合法 Student s2{ "张三", 20251117 }; // 省略等号 // C++98 :使用圆括号 () 或带等号的圆括号 Student s3("Alice", 123); // 直接初始化 Student s4 = Student("Bob", 456); // 拷贝初始化(调用构造函数创建临时对象,再拷贝构造)
STL容器
允许直接通过 { } 初始化,也可以省略等号。
cpp#include <vector> #include <map> #include <string> // 初始化 vector std::vector<int> v = { 1, 2, 3, 4, 5 }; std::vector<int> v{ 1, 2, 3, 4, 5 }; // 省略等号 // 初始化 map std::map<std::string, int> mp{ {"Alice", 95}, {"Bob", 87}, {"Charlie", 92} }; // 初始化嵌套容器 std::vector<std::vector<int>> vv{ {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
以上就是C++11变化的地方了,{ }
2. std::initializer_list
std::initializer_list是C++11才增加的一个类型,文档链接:initializer_list - C++ Reference 如图所示:
它用于表示和访问一组特定类型的常量值的数组,主要用于支持使用花括号 { } 进行的列表初始化。而我们使用 { } 初始化的时候编译器都会将这个数据列表转换成一个initializer_list,如以下代码:
使用场景
std::initializer_list一般是作为构造函数的参数,在C++11中,在STL的容器中就增加了许多这样的构造函数,如下所示,这里是vector和map中构造函数的变化。
对于其他STL容器也有一个initializer_list参数的构造函数。也正式因为有了这样的构造函数,所以在我们才可以使用 { } 直接初始化各个容器对象,如:
cpp
// 初始化 vector
std::vector<int> v = { 1, 2, 3, 4, 5 }; // 调用了 initializer_list 的构造函数
解释
使用 { } 括起来的初始化列表,在被传递给接受 std::initializer_list<value_type> 的函数或变量时,会被编译器自动转换为一个 std::initializer_list<value_type> 类型的对象,再去调用对应的构造函数。
除了构造时有initializer_list的使用,operator= 也重载了initializer_list参数的使用。
使用示例:
cpp
vector<int> v;
v = { 1, 2, 3, 4, 5 }; // 调用了initializer_list参数的operator=
二、关键字:auto、decltype、nullptr
1. auto
在 C++98/03 标准中,auto 用于声明一个具有自动存储期的变量。这意味着变量在进入其作用域时被创建,在离开作用域时被销毁。它的实际用途为几乎为零,因为函数内部定义的局部变量默认就是自动存储期的,所以显式地使用 auto 是多余的。使用示例:
cpp
void func() {
auto int x = 10; // 'auto' 是多余的,x 默认就是自动存储期
// 等价于 int x = 10;
}
因此,在 C++98 的代码中几乎看不到 auto 。
在C++11中则彻底废弃了auto 在 C++98 中的旧含义,赋予了它新的功能:自动类型推导。即可以自动推到变量类型,在处理复杂类型的时候,就大大方便了我们在C++中的使用。如以下代码:
cpp
vector<string> v = { "sort","insert","find"};
// C++98: 类型冗长,可读性差
std::vector<std::string>::iterator it = v.begin();
// C++11: 简洁明了
auto it2 = v.begin(); // 编译器自动推导为 vector<string>::iterator
2. decltype
关键字decltype可以将变量的类型声明为表达式指定的类型。比如,我们有一个int类型的变量,现在我们想定义一个与a相同类型的变量b,要求不使用int关键字,则就可以通过decltype来帮助我们完成定义,如下所示:
cpp
int main()
{
int a = 10;
decltype(a) b; // b的类型也为 int
return 0;
}
也适用于表达式,比如,我们要定义一个与一个表达式结果相同的变量,则就可以使用decltype来解决,如下所示:
cpp
int main()
{
int x = 2;
double y = 3.3;
decltype(x * y) z; // x*y的结果为double,则z的类型也就为double
return 0;
}
3. nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示 整形常量。比如函数调用时:
这里我们理解的NULL是空指针,则应该调用void fun(int* a),但是这里却没有调,与想调用指针版本的初衷违背。
所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针,不会有歧义了。
三、范围for循环
C++11中,引入了基于范围的for循环,它适用于对一个有范围的集合进行遍历,比如:我们要遍历一个数组:
cpp
int main()
{
int arr[] = { 1, 2, 3, 4, 5 };
//for (auto e : arr) // 会自动推导e的类型 -- 方法1
//for (auto& e : arr) // 传引用就可以直接拿到数组中的值 -- 方法2
for (int e : arr) // 使用原本的类型int接收-- 方法3
{
cout << e << " "; // 输出当前元素
}
return 0;
}
这里的意思就是:每次循环,arr 的当前元素会被复制到 e 中(值传递),然后循环会依次访问 数组中每一个元素。并且它会自动判断结束。
四、认识STL中的变化
在C++11中的变化有一些几点:
1、增加了几个新容器 ,如图所示(其中框出来的就是C++11新增的容器):
首先我们先看看array,如图所示:
简单来说,array就是一个静态数组,通过T控制类型,N控制数组的大小。其余接口操作也很简单,参考文档链接:array - C++ Reference 即可。其实实际上array并没有怎么使用,因为它和我们的C风格数组差别并不大,唯一提高的就是就是在检查越界上面:在C风格数组中,越界访问时应不会检查,但这样的程序一般会输出垃圾值,或者直接崩溃,甚至出现安全与逻辑错误;而std::array中的是通过operator[ ] 进行访问的,它的内部会进行严格的检查,安全性能更高。
然后就是std::forward_list,它的底层是一个单链表,使用方法和list很类似。
最后就是unordered_map和unordered_set,前面我们已经讲过了,使用方法和set、map类似。
2、增加了新接口
如图所示:
它们其实就是const迭代器和const反向迭代器,实际意义不大,因为这两种迭代器我们在上面的四个函数就已经重载过了,一般我们使用上面四种(即bengin、end、rbegin、rend)就够了。
3. 所有容器都增加了emplace系列
如图所示:
这里的emplace需要我们理解了右值引用 和可变模板参数才能理解,我们在本文后面再详细解释。
4. 容器增加了移动构造和移动赋值
移动构造和移动赋值可以大大节省我们使用容器的效率。这也需要我们理解了右值引用才能理解,我们在本文后面再详细解释。
五、★右值引用和移动语义★
这是我们学习C++11中的一个难点
1. 左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,而我们 之前使用到的引用叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
(1)概念
首先我们需要来认识一下什么事左值?什么是有右值?
左值 就是一个我们可以对它取地址,一般可以对它赋值 的可以表示数据的表达式,可以出现在赋值符号的左边,比如:
cpp
// 左值
int a = 10; // 普通变量
const int b = 20; // const变量
int* p = new int(0); // 指针变量,p是左值,*p也是左值
"xxxxx"; // 字符串 - 左值,因为它可以取地址:&"xxxxx"合法
auto pp = &"aaaaa"; // 相当于 char (*pp)[6] = &"aaaaa";
右值 则是一个我们不可以对它取地址,也不可以修改 的可以表示数据的表达式,只能出现在赋值符号的右边 。通常是临时的,生命周期很短。如:
cpp
int fmin(int a, int b)
{
return a < b ? a : b;
}
int main()
{
double x = 1.1, y = 2.2;
// 以下都是右值
10; // 字面量
x + y; // 表达式结果
fmin(x, y); // 函数返回非引用类型
// 这里编译会报错:error C2106: "=": 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
区分左值与右值关键在于这个数据可不可以取地址。下面再来认识一下什么是左值引用?什么是右值引用?
左值引用就是对左值去取别名,我们在以前已经学过了,即:左值引用讲解链接 。常见使用如下所示:
cpp
int a = 10;
int& ra = a; // 左值引用
而对于右值引用,其实就是对右值取别名,它的写法和左值有些不同,如下所示:
cpp
int fmin(int a, int b)
{
return a < b ? a : b;
}
int main()
{
double x = 1.1, y = 2.2;
// 右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
return 0;
}
(2)区别
左值引用:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值(这也是我们为什么一般要在传参数的时候加上const的原因)。
示例:
cpp
int main()
{
// 左值引用只能引用左值
int a = 10;
int& ra1 = a; // ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const引用都可以引用
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值(move是C++11引入的一个函数,作用是将一个对象转换为右值引用)。
- 可以取地址和被修改(在下文"完美转发"会详细解释)
示例:
cpp
int main()
{
int&& r1 = 10; // 合法:右值引用可以引用右值
int a = 10; // a 是左值
// error C2440: "初始化": 无法从"int"转换为"int &&"
int&& r2 = a; // 编译错误:右值引用不能引用左值(变量 a)
int&& r3 = std::move(a); // std::move 将左值 a 转换为右值引用
return 0;
}
2. 移动语义(移动构造和移动赋值)
以下代码是一个简单的string的模拟实现:
cpp
namespace MyCreate
{
class string
{
public:
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)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
首先我们来认识一下移动构造和移动赋值。
移动构造
移动构造就是用一个右值(通常是临时对象) 来构造一个新的对象,即:将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
我们来看下面的这个情况:
在以前没有移动构造的时候(即C++11之前),如下:
此时优化前有两次空间的拷贝和销毁,优化后有一次空间的拷贝和销毁。
在C++11中,有了移动构造,则情况就变了,编译器优化前,因为str是左值,所以需要拷贝构造一个临时对象,又因为临时对象是一个右值,所以会移动构造对象s;优化后就是直接移动构造了,因为在C++11中,此时编译器的优化有两点:
- 连续的构造,合二为一;
- 编译器特殊处理,将这里的返回值str识别成了右值处理(用于调用移动构造)
如图所示:
此时因为移动构造,优化前有一次空间的拷贝和销毁,编译器优化后就没有空间拷贝和空间销毁了,因此就没有了拷贝带来的时间消耗了,大大提高了整个程序的效率。
在这里,移动构造的代码如下所示:
cpp
// 移动拷贝
string(string&& s) noexcept
:_str(nullptr)
, _size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动拷贝构造" << endl;
swap(s);
}
移动赋值
移动赋值就是用一个右值 来给一个已经存在的对象 赋值,也就是窃取别人的资源来赋值自己。
对于如下情况,编译器也进行了特殊处理,将str识别成了右值(调用移动构造),进行了移动构造了一个临时对象,然后再用临时对象移动赋值对象s。如下所示:
这里并不能将构造和赋值合并。
移动赋值的代码如下所示:
cpp
// 移动赋值
string& operator=(string&& s) noexcept
{
cout << "string& operator=(string&& s) -- 移动赋值重载" << endl;
swap(s);
return *this;
}
3. move的作用
move函数的实现比较复杂,但是它的使用是比较简单的,下面我们就来理解一下move的使用功能。它的作用为:
- 将一个左值转换为右值引用。从而让编译器认为这个对象可以被"移动"(即可以调用移动构造或移动赋值),而不是被"拷贝"
比如有一些代码:
cpp
int main()
{
MyCreate::string s1("xxxxxx");
std::move(s1); // 单独的使用move并有什么作用
MyCreate::string copy1 = s1; // s1的资源不会被夺取
// 可以通过构造或赋值来构造夺取原本数据中的资源
MyCreate::string copy2 = move(s1); // 构造
MyCreate::string s2("yyyyyy");
MyCreate::string copy3;
copy3 = move(s2); // s2的资源会被夺取
return 0;
}
4. 应用场景
对于左值引用 的使用场景就是:做函数参数,或则做函数返回值,这样可以减少拷贝,提高效率。
比如我们在一个不需要修改参数的函数中如果直接使用值传递,则传参时,就需要拷贝一份,将实参拷贝给形参。对于一些简单的类型(如 int,double 这样的)效率并没有什么差距,但是对于一些复杂类型(比如:vector<vecotr<string>>这样的),使用值传递,拷贝的时间就会比较大,如果使用左值引用,则就不需要拷贝了,这就减少了时间消耗,提高了效率。
对右值引用,它的价值可以总结为:进一步减少拷贝,弥补左值引用没有解决的场景。
右值引用的常见使用场景:
1、函数返回时,对于深拷贝 的类必须传值返回的情况
如果是浅拷贝的类,则不需要实现移动构造,因为浅拷贝的类没有要转移的资源,它的移动跟着拷贝构造的效率没什么差别,所以不用实现。
而对于深拷贝的类,它的资源在堆上,比如如果返回值是string,而这个string又很大,则编译器会将它识别为右值从而调用移动构造来减少拷贝,大大提高效率。
cpp
string fun()
{
string str;
//...
return str;
}
int main()
{
// 情况1
string ret1 = fun();
// 情况2
string ret2;
ret2 = fun();
}
2、在容器的插入接口中,如果插入的对象是右值,则可以使用移动构造来转移资源给数据结构中的对象,也可以减少拷贝,通过效率。
比如:

5. 万能引用
在模板中的&&代表了万能引用,它既可以引用左值,也可以引用右值。如以下代码:
cpp
template<typename T>
void PerfectForward(T&& t)
{
// ...
}
如果传递的实参是左值,那就是左值引用(有些地方也叫引用折叠)。如:
cpp
template<typename T>
void Fun(T&& t)
{
// 当前t的类型是int&&
cout << t << endl;
}
int main()
{
Fun(10); // 右值
return 0;
}
如果实参是右值,那就是右值引用。如:
cpp
template<typename T>
void Fun(T&& t)
{
// 当前t的类型是int&
cout << t << endl;
}
int main()
{
int a = 20;
Fun(a); // 左值
return 0;
}
6. 完美转发
之前我们说通过万能引用可以引用左值和右值,实参是左值就是左值引用,实参是右值就是右值引用。看如下代码:
cpp
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
运行后会发现输出结果都是左值引用或const左值引用,即:
这是因为右值引用变量 的属性都会被编译器识别成左值,因为右值是不能取地址和被修改的,但是右值引用后的右值是可以取地址和修改的。比如以下代码可以通过编译:
cpp
int main()
{
int&& r = 10; // r是右值引用
r++; //可以修改
int* p = &r; // 可以取地址
return 0;
}
而完美转发则可以在传参的过程中保留对象原生类型属性 ,如果是左值引用那就是左值引用,如果是右值引用,那就是右值引用。它必须在模板中使用 ,即必须配合万能引用一起使用,如下所示:

六、类的新功能
1. 默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
而C++11则新增了两个:移动构造函数和移动赋值运算符重载。注意一下一下三点即可:
- 如果你没有自己实现移动构造函数 ,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(即这三个函数都没有实现)。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数 ,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(即这三个函数都没有实现),那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
2. default、delete关键字
default 关键字可以显示指定对应的默认成员函数生成。
比如如果你定义了其他构造函数,编译器就不再生成默认构造函数,但是使用 default 则可以也可以让它生成默认成员函数。如:
如果你自己定义了析构函数、拷贝构造函数或拷贝赋值重载中任意一个,就都不会生成移动构造和移动赋值重载了。但是我们可以通过default来显示指定编译器强制生成默认的移动构造或移动赋值重载。如:
delete 关键字则和default找相对,delete可以指示编译器不生成对应函数的默认版本,使用方法和default一样,只需要在该函数声明加上=delete即可。
七、可变参数模板
1. 概念
在C++11以前,我们的模板中类模版和函数模版只能含固定数量的模版参数。而在C++11中增加了可变参数模板的概念,它可以让我们的参数可以传递很多个。
下面就是一个基本可变参数的函数模板:
cpp
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{
// ...
}
如果我们要获取一个参数包的值,这里有两种方法:递归函数方式展开和逗号表达式展开。
递归函数方式展开,这种方法很好懂,就是不断减少参数包中的参数,来逐步获取各个参数。如下所示:
cpp
// 递归终止函数
void _ShowList()
{
cout << endl;
}
// 展开函数
template <class T, class ...Args>
void _ShowList(T value, Args... args)
{
cout << value << " ";
_ShowList(args...);
}
// 函数模板
template <class ...Args>
void PrintList(Args... args)
{
_ShowList(args...);
}
int main()
{
PrintList();
PrintList(1);
PrintList(1,2);
PrintList(1,2,3);
PrintList(1, 2, 3, std::string("xxxx")); // 任何类型都可以
return 0;
}
运行结果:
逗号表达式展开,这种方法比较怪,如下所示:
cpp
template <class T>
void PrintArg(T t)
{
cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
int arr[] = { (PrintArg(args), 0)... }; // 逗号表达式展开
cout << endl;
}
int main()
{
ShowList(1);
ShowList(1, 2);
ShowList(1, 2, 3);
return 0;
}
这里来解释一下:
(PrintArg(args), 0)是一个逗号表达式,它会从左向右执行,最终这个逗号表达式的结果为最右边的结果(这里是 0)。(PrintArg(args), 0) 的作用是:先调用 PrintArg 打印参数,然后这个表达式整体的值是 0。
... 是参数包展开的意思,也就是将{ (PrintArg(args), 0)... }展开成{ (PrintArg(args1), 0),PrintArg(args2), 0),PrintArg(args3), 0),PrintArg(args4), 0) ... } 直到参数包全部都展开完。
由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分Printarg(args) 打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。
2. STL中的emplace系列
如图是vector中的接口:

其中emplace对应insert,都是插入的意思,emplace_back对应push_back都是尾插的意思。具体的接口如下所示:
可以看到,它们都是使用可变参数模板 实现的,并且是万能引用,那它们具体有什么作用呢?
我们先来看看下面这段代码:
cppclass Date { public: Date(int year = 1, int month = 1, int day = 1) :_year(year) , _month(month) , _day(day) { cout << "Date构造" << endl; } Date(const Date& d) :_year(d._year) , _month(d._month) , _day(d._day) { cout << "Date拷贝构造" << endl; } private: int _year; int _month; int _day; }; template <class ...Args> Date* Create(Args... args) { Date* ret = new Date(args...); // 使用拷贝模板参数来构造 return ret; }实现了这样的一个有缺省值的构造函数,和一个有可变参数模板的函数模板。这是我们就可以如下这样定义这个函数了:
cppint main() { // 通过传递参数来构造 // 这种方法会通过参数包一直传递到Date的构造函数中从而直接使用传递的参数构造 Date* d1 = Create(); Date* d2 = Create(1); Date* d3 = Create(1, 2); Date* d4 = Create(1, 2, 3); // 先构造一个Date对象,此时参数包中就只有一个对象,这是会调用拷贝构造来创建Date对象 Date* d5 = Create(Date(1, 2)); Date* d6 = Create(Date(1, 2, 3)); return 0; }运行结果:
总结一下:使用可变参数模板来创建对象就变得很灵活了,既可以传参直接调用构造函数创建,也可以传递对象进行创建。
所以对对emplace系列的接口也可以这样做,既可以通过传递各个参数直接在容器内部构造对象 ,也可传递对象通过移动构造来创建对象。
那么,我们为什么需要 emplace 接口呢?
在 C++11 之前,我们通常使用 push_back 等接口向容器添加对象,但这种方法只能通过传递对象来添加对象,如图所示:
所以如果使用push_back来添加数据。
- 就是需要先构造,再(对于没有移动构造的对象/数据)拷贝构造,此时的就需要两次资源的拷贝。
- 但是对于具有移动构造的就会先构造,再移动构造,此时拷贝资源也只需要一次,和emplace系列接口相比效率差不多。
- 而emplace只需要将参数一直向下传递 ,直接一次构造就可以完成插入了,效率比较高。
而C++11中一般都是移动构造,所以一般来说emplace系列和常规的插入(insert,push_back等)的效率其实都差不多。
感谢各位观看!希望能多多支持!
