继承、派生

在自然界中,继承与派生是一种非常普遍的现象

例:猫类,狗类都属于哺乳动物类,具备胎生,哺乳,恒温等哺乳动物的所有性质,同时又具有各自的特有的性质

这是就是继承关系的重要性质,形成继承关系的两个类之间,具有IS_A的关系

继承:一旦指定某种事物父代的本质特征,那么它的子代会自动具有这些性质
继承是一种朴素的可重用的概念

派生:子代可以拥有父代所没有的特性,这是可扩充的概念
继承就是在一个已存在的类基础上建立另一个新的类

已存在的类,称为基类或父类,新建立的类称为派生类
派生类的功能主要通过以下方式来体现:

  • 吸收基类的成员
  • 改造基类的成员
  • 添加新成员

从编码的角度,派生类从基类中以较低的代价换取了较大的灵活性:
派生类可以对继承的属性进行扩展,限制或改变,一旦产生可靠的基类,只需要调试派生类所作的修改即可

在面向对象思想发展的初期,通过继承复用代码曾经被认为是面向对象最重要的目标之一,但遗憾的是,实践中人们发现在开发中滥用继承的话是后患无穷的
这是因为代码中存在一定的耦合性,基类会产生较深的类型继承树,因此常常牵一发而动全身

继承是对类型之间的关系建模,共享公共的东西,仅特化本质上不同的东西
派生类能继承基类定义的成员,可以无需改变而使用那些与派生类型具体特性不相关的操作,并可重定义那些与派生类型相关的成员函数,将函数特化仅考虑派生类型的特性
除从基类继承成员外,派生类还可以定义更多的成员
注意:例外的是基类的构造函数和析构函数都是不能被继承的,因为这两种成员都是描述对象在其本身层次的行为,与基类无关

继承方式(以单继承为例)

公有继承

继承方式包括:public,protected,private
派生类虽然继承基类的所有成员,但派生类并非都能访问基类全部成员,继承方式会影响派生类对基类中各种成员的使用
在派生类中除公有成员,保护成员,私有成员外还存在不可访问成员,即在类外不可直接访问,在派生类内部也不可直接访问
公有派生类基本格式为:

class 派生类名:public 基类名{};

在公有派生类中,基类成员的在其中的引用权限为:
公有成员和保护成员访问权限不变,私有成员变为不可访问成员,例:

class Base{
    int a;
public:
    int b;
    Base(int v1=0,int v2=0):a(v1),b(v2){}
};
class Derived:public Base{
    int c;
public:int d;
    Derived(int v1=0,int v2=0):c(v1),d(v2){}
    void func();
};
void Derived::func(){
    cout<<a<<' '<<b<<' '<<c<<' '<<d<<endl;
}
int main(){
    Derived derived(0,0);           //derived对象中有四个数据成员,其中a,b从基类继承,c,d从派生类派生
    derived.a=1;                    //访问失败,Base类的私有成员在派生类中不可访问,在类外更不可以访问(private权限+任何继承=不能访问)
    derived.b=1;                    //成功通过对象访问公有成员(public继承+public权限=public权限可以访问)
    derived.c=1;                    //访问失败,Derived类的私有成员在类外不可访问(private允许自身成员函数访问但不能通过自身对象访问)
    derived.d=1;                    //Derived类的公有成员在类外可以访问
    derived.func();                 //func()中a是基类私有成员不可访问,b是基类公有成员可以访问,c,d是Derived类自己的成员允许成员函数访问
}  

程序中的大多数情况都是公有派生,这是因为只有公有派生的情况下,才可能出现基类公有成员变成派生类公有成员的情况。想要使用派生类为基类赋值或派生类指针访问基类成员必须是在公有派生的基础上,私有和保护继承无法进行赋值,引用和指针指向,而这一点是面向对象程序设计中多态性实现的前提。

私有继承

私有派生类基本格式为:

class 派生类名:private 基类名{};

在私有派生类中,基类成员的在其中的引用权限为:
公有成员和保护成员变为私有成员,私有成员作为不可访问成员,例:

class Derived:private Base{
    int c;
public:
    int d;
    void func();
};
void Derived::func(){
    cout<<a<<' '<<b<<' '<<c<<' '<<d<<endl;
}
int main(){
    Derived derived;     //无法初始化,因为Base类中的构造函数在Derived类已经变为私有
    derived.b=1;         //访问失败,Base类的公有成员在Derived类中是私有成员,不能在类外访问
}                

因此如果只存在public和private两种访问权限的情况下,对派生类来说基类的私有成员是不可见的(不可访问成员),即使派生类已经继承了基类所有成员
为了解决这种情况,c++才引入保护成员,即用protected关键字说明的成员

保护继承

若希望在派生类中能访问某个成员,并且不希望其在类外可见,应当把它声明为保护成员
如果在一个类中声明了保护成员,就意味着该类可能要用作基类,在它的派生类中会访问这些成员
保护成员的特点是,基类和派生类都能访问,但在类外不可访问,保护派生类基本格式为:

class 派生类名:protected 基类名{};

在保护派生类中,基类成员的在其中的引用权限为:
公有成员和保护成员变为保护成员,私有成员仍然作为不可访问成员

class Derived:protected Base{
protected:
    int c;
public:
    int d;
    Derived(int v1=0,int v2=0):a(v1),b(v2){};
    void func();
};
void Derived::func(){cout<<a<<' '<<b<<' '<<c<<' '<<d<<endl;}
int main(){
    Derived derived;
    derived.a=1;
    derived.b=1;
    derived.c=1;      //保护成员在类外都不可以访问
    derived.func();   //成员函数可以访问a,b,c,d全部成员 
}                                     

同名访问原则

继承关系具有同名覆盖原则(派生类成员覆盖基类成员)
c++允许派生类重新定义基类中的成员,此时称派生类的成员覆盖了基类的同名成员

在派生类中如果想使用基类中的同名成员,可以显式地使用这种基本格式:基类名::成员名

这本质上来说还是一个作用域嵌套的问题,当存在继承关系时,派生类的作用域嵌套在基类的作用域之内
当派生类对象访问成员时,会在作用域链中寻找最匹配的成员,对于成员变量直接查找该成员即可,对于成员函数,编译器仅仅根据函数名字来查找,当内层作用域有同名函数时,将这些同名函数作为一组重载候选函数。

class Base{
protected:
    int a;
public:
    Base(int v=0):a(v){}
};
class Derived:public Base{
    int a;
public:
    Derived(int v):a(v){}
    void func(){
        Base::a=a;
        cout<<Base::a;
    }
};
int main(){
    Derived derived(1);
    derived.func();
}   //输出结果:1    1为derived对象的Base::a成员值 

赋值兼容原则

赋值兼容的实现在c语言中极为重要,因为这是c面向对象体系中最为关键的多态性质实现的基础,c++多态正是靠基类指针或引用指向派生类的虚函数从而在运行时刻完成同名函数的动态绑定。
基本数据类型之间都是可以相互转换的,即使不能隐式转换通常情况也可以进行强制转换,但不同类之间通常是不可以进行转换的
但是,基类和派生类在满足一定条件下是可以转换的,转换的规则是赋值兼容原则
在公有派生的情况下,派生类对象可以作为基类对象来使用,即

  • 派生类的对象可以直接赋值给基类的对象(基类对象将派生类对象中属于基类部分的成员数据拷贝)
  • 基类的引用可以引用一个派生类的对象(同理,引用的是派生类对象中属于基类部分的成员数据)
  • 基类对象的指针可以指向一个派生类对象(同理,指向属于基类部分的成员数据)

这是因为派生类拥有从基类继承过来的成员,并且派生类对象和基类对象的内存布局方式是相似的
当一个派生类对象直接赋值给基类对象时,不是所有数据都赋值给了基类对象,赋予的只是派生类对象的一部分,这部分即派生类对象的切片(sliced),通过基类指针或引用能看到的是一个基类对象,派生类中成员对于基类指针或引用来说是不可见的,而反之不成立,通常情况派生类不能用基类本身、引用、指针来赋值,就像所有的狗都是动物,但不是所有动物都是狗,例:

class Animal{
public:
    void eat();
};
class Dog:public Animal{
    public:void eat();
};
void Animal::eat(){
    cout<<"eating..."<<' ';
}
void Dog::eat(){cout<<"wang wang"<<endl;}
int main(){
    Dog d;
    Animal a1=d,*a2=&d,&a3=d;
    a1.eat();
    a2->eat();
    a3.eat();
}
//输出结果:eating... eating... eating...         无论是直接复制,引用,指针的方式,最后Animal对象保留的只有其自身的成员
int main(){
    Dog d;
    Animal a;
    d=a;
    (Dog)a=d;
}      //无论是拿Animal对象给Dog对象赋值,还是对Animal强制类型转换均无法通过编译

其实从内存分布的角度来说更好理解,派生类对象赋值给基类直接按成员拷贝甚至位拷贝都行,但是派生类对象类型接收基类对象类型的话显然是不完整的,并且c++并没有明确规定派生类的对象在内存中如何分布,在一个对象中继承自基类的部分和派生类自定义的部分不一定连续存储,这就导致基类的对象或指针没有办法处理派生类的信息。
再换个角度来说,这种编译器允许的基类和派生类转换的规则称为向上转型,是没有安全隐患的。反过来用派生类指针指向基类对象,称为向下转型,默认是不允许的。把基类对象赋给派生类对象是肯定无法进行的,因为无论位拷贝还是成员拷贝都没法进行;如果是指针或引用,可以使用强制类型转换将基类转为派生类再赋值,但这种情况下派生类指针引用如果调用自身成员函数中有对基类中没有的成员的访问的话就会出现问题

  • 如果不是虚继承,那么继承的成员在前,新定义的成员在后,说明新成员的偏移量更大(必然超过基类对象的大小),此时就会越界访问到赋值的基类对象后面的内存空间
  • 这种情况也就是默认的强制类型转换static_cast<目标类型>(转换对象)

因此,如果想要对类对象指针或引用进行类型转换,最好不要使用默认的强制转换()或者直接static_cast,而是使用dynamic_cast,后者会自动对转换对象的继承树进行遍历,如果发现是向下转换会直接转换失败返回nullptr,保证不会出现向下转换情况的发生