第十一课
1,多继承定义
派生类只有一个基类,称为单继承.
C++支持多继承,即一个派生类可以有两个或多个基类;
2,多继承声明
class 派生类名:访问控制 基类名1,访问控制 基类名2...
{
数据成员和成员函数声明;
};
注意点:
1,类C可以根据访问控制同时继承类A和类B的成员,并添加自己的成员;
2,如果类A和类B有同名的成员,需要使用域运算符::显示调用;
3,多继承的二义性
1,如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性;
2,为了解决多继承时的命名冲突和冗余数据问题,C++提出了虚继承,使得在派生类中只保留一份间接基类的成员,使用关键字Virtual;
案例:
class A
{
protected:
int a;
};
class B:virtual public A
{
protected:
int b;
};
4,多态
1,发现问题
当子类定义了与父类中原型相同的函数,子类会覆盖父类同名成员函数(子类重定义父类同名成员);
案例:
class A
{
public:
void Voice()
{
cout<<"A V"<<endl;
}
};
class B:public A
{
public:
void Voice()
{
cout<<"B V"<<endl;
}
};
void MV(A* v)
{
v->Voice();
}
int main()
{
A *v=new A;
MV(v);//调用父类的Voice;
v=new B;
MV(v);//还是调用的父类Voice;
return 0;
}
得到问题:
父类指针无论是指向自己,还是指向子类,都是调用父类的成员函数;
我们所希望的:
1,根据父类指针指向的对象的实际类型,调用不同类的成员函数;
2,父类指针指向父类,调用父类成员函数;
3,父类指针指向子类,调用子类成员函数;
5,解决问题-多态的实现:
在父类重写的函数的前面加上virtual关键字(子类可写可不写,默认会自己写);
案例:
class A
{
public:
virtual void Voice()//声明为虚函数
{
cout << "A V" << endl;
}
};
class B :public A
{
public:
void Voice()
{
cout << "B V" << endl;
}
};
void MV(A* v)
{
v->Voice();
}
int main()
{
A* v = new A;
MV(v);//调用父类的Voice;
v = new B;
MV(v);//变成调用的子类Voice;
return 0;
}
6,多态成立的三要素:
1,要有继承;
2,要有虚函数重写;
3,要有父类指针(或引用),指向子类对象;
#### 7,案例分析多态的好处
```C++
class Shape
{
public:
Shape(int x,int y,int w,int h):_x(x),_y(y),_w(w),_h(h){}
virtual int GetArea()
{
cout << "我他喵的是形状,哪来的面积?" << endl;
return 0;
}
~Shape(){}
protected:
int _x;
int _y;
int _w;
int _h;
};
class Rect :public Shape
{
public:
//子类使用父类的构造函数 关键字:using A::A
//如果子类的构造函数和父类差不多,那么就可以使用这个语句,把父类的构造函数继承过来
using Shape::Shape;
int GetArea()
{
cout << "RECT:" << endl;
return (_w * _h);
}
};
class Circle:public Shape
{
public:
using Shape::Shape;
int GetArea()
{
cout << "CIRCLE:" << endl;
return (3.14 * _r * _r);
}
private:
int _r=8;
};
void show(Shape& s)
{
cout <<"Area:"<< s.GetArea() << endl;
}
void show(Shape* s)
{
cout << "Area:" << s->GetArea() << endl;
}
int main()
{
/*Rect r(5, 4,7,8);
cout<<r.GetArea();*/
Shape* base = nullptr;
base = new Rect(2, 3, 4, 5);
show(base);
delete base;
base = new Circle(2, 3, 4, 5);
show(base);
delete base;
//使用多态后,可以十分方便地添加功能而不影响其他部分
return 0;
}
10,虚析构函数
注意事项:
1,构造函数不能是虚函数,建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数;
2,析构函数必须是虚函数,在有继承关系和父类指针指向子类对象时,如果析构函数不是虚函数,则对象析构时,可能存在内存泄漏的问题;
类型一:
本类指针指向本类对象:
`不管析构函数是否是虚函数(即是否加virtual关键字),delete的基类和子类都会被释放;
类型二:
父类指针指向子类对象:
`若析构函数是虚函数(加上virtual),delete时基类和子类都会被释放;
`若析构函数不是虚函数(没加virtual),delete时只释放基类,不释放子类;
11,final和override
1,final可以用来修饰父类的虚函数,表示在子类中禁止对该虚函数进行重写;
2,override可以用来修饰子类的虚函数,表示是虚函数重写;
3,用在类名后面,表示无法继承该类;
12,动态联编和静态联编
联编是指一个程序模块,代码之间互相关联的过程;
静态联编:是程序的匹配,连接在编译阶段实现,也称为早期联编;
`重载函数的使用就是静态联编;
动态联编:是程序的匹配,连接在运行阶段实现,也称为迟邦定.(将函数体和函数调用关联起来,就叫绑定);
`分支语句就是动态联编,多态也是多态联编;
13,多态中的动态联编的实现
若我们声明了类中的成员函数为虚函数,那么C++编译器就会为类生成一个虚函数表,通过这个表即可实现动态联编;
14,虚函数表(虚函数指针数组)
`虚函数表是顺序存放在虚函数地址的,虚函数表是顺序表(数组),依次存放着类里面的虚函数;
`虚函数表是由编译器自动生成与维护的,相同类的不同对象的虚函数表是一样的;
`当我们在类中定义了virtual函数,C++编译器就会给对象添加一个vptr指针,这个指针存的就是虚函数表首地址;
15,vptr指针
1,使用sizeof可以得到该指针存在的事实;并且可以发现它指向的数组里存着所有虚函数的地址;
操作分析:(设置对象为p)
1,vptr指针是类的第一个成员,所以需要对对象取地址,即&p,得到vptr指针的首地址;
2,指针是四个字节,所以把&p强转为int*,可把对象分为每个元素都是四个字节的数组,然后获得第一个元素,即vptr指针( *(int *)&p)或者 *(int *)&p)[0];
3,因为vptr指针存储的是虚函数表的首地址,所以把vptr指针转成int*指针,然后就可以访问每个成员了(这里的成员都是函数指针): *((int )( * (int)&p)+i);
4,最后把得到的结果转为函数指针即可调用;
案例:
class Base//空类大小为1字节,若有对象则替换掉
{
public:
virtual void imm()
{
cout << "fjjdf" << endl;
}
virtual void oiuyiy()
{
cout << "turti" << endl;
}
};
int main()
{
cout << sizeof(Base) << endl;
Base f;
&f;//对象的地址
((int*)&f);//强转,把对象的内存空间,看成一个int类型的数组;
*((int*)&f + 0);//相当于((int*)&f)[0];//拿到了第一个指针,即vptr
typedef void (*FunType)();//函数指针类型
*((int*)&f);//把vptr里面的地址取出来,这个是虚函数表的地址
FunType fun = (FunType) * ((int*)*((int*)&f)+0);//获取第一个虚函数
FunType fun2 = (FunType) * ((int*)*((int*)&f) + 1);//获取第二个虚函数
fun();//输出fjjdf
fun2();//输出turti
return 0;
}
16.纯虚函数和抽象类
1,纯虚函数:是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本(如果不定义,那么派生类也是抽象类);
2,纯虚函数为各派生类提供一个公共界面(接口的封装和设计,软件的模块功能划分);
定义:
virtual 类型 函数名(参数列表) =0;//纯虚函数
1,抽象类:一个具有纯虚函数的基类称为抽象类;
`特点:
不能被实例化(定义对象);
//若需定义对象:需重写其他子类的成员函数;
可以定义指针或引用;