C++面向对象笔记(5):多态篇

C++多态篇

本章主要的内容:

  1. 普通虚函数、虚析构函数。

  2. 纯虚函数(抽象类、接口类)

  3. RTTI

  4. 异常处理

  5. 隐藏VS覆盖,之间的关系

  6. 早绑定、晚绑定。

  7. 虚函数表。

1.什么是多态

  1. 多态具体到语法中是指,使用父类指针指向子类对象,并可以通过该指针调用子类的函数(方法)

  2. 产生多态的基础是继承关系,没有继承就没有多态。

  3. 多态的语法核心是virtual关键字,必须使用virtual才能使多个类间建立多态关系。(virtual是可以继承的,父类的函数(方法)写了virtual,子类同名的函数(方法)继承virtual;但是,还是推荐自己手动写上,以起到提示的作用)

  4. 封装、继承、多态是面向对象的三大特性。

1.多态分类-静态多态和动态多态(早绑定和晚绑定)

分为静态多态动态多态

1.**静态多态:**也称“早绑定”。

如下所示:

如下的调用方式(函数的重载),程序在编译阶段就知道rect.calcArea()到底要调用哪个函数了,很早的就将函数编译进去了,所以又叫早绑定。

class Rect{
public:
    int calcArea(int width);
    int calcArea(int width, int height);
}

int main(){
    Rect rect;
    rect.calcArea(10);
    rect.calcArea(10,20);
    //...
}

2.**动态多态:**也叫“晚绑定”。

对不同的对象,下达相同的指令,导致对象做着不同的操作。

例如:shape父类有Circle和Rect两个子类,利用父类的指针调用Circle和Rect中计算面积的同名函数,那么同名的函数,却能做出不同的动作,这就是动态多态。

动态多态的前提:必须以封装和继承为基础。

3.普通虚函数-实现动态多态(晚绑定)

在父类指针中调用子类的成员函数:

ShapeCircleRect父类,3个类都有calcArea(),现在想要分别调用CircleRect中的calcArea()

int main(){
    Shape *shape1 = new Circle(4.0);
    Shape *shape2 = new Rect(3.0,5.0);
    shape1->calcArea();
    shape2->calcArea();
    //...
}
/*输出:
Shape->calcArea()
Shape->calcArea()
*/

当运行程序时,就会发现实际上调用的都是Shape中的calcArea(),但是实际上我们想要调用的是CircleRect中的calcArea(),如果要解决当前的问题,就需要使用动态多态

实现动态多态:

我们需要使用virtual关键字,使其成为虚函数,步骤如下:

1.在父类声明中,对需要实现动态多态的函数添加virtual

class Shape{
public:
    virtual double calcArea(){	// 添加virtual
        cout << "calcArea" << endl;
        return 0;
    }
}

2.在子类声明中,对同名函数中添加virtual

系统会自动加,但是手动加上能起到提示的作用,还是建议手动添加。

class Circle : public Shape{
public:
    Circle(double r);
    virtual double calcArea();	// 添加virtual
private:
    double m_dR;
}

3.然后就可以使用本章最开始的代码,对子类中的函数进行调用了。

int main(){
    Shape *shape1 = new Circle(4.0);
    Shape *shape2 = new Rect(3.0,5.0);
    shape1->calcArea();
    shape2->calcArea();
    //...
}
/*输出:
Circle->calcArea()
Rect->calcArea()
*/

4.虚析构函数-解决动态多态中存在的问题

首先说一下动态多态的问题:内存泄露问题。用上一节的代码来讲解。

导致内存泄露的原因是:

  1. 动态多态的调用如下:

    父类的指针去指向子类对象,Shape *shape1 = new Circle(4.0);;并通过父类指针去操作子类中的虚函数,shape1->calcArea();

  2. 使用delete手动释放内存时,析构函数的调用:

    当使用delete去销毁对象时,如果delete后面跟着的是父类的指针,那么将只调用父类的析构函数。如果delete后面跟着的是子类的指针,那么将调用子类以及父类的析构函数。

在此表现为,子类的析构函数没有被调用,只调用了父类的析构函数。如果我们子类的构造函数手动申请了内存(用new,申请在堆中),那么这段内存也就自然没有被释放,比如下面这种类的声明:

// Circle类的声明
class Circle : public Shape{
    public//...
        virtual double calcArea();
    private:
    	//...
    	Coordinate *m_pCenter;// Coordinate类的指针
}

// Circle类的实现-构造函数
Circle::Circle(int x,int y,double r){
    //...
    m_pCenter = new Coordinate(x,y);// 在堆上创建Coordinate类
}
// Circle类的实现-析构函数
Circle::~Circle(){
    delete m_pCenter;// 释放堆中申请的内存
    m_pCenter = nullptr;
}

如上所示,如果没有调用Circle类的析构函数,m_pCenter所指向的类是不会被释放的。

再次强调 - 使用delete释放对象:

当使用delete去销毁对象时,如果delete后面跟着的是父类的指针,那么将只调用父类的析构函数。如果delete后面跟着子类的指针,那么将调用子类和父类的析构函数。

解决动态多态带来的内存释放问题:

要解决这个问题就需要使用虚析构函数,在上一篇笔记有说明以及代码(04继承篇第4章“isA语法“)。

在析构函数上使用virtual修饰即可;需要注意的是,和动态多态中的普通虚函数一样,virtual会被继承,所以在父类的析构函数上加virtual修饰之后,也需要在子类的析构函数上加virtual修饰,起到提示的作用。

虚函数的使用限制:

  1. 只能修饰类中的成员函数。
  2. 不能修饰静态的成员函数。(因为静态成员函数不属于任何一个对象,它是和类同生共死的)
  3. 不能修饰内联函数,如果使用会使计算机忽视inline修饰符。
  4. 构造函数不能成为虚函数。

关于类的构造函数和析构函数调用顺序:

本例中使用了new来在堆上创建Coordinate的内存空间,而堆上的空间是需要我们手动申请和释放的;

所以Coordinate对象析构函数的调用,完全取决于我们什么时候使用delete释放对象,所以Coordinate对象析构函数并不一定在Circle的析构函数之前调用;

例如本例中,就是将Coordinate对象的释放放在Circle的析构函数中,导致Coordinate对象析构函数在Circle的析构函数运行时进行调用。

本章小总结:

  1. 只有虚析构函数,没有虚构造函数。
  2. 虚函数特性可以被继承,当子类中定义的函数与父类中虚函数的声明相同时,该函数也是虚函数。
  3. 虚析构函数是为了避免使用父类指针释放子类对象时造成内存泄露。

5.虚函数的实现原理-多态的实现原理

虚函数的实现:

在本例中,子类Circle没有实现calcArea()这个虚函数,所以直接继承父类的虚函数。

// 父类,声明了calcArea()这个虚函数
class Shape{
public:
    // 虚函数
    virtual double calcArea(){
    }
protected:
    int m_iEdge;
};

// 子类,没有声明calcArea()虚函数,继承父类的虚函数
class Circle : public Shape {
public:
    Circle(double r);
private:
    double m_dR;
}

虚函数表的指针

在具有虚函数的情况下,实例化一个对象的时候,这个对象的第一块内存当中是一个指针,那么这个指针就是虚函数表的指针

父类、子类与虚函数表之间的关系:

父类:

[外链图片转存失败(img-s7WRdUvO-1563352606307)(images/muke_C++/010.png)]

vftable_ptr:虚函数表指针,指向虚函数表。

calcArea_ptr:在虚函数表中的函数指针,指向需要调用的函数。

子类:

[外链图片转存失败(img-UycqnOhN-1563352606308)(images/muke_C++/011.png)]

如果对当前的子类添加虚函数的声明:

class Circle : public Shape {
public:
    Circle(double r);
    virtual double calcArea();// 添加了虚函数的声明
private:
    double m_dR;
}

子类与虚函数表之间的关系变为:

在这里calcArea_ptr这个指针函数指向的地址发生了变化,不再指向与父类相同的calcArea()函数的地址。(覆盖对0x3355的指向,重新指向到0x4B2C上)

[外链图片转存失败(img-LBetAv9r-1563352606309)(images/muke_C++/012.png)]

上面这些就是实现多态的原理了。

6.函数的覆盖与隐藏

函数的隐藏:

在我们还没有学习多态的时候,如果定义了父类和子类,父类和子类出现了重名函数,这个时候就称之为 函数的隐藏。(看第4篇继承篇第3章“继承中同名成员的隐藏”)

// Person为Soldier的父类
int main(){
    Soldier so;
    so.play();			// 调用子类中的play()
    return 0;
}

函数的覆盖:

函数的覆盖特点是使用了虚函数

如果没有在子类当中定义同名的虚函数,那么在子类虚函数表当中,就会写上父类的相应的那个虚函数的函数入口地址。如果我们在子类当中也定义了重名的虚函数,那么在子类的虚函数表当中,我们就会把原来的父类的虚函数的函数地址,覆盖成子类的虚函数的函数地址,那么这种情况就称值为 函数的覆盖。(看本篇第2章“普通虚函数”)

7.虚析构函数的实现原理

虚析构函数的特点是:

当我们通过父类当中,通过virtual修饰析构函数之后,我们通过父类的指针再去指向子类的对象,然后通过delete接父类指针,就可以释放掉子类对象了。

虚析构函数的理论前提

执行完子类的析构函数就会执行父类的析构函数。

结合特点和理论前提

可以看出,实际上虚析构函数的特点就是函数的覆盖带来的。父类的指针指向子类,然后就可以通过delete调用子类和父类的析构函数。

对象的大小

对象的大小包含类的成员属性、虚函数表的指针大小,不包含成员函数的大小。

8.虚函数小结

  1. 在C++中多态的实现是通过虚函数表实现的。

  2. 当类中仅含有虚析构函数,不含其它虚函数时,不产生虚函数表也会产生虚函数表

  3. 每个类只有一份虚函数表,所有该类的对象共用同一张虚函数表。

  4. 两张虚函数表中的函数指针可能指向同一个函数。

问:

类普通成员函数怎么调用?

为什么成员数据有内存存放,虚成员函数能通过虚函数指针->虚函数表找到?

普通成员函数至少有函数指针吧,不然怎么找到实现呢?

如果有为什么对象大小只有数据成员的大小?

答:

因为,内存中有程序代码区,堆区,栈区,全局区(静态区),文字常量区。在定义一个类时,它的成员函数,虚构函数,构造函数就被存入程序代码区,供所有对象调用。

在实例化一个类的对象时,并没有拷贝类的函数,仅仅存入了数据成员,因此类的对象中有数据成员,然而当用父类的指针指向子类的对象时,调用同名函数时会调用父类的同名函数,当想要调用子类的同名函数引入了虚函数,

当调用普通成员函数时,计算机可以在代码区识别该函数,无需用函数指针,因而,在对象中只有数据成员的大小。

转自:类普通成员函数怎么调用?

问:

为什么要用父类的指针实例化子类的对象?为什么不直接使用子类的指针实例化子类的对象?

答:

父类A既可以用A本身实例化,也可以用子类B来实例化,一种方法,多种表达方式,所以是多态。

转载自:为什么要用父类的指针实例化子类的对象?

9.纯虚函数

写法:没有函数体,等于0。即,只有函数声明没有函数定义的虚函数

class Shape{

public:
    // 虚函数
    virtual double calcArea(){
        return 0;
    }
    // 纯虚函数
    virtual double calcPerimeter() = 0;
}

纯虚函数与虚函数表

[外链图片转存失败(img-pTWRysfZ-1563352606309)(images/muke_C++/013.png)]

纯虚函数的不像普通虚函数那样有函数指针(或者说函数指针没有指向实现的函数,因为实现的函数压根不存在)。

10.抽象类

含有纯虚函数的类叫抽象类。

  1. 不允许抽象类实例化对象。

  2. 抽象类的子类也可能是抽象类。

11.接口类

没有构造函数、析构函数,没有数据成员,只有纯虚函数。

  1. 仅含有纯虚函数的类称为接口类。
  2. 接口类也是抽象类
  3. 接口类不能可以被继承
  4. 不能使用接口类实例化对象接口类中仅有纯虚函数,不能含有其它函数,但可以不能含有数据成员。
  5. 可以使用接口类指针指向其子类对象,并调用子类对象中实现的接口类中纯虚函数。
  6. 一个类可以继承一个接口类,也可以继承多个接口类。
  7. 一个类可以继承接口类的同时也继承非接口类。

接口类和抽象类在构成上的区别

接口类:只有纯虚函数,不包含其它非纯虚函数。(无其他成员函数、构造函数、析构函数、数据成员)

抽象类:含有纯虚函数,还可以有其他成员函数。(可以含有成员函数、数据成员、构造函数、析构函数)

12.RTTI-运行时内存识别(Run-Time Type Identification)

RTTI中主要会用到dynamic_cast 和typeid。

看一下常见用法:

// 判断当前指针或引用的类型
// 头文件: #include<typeinfo>
if(typeid(*obj) == typeid(Bus)){
}

// 指针类型转换
// Bird是Flyable的子类(必须要有继承关系)
// 并且必须是多态类型(类中含有虚函数)
Flyable *p2 = new Bird();
Bird *b2 = dynamic_cast<Bird *>(p2);

用代码举个例子:

// Flyable,接口类
class Flyable{
    
public:
    virtual void takeoff() = 0;// 起飞,纯虚函数
    virtual void land() = 0;// 降落,纯虚函数
};

// Bird,继承Flyable,,实现了父类的纯虚函数
class Bird : public Flyable{

public:
    void foraging(){......}
    virtual void takeoff(){......}
    virtual void land(){......}
private:
    // ...
};

// Plane,继承Flyable,实现了父类的纯虚函数
class Plane : public Flyable{
    
public:
    void carry(){......}
    virtual void takeoff(){......}
    virtual void land(){......}
}

// 全局函数:
void doSomethine(Flyable *obj){
    
    obj->takeoff();
    
    // 这里要实现如下效果:
    // 如果是Bird,则运行“觅食”
    // 如果是Plane,则运行“运输”
    cout << typeid(*obj).name() << endl;	// 输出类名
    if(typeid(*obj) == typeid(Bird)){		// 判断类的类型(typeid)
        Bird *bird = dynamic_cast<Bird *>(obj);// 转化为子类指针
        bird->foraging();					// 调用子类函数
    }
    
    obj->land();
}

// 调用全局函数,传递地址到指针中
int main() {
    
    Bird b;
    doSomething(&b);
    
    return 0;
}

dynamic_cast 注意事项:

  1. 只能应用于指针和引用的转换。
  2. 要转换的类型中必须包含虚函数。
  3. 转换成功返回子类的地址,失败返回NULL。

typeid 注意事项:

  1. type_id返回一个type_info对象的引用。
  2. 如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数。
  3. 只能获取对象的实际类型

RTTI小结:

  1. RTTI技术可以通过父类指针识别其所指向对象的真实数据类型

  2. 运行时类型别必须建立在虚函数的基础上,否则无需RTTI技术

  3. 只要存在继承关系就一定可以使用运行时类型识别技术要使用RTTI,必须存在继承关系,继承关系不是RTTI的充分条件,只是必要条件,所以存在继承关系的类不一定可以用RTTI技术。

13.异常处理

常见的异常:

  1. 数组下表越界
  2. 除数为0
  3. 内存不足

异常处理关键字:

try...catch...:在try运行逻辑代码,如果要抛出异常,使用throw,在catch中进行捕获并处理。

throw:抛出异常。(注意:throw之后的代码将不会再运行)

try与catch是一对多的关系,举个例子:

void fun1(){
    
    throw 1;
}


int main(void){
    
    try{
    	fun1();
	}
	catch(int){// 当抛出的异常为int类型
	    //...
	}
	catch(double){// 当抛出的异常为double类型
	    //...
	}
	catch(...){// 这种写法可以捕获所有的异常,用来兜底
	    //...
	}
    
}

如何获取到抛出的异常信息

#include <iostream>
#include <string>
#include <stdlib.h>
using namespace std;

/**
 * 定义函数division
 * 参数整型dividend、整型divisor
 */
int division(int dividend, int divisor)
{
    if(0 == divisor)
	{
        // 抛出异常,字符串“除数不能为0”
		throw string("除数不能为0");
	}
	else
	{
		return dividend / divisor;
	}
}

int main(void)
{
	int d1 = 10;
	int d2 = 2;
	int r = 0;
	cin >> d1;
	cin >> d2;
    // 使用try...catch...捕获异常
	try{
	    int x = division(d1,d2);
	    cout << "division = " << x << endl;
	}catch(string &errorStr){
	    cout << errorStr << endl;
	}

	return 0;
}

异常处理和多态之间的关系

可以创建一个Exception的父类,下面有IndexExceptionMemoryException等子类,然后我们就可以利用多态的特性,用父类捕获各子类,然后做出不同的操作了。

举个例子:

#include <iostream>
using namespace std;

// 父类Exception定义
class Exception{
public:
    // 虚析构函数,用于父类指向子类时,
    // 防止子类的构造函数不被调用,导致的内存泄漏问题
    virtual ~Exception(){}

    // 纯虚函数,在子类中实现
    //virtual void printException() = 0;
	// 虚函数,子类的实现可以将其覆盖
    virtual void printException() {
        cout << "Exception -- printException" << endl;
    }
};

// 两个Exception子类定义
class IndexException : public Exception {
public:
    virtual void printException(){
        cout << "IndexException -- printException" << endl;
    }
};
class MemoryException : public Exception {
public:
    virtual void printException(){
        cout << "MemoryException -- printException" << endl;
    }
};

// 业务处理函数,假如在此函数中发生异常,我们将异常进行抛出
void businessProcessing(){

    // 抛出索引异常
    //throw IndexException();
    // 抛出内存异常
    throw MemoryException();
}

int main() {
	// 对抛出的不同异常,可以做不同的操作(调用相应类中的printException函数)
    try{
        businessProcessing();
    }
    catch(Exception &e){
        e.printException();
    }
    return 0;
}
/*输出:
MemoryException -- printException
或
IndexException -- printException
*/

这里就利用了类的多态 ,这样在业务处理中我们就能用Exception去使用MemoryExceptionIndexException

本篇为视频教程笔记,视频如下:

C++远征之多态篇

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值