C++之继承(Inheritance)

在 C++中代码的可重用性(software reusability)是通过继承(inheritance)这一机制来实现的。如果没有掌握继承性, 就没有掌握类与对象的精华。

类的继承, 是新的类从已有类那里得到已有的特性。 或从已有类产生新类的过程就是类的派生。 原有的类称为基类或父类, 产生的新类称为派生类或子类。派生与继承, 是同一种意义两种称谓。

如果 A is a B, 则 B 是 A 的基类, A 是 B 的派生类。 为继承关系。 如果 A 包含 B,则 B 是 A 的组成部分。 为聚合关系, 可以由组成部分聚合成为一个类。 所以只有是is a 的关系,才能够继承。

默认的继承方式是 private 私有继承。

一个派生类可以同时有多个基类, 这种情况称为多重继承, 派生类只有一个基类, 称为单继承。

单继承的语法:

class 派生类名: [继承方式] 基类名
{
    派生类成员声明;
};

1、继承方式

public继承时:父类中的protected是只有被继承的子类和本类的成员函数可以访问。父类中的private成员,子类也无法访问。

继承方式语义
公有继承 public基类的公有成员和保护成员在派生类中保持原有访问属性, 其私有成员仍为基类的私有成员,子类不可以访问
私有继承 private基类的公有成员和保护成员在派生类中成了私有成员, 其私有成员仍为基类的私有成员。
保护继承 protected基类的公有成员和保护成员在派生类中成了保护成员, 其私有成员仍为基类的私有成员。

基本上都是用public来继承,Google的C++规范明确指出只能用public,不允许用别的方式。

继承的规则:

1 全盘接收, 除了构造器与析构器。 基类有可能会造成派生类的成员冗余, 所以说基类是需设计的。

2 派生类有了自己的个性, 使派生类有了意义。

派生类中的成员, 包含两大部分, 一类是从基类继承过来的, 一类是自己增加的成员。从基类继承过过来的表现其共性, 而新增的成员体现了其个性。 在内存中是紧挨着的。

2、构造顺序

由于子类中, 包含了两部分内容, 一部分来自, 父类, 一部分来自, 子类。 父类的部分, 要由调用父类的构造器来完成, 子类的部分, 在子类的构造器中来完成始化。 子类中,有内嵌的子对象(内嵌子对象就是类中的数据成员是其他类的一个对象)也需要构造。

子类会调用父类构造器,只有在父类含有标配的情况下才会调用,否则只能在子类中显示的调用。(标配就是不需要提供参数的构造器,也就是无参构造器或参数列表都有默认参数的情况),内嵌子对象也一样。
显示调用如下:
注意:基类的构造器和内嵌子对象只能放在参数列表中,而且参数列表中的是内嵌子对象,是放的对象,不是类,而且基类和内嵌子对象顺序只能是下面这样

派生类名::派生类名(总参列表)
    :基类名(参数表),内嵌子对象(参数表)
{
    派生类新增成员的初始化语句; //也可出现地参数列表中
}

3、派生类的拷贝构造

因为拷贝构造也是一种构造器,构造器都没有被继承下来,所以拷贝构造器也没有被继承下来。

当子类, 不自实现时,默认调用父类的拷贝构造。
若自实现,不显示的调用父类的拷贝构造, 此时只会调用父类的构造器,此时失去了拷贝构造的意义
子类对象赋给父类的引用,赋值兼容
当内嵌子对象,子类不自实现时拷贝构造, 默认调用内嵌子对象的拷贝构造。
若自实现,不作显示的调用内嵌子对象的拷贝构造, 此时只会调用内嵌子对象的构造器, 此时失去了拷贝构造的意义

#include <iostream>
using namespace std;

class A
{ 
public:
    A()
    {
        cout<<"A()"<<endl;
    } 
    
    A(const A & anthter)
    {
        cout<<"A(const A & anthter)"<<endl;
    }
};

class C
{ 
public:
    C()
    {
        cout<<"C()"<<endl;
    } 
    C(const C & another)
    {
        cout<<"C(const c & another)"<<endl;
    }
};

class B:public A
{ 
public:
    B()
    {
        cout<<"B()"<<endl;
    } 
    B(const B & another)
        :A(another),_c(another._c)  //初始化列表调用父类和内嵌子对象的拷贝构造器,要用对象名,不是类名
    { } 
    
    C _c;
};

int main()
{
    B b;
    cout<<"+++++++++++++++"<<endl;
    B bb(b);
    return 0;
}

4、派生类的赋值重载

子类中若未实现赋值重载, 调用父类赋值重载(深或浅)同时调用内嵌子对象赋值重载。

子类中 实现赋值重载, 若无显示的调用父类的赋值重载, 父类成员的保持己有构造。 此时将失去赋值的意义, 通常要显示的调用父类的赋值重载。若无显示的调用内嵌子对象赋值重载,则内嵌子对象保持己有构造。 此时将失去赋值的意义, 通常要显示的调用内嵌子对象赋值重载。

显示的调用父类的赋值重载, 需要加父类和域作用域运算符, 避免死递归。

#include <iostream>
using namespace std;
class A
{ 
public:
    A()
    {
        cout<<"A"<<endl;
    } 
    A& operator=(const A &)
    {
        cout<<" A & operator=(const A &)"<<endl;
    }
};
class C
{ 
public:
    C()
    {
        cout<<"C()"<<endl;
    } 
    
    C& operator=(const C &)
    {
        cout<<" C & operator=(const C &)"<<endl;
    }
};

class B:public A
{ 
public:
    B & operator=(const B &another)
    {
        _c = another._c;//实现内嵌子对象的赋值重载,调用子对象的=
        A::operator=(another);//实现父类的赋值重载,调用子对象的=
        cout<<" B & operator=(const B &)"<<endl;
    }
    C _c; //内嵌子对象
};
int main()
{
    B b1,b2;
    cout<<"++++++++++"<<endl;
    b1 = b2;
    return 0;
}

5、派生类的友元函数

由于友元函数并非类成员, 因引不能被继承, 在某种需求下, 可能希望派生类的友元函数能够使用基类中的友元函数。 为此可以通过强制类型转换, 将派生类的指针或是引用强转为其父类的引用或是指针, 然后使用转换后的引用或是指针来调用基类中的友元函数。

#include <iostream>
using namespace std;

class Student
{
    friend ostream &operator<<(ostream & out, Student & stu);//父类友元函数
public:
    Student(int i,int j)
        :a(i),b(j){}
private:
    int a;
    int b;
};

ostream &operator<<(ostream & out, Student & stu) //全局友元函数
{
    out<<stu.a<<endl;
    out<<stu.b<<endl;
}

class Graduate:public Student
{
    friend ostream &operator<<(ostream & out, Graduate & gra) //子类友元函数
    {
        cout<<(Student&)gra; //要强制类型转化为(Student&)
        cout<<gra.c<<endl;
    }
public:
    Graduate(int i,int j,int k)
        :Student(i,j),c(k){}
private:
    int c;
};

int main()
{
    Graduate  g(1,2,3);
    cout<<g<<endl;
    return 0;
}

6、派生类析构函数

析构函数的执行顺序与构造函数相反。

派生类的析构函数的功能, 保证层级内与其对应的构造函数, 完成清理工作。 无需指明析构关系。

7、多继承

从继承类别上分,继承可分为单继承和多继承,多继承,即父类不止一个。

多继承,就像生活中的沙发床,它既是沙发,又是床,所以可以继承两个父类,一个是沙发类,一个是床类,就成了沙发床。

继承语法

派生类名:public 基类名 1, public 基类名 2, ...,protected 基类名 n

构造器格式

派生类名::派生类名(总参列表)
    : 基类名 1(参数表 1), 基类名(参数名 2)....基类名 n(参数名 n),
        内嵌子对象 1(参数表 1),内嵌子对象 2(参数表 2)...内嵌子对象 n(参数表 n)
{
    派生类新增成员的初始化语句;
} 

此时只要正常的多继承就可以了,但是如果两个父类中数据成员重复,比如后面写的,那么就是接下来的虚继承问题。

三角问题

多个父类中重名的成员, 继承到子类中后, 为了避免冲突, 携带了各父类的作用域信息, 子类中要访问继承下来的重名成员, 则会产生二义性, 为了避免冲突, 访问时需要提供父类的作用域信息。

比如子类继承了父类X和父类Y,子类和父类中都有_data这个数据成员,那么我们在子类Z中的访问应该是这样的

void dis()
{
    cout<<X::_data<<endl;
    cout<<Y::_data<<endl;
} 

虚继承——四角问题

​ 三角关系中需要解决的问题有两类:

  • 数据冗余问题,
  • 访问不方便的问题。

解决方案, 是三角转四角的问题。 具体操作:

  • 提取公共成员构成祖父类, 即虚基类,
  • 各父类虚继承虚基类

在多继承中, 保存共同基类的多份同名成员, 虽然有时是必要的, 可以在不同的数据成员中分别存放不同的数据, 但在大多数情况下, 是我们不希望出现的。因为保留多份数据成员的拷贝, 不仅占有较多的存储空间, 还增加了访问的困难。 为此, C++提供了, 虚基类和虚继承机制, 实现了在多继承中只保留一份共同成员。

否则保留了多份成员,这就是虚继承的意义

比如类D继承B和C,然后B和C继承A,如果不采用虚继承的方式,那么就是这样存储的。

采用虚继承就是这样的:

虚基类

经提取, 存有公共元素的, 被虚继承的祖父类, 称为虚基类。 虚基类, 需要设计和抽象, 虚继承, 是一种继承的扩展。

语法:

class 派生类名:virtual 继承方式 虚基类

初始化顺序

先祖父类, 父类(从左向右), 子类, 仅最后一次初始化有效。

比如:

class SofaBed:public Sofa, public Bed

这里就是先初始化虚基类(祖父类),然后初始化Sofa类,因为上边代码中Sofa写在Bed前面,最后初始化Bed类,最后一次才是最有效的。

Last modification:October 6th, 2019 at 02:15 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment