C++11新特性大揭秘:优化性能与简化代码的利器
C++11当中的{}与传统的{}
C++11之前的{}初始化
传统的{}初始化(C++03以及之前)——传统C++中, {}主要用于聚合初始化,仅用于聚合类型。 聚合类型:
- 结构体/类:无用户自定义构造函数、无基类、无非公有成员、无虚函数。
- 数组:固定长度的原生数组。
示例:
代码语言:javascript代码运行次数:0运行复制// 结构体初始化
struct Point { int x; int y; };
Point p = {10, 20}; // 聚合初始化
// 数组初始化
int arr[] = {1, 2, 3};
// 枚举初始化(仅限C++11前部分编译器扩展)
enum Color { Red, Green, Blue };
Color c = {Red};
限制:
- 不能用于非聚合类型(如包含构造函数的类)。
- 不支持动态容器(如 std::vector)的直接初始化。
C++11之后的 {} 初始化(列表初始化)
C++11引入了统一初始化语法(Uniform Initialization),扩展了 {} 的用途,使其适用于所有类型的初始化。
核心特性:
- 适用所有类型:包括基本类型、类对象、STL容器等。
- 防止窄化转换(Narrowing Conversion):禁止隐式截断(如 double → int 无显式转换时报错)。
- 支持 std::initializer_list:允许类定义接受初始化列表的构造函数。 示例:
// 基本类型
int x{5}; // 替代 int x = 5;
int y{}; // 值初始化(0)
// 类对象
class Widget {
public:
Widget(int a, double b) { /* ... */ }
};
Widget w1{10, 3.14}; // 调用构造函数
Widget w2{}; // 默认构造函数
// STL容器
std::vector<int> v{1, 2, 3}; // 初始化列表构造函数
std::map<int, std::string> m{{1, "a"}, {2, "b"}};
// 动态分配对象
int* ptr = new int[3]{1, 2, 3};
// 函数返回值
std::vector<int> create_vec() { return {1, 2, 3}; }
类成员初始化(C++11新增)
代码语言:javascript代码运行次数:0运行复制class MyClass {
private:
int a{10}; // 类内成员初始化
std::string s{"Hello"};
};
防止最令人烦恼的解析
代码语言:javascript代码运行次数:0运行复制Widget w1(); // 函数声明(传统问题)
Widget w2{}; // 明确调用默认构造函数
C++11前后{}的比较
C++11中的std::initializer_list
std::initializer_list 的优先级 若类同时定义了接受 std::initializer_list 的构造函数和其他构造函数,编译器会优先匹std::initializer_list,可能导致意外行为:
代码语言:javascript代码运行次数:0运行复制std::vector<int> v1(5, 1); // 5个元素,每个为1
std::vector<int> v2{5, 1}; // 2个元素:5和1
空 {} 执行值初始化(Value Initialization):
- 基本类型初始化为 0 或 nullptr。
- 类类型调用默认构造函数。 自动类型推导 使用 auto 时需注意类型推导:
auto x{5}; // C++11中推导为 std::initializer_list<int>
auto y = {5}; // 同上
auto z(5); // 推导为 int
// C++17修复此问题:auto x{5} 推导为 int
示例代码对比:
代码语言:javascript代码运行次数:0运行复制// 传统初始化(C++03)
int arr[] = {1, 2, 3};
Point p = {10, 20};
// C++11列表初始化
std::vector<int> nums{1, 2, 3};
Widget w{3, 2.7};
// 窄化转换错误
double d = 3.14;
int a{d}; // 错误:窄化转换(C++11报错)
int b(d); // 允许(但可能丢失精度)
C++11当中的右值引用
前言:C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
如何区别左值(Lvalue)与右值(Rvalue)
左值:有明确内存地址、可被取地址的表达式(如变量、函数返回的左值引用)。
代码语言:javascript代码运行次数:0运行复制int a = 10; // a 是左值
int& ref = a; // ref 是左值引用
右值:临时对象、字面量、表达式计算的中间结果(如 x + y、函数返回的非引用类型)。
代码语言:javascript代码运行次数:0运行复制42; // 字面量是右值
int&& rref = 10; // 右值引用绑定到字面量
值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left value、right value 的缩写。现代C++中,lvalue被解释为loactorvalue的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽rvalue被解释为readvalue,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址。
右值引用语法 右值引用通过 && 声明,只能绑定到右值。
代码语言:javascript代码运行次数:0运行复制int x = 10;
int&& r1 = x; // 错误!不能绑定到左值
int&& r2 = x + 1; // 正确!绑定到临时结果(右值)
C++11右值引用核心工具:std::move(标记可移动对象)、std::forward(保持值类别)。
右值引用的核心作用:移动语义
问题背景:深拷贝的开销 传统 C++ 中,对象传递或赋值时默认使用 深拷贝。对于持有动态资源(如堆内存)的类,频繁拷贝会导致性能问题:
代码语言:javascript代码运行次数:0运行复制class BigData
{
public:
BigData(size_t size) : data(new int[size]) {}
~BigData() { delete[] data; }
// 拷贝构造函数(深拷贝)
BigData(const BigData& other) : data(new int[other.size])
{
std::copy(other.data, other.data + size, data);
}
private:
int* data;
};
BigData a(1000);
BigData b = a; // 深拷贝:复制 1000 个元素(高开销)
移动构造函数与移动赋值运算符 右值引用允许定义 移动语义,直接将资源从临时对象“窃取”过来,避免深拷贝:
代码语言:javascript代码运行次数:0运行复制class BigData
{
public:
// 移动构造函数
BigData(BigData&& other) noexcept
: data(other.data)
{ // 直接接管资源
other.data = nullptr; // 原对象置空(避免重复释放)
}
// 移动赋值运算符
BigData& operator=(BigData&& other) noexcept
{
if (this != &other)
{
delete[] data; // 释放当前资源
data = other.data; // 接管资源
other.data = nullptr;
}
return *this;
}
private:
int* data;
};
BigData a(1000);
BigData b = std::move(a); // 调用移动构造函数(零拷贝)
关键函数 std::move
std::move 将左值强制转换为右值引用,标记对象可被移动。 调用后,原对象处于“有效但未定义状态”(通常不再使用)。
代码语言:javascript代码运行次数:0运行复制BigData a(1000);
BigData b = std::move(a); // a 的资源被转移给 b
// 此时 a.data 为 nullptr
右值引用的核心作用:完美转发
问题背景:参数转发中的值类别丢失 模板函数转发参数时,若直接传递参数,可能丢失其原始值类别(左值/右值):
代码语言:javascript代码运行次数:0运行复制template<typename T>
void wrapper(T arg)
{
func(arg); // arg 始终是左值,无法保持右值特性
}
wrapper(10); // 传入右值,但 arg 变为左值
通用引用与 std::forward
- 通用引用(Universal Reference):通过 T&& 声明的模板参数,可绑定到左值或右值。
- std::forward 根据模板参数 T 的类型,保持参数的原始值类别。
template<typename T>
void wrapper(T&& arg)
{ // 通用引用
func(std::forward<T>(arg)); // 完美转发
}
int x = 10;
wrapper(x); // 转发左值
wrapper(10); // 转发右值
右值引用的应用场景
优化容器操作 STL 容器(如 std::vector)利用移动语义减少元素拷贝:
代码语言:javascript代码运行次数:0运行复制std::vector<std::string> v;
std::string s = "Hello";
v.push_back(s); // 拷贝构造(深拷贝)
v.push_back(std::move(s)); // 移动构造(高效)
工厂函数返回对象 C++11 后,函数返回局部对象时,编译器会自动使用移动语义(若存在移动构造函数):
代码语言:javascript代码运行次数:0运行复制BigData createData()
{
BigData data(1000);
return data; // 优先调用移动构造函数(而非拷贝)
}
实现高性能智能指针 如 std::unique_ptr 的移动语义确保资源唯一所有权:
代码语言:javascript代码运行次数:0运行复制std::unique_ptr<int> p1 = std::make_unique<int>(10);
std::unique_ptr<int> p2 = std::move(p1); // p1 变为 nullptr
引⽤折叠
C++中不能直接定义引⽤的引⽤如int& && r = i; 这样写会直接报错,通过模板或typedef中的类型操作可以构成引⽤的引⽤。
通过模板或typedef中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则,⼤家需要⼀个⼀个仔细理解⼀下。
像f2这样的函数模板中,T&&x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤。
Function(T&&t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的Function,实参是右值,实例化出右值引⽤版本形参的Function
代码语言:javascript代码运行次数:0运行复制// 引用折叠的例子
template<class T>
void f1(T& x) {
// 引用折叠限定,f1实例化以后总是一个左值引用
}
template<class T>
void f2(T&& x) {
// 引用折叠限定,f2实例化后可以是左值引用,也可以是右值引用
}
int main() {
typedef int& lref; // lref是int&的别名
typedef int&& rref; // rref是int&&的别名
int n = 0;
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
// 没有引用折叠时 -> f1实例化为 void f1(int& x)
f1<int>(n); // 正常编译
f1<int>(0); // 编译报错,0是右值
// 引用折叠时 -> f1实例化为 void f1(int& x)
f1<int&>(n); // 正常编译
f1<int&>(0); // 编译报错,0是右值
// 引用折叠时 -> f1实例化为 void f1(int&& x)
f1<int&&>(n); // 编译报错,n是左值
f1<int&&>(0); // 正常编译
// 折叠->实例化为
f1<const int&>(n);
f1<const int&>(0);
// 折叠->实例化为
f1<const int&&>(n);
f1<const int&&>(0);
// 没有折叠时,f2实例化为 void f2(int&& x)
f2<int>(n); // 编译报错,n 是左值
f2<int>(0); // 正常编译,0 是右值
// 折叠时,实例化为 void f2(int& x)
f2<int&>(n); // 正常编译,n 是左值
f2<int&>(0); // 编译报错,0 是右值
// 折叠时,实例化为 void f2(int&& x)
f2<int&&>(n); // 编译报错,n 是左值
f2<int&&>(0); // 正常编译,0 是右值
代码语言:javascript代码运行次数:0运行复制#include <iostream>
using namespace std;
// 完美转发函数模板
template<class T>
void Function(T&& t) {
int a = 0;
T x = a; // T是推导出的类型
// x++; // 这里会报错,具体原因取决于T的类型
cout << &a << endl;
cout << &x << endl << endl;
}
int main() {
// 调用Function(10),10是右值,推导出T为int,实例化为void Function(int&& t)
Function(10); // 右值传递,实例化为 void Function(int&& t)
int a;
// 调用Function(a),a是左值,推导出T为int&,引用折叠,实例化为 void Function(int& t)
Function(a); // 左值传递,实例化为 void Function(int& t)
// 调用Function(std::move(a)),std::move(a)是右值,推导出T为int,实例化为 void Function(int&& t)
Function(std::move(a)); // 右值传递,实例化为 void Function(int&& t)
const int b = 8;
// 调用Function(b),b是左值,推导出T为const int&,引用折叠,实例化为 void Function(const int& t)
Function(b); // 左值传递,实例化为 void Function(const int& t)
// 调用Function(std::move(b)),std::move(b)是右值,推导出T为const int,实例化为 void Function(const int&& t)
Function(std::move(b)); // 右值传递,实例化为 void Function(const int&& t)
return 0;
}
可变参数模板
基本语法及原理
C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数。 template <class …Args> void Func(Args… args) {} template <class …Args> void Func(Args&… args) {} template <class …Args> void Func(Args&&… args) {} 我们⽤省略号来指出⼀个模板参数或函数参数的表⽰⼀个包,在模板参数列表中,class…或typename…指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟…指出接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
- 这⾥我们可以使⽤sizeof…运算符去计算参数包中参数的个数。
#include <iostream>
#include <string>
using namespace std;
// 可变参数模板,使用参数包
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl; // 打印参数包中参数的个数
}
int main()
{
double x = 2.2;
Print(); // 包含 0 个参数
Print(1); // 包含 1 个参数
Print(1, string("xxxxx")); // 包含 2 个参数
Print(1.1, string("xxxxx"), x); // 包含 3 个参数
return 0;
}
包扩展
对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(…)来触发扩展操作。底层的实现细节如图1所⽰。 C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
#include <iostream>
#include <string>
using namespace std;
void ShowList() {
// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args) {
cout << x << " ";
// args是N个参数的参数包
// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args) {
ShowList(args...);
}
int main() {
Print();
Print(1);
Print(1, string("xxxxx"));
Print(1, string("xxxxx"), 2.2);
return 0;
}
代码语言:javascript代码运行次数:0运行复制#include <iostream>
#include <string>
using namespace std;
template <class T>
const T& GetArg(const T& x)
{
cout << x << " ";
return x;
}
template <class ...Args>
void Arguments(Args... args) {}
template <class ...Args>
void Print(Args... args)
{
// 注意GetArg必须返回或者传递的对象,这样才能组成参数包给Arguments
Arguments(GetArg(args)...);
}
// 本质可以理解为编译器编译时,包的扩展模式
// 将上面的函数模板扩展实例化为下面的函数
// 例如:
// void Print(int x, string y, double z)
// {
// Arguments(GetArg(x), GetArg(y), GetArg(z));
// }
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}
empalce系列接⼝
在C++11中,emplace 是容器类(如 std::vector、std::map、std::unordered_map 等)提供的接口,它允许在容器中原地构造元素,而不需要先构造对象再复制或移动到容器中。这样可以避免不必要的构造和复制,提高性能。
emplace的主要接口: C++11 引入了以下几个与 emplace 相关的接口:
emplace_back():用于在容器的末尾原地构造元素(适用于 std::vector、std::deque 和 std::list)。 emplace_front():用于在容器的前端原地构造元素(适用于 std::deque 和 std::list)。 emplace():用于在关联容器中插入元素(适用于 std::map、std::multimap、std::unordered_map、std::unordered_multimap 等)。
- emplace_back() emplace_back() 允许在 std::vector 或其他类似容器的末尾原地构造元素,而不需要先创建临时对象。
例如:
代码语言:javascript代码运行次数:0运行复制#include <iostream>
#include <vector>
class MyClass
{
public:
MyClass(int a, double b)
{
std::cout << "MyClass constructed with " << a << " and " << b << std::endl;
}
};
int main()
{
std::vector<MyClass> vec;
vec.emplace_back(10, 3.14); // 原地构造 MyClass(10, 3.14)
return 0;
}
- emplace_front() emplace_front() 类似于 emplace_back(),但它是在容器的前端原地构造元素(适用于 std::deque 和 std::list)。
示例:
代码语言:javascript代码运行次数:0运行复制#include <iostream>
#include <deque>
class MyClass
{
public:
MyClass(int a, double b)
{
std::cout << "MyClass constructed with " << a << " and " << b << std::endl;
}
};
int main()
{
std::deque<MyClass> dq;
dq.emplace_front(10, 3.14); // 在前端原地构造 MyClass(10, 3.14)
return 0;
}
- emplace() emplace() 用于关联容器(如 std::map、std::unordered_map)中插入元素。与 insert() 不同,emplace() 会直接在容器中构造元素,而不需要创建临时对象。它接受构造元素所需的参数,并原地构造该元素。
对于 std::map:
代码语言:javascript代码运行次数:0运行复制#include <iostream>
#include <map>
int main()
{
std::map<int, std::string> myMap;
myMap.emplace(1, "one"); // 原地构造键值对 (1, "one")
myMap.emplace(2, "two");
for (const auto& pair : myMap)
{
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
为什么使用 emplace?
- 性能优势:emplace 不需要先构造临时对象再插入,它直接在容器中构造元素,减少了不必要的复制或移动操作。
- 更灵活的构造方式:通过 emplace,你可以传递元素构造所需的参数,容器会直接使用这些参数来构造元素,这对复杂类型特别有用。
- 简洁性:通过 emplace,你可以避免创建临时对象并直接在容器内构造对象。