C++远征之多态篇
面向对象三大特征:封装,继承,多态
多态: 发出一条命令时,不同的对象接收到同样的命令做出的动作不同
多态篇会学习到的目录:
- 普通虚函数 & 虚析构函数
- 纯虚函数:抽象类 & 接口类
- RTTI (运行时类型识别)
- 异常处理
- 概念区分: 隐藏 & 覆盖 | 早绑定与晚绑定
- 虚函数表(核心部分)
多态的内容很多,概念也听起来有点变态
不过这也是最精彩的部分了。
c++ 虚函数
什么是多态?
多态是指相同对象收到不同消息或不同对象收到相同消息时产生不同的动作
静态多态 & 动态多态
- 静态多态(早绑定)
- 动态多态(晚绑定)
静态多态:相同对象收到不同消息
例子:
矩形类有两个同名的计算面积的函数,参数不同,这是两个互为重载的函数。
class Rect
{
public:
int calcArea(int width);
int calcArea(int width,int height);//互为重载
}
int main(void)
{
Rect rect;
rect.calcArea(10);
rect.calcArea(10,20);
return 0;
}
当我们传入一个参数,两个参数会调用两个不同的同名函数。
计算机在编译阶段就会自动根据参数使用不同的参数来确定使用哪个函数。
这里程序在运行之前也就是编译阶段,就决定了运行哪个函数。很早的就决定运行哪个了,这种情况就叫做早绑定,或静态多态。
动态多态(晚绑定): 不同对象收到相同消息
圆形和矩形都有自己的计算面积的方法,两种方法肯定是不同的。
这就是对不同的对象下达相同的指令,却做着不同的操作。
动态多态前提:必须以封装(数据封装到类中)与继承(继承关系)为基础。
动态多态,起码有两个类,一个是子类,一个是父类。使用三个类时表现的更为明显。
代码例子:
Shape类
class Shape
{
public:
double calcArea()
{
cout << "calcArea" <<endl;
return 0;
}
}
继承Shape的Circle类
class Circle:public Shape
{
public:
Circle(double r);
double calcArea();
private:
double m_dR;
}
//Circle计算面积实现
double Circle::calcArea()
{
return 3.14 * m_dR * m_dR;
}
//矩形
class Rect::public Shape
{
public:
Rect(double width,double height);
double calcArea();
private:
double m_dWidth;
double m_dHeight;
}
//矩形计算面积实现
double Rect::calcArea()
{
return m_dWidth * m_dHeight;
}
main函数中的使用:
int main()
{
Shape *shape1 = new Circle(4.0);
Shape *shape2 = new Rect(3.0,5.0);
shape1 -> calcArea();
shape2 -> calcArea();
return 0;
}
上述代码的效果将不是我们预期的多态,会调用两次父类的clacArea();
使用父类指针指向子类对象,子类与父类有同名函数,加virtual成为虚函数,则调用相同的函数名的时候调用的是子类的函数。 不添加的时候,使用父类指针指向的是父类自身的calc。
使用virtual关键字使得成员函数变成虚函数。
virtual 虚函数
class Shape
{
public:
virtual double calcArea() //虚函数
{
cout << "calcArea" <<endl;
return 0;
}
}
在父类中将想要实现多态的函数添加virtual关键字,使其变为虚函数。
- 子类中不加也可以(系统会自动加上),不过如果考虑子类也有可能成为父类情况下也加上。
加上virtual后,父类指针指向子类对象。子类与父类有同名函数,父类指针调用到的是子类方法。
如果是用父类指针,不加virtual关键字的话就会调用父类。 如果是用子类指针,则调用子类,因为父类继承过来的同名函数造成了隐藏,只能通过.Father::
访问
虚函数代码示例
附录代码 2-2-VirtualFunction:
Shape.h
#ifndef SHAPE_H
#define SHAPE_H
#include <iostream>
using namespace std;
class Shape
{
public:
Shape();
~Shape();
double calcArea();
};
#endif
Shape.cpp
#include "Shape.h"
Shape::Shape()
{
cout << "Shape()" << endl;
}
Shape::~Shape()
{
cout << "~Shape()" << endl;
}
double Shape::calcArea()
{
cout << "Shape - > calcArea()" << endl;
return 0;
}
Circle.h
#ifndef CIRCLE_H
#define CIRCLE_H
#include "Shape.h"
class Circle:public Shape
{
public:
Circle(double r);
~Circle();
double calcArea(); // 同名且参数返回值一致
protected:
double m_dR;
};
#endif
Circle.cpp
#include "Circle.h"
Circle::Circle(double r)
{
cout << "Circle()" << endl;
m_dR = r;
}
Circle::~Circle()
{
cout << "~Circle()" << endl;
}
double Circle::calcArea()
{
cout << "Circle-->calcArea()" << endl;
return 3.14 * m_dR * m_dR;
}
Rect.h
#ifndef RECT_H
#define RECT_H
#include "Shape.h"
class Rect : public Shape
{
public:
Rect(double width,double height);
~Rect();
double calcArea();
protected:
double m_dwidth;
double m_dHeight;
};
#endif // RECT_H
Rect.cpp
#include "Rect.h"
Rect::Rect(double m_dwidth, double m_dHeight)
{
cout << "Rect()" << endl;
this->m_dHeight = m_dHeight;
this->m_dwidth = m_dwidth;
}
Rect::~Rect()
{
cout << "~Rect()" << endl;
}
double Rect::calcArea()
{
cout << "Rect::calcArea()"<< endl;
return m_dwidth * m_dHeight;
}
main.cpp
#include <iostream>
#include "Circle.h"
#include "Rect.h"
#include <stdlib.h>
using namespace std;
int main()
{
// 定义两个父类指针,指向子类对象
Shape *shape1 = new Circle(3.0);
Shape *shape2 = new Rect(3.0, 4.0);
shape1->calcArea();
shape2->calcArea();
//当基类不添加virtual时。打印两遍基类的。
delete shape1;
shape1 = NULL;
delete shape2;
shape2 = NULL;
system("pause");
return 0;
}
上述代码问题1: 销毁父类指针是否可以连带销毁子类对象。
问题2: 使用指向子类对象的父类指针是否能直接调用到子类方法。
可以看到问题1,只销毁了父类对象,子类对象没有被一并销毁。问题2,父类指针调用的是父类中的方法,子类方法并没有被调用到。
在给shape的clacArea函数加上virtual后,父类指针可直接调用到子类的成员函数。
Shape.h中的析构函数,与子类重名的函数都加上virtual
class Shape
{
public:
Shape();
virtual ~Shape();
virtual double calcArea();
};
推荐为子类也加上virtual。这里是两个同级(儿子级)对象实例化,所以实例化出两个爸爸是正常的。 如果是一个儿子有两个爸爸,两个爸爸有同名数据成员,应该虚继承(即用子类自己的,两爸爸的无法抉择,谁的就都不要了)。
虚析构函数解决内存泄露
动态多态的内存泄漏
class Shape
{
public:
Shape();
virtual double calcArea();
}
与之前的相比多定义了一个指针数据成员,圆心坐标。
class Circle: public Shape
{
public:
Circle(int x,int y,double r);
~Circle();
virtual double calcArea();
private:
double m_dR;
Coordinate *m_pCenter;//圆心坐标
}
构造函数中实例化Coordinate,析构函数中释放掉。
Circle::Circle(int x,int y,double r)
{
m_pCenter = new Coordinate(x,y);
m_dR = r;
}
Circle::~Circle()
{
delete m_pCenter;
m_pCenter = NULL;
}
上述代码我们可以实现堆中内存的释放。
但是在多态中:
int main(void)
{
Shape *shape1 = new Circle(3,5,4.0)
shape1 -> calcArea();
delete shape1;
shape1 = NULL;
return 0;
}
delete后面跟着父类的指针。只会执行父类的析构函数。那么就无法执行Circle的析构函数,就会造成内存泄露。
之前虽然没有执行子类的析构函数,但是因为子类没有new 申请内存,所以没有泄露。
上述代码在Shape的析构函数未添加virtual时将只会释放Shape对象。而实例化出的Circle对象不会被销毁。
父类指向的子类对象,会先执行子类的析构函数,然后执行父类析构函数。
virtual -> 析构函数
只需要在基类的析构函数添加virtual。父类指针可以一起销毁掉子类的对象。
virtual在函数中的使用限制
- 普通函数不能是虚函数。 (必须是类的成员函数,不能是全局函数)
- 静态成员函数不能是虚函数。如下面代码。
class Animal
{
public:
virtual static int getCount()//因为被修饰过的静态函数是类的,不属于任何一个对象。
}
- 内联函数不能是虚函数
会忽略inline。使他变成一个纯粹的虚函数。
class Animal
{
public:
inline virtual int eat()
{
}
}
- 构造函数不能修饰虚函数
虚析构函数使用方法代码示例
2-5-VirtualDestructorFunction
Shape.h:
#ifndef SHAPE_H
#define SHAPE_H
#include <iostream>
using namespace std;
class Shape
{
public:
Shape();
virtual ~Shape();
virtual double calcArea();
};
#endif
Shape.cpp
#include "Shape.h"
Shape::Shape()
{
cout << "Shape()" << endl;
}
Shape::~Shape()
{
cout << "~Shape()" << endl;
}
double Shape::calcArea()
{
cout << "Shape - > calcArea()" << endl;
return 0;
}
Rect.h:
#ifndef RECT_H
#define RECT_H
#include "Shape.h"
class Rect : public Shape
{
public:
Rect(double width,double height);
~Rect();
double calcArea();
protected:
double m_dwidth;
double m_dHeight;
};
#endif // RECT_H
Rect.cpp:
#include "Rect.h"
Rect::Rect(double m_dwidth, double m_dHeight)
{
cout << "Rect()" << endl;
this->m_dHeight = m_dHeight;
this->m_dwidth = m_dwidth;
}
Rect::~Rect()
{
cout << "~Rect()" << endl;
}
double Rect::calcArea()
{
cout << "Rect::calcArea()"<< endl;
return m_dwidth * m_dHeight;
}
Circle.h 添加坐标类数据成员指针:
#ifndef CIRCLE_H
#define CIRCLE_H
#include "Shape.h"
#include "Coordinate.h"
class Circle:public Shape
{
public:
Circle(double r);
~Circle();
double calcArea();
protected:
double m_dR;
Coordinate *m_pCenter;
};
#endif
Circle.cpp 实例化坐标对象,析构中释放:
#include "Circle.h"
Circle::Circle(double r)
{
cout << "Circle()" << endl;
m_dR = r;
m_pCenter = new Coordinate(3, 5);
}
Circle::~Circle()
{
cout << "~Circle()" << endl;
delete m_pCenter;
m_pCenter = NULL;
}
double Circle::calcArea()
{
cout << "Circle-->calcArea()" << endl;
return 3.14 * m_dR * m_dR;
}
Coordinate.h
#ifndef COORDINATE_H
#define COORDINATE_H
#include <iostream>
using namespace std;
class Coordinate
{
public:
Coordinate(int x, int y);
~Coordinate();
private:
int m_iX;
int m_iY;
};
#endif
Coordinate.cpp
#include "Coordinate.h"
#include <iostream>
using namespace std;
Coordinate::Coordinate(int x, int y)
{
cout << "Coordinate()" << endl;
m_iX = x;
m_iY = y;
}
Coordinate::~Coordinate()
{
cout << "~Coordinate()" << endl;
}
main.cpp:
#include <iostream>
#include "Circle.h"
#include "Rect.h"
#include <stdlib.h>
using namespace std;
int main()
{
Shape *shape2 = new Rect(3.0, 4.0);
Shape *shape1 = new Circle(3.0);
shape1->calcArea();
shape2->calcArea();
//当基类不添加virtual时。打印两遍基类的。
delete shape1;
shape1 = NULL;
delete shape2;
shape2 = NULL;
system("pause");
return 0;
}
给Shape的析构函数变成一个虚析构函数。推荐大家把子类的析构函数也加上virtual。因为子类也有可能成为其他类的父类。
virtual限制情况
virtual Shape();
// 报错: “inline”是构造函数的唯一合法存储类
virtual void test() {
// 报错: error C2575: “test”: 只有成员函数和基可以是虚拟的
}
class Shape
{
public:
Shape();
virtual static void test();
//error C2216: “virtual”不能和“static”一起使用
virtual ~Shape();
virtual double calcArea();
};
inline会失效:
virtual inline void test() {
}
练习题
- 虚函数特性可以被继承,当子类中定义的函数与父类中虚函数的声明相同时,该函数也是虚函数。
- 虚函数使用virtual关键字定义,但使用virtual关键字时,并非全部是虚函数(可以是虚继承)
- 虚析构函数是为了避免使用父类指针释放子类对象时造成内存泄露。
虚函数与虚析构函数原理
如何实现虚函数和虚析构函数: 虚函数的实现原理
涉及到函数指针,介绍一下函数指针
指针指向对象 - - - 对象指针
指针指向函数 - - - 函数指针
函数的本质就是一段二进制的代码,指针指向代码的开头,然后一行一行执行到函数结尾
指针指向代码内存的首地址,函数入口地址。
class Shape
{
public:
virtual double calcArea() //虚函数
{
return 0;
}
protected:
int m_iEdge;
}
//子类
class Circle: public Shape
{
public:
Circle(double r);
//Circle使用的也是Shape的虚函数
private:
double m_dR;
}
当我们实例化一个Shape对象时,shape中除了数据成员m_iEdge,还有另外一个成员(虚函数表指针;也是一个指针占有四个内存单元,存放地址,指向一个虚函数表)。该表会与Shape类的定义同时出现,在计算机中虚函数表占有一定内存空间。
假设虚函数表的起始地址: 0xCCFF。那么虚函数表指针
的值就是0xCCFF。父类的虚函数表只有一个,通过父类实例化出的多个对象,他们的虚函数表指针都只有一个: 0xCCFF。
父类的虚函数表中定义了一个函数指针calcArea_ptr
-> 指向calcArea()的入口地址。
Circle中并没有定义虚函数,但是他却从父类中继承了虚函数。所以我们在实例化Circle也会产生一个虚函数表指针,它是Circle自己的虚函数表。
在Circle中计算面积的方法首地址与父类的一致,这使得在Circle中访问父类的计算面积函数也能通过虚函数表指针找到自己的虚函数表。在自己的虚函数表中找到的计算面积函数指针也是指向父类的的计算面积函数的。
如果我们在Circle中定义了计算面积的函数:
class Circle: public Shape
{
public:
Circle(double r);
virtual double calcArea();
private:
double m_dR;
}
Shape没有发生变化。
对于Circle来说则有变化:
此时Circle中关于计算面积的函数指针指向自己的计算面积方法的首地址。
当Shape指针指向Circle对象,会通过Circle中的虚函数表指针,找到Circle自己的虚函数表,指向Circle自己的计算面积函数。
函数的覆盖与隐藏
父类和子类出现同名函数,称之为函数隐藏。
- 如果没有在子类中定义同名虚函数,那么子类的虚函数表指针会写上父类函数的地址。
- 如果在子类中也定义了同名的虚函数,那么将覆盖父类的函数指针指向子类的函数。
上面这种情况称之为函数的覆盖。
虚析构函数原理。
特点:在父类中通过virtual修饰析构函数。通过父类指针指向子类对象,那么释放父类指针,可以同时释放子类对象。
理论前提:
执行完子类的析构函数就会执行父类的析构函数。
只要我们可以实现执行子类的析构函数,就可以实现一次性释放两个。
Shape.h
class Shape
{
public:
virtual double calcArea() //虚函数
{
return 0;
}
virtual ~Shape(){} //虚析构函数
protected:
int m_iEdge;
}
Circle.h
class Circle: public Shape
{
public:
Circle(double r);
virtual double calcArea();
virtual ~Circle(); //不写计算机也会自行定义。
private:
double m_dR;
}
main.cpp:
int main()
{
Shape *shape = new Circle(10.0);
delete shape;
shape = NULL;
return 0;
}
如果我们在父类中定义了虚析构函数,那么我们在父类的虚函数表中就会有一个父类的析构函数的函数指针。
那么子类的虚函数表中也会有一个函数指针,指向子类的析构函数。这个时候使用父类对象指向子类对象, 就会执行子类的析构函数,子类的执行完之后,系统会自动执行父类的析构函数。
虚析构函数与上面虚函数是同理可得的: 就是子类中有同名函数(同为析构函数), 那么这个虚函数表中指针将指向子类的函数。而因为子类析构函数执行会触发父类自动执行,所以实现了销毁父类指针,释放子类和父类的对象。
- 比如此时我们删去子类的析构函数。那么将只执行父类的析构函数。
证明虚函数表指针的存在(代码示例)
我们需要知道的一些概念:
- 对象的大小: 在类实例化的对象当中数据成员所占据的大小,不包括成员函数。
- Shape没有数据成员(理论上不占内存); Circle有一个int型的数据成员,应该占四个。
- 对象的地址:通过类实例化的对象,它占据的内存单元的首地址
- 对象成员的地址:当用一个类实例化一个对象之后,这个对象中可能与一个或多个数据成员,每一个数据成员所占据的地址就是这个对象的成员地址;对象的数据成员由于数据类型不同,占据的内存大小也不同,地址也是不同的。
- 虚函数表指针:在具有虚函数的情况下实例化对象时,这个对象的第一个内存存储的是一个指针,即虚函数表的指针,占四个内存单元,因此我们可以通过计算对象的大小来证明指针的存在。
2-8-VirtualTablePointer
Shape.h
#ifndef SHAPE_H
#define SHAPE_H
#include <iostream>
using namespace std;
class Shape
{
public:
Shape();
~Shape();
double calcArea();
//virtual ~Shape();
//virtual double calcArea();
};
#endif
Shape.cpp
#include "Shape.h"
Shape::Shape()
{
//cout << "Shape()" << endl;
}
Shape::~Shape()
{
//cout << "~Shape()" << endl;
}
double Shape::calcArea()
{
cout << "Shape - > calcArea()" << endl;
return 0;
}
Circle.h
#ifndef CIRCLE_H
#define CIRCLE_H
#include "Shape.h"
class Circle:public Shape
{
public:
Circle(int r);
~Circle();
protected:
int m_iR;
};
#endif
Circle.cpp
#include "Circle.h"
Circle::Circle(int r)
{
m_iR = r;
}
Circle::~Circle()
{
}
main.cpp:
#include <iostream>
#include "Circle.h"
#include <stdlib.h>
using namespace std;
int main()
{
Shape shape;
cout << sizeof(shape) << endl;
// Shape对象没有任何的数据成员。理论应该为0.
Circle circle(100);
cout << sizeof(circle) << endl;
// Circle 有一个int数据成员 理论为4.
system("pause");
return 0;
}
运行结果:
4是因为int占四个字节。如何解释1?
- 在没有一个数据成员的情况下,C++会给该对象1个内存单元来标记此对象的存在,
- 而对于含有内存单元的对象来说,他的大小则是该数据成员的大小
main.cpp:
#include <iostream>
#include "Circle.h"
#include <stdlib.h>
using namespace std;
int main()
{
Shape shape;
int *p = (int *)&shape;
// 强制类型转换
cout << p << endl;
Circle circle(100);
int *q = (int *)&circle;
cout << q << endl;
// Shape和Circle对象在内存中地址不同。
cout << (unsigned int)(*q) << endl;
// 打印出里面数据成员的值
system("pause");
return 0;
}
运行结果:
虚函数示例2
2-9-VirtualFunction2
Shape.h (其中calcArea被加上了virtual关键字)
class Shape
{
public:
Shape();
~Shape();
// virtual ~Shape();
virtual double calcArea();
};
此时实例化Shape对象就应该有一个虚函数表指针了,对象大小从1会变成5。
main.cpp
int main()
{
Shape shape;
cout << sizeof(shape) << endl;
Circle circle(100);
cout << sizeof(circle) << endl;
//打印出里面存着的值
system("pause");
return 0;
}
运行结果:
当我们使Shape拥有一个虚函数时。
- 指向Shape的指针会指向一个虚拟函数表。因为是个指针,所以占四个内存空间。
Circle因为继承自Shape也会拥有一个虚函数表,加上自己原本的数据成员,对象大小为8。
Shape.h改为如下:
class Shape
{
public:
Shape();
double calcArea();
virtual ~Shape(); // 虚析构函数也有虚函数表。
};
运行结果:
虚函数表指针位于内存中的前四个单元。
int main()
{
Shape shape;
int *p = (int *)&shape;
cout << (unsigned int)(*p) << endl;
// 虚函数表地址
Circle circle(100);
int *q = (int *)&circle;
cout << (unsigned int)(*q) << endl;
// 打印出的还是虚函数表地址
system("pause");
return 0;
}
运行结果:
Circle中前四个内存单元是虚函数表指针地址。后四个是数据成员100。
int *q = (int *)&circle;
q++;
cout << (unsigned int)(*q) << endl;
输出结果为100.说明:
- Circle指针8位。前四位是指针指向虚地址表首地址。后四位是里面存着的内容。
- 父类和子类拥有不同的属于自己的虚函数表。
练习;
- 在C++中多态的实现是通过虚函数表实现的
- 每个类只有一份虚函数表,所有该类的对象共用同一张虚函数表
- 两张虚函数表中的函数指针可能指向同一个函数。(当子类没有父类的同名函数)
巩固练习:
定义一个动物(animal)类,要求含有虚函数eat和move,并定义构造函数和虚析构函数
定义一个狗(Dog)类,要求共有继承动物类,定义构造函数和虚析构函数,并实现自己的eat和move函数
使用父类对象实例化子类,调用子类成员函数
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
/**
* 定义动物类:Animal
* 成员函数:eat()、move()
*/
class Animal
{
public:
// 构造函数
Animal(){cout << "Animal" << endl;}
// 析构函数
virtual ~Animal(){cout << "~Animal" << endl;}
// 成员函数eat()
virtual void eat(){cout << "Animal -- eat" << endl;}
// 成员函数move()
virtual void move(){cout << "Animal -- move" << endl;}
};
/**
* 定义狗类:Dog
* 此类公有继承动物类
* 成员函数:父类中的成员函数
*/
class Dog : public Animal
{
public:
// 构造函数
Dog(){cout << "Dog" << endl;}
// 析构函数
virtual ~Dog(){cout << "~Dog" << endl;}
// 成员函数eat()
virtual void eat(){cout << "Dog -- eat" << endl;}
// 成员函数move()
virtual void move(){cout << "Dog -- move" << endl;}
};
int main(void)
{
// 通过父类对象实例化狗类
Animal *a = new Dog();
// 调用成员函数
a ->eat();
a ->move();
// 释放内存
delete a;
a = NULL;
return 0;
}
运行结果:
- 因为父类使用虚析构函数。所以释放内存。
- 因为子类中有父类的同名函数,所以在子类的虚函数表中原本指向父类方法的指针被覆盖为子类自己的函数指针。
- 实现了父类对象调用子类方法。
纯虚函数 & 抽象类
例子:
class Shape
{
public:
virtual double calcArea()//虚函数
{return 0;}
virtual double calcPerimeter() = 0;//纯虚函数
}
纯虚函数:
- 没有函数体
- 直接等于0
当我们定义了一个纯虚函数,他同样会在虚函数表中出现,如图calcPerimeter ptr就是纯虚函数的指针,他的值是0(意思就是他没有指向代码区,不会实现任何方法)。他这样的目的是为了让子类在继承他的时候,再实现他的方法。
- 在虚函数表中直接写为0,
- 包含纯虚函数的类,就是抽象类。上面含有纯虚函数的shape类就是一个抽象类。
- 纯虚函数无法调用,所以抽象类无法实例化对象
class Person
{
public:
Person(string name);
virtual void work() =0;
virtual void printInfo() =0;
};
class Worker: public Person
{
public:
Worker(string name)
virtual void work() = 0;
virtual void printInfo() { cout << m_strName <<endl;}
private:
string m_strName;
};
class Dustman: public Worker
{
public:
Worker(string name)
virtual void work() {cout << "扫地"};
virtual void printInfo() { cout << m_strName <<endl;}
private:
string m_strName;
};
- 抽象类的子类也有可能是抽象类。抽象类的子类只有把抽象类当中的所有纯虚函数都做了实现,子类才可以实例化对象。
- 上面代码中work只把子类的两个实现了一个。只有dustman
才能实例化对象。
抽象类代码示例
如果Worker没有实现work。则不可以实例化work。
当Worker的子类dustman实现了work。就可以实例化dustman。
代码:
3-2-AbstractClass
Person.h
#ifndef PERSON_H//假如没有定义
#define PERSON_H//定义
#include <string>
using namespace std;
class Person
{
public:
Person(string name);
virtual ~Person() {};
virtual void work() =0; // 纯虚函数
private:
string m_strName;
};
#endif //结束符
Person.cpp
#include "Person.h"
Person::Person(string name)
{
m_strName = name;
// 不实现纯虚函数
}
Worker.h
#include <string>
using namespace std;
#include "Person.h"
class Worker:public Person
{
public:
Worker(string name,int age);
//virtual void work();
virtual ~Worker() {};
private:
int m_iAge;
};
Worker.cpp
#include "Worker.h"
#include <iostream>
using namespace std;
Worker::Worker(string name,int age):Person(name)
{
m_iAge = age;
}
//void Worker::work()
//{
// cout << "work()" << endl;
//}
Dustman.h
#ifndef DUSTMAN_H
#define DUSTMAN_H
#include "Worker.h"
class Dustman :public Worker
{
public:
Dustman(string name, int age);
virtual void work();
};
#endif
Dustman.cpp
#include "Dustman.h"
#include <iostream>
using namespace std;
Dustman::Dustman(string name, int age) :Worker(name, age)
{
}
void Dustman::work() {
cout << "扫地" << endl;
}
main.cpp
#include <iostream>
#include "Person.h"
#include "Worker.h"
#include <stdlib.h>
#include "Dustman.h"
int main()
{
//Person person("张三"); // 报错:“Person”: 不能实例化抽象类
//Worker worker("zhangsan", 17); // 报错:“Worker”: 不能实例化抽象类
Dustman dustman("zhangsan", 20);
system("pause");
return 0;
}
一个抽象类之所以叫抽象类,是因为它里面有一个或以上的纯虚函数。纯虚函数的写法是:
// virtual 函数返回类型 函数名()=0;
// 纯虚函数里面不用写任何代码
virtual void work() =0; // 纯虚函数
类包含了纯虚函数就会无法实例化,抽象函数我们本身就不需要它实例化。
例如Circle继承了shape,Circle为了可以计算周长,定义了一个叫calcPerimeter的方法,因此把他父类Shape的纯虚函数calcPerimeter覆盖了,这样就可以成功实例化通过子类Circle来计算周长。
练习
- 只有函数声明没有函数定义,直接等于0的虚函数是纯虚函数。
- 含有纯虚函数的类叫做抽象类。
- 不可以使用含有纯虚函数的类实例化对象。
- 抽象类的子类也可以是抽象类。
单元练习
定义一个动物(animal)类,要求含有虚函数eat和纯虚函数move以及数据成员m_strName,并定义构造函数和虚析构函数
定义一个狗(Dog)类,要求公有继承动物类,定义构造函数和虚析构函数,并实现自己的eat和move函数
通过动物类实例化狗类,调用狗类当中的成员函数
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
/**
* 定义动物类:Animal
* 虚函数:eat()
* 纯虚函数:move()
* 数据成员:m_strName
*/
class Animal
{
public:
// 默认构造函数
Animal(){};
// 含参构造函数
Animal(string name){m_strName = name; cout << "Animal" << endl;}
// 虚析构函数
virtual ~Animal(){cout << "~Animal" << endl;}
// 虚成员函数
virtual void eat(){cout << "Animal--" << m_strName << "-- eat" << endl;}
// 纯虚函数
virtual void move() = 0;
public:
// 数据成员
string m_strName;
};
/**
* 定义狗类:Dog
* 公有继承动物类
* 虚成员函数:eat()、move()
*/
class Dog: public Animal
{
public:
// 默认构造函数
Dog(){};
// 含参构造函数
Dog(string name){m_strName = name; cout << "Dog" << endl;}
// 虚析构函数
virtual ~Dog(){cout << "~Dog" << endl;}
// 虚成员函数eat()
virtual void eat(){cout << "Dog--" << m_strName << " -- eat" << endl;}
// 虚成员函数move()
virtual void move(){cout << "Dog--" << m_strName << " -- move" << endl;}
public:
// 数据成员
string m_strName;
};
int main(void)
{
// 通过动物类实例化狗类
Animal *p = new Dog("狗类");
// 调用成员函数
p ->eat();
p ->move();
// 释放内存
delete p;
p = NULL;
return 0;
}
运行结果:
因为狗实现了动物类的所有纯虚构函数,所以它可以被实例化。因为父类的eat和move都是虚函数。所以子类的纯虚函数表覆盖了父类的方法。
因为是虚析构函数所以父类指针销毁。子类的也一起没了。
如果不是分.h和.cpp文件写,记得在默认构造函数加上{}
接口类
- 含有纯虚函数的类叫抽象类;仅含有纯虚函数的类叫接口类
- 接口类没有数据成员,只有成员函数,仅有的成员函数还全部都是纯虚函数
class Shape
{
public:
virtual double calcArea() = 0;//计算面积
virtual double calcPerimeter() = 0;//计算周长
}
上述Shape类是一个接口类。
接口类表达的是一种能力或协议:
class Flyable
{
public:
virtual void takeoff() = 0;//起飞
virtual void land() = 0; //降落
}
飞行能力要实现起飞降落。
class Bird:public Flyable
{
public:
virtual void takeoff(){}
virtual void land(){}
private:
//...
}
鸟要实例化就得实现起飞和降落这两个函数。
如果我们在使用的时候有这样一个函数,函数需要传入的指针是能飞的。鸟继承自父类flyable,is-a关系。
飞行比赛
class flyMatch(Flyable *a,Flyable *b)
{
//...
a->takeoff();
b->takeoff();
a->land;
b->land;
}
如果你要参加飞行比赛就得会飞,你要会飞就得实现这两个函数。相当于一种协议。
同样道理射击能力
class CanShot
{
public:
virtual void aim() = 0;//瞄准
virtual void reload() =0;//装弹
}
飞机多继承飞行能力和射击之后,变成战斗机。
它要实例化就得实现下面四个函数。
class Plane:public Flyable,public CanShot
{
virtual void takeoff(){}
virtual void land(){}
virtual void aim(){}
virtual void reload(){}
}
//传入两个plane,plane is a canshot
void fight(CanShot *a,CanShot *b)
{
a -> aim();
b -> aim();
a -> reload();
b -> reload();
}
复杂情况:
class Plane:public Flyable
{
//...
virtual void takeoff(){}
virtual void land(){}
}
// 要实例化飞机,就必须实现起飞和降落
class FighterJet:public Plane,public CanShot
{
virtual void aim(){}
virtual void reload(){}
}
这种继承情况下Plane不是一个接口类,而Canshot是一个接口类。
void airBattle(FighterJet *a,FighterJet *b)
{
//调用flyable中约定的函数
//调用canshot中约定的函数
}
接口类代码示例
3-6-InterfaceClass
Flyable.h
#ifndef FLYABLE_H
#define FLYABLE_H
class Flyable
{
public:
virtual void takeoff() = 0;//起飞
virtual void land() = 0; //降落
};
#endif
Plane.h
#ifndef PLANE_H
#define PLANE_H
#include "Flyable.h"
#include <string>
using namespace std;
class Plane :public Flyable
{
public:
Plane(string code);
virtual void takeoff();
virtual void land();
void printCode();
private:
string m_strCode;
};
#endif
Plane.cpp
#include "Plane.h"
#include <iostream>
using namespace std;
Plane::Plane(string code)
{
m_strCode = code;
}
void Plane::takeoff()
{
cout << "plane - takeoff" << endl;
}
void Plane::land()
{
cout << "plane - land" << endl;
}
void Plane::printCode()
{
cout << m_strCode << endl;
}
FighterPlane.h
#ifndef FIGHTERPLANE_H
#define FIGHTERPLANE_H
#include "Plane.h"
class FighterPlane:public Plane
{
public:
FighterPlane(string code);
virtual void takeoff();
//因为plane已经实现过了,所以它可实现也可也不
virtual void land();
};
#endif
FighterPlane.cpp
#include <iostream>
#include "FighterPlane.h"
using namespace std;
FighterPlane::FighterPlane(string code) :Plane(code)
{
}
void FighterPlane::takeoff()
{
cout << "FighterPlane -- takeoff" <<endl;
}
void FighterPlane::land()
{
cout << "FighterPlane -- land" << endl;
}
main.cpp:
#include <iostream>
using namespace std;
#include <stdlib.h>
#include "FighterPlane.h"
void flyMatch(Flyable *f1,Flyable *f2)
{
f1->takeoff();
f1->land();
f2->takeoff();
f2->land();
}
int main(void)
{
Plane p1("001");
Plane p2("002");
p1.printCode();
p2.printCode();
flyMatch(&p1,&p2);
system("pause");
return 0;
}
看出飞机可以作为参数传入flymatch;这限制了传入参数的对象类型,可以在函数体中调用接口类的方法。
int main(void)
{
FighterPlane p1("001");
FighterPlane p2("002");
p1.printCode();
p2.printCode();
flyMatch(&p1,&p2);
system("pause");
return 0;
}
可以看到继承飞机的战斗机也是可以参加飞行比赛的,因为它也有飞行能力。
改变代码如下:
让战斗机继承flyable和plane。飞机不再继承flyable。
class FighterPlane :public Plane,public Flyable
{};
class Plane //并把飞机中的纯虚函数 声明 & 定义 去掉
此时它就既可以当做flyable传入,也可以当做plane传入。
#include <iostream>
using namespace std;
#include <stdlib.h>
#include "FighterPlane.h"
void flyMatch(Plane *f1,Plane *f2)
{
f1->printCode();
f2->printCode();
}
int main(void)
{
FighterPlane p1("001");
FighterPlane p2("002");
flyMatch(&p1,&p2);
system("pause");
return 0;
}
同时继承两个,既可以当做flyable传入,也可以当做plane传入。
- 接口类中仅有纯虚函数,不能含有其他函数,也不能含有数据成员。
- 可以使用接口类指针指向其子类对象,并调用子类对象中实现的接口类中的纯虚函数。
- 一个类可以继承一个接口类,也可以继承多个接口类。
- 一个类继承接口类的同时也可以继承非接口类。
巩固练习
定义一个能够射击(CanShut)类,要求含有纯虚函数aim和reload
定义一个枪(Gun)类,继承CanShut类,并实现函数aim和reload。
定义函数Hunting(CanShut *s)
,调用s指向对象的函数。
在函数中传入Gun的对象,查看结果
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;
/**
* 定义射击类:CanShut
* 定义纯虚函数:aim、reload
*/
class CanShut
{
public:
virtual void aim() =0;
virtual void reload() =0;
};
/**
* 定义枪类:Gun
* 公有继承射击类
* 实现成员函数:aim、reload
*/
class Gun : public CanShut
{
public:
virtual void aim()
{
cout << "Gun -- aim" << endl;
}
virtual void reload()
{
cout << "Gun -- reload" << endl;
}
};
/**
* 定义含参函数射击:hunting
* 调用参数的aim与reload函数
*/
void hunting(CanShut *s)
{
s->aim();
s->reload();
}
int main(void)
{
// 实例化枪对象
CanShut *p = new Gun();
// 调用含参函数hunting,将对象枪传入函数中
hunting(p);//因为已经是个指针。所以直接传入指针本身。
//如果是对象。那要加上&取地址符
// 释放内存
delete p;
p = NULL;
return 0;
}
输出:
RTTI (运行时类型识别)
Run-Time Type Identification
介绍知识点: typeid
dynamic_cast
例子:
class Flyable
{
public:
virtual void takeoff() = 0;//起飞
virtual void land() = 0; //降落
};
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(){}
};
使用时:
void doSomething(Flyable *obj)
{
obj ->takeoff();
//如果是bird,则觅食
//如果是plane,则运输
obj -> land();
}
如果对指针能进行判断,然后根据传入指针不同调用不同方法。实现小鸟觅食,plane运输。
void doSomething(Flyable *obj)
{
obj ->takeoff();
cout << typeid(*obj).name() <<endl; // 打印出对象类型
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 -> carry();
}
obj -> land();
}
总结:
dynamic_cast注意事项:
- 只能应用于指针和引用的转换
- 要转换的类型中必须包含虚函数
- 转换成功返回子类的地址,失败返回NULL
typeid注意事项:
-
type_id
返回一个type_info
对象的引用 - 如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数
- 只能获取对象的实际类型。(不能判断当前指针是基类还是子类)
name()
& 运算符重载等号,使得我们可以直接用==
进行比对
RTTI代码示例
4-2-RTTICode
Flyable.h
#ifndef FLYABLE_H
#define FLYABLE_H
class Flyable
{
public:
virtual void takeoff() = 0;//起飞
virtual void land() = 0; //降落
};
#endif
Plane.h
#ifndef PLANE_H
#define PLANE_H
#include <string>
#include "Flyable.h"
using namespace std;
class Plane :public Flyable
{
public:
void carry();
virtual void takeoff();
virtual void land();
};
#endif
Plane.cpp
#include <iostream>
#include "Plane.h"
using namespace std;
void Plane::carry()
{
cout << "Plane::carry()" << endl;
}
void Plane::takeoff()
{
cout << "Plane::takeoff()" << endl;
}
void Plane::land()
{
cout << "Plane::land()" << endl;
}
Bird.h
#ifndef BIRD_H
#define BIRD_H
#include "Flyable.h"
#include <string>
using namespace std;
class Bird :public Flyable
{
public:
void foraging();
virtual void takeoff();
virtual void land();
};
#endif // !BIRD_H
Bird.cpp
#include <iostream>
#include "Bird.h"
using namespace std;
void Bird::foraging()
{
cout << "Bird::foraging()" << endl;
}
void Bird::takeoff()
{
cout << " Bird::takeoff()" << endl;
}
void Bird::land()
{
cout << " Bird::land()" << endl;
}
main.cpp:
#include <iostream>
#include "Bird.h"
#include "Plane.h"
using namespace std;
#include <stdlib.h>
void doSomething(Flyable *obj)
{
cout << typeid(*obj).name() << endl;
obj->takeoff();
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->carry();
}
obj->land();
}
int main()
{
Bird b;
doSomething(&b);
system("pause");
return 0;
}
运行结果:
- 传入的是bird,所以执行了bird分支下的觅食。
- 当传入是plane时,则会执行carry。
int main()
{
Plane p;
doSomething(&p);
system("pause");
return 0;
}
代码说明typeid, dynamic_cast的注意事项
int main()
{
int i =0;
cout << typeid(i).name() << endl;
}
输出为int,打印出数据类型。可以看到数据类型,基本数据类型的也可以查看到。
int main()
{
Flyable *p = new Bird();
cout << typeid(p).name() << endl;
cout << typeid(*p).name() << endl;
system("pause");
return 0;
}
可以看到直接对p进行typeid,打印出的是指针的类型。*p
则是p指向的对象的类型。
看dynamic_cast的使用限制:
将Flyable.h的两个纯虚函数改为普通的。
#ifndef FLYABLE_H
#define FLYABLE_H
class Flyable
{
public:
void takeoff(){}//起飞
void land() {}//降落
};
#endif
通过大括号来实现。
将Bird.h中两个虚函数去掉。
#ifndef BIRD_H
#define BIRD_H
#include "Flyable.h"
#include <string>
using namespace std;
class Bird :public Flyable
{
public:
void foraging();
void takeoff();
void land();
};
#endif // !BIRD_H
Bird和flyable此时变成普通的继承。
int main()
{
Flyable *p = new Bird();
Bird *b = dynamic_cast<Bird *>p;
// 会报错: “dynamic_cast”:“Flyable”不是多态类型
system("pause");
return 0;
}
对于dynamic_cast的使用,要求转换类型还是被转类型都要有虚函数。
int main()
{
Flyable p;
Bird b = dynamic_cast<Bird>p;
//“dynamic_cast”:“Flyable”不是多态类型
system("pause");
return 0;
只能应用于指针和引用的转换,且必须转换的两个类中含有虚函数。
练习题
- 继承关系不是RTTI的充分条件,只是必要条件,所以存在继承关系的类不一定可以用RTTI技术
- RTTI的含义是运行时类型识别
- RTTI技术可以通过父类指针识别其所指向对象的真实数据类型
- 运行时类型别必须建立在虚函数的基础上,否则无需RTTI技术
巩固练习
定义一个能够移动(Movable)类,要求含有纯虚函数move
定义一个公交车(Bus)类,继承Movable类,并实现函数move,定义函数carry
定义一个坦克(Tank)类,继承Movable类,并实现函数move,定义函数shot。
定义函数doSomething(Movable *obj)
,根据s指向对象的类型调用相应的函数。
实例化公交车类和坦克类,将对象传入到doSomething函数中,调用相应函数
#include <iostream>
#include <stdlib.h>
#include <string>
#include <typeinfo>
using namespace std;
/**
* 定义移动类:Movable
* 纯虚函数:move
*/
class Movable
{
public:
virtual void move() = 0;
};
/**
* 定义公交车类:Bus
* 公有继承移动类
* 特有方法carry
*/
class Bus : public Movable
{
public:
virtual void move()
{
cout << "Bus -- move" << endl;
}
void carry()
{
cout << "Bus -- carry" << endl;
}
};
/**
* 定义坦克类:Tank
* 公有继承移动类
* 特有方法fire
*/
class Tank :public Movable
{
public:
virtual void move()
{
cout << "Tank -- move" << endl;
}
void fire()
{
cout << "Tank -- fire" << endl;
}
};
/**
* 定义函数doSomething含参数
* 使用dynamic_cast转换类型
*/
void doSomething(Movable *obj)
{
obj->move();
if(typeid(*obj) == typeid(Bus))
{
Bus *bus = dynamic_cast<Bus *>(obj);
bus->carry();
}
if(typeid(*obj) == typeid(Tank))
{
Tank *tank = dynamic_cast<Tank *>(obj);
tank->fire();
}
}
int main(void)
{
Bus b;
Tank t;
doSomething(&b);
doSomething(&t);
return 0;
}
运行结果:
异常处理
异常:程序运行期出现的错误。
异常处理:对有可能发生异常的地方做预见性的安排
如常见提示: 网线,内存不足。
异常处理的关键字:
try...catch...
尝试运行正常的逻辑,捕获之后进行处理。
throw抛出异常
思想:
主逻辑(try)与异常处理逻辑(catch)分离
三个函数f1,f2,f3。用f2调用f1,f3调用f2。
当f1出现异常会往上抛,如果f2可以处理就可以处理完成,
如果不能处理,会继续进行异常的传播直到f3捕获并处理。
如果没人处理就会抛给系统处理。
void fun1()
{
throw 1; // 抛出数字1
}
int main(){
try {
fun1(); // 如果正常运行,catch里的不会被执行
}catch(int) //throw的是1,所以用int类型捕获
{
//.....
}
return 0;
}
try{
fun1();
}
catch(int)
{}
catch(double)
{}
catch(...) //括号里三个点,捕获所有的异常
{}
一个try可以有多个catch,不同异常做不同处理。
- 抛出值 & 捕获数据类型。
下面我们来做捕获值:
char getChar(const string& aStr,const int aIndex)
{
if (aIndex > aStr.size())
{
throw string("ivalid index!");
}
return aStr[aIndex]; //根据字符串和下标拿到对应下标位置的字符
}
string str("hello world");
char ch;
try{
ch = getChar(str,100); //这句抛异常,下句不会运行
cout << ch << endl;
}catch(string& aval){
cout << aval << endl;
}
常见的异常:
- 数组下标越界
- 除数为0
- 内存不足
异常处理与多态的关系:
定义一个接口exception,多个子类来继承该类; 那么我们可以通过父类对象捕获不同子类对象的异常。
void fun1()
{
throw new SizeErr();
}
void fun2()
{
throw new MemoryErr();
}
try{
fun1();
}catch(Exception &e)
{
e.xxx();
}
try{
fun2();
}catch(Exception &e)
{
e.xxx();
}
通过父类的引用,调用相应的子类处理函数。
异常处理代码示例
5-2-ErrorDeal
Exception.h
#ifndef EXCEPTION_H
#define EXCEPTION_H
class Exception
{
public:
virtual void printException();
virtual ~Exception() {}
};
#endif
Exception.cpp
#include "Exception.h"
#include <iostream>
using namespace std;
void Exception::printException()
{
cout << " Exception::printException()" << endl;
}
IndexException.h
#ifndef INDEX_EXCEPTION_H
#define INDEX_EXCEPTION_H
#include "Exception.h"
class IndexException:public Exception
{
public:
virtual void printException();
};
#endif
IndexException.cpp
#include "IndexException.h"
#include <iostream>
using namespace std;
void IndexException::printException()
{
cout << "提示:下标越界" << endl;
}
main.cpp
#include <iostream>
#include <stdlib.h>
#include "IndexException.h"
using namespace std;
void test()
{
throw 0.1;
}
int main(void)
{
try
{
test();
}
catch (double)
{
cout << "exception" << endl;
}
system("pause");
return 0;
}
throw 1.0, double类型捕获。
catch (double &e)
{
cout << e << endl;
}
可以打印出抛出来的异常值:如0.1
main.cpp
#include <iostream>
#include <stdlib.h>
#include "IndexException.h"
using namespace std;
void test()
{
throw IndexException();
}
int main(void)
{
try
{
test();
}
catch (IndexException &e)
{
e.printException();
}
system("pause");
return 0;
}
运行结果:
可以看到成功的捕获到了下标越界异常。
int main(void)
{
try
{
test();
}
catch (Exception &e)
{
e.printException();
}
system("pause");
return 0;
}
依然打印出数组的提示,父类的引用可以使用到子类的处理函数。
int main(void)
{
try
{
test();
}
catch (...)
{
cout << "error" << endl;
}
system("pause");
return 0;
}
通过(...)可以捕获到所有异常。
练习题
- 在C++中异常处理通常使用
try...catch...
语法结构。 - 一个try语句可以对应一个或多个catch语句,但不能没有catch语句
- C++中使用throw抛出异常,通过catch捕获异常
巩固练习
函数division的两个参数为dividend(被除数)和divisor(除数)
要求用户输入除数和被除数,并作为参数传递给division函数
如果除数为0,则抛出异常,并被捕获,将异常的内容显示到屏幕上
#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 = 0;
int d2 = 0;
int r = 0;
cin >> d1;
cin >> d2;
// 使用try...catch...捕获异常
try{
r = division(d1,d2);
cout << r << endl;
}catch(string &str){
cout << str <<endl;
}
return 0;
}
运行结果:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。