C++基础(四)多态

1. 什么是多态

面对对象三大特征:封装,多态,继承
多态就是相同对象收到不同消息或不同对象收到相同消息时产生不同的动作,简而言之就是静态多态(早绑定)动态多态(晚绑定)
静态多态:就是重载成员函数,程序在编译阶段就已经确定了要用哪个函数,很早的就将函数编译进去了。比如矩形类中有两个计算面积的函数:

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

动态多态:比如同样是计算面积这个指令,对圆来说有自己的计算面积的函数,对矩形来说它也有自己的计算面积的函数。动态多态必须以封装和继承为基础

2. 虚函数

class Shape//形状类
{
public:
    double calcArea()
    {
        cout<<"calcArea"<<endl;
        return 0;
     }
};
class Circle:public Shape      //公有继承自形状类的圆形类
{
public:
    Circle(double r);
    double calcArea();
private:
    double m_dR;
};
double Circle::calcArea()
{
    return 3.14*m_dR*m_dR;
}
class Rect:public Shape       //公有继承自形状类的矩形类
{
public:
    Rect(double width,double height);
    double calArea();
private:
    double m_dWidth;
    double m_dHeight;
}double Rect::calcArea()
{
    return m_dWidth*m_dHeight;
}
int main()
{
    Shape *shape1=new Circle(4.0);
    Shape *shape2=new Rect(3.0,5.0);
    shape1->calcArea();//这里调用的其实是父类的函数
    shape2->calcArea();//这里调用的其实是父类的函数
    .......
    return 0;
}

上面的例子并不是我们想要的结果,shape1和shape2都调用的父类的函数。解决办法是把父类函数加上virtual关键字,成为虚函数,virtual double calcArea();给子类的重名函数也加上virtual,可以不加,但建议加。

3. 虚析构函数

前面已经讲过,父类指针指向子类,如Shape* s=new Circle(),Circle继承Shape,在销毁父类时,即delete s;s=NULL;只会执行父类的析构函数,这样如果子类有指针类型的成员变量并且在构造函数中申请了内存,在析构函数中释放,就会造成内存泄漏。解决的办法是将父类的析构函数加上关键字virtual,成为虚析构函数。

virtual在函数中的使用限制:

  1. 不能修饰普通函数,即普通函数不能是虚函数,虚函数必须是一个类的成员函数,不能是全局函数
  2. 不能修饰静态成员函数,因为静态成员函数不属于任何一个对象,它是和类同生共死的
  3. 不能修饰内联函数,如果修饰内联函数,如果这样写,inline virtual int work(); 计算机会忽略inline关键字,使它成为纯粹的虚函数
  4. 不能修饰构造函数

4. 虚函数与虚析构函数原理

  1. 虚函数原理
    首先介绍一下函数指针。指针可以指向对象,也可以指向函数,函数的本质就是一段二进制代码,写在内存中,我们可以通过指针指向这段代码的开头,那么计算机就会从开头一直往下执行。以Shape类为例:
class Shape
{
public:
    virtual double calcArea()
     {
         cout<<"calcArea"<<endl;
         return 0;
     }
protected:
	int m_iEdge;
};
class Circle:public Shape
{
public:
     Circle(double r);
private:
     double m_dR;
};

在这里插入图片描述
在Shape对象中,除了数据成员,还有一个成员——虚函数表指针vftable_ptr,它指向一个虚函数表,这个虚函数表与Shape类的定义同时出现。假设虚函数表的起始位置是0xCCFF,那么vftable_ptr的值就是0xCCFF,父类的虚函数表只有一个,通过父类实例化出来的所有的对象的虚函数表指针的值都是0xCCFF。
虚函数表中有一个函数指针calcArea_ptr,它指向calcArea函数的入口地址,假设calcArea函数的入口地址是0x3355,那么calcArea_ptr的值就是0x3355。
调用的时候先找到虚函数表指针,通过虚函数表指针找到虚函数表,再通过位置的偏移找到相应的虚函数的入口地址,找到虚函数。
在这里插入图片描述
在实例化Circle类时,Circle类中并没有定义虚函数,但却从父类Shape中继承了虚函数,因此Circle类也会产生虚函数表,跟父类的虚函数表地址不同,但虚函数calcArea的函数指针都是一样的,都是0x3355,这就保证在Circle中也能访问到父类的calcArea函数。
如果在Circle中定义了calcArea函数呢?这时候,Shape类情况是不变的,对Circle来说,情况如下图所示:
在这里插入图片描述
Circle的虚函数表和之前是一样的,但Circle已经定义了自己calcArea函数,虚函数表中指针calcArea_ptr就覆盖了原来的指针,变成了0x4B2C,而不是0x3355。如果用Shape指针指向Circle对象,那么就会通过Circle的虚函数表指针找到Circle的虚函数表,通过偏移量找到表中的Circle的虚函数的函数入口地址,执行子类的函数。

之前讲了函数的隐藏,就是子类中有和父类同名的函数,父类的函数就会被隐藏。如果子类中没有定义从父类继承的同名的虚函数,那么子类的虚函数表中就会写上父类相应的虚函数的函数入口地址;如果子类定义了这个虚函数,那么子类的虚函数表中就会覆盖父类的虚函数的函数地址,这种情况就叫函数的覆盖

  1. 虚析构函数原理

5. 纯虚函数和抽象类

像这样的成员函数 virtual int calcPerimeter()=0;纯虚函数
在这里插入图片描述
在虚函数表中,如果是一个普通的虚函数,那么虚函数表中的函数指针就是有意义的值,如果是纯虚函数,函数指针的值就是0。
纯虚函数和虚函数一样,也一定是一个类的成员函数,包含纯虚函数的类叫做抽象类,哪怕这个类只有一个纯虚函数。
抽象类无法实例化对象,抽象类的子类也可以是抽象类,抽象类的子类只有把纯虚函数全部做了实现才能实例化对象。

6. 接口类

如果在抽象类中仅含有纯虚函数,不含有别的任何东西(成员函数和成员变量),这个类就叫接口类。接口类更多的是表达一种能力或者协议,比如:

class Flyable
{
public:
	virtual void takeoff() {}
	virtual void land() {}
};
class Bird:public Flyable// isA的关系
{
public:
	...
	virtual void takeoff() {}
	virtual void land() {}
private:
	...
};
//使用
void flyMatch(Flyable* a,Flyable* b)
{
	a->takeoff();
	b->takeoff();
	a->land();
	b->land();
}

7. RTTI(运行时类型识别)

还是以上面的类为例:

class Flyable
{
public:
	virtual void takeoff() {} //起飞
	virtual void land() {} //降落
};
class Bird:public Flyable
{
public:
	void foraging() {} //觅食
	virtual void takeoff() {}
	virtual void land() {}
private:
	...
};
class Plane:public Flyable
{
public:
	void carry() {} //运输
	virtual void takeoff() {}
	virtual void land() {}
private:
	...
};
//使用
void doSomething(Flyable* obj)
{
	obj->takeoff();
	//如果是Bird,就觅食,如果是Plane,就运输,要怎么实现呢?
	obj->land();
}

这就用到RTTI,具体做法如下:

void doSomething(Flyable* obj)
{
	obj->takeoff();
	cout<<typeid(*obj).name()<<endl;//打印obj指向的实际对象类型
	if(typeid(*obj)==typeid(Bird))//将类型进行比对
	{
		Bird *bird=dynamic_cast<Bird *>(obj);//将obj转化为Bird
		bird->foraging();
	}
	obj->land();
}

dynamic_cast注意事项:

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

typeid注意事项:

  1. typeid返回一个type_info对象的引用
  2. 如果想通过基类指针获得派生类的数据类型,基类必须带有虚函数,否则只能返回定义时使用的数据类型
  3. 只能获取对象的实际类型,即使这个类含有虚函数,也只能判断当前对象是基类还是子类,不能判断当前指针是基类还是子类

type_info类定义如下:

class type_info
{
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:
	...
};

8. 异常处理

异常就是程序运行过程中出现的错误,不进行处理就会出现程序崩溃,异常处理就是对有可能发生异常的地方做出预见性的安排。如果有些异常没有预料到,就会直接抛给系统,系统会直接杀死程序,这就造成程序崩溃。
关键字:try...catch... throw
举个例子,有三个函数,f1,f2,f3,其中f2调用f1,f3调用f2,如果f1出现了异常,就会抛给f2,f2如果能处理就处理,处理不了就继续向上抛给f3,f3捕获异常如果能处理就处理,处理不了就继续向上抛,如果没有函数能处理,就抛给系统。

void f1()
{
	throw 1;
}
int main(void)
{
	try
	{ f1();}//如果f1遇到了异常,f1后面的代码将不会执行
	catch(int)//捕获,int表示抛出的异常类型是int型
	{...}//处理
	return 0;
}

try与catch是一对多的关系:

try
{ f1();}
catch(int)
{...}
catch(double)
{...}
catch(...)//如果所有的catch块都不能捕获异常,...表示可以捕获所有的异常,但并不建议直接用,建议用在最后
{...}

如果想要捕获异常具体的值怎么办?

//根据下标获取字符
char getChar(const string& aStr,const int aIndex)
{
	if(aIndex>aStr.size())
	{
		throw string("invalid index!");
	}
	return sStr[aIndex];
}
//main中
string str("hello world");
char ch;
try{
	ch=getChar(str,100);
	cout<<ch<<endl;
}
catch(string& aval){
	cout<<aval<<endl;
}

这样就可以打印出具体的错误。

C++中常见的异常:数组下标越界,除数为0,内存不足。
异常和多态的关系:
在这里插入图片描述
通过细分的子类继承接口类,这样就都可以用父类捕获。例如:

void fun1()
{
	throw new SizeErr();
}
void fun1()
{
	throw new MemoryErr();
}
//这样不管是fun1还是fun2,都以用Exception来捕获
try
{
	fun1()}
catch(Exception& e)
{
	e.xxx();
}
try
{
	fun2()}
catch(Exception& e)
{
	e.xxx();
}

整理自慕课网c++远征

猜你喜欢

转载自blog.csdn.net/weixin_43927408/article/details/88431687