C++ :多态(类型转换及问题、虚函数、C++如何实现动态绑定、多态成立的条件、抽象基类和纯虚函数、虚析构函数)

mac2025-09-14  15

多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。


目录

一、类型转换及问题

1.2 问题解决思路

1.3 问题解决方案(虚函数,vitual function)

二、C++如何实现动态绑定

三、多态成立的条件

四、抽象基类和纯虚函数(pure virtual function)

五、虚析构函数


 

c++支持编译时多态(静态多态)运行时多态(动态多态)运算符重载和函数重载就是编译时多态派生类和虚函数实现运行时多态

静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。

 

一、类型转换及问题

1.1

对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。

也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。

class Animal{ public: void speak(){ cout << "动物在唱歌..." << endl; } }; class Dog : public Animal{ public: void speak(){ cout << "小狗在唱歌..." << endl; } }; void DoBussiness(Animal& animal){ animal.speak(); } void test(){ Dog dog; DoBussiness(dog); } 运行结果: 动物在唱歌问题抛出: 我们给DoBussiness传入的对象是dog,而不是animal对象,输出的结果应该是  小狗在唱歌。

 

1.2 问题解决思路

解决这个问题,我们需要了解下绑定(捆绑,binding)概念。

把函数体与函数调用相联系称为绑定(捆绑,binding)

当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding).C语言中只有一种函数调用方式,就是早绑定。

上面的问题就是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,编译器确定了应该调用的speak是Animal::speak的,而不是真正传入的对象Dog::speak。

解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,late binding),意味着绑定要根据对象的实际类型,发生在运行。

 

1.3 问题解决方案(虚函数,vitual function)

C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写

对于特定的函数进行动态绑定,c++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用.

注意几点:

为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要.如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual的.在派生类中virtual函数的重定义称为重写(override).Virtual关键字只能修饰成员函数.构造函数不能为虚函数

 

二、C++如何实现动态绑定

当我们告诉通过创建一个virtual函数来告诉编译器要进行动态绑定,那么编译器就会根据动态绑定机制来实现我们的要求, 不会再执行早绑定。

问题:C++的动态捆绑机制是怎么样的? 当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr),这个指针是指向对象的虚函数表。在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。子类继承基类,子类继承了基类的vptr指针,这个vptr指针是指向基类虚函数表,当子类调用构造函数,使得子类的vptr指针指向了子类的虚函数表。 当子类无重写基类虚函数时:(子类无重写基类虚函数是无意义的

(类animal中有虚函数func1、func2.  类dog中有除继承外虚函数func3、func4)

过程分析:     Animal* animal = new Dog;     animal->fun1();     当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,此时由于子类并没有重写也就是覆盖基类的func1函数,所以调用func1时,仍然调用的是基类的func1.执行结果: 我是基类的func1测试结论: 无重写基类的虚函数,无意义

 

当子类重写基类虚函数时:(这才是我们讨论的重写

(上图红色为重写的函数)

过程分析:     Animal* animal = new Dog;     animal->fun1();     当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,由于子类重写基类的func1函数,所以调用func1时,调用的是子类的func1.执行结果: 我是子类的func1

 

三、多态成立的条件

有继承子类重写父类虚函数函数           a) 返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外)           b) 子类中virtual关键字可写可不写,建议写父类指针或父类引用指向子类对象

 

四、抽象基类和纯虚函数(pure virtual function)

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

做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class).

注意点:

初始化语法: virtual 返回值类型 函数名 (形参) = 0有了纯虚函数的类,也称为抽象类,不能实例化出对象。子类必须重写父类纯虚函数,否则子类也是抽象类。Virtual void fun() = 0;    告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。

 

五、虚析构函数

利用虚析构可以解决 :不调用子类的析构函数的问题

纯虚析构需要有声明,也需要有实现,如果函数体中 有了纯虚析构,那么这个函数也属于抽象类。

例子:

class Animal { public: Animal() { cout << "Animal构造函数调用" << endl; } virtual void speak() = 0; virtual ~Animal() { cout << "Animal析构函数调用" << endl; } }; Animal::~Animal() { cout << "Animal纯虚析构函数调用" << endl; } class Cat :public Animal { public: Cat(char * name) { cout << "Cat构造函数调用" << endl; this->m_Name = new char[strlen(name) + 1]; strcpy(this->m_Name, name); } void speak() { cout << this->m_Name<< "小猫在说话" << endl; } ~Cat() { if (this->m_Name != NULL) { cout << "Cat析构函数调用" << endl; delete[]this->m_Name; this->m_Name = NULL; } } char * m_Name; //姓名 }; void test01() { Animal * cat = new Cat("Tom"); cat->speak(); delete cat; }

 

 

 

 

最新回复(0)