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;
}
1
2
3
输出结果
16
8

可以看到继承父类的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;
}
1
2
Entity
steve

符合预期,没什么异常

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;
}
1
2
3
Entity
steve
Entity

这是因为当我们调用某个方法时,会调通属于该类型的方法。可以进一步观察下面的例子。对于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();
// std::cout << e->GetName() << std::endl;
PrintName(e);
Player* p = new Player("steve");
// std::cout << p->GetName() << std::endl;
PrintName(p);
// Entity* entity = p;
// std::cout << entity->GetName() << std::endl;
return 0;
}
1
2
Entity
Entity

如果想让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();
// std::cout << e->GetName() << std::endl;
PrintName(e);
Player* p = new Player("steve");
// std::cout << p->GetName() << std::endl;
PrintName(p);
// Entity* entity = p;
// std::cout << entity->GetName() << std::endl;
return 0;
}

此时Entity* e = new Entity();爆红,如果Player子类去掉函数的具体实现一样也会爆红。

image-20241212013606795

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;
}
1
2
Entity
Player