C++继承
demo1
代码样例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include <iostream>
class Entity { public: float x, y;
void Move(float xa, float ya) { x += xa; y += ya; } };
class Player : public Entity { public: const char *Name;
void PrintName() { std::cout << Name << std::endl; } };
int main() {
Player player; player.x = 1; player.y = 2; player.Move(5, 5); std::cout << sizeof(Player) << std::endl; std::cout << sizeof(Entity) << std::endl; return 0; }
|
可以看到继承父类的Player占用内存比父类大,这是因为多了Name成员变量。
虚函数
虚函数允许我们在子类中重写方法。假设B是A的子类,如果在A类中创建一个方法并使用virtual修饰,我们就可以选择在B类中重写那个方法,让它做其他的事情。
demo1 普通继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream>
class Entity { public: std::string GetName() { return "Entity"; } };
class Player : public Entity { private: std::string m_Name; public: Player(const std::string &mName) : m_Name(mName) {}
std::string GetName() { return m_Name; } };
int main() { Entity* e = new Entity(); std::cout << e->GetName() << std::endl; Player* p = new Player("steve"); std::cout << p->GetName() << std::endl; return 0; }
|
符合预期,没什么异常
demo2 多态
如果我们使用多态的概念,那么上个demo的写法就会有问题了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <iostream>
class Entity { public: std::string GetName() { return "Entity"; } };
class Player : public Entity { private: std::string m_Name; public: Player(const std::string &mName) : m_Name(mName) {}
std::string GetName() { return m_Name; } };
int main() { Entity* e = new Entity(); std::cout << e->GetName() << std::endl; Player* p = new Player("steve"); std::cout << p->GetName() << std::endl; Entity* entity = p; std::cout << entity->GetName() << std::endl; return 0; }
|
这是因为当我们调用某个方法时,会调通属于该类型的方法。可以进一步观察下面的例子。对于PrintName而言它的参数是Entity类型,这意味着当我们调用GetName函数时,如果是Entity类型,那么它会从Entity类中找到GetName函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| #include <iostream>
class Entity { public: std::string GetName() { return "Entity"; } };
class Player : public Entity { private: std::string m_Name; public: Player(const std::string &mName) : m_Name(mName) {}
std::string GetName() { return m_Name; } };
void PrintName(Entity* entity) { std::cout << entity->GetName() << std::endl; }
int main() { Entity* e = new Entity();
PrintName(e); Player* p = new Player("steve");
PrintName(p);
return 0; }
|
如果想让C++在调用GetName意识到,PrintName函数中传递的是Player而不是Entity,就需要使用虚函数。虚函数引入了一种叫做Dynamic Dispatch(动态联编)的东西,它通常通过V表(虚函数表)来实现编译。V表就是一个表,它包含基类中所有虚函数的映射,这样我们可以在它运行时,将它们映射到正确的覆写(override)函数。简而言之,如果你想覆写一个函数,你必须将基类中的基函数声明为虚函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream>
class Entity { public: std::string GetName() { return "Entity"; } };
class Player : public Entity { private: std::string m_Name; public: Player(const std::string &mName) : m_Name(mName) {}
virtual std::string GetName() { return m_Name; } };
void PrintName(Entity* entity) { std::cout << entity->GetName() << std::endl; }
int main() { Entity* e = new Entity(); PrintName(e); Player* p = new Player("steve"); PrintName(p); return 0; }
|
添加了virtual声明之后就相当于告诉编译器,为GetName函数生成一个V表,以便能够成功通过映射找到子类覆写的函数。除此之外,在C++11中引入了”将覆写函数标记为关键字override”的特性。定义PrintName函数,通过基类Entity实现多态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream>
class Entity { public: virtual std::string GetName() { return "Entity"; } };
class Player : public Entity { private: std::string m_Name; public: Player(const std::string &mName) : m_Name(mName) {}
std::string GetName() { return m_Name; } };
void PrintName(Entity* entity) { std::cout << entity->GetName() << std::endl; }
int main() { Entity* e = new Entity(); PrintName(e); Player* p = new Player("steve"); PrintName(p); return 0; }
|
通过加入override关键字可以显式的告诉编译器这是一个覆写函数,使代码更具可读性,帮助预防bug的发生,比如函数名的一些拼写错误,因为找不到可以覆写的函数,或者当我们尝试覆写一个非虚函数也会报错(只有被覆写的虚函数才能被标记为override)。
但是虚函数可能会带来两种与虚函数相关的运行时成本。首先我们需要额外的内存来存储V表,这样才能分配到正确的函数,包括基类中需要有一个成员指针指向V表。其次,每次我们调用虚函数需要遍历这个表来确定要映射到哪个函数,这带来了额外的开销。
纯虚函数
C++的纯虚函数本质上与其他语言(如Java或C#)中的抽象方法或接口相同,纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数。
demo1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream>
class Entity { public: virtual std::string GetName() = 0; };
class Player : public Entity { private: std::string m_Name; public: Player(const std::string &mName) : m_Name(mName) {}
std::string GetName() override { return m_Name; } };
void PrintName(Entity* entity) { std::cout << entity->GetName() << std::endl; }
int main() { Entity* e = new Entity();
PrintName(e); Player* p = new Player("steve");
PrintName(p);
return 0; }
|
此时Entity* e = new Entity();爆红,如果Player子类去掉函数的具体实现一样也会爆红。

demo2
假设现在我们想要一个函数,可以打印输入变量的类名
1 2 3
| void Print(??? obj){ std::cout<<obj->GetClassName()<<std::endl; }
|
在问号处,我们需要一个类型,该类型能够保证我们有GetClassName函数,这个类型就是所谓的接口(C++使用抽象类实现接口)。以下代码定义了Printable接口,Entity实现了接口函数,Player对Entity实现的接口函数进行了覆写。Printable入参不关心输入参数的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #include <iostream>
class Printable { public: virtual std::string GetClassName() = 0; };
class Entity : public Printable { public: virtual std::string GetName() { return "Entity"; }
std::string GetClassName() override { return "Entity"; } };
class Player : public Entity { private: std::string m_Name; public: Player(const std::string &mName) : m_Name(mName) {}
std::string GetName() override { return m_Name; }
std::string GetClassName() override { return "Player"; } };
void PrintName(Entity* entity) { std::cout << entity->GetName() << std::endl; }
void Print(Printable* obj) { std::cout << obj->GetClassName() << std::endl; }
int main() { Entity* e = new Entity(); Player* p = new Player("steve");
Print(e); Print(p); return 0; }
|