引言
多态是面向程序设计语言中数据封装和继承之外的第三个基本特征。多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将“what”和“how”分离开来。
多态性改善了代码的可读性和组织性,同时也使得创建的代码就有具有展性。C++支持编译时多态(静态多态) 和 运行时多态(动态多态),运算符重载和函数重载是编译时多态(运算符重载本质上也是函数重载,要求函数名相同,但形参列表中参数个数、类型、类型顺序三者至少有一个不同,而对于指针或引用,底层const同样能够实现函数重载),而继承联合虚函数能够实现运行时多态。
静态多态和动态多态的区别是函数地址是早绑定(静态联编) 还是 晚绑定(动态联编)。如果函数的调用在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态,即地址是早绑定的;如果函数的调用地址不能在编译期间确定,而需要在运行时才能决定,就属于晚绑定。
#include<iostream>
using namespace std;
class Animal
{
public:
Animal() { cout << "Animal structure!" << endl; }
Animal(const Animal& animal) { cout << "Animal copy structure!" << endl; }
void sleep() { cout << "Animal sleep!" << endl; }
};
class Cat:public Animal
{
public:
Cat() { cout << "Cat structure!" << endl; }
Cat(const Cat& cat) { cout << "Cat copy structure!" << endl; }
void sleep() { cout << "Cat sleep!" << endl; }
};
void test()
{
//Animal animal;//调用Animal构造函数
//Animal* a;//不会调用Animal构造函数
//用基类指针或引用保存子类对象(向上转换)
Animal* p = new Cat;//先调用Animal构造函数,再调用Cat构造函数
p->sleep();//调用基类sleep
cout << "-------------------------------------" << endl;
Cat cat;//先调用Animal构造函数,再调用Cat构造函数
Animal& animal = cat;//不会调用Animal构造函数
animal.sleep();//调用基类sleep
cout << "-------------------------------------" << endl;
//Cat* c = new Animal;//error:"Animal *" 类型的值不能用于初始化 "Cat *" 类型的实体
}
void main() { test(); }
按照正常的类的继承流程,基类指针、引用能够访问子类对象的内存(即向上类型转换),但只能访问子类对象从基类继承来的那部分数据,而无法访问子类对象自身特有的数据。而为了让基类指针、引用能够访问子类对象自身特有的那部分数据,就引入了虚函数的概念。
在没有特殊转换的情况下,派生类指针、引用无法访问基类对象的内存(即向下类型转换),因为派生类指针、引用所能够访问的派生类对象本身特有的那部分内存数据相对于基类对象而言并不存在,是非法内存,因此编译器直接拒绝了派生类指针、引用无法访问基类对象的内存,即使基类的部分内存是能够被合法访问的。
虚函数与多态
虚函数
虚函数的本质是一个函数指针变量,指向虚函数表,虚函数表中存放的是函数的入口地址。为的是能够使用基类指针、引用访问子类对象中的成员方法。
#include<iostream>
using namespace std;
class Animal
{
public:
Animal() { cout << "Animal structure!" << endl; }
Animal(const Animal& animal) { cout << "Animal copy structure!" << endl; }
virtual void sleep() { cout << "Animal sleep!" << endl; }//父类虚函数,只要涉及到继承,子类中的同名函数都是虚函数
};
class Cat:public Animal
{
public:
Cat() { cout << "Cat structure!" << endl; }
Cat(const Cat& cat) { cout << "Cat copy structure!" << endl; }
virtual void sleep() { cout << "Cat sleep!" << endl; }//子类virtual可加可不加
};
void test()
{
//用基类指针或引用保存子类对象(向上转换),同时操作子类对象成员方法
Animal* p = new Cat;//先调用Animal构造函数,再调用Cat构造函数
p->sleep();//调用子类sleep
cout << "-------------------------------------------" << endl;
//如果没有涉及到继承,函数指针变量指向自身的方法
Animal animal;
animal.sleep();
}
void main() { test(); }
当虚函数涉及到继承时,子类会继承父类的虚函数指针vfptr和虚函数表vftable(虚函数表中保存了所有虚函数的入口地址,虚函数指针指向虚函数表,后面会做进一步说明),编译器会将虚函数表中的函数入口地址更新成子类同名(返回值、参数相同)的函数入口地址。
如果基类指针、引用访问虚函数的时候就会直接调用子类的虚函数。
在本例中,指向派生类对象的基类指针p在调用sleep()方法时,会先去寻找派生类继承自基类的sleep()方法,结果发现sleep()是虚函数,因此通过虚函数指针转而索引虚函数表找到派生类自身的虚函数,从而完成调用。
从上面的例子中可能无法体会到虚函数这种机制的强大,那么请看接下来的例子。
在设计函数时,将形参设为父类的引用,将实参根据需要设计为某一个子类,那么在该函数中就能够调用该子类的虚函数。
#include<iostream>
using namespace std;
class Animal
{
public:
Animal() { cout << "Animal structure!" << endl; }
Animal(const Animal& animal) { cout << "Animal copy structure!" << endl; }
virtual void sleep() { cout << "Animal sleep!" << endl; }//父类虚函数,只要涉及到继承,子类中的同名函数都是虚函数
};
class Cat :public Animal
{
public:
Cat() { cout << "Cat structure!" << endl; }
Cat(const Cat& cat) { cout << "Cat copy structure!" << endl; }
virtual void sleep() { cout << "Cat sleep!" << endl; }//子类virtual可加可不加
};
class Cat1 :public Animal
{
public:
virtual void sleep() { cout << "安静!" << endl; }//子类virtual可加可不加
};
class Cat2 :public Animal
{
public:
virtual void sleep() { cout << "轻度!" << endl; }//子类virtual可加可不加
};
class Cat3 :public Animal
{
public:
virtual void sleep() { cout << "雨声!" << endl; }//子类virtual可加可不加
};
class Cat4 :public Animal
{
public:
virtual void sleep() { cout << "雷声!" << endl; }//子类virtual可加可不加
};
void test(Animal& animal)
{
animal.sleep();
}
void main()
{
Cat1 cat1;//先调用Animal类的构造函数,再调用Cat1类的构造函数
Cat2 cat2;
Cat3 cat3;
Cat4 cat4;
test(cat1);
test(cat2);
test(cat3);
test(cat4);
}
在上面的例子中,,将函数的形参设置为基类指针或引用,然后传入不同的派生类对象,即可在函数中调用不同派生类的方法,这同回调函数有着异曲同工之妙。
多态
C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,子类重新定义父类虚函数的做法称为覆盖(override)或者重写。
对于特定的函数进行动态绑定,C++要求在基类中声明这个函数的时候用virtual关键字,动态绑定也就对virtual函数起作用。
如果一个函数在基类中用virtual关键字声明,则在派生类中它都是virtual的,此时派生类中该函数声明时virtual关键字可加可不加(建议加上,方便阅读)。而且virtual关键字只需要在函数声明时加上即可,定义时不需要加。virtual关键字不能修饰构造函数,即构造函数不能为虚函数。
动态绑定机理: 所有的工作都是编译器幕后完成的,通过创建virtual函数告诉编译器进行动态绑定,编译器就会根据动态绑定机制不再执行早绑定。当编译器发现类中有虚函数,会创建一张虚函数表,把虚函数的入口地址放在虚函数表中,并且在类中秘密增加一个指针(vpointer,vptr),指向对象的虚函数表。在多态调用时,根据vptr指针找到虚函数来实现动态绑定。
虚析构
引入
#include<iostream>
using namespace std;
class Animal
{
public:
Animal() { cout << "Animal structure!" << endl; }
Animal(const Animal& animal) { cout << "Animal copy structure!" << endl; }
virtual void sleep() { cout << "Animal sleep!" << endl; }//父类虚函数,只要涉及到继承,子类中的同名函数都是虚函数
~Animal() { cout << "Animal析构函数!" << endl; }
};
class Cat :public Animal
{
public:
Cat() { cout << "Cat structure!" << endl; }
Cat(const Cat& cat) { cout << "Cat copy structure!" << endl; }
virtual void sleep() { cout << "Cat sleep!" << endl; }//子类virtual可加可不加
~Cat() { cout << "Cat析构函数!" << endl; }
};
void test()
{
//用基类指针或引用保存子类对象(向上转换),同时操作子类对象成员方法
Animal* p = new Cat;//先调用Animal构造函数,再调用Cat构造函数
p->sleep();//调用子类sleep
//出现问题:只能析构父类
delete p;
}
void main()
{
test();
}
为了解决上面的问题,需要引入虚析构。
虚析构的作用是通过基类指针、引用释放派生类的所有空间。
虚析构即在析构函数前加virtual修饰。
#include<iostream>
using namespace std;
class Animal
{
public:
Animal() { cout << "Animal structure!" << endl; }
Animal(const Animal& animal) { cout << "Animal copy structure!" << endl; }
virtual void sleep() { cout << "Animal sleep!" << endl; }//父类虚函数,只要涉及到继承,子类中的同名函数都是虚函数
virtual ~Animal() { cout << "Animal析构函数!" << endl; }//虚析构
};
class Cat :public Animal
{
public:
Cat() { cout << "Cat structure!" << endl; }
Cat(const Cat& cat) { cout << "Cat copy structure!" << endl; }
virtual void sleep() { cout << "Cat sleep!" << endl; }//子类virtual可加可不加
~Cat() { cout << "Cat析构函数!" << endl; }
};
void test()
{
//用基类指针或引用保存子类对象(向上转换),同时操作子类对象成员方法
Animal* p = new Cat;//先调用Animal构造函数,再调用Cat构造函数
p->sleep();//调用子类sleep
//出现问题:只能析构父类
delete p;
}
void main()
{
test();
}
纯虚函数和抽象类
//纯虚函数格式
virtual void sleep(void)=0;
如果一个类中有纯虚函数(pure virtual function),那么这个类就是一个抽象类。
抽象类不能实例化对象。
抽象类派生出子类,那么子类必须实现所有的纯虚函数,如果漏掉一个,子类也是一个抽象类。
在设计时,常希望基类只作为派生类的一个接口,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员函数,而不一定要提供一段可能对这个函数毫无意义的代码。
做到这一点,可以在基类中加入至少一个纯虚函数,使基类成为抽象类。纯虚函数使用关键字virtual,并在其后面加上=0。
如果试图去实例化一个抽象类,编译器会阻止这一操作。当继承一个抽象类时,必须实现所有纯虚函数,否则由抽象类派生的类也是抽象类。
建立公共接口的目的使为了将子类公共的操作抽象出来,可以通过一个公共的接口来操纵一组类,且这个公共接口不需要实现(或不需要全部实现)。
#include<iostream>
using namespace std;
//抽象类,提供一个固定的流程接口
class AbstractDrink
{
public:
//烧水
virtual void boil() = 0;
//冲泡
virtual void brew() = 0;
//倒入杯中
virtual void pour() = 0;
//加入辅料
virtual void addSomething() = 0;
//规定流程
void makeRule()
{
boil();
brew();
pour();
addSomething();
}
};
//制作咖啡
class Coffee:public AbstractDrink
{
public:
//烧水
virtual void boil() { cout << "煮农夫山泉!" << endl; }
//冲泡
virtual void brew() { cout << "冲泡咖啡!" << endl; };
//倒入杯中
virtual void pour() {cout<< "将咖啡倒入杯中!" << endl;}
//加入辅料
virtual void addSomething() { cout << "加入牛奶!" << endl; };
};
//制作茶水
class Tea :public AbstractDrink
{
public:
//烧水
virtual void boil() { cout << "煮自来水!" << endl; }
//冲泡
virtual void brew() { cout << "冲泡茶叶!" << endl; };
//倒入杯中
virtual void pour() { cout << "将茶水倒入杯中!" << endl; }
//加入辅料
virtual void addSomething() { cout << "加入食盐!" << endl; };
};
//业务函数
void test(AbstractDrink* drink)
{
drink->makeRule();
delete drink;
}
void main()
{
test(new Coffee);
cout << "-------------------------------------" << endl;
test(new Tea);
}
纯虚析构函数:
class B
{
public:
virtual ~B()=0;//1、virtual修饰,加上=0
};
B::~B(){}//2、必须类外实现析构函数的函数体
用基类指针释放子类对象时,先调用子类析构,再调用父类析构,如果父类析构不实现,就无法实现调用。
总结
①虚函数:用virtual修饰的函数,有函数体(作用于成员函数)
目的:用基类指针、引用操作子类对象的方法
class Base
{
public:
virtual my_fun(void)
{
//有函数体
}
};
②纯虚函数:用virtual修饰的函数,加=0,没有函数体,所在的类为抽象类。
目的:为子类提供固定的流程和接口
class Base
{
public:
virtual my_fun(void)=0;
};
③虚析构:用virtual修饰类中的析构函数
目的:解决基类指针指向派生类对象,并用基类指针删除派生类的对象。
class Base
{
public:
virtual ~Base()
{
//函数体
}
};
④纯虚析构:用virtual修饰类中的析构函数,加=0,此时析构函数的函数体必须在类外实现。
目的:用基类指针删除派生类对象,并提供固定的接口。
class Base
{
public:
virtual ~Base()=0;
};
B::~B()
{
//函数体
}