文章目录
前言
C++ 是一种功能强大的编程语言,以其面向对象编程(OOP)特性而闻名。虚函数和多态是 C++ 面向对象编程中的两个重要概念,它们使得代码更灵活、更具扩展性。本篇文章将介绍虚函数与多态,包括其定义、使用方法及其在实际编程中的作用。
虚函数是什么?
虚函数是一种在基类中使用 virtual
关键字声明的函数,它允许在运行时通过基类指针或引用调用子类的重写版本。这种机制使得 C++ 能够实现多态性。
如何使用虚函数?
在基类中声明虚函数,并在子类中重写该虚函数。基类指针或引用可以指向子类对象,并在运行时调用子类的重写版本。
#include <iostream>
class Base {
public:
virtual void show() {
std::cout << "Base show" << std::endl;
}
};
class Derived : public Base {
public:
void show() override {
std::cout << "Derived show" << std::endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->show(); // 调用的是 Derived 的 show 方法
delete ptr;
return 0;
}
纯虚函数是什么?
纯虚函数是一种没有实现的虚函数,必须在派生类中实现。它使用 = 0
语法来声明。声明纯虚函数的类称为抽象类,不能直接实例化。
class AbstractBase {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数
};
class ConcreteDerived : public AbstractBase {
public:
void pureVirtualFunction() override {
std::cout << "ConcreteDerived implementation of pureVirtualFunction" << std::endl;
}
};
int main() {
// AbstractBase obj; // 错误,不能实例化抽象类
ConcreteDerived obj;
obj.pureVirtualFunction(); // 调用派生类实现的纯虚函数
return 0;
}
虚函数与普通函数的区别
-
调用时机:
- 普通函数:在编译时确定调用的函数。
- 虚函数:在运行时确定调用的函数(动态绑定)。
-
多态性:
- 普通函数:不支持多态性。
- 虚函数:支持多态性,可以通过基类指针或引用调用派生类的重写版本。
虚表
虚表是什么?
在 C++ 中,虚表(vtable)是一个由编译器维护的数据结构,用于实现虚函数的动态绑定。当一个类中包含虚函数时,编译器会为这个类创建一个虚表。虚表是一个指针数组,其中每个指针指向该类的虚函数实现。当通过基类指针或引用调用虚函数时,程序会通过虚表找到实际要调用的函数。
每个包含虚函数的类对象都有一个隐藏的指针,称为虚表指针(vptr),它指向该对象所属类的虚表。因此,通过虚表指针,程序可以在运行时动态地确定调用哪个函数。
含有虚表的类内存结构图
有下面这样的两个类:
class Base {
public:
virtual void func1();
int baseData;
};
class Derived : public Base {
public:
void func1() override;
void func2();
int derivedData;
};
它的内存结构图如下
+-----------------+
| Derived object |
+-----------------+
| vptr ---------->+-------------------------+
| derivedData | |
+-----------------+ |
| baseData | |
+-----------------+ |
|
|
|
+-----------------+ +-----------------+
| Virtual Table | | Virtual Table |
| for Derived | | for Base |
+-----------------+ +-----------------+
| func1() | | func1() |
| Derived::func1 | | Base::func1 |
+-----------------+ +-----------------+
| func2() |
| Derived::func2 |
+-----------------+
如何找到虚表的地址?
找到虚表地址的方法涉及使用一些底层的 C++ 技巧和对内存布局的理解。以下是一个简单的示例代码,用于展示如何找到一个类对象的虚表地址并打印虚表中的内容。
示例代码
#include <iostream>
class Base {
public:
virtual void func1() {
std::cout << "Base::func1" << std::endl;
}
virtual void func2() {
std::cout << "Base::func2" << std::endl;
}
};
class Derived : public Base {
public:
void func1() override {
std::cout << "Derived::func1" << std::endl;
}
void func2() override {
std::cout << "Derived::func2" << std::endl;
}
};
typedef void(*FuncPtr)();
void printVTable(FuncPtr* vtable) {
std::cout << "Virtual Table Address: " << vtable << std::endl;
for (int i = 0; vtable[i] != nullptr; ++i) {
std::cout << "Function " << i << " address: " << vtable[i] << std::endl;
}
}
int main() {
Derived obj;
// 获取虚表指针的地址
FuncPtr* vptr = *reinterpret_cast<FuncPtr**>(&obj);
printVTable(vptr);
return 0;
}
代码解释
-
类定义:
Base
类中定义了两个虚函数func1
和func2
。Derived
类继承自Base
并重写了这两个虚函数。
-
函数指针类型:
typedef void(*FuncPtr)();
定义了一个函数指针类型FuncPtr
,用于指向虚函数。
-
打印虚表函数:
printVTable
函数接受一个虚表指针数组并打印虚表地址和其中每个函数的地址。
-
main 函数:
- 创建一个
Derived
类对象obj
。 - 使用
reinterpret_cast
将对象的地址转换为虚表指针数组的指针,从而获取虚表地址。 - 调用
printVTable
函数打印虚表内容。
- 创建一个
多态是什么?
多态性是面向对象编程的一种特性,它允许通过基类指针或引用在运行时调用不同子类的重写方法。多态性使得代码更灵活和可扩展。
如何使用多态?
通过基类指针或引用指向子类对象,并调用基类中的虚函数。在运行时,根据实际指向的对象类型调用相应的重写方法。
#include <iostream>
class Animal {
public:
virtual void sound() {
std::cout << "Some generic animal sound" << std::endl;
}
};
class Dog : public Animal {
public:
void sound() override {
std::cout << "Bark" << std::endl;
}
};
class Cat : public Animal {
public:
void sound() override {
std::cout << "Meow" << std::endl;
}
};
void makeSound(Animal* animal) {
animal->sound(); // 根据实际对象类型调用相应的 sound 方法
}
int main() {
Dog dog;
Cat cat;
makeSound(&dog); // 输出 Bark
makeSound(&cat); // 输出 Meow
return 0;
}
为什么要使用多态?
- 代码复用:多态性允许通过统一的接口调用不同子类的方法,提高代码的复用性和可维护性。
- 灵活性:多态性使得程序在运行时根据实际情况调用不同的方法,提高了代码的灵活性和扩展性。
- 可扩展性:可以在不修改已有代码的情况下添加新的子类,并在运行时通过多态性使用它们。
多态遇到的所有情况
-
通过基类指针或引用调用子类方法:
- 使用基类指针或引用指向子类对象,调用虚函数实现多态性。
-
对象切片问题:
- 如果将子类对象赋值给基类对象,会发生对象切片问题,子类的附加属性和方法会被切掉。
Base baseObj = derivedObj; // 对象切片,丢失 Derived 部分
-
多重继承中的多态:
- C++ 支持多重继承,可以通过虚继承解决多重继承中的菱形继承问题,使多态性依然有效。
-
虚析构函数:
- 在有多态的情况下,基类通常需要一个虚析构函数,以确保通过基类指针删除派生类对象时,正确调用派生类的析构函数。
class Base { public: virtual ~Base() { std::cout << "Base destructor" << std::endl; } }; class Derived : public Base { public: ~Derived() { std::cout << "Derived destructor" << std::endl; } }; int main() { Base* ptr = new Derived(); delete ptr; // 正确调用 Derived 的析构函数 return 0; }
总结
虚函数和多态是 C++ 中面向对象编程的重要特性,通过虚函数可以实现多态性,使得代码更加灵活和可扩展。虚函数允许在运行时通过基类指针或引用调用子类的重写方法,而纯虚函数则强制派生类实现某些方法,使得抽象类无法实例化。多态性使得程序可以在运行时根据实际对象类型调用相应的方法,提升了代码的复用性、灵活性和可扩展性。然而,在使用多态时,需要注意对象切片问题和虚析构函数的使用,以确保正确的对象管理和方法调用。掌握虚函数和多态的概念和应用,对于提高 C++ 编程水平和开发效率至关重要。