文章目录
整理 <<深入理解C++11 C++11新特性解析与应用>> 笔记
第2章 保证稳定性和兼容性
final/override 控制
struct Object {
virtual void fun() = 0;
}
struct Base: public Object {
void fun() final; // 声明为final
}
struct Derived: public Base {
void fun(); // 无法通过编译
}
struct Base {
virtual void Turing() = 0;
virtual void Dijkstra() = 0;
virtual void VNeumann(int g) = 0;
virtual void DKnuth() = 0;
void Print();
}
struct DerivedMid : public Base {
// void VNeumann(double g);
// 接口被隔离,曾想多一个版本的VNeumann函数
}
struct DerivedTop : public DerivedMid {
void Turing() override;
void Dikjstra() override; // 无法通过编译, 拼写错误,非重载
void VNeumann(double g) override; // 无法通过编译, 参数不一致,非重载
void DKnuth() const override; // 无法通过编译,常量性不一致,非重载
void Print() override; // 无法通过编译, 非虚函数重载
}
局部和匿名类型作模板实参
template<typename T> class X {};
template<typename T> void TempFun(T t) {}
struct A{} a;
struct { int i; } b; // 匿名类型
typedef struct { int t; } B;
void Fun() {
struct C {} c; // 局部类型
X<A> x1; // c++98 通过, c++11 通过
X<B> x2; // c++98 错误, c++11 通过
X<C> x3; // c++98 错误, c++11 通过
TempFun(a); // c++98 通过, c++11 通过
TempFun(b); // c++98 错误, c++11 通过
TempFun(c); // c++98 错误, c++11 通过
}
但是不能就地定义, 使用decltype获取类型
MyTemplate<struct { int a; } > t; // 编译错误,不能就地定义
struct {
int a;
}A;
MyTemplate<decltype(A)> t; // 编译通过
第3章 通用为本,专用为末
继承构造函数
情景: 在派生类中我们写的构造函数完完全全是为了构造基类,而基类有很多构造函数,那么我们需要为派生类写很多的"透传"构造函数. c++11 使用using声明继承基类的构造函数来解决这个重复写的问题.
struct A {
A(int i) {}
A(double d, int i) {}
A(float f, int i, const char* c) {}
// ...
};
struct B: A {
using A::A; // 继承构造函数
// ...
virtual void ExtraInterface () {}
};
通过using A::A
的声明,把基类中的构造函数都继承到派生类B中.而且这是隐式声明的,意味这如果没有实际调用,编译器不会生成相关代码,节省了目标代码空间.
有的时候,会发生继承构造函数冲突问题,需要显示定义,阻止隐式生成相应的构造函数来解决冲突
struct A { A(int) {} };
struct B { B(int) {} };
struct C: A, B {
using A::A;
using B::B;
C(int) {} // 显式定义
}
注意:
- 基类的构造函数是私有函数,不能继承
- 使用了继承构造函数,那么不再为派生类生成默认构造函数.
委派构造函数
委派构造,就是指委派函数将构造的任务委派给委派构造函数(delegating constructor). 构造函数不能同时"委派"和使用初始化列表.初始化列表的初始化方式总是先于构造函数完成(实际在编译完成时已经决定了).
可以形成委派链,但是不能有委派环.
class Info {
public:
Info(): Info(1, 'a') {}
Info(int i): Info(i, 'a') {}
Info(char e): Info(1, e) {}
private:
Info(int i, char e): type(i), name(e) {}
int type;
char name;
}
委派函数实际应用是使用构造模板函数产生目标构造函数, 委托构造函数使得泛型编程成为一种可能
#include <iostream>
#include <deque>
#include <vector>
using namespace std;
class TDConstructed {
template<class T> TDConstructed(T first, T second): l(first, last) {}
list<int> l;
public:
TDConstructed(vector<short>& v): TDConstructed(v.begin(), v.end()) {}
TDConstructed(deque<int>& d): TDConstructed(d.begin(), d.end()) {}
};
右值引用:移动语义和完美转发
移动语义
#include <iostream>
using namespace std;
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 = 0;
static int n_cptr = 0;
static int n_dstr = 0;
};
HasPtrMen getTemp() { return HasPtrMen(); }
int main() {
HasPtrMen a = getTemp();
}
// c++ -std=c++11 test.cpp -fno-elide-constructors
// 需要关掉编译器返回值优化选项(RVO, Return Value Optimization)
Construct: 1
Copy construct: 1
Destruct: 1
Copy construct: 2
Destruct: 2
Destruct: 3
调用过程
graph TD
GetTemp(( ))--构造-->HasPrtMem
HasPrtMem--拷贝构造-->HasPtrMem临时对象
HasPtrMem临时对象--拷贝构造-->a
添加移动构造函数
HasPtrMem(HasPtrMem&& h): d(h.d) {
h.d = nullptr; // 将临时值的指针成员置空
cout << "Construct: " << ++n_mvtr << endl;
}
右值和左值的区别
可以取地址,有名字的是左值.不能取地址的,没有名字的是右值.
在C++11程序中,所有值必须属于左值,纯右值,将亡值三者之一.右值分为:纯右值,将亡值
纯右值: 用于辨认临时变量和一些不跟对象关联的值,如 1 + 3 产生的临时变量值
将亡值: 是C++11新增的跟右值引用相关的表达式.如T&&的函数返回值.
T ReturnRvalue() {}
T&& a = ReturnRvalue(); // 调用移动构造函数,为临时对象续命
注意: 右值引用不能绑定到左值上
int a;
int&& b = a; // 编译失败
注意
常量左值引用是一个"万能"的存在,只读不能修改
T ReturnRvalue() {}
T& e = ReturnRvalue(); // 编译失败
const T& f = ReturnRvalue(); // 编译通过
T&& g = ReturnRvalue(); // 编译通过
std::move 强制转化为右值
std::move
从实现上讲,基本等同与一个类型转换static_cast<T&&>(lvalue
std::move
转化的左值不会立刻被析构
#include <iostream>
class Moveable {
public:
Moveable(): i(new int(3)) {}
~Moveable() { delete i; }
Moveable(const Moveable& m): i(new int(*m.i)) {}
Moveable(Moveable&& m): i(m.i) { m.i = nullptr; }
int* i;
};
int main() {
Moveable a;
Moveable c(move(a)); // 调用移动构造函数
cout << *a.i << endl; // 运行错误
}
move() 使用场景
在移动构造函数中,为了保证成员变量也能使用移动构造函数,需要用到move()
class Moveable {
public:
Moveable(Moveable&& m): i(m.i), h(move(m.h)) {
m.i = nullptr;
}
int* i;
HugeMem h;
}
移动语义其他问题
Moveable(const Moveable &&);
const Moveable ReturnVal();
上面的生命都会使得右值常量化,那么对临时变量的修改不能进行,无法实现移动语义.
在C++11中, 拷贝构造/赋值构造/移动构造/移动赋值 函数必须同时提供,或者同时不提供. 一旦提供一个,编译器将不会隐式生成其他三个.
对与移动语义,抛异常是危险的,因为可能移动语义未完成,异常抛出,导致一些指针成悬挂指针,因此尽量写不抛出异常的移动构造函数,一旦有异常,直接终止程序, 添加 noexcept
关键字
完美转发
#include <iostream>
using namespace std;
void RunCode(int & m) { cout << "lvalue ref"<< endl; }
void RunCode(int && m) { cout << "rvalue ref"<< endl; }
void RunCode(const int & m) { cout << "const lvalue ref"<< endl; }
void RunCode(const int && m) { cout << "const rvalue ref"<< endl; }
template<typename T>
void PerfectForward(T&& t) { RunCode(forward<T>(t)); }
int main() {
int a;
int b;
const int c = 1;
const int d = 0;
PerfectForward(a); // lvalue ref
PerfectForward(move(b)); // rvalue ref
PerfectForward(c); // const lvalue ref
PerfectForward(move(d)); // const rvalue ref
}
列表初始化
初始化列表
#include <iostream>
#include <map>
using namespace std;
int a[] = {1, 3, 5};
map<int, float> d = {{1, 1.0}, {2, 2.0}, {5, 3.0}};
int* i = new int(1);
double* e = new double{1.2};
int b[]{1, 3, 5};
vector<int> c{1, 3, 5};
自定义的类使用列表初始化
#include <initializer_list>
enum Gender { boy, girl };
class People {
public:
People(initializer_list<pair<string, Gender> > l) {
for (auto i = l.begin(); i != l.end(); i++)
{ data.push_back(*i); }
}
private:
vector<pair<string, Gender> > data;
};
People ship2012 = {{"Garfield", boy}, {"HelloKitty", girl}};
应用
class Mydata {
public:
Mydata& operator[](initializer_list<int> l ) {
for (auto i = l.begin(); i != l.end(); i++) {
idx.push_back(*i);
}
return *this;
}
Mydata& operator=(int x) {
if (idx.empty()) { return *this; }
for (auto i = idx.begin(); i != idx.end(); ++i) {
d.resize((*i > d.size()) ? *i : d.size());
d[*i - 1] = v;
}
idx.clear();
}
return *this;
private:
vector<int> idx;
vector<int> d;
}
int main() {
Mydata d;
d[{2, 3, 5}] = 7;
d[{1, 4, 5, 8}] = 4;
// d: 4 7 7 4 4 0 0 4
}
POD类型
POD(Plain Old Data)的好处:
- 字节赋值,安全的使用memset和memcpy对POD类型初始化
- 对C内存布局兼容
- 保证了静态初始化的安全有效
模板别名
在C++11中,using关键字的能力已经包括的typedef的部分
using uint = unsigned int;
typedef unsigned int UINT;
在使用模板编程时,using的语法更灵活
template<typename T> using MapString = std::map<T, char*>;
MapString<int> numberedString;
// std::map<int, char*> numberedString;
第4章 新手易学,老兵易用
auto类型推导
使用细则
auto声明不带走常量性和易失性
double foo();
float* bar();
const auto a = foo(); // const double
const auto& b = foo(); // const double&
volatile auto* c = foo(); // volatile float*
auto d = a; // double
auto& e = a; // const double&
auto f = c; // float*
volatile auto& g = c; // volatile float* &
## decltype
int main() {
int i;
decltype(i) j = 0;
cout << typeid(j).name() << endl; // int
float a;
double b;
decltype(a + b) c;
cout << typeid(c).name() << endl; // double
}
decltype 以一个普通的表达式为参数,返回该表达式的类型.decltype类型推导在编译时进行.
decltype四规则
假设 e的类型是T
- 如果e是一个没有带括号的标记表达式或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型.
- 否则, 如果e是一个将亡值,那么decltype(e) 为 T&&
- 否则, 如果e是一个左值,那么decltype(e) 为 T&
- 否则, decltype为 T
只有1, 3容易搞混.单个标记符对应的表达式就是标记符表达式.如int arr[4]
中的arr
, 而arr[3]
就不是
int i;
decltype(i) a; // int 规则一
decltype((i)) b; // int& 规则三, 编译错误
decltype能带走const和volatile关键字
int* p;
decltype(p)* a; // int**
int i = 1;
int&j = i;
decltype(j)& var2 = i; // int&, 冗余的&, 忽略
追踪返回类型
template<typename T1, typename T2>
auto Sum(T1& t1, T2& t2) -> decltype(t1 + t2) {
return t1 + t2;
}
复合符号 -> decltype(t1 + t2)
称为追踪返回类型
第5章 提高类型安全
强类型枚举
强类型枚举不会将名字输出到全局空间, 引用时必须加上名字空间才可以.
enum class Type {
General,
Light,
Medium,
Heavy
};
C++11的智能指针
unique_ptr
不能与其他unique_ptr
类型的指针对象共享所指对象的内存,只能通过move
函数将"所有权"转移.转移后,原来的unique_ptr
不能在用.
事实上,unique_ptr
是一个删除了拷贝构造函数,保留了移动构造函数的指针封装类型.
shared_ptr
只有在引用计数归零的时候,share_ptr才会真正释放所占有的堆内存空间
weak_ptr
可以指向shared_ptr
指针指向的对象内存,但是不拥有该内存.在使用lock
成员时,返回一个指向内存的shared_ptr
对象,且在所指内存无效时返回nullptr
.可以验证shared_ptr
智能指针的有效性.
#include <iostream>
#include <memory>
using namespace std;
void check(weak_ptr<int>& wp) {
shared_ptr<int> sp = wp.lock();
if (sp != nullptr) cout << "still " << *sp << endl;
else cout << "pointer is invalid." << endl;
}
int main() {
unique_ptr<int> up1(new int(11));
unique_ptr<int> up2 = up1; // 编译错误
cout << *up1 << endl; // 11
unique_ptr<int> up3 = move(up1); // 现在p3是数据唯一的unique_ptr 指针
cout << *up3 << endl; // 11
cout << *up1 << endl; // 运行时错误
up3.reset();
up1.reset(); // 不会导致运行时错误
cout << *up3; // 运行时错误
shared_ptr<int> sp1(new int(22));
shared_ptr<int> sp2 = sp1;
cout << *sp1 << endl; // 22
cout << *sp2 << endl; // 22
sp1.reset();
cout << *sp2 << endl; // 22
weak_ptr<int> wp = sp2;
check(wp); // still 22
sp2.reset();
check(wp); // pointer is invlid
return 0;
}
C++与垃圾回收
第三方库Boehm 支持标记-清楚方法的垃圾回收.需要使用库的堆内存分配函数显式代替malloc,继而将堆内存的管理交给垃圾回收器来完成垃圾回收.不过由于C/C++中指针类型的使用十分灵活,这样的库在实际使用中会有一些限制,可移植性也不好.
第6章 提高性能及操作硬件的能力
常量表达式
const
是运行时常量, constexpr
是编译时常量
#include <iostream>
using namespace std;
const int getConst() { return 1; }
constexpr int getConstexpr() { return 2; }
int main() {
int input;
cin >> input;
switch(input) {
case getConst(): // 编译失败
break;
case getConstexpr(): // 编译通过
break;
default:
break;
}
return 0;
}
常量表达式函数
constexpr int getConstexpr() { return 2; }
要求:
- 函数体只有单一的return语句,不能有其他语句
- 函数必须有返回值,不能是void函数
- 使用前定义
- return返回语句表达式不能使用非常量表达式,全局数据,且必须是一个常量表达式
最明显的限制是第一条:要求函数体只有一条语句, 且该语句必须是return语句,意味着
constexpr int getConstexpr() {
const int i = 2;
return 2;
}
是编译不通过的.
不过, 一切不会产生实际代码的语句在常量表达式函数中是可以使用的.
constexpr int getConstexpr() {
static_assert(0 == 0, "assert fail.");
return 2;
}
此外, 如using, typedef等命令也能通过编译
此外, 一些危险操作,比如赋值操作在常量表达式函数中是不允许的
constexpr int getConstexpr(int x) { return x = 1; }
变长模板
[todo]
原子类型与原子操作
原子操作与C++11原子类型
#include <atomic>
#include <iostream>
#include <thread>
using namespace std;
atomic_llong total{0};
void func(int) {
for (long long i = 0; i < 100000000LL; i++) {
total += i;
}
}
int main() {
thread t1(func, 0);
thread t2(func, 0);
t1.join();
t2.join();
cout << total << endl; // 9999999900000000
return 0;
}
使用了原子类型atomic_llong
可以保证数据的原子访问.其他内置类型的原子类型定义
原子类型名称 | 对应的内置类型 |
---|---|
atomic_bool | bool |
atomic_char | char |
atomic_schar | signed char |
atomic_uchar | unsigned char |
atomic_int | int |
atomic_uint | unsigned int |
atomic_short | short |
atomic_ushort | unsigned short |
atomic_long | long |
atomic_ulong | unsigned long |
atomic_llong | long long |
atomic_ullong | unsigned long long |
atomic_char16_t | char16_t |
atomic_char32_t | char32_t |
atomic_wchar_t | wchar_t |
还可以自定义原子类型 std::atomic<T>
, 对于原子类型,智能从模板参数类型中进行构造,不允许拷贝构造,移动构造,赋值构造.
atomic<float> af{1.2f} // 编译成功
atomic<float> af = 1.2f // 编译失败
atomic<float> af1{af} // 编译失败
atomic<float> af1 = af; // 编译失败
从std::atomic<T>
到T
可以直接赋值
atomic<float> af{1.2f}
float f = af
第7章 为改变思考方式而改变
指针空值 — nullptr
nullptr
的数据类型是nullptr_t
typedef decltype(nullptr) nullptr_t;
特点:
nullptr_t
类型数据可以隐式转换成任意一个指针类型nullptr_t
不能转化成非指针类型nullptr_t
不适用于算数运算表达式nullptr_t
只能和指针类型进行比较
默认函数的控制
默认函数有:
- 构造函数
- 拷贝构造函数
- 拷贝赋值函数(operator=)
- 移动构造函数
- 移动拷贝函数
- 析构函数
此外,C++为自定义类型提供全局默认操作符函数 - operator,
- operator &
- operator &&
- operator *
- operator->
- operator->*
- operator new
- operator delete
类与默认函数
class NoCopyCator {
public:
NoCopyCator() = default;
NoCopyCator(const NoCopyCator&) = delete;
// 有效阻止用户错用拷贝构造函数
};
}
default 与 delete
class ConvType
{
public:
ConvType(int i) {}
// 删除 char 版本
ConvType(char c) = delete;
};
void func(ConvType c) {}
int main() {
func(3);
func('a'); // 编译失败
ConvType c1(3);
ConvType c2('a'); // 编译失败
}
delete
不要与explicit
连用
class ConvType
{
public:
ConvType(int i) {}
explicit ConvType(char c) = delete;
// `delete`不要与`explicit`连用
};
void func(ConvType c) {}
int main() {
func(3);
func('a'); // 编译通过,调用 int 版本
ConvType c1(3);
ConvType c2('a'); // 编译失败
}
可以显式删除operator new 函数来避免堆上分配对象
class NoHeapAlloc {
public:
void* operator new(std::size_t) = delete;
};
int main() {
NoHeapAlloc all;
NoHeapAlloc* p = new NoHeapAlloc; // 编译失败
return 0;
}
如果想限制对象在栈上的存储,可以删除析构函数,这样对象可以在堆中存储,不能在栈上或者静态构造
lambda函数
语法定义
[capture](parameters) mutable -> return-type { statement }
其中
captrue
: 捕捉列表, [var]值传递,[=]捕捉父作用域的所有变量(值传递, 包括this), [&var]引用捕捉var, [&]引用捕捉父作用域所有变量, [=, &a, &b]表示a, b引用捕捉,其他值传递.parameters
: 函数列表mutable
: 默认lambda函数式常量函数,表示可以修改按值传入的变量的副本(不是值本身),类似于不带const关键字的形参。使用mutable关键字后对按值传入的变量进行的修改,不会将改变传递到Lambda表达式之外。return-type
:返回值,采用追踪返回类形式声明函数的返回类型,在返回类型明确时,可以省略statement
:函数体
实现原理:现有的C++11标准中lambda等价与有常量的仿函数
[val]() {
val = 3; // 编译失败
}
class const_val_lambda {
public:
const_val_lambda(int v): val(v) {}
void operator()() const { val = 3; } // 编译失败
private:
int val;
}
第 8 章 融入实际应用
原生字符串字面量
#include <iostream>
using namespace std;
int main() {
cout <<
R"(hello, \n
world)"<< endl;
return 0;
}
输出
hello, \n
world