C++学习笔记(多态)

观看慕课网《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的虚函数表示意图
父类Shape的虚函数表示意图
子类Circle的虚函数表示意图,此时Circle是没有去定义虚函数calcArea的,也即没有同名函数,所以会把父类的虚函数CalcArea的函数地址复制过来
子类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 Planepublic 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;
	}
	
	
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值