数据结构与算法读书笔记 - 008 - 类的继承和动态绑定

————————————————————

概念
1)类的用户指:
类的对象,派生类。。。

2)- 1 派生类对象包括的子对象,是以包含“成员”的形式。
包括一个基类的子对象,包含基类的成员,一个派生类的子对象,包含派生类成员。这里的成员,应该仅是非静态的数据成员,而已

2)- 2 至于所有成员,派生类会继承基类的所有 -
数据,函数,静态,类型。。。

3)- 1 因为派生类对象中有一部分是基类,所以基类的指针可以指向派生类对象,基类对象的引用也可以指向派生类对象。
无论是一个变量的名称,还是指针,还是引用,都是指向一个内存地址,并指出那块内存有多大小。那么,C++中必然有一个机制,是在派生类的基类子对象的某个地方指明了另外的派生类子对象的位置和大小。
(没错,如Primer所说,存在继承关系的类是一个重要的例外。。。)
(但是基类不能(隐式地)转换为派生类 - 因为这个基类对象不一定在派生类对象当中(p534),即使一个基类指针绑定的是派生类,也不能将其(隐式地)转换为派生类对象 - 想象一个这样的场景:一个基类指针,被给了一个派生类对象的地址(发生了隐式的类型转换,合法),再将这个基类指针,赋值给一个派生类的指针 - 不合法,因为基类不能向派生类转换。。。

3)- 2 派生类对象本身转换为基类对象 -
这里的对象转换,需要转换成为的类类型,后面跟着一个小括号包裹着被转化的对象:

base base_boject = base(derived_object);

这中转换实际上是将一个对象传入一个函数,如构造函数。而有些构造函数,如移动构造函数和引用构造函数是使用引用形参的。那么给这些引用形参赋值的时候就可以发生类型转换,将传入的派生类对象绑定在基类类型的引用上。
但是这个构造函数也只使用了派生类对象的基类部分。派生类部分被切掉了。这就是所谓的派生类对象到基类对象的转换,呵呵。。。

4)用派生类的构造函数也来构造基类子对象。
感觉就像基类子对象本就是派生类的一个数据成员一样。不过需要基类本身定义了传入值的构造函数才能在构造列表中用小括号往基类子对象中传值,否则。。。呃应该就不写它了吧,毕竟对待需要默认初始化的数据成员就是这样的。。。

5)派生类的作用域嵌套在基类当中 -
看起来是反的,不过“域”的概念就导致了如此,越是接触的范围比较广的,越是在中间,在内层。。。

6)尽量使用类的接口 -
即使是派生类使用基类也是如此。
(我在写派生类时候就直接用了基类的私有成员而且用了*=,已经把那个私有成员改掉了。。。)

7)C++是允许仅声明一个类的名称的。
以便后面的程序知道有这么一个名字。但是如果只声明了这个名字,也只能使用这个名字(大概是指针或引用吧)。既然没有声明其成员,自然也没法使用其成员了。。。

class base;
class derived;

8) - 1调用函数的概念,调用虚函数的概念
Primer中对调用虚函数的描写让我对调用成员函数有了更新的认识 - 我之前多少以为,或多少有这种潜意识(虽然也曾模糊地反对过这种意识和认知)调用一个成员函数,是和那个对象有关的,或至少和那个类有关的 - 现在看来,和那个类的关系更多一些,或说调用一个函数,是只和那个类的作用域有关的。调用成员函数并不像看起来那样,是包含在一个类里的 - 在编译器的眼中,对象的作用只是告诉它是那个类,并非像如在我眼中,那个成员函数有一层对象或类的外衣。在编译器眼中那只不过是一个赤裸裸的函数,罢了。。。
默认实参由静态类型决定 -> 感觉这也是一个容易出错的点。。。
*只要是虚函数,就会发生虚调用(先这样说,我也不是很确定。。。)
类型转换发生在给指针赋值。虚调用发生在用这个指针调用函数的时候。并非有继承关系时发生虚调用,而是函数是虚函数的时候发生虚调用
那想一想这个过程 - 1)只有派生类可以向基类来转化。2)使用基类指针来调用函数。- 这样就形成了一个系统,即虚调用的用法。。。
非虚函数由指针的静态类型来检查 - 静态类型吗再一遍就是声明这个指针时候的类型。如果一个函数是非虚函数,那么编译器会检查这个静态类型中有无这个函数。。。
对于一个非虚函数来说,如果一个基类的指针绑定了派生类的对象,而派生类和基类都有一样的非虚函数,那么使用这个指针调用这个非虚函数,调用的是静态类型-基类的函数,亦是试了一下知道的

	class base_f
	{
	public:
	   	base_f(const string &strInput):str(strInput){}
	    string reStr(){return str;}
	private:
   	 string str;
	};

	class derived_f : public base_f
	{
	public:
 	   derived_f(const string &strForB, const string &strForD):base_f(strForB), str(strForD){}
 	   string reStr(){return str;}
	private:
	    string str;
	};

	base_f bfObj("this is base_f");
    derived_f dfObj("this is the base part of derived_d", "this is the derived part of derived_d");
    base_f *pf = &dfObj;
    cout << pf->reStr() << endl;

运行结果:
this is the base part of derived_d

从名词上来说,应该是这样:
1)如果绑定指针到基类或派生类,这种应该叫动态绑定(在知道有基类派生类的区别的时候)
2)如果用这个指针调用函数,则叫虚调用。。。不过怎么叫其实无所谓。。。

8) - 2 纯虚函数和抽象基类
1)用 某函数的声明 = 0 的形式声明纯虚函数
2)有纯虚函数的基类,或者直接继承了基类的纯虚函数未加覆盖的派生了,是为抽象基类 - 不能创建此种类的对象。。。
(有意思的是,抽象基类不能拥有自己的独立的对象,但是它的对象却作为子对象被包含在所有派生类的对象当中。。。)

9)类的编写者的自我规范和避错 -> 虚函数的override和final
同样的例子是将移后源或者delete的指针赋值为nullptr。使用delete的指针的后果十分可怕,很可能一个程序运行几年都无法察觉的错误。。。

10)访问权限:
1)派生类对直接基类的访问权限,与基类中的访问说明符有关 -> 无论派生访问说明符是什么,派生类成员友元,都只能访问直接基类的public和protect,不能访问private
2)派生访问说明符控制派生类的用户对派生类的基类的public成员的访问权限(先确定到基类的public,基类的protect成员书中没有提。。。)。规则大概就是,对于派生类的派生类的成员,友元,用户来说,访问权限 = 派生类的基类的public成员的public权限 + 派生类的派生访问说明符的权限。有点像&&运算。。。
*派生类的成员(友元)和对象(使用某个成员的对象)要区别对待!天壤之别。。。
*使用using改变访问权限 - 所以说,这些成员一直都存在,但通常不会在派生类中写出来。不过using只不过是声明一下(其用户对其基类部分的成员的)权限(不知道是什么具体的原理),并非将其放到了派生类里。。。
函数也用using声明,如果不想手动覆盖:“对派生类没有重新定义的重载版本实际上是对using声明点的访问”

实验证明,派生类对基类的访问权限也包含基类对它自己的基类子对象的访问权限 -
(因为如果在定义一个派生类的时候,如果直接使用一个来自基类的成员,在创建派生类对象的时候他就会自动意味着在使用基类子对象的成员了吧,我猜)

class base_d
{
private:
    int a = 20;
};

class derived_d : public base_d
{
public:
    //int reInt(){return a;}
    //'a' is a private member of 'base_d'
};

在编译时进行类型查找,简而言之 - 从静态类型(那个指针声明时的类型)的那个class开始查找(嗯关键字是查找)

访问权限的确会影响合成的函数的合成。。。

11)将析构函数定义为虚函数的原因:
如果基类的析构函数不是虚函数,delete一个指向派生类的基类指针会产生未定义的行为。
如果析构函数不是虚函数,delete指向派生类的基类指针,将会运行静态类型的,即基类,的析构函数 - 先执行函数体,在销毁成员。我想这里的未定义行为更指的是会留下派生类的子对象 - 根据c++底层的特性猜测。。。
(定义了析构函数 - 而基类和派生类必须要定义析构函数 - 的类,即使析构函数是=default的,是没有合成的移动操作的。。。)
那么不使用delete,即不用来动态分配的类,岂不是就不用定义虚析构函数了?呃。。。

关于删除的成员和合成的成员:1)定义成了=default的是显示地使用合成的函数,即使用了default,它亦是合成的。。。
2)析构函数,在概念上包含了销毁成员的部分,这一部分是隐式的那一部分。。。
3)编译器不会合成一个删除掉的操作,即使是显式地定义为=default也不行。。。(所以是先判断是不是删除的,然后再判断要不要合成。如果没有删除,也不一定去合成。。。删除了肯定没有,没删除也不一定有。。。)
4)=delete删除的构造函数被认为是“定义过的”
5)=default也被认为是定义过了,那么既然存在,派生类中也许就可以继续定义了(当然派生类还是要手动写一下,因为)
感觉这里比真正的拷贝控制那里说的还要清楚一些。。。其实我还是没有完全记住,只是又过了一遍大概的原理,如果以后需要再次理解,查阅Primer->p554
6)在派生类中,要注意无论基类有没有定义虚的析构函数,派生类总会又一个析构函数的,如果自己不定义,编译器也会定义一个。如果基类中的析构函数是虚函数,那么派生类中的析构函数,即使是合成的,而且编译器会去合成的,合成出来也变成了虚函数。
7)如果析构函数是合成出来的,而且也没定义拷贝控制成员,那么编译器还会定义移动控制成员。。。
8)析构函数(基本上)不能是删除的(如果删除了,就没法释放资源。。。)
9)合成的拷贝控制成员可能是删除的(就是比如有个类的成员的拷贝控制成员是删除的)

有关这一话题的页数:p450(合成的和删除的析构和拷贝控制成员),p476(合成的和删除的移动,拷贝控制成员),p552+(派生类中的成和删除的析构函数,拷贝和移动控制成员)

12)销毁
自我感觉对基类和派生类“只负责销毁自己的成员”有了多一些的理解。在于理解了,基类和派生类的数据成员是分开来的。只有虚函数才存在继承关系。虚函数的动态调用,大概是基类和派生类唯一比较有用的地方了。。。

13)静态类型,动态类型,类型转化
这里的概念在书中介绍的貌似不是特别清晰,这里是我理解的:
1)这个类型指的是指针或引用的类型,不是对象类型
2)静态类型指指针或引用声明时候的类型,动态类型则指指针地址真正指向的对象的类型
3)对象之间不存在类型转换(这个书中说的很冰白)
4)所以类型转换指的是指针或引用的类型(这句话同1))
5)只有派生类可以转化为基类(仍然指指针或引用)
6)这个类型转换是真真正正地转换了 - 不是取出基类的那部分看它的地址,不是这样。。。1)如果是对派生类对象取地址,取出来的地址虽然是一个右值,但其类型仍是派生类类型指针,把它赋给基类类型指针,隐式的但实实在在地发成了类型转换 - 由派生类指针转成基类指针 2)如果是make_shared函数,生成派生类也是返回派生类指针。如果把它push_back给一个基类vector,那么这个类型转换应该是由vector进行的,或者使用容器过程中的某些机制,但仍转换成了基类指针。3)我猜想如果是直接把一个派生类指针左值赋值给一个基类类型的指针,应该也是发生了类型转换的,进行了拷贝,但类型转换了。。。

————————————————————

注意事项:
1) 派生类必须在其内部对所有重新定义的虚函数进行声明。

2)很有可能,基类定义了虚函数,派生类一定要去覆盖。按书中所说:一种是基类希望派生类覆盖的函数。。。对于前者,基类通常将其定义为虚函数(virtual)。对的,不一定要去覆盖。。。

带“必须”的是,派生类必须将其继承来的成员中需要覆盖的那些重新声明 - 大概即意味着,所有虚函数都会被继承,但是如果不覆盖,就还是原来的用法,只有覆盖了,才是新的用法(有个问题在于 - 派生类如果没有访问基类的private成员的权限,但继承而来但没有覆盖的函数需要使用基类的private成员,那基类子对象到底让不让派生类使用呢?)

	class base
	{
	private:
   	 virtual int privMem(){return 20;}
	};

	class derived : private base
	{
	
	};

	class derived_b : public base
	{
	    
	};

	derived d_a;
    //cout << d_a.priMem() << endl;
    //No member named 'priMem' in 'derived'
    
    derived_b d_b;
    //cout << d_b.privMem() << endl;
    //'privMem' is a private member of 'base'

根据 概念10)访问权限, 派生访问说明符对派生类的成员和友元对基类的访问权限没什么影响。
不过,上面的例子中,基类的成员是private,这样一来,谁都没法使用它了。。。

这个概念在上面说的有些模棱两可:“但是如果不覆盖,就还是原来的用法”。。。原来的用法,呵呵 - 让这个概念更确定一些:p530: 派生类经常(但不总是)覆盖它继承的虚函数。如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其它的普通成员,派生类会直接继承其在基类中的版本 -
也就是说,如果不覆盖,派生类中仍会有一个和基类中虚函数一样的成员,这个成员是虚函数,名字返回类型和形参和基类中的这个虚函数完全一样。。。

3)virtual关键字,只声明,不外部
只在基类中声明一次就可以了。可以在派生类中声明但不强制。因为如果声明为virtual一次,永远都是virtual。。。->
基类某函数virtual,派生类自动virtual。
(可不可以在派生类中声明新的虚函数呢?)

  1. 每个虚函数都必须提供定义
    通常情况下不使用的函数不需要定义 - 相比“每个虚函数都需要定义”,这个对我来说是个更新的概念。因为我之前也不知道函数如果不使用就不需要定义。。。但是在实践中可行么?
    啊,的确,我写了个只有声明的成员函数,的确不需要定义,之后创建了base_c的对象代码也可以正常编译运行:
class base_c
{
public:
    int wontUse();
private:
    int a = 0;
};


6)如果派生类中定义了与基类中同名的对象,也许派生类中的对象会覆盖掉基类中的名字吧。。。
会的。(p548)
而且名字查找先于类型检查。。。

————————————————————
参考 / 读书笔记读的书:
————————————————————

C++ Primer(第五版)ISBN 978-7-121-15535-2
数据结构,算法与应用:C++语言描述(第二版)ISBN 978-7-111-49600-7

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值