C++11新特性
@
- 基础数值类型
- 原始字符串字面常量(raw string literal)
- 类型别名声明
- 类内初始化
- 列表初始化返回值
- 定义尾置返回类型
constexpr构造函数- 用
string对象处理文件名 - 引用限定(reference qualifier)成员函数
- 引用折叠规则
- auto & decltype
- 左值右值
- 用大括号包围的值列表赋值
- 列表初始化
- 关联容器的列表初始化
- 列表初始化
pair的返回类型,pair的列表初始化 - 动态分配对象的列表初始化
- 动态分配数组的列表初始化
- std::function & std::bind & lambda表达式
- 模板的改进
- 并发
- 智能指针
- 基于范围的for循环
- 委托构造函数(delegating constructor)
allocator::construct可使用任意构造函数- 继承构造函数
- nullptr
- final & override
- default
- delete
- explicit
- const
- constexpr
- enum class
- 非受限联合体/类类型的
union成员 - sizeof
- assertion
- 自定义字面量
- 内存对齐
thread_local- 随机数功能
- 正则表达式(regular expression)
- chrono
- 新增数据结构
- 新增算法
- 容器的
cbegin和cend函数 - 容器的非成员函数
swap - 容器
insert成员的返回类型 - 容器的
emplace成员 - 标准库函数begin和end
- 无序容器
- 除法的舍入规则
shrink_to_fitstring的数值转换函数- 新的
bitset运算 - 浮点数格式控制
noexcept- 内联命名空间
- 继承的构造函数与多重继承
- 标准库
mem_fn类模板 - 参考资料
本文将对C++11的新特性做一些总结
基础数值类型
c++11新增了几种数据类型:long long、char16_t、char32_t等
原始字符串字面常量(raw string literal)
原始字符串简单来说,“原生的、不加处理的”,字符表示的就是自己(所见即所得),引号、斜杠无需 “\” 转义,比如常用的目录表示,引入原始字符串后,非常方便。
格式如下:
R"(原始字符串)";
类型别名声明
C++11标准规定了一种新的方法,使用别名声明(alias declaration)来定义类型的别名:
using SI = Sales_item; // SI是Sales_item的同义词
这种方法用关键字using作为别名声明开始,后面紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。
类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。
类内初始化
C++11新标准规定,可以为数据成员提供一个类内初始值(in-class initializer),必须置于等号右侧或者花括号内。创建对象时,类内初始值将用于初始化数据成员。没有初始值的成员将被默认初始化。
列表初始化返回值
C++11规定,函数可以返回花括号包围的值的列表。类似于其他返回结果,此处的列表也用来对表示函数返回的临时变量进行初始化。如果类别为空,临时量执行值初始化;否则返回的值由函数的返回类型决定。
vector<string> process()
{
// ....
// expected和actual均为string对象
if (expected.empty()) {
return {}; // 返回一个空vector列表
} else if (expected == actual) {
return {"functionX", "Ok"}; // 返回列表初始化的vector对象
} else {
return {"functionX", expected, actual};
}
}
定义尾置返回类型
在C++11标准中,定义了尾置返回类型(trailing return type),任何函数的定义都能够使用尾置返回。
尾置返回类型跟在形参列表后面并以一个->符号开头。为了表示函数真正的返回类型跟在形参列表之后,我们在本应该出现返回类型的地方放置一个auto:
// func 接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组。
auto func(int i) -> int(*)[10];
当我们需要为一个lambda定义返回类型时,必须使用尾置返回类型。
transform(vi.begin(), vi.end(), vi.begin(),
[](int i) -> int
{ if (i < 0) return -i; else return i;});
constexpr构造函数
用string对象处理文件名
在C++11标准中,文件名既可以是库类型string对象,也可以是C风格字符数组。旧版本只支持C风格字符数组。
引用限定(reference qualifier)成员函数
引用折叠规则
auto & decltype
关于C++11新特性,最先提到的肯定是类型推导,C++11引入了auto和decltype关键字,使用他们可以在编译期就推导出变量或者表达式的类型,方便开发者编码也简化了代码。
auto
- auto:让编译器在编译器就推导出变量的类型,可以通过=右边的类型推导出变量的类型。
auto a = 10; // 10是int型,可以自动推导出a是int
int i = 10;
auto b = i; // b是int型
auto d = 2.0; // d是double型
auto推导规则
代码1:
int i = 10;
auto a = i, &b = i, *c = &i; // a是int,b是i的引用,c是i的指针,auto就相当于int
auto d = 0, f = 1.0; // error,0和1.0类型不同,对于编译器有二义性,没法推导
auto e; // error,使用auto必须马上初始化,否则无法推导类型
代码2:
void func(auto value) {} // error,auto不能用作函数参数
class A {
auto a = 1; // error,在类中auto不能用作非静态成员变量
static auto b = 1; // error,这里与auto无关,正常static int b = 1也不可以
static const auto c = 1; // ok
};
void func2() {
int a[10] = {0};
auto b = a; // ok
auto c[10] = a; // error,auto不能定义数组,可以定义指针
vector<int> d;
vector<auto> f = d; // error,auto无法推导出模板参数
}
总结一下auto的限制:
- auto的使用必须马上初始化,否则无法推导出类型
- auto在一行定义多个变量时,各个变量的推导不能产生二义性,否则编译失败
- auto不能用作函数参数
- 在类中auto不能用作非静态成员变量
- auto不能定义数组,可以定义指针
auto无法推导出模板参数
再看这段代码:
int i = 0;
auto *a = &i; // a是int*
auto &b = i; // b是int&
auto c = b; // c是int,忽略了引用
const auto d = i; // d是const int
auto e = d; // e是int
const auto& f = e; // f是const int&
auto &g = f; // g是const int&
首先介绍下cv是指const 和volatile
- 在不声明为引用或指针时,auto会忽略等号右边的引用类型和cv限定
- 在声明为引用或者指针时,auto会保留等号右边的引用和cv属性
什么时候使用auto
这里没有绝对答案,只能说一下我自己的理解,个人认为在不影响代码代码可读性的前提下尽可能使用auto是蛮好的,复杂类型就使用auto,int、double这种就没有必要使用auto了吧,看下面这段代码:
auto func = [&] {
cout << "xxx";
}; // 对于func你难道不使用auto吗,反正我是不关心lambda表达式究竟是什么类型。
auto asyncfunc = std::async(std::launch::async, func);
// 对于asyncfunc你难道不使用auto吗,我是懒得写std::futurexxx等代码,而且我也记不住它返回的究竟是什么...
auto和动态分配
如果我们提供一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们想要分配的对象的类型,但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto:
auto p1 = new auto(obj); // p指向一个与obj类型相同的对象,该对象用obj进行初始化
auto p2 = new auto{a, b, c}; // 错误:括号中只能有单个初始化器
auto不能用于分配数组
虽然我们用空括号对数组中元素进行值初始化,但不能在括号中给出初始化器,这意味着不能用auto分配数组。
decltype
decltype与auto关键字一样,用于进行编译时类型推导,不过它与auto还是有一些区别的。decltype的类型推导并不是像auto一样是从变量声明的初始化表达式获得变量的类型,而是总是以一个普通表达式作为参数,返回该表达式的类型,而且decltype并不会对表达式进行求值。
- decltype:相对于auto用于推导变量类型,而decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。
decltype用法:
- 推导出表达式类型
// 推导出表达式类型
cont int &i = 1;
int a = 2;
decltype(i) b = 2; // b是const int&
int func() { return 0; }
decltype(func()) i; // i为int类型
int x = 0;
decltype(x) y; // y是int类型
decltype(x + y) z; // z是int类型
- 与using/typedef合用,用于定义类型
// 与using/typedef合用,用于定义类型。
using size_t = decltype(sizeof(0));//sizeof(a)的返回值为size_t类型
using ptrdiff_t = decltype((int*)0 - (int*)0);
using nullptr_t = decltype(nullptr);
vector<int >vec;
typedef decltype(vec.begin()) vectype;
for (vectype i = vec.begin; i != vec.end(); i++)
{
//...
}
- 重用匿名类型
在C++中,我们有时候会遇上一些匿名类型,如:
struct
{
int d ;
doubel b;
}anon_s;
而借助decltype,我们可以重新使用这个匿名的结构体:
decltype(anon_s) as ;//定义了一个上面匿名的结构体
- 泛型编程中结合auto,用于追踪函数的返回值类型
这也是decltype最大的用途了。
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty)
{
return x*y;
}
注意:decltype不会像auto一样忽略引用和cv属性,decltype会保留表达式的引用和cv属性
const int &i = 1;
int a = 2;
decltype(i) b = 2; // b是const int&
decltype推导规则
对于decltype(exp)有
- exp是表达式,decltype(exp)和exp类型相同
- exp是函数调用,decltype(exp)和函数返回值类型相同
- 其它情况,若exp是左值,decltype(exp)是exp类型的左值引用
int a = 0, b = 0;
decltype(a + b) c = 0; // c是int,因为(a+b)返回一个右值
decltype(a += b) d = c;// d是int&,因为(a+=b)返回一个左值
d = 20;
cout << "c " << c << endl; // 输出c 20
auto和decltype的配合使用
auto和decltype一般配合使用在推导函数返回值的类型问题上。
下面这段代码
template<typename T, typename U>
return_value add(T t, U u) { // t和v类型不确定,无法推导出return_value类型
return t + u;
}
上面代码由于t和u类型不确定,那如何推导出返回值类型呢,我们可能会想到这种
template<typename T, typename U>
decltype(t + u) add(T t, U u) { // t和u尚未定义
return t + u;
}
这段代码在C++11上是编译不过的,因为在decltype(t +u)推导时,t和u尚未定义,就会编译出错,所以有了下面的叫做返回类型后置的配合使用方法:
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
返回值后置类型语法就是为了解决函数返回制类型依赖于参数但却难以确定返回值类型的问题。
decltype(e)(其中e的类型为T)推导规则
- 如果exp是一个没有带括号的标记符表达式(结构化绑定除外)或者类成员访问表达式,那么的
decltype(e)推断出的类型就是e的类型T。此外,如果e是一个被重载的函数,则会导致编译错误。 - 如果e是一个函数调用或者仿函数调用,那么decltype(e)推断出的类型是其返回值的类型
- 如果e是一个类型为T的将亡值,那么decltype(e)为T&&。
- 如果e是一个类型为T的左值,那么decltype(e)为T&。
- 除去以上情况,decltype(e)为T。
标记符指的是除去关键字、字面量等编译器需要使用的标记之外的程序员自己定义的标记,而单个标记符对应的表达式即为标记符表达式。例如:
int arr[4]
则arr为一个标记符表达式,而arr[3]+0不是。
我们来看下面这段代码:
int i=10;
decltype(i) a; //a推导为int
decltype((i))b=i;//b推导为int&,必须为其初始化,否则编译错误
仅仅为i加上了(),就导致类型推导结果的差异。这是因为,i是一个标记符表达式,根据推导规则1,类型被推导为int。而(i)为一个左值表达式,所以类型被推导为int&。
通过下面这段代码可以对推导四个规则作进一步了解
int i = 4;
int arr[5] = { 0 };
int *ptr = arr;
struct S{ double d; }s ;
void Overloaded(int);
void Overloaded(char);//重载的函数
int && RvalRef();
const bool Func(int);
//规则一:推导为其类型
decltype (arr) var1; //int 标记符表达式
decltype (ptr) var2;//int * 标记符表达式
decltype(s.d) var3;//doubel 成员访问表达式
//decltype(Overloaded) var4;//重载函数。编译错误。
//规则二:将亡值。推导为类型的右值引用。
decltype (RvalRef()) var5 = 1;
//规则三:左值,推导为类型的引用。
decltype ((i))var6 = i; //int&
decltype (true ? i : i) var7 = i; //int& 条件表达式返回左值。
decltype (++i) var8 = i; //int& ++i返回i的左值。
decltype(arr[5]) var9 = i;//int&. []操作返回左值
decltype(*ptr)var10 = i;//int& *操作返回左值
decltype("hello")var11 = "hello"; //const char(&)[9] 字符串字面常量为左值,且为const左值。
//规则四:以上都不是,则推导为本类型
decltype(1) var12;//const int
decltype(Func(1)) var13=true;//const bool
decltype(i++) var14 = i;//int i++返回右值
这里需要提示的是,字符串字面值常量是个左值,且是const左值,而非字符串字面值常量则是个右值。
这么多规则,对于我们写代码的来说难免太难记了,特别是规则三。我们可以利用C++11标准库中添加的模板类is_lvalue_reference来判断表达式是否为左值:
cout << is_lvalue_reference<decltype(++i)>::value << endl;
结果1表示为左值,结果为0为非右值。
同样的,也有is_rvalue_reference这样的模板类来判断decltype推断结果是否为右值。
左值右值
众所周知C++11新增了右值引用,这里涉及到很多概念:
- 左值:可以取地址并且有名字的东西就是左值。
- 右值:不能取地址的没有名字的东西就是右值。
- 纯右值:运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。
- 将亡值:可以理解为即将要销毁的值。
- 左值引用:对左值进行引用的类型。
- 右值引用:对右值进行引用的类型。
- 移动语义:转移资源所有权,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用。
- 完美转发:可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参。
- 返回值优化:当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++标准允许省略调用这些复制构造函数。
左值、右值
概念1:
- 左值:可以放到等号左边的东西叫左值。
- 右值:不可以放到等号左边的东西就叫右值。
概念2:
- 左值:可以取地址并且有名字的东西就是左值。
- 右值:不能取地址的没有名字的东西就是右值。
举例:
int a = b + c;
a是左值,a有变量名,也可以取地址,可以放到等号左边, 表达式b+c的返回值是右值,没有名字且不能取地址,&(b+c)不能通过编译,而且也不能放到等号左边。
int a = 4; // a是左值,4作为普通字面量是右值
左值一般有:
- 函数名和变量名
- 返回左值引用的函数调用
- 前置自增自减表达式++i、--i
- 由赋值表达式或赋值运算符连接的表达式(a=b, a += b等)
- 解引用表达式*p
- 字符串字面值"abcd"
纯右值、将亡值
纯右值和将亡值都属于右值。
纯右值
运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。
举例:
- 除字符串字面值外的字面值
- 返回非引用类型的函数调用
- 后置自增自减表达式i++、i--
- 算术表达式(a+b, a*b, a&&b, a==b等)
- 取地址表达式等(&a)
将亡值
将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。
左值引用、右值引用
根据名字大概就可以猜到意思,左值引用就是对左值进行引用的类型,右值引用就是对右值进行引用的类型,他们都是引用,都是对象的一个别名,并不拥有所绑定对象的堆存,所以都必须立即初始化。
type &name = exp; // 左值引用
type &&name = exp; // 右值引用
左值引用
看代码:
int a = 5;
int &b = a; // b是左值引用
b = 4;
int &c = 10; // error,10无法取地址,无法进行引用
const int &d = 10; // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址
可以得出结论:对于左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用const引用形式,但这样就只能通过引用来读取输出,不能修改数组,因为是常量引用。
右值引用
如果使用右值引用,那表达式等号右边的值需要时右值,可以使用std::move函数强制把左值转换为右值。
通常情况下,static_cast只能用于其他合法的类型转换。但是,c++11有一条针对右值引用的特许规则:虽然不能隐式地将一个左值转换为右值引用,但我们可以用static_cast显式地将一个左值转换为一个右值引用。
int a = 4;
int &&b = a; // error, a是左值
int &&c = std::move(a); // ok
移动语义
谈移动语义前,我们首先需要了解深拷贝与浅拷贝的概念
深拷贝、浅拷贝
直接拿代码举例:
class A {
public:
A(int size) : size_(size) {
data_ = new int[size];
}
A(){}
A(const A& a) {
size_ = a.size_;
data_ = a.data_;
cout << "copy " << endl;
}
~A() {
delete[] data_;
}
int *data_;
int size_;
};
int main() {
A a(10);
A b = a;
cout << "b " << b.data_ << endl;
cout << "a " << a.data_ << endl;
return 0;
}
上面代码中,两个输出的是相同的地址,a和b的data_指针指向了同一块内存,这就是浅拷贝,只是数据的简单赋值,那再析构时data_内存会被释放两次,导致程序出问题,这里正常会出现double free导致程序崩溃的,但是不知道为什么我自己测试程序却没有崩溃,能力有限,没搞明白,无论怎样,这样的程序肯定是有隐患的,如何消除这种隐患呢,可以使用如下深拷贝:
class A {
public:
A(int size) : size_(size) {
data_ = new int[size];
}
A(){}
A(const A& a) {
size_ = a.size_;
data_ = new int[size_];
cout << "copy " << endl;
}
~A() {
delete[] data_;
}
int *data_;
int size_;
};
int main() {
A a(10);
A b = a;
cout << "b " << b.data_ << endl;
cout << "a " << a.data_ << endl;
return 0;
}
深拷贝就是再拷贝对象时,如果被拷贝对象内部还有指针引用指向其它资源,自己需要重新开辟一块新内存存储资源,而不是简单的赋值。
聊完了深拷贝浅拷贝我们可以聊聊移动语义啦:
移动语义,在程序喵看来可以理解为转移所有权,之前的拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,怎么利用移动语义呢,是通过移动构造函数。
class A {
public:
A(int size) : size_(size) {
data_ = new int[size];
}
A(){}
A(const A& a) {
size_ = a.size_;
data_ = new int[size_];
cout << "copy " << endl;
}
A(A&& a) {
this->data_ = a.data_;
a.data_ = nullptr;
cout << "move " << endl;
}
~A() {
if (data_ != nullptr) {
delete[] data_;
}
}
int *data_;
int size_;
};
int main() {
A a(10);
A b = a;
A c = std::move(a); // 调用移动构造函数
return 0;
}
如果不使用std::move(),会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提供程序性能,C++所有的STL都实现了移动语义,方便我们使用。例如:
std::vector<string> vecs;
...
std::vector<string> vecm = std::move(vecs); // 免去很多拷贝
注意:移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数。
移动构造函数和移动赋值
移动迭代器
C++11标准库中定义了一种移动迭代器(move iterator)适配器。一个移动迭代器通过改变给定迭代器的解引用运算符的行为来适配次迭代器。一般来说,一个迭代器的解引用运算符返回一个指向元素的左值。与其他迭代器不同,移动迭代器的解引用运算符生成一个右值引用。
完美转发——标准库forward函数
完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。那如何实现完美转发呢,答案是使用std::forward()。
void PrintV(int &t) {
cout << "lvalue" << endl;
}
void PrintV(int &&t) {
cout << "rvalue" << endl;
}
template<typename T>
void Test(T &&t) {
PrintV(t);
PrintV(std::forward<T>(t));
PrintV(std::move(t));
}
int main() {
Test(1); // lvalue rvalue rvalue
int a = 1;
Test(a); // lvalue lvalue rvalue
Test(std::forward<int>(a)); // lvalue rvalue rvalue
Test(std::forward<int&>(a)); // lvalue lvalue rvalue
Test(std::forward<int&&>(a)); // lvalue rvalue rvalue
return 0;
}
- Test(1):1是右值,模板中T &&t这种为万能引用,右值1传到Test函数中变成了右值引用,但是调用PrintV()时候,t变成了左值,因为它变成了一个拥有名字的变量,所以打印lvalue,而PrintV(std::forward(t))时候,会进行完美转发,按照原来的类型转发,所以打印rvalue,PrintV(std::move(t))毫无疑问会打印rvalue。
- Test(a):a是左值,模板中T &&这种为万能引用,左值a传到Test函数中变成了左值引用,所以有代码中打印。
- Test(std::forward(a)):转发为左值还是右值,依赖于T,T是左值那就转发为左值,T是右值那就转发为右值。
返回值优化
返回值优化(RVO)是一种C++编译优化技术,当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++标准允许省略调用这些复制构造函数。
那什么时候编译器会进行返回值优化呢?
- return的值类型与函数的返回值类型相同
- return的是一个局部对象
看几个例子:
示例1:
std::vector<int> return_vector(void) {
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> &&rval_ref = return_vector();
不会触发RVO,拷贝构造了一个临时的对象,临时对象的生命周期和rval_ref绑定,等价于下面这段代码:
const std::vector<int>& rval_ref = return_vector();
示例2:
std::vector<int>&& return_vector(void) {
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
这段代码会造成运行时错误,因为rval_ref引用了被析构的tmp。讲道理来说这段代码是错的,但我自己运行过程中却成功了,我没有那么幸运,这里不纠结,继续向下看什么时候会触发RVO。
示例3:
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return std::move(tmp);
}
std::vector<int> &&rval_ref = return_vector();
和示例1类似,std::move一个临时对象是没有必要的,也会忽略掉返回值优化。
最好的代码:
std::vector<int> return_vector(void)
{
std::vector<int> tmp {1,2,3,4,5};
return tmp;
}
std::vector<int> rval_ref = return_vector();
这段代码会触发RVO,不拷贝也不移动,不生成临时对象。
用移动类对象代替拷贝类对象
用大括号包围的值列表赋值
C++11允许使用花括号括起来的初始值列表作为复制语句的右侧运算对象:
vector<int> vi;
vi = {1,2,3,4};
列表初始化
C++11新增了列表初始化的概念。
在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。
struct A {
public:
A(int) {}
private:
A(const A&) {}
};
int main() {
A a(123);
A b = 123; // error
A c = { 123 };
A d{123}; // c++11
int e = {123};
int f{123}; // c++11
return 0;
}
列表初始化也可以用在函数的返回值上
std::vector<int> func() {
return {};
}
列表初始化的一些规则:
首先说下聚合类型可以进行直接列表初始化,这里需要了解什么是聚合类型:
- 类型是一个普通数组,如int[5],char[],double[]等
- 类型是一个类,且满足以下条件:
- 没有用户声明的构造函数
- 没有用户提供的构造函数(允许显示预置或弃置的构造函数)
- 没有私有或保护的非静态数据成员
- 没有基类
- 没有虚函数
- 没有{}和=直接初始化的非静态数据成员
- 没有默认成员初始化器
struct A {
int a;
int b;
int c;
A(int, int){}
};
int main() {
A a{1, 2, 3};// error,A有自定义的构造函数,不能列表初始化
}
上述代码类A不是聚合类型,无法进行列表初始化,必须以自定义的构造函数来构造对象。
struct A {
int a;
int b;
virtual void func() {} // 含有虚函数,不是聚合类
};
struct Base {};
struct B : public Base { // 有基类,不是聚合类
int a;
int b;
};
struct C {
int a;
int b = 10; // 有等号初始化,不是聚合类
};
struct D {
int a;
int b;
private:
int c; // 含有私有的非静态数据成员,不是聚合类
};
struct E {
int a;
int b;
E() : a(0), b(0) {} // 含有默认成员初始化器,不是聚合类
};
上面列举了一些不是聚合类的例子,对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值;对于非聚合类型,需要先自定义一个对应的构造函数,此时列表初始化将调用相应的构造函数。
std::initializer_list
我们平时开发使用STL过程中可能发现它的初始化列表可以是任意长度,大家有没有想过它是怎么实现的呢,答案是std::initializer_list,看下面这段示例代码:
struct CustomVec {
std::vector<int> data;
CustomVec(std::initializer_list<int> list) {
for (auto iter = list.begin(); iter != list.end(); ++iter) {
data.push_back(*iter);
}
}
};
我想通过上面这段代码大家可能已经知道STL是如何实现的任意长度初始化了吧,这个std::initializer_list其实也可以作为函数参数。
注意:std::initializer_list,它可以接收任意长度的初始化列表,但是里面必须是相同类型T,或者都可以转换为T。
列表初始化的好处
个人认为列表初始化的好处如下:
- 方便,且基本上可以替代括号初始化
- 可以使用初始化列表接受任意长度
- 可以防止类型窄化,避免精度丢失的隐式类型转换(如果我们使用列表初始化且初始值存在丢失信息的风险,则编译器将报错)
什么是类型窄化,列表初始化通过禁止下列转换,对隐式转化加以限制:
- 从浮点类型到整数类型的转换
- 从 long double 到 double 或 float 的转换,以及从 double 到 float 的转换,除非源是常量表达式且不发生溢出
- 从整数类型到浮点类型的转换,除非源是其值能完全存储于目标类型的常量表达式
- 从整数或无作用域枚举类型到不能表示原类型所有值的整数类型的转换,除非源是其值能完全存储于目标类型的常量表达式
示例:
int main() {
int a = 1.2; // ok
int b = {1.2}; // error
float c = 1e70; // ok
float d = {1e70}; // error
float e = (unsigned long long)-1; // ok
float f = {(unsigned long long)-1}; // error
float g = (unsigned long long)1; // ok
float h = {(unsigned long long)1}; // ok
const int i = 1000;
const int j = 2;
char k = i; // ok
char l = {i}; // error
char m = j; // ok
char m = {j}; // ok,因为是const类型,这里如果去掉const属性,也会报错
}
打印如下:
test.cc:24:17: error: narrowing conversion of ‘1.2e+0’ from ‘double’ to ‘int’ inside { } [-Wnarrowing]
int b = {1.2};
^
test.cc:30:38: error: narrowing conversion of ‘18446744073709551615’ from ‘long long unsigned int’ to ‘float’ inside { } [-Wnarrowing]
float f = {(unsigned long long)-1};
^
test.cc:36:14: warning: overflow in implicit constant conversion [-Woverflow]
char k = i;
^
test.cc:37:16: error: narrowing conversion of ‘1000’ from ‘int’ to ‘char’ inside { } [-Wnarrowing]
char l = {i};
关联容器的列表初始化
列表初始化pair的返回类型,pair的列表初始化
c++11下,返回一个pair的函数中,我们可以对返回值进行列表初始化。
pair<string int>
process(vector<string> &v)
{
if(!v.empty())
return {v.back(), v.back().size())}; // 列表初始化
else
return pair<string, int>(); //隐式构造返回值
}
动态分配对象的列表初始化
vector<int> *pv = new vector<int>{0,1,2,3,4};
动态分配数组的列表初始化
// 4个int分别用列表中对应的初始化器初始化
int *pia3 = new int[4]{0,1,2,3};
// 4个string,前2个用给定的初始化器初始化,剩下的进行值初始化
string *psa2 = new string[4]{"a", "an"};
std::function & std::bind & lambda表达式
c++11新增了std::function、std::bind、lambda表达式等封装使函数调用更加方便.
std::function类模板
std::function 是一个模板。
讲std::function前首先需要了解下什么是可调用对象
满足以下条件之一就可称为可调用对象:
- 是一个函数指针
- 是一个具有operator()成员函数的类对象(传说中的仿函数),lambda表达式
- 是一个可被转换为函数指针的类对象
- 是一个类成员(函数)指针
- bind表达式或其它函数对象
而std::function就是上面这种可调用对象的封装器,可以把std::function看做一个函数对象,用于表示函数这个抽象概念。std::function的实例可以存储、复制和调用任何可调用对象,存储的可调用对象称为std::function的目标,若std::function不含目标,则称它为空,调用空的std::function的目标会抛出std::bad_function_call异常。
使用参考如下实例代码:
std::function<void(int)> f; // 这里表示function的对象f的参数是int,返回值是void
#include <functional>
#include <iostream>
struct Foo {
Foo(int num) : num_(num) {}
void print_add(int i) const { std::cout << num_ + i << '\n'; }
int num_;
};
void print_num(int i) { std::cout << i << '\n'; }
struct PrintNum {
void operator()(int i) const { std::cout << i << '\n'; }
};
int main() {
// 存储自由函数
std::function<void(int)> f_display = print_num;
f_display(-9);
// 存储 lambda
std::function<void()> f_display_42 = []() { print_num(42); };
f_display_42();
// 存储到 std::bind 调用的结果
std::function<void()> f_display_31337 = std::bind(print_num, 31337);
f_display_31337();
// 存储到成员函数的调用
std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
const Foo foo(314159);
f_add_display(foo, 1);
f_add_display(314159, 1);
// 存储到数据成员访问器的调用
std::function<int(Foo const&)> f_num = &Foo::num_;
std::cout << "num_: " << f_num(foo) << '\n';
// 存储到成员函数及对象的调用
using std::placeholders::_1;
std::function<void(int)> f_add_display2 = std::bind(&Foo::print_add, foo, _1);
f_add_display2(2);
// 存储到成员函数和对象指针的调用
std::function<void(int)> f_add_display3 = std::bind(&Foo::print_add, &foo, _1);
f_add_display3(3);
// 存储到函数对象的调用
std::function<void(int)> f_display_obj = PrintNum();
f_display_obj(18);
}
从上面可以看到std::function的使用方法,当给std::function填入合适的参数表和返回值后,它就变成了可以容纳所有这一类调用方式的函数封装器。std::function还可以用作回调函数,或者在C++里如果需要使用回调那就一定要使用std::function,特别方便。
std::bind
使用std::bind可以将可调用对象和参数一起绑定,绑定后的结果使用std::function进行保存,并延迟调用到任何我们需要的时候。
可以将bind函数看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表
auto newCallable = bind(callable, arg_list);
其中, newCallable本身是一个可调用对象,arg_list是一个逗号分割的参数列表,对应给定的callable的参数。即,当我们调用newCallable时,newCallable会调用callable,并传给他arg_list中的参数。
std::bind通常有两大作用:
- 将可调用对象与参数一起绑定为另一个std::function供调用
- 将n元可调用对象转成m(m < n)元可调用对象,绑定一部分参数,这里需要使用std::placeholders
具体示例:
#include <functional>
#include <iostream>
#include <memory>
void f(int n1, int n2, int n3, const int& n4, int n5) {
std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << std::endl;
}
int g(int n1) { return n1; }
struct Foo {
void print_sum(int n1, int n2) { std::cout << n1 + n2 << std::endl; }
int data = 10;
};
int main() {
using namespace std::placeholders; // 针对 _1, _2, _3...
// 演示参数重排序和按引用传递
int n = 7;
// ( _1 与 _2 来自 std::placeholders ,并表示将来会传递给 f1 的参数)
auto f1 = std::bind(f, _2, 42, _1, std::cref(n), n);
n = 10;
f1(1, 2, 1001); // 1 为 _1 所绑定, 2 为 _2 所绑定,不使用 1001
// 进行到 f(2, 42, 1, n, 7) 的调用
// 嵌套 bind 子表达式共享占位符
auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
f2(10, 11, 12); // 进行到 f(12, g(12), 12, 4, 5); 的调用
// 绑定指向成员函数指针
Foo foo;
auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
f3(5);
// 绑定指向数据成员指针
auto f4 = std::bind(&Foo::data, _1);
std::cout << f4(foo) << std::endl;
// 智能指针亦能用于调用被引用对象的成员
std::cout << f4(std::make_shared<Foo>(foo)) << std::endl;
}
lambda表达式
lambda表达式可以说是c++11引用的最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:
auto func = [capture] (params) opt -> ret { func_body; };
其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable之类), ret是返回值类型,func_body是函数体。
一个完整的lambda表达式:
auto func1 = [](int a) -> int { return a + 1; };
auto func2 = [](int a) { return a + 2; };
cout << func1(1) << " " << func2(2) << endl;
如上代码,很多时候lambda表达式返回值是很明显的,c++11允许省略表达式的返回值定义。
lambda表达式允许捕获一定范围内的变量:
[]不捕获任何变量[&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用[=]值捕获,捕获外部作用域所有变量,在函数内内有个副本使用[=, &a]值捕获外部作用域所有变量,按引用捕获a变量[a]只值捕获a变量,不捕获其它变量[this]捕获当前类中的this指针
lambda表达式示例代码:
int a = 0;
auto f1 = [=](){ return a; }; // 值捕获a
cout << f1() << endl;
auto f2 = [=]() { return a++; }; // 修改按值捕获的外部变量,error
auto f3 = [=]() mutable { return a++; };
代码中的f2是编译不过的,因为我们修改了按值捕获的外部变量,其实lambda表达式就相当于是一个仿函数,仿函数是一个有operator()成员函数的类对象,这个operator()默认是const的,所以不能修改成员变量,而加了mutable,就是去掉const属性。
还可以使用lambda表达式自定义stl的规则,例如自定义sort排序规则:
struct A {
int a;
int b;
};
int main() {
vector<A> vec;
std::sort(vec.begin(), vec.end(), [](const A &left, const A &right) { return left.a < right.a; });
}
总结
std::function和std::bind使得我们平时编程过程中封装函数更加的方便,而lambda表达式将这种方便发挥到了极致,可以在需要的时间就地定义匿名函数,不再需要定义类或者函数等,在自定义STL规则时候也非常方便,让代码更简洁,更灵活,提高开发效率。
模板的改进
C++11关于模板有一些细节的改进:
- 模板的右尖括号
- 模板的别名
- 函数模板的默认模板参数
模板的右尖括号
C++11之前是不允许两个右尖括号(>>)出现的,会被认为是右移操作符,所以需要中间加个空格进行分割,避免发生编译错误。
int main() {
std::vector<std::vector<int>> a; // error
std::vector<std::vector<int> > b; // ok
}
在实例化模板时会出现连续两个右尖括号,同样static_cast、dynamic_cast、reinterpret_cast、const_cast表达式转换时也会遇到相同的情况。C++98标准是让程序员在>>之间填上一个空格,在C++11中,这种限制被取消了。在C++11标准中,要求编译器对模板的右尖括号做单独处理,使编译器能够正确判断出>>是一个右移操作符还是模板参数表的结束标记。
模板的别名
C++11引入了using,可以轻松的定义别名,而不是使用繁琐的typedef。
#include <iostream>
#include <type_traits> //std::is_same
using namespace std;
using uint = unsigned int;
typedef unsigned int UINT;
using sint = int;
int main()
{
//std::is_same 判断类型是否一致
//这个结构体作用很简单,就是两个一样的类型会返回true
cout << is_same<uint, UINT>::value << endl; // 1
return 0;
}
typedef std::vector<std::vector<int>> vvi; // before c++11
using vvi = std::vector<std::vector<int>>; // c++11
template<class T>
struct Alloc { };
template<class T>
using Vec = vector<T, Alloc<T>>; // 类型标识为 vector<T, Alloc<T>>
Vec<int> v; // Vec<int> 同 vector<int, Alloc<int>>
typedef void (*func)(int, int);
using func = void (*)(int, int);
template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors是一个pair<string, string>
声明模板类型形参为友元
在C++11中,我们可以将模板类型参数声明为友元:
template <typename Type> class Bar {
friend Type; // 将访问权限授予用来实例化Bar的类型
// ...
};
值得注意的是,虽然友元通常来说应该是一个类或者是一个函数,但是我们完全可以用一个内置类型来实例化Bar.这种与内置类型的友好关系是允许的,以便我们能用内置类型来实例化Bar这样的类。
实例化的显示控制
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在c++11标准中,我们可以通过显式实例化(explicit instantiation)来避免这种开销。
extern template declaration; // 实例化声明
template declaration; // 实例化定义
declaration 是一个类或函数声明,其中所有的模板参数已被替换成模板实参。例如
// 实例化声明与定义
extern template class Blob<string>; // 声明
template int compare(const int&, const int&); // 定义
当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序的其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用此实例化版本的代码之前。
函数模板的默认模板参数
C++11之前只有类模板支持默认模板参数,函数模板是不支持默认模板参数的,C++11后都支持。
//1、普通函数带默认参数,c++98 编译通过,c++11 编译通过
void DefParm(int m = 3) {}
//2、类模板是支持默认的模板参数,c++98 编译通过,c++11 编译通过
template <typename T = int>
class DefClass {};
//3、函数模板的默认模板参数, c++98 - 编译失败,c++11 - 编译通过
template <typename T = int> void DefTempParm() {}
template <typename T, typename U=int>
class A {
T value;
};
template <typename T=int, typename U> // error
class A {
T value;
};
类模板的默认模板参数必须从右往左定义,而函数模板则没有这个限制。
template<class T1, class T2 = int> class DefClass1;
template<class T1 = int, class T2> class DefClass2; // 无法通过编译
template<class T, int i = 0> class DefClass3;
template<int i = 0, class T> class DefClass4; // 无法通过编译
template<class T1 = int, class T2> void DefFunc1(T1 a, T2 b);
template<int i = 0, class T> void DefFunc2(T a);
template <typename R, typename U=int>
R func1(U val) {
return val;
}
template <typename R=int, typename U>
R func2(U val) {
return val;
}
int main() {
cout << func1<int, double>(99.9) << endl; // 99
cout << func1<double, double>(99.9) << endl; // 99.9
cout << func1<double>(99.9) << endl; // 99.9
cout << func1<int>(99.9) << endl; // 99
cout << func2<int, double>(99.9) << endl; // 99
cout << func1<double, double>(99.9) << endl; // 99.9
cout << func2<double>(99.9) << endl; // 99.9
cout << func2<int>(99.9) << endl; // 99
return 0;
}
对于函数模板,参数的填充顺序是从左到右的。
可变参数模板
一个可变参数模板(variadic template)就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包(parameter packet)。存在两种参数包:模板参数包(template parameter packet),表示零个或多个模板参数;函数参数包(function parameter packet),表示零个或多个函数参数。
C++11支持变长参数模板:
template <typename T>
void func(const T& t){
cout << t << '\n';
}
template <typename T, typename ... Args>
void func(const T& t, Args ... args){
cout << t << ',';
func(args...);
}
sizeof...运算符
当我们需要知道包中有多少元素时,可以使用sizeof...运算符。类似sizeof,sizeof...也返回一个常量表达式,而且不会对其实参求值。
template<typename ... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; // 类型参数的数目
cout << sizeof...(args) << endl; // 函数参数的数目
}
可变参数模板与转发
在c++11中,我们可以组合使用可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数。
可变参数函数通常将他们的参数转发给其他函数,这种函数通常具有与我们的emplace_back函数一样的形式:
// fun 有零个或多个参数,每个参数都是一个模板参数类型的右值引用
template<typename... Args>
void fun(Args&&... args) // 将Args扩展为一个右值引用的列表
{
// work的实参既扩展Args又扩展args
work(std::forward<Args>(args)...);
}
这里我们希望将fun的所有实参转发给另一个名为work的函数,假定由他完成函数的实际工作。类似emplace_back中对construct的调用,work调用中的扩展既扩展了模板参数包也扩展了函数参数包。
由于fun的参数是右值引用,因此我们可以传递给他任意类型的实参;由于我们使用std::forward传递这些实参,因此他们的所有类型信息在调用work时都会得到保持。
并发
c++11关于并发引入了好多好东西,有:
- std::thread相关
- std::mutex相关
- std::lock相关
- std::atomic相关
- std::call_once相关
- volatile相关
- std::condition_variable相关
- std::future相关
- async相关
std::thread相关
c++11之前你可能使用pthread_xxx来创建线程,繁琐且不易读,c++11引入了std::thread来创建线程,支持对线程join或者detach。直接看代码:
#include <iostream>
#include <thread>
using namespace std;
int main() {
auto func = []() {
for (int i = 0; i < 10; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread t(func);
if (t.joinable()) {
t.detach();
}
auto func1 = [](int k) {
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread tt(func1, 20);
if (tt.joinable()) { // 检查线程可否被join
tt.join();
}
return 0;
}
上述代码中,函数func和func1运行在线程对象t和tt中,从刚创建对象开始就会新建一个线程用于执行函数,调用join函数将会阻塞主线程,直到线程函数执行结束,线程函数的返回值将会被忽略。如果不希望线程被阻塞执行,可以调用线程对象的detach函数,表示将线程和线程对象分离。
如果没有调用join或者detach函数,假如线程函数执行时间较长,此时线程对象的生命周期结束调用析构函数清理资源,这时可能会发生错误,这里有两种解决办法,一个是调用join(),保证线程函数的生命周期和线程对象的生命周期相同,另一个是调用detach(),将线程和线程对象分离,这里需要注意,如果线程已经和对象分离,那我们就再也无法控制线程什么时候结束了,不能再通过join来等待线程执行完。
这里可以对thread进行封装,避免没有调用join或者detach可导致程序出错的情况出现:
class ThreadGuard {
public:
enum class DesAction { join, detach };
ThreadGuard(std::thread&& t, DesAction a) : t_(std::move(t)), action_(a){};
~ThreadGuard() {
if (t_.joinable()) {
if (action_ == DesAction::join) {
t_.join();
} else {
t_.detach();
}
}
}
ThreadGuard(ThreadGuard&&) = default;
ThreadGuard& operator=(ThreadGuard&&) = default;
std::thread& get() { return t_; }
private:
std::thread t_;
DesAction action_;
};
int main() {
ThreadGuard t(std::thread([]() {
for (int i = 0; i < 10; ++i) {
std::cout << "thread guard " << i << " ";
}
std::cout << std::endl;}), ThreadGuard::DesAction::join);
return 0;
}
c++11还提供了获取线程id,或者系统cpu个数,获取thread native_handle,使得线程休眠等功能
std::thread t(func);
cout << "当前线程ID " << t.get_id() << endl;
cout << "当前cpu个数 " << std::thread::hardware_concurrency() << endl;
auto handle = t.native_handle();// handle可用于pthread相关操作
std::this_thread::sleep_for(std::chrono::seconds(1));
std::mutex相关
std::mutex是一种线程同步的手段,用于保存多线程同时操作的共享数据。
mutex分为四种:
- std::mutex:独占的互斥量,不能递归使用,不带超时功能
- std::recursive_mutex:递归互斥量,可重入,不带超时功能
- std::timed_mutex:带超时的互斥量,不能递归
- std::recursive_timed_mutex:带超时的互斥量,可以递归使用
拿一个std::mutex和std::timed_mutex举例吧,别的都是类似的使用方式:
std::mutex:
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
std::mutex mutex_;
int main() {
auto func1 = [](int k) {
mutex_.lock();
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
mutex_.unlock();
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
std::timed_mutex:
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
using namespace std;
std::timed_mutex timed_mutex_;
int main() {
auto func1 = [](int k) {
timed_mutex_.try_lock_for(std::chrono::milliseconds(200));
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
timed_mutex_.unlock();
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
std::lock相关
这里主要介绍两种RAII方式的锁封装,可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。
c++11主要有std::lock_guard和std::unique_lock两种方式,使用方式都类似,如下:
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>
using namespace std;
std::mutex mutex_;
int main() {
auto func1 = [](int k) {
// std::lock_guard<std::mutex> lock(mutex_);
std::unique_lock<std::mutex> lock(mutex_);
for (int i = 0; i < k; ++i) {
cout << i << " ";
}
cout << endl;
};
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(func1, 200);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
std::lock_gurad相比于std::unique_lock更加轻量级,少了一些成员函数,std::unique_lock类有unlock函数,可以手动释放锁,所以条件变量都配合std::unique_lock使用,而不是std::lock_guard,因为条件变量在wait时需要有手动释放锁的能力,具体关于条件变量后面会讲到。
std::atomic相关
c++11提供了原子类型std::atomic,理论上这个T可以是任意类型,但是我平时只存放整形,别的还真的没用过,整形有这种原子变量已经足够方便,就不需要使用std::mutex来保护该变量啦。看一个计数器的代码:
struct OriginCounter { // 普通的计数器
int count;
std::mutex mutex_;
void add() {
std::lock_guard<std::mutex> lock(mutex_);
++count;
}
void sub() {
std::lock_guard<std::mutex> lock(mutex_);
--count;
}
int get() {
std::lock_guard<std::mutex> lock(mutex_);
return count;
}
};
struct NewCounter { // 使用原子变量的计数器
std::atomic<int> count;
void add() {
++count;
// count.store(++count);这种方式也可以
}
void sub() {
--count;
// count.store(--count);
}
int get() {
return count.load();
}
};
std::call_once相关
c++11提供了std::call_once来保证某一函数在多线程环境中只调用一次,它需要配合std::once_flag使用,直接看使用代码吧:
std::once_flag onceflag;
void CallOnce() {
std::call_once(onceflag, []() {
cout << "call once" << endl;
});
}
int main() {
std::thread threads[5];
for (int i = 0; i < 5; ++i) {
threads[i] = std::thread(CallOnce);
}
for (auto& th : threads) {
th.join();
}
return 0;
}
volatile相关
貌似把volatile放在并发里介绍不太合适,但是貌似很多人都会把volatile和多线程联系在一起,那就一起介绍下吧。
volatile通常用来建立内存屏障,volatile修饰的变量,编译器对访问该变量的代码通常不再进行优化,看下面代码:
int *p = xxx;
int a = *p;
int b = *p;
a和b都等于p指向的值,一般编译器会对此做优化,把*p的值放入寄存器,就是传说中的工作内存(不是主内存),之后a和b都等于寄存器的值,但是如果中间p地址的值改变,内存上的值改变啦,但a,b还是从寄存器中取的值(不一定,看编译器优化结果),这就不符合需求,所以在此对p加volatile修饰可以避免进行此类优化。
注意:volatile不能解决多线程安全问题,针对特种内存才需要使用volatile,它和atomic的特点如下:
- std::atomic用于多线程访问的数据,且不用互斥量,用于并发编程中
- volatile用于读写操作不可以被优化掉的内存,用于特种内存中
std::condition_variable相关
条件变量是c++11引入的一种同步机制,它可以阻塞一个线程或者个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程,条件变量需要和锁配合使用,这里的锁就是上面介绍的std::unique_lock。
这里使用条件变量实现一个CountDownLatch:
class CountDownLatch {
public:
explicit CountDownLatch(uint32_t count) : count_(count);
void CountDown() {
std::unique_lock<std::mutex> lock(mutex_);
--count_;
if (count_ == 0) {
cv_.notify_all();
}
}
void Await(uint32_t time_ms = 0) {
std::unique_lock<std::mutex> lock(mutex_);
while (count_ > 0) {
if (time_ms > 0) {
cv_.wait_for(lock, std::chrono::milliseconds(time_ms));
} else {
cv_.wait(lock);
}
}
}
uint32_t GetCount() const {
std::unique_lock<std::mutex> lock(mutex_);
return count_;
}
private:
std::condition_variable cv_;
mutable std::mutex mutex_;
uint32_t count_ = 0;
};
关于条件变量其实还涉及到通知丢失和虚假唤醒问题,因为不是本文的主题,这里暂不介绍
std::future相关
c++11关于异步操作提供了future相关的类,主要有std::future、std::promise和std::packaged_task,std::future比std::thread高级些,std::future作为异步结果的传输通道,通过get()可以很方便的获取线程函数的返回值,std::promise用来包装一个值,将数据和future绑定起来,而std::packaged_task则用来包装一个调用对象,将函数和future绑定起来,方便异步调用。而std::future是不可以复制的,如果需要复制放到容器中可以使用std::shared_future。
std::promise与std::future配合使用
#include <functional>
#include <future>
#include <iostream>
#include <thread>
using namespace std;
void func(std::future<int>& fut) {
int x = fut.get();
cout << "value: " << x << endl;
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(func, std::ref(fut));
prom.set_value(144);
t.join();
return 0;
}
std::packaged_task与std::future配合使用
#include <functional>
#include <future>
#include <iostream>
#include <thread>
using namespace std;
int func(int in) {
return in + 1;
}
int main() {
std::packaged_task<int(int)> task(func);
std::future<int> fut = task.get_future();
std::thread(std::move(task), 5).detach();
cout << "result " << fut.get() << endl;
return 0;
}
三者之间的关系
std::future用于访问异步操作的结果,而std::promise和std::packaged_task在future高一层,它们内部都有一个future,promise包装的是一个值,packaged_task包装的是一个函数,当需要获取线程中的某个值,可以使用std::promise,当需要获取线程函数返回值,可以使用std::packaged_task。
async相关
async是比future,packaged_task,promise更高级的东西,它是基于任务的异步操作,通过async可以直接创建异步的任务,返回的结果会保存在future中,不需要像packaged_task和promise那么麻烦,关于线程操作应该优先使用async,看一段使用代码:
#include <functional>
#include <future>
#include <iostream>
#include <thread>
using namespace std;
int func(int in) { return in + 1; }
int main() {
auto res = std::async(func, 5);
// res.wait();
cout << res.get() << endl; // 阻塞直到函数返回
return 0;
}
使用async异步执行函数是不是方便多啦。
async具体语法如下:
async(std::launch::async | std::launch::deferred, func, args...);
第一个参数是创建策略:
- std::launch::async表示任务执行在另一线程
- std::launch::deferred表示延迟执行任务,调用get或者wait时才会执行,不会创建线程,惰性执行在当前线程。
如果不明确指定创建策略,以上两个都不是async的默认策略,而是未定义,它是一个基于任务的程序设计,内部有一个调度器(线程池),会根据实际情况决定采用哪种策略。
若从 std::async 获得的 std::future 未被移动或绑定到引用,则在完整表达式结尾, std::future的析构函数将阻塞直至异步计算完成,实际上相当于同步操作:
std::async(std::launch::async, []{ f(); }); // 临时量的析构函数等待 f()
std::async(std::launch::async, []{ g(); }); // f() 完成前不开始
注意:关于async启动策略这里网上和各种书籍介绍的五花八门,这里会以cppreference为主。
有时候我们如果想真正执行异步操作可以对async进行封装,强制使用std::launch::async策略来调用async。
template <typename F, typename... Args>
inline auto ReallyAsync(F&& f, Args&&... params) {
return std::async(std::launch::async, std::forward<F>(f), std::forward<Args>(params)...);
}
智能指针
很多人谈到c++,说它特别难,可能有一部分就是因为c++的内存管理吧,不像java那样有虚拟机动态的管理内存,在程序运行过程中可能就会出现内存泄漏,然而这种问题其实都可以通过c++11引入的智能指针来解决,相反我还认为这种内存管理还是c++语言的优势,因为尽在掌握。
c++11引入了三种智能指针:
std::shared_ptrstd::weak_ptrstd::unique_ptr
shared_ptr
shared_ptr使用了引用计数,每一个shared_ptr的拷贝都指向相同的内存,每次拷贝都会触发引用计数+1,每次生命周期结束析构的时候引用计数-1,在最后一个shared_ptr析构的时候,内存才会释放。
使用方法如下:
struct ClassWrapper {
ClassWrapper() {
cout << "construct" << endl;
data = new int[10];
}
~ClassWrapper() {
cout << "deconstruct" << endl;
if (data != nullptr) {
delete[] data;
}
}
void Print() {
cout << "print" << endl;
}
int* data;
};
void Func(std::shared_ptr<ClassWrapper> ptr) {
ptr->Print();
}
int main() {
auto smart_ptr = std::make_shared<ClassWrapper>();
auto ptr2 = smart_ptr; // 引用计数+1
ptr2->Print();
Func(smart_ptr); // 引用计数+1
smart_ptr->Print();
ClassWrapper *p = smart_ptr.get(); // 可以通过get获取裸指针
p->Print();
return 0;
}
智能指针还可以自定义删除器,在引用计数为0的时候自动调用删除器来释放对象的内存,代码如下:
std::shared_ptr<int> ptr(new int, [](int *p){ delete p; });
关于shared_ptr有几点需要注意:
- 不要用一个裸指针初始化多个
shared_ptr,会出现double_free导致程序崩溃 - 通过
shared_from_this()返回this指针,不要把this指针作为shared_ptr返回出来,因为this指针本质就是裸指针,通过this返回可能 会导致重复析构,不能把this指针交给智能指针管理。
class A {
shared_ptr<A> GetSelf() {
return shared_from_this();
// return shared_ptr<A>(this); 错误,会导致double free
}
};
- 尽量使用
make_shared,少用new。 - 不要delete get()返回来的裸指针。
- 不是new出来的空间要自定义删除器。
- 要避免循环引用,循环引用导致内存永远不会被释放,造成内存泄漏。
using namespace std;
struct A;
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() {
cout << "A delete" << endl;
}
};
struct B {
std::shared_ptr<A> aptr;
~B() {
cout << "B delete" << endl;
}
};
int main() {
auto aaptr = std::make_shared<A>();
auto bbptr = std::make_shared<B>();
aaptr->bptr = bbptr;
bbptr->aptr = aaptr;
return 0;
}
上面代码,产生了循环引用,导致aptr和bptr的引用计数为2,离开作用域后aptr和bptr的引用计数-1,但是永远不会为0,导致指针永远不会析构,产生了内存泄漏,如何解决这种问题呢,答案是使用weak_ptr。
weak_ptr
weak_ptr是用来监视shared_ptr的生命周期,它不管理shared_ptr内部的指针,它的拷贝的析构都不会影响引用计数,纯粹是作为一个旁观者监视shared_ptr中管理的资源是否存在,可以用来返回this指针和解决循环引用问题。
- 作用1:返回this指针,上面介绍的shared_from_this()其实就是通过weak_ptr返回的this指针,这里参考我之前写的源码分析shared_ptr实现的文章,最后附上链接。
- 作用2:解决循环引用问题。
struct A;
struct B;
struct A {
std::shared_ptr<B> bptr;
~A() {
cout << "A delete" << endl;
}
void Print() {
cout << "A" << endl;
}
};
struct B {
std::weak_ptr<A> aptr; // 这里改成weak_ptr
~B() {
cout << "B delete" << endl;
}
void PrintA() {
if (!aptr.expired()) { // 监视shared_ptr的生命周期
auto ptr = aptr.lock();
ptr->Print();
}
}
};
int main() {
auto aaptr = std::make_shared<A>();
auto bbptr = std::make_shared<B>();
aaptr->bptr = bbptr;
bbptr->aptr = aaptr;
bbptr->PrintA();
return 0;
}
输出:
A
A delete
B delete
unique_ptr
std::unique_ptr是一个独占型的智能指针,它不允许其它智能指针共享其内部指针,也不允许unique_ptr的拷贝和赋值。使用方法和shared_ptr类似,区别是不可以拷贝:
using namespace std;
struct A {
~A() {
cout << "A delete" << endl;
}
void Print() {
cout << "A" << endl;
}
};
int main() {
auto ptr = std::unique_ptr<A>(new A);
auto tptr = std::make_unique<A>(); // error, c++11还不行,需要c++14
std::unique_ptr<A> tem = ptr; // error, unique_ptr不允许移动
ptr->Print();
return 0;
}
unique_ptr也可以像shared_ptr一样自定义删除器,使用方法和shared_ptr相同。
基于范围的for循环
直接看代码
vector<int> vec;
for (auto iter = vec.begin(); iter != vec.end(); iter++) { // before c++11
cout << *iter << endl;
}
for (int i : vec) { // c++11基于范围的for循环
cout << "i" << endl;
}
范围for语句不能用于动态分配数组
由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin和end.这些函数使用数组维度来返回指向首元素和尾后元素的指针。处于相同的原因,也不能使用范围for语句来处理(所谓的)动态数组中的元素。
委托构造函数(delegating constructor)
委托构造函数允许在同一个类中一个构造函数调用另外一个构造函数,可以在变量初始化时简化操作,通过代码来感受下委托构造函数的妙处吧:
不使用委托构造函数:
struct A {
A(){}
A(int a) { a_ = a; }
A(int a, int b) { // 好麻烦
a_ = a;
b_ = b;
}
A(int a, int b, int c) { // 好麻烦
a_ = a;
b_ = b;
c_ = c;
}
int a_;
int b_;
int c_;
};
使用委托构造函数:
struct A {
A(){}
A(int a) { a_ = a; }
A(int a, int b) : A(a) { b_ = b; }
A(int a, int b, int c) : A(a, b) { c_ = c; }
int a_;
int b_;
int c_;
};
初始化变量是不是方便了许多。
allocator::construct可使用任意构造函数
在c++11中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器。
auto q = p; // q指向最后构造的元素之后的位置
alloc.construct(q++); // *q为空字符串
alloc.construct(q++, 10, 'c'); // *q为cccccccccc
alloc.construct(q++, "hi"); // *q为hi
继承构造函数
继承构造函数可以让派生类直接使用基类的构造函数,如果有一个派生类,我们希望派生类采用和基类一样的构造方式,可以直接使用基类的构造函数,而不是再重新写一遍构造函数,老规矩,看代码:
不使用继承构造函数:
struct Base {
Base() {}
Base(int a) { a_ = a; }
Base(int a, int b) : Base(a) { b_ = b; }
Base(int a, int b, int c) : Base(a, b) { c_ = c; }
int a_;
int b_;
int c_;
};
struct Derived : Base {
Derived() {}
Derived(int a) : Base(a) {} // 好麻烦
Derived(int a, int b) : Base(a, b) {} // 好麻烦
Derived(int a, int b, int c) : Base(a, b, c) {} // 好麻烦
};
int main() {
Derived a(1, 2, 3);
return 0;
}
使用继承构造函数:
struct Base {
Base() {}
Base(int a) { a_ = a; }
Base(int a, int b) : Base(a) { b_ = b; }
Base(int a, int b, int c) : Base(a, b) { c_ = c; }
int a_;
int b_;
int c_;
};
struct Derived : Base {
using Base::Base;
};
int main() {
Derived a(1, 2, 3);
return 0;
}
只需要使用using Base::Base继承构造函数,就免去了很多重写代码的麻烦。
nullptr
nullptr是c++11用来表示空指针新引入的常量值,nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型。
在c++中如果表示空指针语义时建议使用nullptr而不要使用NULL,因为NULL本质上是个int型的0,其实不是个指针。举例:
void func(void *ptr) {
cout << "func ptr" << endl;
}
void func(int i) {
cout << "func i" << endl;
}
int main() {
func(NULL); // 编译失败,会产生二义性
func(nullptr); // 输出func ptr
return 0;
}
final & override
c++11关于继承新增了两个关键字,final用于修饰一个类,表示禁止该类进一步派生和虚函数的进一步重载,override用于修饰派生类中的成员函数,标明该函数重写了基类函数,如果一个函数声明了override但父类却没有这个虚函数,编译报错,使用override关键字可以避免开发者在重写基类函数时无意产生的错误。
示例代码1:
struct Base {
virtual void func() {
cout << "base" << endl;
}
};
struct Derived : public Base{
void func() override { // 确保func被重写
cout << "derived" << endl;
}
void fu() override { // error,基类没有fu(),不可以被重写
}
};
示例代码2:
struct Base final {
virtual void func() {
cout << "base" << endl;
}
};
struct Derived : public Base{ // 编译失败,final修饰的类不可以被继承
void func() override {
cout << "derived" << endl;
}
};
default
c++11引入default特性,多数时候用于声明构造函数为默认构造函数,如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数,如下代码:
struct A {
int a;
A(int i) { a = i; }
};
int main() {
A a; // 编译出错
return 0;
}
上面代码编译出错,因为没有匹配的构造函数,因为编译器没有生成默认构造函数,而通过default,程序员只需在函数声明后加上“=default;”,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体,如下:
struct A {
A() = default;
int a;
A(int i) { a = i; }
};
int main() {
A a;
return 0;
}
编译通过。
将=default用于拷贝控制成员
我们可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本
delete
c++中,如果开发人员没有定义特殊成员函数,那么编译器在需要特殊成员函数时候会隐式自动生成一个默认的特殊成员函数,例如拷贝构造函数或者拷贝赋值操作符,如下代码:
struct A {
A() = default;
int a;
A(int i) { a = i; }
};
int main() {
A a1;
A a2 = a1; // 正确,调用编译器隐式生成的默认拷贝构造函数
A a3;
a3 = a1; // 正确,调用编译器隐式生成的默认拷贝赋值操作符
}
而我们有时候想禁止对象的拷贝与赋值,可以使用delete修饰,如下:
struct A {
A() = default;
A(const A&) = delete;
A& operator=(const A&) = delete;
int a;
A(int i) { a = i; }
};
int main() {
A a1;
A a2 = a1; // 错误,拷贝构造函数被禁用
A a3;
a3 = a1; // 错误,拷贝赋值操作符被禁用
}
delele函数在c++11中很常用,std::unique_ptr就是通过delete修饰来禁止对象的拷贝的。
explicit
explicit类型转换运算符, 显式的类型转换运算符(explicit conversion operator)。
explicit专用于修饰构造函数,表示只能显式构造,不可以被隐式转换,根据代码看explicit的作用:
不用explicit:
struct A {
A(int value) { // 没有explicit关键字
cout << "value" << endl;
}
};
int main() {
A a = 1; // 可以隐式转换
return 0;
}
使用explicit:
struct A {
explicit A(int value) {
cout << "value" << endl;
}
};
int main() {
A a = 1; // error,不可以隐式转换
A aa(2); // ok
return 0;
}
const
因为要讲后面的constexpr,所以这里简单介绍下const。
const字面意思为只读,可用于定义变量,表示变量是只读的,不可以更改,如果更改,编译期间就会报错。
主要用法如下:
- 用于定义常量,const的修饰的变量不可更改。
const int value = 5;
- 指针也可以使用const,这里有个小技巧,从右向左读,即可知道const究竟修饰的是指针还是指针所指向的内容。
char *const ptr; // 指针本身是常量
const char* ptr; // 指针指向的变量为常量
- 在函数参数中使用const,一般会传递类对象时会传递一个const的引用或者指针,这样可以避免对象的拷贝,也可以防止对象被修改。
class A{};
void func(const A& a);
- const修饰类的成员变量,表示是成员常量,不能被修改,可以在初始化列表中被赋值。
class A {
const int value = 5;
};
class B {
const int value;
B(int v) : value(v){}
};
- 修饰类成员函数,表示在该函数内不可以修改该类的成员变量。
class A{
void func() const;
};
- 修饰类对象,类对象只能调用该对象的const成员函数。
class A {
void func() const;
};
const A a;
a.func();
constexpr
C++11允许变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化。尽量不要用普通函数作为constexpr变量的初始值,除非是constexpr函数,这种函数足够简单以使得编译时就可以计算其结果。
constexpr是c++11新引入的关键字,用于编译时的常量和常量函数,这里直接介绍constexpr和const的区别:
两者都代表可读,const只表示read only的语义,只保证了运行时不可以被修改,但它修饰的仍然有可能是个动态变量,而constexpr修饰的才是真正的常量,它会在编译期间就会被计算出来,整个运行过程中都不可以被改变,constexpr可以用于修饰函数,这个函数的返回值会尽可能在编译期间被计算出来当作一个常量,但是如果编译期间此函数不能被计算出来,那它就会当作一个普通函数被处理。如下代码:
#include<iostream>
using namespace std;
constexpr int func(int i) {
return i + 1;
}
int main() {
int i = 2;
func(i);// 普通函数
func(2);// 编译期间就会被计算出来
}
enum class
有作用域的enum
c++11新增有作用域的枚举类型,看代码
不带作用域的枚举代码:
enum AColor {
kRed,
kGreen,
kBlue
};
enum BColor {
kWhite,
kBlack,
kYellow
};
int main() {
if (kRed == kWhite) {
cout << "red == white" << endl;
}
return 0;
}
如上代码,不带作用域的枚举类型可以自动转换成整形,且不同的枚举可以相互比较,代码中的红色居然可以和白色比较,这都是潜在的难以调试的bug,而这种完全可以通过有作用域的枚举来规避。
有作用域的枚举代码:
enum class AColor {
kRed,
kGreen,
kBlue
};
enum class BColor {
kWhite,
kBlack,
kYellow
};
int main() {
if (AColor::kRed == BColor::kWhite) { // 编译失败
cout << "red == white" << endl;
}
return 0;
}
使用带有作用域的枚举类型后,对不同的枚举进行比较会导致编译失败,消除潜在bug,同时带作用域的枚举类型可以选择底层类型,默认是int,可以改成char等别的类型。
enum class AColor : char {
kRed,
kGreen,
kBlue
};
我们平时编程过程中使用枚举,一定要使用有作用域的枚举取代传统的枚举。
说明类型用于保存enum对象
尽管每个enum都定义了唯一的类型,但实际上enum是由某种整数类型表示的。在C++11中,我们可以在enum的名字之后加上冒号以及我们想在该enum中使用的类型:
enum intValues:unsigned long long {
charType = 255, shortType = 65535, intType=65535,
longType = 4294967295UL,
long_longType = 18446744073709551615ULL
};
enum的前置声明
在c++11中,我们可以提前声明enum.enum的前置声明(无论隐式韩式显式的)必须指定其成员的大小:
// 不限定作用域的枚举类型intValues的前置声明
enum intValues:usigned long long; // 不限定作用域的,必须指定成员类型
enum class open_modes; // 限定作用域的枚举类型可以使用默认成员类型int
非受限联合体/类类型的union成员
C++早期版本规定,在union中不能含有定义了构造函数或者拷贝控制成员的类类型成员。C++11取消了这一限制。不过,如果union的成员类型定义了自己的构造函数和/或拷贝控制成员,则该union的用法要比只含有内置类型成员的union复杂得多。
c++11之前union中数据成员的类型不允许有非POD类型,而这个限制在c++11被取消,允许数据成员类型有非POD类型,看代码:
struct A {
int a;
int *b;
};
union U {
A a; // 非POD类型 c++11之前不可以这样定义联合体
int b;
};
对于什么是POD类型,大家可以自行查下资料,大体上可以理解为对象可以直接memcpy的类型。
sizeof
c++11中sizeof可以用的类的数据成员上,看代码:
c++11前:
struct A {
int data[10];
int a;
};
int main() {
A a;
cout << "size " << sizeof(a.data) << endl;
return 0;
}
c++11后:
struct A {
int data[10];
int a;
};
int main() {
cout << "size " << sizeof(A::data) << endl;
return 0;
}
想知道类中数据成员的大小在c++11中是不是方便了许多,而不需要定义一个对象,在计算对象的成员大小。
sizeof运算符的结果部分依赖于其作用的类型:
- 对char类型或者char的表达式执行sizeof运算,结果为1
- 对引用类型执行sizeof运算得到被引用对象所占空间大小
- 对指针执行sizeof得到指针本身所占空间大小
- 对解引用指针执行sizeof运算得到指针指向的对象所占空间大小,指针不需要有效
- 对数组执行sizeof运算得到整个数组所占空间大小
- 对string对象或者vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间
assertion
static_assert(true/false, message);
c++11引入static_assert声明,用于在编译期间检查,如果第一个参数值为false,则打印message,编译失败。
自定义字面量
c++11可以自定义字面量,我们平时c++中都或多或少使用过chrono中的时间,例如:
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 100ms
std::this_thread::sleep_for(std::chrono::seconds(100)); // 100s
其实没必要这么麻烦,也可以这么写:
std::this_thread::sleep_for(100ms); // c++14里可以这么使用,这里只是举个自定义字面量使用的例子
std::this_thread::sleep_for(100s);
这就是自定义字面量的使用,示例如下:
struct mytype {
unsigned long long value;
};
constexpr mytype operator"" _mytype ( unsigned long long n ) {
return mytype{n};
}
mytype mm = 123_mytype;
cout << mm.value << endl;
关于自定义字面量,可以看下chrono的源代码,相信大家会有很大收获,需要源码分析chrono的话,可以留言给我。
内存对齐
什么是内存对齐
理论上计算机对于任何变量的访问都可以从任意位置开始,然而实际上系统会对这些变量的存放地址有限制,通常将变量首地址设为某个数N的倍数,这就是内存对齐。
为什么要内存对齐
- 硬件平台限制,内存以字节为单位,不同硬件平台不一定支持任何内存地址的存取,一般可能以双字节、4字节等为单位存取内存,为了保证处理器正确存取数据,需要进行内存对齐。
- 提高CPU内存访问速度,一般处理器的内存存取粒度都是N的整数倍,假如访问N大小的数据,没有进行内存对齐,有可能就需要两次访问才可以读取出数据,而进行内存对齐可以一次性把数据全部读取出来,提高效率。
在c++11之前如果想创建内存对齐需要:
void align_cpp11_before()
{
static char data[sizeof(void *) + sizeof(A)];
const uintptr_t kAlign = sizeof(void *) - 1;
char *align_ptr =
reinterpret_cast<char *>(reinterpret_cast<uintptr_t>(data + kAlign) & ~kAlign);
A *attr = new (align_ptr) A;
}
c++11关于内存对齐新增了一些函数:
void align_cpp11_after()
{
static std::aligned_storage<sizeof(A),
alignof(A)>::type data;
A *attr = new (&data) A;
}
还有:alignof()、std::alignment_of()、alignas().
thread_local
c++11引入thread_local,用thread_local修饰的变量具有thread周期,每一个线程都拥有并只拥有一个该变量的独立实例,一般用于需要保证线程安全的函数中。
#include <iostream>
#include <thread>
class A {
public:
A() {}
~A() {}
void test(const std::string &name) {
thread_local int count = 0;
++count;
std::cout << name << ": " << count << std::endl;
}
};
void func(const std::string &name) {
A a1;
a1.test(name);
a1.test(name);
A a2;
a2.test(name);
a2.test(name);
}
int main() {
std::thread(func, "thread1").join();
std::thread(func, "thread2").join();
return 0;
}
输出:
thread1: 1
thread1: 2
thread1: 3
thread1: 4
thread2: 1
thread2: 2
thread2: 3
thread2: 4
验证上述说法,对于一个线程私有变量,一个线程拥有且只拥有一个该实例,类似于static。
随机数功能
在C++11之前,C和C++都依赖一个简单的C库函数rand来生成随机数,此函数生成均匀分布的伪随机整数,而一些程序需要非均匀分布的数。
c++11关于随机数功能则较之前丰富了很多,典型的可以选择概率分布类型,先看如下代码:
#include <time.h>
#include <iostream>
#include <random>
using namespace std;
int main() {
std::default_random_engine random(time(nullptr));
std::uniform_int_distribution<int> int_dis(0, 100); // 整数均匀分布
std::uniform_real_distribution<float> real_dis(0.0, 1.0); // 浮点数均匀分布
for (int i = 0; i < 10; ++i) {
cout << int_dis(random) << ' ';
}
cout << endl;
for (int i = 0; i < 10; ++i) {
cout << real_dis(random) << ' ';
}
cout << endl;
return 0;
}
输出:
38 100 93 7 66 0 68 99 41 7
0.232202 0.617716 0.959241 0.970859 0.230406 0.430682 0.477359 0.971858 0.0171148 0.64863
代码中举例的是整数均匀分布和浮点数均匀分布,c++11提供的概率分布类型还有好多,例如伯努利分布、正态分布等,具体可以见最后的参考资料。
正则表达式(regular expression)
c++11引入了regex库更好的支持正则表达式,见代码:
#include <iostream>
#include <iterator>
#include <regex>
#include <string>
int main() {
std::string s = "I know, I'll use2 regular expressions.";
// 忽略大小写
std::regex self_regex("REGULAR EXPRESSIONS", std::regex_constants::icase);
if (std::regex_search(s, self_regex)) {
std::cout << "Text contains the phrase 'regular expressions'\n";
}
std::regex word_regex("(\\w+)"); // 匹配字母数字等字符
auto words_begin = std::sregex_iterator(s.begin(), s.end(), word_regex);
auto words_end = std::sregex_iterator();
std::cout << "Found " << std::distance(words_begin, words_end) << " words\n";
const int N = 6;
std::cout << "Words longer than " << N << " characters:\n";
for (std::sregex_iterator i = words_begin; i != words_end; ++i) {
std::smatch match = *i;
std::string match_str = match.str();
if (match_str.size() > N) {
std::cout << " " << match_str << '\n';
}
}
std::regex long_word_regex("(\\w{7,})");
// 超过7个字符的单词用[]包围
std::string new_s = std::regex_replace(s, long_word_regex, "[$&]");
std::cout << new_s << '\n';
}
chrono
c++11关于时间引入了chrono库,源于boost,功能强大,chrono主要有三个点:
durationtime_pointclocks
duration
std::chrono::duration表示一段时间,常见的单位有s、ms等,示例代码:
// 拿休眠一段时间举例,这里表示休眠100ms
std::this_thread::sleep_for(std::chrono::milliseconds(100));
sleep_for里面其实就是std::chrono::duration,表示一段时间,实际是这样:
typedef duration<int64_t, milli> milliseconds;
typedef duration<int64_t> seconds;
duration具体模板如下:
1 template <class Rep, class Period = ratio<1> > class duration;
Rep表示一种数值类型,用来表示Period的数量,比如int、float、double,Period是ratio类型,用来表示【用秒表示的时间单位】比如second,常用的duration<Rep, Period>已经定义好了,在std::chrono::duration下:
ratio<3600, 1>:hoursratio<60, 1>:minutesratio<1, 1>:secondsratio<1, 1000>:microsecondsratio<1, 1000000>:microsecondsratio<1, 1000000000>:nanosecons
ratio的具体模板如下:
template <intmax_t N, intmax_t D = 1> class ratio;
N代表分子,D代表分母,所以ratio表示一个分数,我们可以自定义Period,比如ratio<2, 1>表示单位时间是2秒。
time_point
表示一个具体时间点,如2020年5月10日10点10分10秒,拿获取当前时间举例:
std::chrono::time_point<std::chrono::high_resolution_clock> Now() {
return std::chrono::high_resolution_clock::now();
}
// std::chrono::high_resolution_clock为高精度时钟,下面会提到
clocks
时钟,chrono里面提供了三种时钟:
steady_clocksystem_clockhigh_resolution_clock
steady_clock
稳定的时间间隔,表示相对时间,相对于系统开机启动的时间,无论系统时间如何被更改,后一次调用now()肯定比前一次调用now()的数值大,可用于计时。
system_clock
表示当前的系统时钟,可以用于获取当前时间:
int main() {
using std::chrono::system_clock;
system_clock::time_point today = system_clock::now();
std::time_t tt = system_clock::to_time_t(today);
std::cout << "today is: " << ctime(&tt);
return 0;
}
// today is: Sun May 10 09:48:36 2020
high_resolution_clock
high_resolution_clock表示系统可用的最高精度的时钟,实际上就是system_clock或者steady_clock其中一种的定义,官方没有说明具体是哪个,不同系统可能不一样,我之前看gcc chrono源码中high_resolution_clock是steady_clock的typedef。
新增数据结构
std::forward_list:单向链表,只可以前进,在特定场景下使用,相比于std::list节省了内存,提高了性能
std::forward_list<int> fl = {1, 2, 3, 4, 5};
for (const auto &elem : fl) {
cout << elem;
}
std::unordered_set:基于hash表实现的set,内部不会排序,使用方法和set类似std::unordered_map:基于hash表实现的map,内部不会排序,使用方法和set类似std::array:数组,在越界访问时抛出异常,建议使用std::array替代普通的数组std::tuple:元组类型,类似pair,但比pair扩展性好
typedef std::tuple<int, double, int, double> Mytuple;
Mytuple t(0, 1, 2, 3);
std::cout << "0 " << std::get<0>(t);
std::cout << "1 " << std::get<1>(t);
std::cout << "2 " << std::get<2>(t);
std::cout << "3 " << std::get<3>(t);
新增算法
all_of:检测表达式是否对范围[first, last)中所有元素都返回true,如果都满足,则返回true
std::vector<int> v(10, 2);
if (std::all_of(v.cbegin(), v.cend(), [](int i) { return i % 2 == 0; })) {
std::cout << "All numbers are even\n";
}
any_of:检测表达式是否对范围[first, last)中至少一个元素返回true,如果满足,则返回true,否则返回false,用法和上面一样none_of:检测表达式是否对范围[first, last)中所有元素都不返回true,如果都不满足,则返回true,否则返回false,用法和上面一样find_if_not:找到第一个不符合要求的元素迭代器,和find_if相反copy_if:复制满足条件的元素itoa:对容器内的元素按序递增
std::vector<int> l(10);
std::iota(l.begin(), l.end(), 19); // 19为初始值
for (auto n : l) std::cout << n << ' ';
// 19 20 21 22 23 24 25 26 27 28
- minmax_element:返回容器内最大元素和最小元素位置
int main() {
std::vector<int> v = {3, 9, 1, 4, 2, 5, 9};
auto result = std::minmax_element(v.begin(), v.end());
std::cout << "min element at: " << *(result.first) << '\n';
std::cout << "max element at: " << *(result.second) << '\n';
return 0;
}
// min element at: 1
// max element at: 9
is_sorted、is\_sorted_until:返回容器内元素是否已经排好序。
关于c++11的新特性基本上就是这些,相信各位看完一定会有所收获。
容器的cbegin和cend函数
类似于begin和end,上述两个新函数分别返回只是容器第一个元素或者最后一个元素下一个,位置的迭代器,不同的是,不论容器对象本身是否是常量,返回值始终是const_iterator
容器的非成员函数swap
在C++11标准库中,容器既提供成员函数版本的swap,也提供非成员函数版本的swap.而早期版本只提供成员函数版本的swap。非成员版本的swap在泛型编程中是非常重要的。统一使用非成员版本的swap是一个好习惯。
容器insert成员的返回类型
容器的emplace成员
标准库函数begin和end
C++11引入了两个名为begin和end的函数。这两个函数与容器中的两个同名成员功能类似,不过数组毕竟不是类类型,因此这两个函数不是成员函数,正确的使用形式是将数组作为他们的参数:
int ia[] = {0,1,2,3};
int *beg = begin(ia); // 指向ia首元素的指针
int *last = end(ia); // 指向ia尾元素的下一位置的指针
无序容器
C++11定义了4个无序关联容器(unordered associative container)。这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数(hash function)和关键字类型的==运算符。
除法的舍入规则
C++11标准规定商一律向0取整(即直接切除小数部分)。
shrink_to_fit
C++11中,我们可以调用shrink_to_fit来要求deque、vector和string退回不需要的内存空间。次函数指出我们不再需要任何多余的内存空间。但是,具体的实现可以选择忽略此请求,也就是说调用shrink_to_fit并不能保证一定退回内存空间。
string的数值转换函数
C++11引入多个函数,可以实现数值数据与标准库string之间的转换。
新的bitset运算
C++11引入all操作,当所有位置位时返回true。
bitset<32> bitvec(1U); // 32位;低位为1,剩余位为0
bool all_set = bitvec.all(); // false,因为只有1位置位
浮点数格式控制
C++11中,使用hexfloat可以强制浮点数使用十六进制格式,还提供另一个名为defaultfloat的操纵符,他将流恢复到默认状态——根据要打印的值选择记数法。
noexcept
C++11中,我们可以通过提供noexcept说明(noexcept specification)指定摸个函数不会抛出异常。其形式是关键字noexcept紧跟在函数的参数列表后面,用来标识该函数不会抛出异常
void recoup(int) noexcept; // 不会抛出异常,做了不抛出说明(nonthrowing specification)
noexcept说明符的实参常常与noexcept运算符(noexcept operator)混合使用。noexcept运算符是一个一元运算符,他的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。和sizeof类似,noexcept也不会对其运算对象求值
noexcept(recoup(i)); //如果recoup不抛出异常则结果为true,否则为false
更普通的形式:
noexcept(e)
当e调用的所有函数都做了不抛出说明并且e本身不含用throw语句时,上述表达式为true,否则为false。
我们可以使用noexcept运算符得到如下的异常说明:
void f() noexcept(noexcept(g())); // f和g的异常说明一致
如果函数g承诺了不会抛出异常,则f也不会抛出异常;如果g没有异常说明符,或者g虽热有异常说明符但是允许抛出异常,则f也可能抛出异常。
noexcept有两层含义:当跟在函数参数列表之后时他是一个异常说明符;而当作为noexcept异常说明的bool实参出现时,他是一个运算符。
内联命名空间
C++11引入和一种新的嵌套命名空间,称为内联命名空间(inline namespace)。
和普通的嵌套命名空间不同,内联命名空间中的名字可以被外层命名空间直接使用。也就是说,我们无需在内联命名空间的名字前面添加表示该命名空间的前缀,通过外层命名空间的名字就可以直接访问他:
inline namespace FifthEd {
// 该命名空间表示本书第5版代码
}
namespace FifthEd { // 隐式内联
class Query_base{ /* ... */ };
//其他与Query有关的声明
}
关键字inline必须出现在命名空间第一次定义的地方,后续再打开命名空间的时候可以写inline,也可以不写。
继承的构造函数与多重继承
C++11中,允许派生类从他的一个或几个基类中继承构造函数。但是如果多个基类中继承了相同的构造函数(即形参列表完全相同),则程序将产生错误。
struct Base1 {
Base1() = default;
Base1(const std::string&);
Base1(std::shared_ptr<int>);
};
struct Base2 {
Base2() = default;
Base2(const std::string&);
Base2(int);
};
// 错误,D1试图从两个基类中都继承D1::D1(const string&);
struct D1:public Base1, public Base2 {
using Base1::Base1; // 从Base1继承构造函数
using Base2::Base2; // 从Base2继承构造函数
如果一个类从他的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义他自己的版本:
struct D2 : public Base1, public Base2 {
using Base1::Base1;
using Base2::Base2;
// D2必须自定义一个接受string的构造函数
D2(const string &s):Base1(s),Base2(s){}
D2()=default; // 一旦D2定义了他自己的构造函数,则必须出现
};
标准库mem_fn类模板
C++11可以通过使用标准库功能mem_fn来让编译器负责推断成员的类型。
参考资料
https://zh.cppreference.com/w/cpp/language/range-for
https://juejin.im/post/5dcaa857e51d457f7675360b
https://zhuanlan.zhihu.com/p/21930436
https://zh.wikipedia.org/wiki/Nullptr
https://zh.wikipedia.org/wiki/Constexpr
https://zh.cppreference.com/w/cpp/language/enum
https://kheresy.wordpress.com/2019/03/27/using-enum-class/
https://zh.cppreference.com/w/cpp/language/union
http://c.biancheng.net/view/7165.html
https://zhuanlan.zhihu.com/p/77585472
http://www.cplusplus.com/reference/random/
https://zh.cppreference.com/w/cpp/regex
https://www.cnblogs.com/jwk000/p/3560086.html
https://zh.cppreference.com/w/cpp/algorithm/all_any_none_of

浙公网安备 33010602011771号