目录
运用继承体系
- 这一节没什么要说的,有一点就是基类和其派生类构成一个两层的继承体系。
基类应该多么抽象
每个派生类的共同操作应该再抽离出来给基类类定义里。
- 之前咱们设计的抽象基类提供的仅仅只有接口(一堆纯虚函数,和等被派生类继承的基类成员函数的声明/定义),并未有任何实现。
- 但这样的缺点还是有的,如果要加入一个新类,这种设计就让加入新类变得麻烦起来。
- 所以把派生类的共有实现内容剥离出来扔到基类里。举个例子:
基类旧代码:
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;//返回=左边的派生类类对象给这一整体的类对象复制表达式。
}
- 基类的析构函数会在派生类的析构函数调用结束后被自动调用,无须在派生类中对基类的析构函数做明确的调用操作。