C++ OOP编程综述

Last Updated on 2022年9月30日

嵌套类和局部类

  • 定义在类X内的类Y称为嵌套类,它就是一个普通类,只不过使用的作用域被限制了.
  • 定义在函数内的类Y称为局部类,这种类限制很多,主要用于在语言层面支持Lambda,实践中基本不用.

struct和class的内存布局

  • C++的struct和class实例大小不会为0,即便它内部没有任何成员函数或成员变量.主要是因为编译器总是需要为实例分配内存,使得对象能获得有效的地址,且不同对象的地址总应该是不同的.
  • 类型布局相关的种类有很多,如POD,Aggragate,naive等,具体我也不是很清楚,总之很麻烦.
    • 如果需要把sturct或class对象通过C风格直接从内存层面管理(如memcpy),那么就必须要求它的内存布局是平凡的.
  • C++11之后,编译器只保证:
    • 同一个权限声明符block内的对象是顺序存储的(可能因对齐而不连续)
    • 同一个权限声明符level的不同block相对顺序是确定的
    • 编译器允许在满足上面约束的前提下任意重排类的内存布局.
  • 多继承时,相当于每个基类对象顺序排布在内存中.
    • 例如,若C继承自A和B,那么C的布局整体上看是[[vptrA,...][vptrB,...]],vptrA指向了TableC_A,vptrB指向了TableC_B.一般来说TableC_A内不但包含了A相关的虚函数地址,还包含了B相关的虚函数, 及自身新定义的虚函数,用于加快查表的效率.TableC_B内则仅包含了B相关的虚函数.
    • 虚函数表的每个entry一般需要保存两个值,一个是函数的入口,另一个是this指针的调整量.
    • C可以直接截断变成A的一个实例, 但是当涉及B时, 编译器则可能需要插入指令调整指针值.
#include <cstdio>
struct A {
  virtual void a() {
    printf("%p A::a called \n", this);
  }
};

struct B {
  virtual void no_override_b() {
    printf("%p B::no_override_b called \n", this);
  }
  virtual void override_b() {
    printf("%p B::no_override_b called \n", this);
  }
  void normal_b() {
    printf("%p B::normal_b called \n", this);
  }
};

struct C : public A, public B {
  void override_b() override {
    printf("%p C::override_b called \n", this);
  }
  void normal_c() {
    printf("%p C::normal_c called \n", this);
  }
};

int main() {
  C *p = new C;
  p->no_override_b();
  p->override_b();
  p->normal_b();
  p->normal_c();

  printf("p is %p \n",p);
  auto p_b = static_cast<B *>(p);
  printf("p_b is %p \n",p_b);
  p_b->no_override_b();
  p_b->override_b();
  p_b->normal_b();
}

// 0x55d374db4e78 B::no_override_b called 
// 0x55d374db4e70 C::override_b called 
// 0x55d374db4e78 B::normal_b called 
// 0x55d374db4e70 C::normal_c called 
// p is 0x55d374db4e70 
// p_b is 0x55d374db4e78 
// 0x55d374db4e78 B::no_override_b called 
// 0x55d374db4e70 C::override_b called 
// 0x55d374db4e78 B::normal_b called 

OOP基础特性:

  • 如果没有特别说明,就永远只使用public继承,privateprotect继承并非面向对象设计中的组件,而是为了实现其他功能的,在C++中从不应该使用.
  • 继承过程中基类成员的可见性可以通过using T::name手动控制.
    • 例如,基类中有public的function(),我们可以在派生类的private中写using Base::function,那么就不能通过派生类实例访问function了
  • 从实现上说,const成员函数只约束类实例内存区域不可写(bitwise-constness).例如,一个class有一个int *p成员,那么,在const成员函数中,修改p是不可以的,修改*p则是可以的.

friend

  • friend是编译期的,其作用是将当前类的自定义成员向某个作用域开放访问(撤销private限制),继承得到的成员则不受任何影响
    • 这个作用域可以是一个函数,或一个类
    • 只要该作用域能够访问到实例,就能访问该实例的数据成员,或使用成员函数.
    • 即便该作用域不能访问到类实例,也可以直接使用不含this的private成员函数,或者调用private构造函数创建对象.
  • friend语句不是函数声明语句.

this

  • 从逻辑上看,this在形参列表中为(T * const this, ...)
    • 我们可以额外为this添加&,&&const
    • const要求this指向const对象
    • &则要求this指向一个左值
    • &&要求this指向一个右值
  • this的实现方式随编译器ABI而定.

类内基本控制函数

构造函数ClassName(args):{}

  • 对于const类实例,仅当构造函数的代码区执行完后实例才获得const属性.
  • 只有函数调用才能触发隐式类型转换.(重载运算符也是函数调用,也能触发)
  • 添加explicit ClassName(TYPE n)限定符可以阻止编译器进行隐式转换.

拷贝控制

  • 涉及拷贝构造(移动构造),拷贝赋值(移动赋值),析构这三类操作的控制函数被称为copy control,对于旧版本C++,有3个,对于C++11,则有五个
  • 在一些场合,编译器可能优化掉拷贝初始化操作. 例如string str_obj="mystr";,它实际是string str_obj(string("mystr")), 可能会被优化为string str("mystr"),第二步的拷贝初始化就被绕过了.

= delete(C++11)

  • 用于定义"不能被调用"的函数,=delete的函数会参与函数重载,当匹配到=delete的函数时,编译器将报错.例如,对于一个mutex,它显然是不具备拷贝意义的,我们就可以让class Mutex的拷贝构造/拷贝移动为=delete

=default(C++11)

  • 使用=default可以强制编译器按默认规则生成对应的函数
    • 如果编译器无法生成,则生成的版本是=delete的.
  • 在声明处的=default将会使编译器生成inline的版本

编译器的隐式合成行为

  • 编译器会自动插入xxx=default这样的代码,称之为隐式合成.具体而言,只有默认构造函数T()和五个拷贝控制函数是可能被隐式合成的.
    • 有了任意显式构造函数都不会合成T();
    • 有了任意显式拷贝控制都不会合成其余的拷贝控制;(有的编译器不严格执行)
    • 注意:即便是T(xxx) =default这样的代码,仍被认为是"显式"的
  • 如果T存在不能拷贝的成员,则编译器将合成=delete版本的拷贝赋值及拷贝初始化.
  • 只有当所有成员都可以移动时,编译器才会合成移动构造和移动赋值.换言之,只要编译器隐式合成了移动控制函数,就一定是可用的,不会是=delete
  • "没有合成"与"=delete"是不同的. 没有合成意味着它不存在,不会参与重载, =delete的版本会参与函数匹配和重载,如果=delete的版本被匹配到,将导致编译错误.

动态绑定与多态

  • 返回类型放松:对于T内的virtual函数,如果返回类型是T&T*我们可以在override的时将返回类型修改为派生类的T2&T2*
  • 在构造函数和析构函数中调用虚函数仅会静态绑定.这是为了避免安全问题.例如,对于class B: public A{virtual void fun();},在构造A时需要调用fun(),若因动态绑定调用了B的版本,由于此时B仍未构造,此时调用B的版本就可能访问未初始化的区域,这是十分危险的.
  • 多态调用与普通调用的一个简单逻辑展示如下:
    • 当编译器发现p->fun(args)是普通调用时,将其翻译为T::fun(p,args),本质仍是一个普通调用.
    • 当编译器发现p->vfun(args)是虚函数调用时,就将其翻译为:p->__vptr[ind_of_vfun](p + offset,args),所以虚函数的调用依赖实例.