c++第十一天(基类与派生类的赋值问题和动态多态)

22 篇文章 2 订阅

目录

一、派生类和基类的赋值关系:

二、虚指针访问虚表中的虚函数

三、动态多态

四、纯虚函数与抽象类

五、虚析构

六、纯虚函数和overide


一、派生类和基类的赋值关系:

1、派生类可以赋值给基类;基类不可以赋值给派生类。

2、原因:派生类所占的空间比基类大。赋值运算会调用operator =()函数,赋值操作以左边对象为准,因为右边对象的成员比左边对象的成员多(派生类继承基类的信息,还有自己特有的成员),operator=()函数严格按照命令来进行赋值操作时,由于在右边对象中找不到指定的某个成员,所以会导致出错。

3、对象指针赋值

(1)基类对象可以定义一个指针指向派生类,但是派生类不可以定义指针指向基类。

Father f; 
Son s;
Father *p1 = &s;//可以 
Son *p2 = &f;//不可以 

(2)原因:派生类指针指向的空间比基类大。(可能会越界访问) 所以当基类的指针操作派生类的对象时,由于基类指针会像操作基类那样操作派生类对象,而基类对象所占用的内存空间通常会小于派生类对象,所以基类指针不会超出派生类对象去操作数据。反之派生类指针指向基类对象,那么就会把一部分不属于基类对象的内存也包括进来操作,于是在使用该指针进行操作时,常常会删除或修改了基类对象之外的数据,产生一些不易察觉而且后果很严重的错误。所以派生类指针是不允许指向基类对象的。

 (3)基类的指针只能指向派生类继承的基类空间(相当于指向基类本身)

 下列展示一下代码:

demo1
#include <iostream>

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 <iostream>

using namespace std;

定义一个抽象类 
class  base{
public:
    定义一个纯虚函数 
    virtual void show()=0;
};

没有覆盖基类的纯虚函数,xbase 还是一个抽象类 
class xbase : public base {
public: 
    void show_xbase(){
        cout << "show_xbase" <<endl; 
    }
};

class nbase : public base {
public:
    覆盖基类的纯虚函数 
    void show(){
       cout  << "nbase" << endl; 
    }
    void show_xbase(){
        cout << "show_xbase" <<endl; 
    }
};


int main()
{
   定义一个抽象类的对象 
   base tmp;  错误的!无法定义对象 

   xbase t;  不可以的!因为派生类也继承了基类的,纯虚函数!  

   nbase q;  覆盖纯虚函数后,该类就不是抽象类 

}

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 释放的时候调用共有的虚表函数,一起调用析构函数释放。

六、纯虚函数和overide

描述:override保留字表示当前函数重写了基类的虚函数。

虚函数:关键字“virtual”,在基类中声明,且在基类中有函数体;

纯虚函数:在虚函数后面加上“=0”,纯虚函数在基类中没有函数体;

抽象类:包含纯虚函数的类。

子类继承并对纯虚函数进行实现(子类重写基类虚函数),使用保留字“override”。 

基类的纯虚函数:
    virtual void update(const int& num) = 0;

子类重写基类虚函数:
    virtual void update(const int& num) override;   //并在子类中对该函数进行实现。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值