多态
- 定义:
父类型的引用指向子类型的对象。用一句比较通俗的话:同一操作作用于不同的对象,可以产生不同的效果。
-
发出同样的消息,被不同类型的对象接收,可能导致不同的行为
eg、动物的移动行为 Move,对于这一接口1、鱼在水中游
2、鸟在天上飞;
3、马在草原跑
调用同名的函数导致不同的行为,以一致的观点看待从同一个类派生下来 的对象。减轻了分别设计的负担。
- 多态实现:(1)静态多态、(2)动态多态
1、函数重载(静态多态)
2、运算符重载(静态多态)
3、模板(静态多态)
4、虚函数(动态多态)
静态多态和动态多态的区别其实只是在什么时候将函数实现和函数调用关联起来,是在编译时期还是运行时期,即函数地址是早绑定还是晚绑定
静态多态(静态绑定)
编译器在编译期间可以确定函数的调用地址,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数可以调用就调,没有的话就会发出警告或者报错。静态多态也往往被叫做静态联编。
动态多态(动态绑定)
指函数调用的地址不能在编译器期间确定,必须需要在运行时才确定,这就属于晚绑定,动态多态也往往被叫做动态联编。动态多态的条件:基类中必须包含虚函数
- 多态作用:
(1)多态性有助义更好地对程序进行抽象
1、控制模块能(可以看做基类)专注于一般性问题的处理
2、具体的操作交给具体的对象去做(基类不关心派生类如何实现虚函数,只需要关注他们实际支持的接口即可)
(2)多态性有助于提高程序的可扩展性
1、可以把控制模块与被操作的对象分开
2、可以添加已定义类的新对象,并能管理该对象
3、可以添加新类(已有类的派生类)的新对象,并能管理该对象
虚函数
- 使用背景:
在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数。在同一类中是不能定义两个名字相同、参数个数和类型都相同的函数的,否则就是“重复定义” - 定义:
在基类中以virtual修饰的成员函数,允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数
定义格式:virtual 函数类型 函数名称(参数列表)
- 特征:
1、如果在基类中被声明为虚函数,则他在所有的派生类中都是虚函数
2、只有通过基类指针或者引用调用虚函数才能引起动态绑定
3、虚函数不能声明为静态(因为声明为静态,没有this指针)
4、内联函数不能为虚函数?
5、构造函数不能是虚函数;
6、析构函数可以是虚函数,而且通常声明为虚函数。
- static成员不属于任何类对象或类实例,所以即使给此函数加上virutal也是没有任何意义的。
- 静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。
- 虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable.
对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.虚函数的调用关系:this -> vptr -> vtable ->virtual function
内联函数不能为虚函数
1、虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
2、内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
3、inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
构造函数不能是虚函数
1、虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,无法调用构造函数。
析构函数可以是虚函数
基类类型指针或者引用指向派生类,析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数。则只会析构基类,不会析构派生类对象,从而造成内存泄漏
//虚函数应用举例
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Fun1()
{
cout << "Base::Func1......" << endl;
}
virtual void Fun2()
{
cout << "Base::Func2......" << endl;
}
void Fun3()
{
cout << "Base::Func3......" << endl;
}
};
class Derived:public Base
{
public:
/*virtual*/ void Fun1()//不用加virtual,也是虚函数
{
cout << "Derived::Func1......" << endl;
}
/*virtual*/ void Fun2()
{
cout << "Derived::Func2......" << endl;
}
void Fun3()
{
cout << "Derived::Func3......" << endl;
}
};
int main()
{
Base* p;
Derived d;
p = &d;
p->Fun1();//Fun1为虚函数,基类指针指向派生类对象,调用派生类对象的虚函数
p->Fun2();
p->Fun3();//Fun3为非虚函数,根据p指针实际类型来调用相应类的成员函数
return 0;
}
虚析构函数
(1)何时需要虚析构函数
1、当通过基类指针删除派生类对象
2、允许其他人通过基类指针调用对象的析构函数(通过delete这样做是正常的),并且被析构的对象是有重要的析构函数的派生类的对象,就需要让基类的析构函数做为虚函数。
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Fun1()
{
cout << "Base::Func1......" << endl;
}
virtual void Fun2()
{
cout << "Base::Func2......" << endl;
}
void Fun3()
{
cout << "Base::Func3......" << endl;
}
Base()
{
cout << "Base......" << endl;
}
/*virtual*/ ~Base()
{
cout << "~Base......" << endl;
}
};
class Derived:public Base
{
public:
/*virtual*/ void Fun1()//不用加virtual,也是虚函数
{
cout << "Derived::Func1......" << endl;
}
/*virtual*/ void Fun2()
{
cout << "Derived::Func2......" << endl;
}
void Fun3()
{
cout << "Derived::Func3......" << endl;
}
Derived()
{
cout << "Derived......" << endl;
}
~Derived()
{
cout << "~Derived......" << endl;
}
};
int main()
{
Base* p;
p = new Derived;
delete p;
return 0;
}
非虚析构函数实验结果如下:发现没有调用派生类构造函数
虚析构函数实验结果如下:调用了派生类的构造函数
virtual ~Base()
{
cout << "~Base......" << endl;
}
虚表指针
(1)虚函数的动态绑定是通过虚表实现的;
(2)包含虚函数的类,开始的4个字节存放指向虚表的指针
//应用举例
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Fun1()
{
cout << "Base::Func1......" << endl;
}
virtual void Fun2()
{
cout << "Base::Func2......" << endl;
}
int data1_;
};
class Derived :public Base
{
public:
/*virtual*/ void Fun2()
{
cout << "Derived::Func2......" << endl;
}
virtual void Fun3()
{
cout << "Derived::Func3......" << endl;
}
int data2_;
};
//[定义一个函数指针类型]
//(https://blog.youkuaiyun.com/nuoma2013/article/details/72922006)
//作用:调用虚表里面的函数
typedef void(*FUNC)();
int main()
{
cout << "Input Base.................................." << endl;
cout << "Base: "<< sizeof(Base) <<" Byte" <<endl;
Base b;
//指向指针的指针,取对象内存地址分布,见下图
long** p = (long**)&b;
cout << &b << endl;
cout << &p[0] << endl;//数据成员1地址
cout << &p[1] << endl;//数据成员2地址
cout << endl;
cout << &p[0][0] << endl;//虚表内函数成员1地址
FUNC fun = (FUNC)p[0][0];
fun();//调用虚表内第一个函数成员
cout << &p[0][1] << endl;
fun = (FUNC)p[0][1];
fun();
cout << "Input Derived............................" << endl;
cout << "Derived: " << sizeof(Derived) << " Byte" << endl;
Derived d;
p = (long**)&d;
cout << &d << endl;
cout << &p[0] << endl;
cout << &p[1] << endl;
cout << &p[2] << endl;
cout << endl;
cout << &p[0][0] << endl;
fun = (FUNC)p[0][0];
fun();
cout << &p[0][1] << endl;
fun = (FUNC)p[0][1];
fun();
cout << &p[0][2] << endl;
fun = (FUNC)p[0][2];
fun();
return 0;
}
基类内存分布结果分析:数据成员占用内存8字节
基类虚指针大小(4字节)+基类数据成员data1_(4字节)
派生类内存分布结果分析:数据成员占用内存12字节:
虚指针大小(4字节)+基类数据成员data1_(4字节)+派类数据成员data2_(4字节)
Derived类:
(1)没有定义Fun1,所以虚表内继承Base:Fun1;
(2)自定义Fun2,函数名和参数和基类相同,对基类的Fun2覆盖了;
(3)Base没有定义Fun3,自然是自身的Fun3.
抽象类和纯虚函数
//举例说明:draw()绘制形状的动作
class Shape {draw()}//绘制形状的形状,说不通,形状是抽象的事物,现实中不存在形状这个具体的东西。
class Circle {draw()}//Circle 类需要具体绘制圆形这一形状
class Square {draw()}
- 纯虚函数:
一个在基类中声明的虚函数,它在该基类中没有定义具体的操作内容,要求各派生类根据实际需要定义自己的版本,纯虚函数的声明格式为:
virtual 函数类型 函数名(参数表) = 0;
举例说明:
class Shape
{
public:
virtual void draw()
{
cout << "draw()........" << endl;
}
virtual int Area() = 0;
private;
int line_;
}
如果是一个普通的虚函数,那么,在虚函数表中,其函数指针就是一个有意义的值;如果是一个纯虚函数,那么,在虚函数表中,其函数指针的值就是0。也就是说,在虚函数表当中,如果是纯虚函数,那么就实实在在的写上0。
- 抽象类:
只要含有纯虚函数的类就是抽象类。抽象类的唯一用途是为派生类提供基类,纯虚函数的作用是作为派生类中的成员函数的基础,并实现动态多态性。
带有纯虚函数的类称为抽象类():
class <基类名>
{
virtual <类型><函数名>(<参数表>) = 0;
…
};
- 抽象类作用:
(1)抽象类为了抽象和设计 的目的而声名,将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。
(2)暂时无法实现的函数,声明为纯虚函数,实例化留给派生类去做。 - 注意:
1、从抽象类继承的类,必须为每一个纯虚函数赋予功能。
2、不能定义一个抽象类的对象。
3、但是可以定义一个指向抽象类的指针。(用来实现多态性)
4、抽象类可以派生出抽象类,因为如果子类没有将纯虚函数覆盖完,该子类还是抽象类。对于抽象类来说,它无法实例化对象,而对于抽象类的子类来说,只有把抽象类中的纯虚函数全部实现之后,那么这个子类才可以实例化对象。
5、假如确实某个基类的虚函数一定会被其他所有的派生类覆盖掉,那么不如将其设置为纯虚函数。
析构函数可以是纯虚函数
对于一个没有任何接口的类,想要将其定义为抽象类,只能将虚析构函数声明为纯虚函数;
class Base
{
public:
virtual ~Base() = 0;
};
class Derived:public Base
{
};
Derived d;//Error,因为d如果析构的话,先调用派生类的构造函数,在调用基类的析构函数,而基类的析构函数没有实现
object slicing 和虚函数
- object slicing(对象切割)
当派生类对象赋给一个基类对象时(或者派生类对象强制转换为基类对象),会发生对象切割,因为派生类对象有自己定义的成员。发生对象切割时,派生类中的数据成员会丢失。反之基类赋值给派生类则不行。
举例说明:
//举例:CObject为基类,CDocument 继承 CObject,CMyDoc 继承 CDocument
#include<iostream>
using namespace std;
class CObject
{
public:
virtual void Serialize()
{
cout << "CObject::Serialize()" << endl;
}
};
class CDocument :public CObject
{
public:
virtual void Serialize()
{
cout << "CDocument::Serialize()" << endl;
}
void func()
{
cout << "CDocument::func()" << endl;
Serialize();//非虚调用虚函数
}
int data1_;
};
class CMyDoc :public CDocument
{
public:
virtual void Serialize()
{
cout << "CMyDoc::Serialize()" << endl;
}
int data2_;
};
int main()
{
CMyDoc mydoc;
CMyDoc* pmydoc = new CMyDoc;
cout << "#1 testing" << endl;
mydoc.func();
cout << "#2 testing" << endl;
((CDocument*)(&mydoc))->func();//基类指针指向派生类对象
cout << "#3 testing" << endl;
pmydoc->func();
cout << "#3 testing" << endl;
//mydoc对象强制转换为CDocument对象,向上转型,派生类特有成员会丢失
//完完全全将派生类转化为基类对象,虚表也发生了变化
//派生类对象拷贝构造成基类对象
((CDocument)(mydoc)).func();
return 0;
}
1、分析:((CDocument*)(&mydoc))->func();
类似于“CDocument * p = &mydoc;
p->func();
基类对象指针指向派生类,因为mydoc没有定义func(),所以调用基类的.并且((CDocument*)(&mydoc))-仅仅是一个指针,不会调用拷贝构造函数,所以切片不会发生。
2、分析:((CDocument)(mydoc)).func();发生了切片
派生类对象拷贝构造成基类对象,由编译器生成的拷贝构造函数不需要对虚拟机制进行额外的处理,此时依照浅拷贝,所有属于mydoc的信息都丢掉了。
//派生类对象拷贝构造成基类对象证明
//编译器执行深拷贝,正确的初始化CDocument的虚表,
//而不是仅仅将mydoc的虚表拷贝过来
class CDocument :public CObject
{
public:
CDocument()
{
cout << "CDocument()......." << endl;
}
CDocument(const CDocument& other)
{
cout << "CDocument(const CDocument& other)" << endl;
}
virtual void Serialize()
{
cout << "CDocument::Serialize()" << endl;
}
void func()
{
cout << "CDocument::func()" << endl;
Serialize();//非虚调用虚函数
}
int data1_;
};
overload、override、overwrite
1. Overload(重载):
在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。
Overload
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
2. Override(覆盖):
是指派生类函数覆盖基类函数,特征是:
Override
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
#include <iostream>
using namespace std;
class base
{
public:
virtual void Fun1()
{
cout<<"Base Fun1..."<<endl;
}
virtual void Fun2()
{
cout<<"Base Fun2..."<<endl;
}
void Fun3()
{
cout<<"Base Fun3..."<<endl;
}
};
class Derived:public Base
{
public:
void Fun1()
{
cout<<"Derived Fun1..."<<endl;
}
void Fun1()
{
cout<<"Derived Fun2..."<<endl;
}
void Fun3()
{
cout<<"Derived Fun3..."<<endl;
}
};
int main()
{
Base* p;
Derived d;
p=&d;
p->Fun1(); //因为Fun1是虚函数,所以调p指向的对象的Fun1
p->Fun2(); //同Fun1
p->Fun3(); //Fun3不是虚函数,所以根据指针的类型,是基类指针,调基类的Fun3
return 0;
}
- Overwrite(重写):
是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
//假设基类中有
void show()
{
cout<<"Base::show..."<<endl;
}
//派生类中有
void show(int n)
{
cout<<"Derived::show..."<<endl;
}
//main函数中d.show()会编译出错。相当于基类的show()被隐藏
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
//派生类的show()不带参数,此时会调派生类的show(),若要访问基类的show()或x_
d.Base::show();
d.Base::x_