虚函数
在类的定义中, 前面有 virtual
关键字的成员函数就是 虚函数. virtual
只用在类定义里的函数声明中, 写函数体时不用.
派生类的指针可以赋给基类指针. 通过基类指针调用基类和派生类中的同名同参 虚 函数时:
- 若该指针指向一个基类的对象, 那么被调用是基类的虚函数;
- 若该指针指向一个派生类的对象, 那么被调用的是派生类的虚函数.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class CBase {
public:
virtual void someVirtualFunction() { cout << "base function" << endl; }
};
class CDerived : public CBase {
public:
virtual void someVirtualFunction() { cout << "derived function" << endl; }
};
int main() {
CDerived ODerived;
CBase *p = &ODerived;
p->someVirtualFunction(); // derived function
return 0;
}
|
派生类的对象可以赋给基类引用. 类似于指针, 通过基类引用调用基类和派生类中的同名同参虚函数也是多态的.
1
2
3
4
5
6
|
int main() {
CDerived ODerived;
CBase &r = ODerived;
r.someVirtualFunction(); // derived function
return 0;
}
|
多态不能针对对象. 派生类中的虚函数的访问权限可以是 public
, protected
, private
. 但是, 基类中的虚函数的访问权限不能是 private
, 即使派生类中的虚函数的访问权限是 public
. 反过来是可以的, 而且可以正常多态.
实现原理
采用了动态联编的技巧. 每一个有虚函数的类 (或其派生类) 都有一个虚函数表,该类的任何对象中都放着虚函数表的指针. 虚函数表中列出了该类的虚函数地址. 多出来的 4 个字节就是用来放虚函数表的地址的. 在编译时, 调用语句被编译成对虚函数表的索引, 而不是对函数的直接调用.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class Base {
public:
int i;
virtual void Print() { cout << "Base:Print" << endl; }
};
class Derived : public Base {
public:
int n;
virtual void Print() { cout << "Drived:Print" << endl; }
};
int main() {
Derived d;
cout << sizeof(Base) << "," << sizeof(Derived);
return 0;
}
|
32 位系统下, 这个程序的输出是 8,12
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class A {
public:
virtual void func() { cout << "A::func "; }
};
class B : public A {
public:
virtual void func() { cout << "B::func "; }
};
int main() {
A a;
A *pa = new B();
pa->func(); // 多态, B::func
// 64 位程序
long long *p1 = (long long *)&a;
long long *p2 = (long long *)pa; // 篡改了虚函数表指向
*p2 = *p1;
pa->func(); // A::func
return 0;
}
|
虚析构函数
在非构造/析构函数中调用虚函数时, 调用的是当前对象的虚函数, 而不是基类的虚函数, 是多态. 在构造/析构函数中调用虚函数时, 调用的是当前类的虚函数, 编译时确定, 不是多态.
通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数. 为了解决这个问题, 可以把基类的析构函数声明为虚函数.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
class son {
public:
virtual ~son() { cout << "bye from son" << endl;}
};
class grandson : public son {
public:
~grandson() { cout << "bye from grandson" << endl; }
};
int main() {
son *pson;
pson = new grandson();
delete pson;
return 0;
}
|
此时, 会先调用派生类的析构函数, 再调用基类的析构函数. 另外注意, 构造函数不能是虚函数.
纯虚函数和抽象类
如果在虚函数后面加上 = 0
, 则该虚函数是纯虚函数. 纯虚函数不可以有函数体, 只有声明.
1
2
3
4
5
|
class A {
public:
virtual void print() = 0; // 纯虚函数
void fun() { cout << "fun"; }
}
|
一个类中有纯虚函数的类叫 抽象类. 抽象类不能实例化, 只能作为基类, 不过可以作为指针或引用类型. 在抽象类的成员函数内可以调用纯虚函数, 但是在构造函数或析构函数内部不能调用纯虚函数.