虚函数

类似于静态函数,类的成员函数也可以被定义为虚函数。

1
2
3
4
class A{
public:
virtual void function(){}
};

定义为虚函数之后,编译器会被显式告知这是一个多态函数,将会在派生类中被重写。

多态的意思是,这个函数在基类和派生类当中是不一样的。通过基类对象和派生类对象调用出的函数不一样。

那么,这种写法和直接在派生类中重写一个同名函数有什么区别呢?

区别在于,假如是重写一个同名函数,调用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class A{
public:
void function(){
cout<<"A\n";
}
};
class B:public A{
public:
void function(){
cout<<"B\n";
}
};
int main(){
A obj1;
B obj2;
obj2.function();//输出 B
A* p = &obj2;//此时obj2已经被解释成A类对象
p->function();//输出A
return 0;
}
输出:
B
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
class A{
public:
virtual void function(){
cout<<"A\n";
}
};

class B:public A{
public:
void function(){
cout<<"B\n";
}
};
int main(){
A obj1;
B obj2;
obj2.function();//输出 B
A* p = &obj2;
p->function();//输出B
return 0;
}

输出:
B
B

为什么通过指针实现了隐式转换,还是调用了派生类中的function()呢?

实际上,对于包含虚函数的类,编译器会建立一个虚函数表。

在初始创建对象的时候,就会把这个对象的虚函数对应绑定虚函数表的位置,后续调用本质上是去查表调用。

因此即使隐式转换了,虚函数表的对应关系没有变,调用就不会改变。

多重继承的虚函数行为

在继承链里面,从第一个virtual开始都符合虚函数行为,无论后续派生类有没有加virtual关键字

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
#include<iostream>
#include<vector>
using namespace std;
class A {
public:
void func(){
cout<<"A"<<endl;
}
};
class B : public A {
public:
virtual void func(){
cout<<"B"<<endl;
}
};
class C: public B {
public:
void func(){
cout<<"C"<<endl;
}
};
class D : public C {
public:
void func(){
cout<<"D"<<endl;
}
};
int main(){
C obj1;
C obj2;
B* p1 = &obj1;
C* p2 = &obj2;
p1->func();
p2->func();
return 0;
}

输出:
C
C

类型转换

派生类隐式转换成基类

由于这是一个从“内容多”到“内容少”的过程,不需要编译器自主定义一些值,所以不会产生未定义行为,可以直接转换。
在以下代码示例中,A 为基类,B 为派生类。

1
2
3
4
5
6
7
8
class A {};
class B : public A {};

void func() {
B b;
A& a_ref = b; // 一切正常,派生类对象可以隐式转换为基类引用
A* a_ptr = &b; // 一切正常,派生类指针可以隐式转换为基类指针
}

基类显式转换成派生类

那么,假如我们想实现从“内容少”到“内容多”,行不行呢?

很遗憾,假如是严格的转换,是不可以的。

我们不可以把一个指向基类对象的指针转换成指向派生类对象。

但是我们可以把某个指针“拨乱反正”。

上面的代码有一些基类指针,指向的是派生类对象,我们知道这种隐式转换是可以的。

但是,既然指向的是派生类对象,那么完全可以把指向派生类的基类指针转换成派生类指针,这就是显式转换的过程。

1
2
3
4
5
6
7
8
9
class A {};
class B : public A {};
class C{};
void func() {
B b;
A* a_ptr = &b; // 一切正常,派生类指针可以隐式转换为基类指针
B* b_ptr = dynamic_cast<B*>(a_ptr); //可以
C* c_ptr = dynamic_cast<B*>(a_ptr);//未定义行为
}

我们可以通过几种转换符实现这个转换,它们分别是 dynamic_caststatic_castconst_castreinterpret_cast

dynamic_cast

  • 用于多态类型之间的安全向下转换(基类 → 派生类)。
  • 会在运行时检查类型是否合法。
  • 如果转换失败,返回 nullptr(指针)或抛出 bad_cast(引用)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A {
public:
virtual ~A() {}
};

class B : public A {
public:
void hello() {}
};

void func(A* a) {
B* b = dynamic_cast<B*>(a);
if (b) {
b->hello(); // 安全使用
} else {
std::cout << "转换失败\n";
}
}

static_cast

  • 编译时转换,无运行时检查。
  • 用于已知类型关系的转换,例如基类指针转换为派生类指针(但必须确保实际对象类型正确,否则行为未定义)。
1
2
3
4
5
6
7
8
9
10
class A {};
class B : public A {
public:
void hello() {}
};

void func(A* a) {
B* b = static_cast<B*>(a); // 编译时转换,若a原本不是B对象则可能崩溃
b->hello(); // 如果a实际不是B对象,会出现未定义行为
}

const_cast

  • 用于去除 constvolatile 修饰符。
  • 常用于传递给需要非常量参数的函数。
  • 注意:如果原对象本身是 const 定义的,那么修改它的值会导致未定义行为
1
2
3
4
5
6
7
const int a = 100;
int b = 200;

const int* pa = &a;
const int* pb = &b;
int* pc = const_cast<int*>(pb);//正确
int* pd = const_cast<int*>(pa);//未定义行为

reinterpret_cast

  • 最危险的转换,通常用于底层操作。
  • 可将任意指针类型互转、指针转整数等。
  • 没有类型安全保证,使用需极度谨慎。
1
2
3
4
5
6
void func() {
int a = 10;
void* p = &a;
int* p2 = reinterpret_cast<int*>(p); // 强制解释为int指针
std::cout << *p2 << std::endl; // 正常输出10
}

总结

转换方式 安全性 检查方式 使用场景
dynamic_cast 安全 运行时检查 多态类型安全转换
static_cast 有风险 编译时检查 已知类型关系(如数值、类层次)
const_cast 安全(小心) 编译时检查 去除 const/volatile 修饰符
reinterpret_cast 极不安全 无检查 底层指针操作、类型重解释