1.下面的例子代码,就说明了标题:
2.从内存的角度介绍is a:
如下图所示:当基类含有两个数据成员m_strName和m_iAge时,不管是公有私有还是保护类型的,都会被子类继承过来,同时子类应该还有他自身的数据成员,m_strCode和m_ISalary,当我们用子类的对象给基类的对象赋值或者初始化基类的对象时,它的本质就是将从父类继承来的数据成员的值赋给父类的对象,而此时子类对象中的其他数据将会被截断,也就是丢失。
如果用父类指针指向子类对象,那么父类指针也只能访问到父类所拥有的数据成员和成员函数,而无法访问到子类所独有的数据成员和成员函数。
基于上述内存结构,有一个问题!!!如下图所示:
当用基类指针指向子类从堆中分配的对象时,如下形式 A*p = new B;当调用delete p;p=NULL;销毁对象时,是调用父类A的析构函数还是调用子类B的构造函数呢?如下图所示:答案是会调用父类的构造函数,这样问题就来了,子类不是从父类继承来的那些独有的成员变量的内存将得不到释放,将会造成内存泄露,这种情况应该如何避免内存泄露
呢?这就引入了一个新的知识点:虚析构函数。
虚析构函数就是在父类的析构函数前加上virtual关键字,这种特性将会被继承下去,也即子类的析构函数也为虚析构函数,在下面的例子中做如下改变:
virtual ~Person(){}//将父类的析构函数变为虚析构函数
virtual ~Soldier(){}//子类的析构函数继承了这种特性,也变成了虚析构函数,即便子类不写virtual,子类构造函数也是虚析构函数
虚析构函数的使用场合:当存在继承关系,用父类的指针指向从堆中分配的子类的对象时,然后又想用父类的指针去释放掉内存,就会用到虚析构函数,用了虚析构函数后,再调用delete Person时,就会先调用子类的析构函数,再调用父类的构造函数了,如下图所示:
3.从函数参数的角度分析is a 的关系
第一个函数,参数是对象父类对象,我们的实参可以是父类的对象,也可以是子类的对象,打印出的结果显示,实例化子类时,会先调用父类的构造函数,再调用自己的构造函数。
因为参数是父类Person的对象我们再调用test1给它传参数的时候,就会临时的产生一个对象p,用它来调用play函数,并且在函数执行完毕以后,p这个临时对象就会被销毁,因为调用了两次test1,这就是为什么打印出的结果中会调用两次父类Person的析构函数。但是注意的是实参会给临时对象p赋值,即发生了对象之间的赋值,所以会调用Person的拷贝构造函数如下图所示:
如果同样的参数调用test2(Person &p),调用代码如下:test2(p);test2(s);打印出的结果如下图所示,可见并没有调用析构函数,这是因为用引用作为餐宿的时候,实参传进来的时候,只是相当于给实参起了一个别名p,并没有产生临时的对象,所以函数执行完的时候也不用析构临时对象,就不会调用~Person()了;
如果调用test3(Person* p),调用代码如下:test3(&p);test3(&s);打印出的结果和调用test2(Person& p)一样,也没有调用析构函数,原因是:用指针作为参数时,只是让指针去指向实参的地址,所以也没有产生临时对象,所以函数执行完后也不会调用析构函数。
因为test2()和test3()用引用和指针这两种形参形式都不会产生临时变量,不用去调用拷贝构造函数和析构函数,所以执行效率更高。
补充小知识点:你区分的了吗?
前提:class B:public A
B b;
A a = b;//用子类B的对象初始化父类A的对象
A a1;
a1 = b;//用子类B的对象赋值给父类A的对象
A*p = &b;//用父类A的指针指向子类B的对象b
A&a2 = b;//用子类B的对象初始化父类A的引用