深入理解C++11


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

注意:

  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: 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)的好处:

  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 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

  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_boolbool
atomic_charchar
atomic_scharsigned char
atomic_ucharunsigned char
atomic_intint
atomic_uintunsigned int
atomic_shortshort
atomic_ushortunsigned short
atomic_longlong
atomic_ulongunsigned long
atomic_llonglong long
atomic_ullongunsigned long long
atomic_char16_tchar16_t
atomic_char32_tchar32_t
atomic_wchar_twchar_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++为自定义类型提供全局默认操作符函数
  7. operator,
  8. operator &
  9. operator &&
  10. operator *
  11. operator->
  12. operator->*
  13. operator new
  14. 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 }
其中

  1. captrue: 捕捉列表, [var]值传递,[=]捕捉父作用域的所有变量(值传递, 包括this), [&var]引用捕捉var, [&]引用捕捉父作用域所有变量, [=, &a, &b]表示a, b引用捕捉,其他值传递.
  2. parameters: 函数列表
  3. mutable: 默认lambda函数式常量函数,表示可以修改按值传入的变量的副本(不是值本身),类似于不带const关键字的形参。使用mutable关键字后对按值传入的变量进行的修改,不会将改变传递到Lambda表达式之外。
  4. return-type:返回值,采用追踪返回类形式声明函数的返回类型,在返回类型明确时,可以省略
  5. 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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 3

打赏作者

zhzdeng

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值