C++ 知识碎片

 
  • 检查函数是否修改通过引用或指针传递的参数,如果不修改,应该使用 const 修饰参数。
    • 优先考虑 const_iterator而非 iterator
    • 如果成员函数不修改对象,应该使用 const 修饰函数。
  • range-for 语句中,应该使用 auto 关键字,避免类型错误。如果不修改元素,应该使用 const 修饰。
  • 使用标准库中的类型和函数,避免自己造轮子。
  • 执行查找常见的模式,看是否有更好的替代方案, 使得代码更清晰地表达意图
    • 简单的for循环,可以使用 std::for_eachrange_for
    • f(T*, int) 接口 vs. f(span<T>) 接口
      • span 是 C++20 中引入的一个新的标准容器,它用于表示连续的一段内存区间,类似于一个轻量级的只读数组容器。

  • 不要将可以在编译时可以很好完成的工作推迟到运行时。
  • 不能在编译时检查的内容应在运行时检查
  • class 专属常量是指只能在类内部使用的常量,可以使用 static const 来定义。
    • static 成员变量只能在类外初始化(为了避免静态变量被多次初始化)
      • static const类型除了整型数可以在类内初始化(只是一个申明式,还要在类外提供定义),其他的只能在类外初始化。
    • 如果是是一个 static const 整数类型成员变量,我们可以使用它们而无需提供定义式,但要取地址时,必须提供定义式。
    • 在C++11中加入了类内初始化,常规的数据成员变量能在类内、构造函数里和初始化列表里进行初始化。const类型的成员变量只能在初始化列表里并且必须在这里进行初始化。static类型只能在类外进行初始化。static const类型除了整型数可以在类内初始化,其他的只能在类外初始化。
      enum hack
      是旧式枚举的经典用法,它是把枚举元素用做一个编译时整型常量来用,在C++ 模板元编程中特别常用。
  • 如果一个函数返回一个值类型,也可以考虑将该值声明为const,这样可以避免用户的误操作。
  • 如果一个成员函数有两个版本,一个是 const 修饰的,一个是非 const 修饰的,为了避免代码重复,可以将 非const 修饰的版本调用 const 修饰的版本。
class A {
public:
    const int  f() const {
       // do something
    }
    int f() {
      return const_cast<int*>(static_cast<const A*>(this)->f());
    }
};
  • 无论是定义于global、namespace作用域内的对象、在class内、在函数内、以及在file作用域内被声明为static的对象,它们都放在静态存储区域,其寿命为整个程序的运行期间。
  • 多个文件中非局部static变量的依赖关系的解决方法
    • 将非局部static变量放在一个专属函数内变为一个局部static变量,这样就可以保证这个变量只在这个函数内可见,只有第一次调用这个函数才会初始化这个变量。这是单例模式的一种实现方式。

  • 不在构造函数和析构函数中调用虚函数
    • 在构造函数调用虚函数,会导致继承类的构造函数中调用的是基类的虚函数,而不是派生类的虚函数。因为在构造函数中,派生类的成员(包括虚函数指针)还没有初始化。

  • 使自己重载的operator=、 operator+ …. 返回一个reference to *this
    • 使自己重载的operator=返回一个reference to *this,这样可以支持连续赋值,如 a=b=c=d。

  • 在拷贝赋值运算符 operator= 中,应该检查是否是自我赋值。

一个简单的例子:

class Bitmap {...};
class Widget {
  ...
private:
  Bitmap *pb;
};

下面是一个错误的实现:

Widget& Widget::operator=(const Widget& rhs) {
  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}

如果 rhs*this 是同一个对象,那么 delete pb 会删除 rhs.pb,然后 pb = new Bitmap(*rhs.pb) 会复制一个已经被删除的对象,这是错误的。正确的实现如下:

Widget& Widget::operator=(const Widget& rhs) {
  if (this == &rhs) return *this;
  delete pb;
  pb = new Bitmap(*rhs.pb);
  return *this;
}

但是,如果 new Bitmap 的抛出异常,那么 pb 就会被删除,但是没有被赋值,这样就会导致 pb 指向一个已经被删除的对象。正确的实现如下:

Widget& Widget::operator=(const Widget& rhs) {
  Bitmap *pOrig = pb;
  pb = new Bitmap(*rhs.pb);
  delete pOrig;
  return *this;
}
  • RAII (Resource Acquisition Is Initialization) 资源获取即初始化
    • RAII 是一种 C++ 软件设计范式,它利用对象生命周期来管理资源。RAII 的关键思想是,资源的获取和释放应该由对象的构造函数和析构函数来管理。这样可以确保资源在对象生命周期结束时被释放,从而避免资源泄漏。

    • 能够解决一个函数中有多个 return 语句,但是需要释放资源的问题。

  • 如果我们要拷贝一个我们自己定义的管理资源的类,我们应该根据我们想要的语义来实现拷贝构造函数和拷贝赋值运算符。
    • 如果我们想要的是共享资源,我们应该使用引用计数;
    • 如果我们想要的是深拷贝,我们应该在拷贝构造函数和拷贝赋值运算符中实现深拷贝。
    • 如果我们不想拷贝资源,我们应该禁用拷贝构造函数和拷贝赋值运算符。
  • 使用new [n] 分配数组内存时,会在分配的内存前面存储数组的长度,所以我们在使用delete []释放数组内存时,才能正确释放内存。
  • decltype(auto) 看似很矛盾,但确实是有用的。c++14中如果只使用auto推导函数的返回值,会忽略引用和cv限定符,decltype(auto)可以保留这些限定符。
  • 对于T类型的不是单纯的变量名的左值表达式,decltype总是产出T的引用即T&。
  • 在有继承关系时(子类相对于其直接父类)
    • 一般继承时,子类的虚函数表中先将父类虚函数放在前,再放自己的虚函数指针。
    • 如果子类中覆盖了父类的虚函数,虚函数指针将被放到子类虚表中原来父类虚函数的位置。
    • 在多继承的情况下,每个父类都有自己的虚表,子类的虚成员函数被放到了第一个父类的表中。也就是说当类在多重继承中时,其实例对象的内存结构并不只记录一个虚函数表指针。基类中有几个存在虚函数,则子类就会保存几个虚函数表指针
  • 内联函数 (inline)

虚函数用于实现运行时的多态,或者称为晚绑定或动态绑定。而内联函数用于提高效率。内联函数的原理是,在编译期间,对调用内联函数的地方的代码替换成函数代码。内联函数对于程序中需要频繁使用和调用的小函数非常有用。默认地,类中定义的所有函数,除了虚函数之外,会隐式地或自动地当成内联函数(注意:内联只是对于编译器的一个建议,编译器可以自己决定是否进行内联).无论何时,使用基类指针或引用来调用虚函数,它都不能为内联函数(因为调用发生在运行时)。但是,无论何时,使用类的对象(不是指针或引用)来调用时,可以当做是内联,因为编译器在编译时确切知道对象是哪个类的。所以inline函数可以是虚函数,取决于这个函数能不能在编译器就确定

  • 静态成员函数 (static)

static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。此外静态与非静态成员函数之间有一个主要的区别,那就是静态成员函数没有this指针,从而导致两者调用方式不同。虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable。虚函数的调用关系:this -> vptr -> vtable ->virtual function,对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual。

  • 构造函数 (constructor)

虚函数基于虚表vtable(内存空间),构造函数 (constructor) 如果是virtual的,调用时也需要根据vtable寻找,但是constructor是virtual的情况下是找不到的,因为constructor自己本身都不存在了,创建不到class的实例,没有实例class的成员(除了public static/protected static for friend class/functions,其余无论是否virtual)都不能被访问了。此外构造函数不仅不能是虚函数。而且在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好,多态是被disable的。

  • 析构函数 (deconstructor)

对于可能作为基类的类的析构函数要求就是virtual的。因为如果不是virtual的,派生类析构的时候调用的是基类的析构函数,而基类的析构函数只要对基类部分进行析构,从而可能导致派生类部分出现内存泄漏问题。

  • 纯虚函数

析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。

虚函数重载时建议都写上override,这样可以让编译器帮助检查是否真的重载了虚函数。如果只写了virtual,而不是override,那么可能会出现一些不是很直观的原因造成的错误(想要重写,结果实际上变成了重载)。 成员函数引用限定(reference qualifiers)是在成员函数的参数列表之后,成员函数的常量性之前的一个新的限定符。引用限定符可以用来限定只有左值或右值才能调用成员函数。引用限定符可以是 &&&,分别表示只有左值或右值才能调用成员函数。引用限定符只能用于成员函数,不能用于全局函数。 如果函数不抛出异常请使用 noexcept 修饰函数,这样可以让编译器优化代码。如果函数不抛出异常请使用 noexcept。 尽可能的使用 constexpr