Table of Contents:

开篇词 | C++这么难,为什么我们还要用C++?

C++ 的国际标准有 6 个版本,98, 03, 11, 14, 17,20 。
C++11则是一次全面的大进化。
C++11—之后,C++ 以每三年一版的频度发布着新的语言标准。
C++ 是一门多范式的通用编程语言,这些不同的范式,都可以在同一项目中组合使用,这就大大增加了开发的灵活性。因此,C++ 适用的领域非常广泛,小到嵌入式,大到分布式服务器,到处可以见到 C++ 的身影。

C++ 的核心竞争力:
* 抽象能力:意味着较高的开发效率,同时,更重要的是,不会因抽象而降低性能。
* 性能:这不用多说了,就是快并且占用资源少。
* 功耗:这是近年来我们越来越关注的问题,跟性能直接相关,性能好了功耗自然就低。移动设备上。
* C语言的兼容性,也是 C++ 的一大优势

目前,跟 C++ 定位差不多、能有直接竞争关系的,也就是既支持高度抽象、又追求高性能的通用编程语言,其实只有 Rust 一种。
我的个人经验,完成同样的功能,C++ 需要的代码行数一般是 Python 的三倍左右,而性能则可以达到Python 的十倍以上。
当你的软件属于运算密集或者内存密集型,你需要性能、且愿意为性能付出额外代价的时候,应该考虑用 C++,特别在你的代码需要部署在多台服务器或者移动设备的场合。反之,如果性能不会成为你开发的软件的瓶颈,那 C++ 可能就不是一个最合适的工具。

课前必读 | 有关术语发音及环境要求

对于 CentOS 7,系统安装的 GCC 版本是 4.8,太老,你可以通过安装 centos-
release-scl 和 devtoolset-7-gcc-c++ 两个包来获得 GCC 7;随后,可以使用命令 scl
enable devtoolset-7 bash 或 . /opt/rh/devtoolset-7/enable 来启用 GCC7

可以使用ubuntu上的版本更加新
即使不用较新的 C++ 特性,你也一定要用比较新的编译器。单单是输出错误信息的友好程度,老版本和新版本就是没法比的。

01 | 堆、栈、RAII:C++里该如何管理资源?

在 C++ 里,所有的变量缺省都是值语义——如果不使用 * 和 & 的话,变量不会像 Java 或 Python 一样引用一个堆上的对象。

例子:

void foo()
{
bar* ptr = new bar();
…
delete ptr;
}

存在两个问题:
1. 省略的代码部分也许会抛出异常,导致最后的 delete ptr 得不到执行。
2. 更重要的,这个代码不符合 C++ 的惯用法。在 C++ 里,这种情况下有 99% 的可能性不应该使用堆内存分配,而应使用栈内存分配。这样写代码的,估计可能是从 Java 转过来的(偷笑)——但我真见过这样的代码

C++ 支持将对象存储在栈上面。但是,在很多情况下,对象不能,或不应该,存储在栈上。比如:
- 对象很大;
- 对象的大小在编译时不能确定;
- 对象生命周期超过了栈的生命周期

栈上的分配极为简单,移动一下栈指针而已。
栈上的释放也极为简单,函数执行结束时移动一下栈指针即可。
由于后进先出的执行过程,不可能出现内存碎片。

编译器会自动调用析构函数,包括在函数执行发生异常的情况。在发生异常时对析构函数的调用,还有一个专门的术语,叫栈展开(stack unwinding)。

RAII

RAII,完整的英文是 Resource Acquisition Is Initialization,是 C++ 所特有的资源管理方式。有少量其他语言,如 D、Ada 和 Rust 也采纳了 RAII。
一句话概况:构造中申请资源,析构中释放资源
RAII 依托栈和析构函数,来对所有的资源——包括堆内存在内——进行管理。对 RAII 的使用,使得 C++ 不需要类似于 Java 那样的垃圾收集方法,也能有效地对内存进行管理。
RAII 的存在,也是垃圾收集虽然理论上可以在 C++ 使用,但从来没有真正流行过的主要原因。

02 | 自己动手,实现C++的智能指针

智能指针本质上并不神秘,其实就是 RAII 资源管理功能的自然展现而已

最简单的实现

class shape_wrapper {
public:
explicit shape_wrapper(
shape* ptr = nullptr)
ptr_(ptr) {}
~shape_wrapper()
{
delete ptr_;
}
shape* get() const { return ptr_; }
private:
    shape* ptr_;
};

这个类可以完成智能指针的最基本的功能:对超出作用域的对象进行释放。但它缺了点东西:
1. 只适用于 shape 类 
2. 该类对象的行为不够像指针 
3. 拷贝该类对象会引发程序行为异常 

增加模板化

template <typename T>
class smart_ptr {
public:
  explicit smart_ptr(T* ptr = nullptr)
    : ptr_(ptr) {}
  ~smart_ptr()
  {
    delete ptr_;
  }
  T* get() const { return ptr_; }
private:
  T* ptr_;
};

目前这个 smart_ptr 的行为还是和指针有点差异的:
它不能用 * 运算符解引用
它不能用 -> 运算符指向对象成员
它不能像指针一样用在布尔表达式里

故增加下面几个方法

template <typename T>
class smart_ptr {
public:

  T& operator*() const { return *ptr_; }
  T* operator->() const { return ptr_; }
  operator bool() const { return ptr_; }
}

shared_ptr的简单实现

class shared_count {
public:
  shared_count() noexcept
    : count_(1) {}
  void add_count() noexcept
  {
    ++count_;
  }
  long reduce_count() noexcept
  {
    return --count_;
  }
  long get_count() const noexcept
  {
    return count_;
  }
private:
  long count_;
};
template <typename T>
class smart_ptr {
public:
  template <typename U>
  friend class smart_ptr;
  explicit smart_ptr(T* ptr = nullptr)
    : ptr_(ptr)
  {
    if (ptr) {
      shared_count_ =
        new shared_count();
    }
  }
  ~smart_ptr()
  {
    printf("~smart_ptr(): %p\n", this);
    if (ptr_ &&
      !shared_count_
         ->reduce_count()) {
      delete ptr_;
      delete shared_count_;
    }
  }
  template <typename U>
  smart_ptr(const smart_ptr<U>& other) noexcept
  {
    ptr_ = other.ptr_;
    if (ptr_) {
      other.shared_count_->add_count();
      shared_count_ = other.shared_count_;
    }
  }
  template <typename U>
  smart_ptr(smart_ptr<U>&& other) noexcept
  {
    ptr_ = other.ptr_;
    if (ptr_) {
      shared_count_ =
        other.shared_count_;
      other.ptr_ = nullptr;
    }
  }
  template <typename U>
  smart_ptr(const smart_ptr<U>& other,
            T* ptr) noexcept
  {
    ptr_ = ptr;
    if (ptr_) {
      other.shared_count_
        ->add_count();
      shared_count_ =
        other.shared_count_;
    }
  }
  smart_ptr&
  operator=(smart_ptr rhs) noexcept
  {
    rhs.swap(*this);
    return *this;
  }
  T* get() const noexcept
  {
    return ptr_;
  }
  long use_count() const noexcept
  {
    if (ptr_) {
      return shared_count_
        ->get_count();
    } else {
      return 0;
    }
  }
  void swap(smart_ptr& rhs) noexcept
  {
    using std::swap;
    swap(ptr_, rhs.ptr_);
    swap(shared_count_,
         rhs.shared_count_);
  }
  T& operator*() const noexcept
  {
    return *ptr_;
  }
  T* operator->() const noexcept
  {
    return ptr_;
  }
  operator bool() const noexcept
  {
    return ptr_;
  }
private:
  T* ptr_;
  shared_count* shared_count_;
};
template <typename T>
void swap(smart_ptr<T>& lhs,
          smart_ptr<T>& rhs) noexcept
{
  lhs.swap(rhs);
}
template <typename T, typename U>
smart_ptr<T> static_pointer_cast(
  const smart_ptr<U>& other) noexcept
{
  T* ptr = static_cast<T*>(other.get());
  return smart_ptr<T>(other, ptr);
}
template <typename T, typename U>
smart_ptr<T> reinterpret_pointer_cast(
  const smart_ptr<U>& other) noexcept
{
  T* ptr = reinterpret_cast<T*>(other.get());
  return smart_ptr<T>(other, ptr);
}
template <typename T, typename U>
smart_ptr<T> const_pointer_cast(
  const smart_ptr<U>& other) noexcept
{
  T* ptr = const_cast<T*>(other.get());
  return smart_ptr<T>(other, ptr);
}
template <typename T, typename U>
smart_ptr<T> dynamic_pointer_cast(
  const smart_ptr<U>& other) noexcept
{
  T* ptr = dynamic_cast<T*>(other.get());
  return smart_ptr<T>(other, ptr);
}

03 | 右值和移动究竟解决了什么问题?

实现移动的意义是减少运行的开销——在引用计数指针的场景下,这个开销并不大。

04 | 容器汇编 I:比较简单的若干容器

image.png
image.png
image.png
- 如果只从头、尾两个位置对 deque 进行增删操作的话,容器里的对象永远不需要移动。
- 容器里的元素只是部分连续的(因而没法提供 data 成员函数)。
- 由于元素的存储大部分仍然连续,它的遍历性能是比较高的。
- 由于每一段存储大小相等,deque 支持使用下标访问容器元素,大致相当于 index[i / chunk_size][i % chunk_size],也保持高效。

deque的扩容

map
 │
 ├── chunk0 → [0][1][2][3]   ← 已满
 ├── chunk1 → [4][5][6][7]
 ├── chunk2 → [8][9][10][11]

此时需要扩展 map

new_map
 ├── nullptr
 ├── nullptr
 ├── chunk_new
 ├── chunk0
 ├── chunk1
 ├── chunk2
 ├── nullptr
 ├── nullptr

05 | 容器汇编 II:需要函数对象的容器

std::array

std::array 本质上几乎就是:

template<typename T, size_t N>
struct array
{
    T elems[N];
};

例如:

std::array<int, 5> arr;

std::array<int, 1000000> arr; 会发生什么?
1000000 * 4 约:4MB 可能直接:stack overflow

06 | 异常:用还是不用,这是个问题

没有异常的世界

// 矩阵定义
typedef struct {
  float* data;
  size_t nrows;
  size_t ncols;
} matrix;

enum matrix_err_code {
  MATRIX_SUCCESS,
  MATRIX_ERR_MEMORY_INSUFFICIENT,

};

// 分配矩阵
int matrix_alloc(matrix* ptr,
                 size_t nrows,
                 size_t ncols)
{
  size_t size =
    nrows * ncols * sizeof(float);
  float* data = malloc(size);
  if (data == NULL) {
    return MATRIX_ERR_MEMORY_INSUFFICIENT;
  }
  ptr->data = data;
  ptr->nrows = nrows;
  ptr->ncols = ncols;
}

// 清理矩阵
void matrix_dealloc(matrix* ptr)
{
  if (ptr->data == NULL) {
    return;
  }
  free(ptr->data);
  ptr->data = NULL;
  ptr->nrows = 0;
  ptr->ncols = 0;
}

// 矩阵乘法
int matrix_multiply(matrix* result,
                    const matrix* lhs,
                    const matrix* rhs)
{
  int errcode;
  if (lhs->ncols != rhs->nrows) {
    return MATRIX_ERR_MISMATCHED_MATRIX_SIZE;
    //  呃,得把这个错误码添到  enum matrix_err_code  里
  }
  errcode = matrix_alloc(
    result, lhs->nrows, rhs->ncols);
  if (errcode != MATRIX_SUCCESS) {
    return errcode;
  }
  //  进行矩阵乘法运算
  return MATRIX_SUCCESS;
}

//===============调用=================

  matrix c;
  //  不清零的话,错误处理和资源清理会更复杂
  memset(c, 0, sizeof(matrix));
  errcode = matrix_multiply(c, a, b);
  if (errcode != MATRIX_SUCCESS) {
    goto error_exit;
  }
  //  使用乘法的结果做其他处理
error_exit:
  matrix_dealloc(&c);
  return errcode;

有异常处理的代码


class matrix {

private:
  float* data_;
  size_t nrows_;
  size_t ncols_;
}


matrix::matrix(size_t nrows,
               size_t ncols)
{
  data_  = new float[nrows * ncols];
  nrows_ = nrows;
  ncols_ = ncols;
}


matrix::~matrix()
{
  delete[] data_;
}
class matrix {

  friend matrix
  operator*(const matrix&,
            const matrix&);
};
matrix operator*(const matrix& lhs,
                 const matrix& rhs)
{
  if (lhs.ncols != rhs.nrows) {
    throw std::runtime_error(
      "matrix sizes mismatch");
  }
  matrix result(lhs.nrows, rhs.ncols);
  //  进行矩阵乘法运算
  return result;
}

// ===============调用====================
matrix c = a * b;

栈展开
栈展开(Stack Unwinding)就是异常从抛出点向上寻找匹配 catch 的过程中,依次销毁沿途函数栈帧中的局部对象并执行其析构函数的过程。为了知道每个位置该析构哪些对象,编译器会生成额外的 异常展开表(unwind table),这也是开启 C++ 异常支持后可执行文件体积变大的主要原因。

因为每个可能抛异常的函数都要保存:
- 如何展开
- 析构哪些对象
- catch 在哪里

例如:

void f()
{
    std::string s1;
    std::string s2;
    std::vector<int> v;

    foo();
}

编译器需要记录:

如果 foo() 抛异常:  
  
v.~vector()  
s2.~string()  
s1.~string()

上面这些信息被写进可执行文件。
因此:开启异常支持 通常会导致:10%~20% 甚至更多的二进制体积增长。

07 | 迭代器和好用的新for循环

迭代器(Iterator)的本质是一个“像指针一样访问元素的对象”。 能前后移动,能读取和修改。
迭代器本质上是“容器访问逻辑的封装”。

高级理解(STL设计思想)

迭代器是“遍历容器”的统一抽象接口,把不同数据结构(数组、链表、红黑树、deque)的访问方式封装成一致的操作,使算法与容器解耦。

这正是 STL 的核心思想:

Algorithm + Iterator + Container

简单实现

template<typename T>
class SimpleArray
{
public:
    class iterator
    {
    public:
        explicit iterator(T* ptr)
            : ptr_(ptr)
        {
        }

        T& operator*() const
        {
            return *ptr_;
        }

        iterator& operator++()
        {
            ++ptr_;
            return *this;
        }

        bool operator!=(const iterator& other) const
        {
            return ptr_ != other.ptr_;
        }

    private:
        T* ptr_;
    };

    explicit SimpleArray(size_t n)
        : size_(n)
    {
        data_ = new T[n];
    }

    ~SimpleArray()
    {
        delete[] data_;
    }

    iterator begin()
    {
        return iterator(data_);
    }

    iterator end()
    {
        return iterator(data_ + size_);
    }

    T& operator[](size_t i)
    {
        return data_[i];
    }

private:
    T* data_;
    size_t size_;
};

//===================使用====================
int main()
{
    SimpleArray<int> arr(5);

    for (int i = 0; i < 5; ++i)
    {
        arr[i] = i + 1;
    }

    for (auto it = arr.begin();
         it != arr.end();
         ++it)                // 核心
    {
        std::cout << *it << ' ';
    }
}