目录
一、派生类和基类的赋值关系:
二、虚指针访问虚表中的虚函数
三、动态多态
四、纯虚函数与抽象类
五、虚析构
六、纯虚函数和overide
1、派生类可以赋值给基类;基类不可以赋值给派生类。
2、原因:派生类所占的空间比基类大。赋值运算会调用operator =()函数,赋值操作以左边对象为准,因为右边对象的成员比左边对象的成员多(派生类继承基类的信息,还有自己特有的成员),operator=()函数严格按照命令来进行赋值操作时,由于在右边对象中找不到指定的某个成员,所以会导致出错。
3、对象指针赋值
(1)基类对象可以定义一个指针指向派生类,但是派生类不可以定义指针指向基类。
Father f;
Son s;
Father *p1 = &s;//可以
Son *p2 = &f;//不可以
(2)原因:派生类指针指向的空间比基类大。(可能会越界访问) 所以当基类的指针操作派生类的对象时,由于基类指针会像操作基类那样操作派生类对象,而基类对象所占用的内存空间通常会小于派生类对象,所以基类指针不会超出派生类对象去操作数据。反之派生类指针指向基类对象,那么就会把一部分不属于基类对象的内存也包括进来操作,于是在使用该指针进行操作时,常常会删除或修改了基类对象之外的数据,产生一些不易察觉而且后果很严重的错误。所以派生类指针是不允许指向基类对象的。
(3)基类的指针只能指向派生类继承的基类空间(相当于指向基类本身)。
下列展示一下代码:
demo1
#include
using namespace std;
基类
class base
{
public:
base(int d) : data(d) {}
int data;
};
//派生类
class xbase : public base
{
public:
xbase(int xd, int d) : base(d)
{
this->xdata = xd;
}
int xdata;
};
int main()
{
定一个基类
base a(100);
派生类
xbase b(123, 233);
cout << a.data << endl;
cout << b.data << endl;
cout << b.xdata << endl;
把派生类赋值给 基类
a = b;
cout << a.data << endl; 输出的是 233 ,因为233是派生类传给基类的,派生类覆盖基类后,基类再调用就就是原来传递的值
// b=a; 基类无法 覆盖 派生
base *p = &b; //基类指向派生类
只能是基类在左边,而且基类指针指向的也是指向继承过来那个区域本身的地址
p->data = 200;
使用基类对派生类作引用 ,为什么类型不需要匹配?? 因为引用是一个指针常量 int *const p,本质是一个常量
base &q = b;
// int *const p1; 有问题,常量指针必须定义的时候初始化
int const *p2; 常量指针定义的时候不用初始化
// xbase *bq = &a;
// xbase &bbq = a;
}
demo2
这个指针的引用比较难看懂,需要指针厉害才行
int main()
{
cout << sizeof(base) << endl;
base tmp;
cout << &tmp << endl;
cout << &tmp.a << endl; tmp 和 tmp.a 的地址是一样的
cout << &tmp.b << endl;
获取虚表的地址
long long *v_addr = (long long *)(*((long long *)&tmp)); 得到首地址 -> 拿该地址上 8 个空间的数据!
cout << v_addr << endl;
利用虚表地址访问虚函数
typedef void (*Fun)(void); 重命名函数指针类型
Fun pfun; 定义一个函数指针 pfun
保存虚函数的地址
pfun = (Fun)*v_addr; 强制类型转换为该函数指针类型
调用虚函数
pfun();
pfun = (Fun) * (v_addr + 1);
pfun();
pfun = (Fun) * (v_addr + 2);
pfun();
}
1、虚表指针,永远都是储存在对象的首地址上的。
2、所有的虚函数,都储存在系统的虚表中。
3、子类会把父类的虚表指针也继承下来,子类与父类共用一个虚表(实现覆盖)。
4、虚表指针(virtual)
(1)虚函数虚表指针永远是储存在对象是首地址中。(它和数据成员的顺序无关) .
(2)virtual ,含有虚方法,系统就会创建一个 *vptr 指针,指向系统中的虚表。
(3)所有的虚函数,都存储在系统的虚表中。
(4)virtual void show(){} //虚函数
(5)子类会把父类的虚表指针也继承下来,子类与父类共用虚表(实现覆盖)。
1、多态:
优点:提高代码的复用性!
概念:一个函数作用于不同的参数,所得到的结果不一样。
静态多态 : 程序在编译阶段已经 确定将要执行的状态。 (函数重载,运算符重载,模板)
动态多态 :程序在运行阶段才能确定将要执行的状态。 (动态绑定)
2、动态多态使用前提:
(1)基类要有虚函数 。
(2)派生类重写基类的虚函数 (实现覆盖)。
(3)通过基类的指针或引用指向派生类的对象,并调用重写后的接口。 (类外函数调用)
缺一不可。
3、什么时候使用动态多态?
答:想要一个函数实现不同的功能时,就采用动态多态的设计方式。(重载只是同名函数具有不同参数,动态多态是同一个函数具有同样的参数)。
格式:virtual 函数返回类型 函数名(参数表)= 0; //纯虚函数 virtual void show( ) = 0;
1、当一个类中含有一个纯虚函数,那么这个类就是抽象类!
2、抽象类不能定义对象。(不能实例化,子类也不能)。
3、当一个函数覆盖纯虚函数,这个类就不是抽象类了。
#include
using namespace std;
定义一个抽象类
class base{
public:
定义一个纯虚函数
virtual void show()=0;
};
没有覆盖基类的纯虚函数,xbase 还是一个抽象类
class xbase : public base {
public:
void show_xbase(){
cout << "show_xbase" <
4、抽象类的作用是说明这个类要实现动态多态 ( 预留接口 ) 。
作用:把所有的析构函数放入虚表,使派生类的析构函数也能执行。
1、基类不能用指针为子类释放空间,无法调用子类析构函数,需要用虚析构才能释放。
2、语法: virtual ~基类名 ( ) { } //虚析构函数
3、要求:在基类的析构函数前面加上virtual,或者在基类和子类的析构函数前面都加上virtual,不能只在子类前面加。
class base {
public:
base() {
cout << "base构造函数" << endl;
}
virtual ~base(){
cout << "~base析构函数" << endl;
}
};
class xbase : public base {
public:
xbase(){
cout << "xbase构造函数" << endl;
p = new int;
cout << "分配堆空间" << p << endl;
}
~xbase(){
cout << "~xbase析构函数" << endl;
cout << "p -> 释放堆空间"<< p << endl;
delete p;
}
private:
int *p;
};
利用基类调用构造函数和析构函数,但不能调用子类的析构函数,需要在基类的析构函数前面加上virtual方法。
void test(base *p){
delete p; 删除基类指针后就不能触发派生类的析构函数了,因为它是调用(指向)派生类空间的。
}
int main()
{
test(new xbase);
}
4、关键点: 在delete删除基类指针 p 后就不能触发派生类的析构函数了,因为它是调用派生类空间的。指针 p 只和基类的析构函数静态绑定,没有和子类的析构函数绑定。在 delete 了 p 之后就只调用了基类的析构函数。
追加:虚析构动态绑定的实现条件,(和动态多态的条件一样)。
(1) 通过指针调用函数。
(2)虚函数。
(3)必须是向上转型(即基类指针指向子类对象)。
问:构造函数在什么时候执行呢?
答:构造函数是在创建对象的时执行的。
5、虚析构的本质:
利用virtual关键字让基类和派生类的析构函数一起放入虚表中(继承下来的,派生类可以不声明virtual),在调用 p 释放的时候调用共有的虚表函数,一起调用析构函数释放。
描述:override保留字表示当前函数重写了基类的虚函数。
虚函数:关键字“virtual”,在基类中声明,且在基类中有函数体;
纯虚函数:在虚函数后面加上“=0”,纯虚函数在基类中没有函数体;
抽象类:包含纯虚函数的类。
子类继承并对纯虚函数进行实现(子类重写基类虚函数),使用保留字“override”。
基类的纯虚函数:
virtual void update(const int& num) = 0;
子类重写基类虚函数:
virtual void update(const int& num) override; //并在子类中对该函数进行实现。