100.虚函数
1.静态联编与动态联编
C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写。对于特定的函数进行动态绑定,C++要求在基类中声明这个函数的时候使用virtual 关键字,动态绑定也就对virtual函数起作用。为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要。如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual 的。在派生类中virtual 函数的重定义称为重写(override)。Virtual 关键字只能修饰成员函数。构造函数不能为虚函数
注意: 仅需要在基类中声明一个函数为virtual。调用所有匹配基类声明行为的派生类函数都将使用虚机制。虽然可以在派生类声明前使用关键字virtual(这也是无害的),但这个样会使得程序显得冗余和杂乱。(我建议写上)
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Animal
{
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
//动态多态产生条件:
//先有继承关系
//父类中有虚函数,子类重写父类中的虚函数
//父类的指针或引用 指向子类的对象
//对于有父子关系的两个类是可以直接转换的
void doSpeak(Animal& animal)//Animal & animal = cat;
{
//如果地址早就绑定好了,地址早绑定,属于静态联编
//如果调用小猫说话,这个时候函数的地址不能早就绑定好了,而是在运行阶段再去绑定函数地址,属于地址晚绑定,叫动态联编
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat);
}
int main()
{
test01();
system("pause");
return EXIT_SUCCESS;
}
输出:
动物在说话
请按任意键继续. . .
修改后:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat : public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog : public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
//对于有父子关系的两个类是可以直接转换的
void doSpeak(Animal& animal)//Animal & animal = cat;
{
//如果地址早就绑定好了,地址早绑定,属于静态联编
//如果调用小猫说话,这个时候函数的地址不能早就绑定好了,而是在运行阶段再去绑定函数地址,属于地址晚绑定,叫动态联编
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat);
Dog dog;
doSpeak(dog);
}
int main()
{
test01();
system("pause");
return EXIT_SUCCESS;
}
输出:
小猫在说话
小狗在说话
请按任意键继续. . .
动态多态产生条件:
1.先有继承关系
2.父类中有虚函数,子类重写父类中的虚函数
3.父类的指针或引用 指向子类的对象
虚函数表
class Cat size(1):
+---
0 | +--- (base class Animal)
| +---
+---
父类成员加上virtual,子类没有重写speak(),虚函数表指针指向table,记录Animal speak地址
class Cat size(4):
+---
0 | +--- (base class Animal)//虚函数表指针指向table,记录Animal speak地址
0 | | {vfptr}
| +---
+---
Cat::$vftable@:
| &Cat_meta
| 0
0 | &Animal::speak
父类成员加上virtual,子类重写speak(),虚函数表指针指向table,记录Cat speak地址
class Cat size(4):
+---
0 | +--- (base class Animal)
0 | | {vfptr}
| +---
+---
Cat::$vftable@:
| &Cat_meta
| 0
0 | &Cat::speak
当父类成员加上virtual时,父类会多出一个虚函数表,vfptr指向这个虚函数表,虚函数表内记录虚函数地址。子类继承父类虚函数表,表内部虚函数指针在子类没有重写父类虚函数时,虚函数指针不变,仍然指向父类的虚函数表,虚函数表内记录的虚函数地址是父类的,所以在函数调用时,调用的是父类的虚函数。当子类重写父类虚函数时,父类虚函数被覆盖,子类虚函数表中记录的虚函数指针记录的是自己的重写的那个虚函数。所以,最终调用的是子类的成员函数。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Animal
{
public:
//虚函数
virtual void speak()
{
cout << "动物在说话" << endl;
}
virtual void eat(int a )
{
cout << "动物在吃饭" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
void eat(int a)
{
cout << "小猫在吃饭" << endl;
}
};
//动态多态产生条件:
//先有继承关系
//父类中有虚函数,子类重写父类中的虚函数
//父类的指针或引用 指向子类的对象
//对于有父子关系的两个类 指针或者引用 是可以直接转换的
void doSpeak(Animal & animal) //Animal & animal = cat;
{
//如果地址早就绑定好了,地址早绑定,属于静态联编
//如果想调用小猫说话,这个时候函数的地址就不能早就绑定好,而是在运行阶段再去绑定函数地址,属于地址晚绑定,叫动态联编
animal.speak();
}
void test02()
{
//cout << "sizeof Animal = " << sizeof (Animal) << endl;
Animal * animal = new Cat;
//animal->speak();
// *(int *)animal 解引用到虚函数表中
// *(int *)*(int *)animal 解引用到函数speak地址
//调用猫说话
((void(*)()) (*(int *)*(int *)animal)) ();
//C/C++默认调用惯例 __cdecl
//用下列调用时候 真实调用惯例 是 __stdcall
//调用猫吃饭
typedef void( __stdcall *FUNPOINT)(int);
(FUNPOINT (*((int*)*(int*)animal + 1)))(10);
}
int main(){
//test01();
test02();
system("pause");
return EXIT_SUCCESS;
}
class Cat size(4):
+---
0 | +--- (base class Animal)
0 | | {vfptr}
| +---
+---
Cat::$vftable@:
| &Cat_meta
| 0
0 | &Cat::speak
1 | &Cat::eat
animal(指针)指向虚函数表,(int *)animal中,对animal进行强制转换,*(int *)animal取出虚函数表中第一个元素地址,也就是speak函数,输出”小猫在说话“。偏移量加1调用eat函数输出”小猫在吃饭“。
*(int *)*(int *)animal 解引用到函数speak地址
利用上面这个函数地址,可以采取其他易于理解的办法调用该函数指针指向的函数。
一般函数指针形式为(去掉函数指针):
void(*)()
typedef void( __stdcall *FUNPOINT)(int);
当加了一个参数之后就会涉及到调用惯例,C/C++默认调用惯例是、__cdecl,上面调用时候 真实调用惯例是 __stdcall
2.多态案例-计算器案例
3.多态案例-计算器案例
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include <string>
//class calculator
//{
//public:
//
// int getResult( string oper)
// {
// if (oper == "+")
// {
// return m_A + m_B;
// }
// else if (oper == "-")
// {
// return m_A - m_B;
// }
// else if (oper == "*")
// {
// return m_A * m_B;
// }
//
// }
//
// int m_A;
// int m_B;
//};
//设计原则 : 开闭原则
// 对扩展进行开放 对修改进行关闭
//利用多态实现计算器
class AbstractCalculator
{
public:
//纯虚函数
//如果一个类中包含了纯虚函数,那么这个类就无法实例化对象了,这个类通常我们称为 抽象类
//抽象类的子类 必须要重写 父类中的纯虚函数,否则也属于抽象类
virtual int getResult() = 0;
//virtual int getResult()
//{
// return 0;
//}
int m_A;
int m_B;
};
//加法计算器
class AddCalculator :public AbstractCalculator
{
public:
virtual int getResult()
{
return m_A + m_B;
}
};
//减法计算器
class SubCalculator :public AbstractCalculator
{
public:
virtual int getResult()
{
return m_A - m_B;
}
};
//乘法计算器
class MulCalculator :public AbstractCalculator
{
public:
virtual int getResult()
{
return m_A * m_B;
}
};
class Test :public AbstractCalculator
{
int getResult(){ return 0; };
};
void test01()
{
//calculator c;
//c.m_A = 10;
//c.m_B = 10;
//cout << c.getResult("+") << endl;
AbstractCalculator * calculator = new AddCalculator;
calculator->m_A = 100;
calculator->m_B = 200;
cout << calculator->getResult() << endl;
delete calculator;
calculator = new SubCalculator;
calculator->m_A = 100;
calculator->m_B = 200;
cout << calculator->getResult() << endl;
}
int main(){
//test01();
//AbstractCalculator abc; 抽象类是无法实例化对象的
//Test t; //如果不重写父类的纯虚函数 ,也无法实例化对象
system("pause");
return EXIT_SUCCESS;
}
3.抽象基类和纯虚函数(pure virtual function)
在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。
做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class).
■纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。
■当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
■Virtual void fun() = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。
建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要实现(或者不需要完全实现)。可以创建一个公共类.
案例: 模板方法模式
//抽象制作饮品
class AbstractDrinking
{
public:
//烧水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//规定流程
void MakeDrink()
{
Boil();
Brew();
PourInCup();
PutSomething();
}
};
//制作咖啡
class Coffee : public AbstractDrinking
{
public:
//烧水
virtual void Boil()
{
cout << "煮农夫山泉!" << endl;
}
//冲泡
virtual void Brew()
{
cout << "冲泡咖啡!" << endl;
}
//倒入杯中
virtual void PourInCup()
{
cout << "将咖啡倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething()
{
cout << "加入牛奶!" << endl;
}
};
//制作茶水
class Tea : public AbstractDrinking
{
public:
//烧水
virtual void Boil()
{
cout << "煮自来水!" << endl;
}
//冲泡
virtual void Brew()
{
cout << "冲泡茶叶!" << endl;
}
//倒入杯中
virtual void PourInCup()
{
cout << "将茶水倒入杯中!" << endl;
}
//加入辅料
virtual void PutSomething()
{
cout << "加入食盐!" << endl;
}
};
//业务函数
void DoBussiness(AbstractDrinking* drink)
{
drink->MakeDrink();
delete drink;
}
void test()
{
DoBussiness(new Coffee);
cout << "--------------" << endl;
DoBussiness(new Tea);
}
4.虚析构和纯虚析构
多继承带来了一些争议,但是接口继承可以说一种毫无争议的运用了。
绝大数面向对象语言都不支持多继承,但是绝大数面向对象语言都支持接口的概念,C++中没有接口的概念,但是可以通过纯虚函数实现接口。
接口类中只有函数原型定义,没有任何数据定义。
多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。
注意:除了析构函数外,其他声明都是纯虚函数。
虚析构函数作用:
虚析构函数是一种特殊的析构函数,用于在基类指针或引用指向派生类对象时进行析构操作。它的作用是在删除基类指针或引用时,调用派生类的析构函数,确保所有与该对象相关的资源都被正确地释放。
具体来说,当使用基类指针或引用来访问派生类对象时,如果基类中没有定义虚析构函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这会导致派生类中的资源没有被正确地释放,从而导致程序错误。
因此,为了保证程序的正确性,我们通常会在基类中定义一个虚析构函数,以便在删除基类指针或引用时调用派生类的析构函数。这样可以确保所有与该对象相关的资源都被正确地释放,避免内存泄漏等问题。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Animal
{
public:
Animal()
{
cout << "Animal的构造函数调用" << endl;
}
virtual void speak()
{
cout << "动物在说话" << endl;
}
//如果子类中有指向堆区的属性,那么要利用虚析构技术 在delete的时候 调用子类的析构函数
//virtual ~Animal()
//{
// cout << "Animal的析构函数调用" << endl;
//}
//纯虚析构 需要有声明 也需要有实现
//如果一个类中 有了 纯虚析构函数,那么这个类也属于抽象类,无法实例化对象了
virtual ~Animal() = 0;
};
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);
}
virtual void speak()
{
cout << this->m_Name <<" 小猫在说话" << endl;
}
~Cat()
{
if (this->m_Name)
{
cout << "Cat的析构函数调用" << endl;
delete[] this->m_Name;
this->m_Name = NULL;
}
}
char * m_Name;
};
void test01()
{
Animal * animal = new Cat("Tom");//子类的属性被创建在堆区,在释放父类指针时是不会释放子类的析构函数的
animal->speak();
delete animal;
}
int main(){
test01();
//Animal aaa; 在Animal中有了纯虚析构,也属于抽象类
system("pause");
return EXIT_SUCCESS;
}
5.动态绑定
C++中的虚函数动态绑定是指在运行时根据实际对象类型来确定调用哪个版本的虚函数。在编译时,编译器无法确定具体调用哪个版本的虚函数,因此需要使用一些技术来实现动态绑定。一种常见的技术是使用模板来实现动态绑定。模板可以将一个类或函数的定义与它的实例分离开来,从而使得编译器可以根据实际类型来确定调用哪个版本的虚函数。另一种技术是使用多态性。多态性允许不同类型的对象具有相同的接口,从而可以通过基类指针或引用来调用派生类的虚函数。当调用虚函数时,编译器会根据实际对象类型来确定调用哪个版本的虚函数。总之,C++中的虚函数动态绑定是通过模板和多态性等技术来实现的,它可以使代码更加灵活和可扩展。
假设我们有一个基类Animal,其中包含一个虚函数makeSound():
class Animal
{
public:
virtual void makeSound()
{
cout << "Animal is making sound" << endl;
}
};
然后我们创建了一个派生类Dog,它也继承自Animal:
class Dog : public Animal
{
public:
void makeSound() override
{
cout << "Dog is barking" << endl;
}
};
现在我们有一个Animal指针p,它指向了一个Dog对象:
Animal* p = new Dog();
p->makeSound(); // 调用的是Dog的makeSound()函数
这里使用了动态绑定技术,因为p是一个指向Animal对象的指针,但它指向的对象是Dog,所以编译器会根据实际类型来确定调用哪个版本的makeSound()函数,即Dog的makeSound()函数。