观看慕课网《C++远征》系列课程的笔记,链接https://www.imooc.com/u/1349694/courses?sort=publish
1、多态: 相同对象在接收不同的命令是,所做的对象是不同的,不同的对象接受相同的命令,所做出的动作是不同的,分别称作静态多态和动态多态。
2、静态多态也叫作早绑定,指的是类成员函数的重载
3、动态多态也叫作晚绑定,是通过虚函数实现的,只有对象指针和对象引用可以实现多态,不可以用对象实例实现多态。
Person* p = new Worker(); //父类指针指向子类对象,new是放回一个地址,注意它和下面的区别
Person &p = Worker(); //父类对象引用子类对象
虚函数及其原理
例子
设想一个场景,图形类可以派生圆形类和矩形类,现在要调用一个函数计算圆形和矩形的面积,应该如何编写代码?
class Shape{
public:
Shape(){
cout<<"Shape()"<<endl;
}
~Shape(){
cout<<"~Shape()"<<endl;
}
//virtual double calcArea()
double calcArea(){
cout<<"calcArea()"<<endl;
return 0.0;
}
};
class Circle:public Shape{
public:
Circle(double r):m_dR(r){
cout<<"Circle()"<<endl;
}
~Circle(){
cout<<"~Circle()"<<endl;
}
double calcArea(){
cout<<"Circle--calcArea()"<<endl;
return 3.14*m_dR*m_dR;
}
protected:
double m_dR;
};
class Rect:public Shape{
public:
Rect(double width, double height):m_dWidth(width),m_dHeight(height){
cout<<"Rect()"<<endl;
}
~Rect(){
cout<<"~Rect()"<<endl;
}
double calcArea(){
cout<<"Rect--calcArea()"<<endl;
return m_dWidth * m_dHeight;
}
protected:
double m_dWidth;
double m_dHeight;
};
int main(){
Shape* shape1 = new Rect(3,6);
Shape* shape2 = new Circle(5);
cout<<shape1->calcArea()<<endl;
cout<<shape2->calcArea()<<endl;
delete shape1;
delete shape2;
shape1=nullptr;
shape2=nullptr;
}
上方代码运行结果:
Shape()
Rect()
Shape()
Circle()
calcArea()
0
calcArea()
0
~Shape()
~Shape()
分析:用父类指针指向两个子类对象,此时shape1和shape2都是运行的父类的calcArea,在销毁的时候只调用了父类的析构函数。解决方法如下:
1、如果使用Rect类指针指向rect对象,Circle类指针指向circle类对象,自然也没有上面的问题。
2、在父类的calcArea函数前加上virtual关键字,可以使得shape1和shape2分别调用Rect类和Circle的calcArea函数。这两个子类的calcArea函数前可以加virtual也不可以不加,因为virtual会自动继承,但是为了代码可读性,建议加上。
3、在父类的析构函数前加上virtual关键字可以使得销毁shape1和shape2时也会调用子类的构造函数,防止内存泄漏,这在上一篇文章讲继承的时候说过。
小结:
1、本节所讲的是动态多态,它是建立在封装和继承上的
2、动态多态具体到语法中是指,使用父类指针指向子类对象,并可以通过该指针调用子类方法
3、virtual是动态多态的语法核心,必须使用virtual才能使多个类间建立多态关系
虚析构函数
1、多态中存在内存泄漏的问题,通过父类指针销毁子类对象时并不会执行子类的析构函数,只会执行父类的析构函数。这在子类中存在堆中申请的数据时会有内存泄漏的问题。
2、解决的方法是用virtual修饰父类的析构函数,不再赘述。
3、virtual的使用限制
virtual不能修饰普通的函数,必须是类成员函数
不能修饰静态成员函数,静态成员函数在之前的零碎知识点也提到了它没有this指针,它只与类挂钩,而不属于任何一个对象
不能修饰内联函数,可以编译,但是inline关键字会忽略掉
不能修饰构造函数
分析:
1、virtual经常用去修饰父类的同名成员函数,比如上面的构造函数、calcArea(),父类与子类中都存在,那么被修饰的成员函数相当于是虚设的,就不会执行它了,改而去修饰子类的同名成员函数。
2、同名函数在理论上是会将父类的隐藏起来,这在用子类指针指向子类对象时的确如此,但多态的目的就在于用父类指针指向子类对象,仍可以去调用子类的同名成员函数。
3、同名跟重载时区分开,同名是不需要考虑参数列表的,只需要考虑函数名,它是在指父类与子类间存在函数名相同的函数。重载是在指同一个类中有多个函数名相同的函数,他们参数列表不同,编译器会自动为他们的名字后方添上参数类型的名字以作区分。
虚函数和虚析构函数的原理
函数指针
函数指针也是四字节的指针,指向代码区中对应的二进制代码,调用函数时是根据函数指针在代码区中寻找到对应的二进制代码,从而执行函数
虚函数表
1、虚函数表是关键的原理。
2、如果类中存在virtual关键字修饰的虚函数,当实例化这个类时,会自动地添上一个默认的数据成员——虚函数表指针(vftagble_ptr),这个指针指向了虚函数表,虚函数表中存放了所有虚函数的入口地址,也即是虚函数的函数指针。
3、子类在实例化时也会自动地添上一个虚函数表指针,这个指针和父类的虚函数表指针指向的是不同的位置,即子类和父类有各的虚函数表。
4、如果子类中没有去定义虚函数的同名函数,那么子类的虚函数表中的虚函数指针,会将父类的虚函数表中的相应虚函数指针复制过来。
5、如果子类定义了虚函数的同名函数,那么子类的虚函数表中的虚函数指针会有自己的相应地址,相当于覆盖了父类虚函数表中的相应虚函数指针。这也是“覆盖”的定义,与之前的“隐藏”需要区分开。
6、覆盖与隐藏
覆盖和隐藏,都是发生在父类和子类存在同名函数的时候,不同在于
i. 隐藏是用在子类指针指向子类对象时,通过子类指针调用同名函数会自动调用子类的同名函数,而把父类的同名函数忽略掉,但非要去访问父类的同名函数也是可以的。注意也不一定要用对象指针访问,直接用对象也是可以的。
ii. 覆盖是用在父类指针指向子类对象时,通过子类指针调用同名函数会自动地调用子类的同名函数,父类的同名函数无法访问到,是为覆盖。注意这种一定是通过对象指针访问到的
7、虚函数表指针是处于对象存储单元的第一位,32位编译器占据4个存储单元,64位占据8个存储单元。所以在用sizeof计算对象大小时会比没有虚函数时多了4或者8
8、不考虑虚函数存在的情况,如果类中没有任何成员变量,则sizeof(对象)=1,不等于0,因为需要标记这个对象的存在。如果类中有虚函数,因为在实例化时会自动添上虚函数表指针,所以sizeof(对象)=4或者8。
贴两张课程中的图帮助理解,对应的类定义也附上。版权属于慕课网
父类Shape的虚函数表示意图
子类Circle的虚函数表示意图,此时Circle是没有去定义虚函数calcArea的,也即没有同名函数,所以会把父类的虚函数CalcArea的函数地 址复制过来
如果子类定义了同名的虚函数,那么就会把子类虚函数表的对应虚函数的函数地址覆盖成自己的
class Shape{
public:
virtual double calcArea(){ return 0;}
protected:
int m_iEdge;
};
class Circle{
public:
Circle(double r);
// virtual double calcArea(){ } //如果有定义同名的虚函数
private:
double m_dR;
}
纯虚函数和抽象类
纯虚函数
1、定义:没有函数体的虚函数,定义时后面跟着=0
2、纯虚函数在虚函数表中也有一个虚函数指针,只不过这个指针指向0
3、注意写个{ }里面什么都不写,这也是有函数体,是不是虚函数要看后面是不是跟着=0
class Shape{
public:
virtual double calcArea(){ return 0;} //虚函数
virtual double calcPerimeter() = 0; //纯虚函数
};
抽象类
1、含有纯虚函数的类叫做抽象类
2、抽象类是不允许实例化对象的
3、抽象类的子类也可能是抽象类,只有将纯虚函数的函数体补充上才可以去实例化对象
4、有时基类很抽象,只知道有某些类成员函数,但是无法确定函数体内容,需要派生出更具体的派生类,让用户能够将函数体补全。
class Person{ //抽象类
public:
Person();
virtual play()=0;
}
class Worker:public Person{ //抽象类
public:
Worker();
virtual void work()=0;
}
接口类
1、定义:仅含有纯虚函数的类称为接口类。没有数据成员,成员函数只有纯虚函数(术语数据成员与成员变量一致),连构造函数和析构函数也没有
2、可以使用接口类指针指向其子类对象,并调用子类对象中实现的接口类中纯虚函数。
3、一个类可以继承一个接口类,也可以继承多个接口类。还可以同时继承接口类和非接口类
class Flyable{
public:
virtual void takeoff()=0;
virtual void land()=0;
};
class Plane:public Flyable{
public:
Plane(string code):m_strCode(code){ }
virtual void takeoff(){
cout<<"Plane-takeoff()"<<endl;
}
virtual void land(){
cout<<"Plane-land()"<<endl;
}
void printCode(){
cout<<m_strCode<<endl;
}
private:
string m_strCode;
};
class FighterPlane:public Plane{
public:
FighterPlane(){ }
virtual void takeoff(){
cout<<"FighterPlane-takeoff()"<<endl;
}
virtual void land(){
cout<<"FighterPlane-land()"<<endl;
}
};
flyMatch(Flyable* f1, Flyable* f2){
f1->takeoff();
f1->land();
f2->takeoff();
f2->land()
}
int main(){
Plane p1("001");
Plane p2("002");
p1.printCode();
p2.printCode();
flyMatch(&p1,&p2);
Plane p3("003");
Plane p4("004");
p3.printCode();
p4.printCode();
flyMatch(&p3,&p4);
}
运行时类型识别
RTTI:Run-Time Type Identification
RTTI的两大关键是typeid获取对象信息和dynamic_cast对象指针转换
class Flyable{
public:
virtual void takeoff()=0;
virtual void land()=0;
};
class Bird:public Flyable{
public:
virtual void takeoff(){
cout<<"Bird-takeoff"<<endl;
};
virtual void land(){
cout<<"Bird-land"<<endl;
};
void foraging(){
cout<<"Bird-foraging"<<endl;
};
};
class Plane:public Flyable{
public:
virtual void takeoff(){
cout<<"Plane-takeoff"<<endl;
};
virtual void land(){
cout<<"Plane-land"<<endl;
};
void carrying(){
cout<<"Plane-carrying"<<endl;
};
};
void doSomething(Flyable* obj){
obj->takeoff();
cout<<typeid(*obj).name()<<endl; //打印出对象obj实际的对象类型,可以是Flyable的或者其派生类Bird和Plane
//const type_info& ti = typeid(*obj); typeid返回的是一个type_info对象的引用
//如果obj指向的是一个Bird对象,则调用Bird的成员函数foraging,否则调用Plane的成员函数carry
if(typeid(*obj)==typeid(Bird)){ //类型比对
Bird *bird = dynamic_cast<Bird *>(obj); //类型指针转换
bird->foraging();
}
if(typeid(*obj)==typeid(Plane)){
Plane *plane = dynamic_cast<Plane *>(obj);
plane->carrying();
}
obj->land();
}
int main(){
Flyable* f1 = new Bird();
Flyable* f2 = new Plane();
doSomething(f1);
doSomething(f2);
}
dynamic_cast的注意事项
1、只能应用于指针和引用的转换,不可以用于对象
Flyable p;
Bird b = dynamic_cast<Bird>p; //报错
2、要转换的类型中必须包含虚函数,如果没有虚函数,也没有RTTI的必要
class Flyable{ //这样定义将不是多态类型,不能使用dynamic_cast
public:
void takeoff(){ }
void land(){ }
};
3、转换成功则返回子类的地址,失败返回nullptr
typeid注意事项
1、typeid返回一个type_Info对象的引用
void type_info{ //c++自带的一个类
public:
const char* name() const;
bool operator==(const type_info& rhs) const;
bool operator!=(const type_info& rhs) const;
int before(const type_info& rhs) const;
virtual ~type_info();
private:
...
}
2、typeid也可以用到一半的数据类型
int a=10;
cout<<typeid(a).name()<<endl; //输出int
3、*p所指对象,p对象指针
Flyable *p = new Bird();
cout<<typeid(p).name()<<endl; //输出 class Flyable *
cout<<typeid(*p).name()<<endl; //输出 class Bird
3、如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数,如果没有虚函数,也没有RTTI的必要
4、只能获取对象的实际类型
异常处理
1、对有可能发生异常的地方做出预见性的安排
2、关键字try…catch…,throw…,try中运行主逻辑,catch捕捉异常并对异常进行处理,throw抛出异常
3、
class Exception{ //异常类父类,是个抽象类
public:
Exception()
virtual ~Exception();
virtual void printException(){
cout<<"Exception--printException"<<endl;
}
};
class IndexException:public Exception{ //Exception的子类,定义了数组越界的异常
public:
IndexException()
virtual ~IndexException();
virtual void printException(){
cout<<"index invalid"<<endl;
}
};
void test1(){
throw 10; //抛出异常,一般是判断数据是否出现问题,是就throw一个异常
}
void test2(){
throw 0.1;
}
void test3(){
throw new IndexException(); //指针实现多态
}
void test4(){
throw IndexException(); //引用实现多态
}
int main(){
//捕获的异常需要和抛出的异常相匹配
//是整型就用catch(int),是小数就用catch(float)
try{
test1(); //主逻辑,中间可能会抛出异常,异常会被catch捕获
cout<<"continue"<<endl; //如果test1()运行时抛出了异常,那么下面都代码都不再运行,直接到catch
}
catch(int){ //异常捕获
cout<<"test1"<<endl; //异常处理
}
//同上
try{
test2();
}
catch(float){
cout<<"test1"<<endl;
}
try{
test2();
}
catch(float errValue){ //抛出的异常是可以用一个变量接住的
cout<<errValue<<endl;
cout<<"test2"<<endl;
}
try{
test3();
}
catch(Exception* err){ //指针实现多态
err->printException();
cout<<"test3"<<endl;
}
try{
test4();
}
catch(Exception& err){ //引用实现多态
err.printException();
cout<<"test4"<<endl;
}
try{
test1();
}
catch(...){ //可以用...来笼统地囊括所有抛出的异常
cout<<"test1"<<endl;
}
}