深入理解C++ 11
前言
说到C++11 ,应该想到什么?
-
什么是
lambda ,及怎么样使用它是最好的? -
decltype 和auto 类型推导有什么关系? -
什么是移动语义,以及
( 右值引用) 是如何解决转发问题的? -
default/deleted 函数以及override 是怎么回事? -
异常描述符被什么替代了
? noexcept 是如何工作的? -
什么是原子类型以及新的内存模型
? -
如何在
C++11 中做并行编程?
语言的哪些关键字和C++11 有关?
-
alignas
-
alignof decltype
-
auto( 重新定义) -
static_assert
-
using( 重新定义) -
noexcept
-
export( 弃用,不过未来可能留作他用) -
nullptr
-
constexpr
-
thread_local
保证稳定性和兼容性
将C99 标准纳入C++11
确定编译环境的预定义宏
-
STDC_HOSTED:编译器的目标系统环境中是否包含完整的
C 库 -
STDC:编译器对于标准
C 库的实现是否和C 标准一致(是否定义、如何定义由编译器决定) -
STDC_VERSION:编译器支持的
C 标准的版本(是否定义、如何定义由编译器决定) -
STDC_ISO_10646:
yyyymmL 格式的整数常量,表示C++ 编译环境符合某个版本的ISO/IEC 10646 标准
func 预定义标识符
函数中可以使用
static const char* func = “
_Pragma 操作符
在之前的
不定参数宏定义以及VA_ARGS
宽窄字符串连接
long long 整形
扩展的整形
宏__cplusplus
一般会和
#ifdef __cplusplus
extern "C" {
#endif
// 一些代码
#ifdef __cplusplus
}
#endif
静态断言
标准
如果希望在预处理时确定某些情况一定不会发生,也可以使用
noexcept 修饰符与noexcept 操作符
作为修饰符
作为操作符
template <class T>
void fun() noexcept(noexcept(T())) {}
替代废弃的throw
快速初始化成员变量
在成员同时使用就地初始化,并且在构造函数的初始化列表中进行初始化时,最终仅会以初始化列表为准对其进行初始化。
非静态成员的sizeof
sizeof(((People*)0)->hand);
sizeof(People::hand)
扩展的friend 语法
在
https://zh.cppreference.com/w/cpp/language/friend
final/override
为了尽可能兼容已有程序,
模板函数的默认模板参数
外部模板
局部和匿名类型作为模板实参
// C++98中,只有A或者a可以作为模板参数
struct A {int a;} a;
// 匿名类型
typedef struct {int a;} B;
// 匿名类型变量
struct {int a;} b;
void test() {
// 局部类型
struct C {int a;} c;
}
通用为本,专用为末
继承构造函数
继承关系中,子类可以自动或得父类的成员和接口,但构造函数无法自动地被子类继承。因为通常子类也有自己的成员,我们要定义子类自己的构造函数,在子类构造函数中去调用父类构造函数以及初始化自己的成员。
但是,如果子类中没有任何成员,或者其成员都用
(书中这一节很多描述都和
委派构造函数
可以在构造函数的
一旦在
右值引用
可以使用两个引用符号
在
const MyCls &myRef = getTemp();
这样的常量左值引用的确引用了
移动语意
在
但是,有时候我们拷贝或赋值时,比如
移动构造函数应该是不会抛出异常的,因为如果移动到一半被终止了,会导致对象的一部分指针成员变成悬挂指针。标准库提供了
完美转发
右值引用的问题
有了右值引用,看起来我们可以完美地实现移动语意了,但是,需要留意的是,我们在将右值赋给一个右值引用后,这个右值引用其实会被当成一个左值引用(毕竟移动语意本身就要求对右值引用进行修改
因此,在访问右值引用,或者在访问右值引用的成员时,必须将其转换成右值引用,否则就会被当成普通的左值引用。
// 像这样的声明赋值没有意义,实际上,a依然会成为一个左值引用
// A &&a = getTemp();
A &a = getTemp();
acceptRValueRef(std::move(a)); // OK,这里使用move把一个被当作左值引用的右值引用转成右值引用
accestRValueRef(std::forward<A>(a)); // OK,forward也能起到转为右值引用的作用
这个现象要求我们在创建移动构造函数时,必须要使用标准库
在将右值引用传入参数为右值引用的函数时,编译器会报错,因为右值引用实际上一旦被赋给引用变量,就会被当成左值引用。要让编译器重新将其重新当成一个右值引用,必须使用
引用折叠
为了在模板编程时,让模板能够同时处理左值和右值引用,
using MyClsLRef = MyCls&;
using MyClsRRef = MyCls&&;
// C++11中被引用折叠规则理解为左值引用
MyClsLRef&& lRef = getMyCls();
// 下面两行是一样的,其中第一行在C++11中被引用折叠规则理解为右值引用
MyClsRRef&& rRef = getMyCls();
MyClsRRef rRef = getMyCls();
// 利用引用折叠规则,可以在模板编写中将参数声明为左值引用类型,这样的模板函数实际上可以同时接收
// 左值引用和右值引用
template <typename T>
void test(T&& t) { ... }
// 当T是一个右值引用时,T&&&&被折叠成右值引用
// 当T是一个左值引用时,T&&&被折叠成左值引用
// 不用考虑T不是一个引用,会有这样的考虑说明对C++不够熟悉,函数参数被声明为引用,传进来的肯定是引用
除了
A getTemp() {
return A();
}
// 转发函数
void forwardToTest(A&& a) {
// do something
// test(a); 无法通过编译,因为一旦右值引用被赋给变量,这个变量就表现成了左值引用
test(std::forward<A&&>(a));
}
// 转发函数
template<typename T>
void forwardToTestTemplate(T&& a) {
// test(a); 同样无法通过编译
test(std::forward<T>(a));
}
template<typename T>
void test(T&& a) {
}
int main() {
forwardToTest(getTemp());
forwardToTestTemplate(getTemp());
A a;
// forwardToTest(a); 无法通过编译,因为a不是右值引用
forwardToTestTemplate(a); // 可以通过编译,因为模板方法有引用折叠规则
}
move 和forward 的区别
move 调用时不需要提供模板参数,它仅被用于将参数强制转为右值引用;forward 调用时必须要提供模板参数,通常会提供这样的模板参数:forward<T&&>,这样的好处是T 如果被声明为左值,转换后还是左值,T 如果被声明为右值,转换后还是右值。
explicit 显示转换操作符
默认情况下,
- 实参的类型转换运算符,如果有转换为目标类型的转换运算符就调用;
- 目标类型的构造函数,看是否有接收实参类型的构造函数,如果有就调用;
有时这很方便,但更多场景下这样的行为只会导致语意上的混乱。为了避免编译器的隐式转换,可以使用
initializer_list 初始化列表
如何使用初始化列表
int a[]={1,3,5};//C++98通过,C++11通过
int b[]{2,4,6};//C++98失败,C++11通过
vector<int> c{1,3,5};//C++98失败,C++11通过
map<int,float> d = {{1,1.0f},{2,2.0f},{5,3.2f}};//C++98失败,C++11通过
如果要让自定义的类支持这种初始化方式,只要声明一个接收在
防止类型收窄
使用初始化列表还可以促使编译器检查类型收窄的情况。初始化列表是目前唯一一种检查类型收窄的方式(不过事实上现在的大多数编译器在没有使用初始化列表时也会检查类型收窄并给出警告,但使用初始化列表编译器会直接给出错误
POD 类型
POD,也就是
(但是,对
union 非受限联合体
-
在
C++98 ,union 中只能包含基础类型和POD 类型,并且不能包含静态方法,但在C++11 中,union 中可以包含任意非引用类型。 -
C++11 中,如果union 任何一个成员拥有非平凡的构造函数,那么编译器就不会为union 生成默认构造函数。 -
C++11 中,允许在类定义使用union 声明成员变量,用这种方式声明的union 不需要有类型名称,被称为匿名的非受限联合体,此时联合体内的所有成员都会自动的成为类的“变长成员”,即实际上它们共享同一处内存,应该只使用它们中的某一个。
用户定义字面量
字面量操作符
-
接收字符串:
< 用户类型> operator " ”_< 后缀字符>(const char* col, size_t size) -
接收整数:
< 用户类型> operator " ”_< 后缀字符>(unsigned long long val) -
接收整数,但整数越界,此时会传入’\0’结尾的
char* :< 用户类型> operator "" _< 后缀字符>(const char*) -
接收浮点:
< 用户类型> operator "" _< 后缀字符>(long double val) -
接收字符:
< 用户类型> operator "" _< 后缀字符>(char val)
内联名字空间
这样的行为和
- 匿名名字空间在
C++98 中用于替代static 修饰符,因为C++ 为类引入了static 成员后,static 的语意变得非常模糊且矛盾,因此在原本使用static 声明文件作用域的变量的地方,可以改成使用匿名名字空间来包围这些变量起到同样的效果; - 内联名字空间则是被标准库用于和宏配合使用,根据当前编译环境决定默认导出同一个功能的哪一个版本的实现,这样做的好处是不关心具体实现的用户可以直接使用默认导出的功能,而了解更全面的细节的用户也可以使用名字空间来指定使用的是哪一个版本的功能。
使用using 声明模板别名
在
template <typename T> using StringObjectMap = std::map<string, T>;
StringObjectMap<MyCls> myMap;
SFINAE 规则
SFINAE,就是
指的是,编译器在尝试模板参数匹配时,只要能够最终找到合适的匹配,中间尝试过的任何匹配失败都不会报错。
只不过,
template<int I> struct A{};char xxx(int);char xxx(float);template <class T> A<sizeof(xxx((T)0))> f(T){}
新手易学,老兵易用
右尖括号的改进
auto 类型推导
在
decltype
auto add(t1, t2) -> decltype(t1 + t2) {return t1 + t2;}
或者可以声明原本无法声明的变量:decltype(t1 + t2) addResult = t1 + t2;(当然,
有时会将
追踪返回类型
auto func(char* a,int b) -> int;
基于范围的for 循环
自定义集合类型要想支持这样的
提高类型安全
强类型枚举
C++98: enum X {…};
C++11: enum class X {…};
堆内存管理:智能指针与垃圾回收
提高性能及操作硬件的能力
常量表达式
变长模板
过去,
现在,
模板参数包与递归
使用
不定长的变长模板类可以通过模板类的递归来解包:
template <typename... Elements> class tuple; // 变长模板声明
// 以下是两个模板偏特化定义,利用模板偏特化会被优先匹配的规则,让变长模板参数递归地被解包
// 对于类型,可以使用递归的继承
template <typename Head, typename... Tail>
class tuple <Head, Tail...> : private tuple<Tail...> {
Head head;
}
template <> class tuple {};
// 对于函数,可以使用递归的函数调用
// 下面实现一个更强大的Printf,不论%后面跟的是什么符号,这个Printf总是会打印正确的类型
void Printf(const char* s) {
while (*s) {
if (*s == '%' && ++s != '%') {
throw runtime_error("invalide format");
}
cout *s++;
}
}
template <typename T, typename... Args>
void Printf(char* s, T value, Args... args) {
while(*s) {
if (*s == '%' && *s++ != '%') {
cout << value;
return Printf(++s, args...);
}
cout << *s++;
}
// 若百分号的数量和参数数量对不上,就抛异常
throw runtime_error("extra arguments provided");
}
进阶
引用类型
定义了模板参数包后,还可以在展开模板参数包时使用引用标记:Args&&…,这样的写法是合法的;
特殊展开
解包时,有些非常特殊的规则,需要特别说明一下:
template <typename... Args> class MyCls: private A<Args>... {};
// 上面的表达式在解包时会解包成多继承:
T<Parent1, Parent2> t; // t的类型是:class MyCls: private A<Parent1>, A<Parent2>
template <typename Args...> class MyCls: private A<Args...> {};
// 而这个表达式在解包时,会在泛型参数表达式中直接展开
T<Parent1, Parent2> t; // t的类型是:class MyCls: private A<Parent1, Parent2>
template <typename Args...> void test(Args... args) {
// 下面这个会被展开成call(a(arg1), a(arg2), ...)
call(a(args)...);
// 而下面这个会被展开成call(a(arg1, arg2, ...))
call(a(args...));
}
获取变长参数包长度
可以使用
模板的模板(的模板的模板…)
变长参数的模板类型本身也可以是一个模板,这一点和以前的非变长模板参数一样。
原子类型和原子操作
通常情况下,如果我们不需要太精细的互斥控制,可以直接使用
线程局部存储
快速退出:quick_exit, at_quick_exit
在过去,大体上有三种退出程序的方式:terminate(), abort(), exit()。
-
terminate 是有未处理的异常时会被调用的方法,可以使用set_terminate 方法更改默认行为,terminate 默认调用abort ; -
abort 是进程不得不终止时,被调用的函数,它会向本进程发送一个SIGABRT 信号,该信号默认会导致操作系统直接释放该进程的所有资源并终止进程; -
exit 是进程自发调用的退出函数,它代表着进程运行到了某个节点,该退出了,它会导致每一个自动变量的析构函数被调用(是的,仅自动变量,也就是栈变量会被调用析构函数,至于单例什么的需要自己处理) ,并调用at_exit 注册的函数,然后才会回收进程;
为改变思想方式而改变
指针空值-nullptr
默认函数的控制
-
空构造函数
-
拷贝构造函数
-
拷贝赋值函数
-
移动构造函数
-
移动拷贝函数
-
析构函数
此外,编译器还为所有自定义类型提供以下全局默认操作符函数:
-
operator,
-
operator&
-
operator&&
-
operator*
-
operator->
-
operator->*
-
operator new
-
operator delete
lambda 函数
-
捕获外部变量:变量需要用
& 或者= 开头来引用,直接写& 或者= 后面不跟变量表示捕获所有外部变量,= 表示按值捕获,& 表示引用捕获; -
mutable:
labmda 默认是内联的const 函数,不可以修改任何捕获的外部的按值捕获的变量(因为目前lambda 的语意其实和仿函数完全一致,在仿函数中,所有捕获的外部变量都是仿函数类的成员,因此const 函数不可以修改类成员,到lambda 这里变成了lambda 不可以修改捕获变量) ,但是引用捕获的变量则可以修改(这个行为也是和仿函数一致的,const 函数内可以调用任意成员引用的方法,修改其属性,因为将引用声明成const ,和将指针声明成const 类似,都仅仅是禁止修改引用本身,但并没有限制对引用或者指针指向的变量进行修改) 。如果希望lambda 不是一个const 函数,就要添加mutable 声明; -
参数列表:
-
返回值:当能够从函数体中推测出明确的返回值类型时,可以忽略;
-
函数体:
融入实际应用
对齐支持
需要注意的是,之前很多编译器也规定了指定数据对齐方式的方式,比如
需要注意的是,虽然标准规定了指定对齐的方式,但每个平台具体支持对齐到多少是不确定的。如果使用
通用属性
有时
GNU 使用attribute ((< 属性列表>)) 来声明属性;Windows 使用__declspec(< 属性列表>) 来声明属性
Unicode 支持
字符集和编码

C++ 中的Unicode 支持
-
u8 - UTF8
-
u - UTF-16
-
U - UTF-32
-
L - wchar_t
之所以没有为
-
文件编码
-
编译器编码设置
-
输出设备
为了确保得到正确的输出,需要确保源文件的编码同系统编码一致、并且用于输出的设备支持被输出的编码(比如不少