系列文章目录
文章目录
前言
多态是面向对象编程中的一个核心概念,它允许我们通过基类指针或引用来操作派生类对象。在 C++ 中,多态主要通过虚函数(Virtual Function)和继承来实现。
多态有两种形式:静态多态(编译时多态)和动态多态(运行时多态)。
- 1. 静态多态:在编译时就确定了函数调用的地址,主要通过函数重载和运算符重载来实现。
- 2. 动态多态:在运行时确定函数调用的地址,主要通过虚函数来实现。
多态的主要优点是提高了代码的可扩展性和可维护性。通过使用基类指针或引用,你可以编写能够处理任何派生类对象的通用代码,而不需要知道这些对象的具体类型。这使得你可以添加新的派生类而不需要修改处理基类的代码,从而提高了代码的可扩展性。同时,多态也使得代码更加模块化,提高了代码的可维护性。
在接下来的文章中,我们将详细介绍 C++ 的多态特性,包括如何使用虚函数来实现动态多态,以及多态的一些使用场景和注意事项。
一、多态的基本语法
为了实现多态,我们引入一个动态绑定的操作
在 C++ 中,动态绑定(也称为晚绑定或运行时多态)是通过虚函数实现的。当你在基类中声明一个函数为
virtual
,并在派生类中重写这个函数,那么你就可以通过基类的指针或引用来调用这个函数,实际执行的将是派生类中的版本。
1.动态绑定实现的条件:
- 继承:必须有一个基类和至少一个派生类。
- 虚函数:基类中需要有一个或多个虚函数,这些函数在派生类中被重写。
- 基类指针或引用:你需要使用基类的指针或引用来指向派生类的对象。
补充:重写允许派生类改变基类中的函数行为,这使得我们可以通过基类的指针或引用来操作派生类的对象。
2.动态绑定的实现
//动物类
class Aninal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
//猫类
class Cat : public Aninal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
//执行说话的函数
//地址早绑定,在编译阶段确定函数地址
//如果想执行让猫说话,这个函数的地址不能提前绑定,需要在运行阶段进行绑定,地址晚绑定
void doSpeak(Aninal &animal)
{
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat);
}
int main()
{
test01();
return 0;
}
二.多态的深入剖析
C++ 中的多态是通过虚函数表(vtable)和虚函数表指针(vptr)来实现的。每个包含虚函数的类(或者从这样的类派生的类)都有一个虚函数表,这个表中存储了虚函数的地址。每个这样的对象都有一个虚函数表指针,这个指针指向该对象所属类的虚函数表。
当你通过基类指针或引用调用虚函数时,编译器会生成代码来获取虚函数表指针,然后通过这个指针找到虚函数表,再从表中获取虚函数的地址,最后调用这个地址对应的函数。这个过程发生在运行时,所以称为动态绑定。
class Base {
public:
virtual void foo() { std::cout << "Base::foo" << std::endl; }
};
class Derived : public Base {
public:
void foo() override { std::cout << "Derived::foo" << std::endl; }
};
int main() {
Base* ptr = new Derived();
ptr->foo(); // 输出 "Derived::foo"
delete ptr;
return 0;
}
三.纯虚函数和抽象类
1.纯虚函数的作用
- 定义接口:纯虚函数允许你定义一个接口,这个接口在基类中没有具体的实现,而是在派生类中实现。这使得你可以编写与具体实现无关的代码,只需要依赖接口。
- 禁止实例化:包含纯虚函数的类被称为抽象类,抽象类不能被实例化。这可以确保只有实现了所有纯虚函数的派生类才能被实例化。
- 强制派生类实现:如果一个类从抽象类派生,并且想要成为一个可以实例化的类,那么它必须实现基类中的所有纯虚函数。这确保了派生类必须提供某些特定的行为。
2.纯虚函数的实现
//纯虚函数及抽象类
class test
{
public:
//子类类必须重写父类中的纯虚函数,否则也是抽象类
//抽象类不能实例化对象
virtual void print() = 0;
};
class test1 : public test
{
public:
virtual void print()
{
cout << "test1" << endl;
}
};
void test3()
{
test * p = new test1;
p->print();
delete p;
}
int main()
{
test3();
return 0;
}
四.虚析构和纯虚析构
1.虚析构和纯虚析构的概念
虚析构函数:如果你有一个指向派生类对象的基类指针,并且通过这个指针删除对象,那么如果基类的析构函数不是虚函数,就只会调用基类的析构函数,而不会调用派生类的析构函数,这可能会导致资源泄漏。如果基类的析构函数是虚函数,那么就会先调用派生类的析构函数,然后再调用基类的析构函数,这样就可以正确地清理资源。
纯虚析构函数:纯虚析构函数是一种特殊的虚析构函数,它在基类中没有实现(或者说,它的实现被声明为 0)。包含纯虚析构函数的类是抽象类,不能被实例化。纯虚析构函数必须在基类中提供定义,否则在删除派生类对象时会出错。
2.虚析构和纯虚析构的实现
//虚析构和纯虚析构
class Animal
{
public:
Animal()
{
cout << "Animal的构造函数调用" << endl;
}
//虚析构和纯虚析构就是用来解决通过父类指针释放子类对象
//如果子类中有属性开辟了堆区数据,那么父类指针释放的时候就无法调用到子类的析构代码
//导致子类出现内存泄漏
//virtual ~Animal()
//{
// cout << "Animal的析构函数调用" << endl;
//}
virtual ~Animal() = 0;
virtual void speak() = 0;
};
Animal::~Animal()
{
cout << "Animal的纯虚析构函数调用" << endl;
}
class Cat : public Animal
{
public:
Cat(string name)
{
cout << "Cat的构造函数调用" << endl;
m_Name = new string(name);
}
virtual void speak()
{
cout << *m_Name << "小猫在说话" << endl;
}
~Cat()
{
if (m_Name != NULL)
{
cout << "Cat的析构函数调用" << endl;
delete m_Name;
m_Name = NULL;
}
}
string *m_Name;
};
void test01()
{
Animal *animal = new Cat("Tom");
animal->speak();
delete animal;
}
int main()
{
test01();
return 0;
}
总结
- 1. 多态是面向对象编程的核心概念,可以通过基类指针或引用操作派生类对象。C++中主要通过虚函数实现多态。
- 2. 多态分为静态多态(编译时多态)和动态多态(运行时多态)。静态多态主要通过函数重载和运算符重载实现。动态多态主要通过虚函数实现。
- 3. 多态的主要优点是提高代码的可扩展性和可维护性。使用基类指针或引用可以编写能处理任何派生类对象的通用代码,而无需知道对象的具体类型。
- 4. 为了实现动态多态,需要满足三个条件:有一个基类和至少一个派生类,基类中有一个或多个虚函数,并在派生类中被重写,使用基类的指针或引用来指向派生类的对象。
- 5. C++实现动态多态主要通过虚函数表(vtable)和虚函数表指针(vptr)。虚函数的运行过程是动态绑定的。
- 6. 纯虚函数和抽象类用来定义接口,其中定义的函数在基类中没有具体实现,而在派生类中实现。抽象类不能被实例化,只能通过派生类进行实现。
- 7. 虚析构和纯虚析构用于在删除基类指针时,能正确析构派生类对象,防止资源泄漏。