cpp纯虚函数和虚继承

虚继承和虚基类

为了解决继承中的二义性问题,提出的虚基类和虚继承。虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的问题。

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
50
51
#include <iostream>
using namespace std;

class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
};

class B : public A {
public:
B() {
cout << "B()" << endl;
}
~B() {
cout << "~B()" << endl;
}
};

class C : public A {
public:
C() {
cout << "C()" << endl;
}
~C() {
cout << "~C()" << endl;
}
};

class D : public B, public C {
public:
D() {
cout << "D()" << endl;
}
~D() {
cout << "~D()" << endl;
}
};
int main() {
D d;
// A* a = &d;
// test.cc: In function ‘int main()’:
// test.cc:46:13: error: ‘A’ is an ambiguous base of ‘D’
// A* a = &d;
// ^
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
@└────> # ./a.out 
A()
B()
A()
C()
D()
~D()
~C()
~A()
~B()
~A()

可以看到,在以上存在多继承且为菱形继承的情况下,构造函数的调用顺序是 ABACD,这表明继承 B 和 C 的时候夹杂了两份 A 的内容。这样也有一个问题,就是在试图用基类指针指向 D 对象时,编译器不知道应该让它指向哪个 A 的内存,因为在 d 中有两个 A 的实例。虚继承就是为了解决这个问题的。

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
#include <iostream>
using namespace std;

class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
};

class B : virtual public A { // 虚继承,A 是 B 的虚基类
public:
B() {
cout << "B()" << endl;
}
~B() {
cout << "~B()" << endl;
}
};

class C : virtual public A { // 虚继承,A 是 C 的虚基类
public:
C() {
cout << "C()" << endl;
}
~C() {
cout << "~C()" << endl;
}
};

class D : public B, public C {
public:
D() {
cout << "D()" << endl;
}
~D() {
cout << "~D()" << endl;
}
};
int main() {
D d;
A* a = &d;
return 0;
}
1
2
3
4
5
6
7
8
9
@└────> # ./a.out 
A()
B()
C()
D()
~D()
~C()
~B()
~A()

这样 D 的内存中就只有一份 A 了,并且用基类指针指向该地址也能正确进行。虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。

纯虚函数

cpp 支持编译时多态和运行时多态,函数重载就是编译时多态,而派生类和虚函数则为运行时多态。

编译时多态和运行时多态的区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是编译时多态,就是说地址是早绑定的。而如果函数的调用地址不能在编译期间确定,而需要在运行时才能决定,这这就属于运行时多态。 向上类型转换问题,对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。

Cpp 动态多态性是通过虚函数来实现的,虚函数允许派生类重新定义基类成员函数,而派生类重新定义基类虚函数的做法称为覆盖(override),或者称为重写。这部分原理可见另一篇文章。 cpp虚函数表

在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class)。

纯虚函数使用关键字 virtual,并在其后面加上 = 0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。virtual void fun() = 0,告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。

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
#include <iostream>
using namespace std;

class A {
public:
A() {
cout << "A()" << endl;
}
virtual void func() = 0; // 纯虚函数,表明 A 已经是抽象类了,不能实例化
virtual ~A() {
cout << "~A()" << endl;
}
};

class B : public A {
public:
B() {
cout << "B()" << endl;
}
void func() { // 实现该函数,否则 B 因为抽象类而不能实例化
cout << "B::func()" << endl;
}
~B() {
cout << "~B()" << endl;
}
};

int main() {
// A a;
//test.cc:26:7: error: cannot declare variable ‘a’ to be of abstract type ‘A’
// A a;
// ^
B b;
A* a = &b;
a->func();
return 0;
}
1
2
3
4
5
6
@└────> # ./a.out 
A()
B()
B::func()
~B()
~A()