C++11新特性学习
C++11新特性
- 背景
- 新特性
- 列表初始化
- 右值引用和移动语义
- 可变参数模板
- 新的类功能
- lambda表达式
背景
C++11是C++编程语言的一个重要版本,它是继 C++98/03 后的标准,经过多年设计和开发,带来了大量的新特性和改进。C++11 在 2011 年被国际标准化组织(ISO)正式发布,主要目的是为了增强语言的现代化、提高开发效率、改进性能并且提高语言的可维护性。
新特性
列表初始化
在C++98中,一般数组和结构体可以使用{}来初始化
struct Point
{int x;int y;
};
int main()
{int a[] = {1, 2, 3};int array[5] = {0};Point p = {1, 2};return 0;
}
在C++11中,统一了初始化的思想,一切对象都可以使用{}来初始化,并且可以省略=
本质是类型转换,中间会产生临时对象,经过编译器优化以后变成直接构造
需要注意的是,STL的一些容器如果要支持列表初始化的话是需要提供一个支持std::initializer_list的构造函数,否则是无法直接使用列表初始化进行构造的
std::initializer_list是一个类,本质上是底层开一个数组,将数据拷贝过来,它内部有两个指针分别指向数组的开始和结束
struct Student
{std::string name;std::size_t id;
};
struct Point
{int x;int y;Student stu;
};
int main()
{std::vector<int> ve{1, 2, 3, 4, 5};std::map<int, char> mm{ {0, 'a'}, {1, 'b'} };int a{1};int a[5]{1, 2, 3, 4, 5};// C++11中这样也是合法的Point p{1, 2, {"xiao", 10} };return 0;
}
右值引用和移动语义
C++98中存在引用的语法,C++98中新增了右值引用语法特性,你可以把右值看做是一种临时值(算数运算的结果),不会有内存地址,出现在=的右边,一般为字面量常量或存储于寄存器中的变量等,所以左值和右值的核心区别是能否取地址
// 10就是一个右值,他不能取地址
int *p = &10;
// 这里tmp是左值,它有地址
int tmp = 10;
int *p1 = &tmp;
// 列表初始化得到的结果是一个右值,所以不可取地址
int *p = &({1, 2}); // 编译器报了语法错误,要求左值
-
左值引用和右值引用
按照左值引用的概念来推测,右值引用就是给右值取别名,所以他可以延长右值变量的生命周期 -
两条规则:
左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
std::move是标准库里提供的一个函数模板,本质是内部进行强制类型转换
变量表达式都是左值属性,也就是说当一个右值被右值引用绑定后,右值引用变量的属性是左值
-
移动构造和移动赋值:
- 移动构造函数:是一种
构造函数,要求第一个参数是该类类型的右值引用,其他额外的参数必须要求缺省值 - 移动赋值是赋值运算符的重载,与拷贝赋值函数构成重载,要求函数第一个参数为
该类类型的右值引用 - 作用是提高效率,减少拷贝,一些需要深拷贝的类这两个函数才会有意义,它的本质是“窃取”资源而非拷贝
- 移动构造函数:是一种
-
C++11之后还提出了纯右值和将亡值的概念:纯右值是指字面值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象;将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达 -
引用折叠:
C++中不能直接定义引⽤的引⽤如int& && r = i,这样写会直接报错,通过模板或typedef中的类型操作可以构成引⽤的引⽤。- 简单来说就是只有
T&& &&的类型是T&&,其余的任何引⽤的引⽤类型都是T&
#include <iostream> #include <type_traits> // 普通类型 template <typename T> void printType(T&& arg) { std::cout << "Type: " << typeid(arg).name() << std::endl; }int main() {int x = 42;// T&& & 变成 T&printType(x); // x 是左值,T推导为 int&,所以 arg 是 int&// T&& && 变成 T&&printType(42); // 42 是右值,T推导为 int, 所以 arg 是 int&&return 0; }输出结果
Type: int& Type: int&& -
完美转发:
-
变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤定后,右值引⽤变量表达式的属性是左值,也就是说
Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性,就需要使⽤完美转发实现。 -
本质上是一个函数模板,主要通过引用折叠的方式实现,下⾯⽰例中传递给
Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤返回。
-
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
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<class T>
void Function(T&& t){Fun(t);}
int main()
{Function(10); int a;Function(a); Function(std::move(a)); const int b = 8;Function(b); Function(std::move(b)); 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...来计算参数包中参数的个数
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;}
- 包扩展:对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(
...)来触发扩展操作。 - 编译时递归解析参数
// 这里相当于是递归的终止条件,没有这个函数编译时就会报错
void showList()
{cout << endl;
}
template <typename T, typename... Args>
void showList(T x, Args... args)
{cout << x << " ";// 相当于递归showList(args...);
}
emplace系列接口
template <class... Args> void emplace_back (Args&&... args);template <class... Args> iterator emplace (const_iterator position, Args&&... args);
C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container<T>,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。- 测试
vector的emplace_back和push_back分别插入100000个值的耗时的对比程序
#include <iostream>
#include <vector>
#include <chrono>class MyClass {
public:MyClass(int a, int b) : x(a), y(b) {}int x, y;
};int main() {const int num_elements = 100000;// 测试 push_backauto start = std::chrono::high_resolution_clock::now();std::vector<MyClass> vec_push_back;for (int i = 0; i < num_elements; ++i) {vec_push_back.push_back(MyClass(i, i + 1)); // 拷贝或移动构造}auto end = std::chrono::high_resolution_clock::now();std::chrono::duration<double> push_back_duration = end - start;std::cout << "push_back time: " << push_back_duration.count() << " seconds" << std::endl;// 测试 emplace_backstart = std::chrono::high_resolution_clock::now();std::vector<MyClass> vec_emplace_back;for (int i = 0; i < num_elements; ++i) {vec_emplace_back.emplace_back(i, i + 1); // 原地构造}end = std::chrono::high_resolution_clock::now();std::chrono::duration<double> emplace_back_duration = end - start;std::cout << "emplace_back time: " << emplace_back_duration.count() << " seconds" << std::endl;return 0;
}
- 测试结果:可以看到
emplace_back是比push_back快的
push_back time: 0.0041896 seconds
emplace_back time: 0.0039139 seconds
新的类功能
C++11中新增了两个默认成员函数:移动构造函数和移动赋值运算符重载- 移动构造函数行为:默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
- 移动赋值重载函数行为:如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。行为和移动构造函数相同
lambda表达式
lambda表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收lambda对象- 语法:
[capture-list](paraments)->return type{function body}

- 捕获列表:
lambda表达式中默认只能⽤lambda函数体和参数中的变量,如果想⽤外层作⽤域中的变量就需要进⾏捕捉lambda表达式如果在函数局部域中,他可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,这也意味着lambda表达式中可以直接使lambda表达式如果定义在全局位置,捕捉列表必须为空。- 当使⽤混合捕捉时,第⼀个元素必须是
&或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。 - 默认情况下,
lambda捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)。
lambda的应用- 定义一个可调用对象,定义函数指针比较麻烦,仿函数需要定义一个类,也麻烦,使用
lambda很快捷
- 定义一个可调用对象,定义函数指针比较麻烦,仿函数需要定义一个类,也麻烦,使用
lambda的原理:- 编译后从汇编指令层的⻆度看,压根就没有
lambda,lambda底层是仿函数对象,也就说我们写了⼀个lambda以后,编译器会⽣成⼀个对应的仿函数的类。 - 仿函数的类名是编译按⼀定规则⽣成的,保证不同的
lambda⽣成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质是⽣成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器要看使⽤哪些就传那些对象。 - 我们来看一下汇编:这里可以看到实际上是
lambda函数的执行调用了Rate类的operator()

这篇文章就到此结束了,如果觉得写的还不错的话,欢迎点赞收藏加关注,如果有写的不对的地方,欢迎批评指正( •̀ ω •́ )✧
- 编译后从汇编指令层的⻆度看,压根就没有
