面向对象的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 函数机制其实又被称为协变返回类型。

多重继承和虚继承