深入理解C++11

来源:互联网 发布:ping ip或域名 编辑:程序博客网 时间:2024/06/01 11:20

  • 第2章 保证稳定性和兼容性
    • finaloverride 控制
    • 局部和匿名类型作模板实参
  • 第3章 通用为本专用为末
    • 继承构造函数
    • 委派构造函数
    • 右值引用移动语义和完美转发
      • 移动语义
      • 右值和左值的区别
      • stdmove 强制转化为右值
      • 完美转发
    • 列表初始化
      • 初始化列表
    • POD类型
    • 模板别名
  • 第4章 新手易学老兵易用
    • auto类型推导
    •  decltype
      • decltype四规则
    • 追踪返回类型
  • 第5章 提高类型安全
    • 强类型枚举
    • C11的智能指针
      • unique_ptr
      • shared_ptr
      • weak_ptr
      • C与垃圾回收
  • 第6章 提高性能及操作硬件的能力
    • 常量表达式
      • 常量表达式函数
    • 变长模板
    • 原子类型与原子操作
      • 原子操作与C11原子类型
  • 第7章 为改变思考方式而改变
    • 指针空值 nullptr
    • 默认函数的控制
      • 类与默认函数
      • default 与 delete
    • lamda函数
  • 第 8 章 融入实际应用
    • 原生字符串字面量

整理  <<深入理解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 DKnurh() = 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() 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) {} // 显式定义}

注意:
1. 基类的构造函数是私有函数,不能继承
2. 使用了继承构造函数,那么不再为派生类生成默认构造函数.

委派构造函数

委派构造,就是指委派函数将构造的任务委派给委派构造函数(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: 1Copy construct: 1Destruct: 1Copy construct: 2Destruct: 2Destruct: 3

调用过程

graph TDGetTemp(( ))--构造-->HasPrtMemHasPrtMem--拷贝构造-->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)的好处:
1. 字节赋值,安全的使用memset和memcpy对POD类型初始化
2. 对C内存布局兼容
3. 保证了静态初始化的安全有效

模板别名

在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 doubleconst auto& b    = foo();   // const double&volatile auto* c = foo();   // volatile float*auto d           = a;       // doubleauto& 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
1. 如果e是一个没有带括号的标记表达式或者类成员访问表达式,那么decltype(e)就是e所命名的实体的类型.
2. 否则, 如果e是一个将亡值,那么decltype(e) 为 T&&
3. 否则, 如果e是一个左值,那么decltype(e) 为 T&
4. 否则, 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; }
要求:
1. 函数体只有单一的return语句,不能有其他语句
2. 函数必须有返回值,不能是void函数
3. 使用前定义
4. 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;

特点:
1. nullptr_t 类型数据可以隐式转换成任意一个指针类型
2. nullptr_t 不能转化成非指针类型
3. nullptr_t 不适用于算数运算表达式
4. nullptr_t 只能和指针类型进行比较

默认函数的控制

默认函数有:
1. 构造函数
2. 拷贝构造函数
3. 拷贝赋值函数(operator=)
4. 移动构造函数
5. 移动拷贝函数
6. 析构函数
此外,C++为自定义类型提供全局默认操作符函数
1. operator,
2. operator &
3. operator &&
4. operator *
5. operator->
6. operator->*
7. operator new
8. 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;}

如果想限制对象在栈上的存储,可以删除析构函数,这样对象可以在堆中存储,不能在栈上或者静态构造

lamda函数

语法定义
[capture](parameters) mutable -> return-type { statement }
其中
1. captrue: 捕捉列表, [var]值传递,[=]捕捉父作用域的所有变量(值传递, 包括this), [&var]引用捕捉var, [&]引用捕捉父作用域所有变量, [=, &a, &b]表示a, b引用捕捉,其他值传递.
2. parameters: 函数列表
3. mutable: 默认情况下,lamda函数是const函数,mutable可以取消其常量性
4. return-type:返回值,采用追踪返回类形式声明函数的返回类型,在返回类型明确时,可以省略
5. statement:函数体

现有的C++11标准中lamda等价与有常量的仿函数

[val]() {  val = 3;      // 编译失败}class const_val_lamda {public:  const_val_lamda(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
原创粉丝点击