Skip to content

1. 继承

1.1 继承的概念

那么什么是继承呢?用来做什么?有什么好处呢?

  • 在已有类型的基础上创建新的类型,新类型拥有(继承了)已有类型的所有特征(属性/行为)
  • 继承主要用来描述那些非常相似,只有细微差别的类之间的关系(和多态一起实现接口重用)
  • 继承可以实现代码重用,减少代码冗余,提高开发效率

通过继承联系在一起的类,构成了一种层次关系

  • 处于层次关系中根部的类,称为基础类(Basic Class),简称基类,也称为父类
  • 处于层次关系中其他的类,这些继承而来的类,称为派生类(Derived class),也称为子类

基类和派生类分别应该完成什么事情(定义哪些属性)呢?

  • 基类负责定义层次关系中所有的类共同拥有的那些行为和特征(共性)
  • 派生类自动获取基类的行为和特征,并且负责定义派生类自己特有的行为和特征(特性)

通过一种机制表达类型之间共性和特性的方式,利用已有的数据类型定义新的数据类型,这种机制就是继承

1.1.1 基类的定义

定义基类的方式和定义普通类的语法规则相同(基类就是一个普通的类型)

cpp
class 类名  
{  
   属性;  
   行为;  
};

但是在设计基类的时候,需要注意:

  1. 基类的成员函数
    • 一种是基类希望派生类直接继承而不需要修改的行为(例如:所有的人都要吃饭睡觉)
      • 应该为普通的成员函数
    • 一种是基类希望派生类提供自己的定义,以覆盖从基类继承过来的定义(例如:学生要学习,老师要上课)
      • 这种函数,应该设计为虚函数(virtual function)(后面会讲)
cpp
class Human  
{  
    public:  
    Human(const string& name, int age):m_name(name), m_age(age), m_Ionum(1234)  
    { }  

    void eat(const string& food)
    {
        cout << "我在吃" << food << endl;
    }
    void sleep(int time)
    {
        cout << "我睡了" << time << "个小时" << endl;
    }
  1. 基类成员的访问控制
    • public: 基类的公有成员在任何地方都可以直接访问,包括派生类
    • private: 基类的私有成员只有友元函数和本类成员函数可以访问,其他地方都不可以访问派生类也不行
    • protected: 如果有些成员不希望对外公开,当时又希望它的派生类能够直接访问,则可以使用protected来修饰这些成员(友元,本类的成员函数,派生类的成员函数)
cpp
class Human
{
public:
    Human(const string& name, int age) :m_name(name), m_age(age), m_Ionum(1234)
    {
    void eat(const string& food)
    {
        cout << "我在吃" << food << endl;
    }
    void sleep(int time)
    {
        cout << "我睡了" << time << "个小时" << endl;
    }
protected://保护型的成员可以在子类中被访问
    string m_name;
    int m_age;
    //私有成员子类中无法访问

    //但是可以提供保护的接口函数来间接访问
    int getIDnum()
    {
        return m_Ionum;
    }
private:
    int m_Ionum;//身份证号
};
1.1.2 派生类的定义

由于派生类是在基类的基础上创建的,所以我们在定义派生类的同时需要指明基类是什么,并且指明以什么方式基础
语法规则:

cpp
class 派生类名称:继承方式1 基类名1,继承方式2 基类名2...
{
   派生类自己新增的属性和行为;
};
  • 如果派生类只有一个基类,则这种方式叫做单继承
  • 如果派生类有多个基类,则这种方式叫做多继承

1.2 继承方式

  • 继承方式(三种):指定基类成员在派生类内部(派生类自己的成员函数)和派生类外部(通过对对象的访问权限)

    • public 公有继承(一定需要掌握)
    • private 私有继承
    • protected 保护继承
  • 继承方式可以省略.class的默认的基础方式是private继承,struct的默认的继承方式是public继承虽然class和struct都有默认的继承方式,但是为了程序的可读性应该显示的说明继承方式

  • 派生类继承自基类,拥有基类的所有的成员(一切),可以看做是派生类中拥有一个基类子对象,所以在构造派生类前,要先构造基类

    • public: 公有继承
      • 基类的公有成员,通过公有继承,成为派生类自己的公有成员。
      • 基类的私有成员,不会被子类直接继承 (可以继承, 但是不能使用)。这意味着子类不能直接访问或操作父类的私有成员。私有成员是父类内部实现的细节,对于类来说是隐藏的。
        然而,尽管子类不能直接访问父类的私有成员,但这些私有成员在子类对象中仍然是存在的。这是因为子类对象在内存中包含了父类对象的部分,包括父类的私有成员。这些私有成员在父类的构造函数中被初始化,并在子类对象中占据一定的内存空间。
      • 基类的保护成员,通过公有继承,成为派生类自己的保护成员:
cpp
#include <iostream>
using namespace std;

class Human
{
    public:
    Human(const string& name, int age) :m_name(name), m_age(age), m_Ionum(1234)
    {
    void eat(const string& food)
    {
        cout << "我在吃" << food << endl;
    }
    void sleep(int time)
    {
        cout << "我睡了" << time << "个小时" << endl;
    }

protected://保护型的成员可以在子类中被访问
    string m_name;
    int m_age;
    //私有成员子类中无法访问

    //但是可以提供保护的接口函数来间接访问
    int getIDnum()
    {
        return m_Ionum;
    }
private:
    int m_Ionum;//身份证号
};

//学生类(人类的一个子类)
class Student :public Human
{
    public:
    //Human(...)说明从基类中继承来的成员的初始化方式
    Student(const string& name, int age, int no) :Human(name, age), m_no(no) {}
    void learn(const string& course) {
        cout << "我在学" << course << endl;
    }
    void who(void) {
        cout << "我叫" << m_name << ",今年" << m_age <<
        "岁,学号是" << m_no << endl;
        cout << "身份证号" << getIDnum() << endl;
    }
private:
    int m_no;
};

int main()
{
    Student stu("蔡徐坤", 18, 001);
    stu.who();
    stu.eat("香翅捞饭");
    stu.sleep(8);
    stu.learn("C++");
}

private: 私有继承(限制比较大,用的少)
○ 基类的公有成员,通过私有继承,成为了派生类自己的私有成员
○ 基类的私有成员,通过私有继承,成为了派生类的一部分,但是派生类不可以直接访问它
○ 基类的保护成员,通过私有继承,成为了派生类自己的私有成员

cpp
#include <iostream>
using namespace std;

class Human
{
    public:
    Human(const string& name, int age) :m_name(name), m_age(age), m_IDnum(1234)
    {}
    void eat(const string& food)
    {
        cout << "我在吃" << food << endl;
    }
    void sleep(int time)
    {
        cout << "我睡了" << time << "个小时" << endl;
    }

protected://保护型的成员可以在子类中被访问
    string m_name;
    int m_age;
    //私有成员子类中无法访问

    //但是可以提供保护的接口函数来间接访问
    int getIDnum()
    {
        return m_IDnum;
    }
private:
    int m_IDnum;//身份证号
};

//教师类(人类的一个子类)
class Teacher :private Human
{
public:
    //Human(...)说明从基类中继承来的成员的初始化方式
    Teacher(const string& name, int age, double salary) :Human(name, age),m_salary(salary) {}
    void teach(const string& course) {
        cout << "我在教" << course << endl;
    }
    void who(void) {
        cout << "我叫" << m_name << ",今年" << m_age <<
        "岁,工资是" << m_salary << endl;
        cout << "身份证号" << getIDnum() << endl;
    }

private:
    double m_salary;
};

int main()
{
    Teacher tea("鹿岭", 19, 2800);
    tea.who();
    //tea.eat("香烟捞饭");//私有继承时,基类公有成员在子类中变为私有,因此不可访问
    //tea.sleep(8);
    tea.teach("C++");
    //tea.getIDnum();//私有继承时,基类保护成员在子类中变为私有,因此不可访问
    return 0;
}

protected:保护继承
○ 基类的公有成员通过保护继承成为了派生类自己的保护成员
○ 基类的私有成员通过保护继承成为了派生类的一部分,但是派生类不可以直接访问它

cpp
#include <iostream>
using namespace std;

class Human
{
    public:
    Human(const string& name, int age) :m_name(name), m_age(age), m_IEnum(1234)
    {
    void eat(const string& food)
    {
        cout << "我在吃" << food << endl;
    }
    void sleep(int time)
    {
        cout << "我睡了" << time << "个小时" << endl;
    }

protected://保护型的成员可以在子类中被访问
    string m_name;
    int m_age;
    //私有成员子类中无法访问

    //但是可以提供保护的接口函数来间接访问
    int getIDnum()
    {
        return m_IEnum;
    }
private:
    int m_IEnum;//身份证号
};

//明显类(人类的一个子类)
class Star :protected Human
{
    public:
    //Human(...)说明从基类中继承来的成员的初始化方式
    Star(const string& name, int age, const string& label) :Human(name, age), m_label(label) {}
    void act(const string& movie) {
        cout << "我在演" << movie << endl;
    }
    void who(void) {
        cout << "我叫" << m_name << ",今年" << m_age <<
        "岁,标签是" << m_label << endl;
        cout << "身份证号" << getIDnum() << endl;
    }

private:
    string m_label;//标签
};

int main()
{
    Star star("吴京", 35, "硬汉");
    star.who();
    //star.eat("香翅捞饭"); //保护继承时,基类公有成员在子类中变为保护,因此不可访问
    //star.sleep(8);
    star.act("战狼3");
    //star.getIDnum(); //保护继承时,基类公有成员在子类中变为保护,因此不可访问

    return 0;
}
  • 关于私有成员: private 成员在子类中任存存在,但是却无法直接访问,无论哪一种继承方式派生类都不能直接使用基类的私有成员
  • 派生类占用的空间的大小,等于基类占用的存储空间大小,加上派生类自己的成员大小(派生类中拥有一个基类子对象,考虑字节对齐)
cpp
#include <iostream>
using namespace std;

class A
{
    void fun(){}
    int a;
    float b;
    char c;
    double d;
};

class B : public A
{
    char c;
    double d;
};

class C
{
};

class D : public C
{
};

int main()
{
    cout << "sizeof(A) = " << sizeof(A) << endl;//24
    cout << "sizeof(B) = " << sizeof(B) << endl;//40
    cout << "sizeof(C) = " << sizeof(C) << endl;//1
    cout << "sizeof(D) = " << sizeof(D) << endl;//1
    return 0;
}

1.3 公有继承的特性

1.3.1 子类继承基类成员

子类对象会继承基类属性的行为,通过子类对象可以访问基类中的成员,如同是基类对象在访问他们一样。

  • 一个派生类对象,从整体上看,由两部分组成:
    • 从基类继承过来的成员("可以理解为一个整体,是一个基类子对象")
    • 派生类自己新增的成员
  • 在类中,可以直接访问基类中的公有或保护成员,如同它们是子类自己的成员一样
  • 基类中的私有成员子类也可以继承过来,但是会受到访问控制属性的限制,无法直接访问,如果子类需要访问基类中的私有成员,可以通过基类提供的公有或保护的接口函数来间接访问
  • 基类的构造函数和析构函数,子类是无法继承的,但是可以在子类的构造函数使用初始化表显示声明基类部分的初始化方式
cpp
#include <iostream>
using namespace std;

class Base
{
    public:
    void fun_pub()
    {
        cout << "public function of Base" << endl;
    }
    protected:
    Base(int i = 0, double d = 0, string str = "") : m_i(i), m_d(d), m_str(str)
    {
    }
    void fun_pro()
    {
        cout << "protected function of Base" << endl;
    }
    private:
    void fun_pri()
    {
        cout << "private function of Base" << endl;
    }
    public:
    int m_i;
    protected:
    double m_d;
    private:
    string m_str;
};

class Derived : public Base
{
    public:
    Derived(int i = 0, double d = 0, string str = "abc") : Base(i, d, str)
    {
    }
    void print()
    {
        cout << "---print function of Derived---" << endl;
        fun_pub();
        fun_pro();
        //Base b;
        //cout << m_i << m_d /*<< m_str*/ << endl;
        //fun_pri();
    }
};

int main()
{
    //Base b;
    Derived d;
    d.print();
    return 0;
}
1.3.2 向上造型

将子类类型的指针或引用转换成基类类型的指针或引用

  • 由于在派生类中,含有基类的所有成员,所以,我们能够把公有继承的派生类对象当成基类对象来使用,具体表现在:
    • 基类的引用可以绑定到派生类对象
    • 基类的指针可以指向派生类对象
    • 可以使用派生类对象初始化基类对象
    • 可以使用派生类对象给基类对象赋值
cpp
Derived d;
Base &r_b = d; //基类的引用可以绑定到派生类对象
Base* p_b = &d; //基类的指针可以指向派生类对象

Base b(d); //可以使用派生类对象初始化基类对象

Base b1;
b1 = d; //可以使用派生类对象给基类对象赋值
  • 当使用派生类对象得到一个基类对象/引用/指针的时候该基类对象/引用/指针是无法访问到派生类的新增成员的,只能访问到派生类从基类继承过来的成员在编译器看来,基类对象/引用/指针的类型仍然是基类!!!

这种操作性缩小的类型转换在编译器看来是安全的,可以直接隐式完成转换

cpp
Derived d;
Base &r_b = d;
r_b.fun_pub();
//r_b.print(); //error 其本质还是基类类型
Base* p_b = &d;
//p_b->print(); //error 其本质还是基类类型

Base b(d);
//b.print(); //error 其本质还是基类类型

Base b1;
b1 = d;
//b1.print(); //error 其本质还是基类类型
1.3.3 子类隐藏基类的成员
  • 子类和基类中有同名的成员函数,因为作用域不同,不会有重载关系,而是一种隐藏关系,如果需要访问基类中隐藏的成员函数,可以通过在子类中加"基类类名..."显示指明
  • 如果隐藏的成员函数满足同名不同参重载条件,也可以通过using声明的方式,将基类的成员函数引入到子类的作用域,让它们形成重载关系,通过重载匹配来解决/不推荐
cpp
#include<iostream>
using namespace std;

class Base
{
public:
    void foo(void) //基类foo函数
    {
        cout << "Base::foo" << endl;
    }
};

class Derived :public Base
{
public:
    void foo(int i) //子类foo函数
    {
        cout << "Derived::foo" << endl;
    }
    //将基类中的foo函数引入当前子类作用域
    using Base::foo;
};

int main() {
    Derived d;
    d./*Base::*/foo();//
    d.Derived::foo(10);//可形成有效重载
    return 0;
}

1.4 子类的构造和析构函数

1.4.1 子类的构造函数
  • 子类对象的构造过程
    • 分配内存
    • 基类构造函数调用(按继承表顺序)
    • 成员变量初始化(按声明顺序)
    • 执行子类的构造函数代码
cpp
#include<iostream>
using namespace std;

class Base
{
    public:
    Base(void) : m_i(0)
    {
        cout << "Base::Base(void)" << endl;
    }
    int m_i;
};

class Derived :public Base
{
    public:
    Derived(void) : m_i1(0)//没有指明基类部分(基类子对象)的初始化方式
    {
        cout << "Derived::Derived(void)" << endl;
    }
    int m_i1;
};

int main()
{
    Derived d1;
    cout << d1.m_i << endl;//0
    cout << d1.m_i1 << endl;//0
    return 0;
}
  • 如果子类构造函数没有指明基类部分(基类对象的初始化方式,那么编译器会自动调用基类的无参构造函数来初始化)
  • 如果希望基类对象以有参的方式被初始化,需要在子类构造函数的初始化表中指明其初始化方式
cpp
#include<iostream>
using namespace std;

class Base
{
    public:
    Base(void) :m_i(0)
    {
        cout << "Base::Base(void)" << endl;
    }
    Base(int i) :m_i(i)
    {
        cout << "Base::Base(int)" << endl;
    }
    int m_i;
};

class Derived :public Base
{
public:
    Derived(void) //没有指明基类部分(基类子对象)的初始化方式
    {
        cout << "Derived::Derived(void)" << endl;
    }
    //Base(i):指明基类子对象的初始化方式
    Derived(int i) :Base(i)
    {
        cout << "Derived::Derived(int)" << endl;
    }
};

int main()
{
    Derived d1;
    cout << d1.m_i << endl;//0
    Derived d2(123);
    cout << d2.m_i << endl;//123
    return 0;
}
1.4.2 子类的析构函数
  • 子类对象的析构过程
    • 执行子类析构函数代码
    • 析构子类对象(按声明的逆序)
    • 析构基类对象(按继承表的逆序)
    • 释放内存
cpp
#include<iostream>
using namespace std;

class Base
{
public:
    Base(void) : m_i(0)
    {
        cout << "Base::Base(void)" << endl;
    }
    ~Base()
    {
        cout << "Base::~Base()" << endl;
    }
    int m_i;
};

class Derived :public Base
{
public:
    Derived(void) : m_i1(0)//没有指明基类部分(基类子对象)的初始化方式
    {
        cout << "Derived::Derived(void)" << endl;
    }
    ~Derived()
    {
        cout << "Derived::~Derived()" << endl;
    }
    int m_i1;
};

int main()
{
    Derived d1;
    cout << d1.m_i << endl; //0
    cout << d1.m_i1 << endl; //0
    return 0;
}

问题:为什么先析构子类对象然后析构基类对象?

资源释放的顺序:子类可能依赖于基类提供的某些资源或状态。如果首先析构基类,那么这些资源或状态可能在于类析构函数需要时已经不存在,从而导致未定义的行为或错误。因此,先析构子类可以确保子类在析构时能够正确地使用基类提供的资源。

依赖关系:在面向对象的设计中,子类常常依赖于基类来提供某些功能或状态。这种依赖关系意味着子类在使用完基类提供的功能后需要负责清理,然后基类再进行最后的清理工作。

防止悬挂指针和引用:如果基类在子类之前被析构,那么任何在子类中指向基类成员的指针或引用都可能变成悬挂的(即指向已经被释放的内存),这会导致未定义的行为。

总结:子类的构造/析构函数都会调用基类的构造/析构函数,但是基类的构造/析构不会调用子类的构造/析构

cpp
#include<iostream>
using namespace std;

class Base
{
public:
    Base(void) : m_i(0)
    {
        cout << "Base::Base(void)" << endl;
    }
    ~Base()
    {
        cout << "Base::~Base()" << endl;
    }
    int m_i;
};

class Derived : public Base
{
public:
    Derived(void) 
    {
        cout << "Derived::Derived(void)" << endl;
    }
    ~Derived()
    {
        cout << "Derived::~Derived()" << endl;
    }
    int m_i1;
};

int main()
{
    Base b;
    return 0;
}
  • 基类的析构函数不会自动调用子类的析构函数,如果对一个指向子类对象的基类指针使用delete操作符,实际被执行的仅是基类的析构函数,子类的析构函数执行不到,有内存泄露的风险
cpp
#include<iostream>
using namespace std;

class Base
{
public:
    Base(void) : m_i(0)
    {
        cout << "Base::Base(void)" << endl;
    }
    ~Base()
    {
        cout << "Base::~Base()" << endl;
    }
    int m_i;
};

class Derived : public Base
{
public:
    Derived(void)
    {
        cout << "Derived::Derived(void)" << endl;
        // 动态分配资源
        m_dynamicArray = new int[10];
    }
    ~Derived()
    {
        cout << "Derived::~Derived()" << endl;
        // 释放动态分配的资源
        delete[] m_dynamicArray;
    }
    int* m_dynamicArray; // 动态分配的整数数组
    int m_i1;
};

int main()
{
    Base* pb = new Derived; // 分配Derived对象
    delete pb; // 通过基类指针释放对象,但由于Derived的析构函数没有释放资源,发生内存泄露
    return 0;
}

1.5 子类的拷贝构造和拷贝赋值函数

在C++中子类不会子类(派生类)不会自动调用基类(父类)的拷贝构造函数或拷贝赋值运算符。

这是因为拷贝构造和拷贝赋值操作是针对对象的整体而言的,而不是仅仅针对对象的某一部分(即基类部分)。当创建一个新的子类对象作为现有子类对象的副本时,你需要显式地处理基类的拷贝。

如果你不提供自定义的拷贝构造函数或拷贝赋值运算符在子类中,编译器会为你生成默认的版本。这些默认的版本会执行成员到成员的拷贝,包括基类的部分。然而,如果基类有自定义的拷贝构造函数或拷贝赋值运算符,并且你需要这些自定义行为在子类中发生,你就需要在子类中显式地调用它们。

cpp
//拷贝构造
class Base[...];
class Derived{
public:
    //Base(that):指明基类子对象以拷贝方式来初始化
    Derived(const Derived &that):Base(that),...{}
};

//拷贝赋值
class Base[...];
class Derived:public Base{
    Derived &operator=(const Derived &that){
    ...
    //显式调用基类的拷贝赋值函数
    Base::operator=(that);
    }
};

1.6 子类的移动构造和移动赋值函数

在C++中,子类(派生类)不会自动调用基类(父类)的移动构造函数或移动赋值运算符。与拷贝构造函数和拷贝赋值运算符类似,这些操作也是针对对象的整体而言的。当你想通过移动一个现有对象来构造或赋值另一个对象时,你需要在子类中显式地处理基类的移动。

如果基类有自定义的移动构造函数或移动赋值运算符,并且你希望这些自定义行为在子类中发生,你需要在子类的相应函数中显式地调用它们。以下是如何在子类的移动构造函数和移动赋值运算符中显式调用基类的相应函数的示例:

cpp
//移动构造
class Base[...];
class Derived{
public:
    //Base(std::move(other)):// 显式调用基类的移动构造函数
    Derived(Derived&& other) noexcept : Base(std::move(other)) ...{}
};

//移动赋值
class Base[...];
class Derived:public Base{
    Derived& operator=(Derived&& other) noexcept{
    ...
    //显式调用基类的移动赋值运算符
    Base::operator=(std::move(other));
    }
};

知识如风,常伴吾身