多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征。
多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。
c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。
静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//计算器
class Caculator{
public:
void setA(int a){
this->mA = a;
}
void setB(int b){
this->mB = b;
}
void setOperator(string oper){
this->mOperator = oper;
}
int getResult(){
if (this->mOperator == "+"){
return mA + mB;
}
else if (this->mOperator == "-"){
return mA - mB;
}
else if (this->mOperator == "*"){
return mA * mB;
}
else if (this->mOperator == "/"){
return mA / mB;
}
}
private:
int mA;
int mB;
string mOperator;
};
<p align="left"><span style="color:green;">//</span><span style="color:green;">这种程序不利于扩展,维护困难,如果修改功能或者扩展功能需要在源代码基础上修改</span></p>
//开闭原则 对扩展开放,对修改关闭 ,把一个简单问题复杂化
//抽象计算器
class AbstractCaculator{
public:
void setA(int a){
this->mA = a;
}
void setB(int b){
this->mB = b;
}
virtual int getResult() = 0; //纯虚函数
protected:
int mA;
int mB;
};
//加法计算器 如果继承一个抽象类,必须实现里面纯虚函数
class plusCaculator : public AbstractCaculator{
public:
virtual int getResult(){
return this->mA + this->mB;
}
};
//减法计算器
class minusCaculator : public AbstractCaculator{
public:
virtual int getResult(){
return this->mA - this->mB;
}
};
//只需要新增一个类 (扩展功能)
class multipliesCaculator : public AbstractCaculator{
public:
virtual int getResult(){
return this->mA * this->mB;
}
};
//做业务
void doBussiness(AbstractCaculator* caculator){
int a = 10;
int b = 20;
caculator->setA(a);
caculator->setB(b);
int ret = caculator->getResult();
//...其他业务
cout << "ret:" << ret << endl;
}
void test(){
AbstractCaculator* caculator = NULL;
caculator = new plusCaculator;
doBussiness(caculator);
//父类指针指向子类对象,多态表现
caculator = new minusCaculator;
doBussiness(caculator);
//来乘法
caculator = new multipliesCaculator;
doBussiness(caculator);
}
int main(){
test();
system("pause");
return EXIT_SUCCESS;
}
向上类型转换及问题
对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。
也就是说:父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。
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对象,输出的结果应该是Dog::speak。当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding).C语言中只有一种函数调用方式,就是早绑定。
上面的问题就是由于早绑定引起的,因为编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,编译器确定了应该调用的speak是Animal::speak的,而不是真正传入的对象Dog::speak。
问题解决方案(虚函数,vitual function)
C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写。
对于特定的函数进行动态绑定,c++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用.
1> 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要.
2> 如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual的.
3> 在派生类中virtual函数的重定义称为重写(override).
4> Virtual关键字只能修饰成员函数.
5> 构造函数不能为虚函数
class Animal{
public:
virtual void speak(){
cout << "动物在唱歌..." << endl;
}
};
class Dog : public Animal{
public:
virtual void speak(){
cout << "小狗在唱歌..." << endl;
}
};
void DoBussiness(Animal& animal){
animal.speak();
}
void test(){
Dog dog;
DoBussiness(dog);
}
问题:C++的动态捆绑机制是怎么样的?
首先,我们看看编译器如何处理虚函数。当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr),这个指针是指向对象的虚函数表。在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。
在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?在对象构建的时候,也就是在对象初始化调用构造函数的时候。编译器首先默认会在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象的虚函数表。
起初,子类继承基类,子类继承了基类的vptr指针,这个vptr指针是指向基类虚函数表,当子类调用构造函数,使得子类的vptr指针指向了子类的虚函数表。
多态的成立条件:
1> 有继承
2> 子类重写父类虚函数函数
a) 返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外)
b) 子类中virtual关键字可写可不写,建议写
3>类型兼容,父类指针,父类引用 指向 子类对象
抽象基类和纯虚函数(pure virtual function)
在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。
做到这点,可以在基类中加入至少一个纯虚函数(pure virtualfunction),使得基类称为抽象类(abstract class).
1> 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。
2> 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
3> Virtualvoid 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);
}
虚析构函数
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//1. 构造函数不能够使用virtual关键字
class MyClass{
public:
MyClass(){
cout << "MyClass构造函数" << endl;
}
virtual void func(){
cout << "MyClass::func" << endl;
}
#if 0
//虚析构函数
virtual ~MyClass(){
cout << "MyClass析构函数" << endl;
}
#endif
//纯虚析构函数 必须要给纯虚函数加上实现体
virtual ~MyClass() = 0;
};
MyClass::~MyClass(){
cout << "MyClass析构函数" << endl;
}
//
class Son : public MyClass{
public:
Son(){
pName = new char[100];
cout << "Son构造函数" << endl;
}
virtual void func(){
cout << "Son::func" << endl;
}
~Son(){
cout << "Son析构函数" << endl;
if (pName != NULL){
delete[] this->pName;
}
}
public:
char* pName;
};
void test(){
MyClass* p = new Son;
p->func();
delete p;
//如果用父类指针去释放子类对象资源,只会调用基类的析构函数,不会调用子类的,导致对象清理不干净,内存泄漏。
//解决办法:使用虚析构函数
}
//接口类
class A{
virtual void a() = 0;
virtual void b() = 0;
virtual void c() = 0;
virtual void d() = 0;
};
//接口类里只有纯虚函数,没有数据(没有定义变量)
//在c++中不建议继承多个类,但是可以继承多个接口
int main(){
test();
system("pause");
return EXIT_SUCCESS;
}
重写 重载 重定义
1) 重载,同一作用域的同名函数
1. 同一个作用域
2. 参数个数,参数顺序,参数类型不同
3. 和函数返回值,没有关系
4. const也可以作为重载条件 //do(const Teacher&t){} do(Teacher& t)
2) 重定义(隐藏)
1. 有继承
2. 子类(派生类)重新定义父类(基类)的同名成员(非virtual函数)
3) 重写(覆盖)
1. 有继承
2. 子类(派生类)重写父类(基类)的virtual函数
3. 函数返回值,函数名字,函数参数,必须和基类中的虚函数一致
class A{
public:
//同一作用域下,func1函数重载
void func1(){}
void func1(int a){}
void func1(int a,int b){}
void func2(){}
virtual void func3(){}
};
class B : public A{
public:
//重定义基类的func2,隐藏了基类的func2方法
void func2(){}
//重写基类的func3函数,也可以覆盖基类func3
virtual void func3(){}
};
指向成员变量的指针
class A{
public:
A(int param){
mParam = param;
}
public:
int mParam;
};
void test(){
A a1(100);
A* a2 = new A(200);
int* p1 = &a1.mParam;
int A::*p2 = &A::mParam;
cout << "*p1:" << *p1 << endl;
cout << "a1.*p2:" << a1.*p2 << endl;
cout << "a2->*p2:" << a2->*p2 << endl;
}
指向成员函数的指针
class A{
public:
int func(int a,int b){
return a + b;
}
};
void test(){
A a1;
A* a2 = new A;
//初始化成员函数指针
int(A::*pFunc)(int, int) = &A::func;
//指针解引用
cout << "(a1.*pFunc)(10,20):" << (a1.*pFunc)(10, 20) << endl;
cout << "(a2->*pFunc)(10,20):" << (a2->*pFunc)(10, 20) << endl;
}
指向静态成员的指针
class A{
public:
static void dis(){
cout << data << endl;
}
static int data;
};
int A::data = 100;
void test(){
int *p = &A::data;
cout << *p << endl;
void(*pfunc)() = &A::dis;
pfunc();
}