Essential C++学习记录&笔记整理36(运用继承体系,基类应该多么抽象,初始化、析构、复制)

运用继承体系

  • 这一节没什么要说的,有一点就是基类和其派生类构成一个两层的继承体系。

基类应该多么抽象

每个派生类的共同操作应该再抽离出来给基类类定义里。

  • 之前咱们设计的抽象基类提供的仅仅只有接口(一堆纯虚函数,和等被派生类继承的基类成员函数的声明/定义),并未有任何实现。
  • 但这样的缺点还是有的,如果要加入一个新类,这种设计就让加入新类变得麻烦起来。
  • 所以把派生类的共有实现内容剥离出来扔到基类里。举个例子:
    基类旧代码:
class num_sequence {
public:
	typedef vector<unsigned int>::iterator iterator;

    virtual ~num_sequence(){};
	virtual num_sequence *clone() const = 0;
			 
	virtual unsigned int elem( int pos ) const = 0;
	virtual bool         is_elem(  unsigned int ) const=0;
	virtual int          pos_elem( unsigned int ) const=0;

    virtual const char*  what_am_i() const = 0;
	static  int          max_elems(){ return _max_elems; }
	virtual ostream&     print( ostream &os = cout ) const = 0;
	
	virtual bool operator ==( const num_sequence& ) const=0;
    virtual bool operator !=( const num_sequence& ) const=0;
	
	//想要让工作num_sequence operator+( const num_sequence& )函数,
	//则需要移除所有的纯虚函数。
    // virtual num_sequence operator+( const num_sequence& ) const=0;

	virtual iterator begin() const = 0;
	virtual iterator end() const = 0;

	virtual int length()  const = 0;
    virtual int beg_pos() const = 0; 

	virtual void set_position( int pos ) = 0;
	virtual void set_length( int pos ) = 0;

	virtual	const vector<unsigned int>* sequence() const=0;
  
protected:
	// static const int    _max_elems = 1024;	
	//编译器不支持const static类型(已测试确实如此,codeblocks17.12)
	enum { _max_elems = 1024 };

    virtual void gen_elems( int pos ) const = 0;
	bool check_integrity( int pos, int size ) const; 
};

派生类(以Fibonacci类为例)旧代码:

class Fibonacci: public num_sequence{
public:
	Fibonnacci(int len=1,int beg_pos=1):_length(len),_beg_pos(beg_pos){}
	//派生类F类的成员逐一初始化的类构造函数(提供参数默认值)
	virtual int elem(int pos)const;//虚函数elem的声明,为了让基类指针能
	//调用派生类的elem,所以把elem()在基类和派生类都设置成虚函数让基类指针能调用派生类同名函数elem。
	//基类指针调用派生类其他的同名函数也是如此做法。
	virtual const char* what_am_i()const{return "Fibonacci";}
	//这里直接给出了what_am_i()虚函数的定义
	virtual ostream& print(ostream &os=cout)const;
	int length()const{return _length;}
	int beg_pos() const {return _beg_pos;}
	//这两个成员函数则是派生类Fibonacci专属的成员
	//声明在public:里的成员函数都可以在派生类外(包括基类)还可以被调用,这个派生类的派生类还可以继承这个成员函数。
protected:
	virtual void gen_elems(int pos)const;
	//这个成员函数声明在protected里,那么这个派生类的派生类还可以继承这个成员函数而派生类外(不包括基类和他的派生类)
	//是不可调用这个成员函数的。
	int _length;
	int _beg_pos;
	static vector<int> _elems;
	//这三个成员是派生类专属保护成员

我现在①把派生类的_length和_beg_pos这两个保护(protected)成员扔到基类的保护成员里把访问这俩公共成员的函数也都扔到基类的公共(public)成员里,如下,基类的新定义就产生了:

class num_sequence{
public:
	virtual ~num_sequence(){}//基类析构函数定义,派生类会用到基类子对象,所以这个基类虚函数析构函数必须有,必须跟着。
	virtual const char* what_am_i() const =0;
	//需要派生类自己去定义这个纯虚函数(到了派生类这个成员函数就是虚函数了)
	int elem(int pos)const;
	ostream& print(ostream &os=cout)const;
	int length()const{return _length;}
	int beg_pos()const{return _beg_pos;}
	//把访问这俩在派生类的公共成员的函数也都扔到基类的公共(public)成员里,且不成为虚函数
	//让派生类去继承这两个公共成员函数
	static int max_elems(){return 64;}//数列(vector存储形式)认为规定允许存多少个元素
protected:
	virtual void gen_elems(int pos)const =0;
	//需要派生类自己去定义这个纯虚函数(到了派生类这个成员函数就是虚函数了)
	bool check_integrity(int pos,int size)const;
	num_sequence(int len,int bp,vector<int>&re):_length(len),_beg_pos(bp),_relems(re){}
	//基类的构造函数定义。放在了这里,着实有点奇怪,且听我慢慢分析(在下面)
	int _length;
	int _beg_pos;
	vector<int> &_relems;
};

现在,每个派生类就编写自己独特的部分就行了。
派生类(以Fibonnacci为例)新定义:

class Fibonacci: public num_sequence{//定义派生类的开头的写法,因为派生类是num_sequence基类的,
//所以要这么写哦。
public:
	Fibonacci(int len=1,int beg_pos=1;//派生类类构造函数声明·1
	virtual const char* what_am_i()const{return "Fibonacci";}
	//对吧,定义派生类自己的what_am_i()函数,程序运行时解析这虚成员函数看让基类指针指向哪个派生类的
	//what_am_i()函数。
protected:
	virtual void gen_elems(int pos)const;
	//同样的道理,声明派生类自己的gen_elements()函数,程序运行时解析这虚成员函数看让基类指针指向哪个派生类的
	//gen_elements()函数。
	static vector<int> _elems;
	//静态类成员。因为是静态类成员所以很方便增删查改这个类成员。

指针和引用的复习

  • 引用**无法代表空对象**,而指针可以,给指针赋0就行。
  • 引用不必检查是否为null(或说0),而指针你必须检查是否为null(或说0)
  • 引用类型的数据成员必须在类构造函数的成员初始化列表(就那个类构造函数形参表后的:的后面那一堆数据成员列表)中加以初始化。而指针类型的数据成员不用这样做。你可以先把指针类型数据成员初始化为null,然后再让它指向某个有效的内存地址。或者直接像引用一样在类构造函数的成员初始化列表/定义里初始化就行。
  • 自己决定用引用还是指针。

初始化、析构、复制

  • 如果基类有了实际的数据成员,则我们必须给基类数据成员初始化。
    怎么办捏?就是给基类类定义里提供构造函数,利用这个构造函数初始化基类所声明的所有数据成员。
  • 然而抽象基类我们无法为他定义类对象(因为抽象基类里存在着纯虚函数,纯虚函数没有函数定义导致该基类的纯虚函数所在接口不完整,也就无法为该抽象基类定义任何类对象)

关于派生类对象的子对象

先说明一下:基类子对象是派生类对象的子对象,派生类对象的子对象,派生类对象的子对象,和派生类子对象是两码事,别tm搞混了。

  • 派生类对象初始化操作,需要调用其基类的构造函数,再调用自己的构造函数
  • 所以,你可以这么认为,哎,我派生类对象里还含有子对象的嘛!这些子对象都是谁啊?
    由基类构造函数初始化的“基类子对象”(因为调用基类构造函数虽然看是派生类类对象初始化操作调用的,实际上是内藏的基类子对象调用
    由派生类构造函数初始化的“派生类子对象”
    好比咱Essential C++学习记录&笔记整理33(虚函数,protected,基类派生类构造函数析构函数调用关系)这里提到的三层继承体系的AudioBook类,对吧,这个类定义的类对象包含三个子对象,一个是基类LibMat子对象,一个是派生类Book子对象,一个是派生类AudioBook(其基类为Book类)子对象。

派生类对象的子对象的各自构造函数调用机制

  • 然而派生类的构造函数**不仅必须给派生类的数据成员初始化,还要给基类的数据成员初始化。**看个例子(一种做法):
inline Fibonacci::Fibonacci(int len,int beg_pos):num_sequence(len,beg_pos,_elems){}

这里是Fibonaaci类(派生类)构造函数定义。其中采用了成员初始化列表(成员逐一初始化)的方式,注意,派生类构造函数的成员初始化列表里调用了基类的构造函数

  • 如果不调用基类的构造函数(不写:及以后的东西({}除外)),什么后果?编译器报错呗,为嘛?你定义了派生类的构造函数,你肯定得去调用对吧(要不然你定义派生类构造函数干嘛?闲的?)。而我们上面提到的规则:

派生类对象初始化操作,需要调用其基类的构造函数,再调用自己的构造函数

注意,这个规则的前提是 基类必须有实际的数据成员
你调用了派生类构造函数给派生类(子)对象初始化,必然先调用基类的构造函数,然而你派生类构造函数定义里没体现基类构造函数的调用,编译器就不认了。所以你要手动!!!!在派生类的构造函数定义的参表后写:基类构造函数名(基类构造函数参表),然后再写{}
这也是基类要求我们明确指定调用哪一个构造函数。(上面那个例子的构造函数有三个参数)

  • 然而派生类的构造函数不仅必须给派生类的数据成员初始化,还要给基类的数据成员初始化。 的另一种做法是:给基类提供默认构造函数
    注意,此时需要把_relems改为指针(你不能把基类的protected成员的引用对象_relems绑定在某个派生类吧?别的派生类不需要vector(_relems)存它派生类自己的数列吗?)
    给出原_relems的定义,帮助你理解这个修改其称为指针的操作。
//基类定义
class num_sequence{
public:
	//...
protected:
	//...
	vector<int> &_relems;
};

不过你要注意:这种做法下,因为把_relems从&改为*,所以在每次访问数列(vector)内容前,检查_relems这个vector<int>*类型指针是否为空指针(null)。
给出“另一种做法是:给基类提供默认构造函数”的实现:

num_sequence::num_sequence(int len=1,int bp=1,vector<int>*pe=0)
:_length(len),_beg_pos(bp),_pelems(pe){}

然而这个默认构造函数也采用了成员初始化列表的形式。
采用这样的做法,如果派生类的构造函数没有在其构造函数参表后写:基类构造函数名(基类构造函数参表)的话,编译器会自动调用基类的默认构造函数

派生类对象子对象的各自拷贝构造函数和拷贝赋值运算符调用机制

  • 派生类的类对象作为另一个该派生类的类对象初值时,如果给派生类定义了拷贝构造函数,在这种情况下👇:
派生类类名 派生类类对象名1(数值);
派生类类名 派生类对象名2=派生类对象名1;
  • 就会调用定义给派生类的拷贝构造函数。
    这个派生类的拷贝构造函数的定义也许是这样:
派生类类名 派生类类名(const 派生类类名 &rhs):基类类名(rhs){}
  • rhs代表=右边的派生类类对象,这个派生类类对象在派生类拷贝构造函数的成员初始化列表(:基类类名(rhs))中被传给基类的拷贝构造函数。
  • 然而如果基类不定义基类的拷贝构造函数,会怎么样啊?不会怎么样,基类就直接用默认的成员逐一初始化操作来拷贝基类的数据成员(这是派生类类对象之间拷贝的内部操作,不用多管多想)
    注意,(肯定是基类就直接用默认的成员逐一初始化操作啊,其派生类定义了派生类的拷贝构造函数,那凭什么派生类还要用成员逐一初始化操作来拷贝派生类类对象(=右边的)的构造成员呢?),从而来达成派生类类名 派生类类对象名2=派生类类对象名1;这个操作呗。
    要是基类定义了基类的拷贝构造函数,那就在派生类的拷贝构造函数的定义(或说调用)里会调用基类的拷贝构造函数呗。
  • 关于派生类和基类的拷贝赋值运算符,也是这样。如果派生类有定义拷贝赋值运算符,在派生类类名 派生类类对象名2=派生类类对象名1;这个操作发生时拷贝赋值运算符函数会被作用,此时注意!你必须明确调用基类的拷贝赋值运算符,实例如下:
//Fibonacci是基类num_sequence的派生类
Fibonacci& Fibonacci::operator=(const Fibonacci &rhs)
{
	if(this!=&rhs)//this指针指向=左边的派生类类对象
	{
		//必须明确调用基类的拷贝赋值运算符!!!!
		num_sequence::operator=(rhs);
	}
	return *this;//返回=左边的派生类类对象给这一整体的类对象复制表达式。
}
  • 基类的析构函数会在派生类的析构函数调用结束后被自动调用,无须在派生类中对基类的析构函数做明确的调用操作。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值