C++类的三大特性之继承

  • Post author:
  • Post category:其他




一:继承的概念与使用



<1> 什么是继承?

  • 通俗的说,继承是类设计层次的复用
  • 我们之前都接触过函数复用,就是在一个函数中调用另一个函数完成其部分或全部功能,比如push_back复用insert
  • 而继承就是有一个类去复用另一个类,我们把被继承的类叫做基类或者父类,继承的类叫做派生类或者子类。
  • 派生类拥有基类的功能的同时,还加上了独属于自己的功能
  • 也许你还不理解为什么要有继承这个东西,举个栗子吧
  • 现在我们需要两个类,一个是学生,一个是教师,他们都有成员变量name,id.但是还有其它的变量或者成员函数,我们定义这两个类时写这些共有的东西就显得冗余,于是我们写个类Person,它只拥有name和id两个成员变量,和Student和Teacher类都有的成员函数,比如Show
class Person
{
public:
    void Show()
    {
        cout << "name: " << _name << endl;
        cout << "id: " << _id << endl;
    }
private:
    string _name;
    string _id;
};
class Student : public Person
{
private:
    double _gpa;
};
class Teacher : public Person
{
private:
    string _title;
};



<2> 如何使用

  • 继承的格式是

    class Student :public Person

    ,其中Student是派生类,public是继承方式,Person是基类,也就是

    class 派生类 :继承方式 基类
  • 这里提到了继承方式,不同的继承方式会导致派生类中访问基类成员的方式发生变化
  • 具体如下表:
类成员/ 继承方式 Public继承 Protect继承 private继承
基类的public成员 派生类public 派生类protect 派生类private
基类的protect成员 派生类protect 派生类protect 派生类private
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见
  • 总结:
  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  3. 基类的私有成员在子类都是不可见,基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,最好显示的写出继承方式。
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
class Person
{
public:
    void Show()
    {
        cout << "name: " << _name << endl;
        cout << "id: " << _id << endl;
    }
    string _name="张三";
    string _id="10086";
};
class Student : protected Person
{
private:
    double _gpa;
};
class Teacher : protected Person
{
private:
    string _title;
};
int main()
{
    Person p;
    Student s;
    cout << p._name << endl;
    cout << s._name << endl;
    return 0;
}

在这里插入图片描述

  • 可以看到我们可以调用Person类的_name,因为它是公有成员变量
  • 但是被Student类进行protected继承后,在Student类中_name就变为了protected的成员变量,我们不能直接访问



二:基类与派生类间的转换

  • 派生类的对象可以直接赋值给

    基类的对象,指针,引用

    ,这种行为也叫做切片
  • 实际上就是把派生类中基类中的那部分切下然后赋值过去

    • 但是基类不能赋值给派生类


      在这里插入图片描述
int main()
{
    Person p;
    Student s;
    p = s;//派生类赋值给基类对象
    Person* ptr = &s;//派生类的地址给基类的指针
    Person& ref = s;//基类的引用指向派生类
    return 0;
}



三:继承的作用域

  • 基类与派生类拥有独立的作用域
  • 当子类和父类有同名成员或函数时,子类成员将会屏蔽父类对同名成员的直接访问,这种情况叫做隐藏或者重定向(要想访问基类成员需要加上基类类名和域作用限定符,比如

    Person::_name

  • 此时基类和派生类函数名相同可能会被误理解为函数重载,但是由于基类和派生类的作用域不同,所以它们是不构成重载的,构成函数重载需要在同一个作用域中

  • 并且基类和派生类只要函数名相同就构成隐藏,不管参数类型是否相同
  • 推荐不要在基类和派生类中定义同名成员

    在这里插入图片描述



四:派生类的默认成员函数



<1> 构造函数

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用



<2>拷贝构造

  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化



<3>赋值运算符重载

  • 派生类的operator=必须要调用基类的operator=完成基类的复制



<4>析构函数

  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员,因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
  • 所以这里我们不用自己调用基类的析构函数,不然基类的析构函数会被调用两次,可能造成申请的空间的重复释放
class Person
{
public:
    Person(const string& name, const string& id)
        :_name(name)
        , _id(id)
    {};
    Person(const Person& p)
        :_name(p._name)
        , _id(p._id)
    {};
    Person& operator=(const Person& p)
    {
        if(this!=&p)
        {
            _name = p._name;
            _id = p._id;
        }
    }
    ~Person()
    {
        cout << "~Person()" << endl;
    }
    void Show()
    {
        cout << "name: " << _name << endl;
        cout << "id: " << _id << endl;
    }
private:
    string _name;
    string _id;
};
class Student : public Person
{
public:
    Student(const string& name, const string& id, double gpa)
        :Person(name, id)
        , _gpa(gpa)
    {};
    Student(const Student& s)
        :Person(s)
        , _gpa(s._gpa)
    {};
       Student& operator=(const Student& s)
    {
           if (this != &s)
           {
               Person::operator=(s);
               _gpa = s._gpa;
           }
    }
    ~Student()
    {
        cout << "~Student()" << endl;
    }
    void Show()
    {
        Person::Show();
        cout << "gpa: " << _gpa << endl;
    }
private:
    double _gpa;
};



五:友元与静态成员



<1>友元

  • 友元关系是不能继承的,也就是说基类的友元并不是派生类的友元,它不能访问派生类的私有成员

    在这里插入图片描述
  • 如图,Show函数是Person的友元,可以访问Person类的私有成员
  • 但是不能访问继承了Person类的Student类的私有成员



<2> 静态成员

  • 基类定义了一个static静态成员,则整个继承体系中只有一个这样的成员
  • 也就是说不会像非静态成员一样,子类成员中也有一个相同的成员
  • 利用这个特性可以计算出我们究竟创建了多少派生类对象和基类对象

    在这里插入图片描述



六:菱形继承与虚继承

在这里插入图片描述

  • 如图所示,一个类继承了两个类,并且这两个类继承了同一个基类,这就形成了菱形继承
  • 这会导致很多问题
  • 比如Person的成员变量在Student类中有一份,在Teacher类中有一份,并且这两份完全一样,都被Assistant继承了,但其实Assitant只要一份就够了,导致了代码冗余
  • 并且导致了二义性,我们使用_name到底是Student类里面的还是Teacher类中的,这虽然可以通过指定作用域解决,但还是有不便之处

    在这里插入图片描述
  • 为了解决上述不便之处,我们可以使用虚继承
  • 即在Student类和Teacher类继承时在前面加上

    virtual

    关键字
  • 就像这样

    class Student: virtual public Person
class Person
{
public:
    int _a;
};
class Student : public Person
{
public:
    int _b;
};
class Teacher : public Person
{
public:
    int _c;
};
class Assistant : public Student , public Teacher 
{
public:
    int _d;
};
int main()
{
    Assistant tmp;
    tmp.Student::_a = 1;
    tmp.Teacher::_a = 2;
    tmp._b = 3;
    tmp._c = 4;
    tmp._d = 5;
    return 0;
}

在这里插入图片描述

  • 可以看到不使用虚继承会有两个_a变量存在,代码冗余

    ![![- 在这里插入图片描述](https://img-blog.csdnimg.cn/05b4d809e264421391a72406a8c902ba.png)
  • 而我们使用了虚继承之后,就只有了一个变量_a,放在对象的最下方,_a同时属于Student和Teacher类,Student类和Teacher类每个类都存了一个指针,叫做虚基表指针
  • 这个虚基表指针指向一张表,那张表中存储了指针所在的位置离_a位置的偏移量,通过这个偏移量就能找到_a



七:继承与组合

  • 除了继承,类直接还能组合,即有一个类A,一个类B,类B是类A的一个成员变量
  • 这就是A组合B,是一种has-a的关系,相当于黑箱复用,类A只能调用类B的公有接口,B对A不透明
  • 而继承相当于白箱复用,是一种is-a的关系,B对A是透明的,这一定程度上破坏了封装
  • 能用组合的地方就少用继承,因为组合的耦合度低,一个类修改了对另一个类影响很小,而继承中基类修改了对派生类影响很大



版权声明:本文为m0_63445149原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。