面向对象的C++

介绍C++中面向对象相关知识。

继承和多态

常见问题

数据成员有多态性么?

如下所示,答案是没有,取决于用什么指针去访问。其实也很容易想到,只有函数才会创建虚表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <cstdio>

struct B {
virtual ~B() {}
virtual int gz() { return z; }
int z = 1;
};

struct D : B {
~D() {}
virtual int gz() { return z; }
int z = 2;
};

int main() {
D* dp = new D();
B* bp = dp;
// dp 2 bp 1 df 2 bf 2
printf("dp %d bp %d df %d bf %d\n", dp->z, bp->z, dp->gz(), bp->gz());
}

public、protected、private 继承的区别是什么?

特别注意,struct 默认继承是 public;class 默认是 private。
一般涉及到虚函数的继承,都得是 public 的,否则在将派生类指针赋值给基类指针时,会报错”error: ‘B’ is an inaccessible base of ‘D’”。

如何声明一个抽象类(纯虚类)?

只要有一个纯虚函数的类就是抽象类。但注意,抽象类中必须定义一个虚的析构函数,并且不能是纯虚的,否则会链接错误。

如何处理同名数据成员和成员函数(非 virual 情况)?

如下所示,默认情况下是访问的派生类。但可以用->B::x.B::x来限定访问基类,或者直接使用基类指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <cstdio>

struct B {
int gz() { return z; }
int z = 1;
};

struct D : B {
int gz() { return z; }
int z = 2;
};

int main() {
D* dp = new D();
B* bp = dp;
// dp 2 df 2 dp(as base) 1 bp 1 df(as base) 1 bf 1
printf("dp %d df %d dp(as base) %d bp %d df(as base) %d bf %d\n", dp->z, dp->gz(), dp->B::z, bp->z, dp->B::gz(), bp->gz());
}

一旦继承,这些同名成员其实都是会保存两份的。比如在 https://github.com/pingcap/tiflash/pull/6041 中我就同时用了继承+持有的方式去实现:通过继承,可以复用接口;通过持有,可以复用实现。但这样的问题就是 SSTReader 的私有成员其实被重复创建了。一种方式是改成 protected,让 MultiSSTReader 共享。但其实这样会破坏我们“只是复用接口才继承”的目的。这么做比较炫技,更 neat 一点的方式是让 SSTReader 变成抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SSTReader {
virtual void next();
virtual char * key();
...
private:
Context * ctx;
Inner * inner;
}

class MultiSSTReader : public SSTReader {
void next() override;
char * key() override;
...
SSTReader * current;
private:
Context * ctx;
Inner * inner;
}

逆变与协变

我们知道在 C++ 中,如果有class D:B,则std::vector<D>std::vector<B>是没有任何继承关系的,也就是 invariance 的。
但是在另外一些语言中,存在协变(covariant)和逆变(contravariance)两种关系。协变简而言之就是如果struct D:B,则C<D>:C<B>,而逆变则翻转了继承关系,有C<B>:C<D>
一般语言中,协变是比较常见的,看起来更合乎逻辑。例如把一个[] Cat数组看做一个[] Animal数组也没什么不对。
C++ 中的 virtual 函数机制其实又被称为协变返回类型。

多重继承和虚继承

模板编程和多态

能否声明一个模板成员函数为虚的呢?

1
2
3
struct Cls{
template<typename T> virtual int a(){}
};

答案是不行的。为此,需要介绍两个概念
模板实例化(instantiation),C++中的模板既不是一个类型,也不是一个对象,也不是一个实体。在C++编译器生成后的real code中,不存在任何模板的概念。那么这个实例化的顺序是怎么样的呢,是整个程序先扫一遍,完成一次性的“替换调用”,还是链式的呢?这里涉及到point of instantiation的概念,详情可参考我的文章stateful constexpr,简单地来说,就是我要用到了就去实例化。
因此,某个函数模板到底会被实例化多少次,那是要等编译结束才会知道的。那么同样需要在编译结束才能知道的是一个类的布局。因为C++需要看到整个类的定义(而不是声明)才能为这个类生成代码,而虚函数所依赖的虚表需要在类被编译完成时才能确定。

其实大家写 Rust 对这一点就会感触很深。