c++ primer plus chapter13 类继承
公有继承格式:
class basic
{
};
class derive : public basic
{
};
basic是基类,derive是派生类。对于公有继承,基类公有成员成为派生类的公有成员,基类的私有成员也会成为派生类的一部分,但是派生类不能直接访问,要通过基类公有方法。
派生类中可以添加自己的成员,派生类构造函数必须给新加的成员和集成的成员提供实数据,但是由于不能直接设置基类的私有成员,因此需要访问基类的构造函数,由于基类应该在派生类之前先被创建,因此应该通过列表初始化的方式初始基类私有成员。若没有调用基类构造函数,那么编译器调用基类的默认构造函数。析构时顺序相反,先析构派生类对象,然后析构基类对象。派生类构造函数书写标准格式如下:
derived::derived(int i, int j) : basic(i), j(j){}
数据会沿着派生链向上传,直到始祖类。
基类指针可以不用显式转换指向派生类对象,基类引用可以不进行显式转换引用派生类对象,这两个关系是不可逆的。但是基类指针只能调用基类方法,原因是派生类方法可能操作派生类新增数据,而这些数据是基类中没有的。
公有继承是一种is-a关系,不是has-a关系,has-a关系需要嵌套;不是is-like-a关系,is-like-a关系不用在面向对象编程中考虑;不是is-implemented-as-a关系,比如可以用数组来实现栈,但是栈类和数组类没有关系,应该在栈类内部包含数组对象隐藏数组实现;不是uses-a关系,两种类需要通信的时候可以构建友元函数。派生类可以增加属性,但是不能删除基类属性。设计思路是,设计一个包含共有特征的ABC类,然后以is-a和has-a关系在这个ABC类的基础上定义相关类。
多态:同一个方法在基类和派生类中行为不同。2种实现多态共有继承的机制:
1.在派生类中重新定义基类方法
2.使用虚方法
同名函数在基类和派生类中作用域不同,不需要虚方法也可以实现。使用虚方法的原因是,基类指针和引用可以指向派生类对象,若不是虚方法,那么将根据引用和指针的类型决定调用基类还是派生类的方法,若是虚方法,将根据指针和引用所指向的对象的类型来决定调用哪个方法,因此即使使用基类指针和引用指向派生类对象,调用的也是派生类方法。虚方法声明时在函数前面加virtual,定义时不要加。基类中声明虚方法,派生类中自然是虚方法(包括派生链后代),显式加virtual为了看着方便。
派生类对象方法中调用基类方法的时候,需要加域解析运算符::,否则若重新定义了重名函数,调的就是自己的函数了。
创建一个基类指针数组,可以突破数组中元素必须类型一致的限制。
虚析构函数:可以保证析构时先析构派生类对象然后析构基类对象,否则若基类指针指向派生类对象,那么只能调到基类析构函数。
静态联编:编译时进行,解释函数
动态联编:运行时进行,比如虚函数,依赖用户输入时,只有运行中才知道调用哪个对象的函数
虚函数工作原理:编译器给每个对象增加一个隐藏成员,保存一个指向函数地址数组的指针,称为虚函数表,表中存储了该类所有虚函数的地址。若派生类没有重新定义虚函数,那么使用基类的地址,否则是新的地址。可以观察对象大小变化,有虚函数后增加了一个指针的大小。
虚函数注意事项:
1.基类中声明为virtual,派生链中所有后代都是虚函数
2.构造函数不能是虚函数,因为是也没有意义
3.析构函数应该是虚函数
4.只有成员函数才有虚函数一说,友元不涉及
5.派生类没有重新定义函数,将使用该函数的基类版本,派生链中使用最新的虚函数版本
6.只要函数重名,基类函数就会被隐藏,就算参数列表不一致也不会被重载,因此重新定义继承的方法,应该保持原型一致,例外是返回值,允许返回类型协变,但是参数必须一致。若基类函数被重载了,派生类应该重新定义所有的基类版本,若只定义一个,那么其他所有版本都会被隐藏
protected成员:对类的外界来说,protected成员和private成员一样,外界无法直接访问;对派生类来说,protected成员和public成员一样,可以直接访问父类的protected成员。对于数据成员最好不要使用protected,通过设计类方法让派生类访问基类数据,但是对于成员函数,protected很有用,让派生类可以访问外界无法使用的内部函数。
抽象基类:即ABC类,解决的问题是椭圆-圆问题。圆是一种椭圆,满足is-a关系,但是这种is-a关系和之前的不同,因为派生类需要的数据成员和方法比基类少而不是需要扩展自己的数据成员,比如椭圆的长半轴、短半轴、方向角等,对于圆是多余的,从椭圆类派生圆类还不如重新写一个圆类。为了解决这个问题,从椭圆和圆中抽象出来共性,放入ABC类中,从ABC类派生出椭圆类和圆类。ABC类的特征是拥有纯虚函数,它是一个未实现的函数,格式如下:
class Test
{
public:
virtual void func() const = 0;
};
只要类中有一个纯虚函数,它就是ABC类,不能给这样的类创建对象,但是c++允许给虚函数提供定义,在派生类中可以选择直接使用。派生类中必须重新定义ABC类中的纯虚函数,否则会编译报错,但是在ABC的孙子类中,不需要重新定义虚函数,因为ABC的子类中一定有纯虚函数的定义了,孙子类中可以找到定义,就不需要重写了。
ABC类的析构函数可以是纯虚函数,但是必须提供定义,否则运行阶段报错,因为对象销毁时找不到析构函数定义。
继承和动态内存分配:有几种情况:
1.基类构造函数中使用了new,并提供了复制构造函数和赋值运算符,派生类中没有new,那么不需要给派生类显式定义复制都早函数和赋值运算符,默认是逐成员复制,对于long,int这种基本类型是常规赋值,对类成员、继承的类组件是调用相应的复制构造函数和赋值运算符实现的。
2.基类有new,派生类中有新的new,必须给派生类显式定义析构函数、复制构造函数、赋值运算符。其中析构函数析构自己的new就可以,复制构造函数调用基类复制构造函数作为初始化列表即可,赋值运算符需要用过作用域解析运算符::显式调用基类的赋值运算符。完整的例子如下:
/* 基类,省略了定义 */
class father
{
private:
char *l;
public:
father(char * = "null");
father(const father &);
father & operator=(const father &);
virtual ~father();
};
/* 公有派生 */
class son : public father
{
private:
char *t;
public:
son();
son(const son &);
son & operator=(const son &);
~son();
};
/* 复制构造函数,使用初始化列表显式调用基类的复制构造函数 */
son::son(const son & s) : father(s)
{
t = new char[strlen(s.t) + 1];
strcpy(t, s.t);
}
/* 赋值运算符,通过显式调用基类的赋值运算符处理基类部分的赋值 */
son & son::operator=(const son & s)
{
if (this = &s)
return;
father::operator=(s);
delete [] t;
t = new char[strlen(s.t) + 1];
strcpy(t, s.t);
return *this;
}
son::~son()
{
delete [] t;
}
友元和继承:考虑这样一种情况,基类和派生类都使用友元重载了operator<<,在派生类的友元中,需要调用基类的operator<<来打印基类数据成员,为了防止调用自己的operator<<需要显式调用,由于友元不在类作用域中,不能使用作用域解析运算符::指定调用谁的operator,因此需要显示转换,如下
class father
{
private:
int i;
friend os & operator<<(ostream & os, const father & f){os << i << endl;return os;}
};
class son : public father
{
private:
int j;
friend os & operator<<(ostream & os, const son & s)
{
/* 通过强制转换调用基类的operator<< */
os << (const father &)s;
os << j << endl;
return os;
}
};