本节内容源于对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. ❤️用武侠小说的形式来阅读LinkedList的源码,绝了!

    一.LinkedList 的剖白 大家好,我是 LinkedList,和 ArrayList 是同门师兄弟,但我俩练的内功却完全不同.师兄练的是动态数组,我练的是链表. 问大家一个问题,知道我为什么要 ...

  2. vue 监听父子组件传参,对象数据变化

    watch:{ 组件传参的字段 :{ handler (newV, oldV){ 这里打印 newV, oldV 就可以看到数据变化了 } , immediate: true, // 重点 deep: ...

  3. Python之smtplib模块

    工作中难免会出现自动发送电子邮件的需求,比如说做完自动化测试之后通过电子邮件的形式将结果反馈出来.Python中提供了标准库smtplib来解决这一问题,该模块定义了一个smtp客户端会话对象,能够将 ...

  4. Linux复习笔记-001-进程的管理

    1.什么是进程? 进程是已经启动的可执行的程序运行实例. 程序是二进制文件,静态 ./bin/date/ /usr/sbin/ 进程:是程序运行的过程 2.Linux为1的进程? centos5或6为 ...

  5. inet_aton和inet_ntoa

    3.1 inet_aton() int inet_aton(const char *cp, struct in_addr *inp); 参数说明: cp : IPv4点分十进制字符串,例如" ...

  6. ES6:使用解构赋值仅用一行定义多个相同的数组,且指向堆不同(解构赋值)

    在开发过程中我们经常要用到一些临时变量对数据进行一些特殊处理,由于良好的编码习惯要在临时变量用完后释放内存,所以当临时变量数量较多时,整体代码会变得冗余. let a = [] let b = [] ...

  7. [ Skill ] Cadence Skill 语言入门

    https://www.cnblogs.com/yeungchie/ 写个大笔记,低速更新中 ... Cadence Skill Cadence 提供二次开发的 SKILL 语言,它是一种基于通用人工 ...

  8. xxl-job <=2.0.2 反序列化漏洞

    xxl-job <=2.0.2 反序列化漏洞 搭建 https://github.com/xuxueli/xxl-job/releases/tag/2.0.2 下载源码,导入idea,mysql ...

  9. POJ——3278 Catch That Cow(BFS队列)

    相比于POJ2251的三维BFS,这道题做法思路完全相同且过程更加简单,也不需要用结构体,check只要判断vis和左右边界的越界情况就OK. 记得清空队列,其他没什么好说的. #include< ...

  10. Hello Wolrd

    这是一篇测试文章.....后续会更新一些文章.