C++11有很多新特性,标准分为核心语言部分和库部分,库部分在书中没有讲,书名是《深入理解C++11:C++11新特性解析与应用》
实际上库部分在讲语言的时候一定会涉及,下面是列出的所有新特性,比较重要常用的我用红线标出
上面是中英文对照,下面是根据C++的使用者对这些特性的区分,重点是所有人和类作者比较关注的特性,也是使用最多的特性
上面提到书里主要讲的是核心语言部分,而下面很重要的库部分多多少少都会用到,需要额外学习
第2章 保证稳定性和兼容性
2.1 保持与C99兼容
C++有一个很重要的特点就是兼容C,而C++第一个标准C++98却早于C99,且C++03没有完全兼容C99,因此有以下特性在C++11中得到补充:
2.1.1 预定义宏
2.1.2 __func__预定义标识符
这个预定义标识符的作用就是存所在函数的名字:
VisualAssistX2022破解下载 VAX番茄助手Visual Assist X 10.9.2488 for VS2022 汉化破解版(附key+方法) 64位 下载-脚本之家
这里破解了一下VS。。。没有括号补全太难受了
2.1.3 _Pragma操作符
和#pragma预处理指令功能相同,由于它是操作符,所以可以用在宏里
2.2 long long整型
可以看到long long在我的64位机器上占8字节=64位
2.3 扩展整型
时常会见到右边这种扩展类型,实际上还是标准类型,只不过是别名(VS环境中stdint.h)
2.4 宏__cplusplus
解决Visual Studio设置C++标准 但是_cplusplus始终为199711-CSDN博客
VS对这个宏有特殊处理,所以在我的C++20标准中还是199711的值
理论上C++11中这个宏的值为201103L
2.5 静态断言
2.5.1 运行时与预处理时
运行时断言就是assert宏,迫使条件不为真时程序退出
预处理断言为#error,见得较少
2.5.2 静态断言与static_assert
static_assert为编译期断言,如果有不符合逻辑的地方程序将无法编译通过
2.6 noexcept
声明函数不会抛出异常,替代了之前的throw(),如果noexcept函数抛出了异常,程序会直接终止,调用std::terminate()而不是捕获异常这种带来额外开销的东西
常量表达式的结果被认为是bool,如果bool值为true则函数不会抛出异常
我的理解应该是异常不是一个高效的程序错误处理机制,不然不会想尽办法减少异常的产生与扩散
看到new的实现,突然有个关于关键字、操作符两者概念的疑问
总结了一下,关键字指不能被用户重新使用的一些词,和你是不是函数、变量什么用途没有关系,由核心语言特性支持,只有使用位置的限制,不可以重载;但是操作符属于特殊的函数,操作符不是关键字,操作符类似数学符号,需要操作数,有优先级和结合律的特性,可以被重载
noexcept也常用来修饰析构函数,析构函数构造函数通常都要noexcept,析构函数都默认noexcept
下面是《More Effective C++》书中给出的理由
1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。
2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
import std;
using namespace std;
struct A {
~A() { throw 1; }
};
struct B {
~B()noexcept(false) { throw 2; }
};
struct C {
B b;
};
void funA() { A a; }
void funB() { B b; }
void funC() { C c; }
int main() {
try {
funB();
}
catch (...) {
cout << "caught funB." << endl;
}
try {
funC();
}
catch (...) {
cout << "caught funC." << endl;
}
try {
funA();
}
catch (...) {
cout << "caught funC." << endl;
}
}
B、C能够抛出异常且在抛出异常后被捕获,A由于默认noexcept无法抛出异常则程序中止
2.7 快速初始化成员变量
标准允许在类内使用=或者{}对成员就地初始化,前提是成员不是static,也不要使用圆括号
struct A {
int i{ 3 };
static double d{ 3.0 };//Non-const static data member must be initialized out of line
string s("hello");//同样报错
};
struct A {
int i{3};//就地初始化
A(int _i) : i(_i) {}//使用初始化列表
};
int main() {
A a(1);
cout << a.i << endl; // 1
}
可以看到初始化列表还是优先于就地初始化的
static变量还是在类外定义,这样能避免重复定义
2.10 final/override控制
struct A {
int i{3};//就地初始化
A(int _i) : i(_i) {}//使用初始化列表
virtual int fun(int i_) { cout << i_ << endl; }
};
struct B : A {
int fun(int i_) override { cout << i_ + 1 << endl; }
// virtual int fun(int i_) override { cout << i_ + 1 << endl; }
//子类加不加virtual都可以
// int fun(double i_) override { cout << i_ + 1 << endl; }
//加了override导致编译器进行检查发现和父类的声明不一致
};
标准认为编译器有必要检查重载的情况,至于不加override写了个类似的函数但以为重载了,编译器认为这就是新函数而不是名字写错了
其他特性没有列到文章里,有一些模板和不重要的特性用到的时候可以再看
第3章 通用为本,专用为末
3.1 继承构造函数
非虚函数子类会继承,是可用的,但如果写了同名的就会屏蔽掉父类版本(静态绑定)
struct A
{
A(int i) {}
A(double d,int i){}
A(float f,int i,const char* c){}
//...等等系列的构造函数版本号
};
struct B:A
{
B(int i):A(i){}
B(double d,int i):A(d,i){}
B(folat f,int i,const char* c):A(f,i,e){}
//......等等好多个和基类构造函数相应的构造函数
};
这样使用父类的构造函数是可以的,但是略显繁琐,我们可以显式继承父类的构造函数,使用using声明即可,然后对子类的数据成员进行就地初始化;或者像上面一样定义子类自己的构造函数
#include <iostream>
using namespace std;
struct A {
int i{3};//就地初始化
A() : i(0) {}
A(int _i) : i(_i) {}//使用初始化列表
void print() {
cout << "A::" << __func__ << endl;
cout << i << endl;
}
};
struct B : A {
B(int i) : A(i) {}
using A::A;
~B() { print(); }// 这是为了反驳书上有问题的一句话,继承来的非虚函数仍然是可以使用的
// 只不过非虚函数看的是静态类型,而且函数是属于父类的,成员是属于自己的
};
int main() {
B b;//继承了父类不带参数的默认构造函数,子类也可以
}
不过要注意父类构造函数的默认参数,而且继承父类构造函数后,编译器不会为子类生成默认构造函数
坑太多了,总结下就是子类要么继承,要么自己写,要么初始化列表里用父类的
3.2 委派构造函数
这样写有点繁琐,每次都要写所有成员,我们希望有一个构造函数完成基本工作,其他构造函数调用它:
不过由于初始化列表的初始化比构造函数体内的初始化过程先进行,这样可能会有一部分初始化顺序的错误发生
这样是一种比较好的设计,用到模板里面就是容器的初始化
3.3 右值引用:移动语义与完美转发
3.3.1 指针成员与拷贝构造
当我们使用类管理资源,定义中含有指针的时候,就需要特别处理构造、复制、赋值、移动、析构这些操作了
class HasPtrMem {
public:
HasPtrMem() :d(new int(0)) {}
HasPtrMem(const HasPtrMem& h) :d(new int(*h.d)) {}//不是赋值而是新开辟内存
~HasPtrMem() { delete d; }//不会出现
int* d;
};
int main() {
HasPtrMem a;
HasPtrMem b(a);
cout << *a.d << endl;
cout << *b.d << endl;
}
复制构造函数这样写是应当的,不应该在另外一个对象里拥有同一片地址,因为究其根本对象b不拥有a的资源,复制构造函数应该遵循拷贝的语义
所谓悬挂指针Dangling Pointer,指的是当指针指向的内存已经被释放时,指针没有改变指向仍然指向那块内存
https://zh.wikipedia.org/wiki/%E8%BF%B7%E9%80%94%E6%8C%87%E9%92%88
3.3.2 移动语义
复制构造函数为指针分配新的内存然后进行内容拷贝而不是地址拷贝的做法已经成为事实标准
class HasPtrMem {
public:
HasPtrMem() :d(new int(0)) {
cout << "Construct: " << ++n_cstr << endl;
}
HasPtrMem(const HasPtrMem& h) :d(new int(*h.d)) {
cout << "Copy Construct: " << ++n_cptr << endl;
}
~HasPtrMem() { cout << "Destruct: " << ++n_dstr << endl; }
int* d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
HasPtrMem GetTemp() { return HasPtrMem(); }
int main() {
HasPtrMem a = GetTemp();
}
上面是我机器上的结果,GCC MSVC都是这样的,下面是书上的结果
不考虑编译器优化的情况下,构造函数在GetTemp函数里显式调用,拷贝构造函数调用两次,第一次是构造函数返回值的临时对象,第二次是用返回值构造对象a
其实中间两次拷贝构造都是没有意义的,于是发明了移动语义
class HasPtrMem {
public:
HasPtrMem() :d(new int(0)) {
cout << "Construct: " << ++n_cstr << endl;
}
HasPtrMem(const HasPtrMem& h) :d(new int(*h.d)) {
cout << "Copy Construct: " << ++n_cptr << endl;
}
HasPtrMem(HasPtrMem&& h) :d(h.d) {
h.d = nullptr;
cout << "Move Construct:" << ++n_mvtr << endl;
}
~HasPtrMem() { cout << "Destruct: " << ++n_dstr << endl; }
int* d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
static int n_mvtr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;
HasPtrMem GetTemp() { HasPtrMem h; return h; }
int main() {
HasPtrMem a = GetTemp();
}
书上给的结果是这样的,但实际上我的机器运行情况还是之前的结果,一次正常构造,一次正常析构
按书上的理论(不考虑优化),之前的两次拷贝构造替换成了现在的移动构造,但是经过优化以后已经没有了移动构造的过程
书上想说的是当拷贝成本过高且不再需要原对象管理资源的时候,我们是可以使用移动语义的,而且C++11之前已经有这种语义的应用,只是缺乏统一的定义
3.3.3 左值、右值与右值引用
Value categories - cppreference.com
下面是在博客上找的,我觉得很有概括性:
其中 xvalue 和 prvalue 统称 rvalue;而 lvalue 和 xvalue 统称 glvalue.
右值引用和左值引用的区别就是右值引用绑定的都是匿名对象,左值引用绑定的都是实体
不过有一个const左值引用这种万用类型兜底,尽管右值引用不能绑定左值,但它可以,这样在移动不行的时候至少还可以拷贝
搞不清楚类型我们就可以使用decltype来判断:
decltype specifier - cppreference.com
int main() {
int a{ 1 };
using T1 = decltype(std::move(a));//xvalue对应右值引用
using T2 = decltype(a);//int
using T3 = decltype(1);//prvalue对应基本类型
//Note that if the name of an object is parenthesized, it is treated as an ordinary lvalue expression
//thus decltype(x) and decltype((x)) are often different types.
using T4 = decltype((a));//lvalue对应左值引用
}
3.3.4 std::move()强制转化为右值
从实现上讲,move=static_cast<T&&>
从结果上讲,move参数没有被所谓的“移动”,其管理的资源没有改变,参数也没有从左值转变为右值
move(a)后b的构造使用移动构造函数,此时a的i指针已经为空,此时再输出他的值会导致异常,程序返回值可以看到是-10不是0
关于书上结果和实际情况不一样的问题,我们可以了解一下返回值优化技术和拷贝消除的特性,C++17已经写入标准,纯右值不到使用时不会占用内存Copy elision - cppreference.com
3.3.6完美转发
为了避免拷贝,我们很容易想到使用const引用来转发,但是这样也有缺陷:
void IRunCodeActually(int&& t) {
cout << "实际功能" << t << " " << endl;
}
template<typename T>
void Forwarding(const T& t) {//不能更改实参,需要有一个非const重载版本
cout << "转发函数" << endl;
IRunCodeActually(t);//功能函数参数是右值引用时将无法使用
//error C2664: “void IRunCodeActually(int &&)”: 无法将参数 1 从“const T”转换为“int &&”
}
int main() {
int a{ 1 };
Forwarding(a);
}
先补充一下万能引用:引用折叠和完美转发 - 知乎
所有的引用折叠最终都代表一个引用,要么是左值引用,要么是右值引用。 规则就是: 如果任一引用为左值引用,则结果为左值引用。否则(即两个都是右值引用),结果为右值引用。 《Effective Modern C++》
所以把转发函数的参数写成万能引用,然后功能函数传入右值引用强制转换就可以达到左值对应左值、右值对右值的完美转发
标准库封装了一个forward函数:
可以看到forward和move差别并不大
Reference declaration - cppreference.com中的Forwarding references一节讲到了转发的技术
forward函数作包装函数很方便,标准库也有很多函数这样实现
第4章 新手易学,老兵易用
4.3 decltype
4.3.1 typeid和decltype
运行时类型识别RTTI的机制是为每个类型产生一个type_info,typeid可以获得对象类型,name成员函数返回类型的名字,hash_code成员函数返回哈希值以便比较类型是否相同
在非泛型编程中没必要对类型进行推导,因为类型在编译期已经确定,没必要进行动态类型识别,但是当类型变成参数、未知数时,我们希望对一些表达式的类型进行编译期推导
4.3.2 decltype的应用
其一就是头文件中对某些别名的类型确定
4.3.3 decltype的推导规则
void Overload(int);
void Overload(char);
int&& Rvalref();
const bool Func(int);
int main() {
int i{ 4 };
int arr[5] = { 0 };
int* ptr = arr;
struct S {
double d;
} s;
//Rule 1
decltype (arr) var1;//int
decltype(ptr) var2; //int*
decltype(s.d) var3; //double
//decltype(Overload) var4; //error
//Rule 2
decltype(Rvalref()) var5 = 1; //int&&
//Rule 3
decltype(true ? i : i) var6 = i; //int&
decltype((i))var7 = i; //int&
decltype(++i) var8 = i; //int&
decltype(arr[3]) var9 = i; //int&
decltype(*ptr) var10{ i }; //int&
decltype("lval") var11{ "lval" }; //const char(&var11)[5]
//Rule 4
decltype(1) var12; //int
decltype(i++) var13; //int
decltype((Func(1))) var14;//bool
//const不见了
}
is_rvalue_reference、is_lvalue_reference、is_const、is_volatile是一些判断类型的小工具
第5章 提高类型安全
5.1 强类型枚举
enum class 这个玩意了解就好
5.2 堆内存管理:智能指针与垃圾回收
上面是一些内存管理的常见问题
5.2.2 C++11的智能指针
垃圾回收策略: