继承
虽然我并不喜欢继承,继承太复杂容易出错,但是不可否认的是继承确实是一个很有用的抽象。
简单的继承
如果不同类型的继承关系是一个树形结构,那么是很清晰的。例如:
using namespace std;
class Animal {
public:
virtual void eat() const { cout << "I eat like a generic Animal." << endl; }
virtual ~Animal() {}
};
class Wolf : public Animal {
public:
void eat() const { cout << "I eat like a wolf!" << endl; }
};
class Fish : public Animal {
public:
void eat() const { cout << "I eat like a fish!" << endl; }
};
class GoldFish : public Fish {
public:
void eat() const { cout << "I eat like a goldfish!" << endl; }
};
class OtherAnimal : public Animal {};
int main() {
std::vector<Animal *> animals;
animals.push_back(new Animal());
animals.push_back(new Wolf());
animals.push_back(new Fish());
animals.push_back(new GoldFish());
animals.push_back(new OtherAnimal());
for (std::vector<Animal *>::const_iterator it = animals.begin();
it != animals.end(); ++it) {
(*it)->eat();
delete *it;
}
return 0;
}
这里使用virtual
修饰了基类Animal
的eat()
和析构函数,让该函数成为 虚函数。派生类中对此函数的不同实现都会继承这一修饰符,允许后续派生类覆盖,达到迟绑定的效果。即便是基类中的成员函数调用虚函数,也会调用到派生类中的版本。
Java、Python中的函数默认都是“虚函数”,不需要特殊声明派生类就可以覆盖。
简单的 vptr & vtable
由于C++的运行时多态性质,当我们调用一个类的虚函数的时候,实际调用的函数可能来自他的派生类。为了实现这一点,C++创建了 vptr 和 vtable。下面介绍具体是如何实现的。稍微修改一下主函数:
vtable是属于某个类的,他是一系列函数指针;vptr是属于一个实例的,它指向vtable。
(gdb) x/xg wolf_1
0x55555556aeb0: 0x0000555555557cb8
(gdb) x/xg wolf_2
0x55555556aed0: 0x0000555555557cb8
(gdb) x/xg fish
0x55555556aef0: 0x0000555555557d30
顺着vptr,查看vtable的内容。不难发现就是指向虚函数的指针。并且虚函数位于程序的RODATA区域,也就是全局只读区。
(gdb) x/2xg 0x0000555555557cb8
0x555555557cb8 <_ZTV4Wolf+16>: 0x0000555555555400 0x0000555555555440
(gdb) info address Wolf::~Wolf
Symbol "Wolf::~Wolf()" is a function at address 0x555555555440.
(gdb) info address Wolf::eat() const
Symbol "Wolf::eat() const" is a function at address 0x555555555400.
总结一下,简单的认识vptr
和vtable
:
- 每一个实例都存储着一个
vptr
,一般是放在offset=0的地方,它指向实例对应的vtable
。 - 每个类都对应着一个
vtable
,存储在RODATA区域,存储着所有虚函数的地址。
更具体的行为是由编译器的设计决定的。