本节内容源于对C++ primer第13章的学习,这本书把C++的原理将得明明白白。网上的博客往往讲得一头雾水。到头来还不如看原书本。

问题

首先给出一题:

#include<stdio.h>
class A{
public:
~A();
};
A::~A(){
printf("deleteA");
} class B:public A{public: ~B();
B::~B(){
printf("deleteB");
} int main()
{ A * pa = new B(); delete pa;
}

问,最后程序输出的是什么?

答案是 deleteA. 为什么?

再看另一题

#include<stdio.h>
class A{
public:
virtual ~A();
};
A::~A(){
printf("deleteA");
} class B:public A{public: virtual ~B();
B::~B(){
printf("deleteB");
} int main()
{ A * pa = new B(); delete pa;
}

又会输出什么? 答案是deleteBdeleteA,这又为什么呢?

阅读以下内容。

基类指针指向派生类对象

以上两个问题都是基类指针指向了派生类对象,为什么会不一样?这涉及到派生类对象的产生过程。

首先,公有派生是派生类继承基类时候用public修饰符。

A:public B{
};

于是B就是公有基类。公有派生会使基类的公有成员成为派生类的公有成员。基类的私有成员虽然会成为派生类一部分,但是派生类无法访问,只能通过基类公有方法访问。

派生类不能直接访问基类的私有成员,必须通过基类方法进行访问。

派生类在产生对象时候,派生类的构造函数会在执行其函数体之前先将参数通过成员初始化列表将参数传递给基类构造函数,然后调用它构造出基类对象——因此基类对象在派生类对象之前便被创建,

这也是题目中基类指针可以指向派生类对象的原因。如果用基类指针,实际上指针所指向的就是派生类对象中嵌套的基类对象,所以该指针只能调用的方法和成员都是基类的。这就解释了题1中为什么会用A类的析构函数。

而为什么题目二中会先调用B的析构函数,再调用A的析构函数呢?

这里有两个原因:

1.派生类的析构函数本身就会先调用自身,在调用派生类自动生成的基类对象的析构函数,原因之前讲过了,派生类会对象会首先自动生成基类对象,这是继承的实质。

2.题目2中的虚函数使得基类指针调用的方法是派生类对象的方法,所以题目2调用了类B的析构函数。

3.综上,由于虚函数,使得析构了B对象,而B对象的析构函数执行最后还会调用基类对象的析构函数将自动生成的基类对象也释放掉。

公有继承的本质:
新的派生类对象将继承基类的对象的公有成员的实现,包括方法和变量。
派生类对象可以直接使用基类的方法(继承了基类的接口)
实际上,相当于派生类自动地实现了基类的方法和变量。
此时调用的方法,都是基类对象的方法和数据。

在C++中,如果基类指针指向派生类对象,且指针调用的函数如果不是虚函数,则该函数具体是哪一个取决于指针的类型。而如果调用的函数是虚函数,则该函数具体属于哪一个取决于被指向的对象的类型。而对象是运行时动态分配的,

所以识别其类型需要编译器进行动态绑定。

所以题目2中,A 和 B的析构函数都是虚函数, 因此指针指向类B对象,析构时调用的也是类B的析构函数。

C++的析构函数执行时,派生类会先调用自己的析构函数,再调用基类的析构函数(因为前者在栈顶,后者在栈底部)。

这里还引申出了一个问题,为什么虚函数要是析构函数? 因为如果不是的话,就如图1,析构的只是派生类中嵌套的基类对象,所以剩余的派生类对象就没有析构。这导致了内存泄漏。

ps(派生类指针最好不要指向基类对象)

如果基类和派生类都定义了相同名称的成员函数,那么经由对象指针调用成员函数时候,到底调用哪一个函数,必须根据该指针的原始类型而定。
而不是视指针所指的对象的类型而定。
基类指针指向派生类指针的时候,所调用的都是基类对象的方法和数据,因为**派生类继承基类并且创建对象后基类对象会首先被创建,并且调用基类构造函数**,而派生类构造函数会初始化新增的成员。
派生类是实际上是包含了基类对象的。所以基类指针所覆盖的大小只包含了派生类的基类部分,所以无法调用派生类对象的成员。
派生类对象的堆栈状态是, 基类构造函数首先调用,再调用派生类构造函数,这通过派生类构造函数调用基类构造函数构建嵌套基类对象完成。
所以析构的时候,首先是派生类析构,然后再析构基类。
C++使用成员初始化列表句法实现了功能: 即在基类对象在程序进入派生类构造函数之前被创建。
派生类构造函数中的实参会先通过成员初始化列表将其传到基类对象构造函数中
后者创建一个嵌套的基类对象,然后程序才会进入派生类构造函数体,并创建派生类。见图13.2 如果不使用初始化成员列表,等同于基类调用默认构造函数。
派生类对象可以使用基类方法,基类指针可以直接指向派生类对象。引用也一样。
不过这样的话,基类指针和引用只能用于调用基类方法。
公有继承是is-a关系, 即 is-a-kind-of 的关系,午餐可能包含水果,但是水果不是午餐的一种。 公有继承将public给派生类对象使用。

多态公有继承

多态,同一个方法随着上下文改变而改变。c++的多态实现,由两种机制实现:

1 在派生类中重写基类函数

2 两个函数都是用虚方法

记住: 如果没有关键字 virtual , 程序将根据指针或引用类型选择方法;如果使用了virtual则程序根据它们指向的对象选择方法。

这是由于派生类在构造的时候会先生成基类对象,派生类会通过成员初始化列表将参数传递给基类构造函数构造基类对象,然后再在派生类构造函数的函数体中构造嵌套的派生类对象。‘

并且公有继承的派生类无法直接访问基类的私有成员,而只能通过基类的公有方法,即派生类方法中可以调用基类公有方法

void B::func(){
A::func();
}

这是除了构造函数使用成员初始化列表访问基类私有成员的另一种做法。

此外注意, B::func函数体中的func使用了作用域解析操作符,表明该函数属于A类,如果不使用作用域解析操作符,则func会被认为是B类的。

但如果派生类没有重写基类的函数,例如:func2,该函数只在基类中有而派生类中没有,那么不写作用域解析操作符也没关系,在C++中,重写并不一定表示覆盖。

常见面试题——析构函数为什么是虚函数

一般情况下,如果是直接使用派生类对象,则它在析构的时候没有这个问题,但是如果使用基类指针指向派生类对象,则通过基类指针调用的析构函数只能是基类的析构函数,所以派生类对象的析构函数就没有调用,这就造成了内存的泄露。

这个根本的原因是由于C++的类继承,派生类在公有继承基类的时候,还在派生类构造函数执行前先执行基类构造函数生成一个嵌套的基类对象,派生类拥有基类对象的公有成员和方法并且可通过公有方法访问基类私有成员。而派生类对象过期的时候会先析构派生类对象,再析构基类对象。

虚函数无法使用静态联编确定使用哪一个函数,而只能通过动态联编。动态联编是编译器生成在程序运行时选择正确虚函数的代码。

对非虚函数进行静态联编,基类指针指向派生类对象,且指针调用非虚方法,这时编译器根据指针类型确定调用哪个方法;而指针调用的方法是虚方法时候,根据指针指向的对象的类型,

但是对象的类型只有在运行时才能确定,在运行时根据对象类型将函数关联到某个具体对象中。

如果派生类不重新定义基类的方法以及如果类不会用作基类,则不需要动态联编。静态联编的效率更好。C++的指导原则就是,不要为不使用的特性付出代价。

如果要在派生类中重新定义基类方法,就是用虚函数,虚函数可以实现多态。

如果在基类和派生类上的析构函数都添加虚函数,则在用基类指针指向派生类的时候,

析构的时候,也是先析构派生类对象,后析构基类对象。

回到那个问题,由于程序是基类指针指向了派生类对象,所以通过该指针只能调用基类的一切方法,而无法调用派生类的。

所以指针释放内存的时候,也只是将派生类构建的基类对象释放了。造成了内存泄漏。

虚函数的实现原理——虚函数表

虚函数通过虚函数表实现,虚函数表就是指向函数地址数组的指针。

所有的虚函数都会被放到该指针数组。

基类对象包含一个指针,指向基类中所有虚函数的地址表。

基类和派生类都会有各自的隐藏指针,保存各自的虚函数表,将类中的所有虚函数按照顺序存放到指针数组里面,

派生类中的虚函数表的顺序一开始和基类一样,如果派生类中有重新定义的

虚表是属于类的,每个类只需要一个虚表即可。同一个类的所有对象都使用通过一个虚表。派生类会继承基类的虚表

每个类会将其中的虚函数组成一个虚表,就是指针数组来指向它们。

经过虚表调用虚函数的过程叫做动态绑定。

动态绑定的时候, 基类指针指向派生类对象,调用虚函数的时候,会调用派生类对象的vptr,vptr指向的虚函数会被调用,而vptr指针调用的对象就是用

虚函数表指向了虚函数,指针调用虚函数的时候,通过虚函数表找到虚函数。就像页表一样。 所有的虚函数,被这些类的虚函数表包含了。

所以通过虚函数表就能够准确地找到具体的虚函数。

为什么构造函数不能是虚函数

因为派生类不继承基类构造函数,派生类的构造函数会在执行前先调用基类构造函数生成基类对象然后再执行自己的构造函数。

虚函数是用于继承和多态的,构造函数不用于继承。

析构函数要是虚函数,因为析构函数应当用于一种特别的方案。

包含纯虚函数的类只能作为基类无法产生对象。用作抽象类。

C++ 类继承 笔记(初步)的更多相关文章

  1. 《C++ Primer Plus》第13章 类继承 笔记

    类继承通过使用已有的类(基类)定义新的来(派生类),使得能够根据需要修改编程代码.共有继承建立is-a关系,这意味着派生类对象也应该是某种基类对象.作为is-a模型的一部分,派生类继承基类的数据称源和 ...

  2. C++学习笔记(十二):类继承、虚函数、纯虚函数、抽象类和嵌套类

    类继承 在C++类继承中,一个派生类可以从一个基类派生,也可以从多个基类派生. 从一个基类派生的继承称为单继承:从多个基类派生的继承称为多继承. //单继承的定义 class B:public A { ...

  3. Android(java)学习笔记118:类继承的注意事项

    /* 继承的注意事项: A:子类只能继承父类所有非私有的成员(成员方法和成员变量) B:子类不能继承父类的构造方法,但是可以通过super(马上讲)关键字去访问父类构造方法. C:不要为了部分功能而去 ...

  4. Programming In Scala笔记-第十一章、Scala中的类继承关系

    本章主要从整体层面了解Scala中的类层级关系. 一.Scala的类层级 在Java中Object类是所有类的最终父类,其他所有类都直接或间接的继承了Object类.在Scala中所有类的最终父类为A ...

  5. c++中的类(class)-----笔记(类继承)

    1,派生类继承了基类的所有成员函数和数据成员(构造函数.析构函数和操作符重载函数外). 2,当不指明继承方式时,默认为私有继承. 3,基类的私有成员仅在基类中可见,在派生类中是不可见的.基类的私有成员 ...

  6. 《C++ Primer Plus》读书笔记之十一—类继承

    第十三章 类继承 1.类继承:扩展和修改类. 2.公有继承格式:冒号指出B类的基类是A,B是派生类. class B :public A { ... }: 3.派生类对象包含基类对象.使用公有派生,基 ...

  7. Android(java)学习笔记59:类继承的 注意事项

    1. 类继承的注意事项: /* 继承的注意事项: A:子类只能继承父类所有非私有的成员(成员方法和成员变量) B:子类不能继承父类的构造方法,但是可以通过super(马上讲)关键字去访问父类构造方法. ...

  8. PythonI/O进阶学习笔记_4.自定义序列类(序列基类继承关系/可切片对象/推导式)

    前言: 本文代码基于python3 Content: 1.python中的序列类分类 2. python序列中abc基类继承关系 3. 由list的extend等方法来看序列类的一些特定方法 4. l ...

  9. odoo开发笔记 -- 模型(类)继承的几种机制

    1. 类继承 2. 原型继承 3. 委托继承 待完善 https://www.cnblogs.com/chenshuquan/p/10523626.html

随机推荐

  1. win10画板超实用的快捷键

    win10画板超实用的快捷键链接: Windows 7 画图中的快捷键 Windows中画图的快捷键 其中有windows默认的快捷键,关于画图工具加入到快捷工具也有详细的介绍.

  2. JUC原子操作类与乐观锁CAS

    JUC原子操作类与乐观锁CAS ​ 硬件中存在并发操作的原语,从而在硬件层面提升效率.在intel的CPU中,使用cmpxchg指令.在Java发展初期,java语言是不能够利用硬件提供的这些便利来提 ...

  3. 并发编程之:JUC并发控制工具

    大家好,我是小黑,一个在互联网苟且偷生的农民工. 在上一期我们讲了Thread.join()方法和CountDownLatch,这两者都可以做到等待一个线程执行完毕之后当前线程继续执行,并且Count ...

  4. Java并发知识总结,超详细!

    首先给大家分享一个github仓库,上面放了200多本经典的计算机书籍,包括C语言.C++.Java.Python.前端.数据库.操作系统.计算机网络.数据结构和算法.机器学习.编程人生等,可以sta ...

  5. python3 spider [ urllib.request ]

    # # 导入urllib库的urlopen函数 # from urllib.request import urlopen # # 发出请求,获取html # html = urlopen(" ...

  6. Activiti 学习(三)—— Activiti 流程启动并完成

    Activiti 流程启动 流程定义部署后,就可以通过工作流管理业务流程了,也就是说前文部署的出差申请流程可以使用了.针对该流程,启动一个流程表示发起一个新的出差申请单,这就相当于 java 类与 j ...

  7. JavaScript深拷贝实现方式

    1.递归 function deepCope (obj) { // 要拷贝的数据为引用类型属性(数组或对象) if (obj && typeof obj === 'object') { ...

  8. 改Jupyter Notebook的默认工作路径?

    如何更改Jupyter Notebook的默认工作路径? 1.在cmd中输入命令使Jupyter产生配置文件:Jupyter_notebook_config.py jupyter notebook - ...

  9. PHP中命名空间是怎样的存在(一)?

    命名空间其实早在PHP5.3就已经出现了.不过大部分同学可能在各种框架的使用中才会接触到命名空间的内容,当然,现代化的开发也都离不开这些能够快速产出的框架.这次我们不从框架的角度,仅从简单的代码角度来 ...

  10. Linux系列(24) - chmod

    前言 在Unix和Linux的中,每个文件(文件夹也被看作是文件)都有三种权限:读.写.运行. 被授予权限的用户身份有三种:当前文件的拥有者,与拥有者属于同组者(同一个group),其他人 hello ...