继承的基本概念

在面向对象编程(OOP)中,继承(Inheritance) 是一种允许一个类(称为派生类 / 子类)复用另一个类(称为基类 / 父类)的属性和方法,并可以在此基础上添加新特性或修改现有特性的机制。它是面向对象的三大核心特性(封装、继承、多态)之一,核心目的是代码复用建立类之间的层次关系

继承的核心概念

  • 基类(Base Class):被继承的类,也称为父类或超类(Superclass),包含可复用的属性和方法。
  • 派生类(Derived Class):继承基类的类,也称为子类(Subclass),可以继承基类的成员(成员变量和成员函数),并可新增自己的成员或重写基类的方法。
  • 继承关系:派生类与基类之间形成 “is-a”(是一个)的逻辑关系。例如,“狗” 是 “动物” 的一种,因此Dog类可以继承Animal类。

继承的基本语法

// 基类:动物
class Animal {
public:
    void eat() { cout << "动物吃东西" << endl; }
    void sleep() { cout << "动物睡觉" << endl; }
protected:
    string name; // 受保护的成员,派生类可访问
};

// 派生类:狗(继承自动物)
class Dog : public Animal { // 公有继承
public:
    void bark() { cout << "狗叫:汪汪" << endl; } // 新增方法
    void setName(string n) { name = n; } // 访问基类的protected成员
};

在这个例子中:

  • Dog类通过public继承方式继承了Animal类的eat()sleep()方法和name成员。
  • Dog类新增了自己的方法bark(),并能访问基类的protected成员name

继承的主要作用

  1. 代码复用
    派生类无需重复编写基类中已有的代码,直接继承并使用基类的成员,减少冗余。例如,DogCat等类都能复用Animal类的eat()sleep()方法。

  2. 建立类层次
    通过继承可以构建类的层次结构,清晰表达类之间的关系。例如:
    生物 → 动物 → 哺乳动物 → 狗
    上层类更通用,下层类更具体。

  3. 支持多态
    继承是多态的基础。派生类可以重写(override)基类的方法,使得不同派生类对同一方法有不同实现,通过基类指针 / 引用调用时能动态选择具体实现。

继承的类型

根据继承方式的不同,基类成员在派生类中的访问权限会发生变化(如前文 “访问权限交集” 规则),主要有 3 种继承方式:

  • 公有继承(public):基类的public成员在派生类中仍为publicprotected成员仍为protected(最常用)。
  • 保护继承(protected):基类的publicprotected成员在派生类中均变为protected
  • 私有继承(private):基类的publicprotected成员在派生类中均变为private
基类成员原权限 公有继承(public) 保护继承(protected) 私有继承(private)
public public(取更宽松) protected private
protected protected protected private
private 不可访问(基类私有成员任何继承方式都无法在派生类外访问) 不可访问 不可访问

继承方式相当于 “权限上限”,基类成员原权限若高于继承方式的权限,则会被 “降级” 到继承方式的权限;若低于或等于,则保持原权限。最终权限是两者中更严格的那个(private > protected > public)。

总结:派生类的访问权限如下:

  1. 不管什么继承方式,派生类内部都不能访问基类的私有成员;

  2. 不管什么继承方式,派生类内部除了基类的私有成员不可以访问,其他的都可以访问;

  3. 不管什么继承方式,派生类对象在类外除了公有继承基类中的公有成员可以访问外,其他的都不能访问。

保护继承和私有继承之间有什么区别呢?

以三层继承为例,如果中间层采用保护继承的方式继承顶层基类,那么在底层派生类中也能访问到顶层基类的公有成员和保护成员。

如果中间层采用私有继承的方式继承顶层基类,那么底层派生类中对顶层基类的任何成员都无法访问了。

继承关系的局限性

创建、销毁的方式不能被继承 —— 构造、析构

复制控制的方式不能被继承 —— 拷贝构造、赋值运算符函数

空间分配的方式不能被继承 —— operator new 、 operator delete

友元不能被继承(友元破坏了封装性,为了降低影响,不允许继承)

单继承下派生类对象的创建和销毁

有这样一种说法:创建派生类对象时,先调用基类构造函数,再调用派生类构造函数,对吗?

错误,创建派生类对象,一定会先调用派生类的构造函数,在此过程中会先去调用基类的构造

来看下面这段代码

#include <iostream>
using std::cout;
using std::endl;
class Base{
public:
    Base(){
        cout << "Base()" << endl;
    }
    ~Base(){
        cout << "~Base()" << endl;
    }

private:
    int _a = 10;
    int _b = 20;
};
class Derived : public Base{
public:
    Derived(){
        cout << "Derived()" << endl;
    }
    ~Derived(){
        cout << "~Derived()" << endl;
    }
private:
    int _c = 30;
    int _d = 40;
};


void test(){
    Derived d;
    cout << sizeof(Base) << endl;//8
    cout << sizeof(Derived) << endl;//16
    Derived * p = &d;
    int * pInt = (int *)p;
    cout << *pInt << endl;//10
    cout << *(pInt + 1) << endl;//20
    cout << *(pInt + 2) << endl;//30
    cout << *(pInt + 3) << endl;//40
}

int main()
{
    test();
    return 0;
}

这段代码直观地证明了:

  • 继承时,派生类对象的内存包含基类成员(在前)和自身成员(在后)。派生类对象的内存空间是基类成员 + 自身成员的总和,基类成员在派生类对象中占据独立的内存空间。
  • 构造 / 析构顺序严格遵循 “基类先构造,派生类后构造;派生类先析构,基类后析构”。创建派生类对象会马上调用派生类的构造函数,但在初始化列表的最开始调用基类的构造函数, 可以理解为派生对象中包含了一个基类对象
  • 访问控制不改变成员的内存存在性,仅限制编译期的访问权限。

创建派生类对象时调用基类构造的机制

语法规则

1.当派生类中没有显式调用基类构造函数时,会自动调用基类的默认无参构造(或者所有参数都有默认值的有参构造);

2.此时如果基类中没有默认无参构造,Derived类的构造函数的初始化列表中也没有显式调用基类构造函数,编译器会报错

——不允许派生类对象的创建;

3.当派生类对象调用基类构造时,希望使用非默认的基类构造函数,必须显式地在初始化列表中写出。

#include <iostream>
using std::cout;
using std::endl;

class Base{

public:
    Base(int baseNumber)
    :_baseNumber(baseNumber)
    {
        cout << "Base(int)" << endl;
    }

private:
    int _baseNumber;
};

class ThirdObject{

public:
    ThirdObject()
    {
        cout << "ThirdObject(int)" << endl;
    }

};

class Derived :public Base{

public:
    Derived(int derivedNumber, int thirdNumber, int baseNumber)
    :Base(baseNumber)
    ,_derivedNumber(derivedNumber)
    ,_base(baseNumber)
    {
        cout << "Derived(int,int,int)" << endl;
    }

private:

    ThirdObject _thirdObj;
    //持有基类对象成员
    Base _base;

    int _derivedNumber;
};

void test(){
    Derived d(1, 2, 3);
}

int main()
{
    test();
    return 0;
}

output

Base(int)          // 基类Base的构造(因Derived继承自Base)
ThirdObject(int)   // 成员对象ThirdObject的构造
Base(int)          // 成员对象Base(_base)的构造
Derived(int,int,int) // 派生类Derived自身的构造

这段代码的核心作用是演示对象构造的优先级顺序
基类构造 → 成员对象构造(按声明顺序) → 派生类自身构造

派生类对象的销毁

当派生类析构函数执行完毕之后,会自动调用基类析构函数,完成基类部分所需要的销毁(回收数据成员申请的堆空间资源)。

记忆:创建一个对象,一定会马上调用自己的构造函数;一个对象被销毁,也一定会马上调用自己的析构函数。

当派生类对象中包含对象成员        

如果此时派生类对象中包含对象成员,那么此时的调用顺序满足:

  • 1.完成派生类对象所占内存空间的开辟
  • 2.调用基类的构造函数,完成从基类吸收的数据成员的初始化
  • 3.完成子对象、const数据成员、引用数据成员的初始化
  • 4.执行派生类构造函数的函数体

对基类成员的隐藏

派生类中声明了和基类的数据成员同名的数据成员,就会对基类的这个数据成员形成隐藏。

派生类对象无法通过数据成员的名字直接访问基类的这个数据成员。

1. 成员变量的隐藏

当派生类定义了与基类同名的成员变量时,基类的同名变量会被隐藏。派生类中直接访问该变量时,默认访问的是派生类自己的变量;若要访问基类的同名变量,需用 基类名:: 限定。

示例:

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        int x = 10; // 基类的x
    };
    
    class Derived : public Base {
    public:
        int x = 20; // 派生类的x,隐藏基类的x
    };
    
    int main() {
        Derived d;
        cout << d.x << endl;       // 输出20(访问派生类的x)
        cout << d.Base::x << endl; // 输出10(显式访问基类的x)
        return 0;
    }

    如果一定要访问基类的这个数据成员,需要加上作用域,但是这种写法不符合面向对象的原则,不推荐实际使用

    2. 成员函数的隐藏

    当派生类定义了与基类同名的成员函数时,无论参数列表是否相同,基类的同名函数都会被隐藏。

    (1)函数名相同,参数列表不同(隐藏)
    class Base {
    public:
        void func(int a) { // 基类的func(带int参数)
            cout << "Base::func(int): " << a << endl;
        }
    };
    
    class Derived : public Base {
    public:
        void func() { // 派生类的func(无参数),隐藏基类的func(int)
            cout << "Derived::func()" << endl;
        }
    };
    
    int main() {
        Derived d;
        d.func();       // 正确:调用派生类的func()
        // d.func(10);  // 错误:基类的func(int)被隐藏,无法直接调用
        d.Base::func(10); // 正确:显式调用基类的func(int)
        return 0;
    }

    派生类对基类的成员函数构成隐藏,只需要派生类中定义一个与基类中成员函数同名的函数即可(函数的返回类型、参数情况都可以不同,依然能隐藏)。

    (2)函数名相同,参数列表相同(非虚函数时为隐藏,虚函数时为覆盖)
    • 若基类函数不是虚函数:派生类同名同参数函数会隐藏基类函数。
    • 若基类函数是虚函数:派生类同名同参数函数会重写(覆盖) 基类函数(多态的基础)。
    class Base {
    public:
        void func() { cout << "Base::func()" << endl; } // 非虚函数
        virtual void vfunc() { cout << "Base::vfunc()" << endl; } // 虚函数
    };
    
    class Derived : public Base {
    public:
        void func() { cout << "Derived::func()" << endl; } // 隐藏基类func()
        void vfunc() { cout << "Derived::vfunc()" << endl; } // 重写基类vfunc()
    };
    
    int main() {
        Derived d;
        Base* b = &d;
        
        b->func();  // 输出Base::func()(隐藏:非虚函数,按指针类型调用)
        d.func();   // 输出Derived::func()(直接访问派生类)
        
        b->vfunc(); // 输出Derived::vfunc()(重写:虚函数,按对象类型调用)
        return 0;
    }

    隐藏的核心特点

    1. 触发条件:派生类成员与基类成员同名(与参数、返回值无关,虚函数重写是特殊情况)。
    2. 访问规则:派生类中直接访问同名成员时,默认指向派生类自己的;访问基类同名成员需用 基类名:: 限定。
    3. 与重写的区别:重写仅针对虚函数且要求签名完全一致,隐藏则对所有同名成员生效。

    总结

    继承中的隐藏是编译器的名字查找规则导致的:当在派生类中查找某个名称时,编译器会先在派生类内部查找,找到后就停止向上查找基类的同名成员。这一机制可能导致意外屏蔽基类成员,因此实际开发中应尽量避免在派生类中定义与基类同名的成员(除非有意隐藏)。

    多继承

    在 C++ 中,多继承(Multiple Inheritance) 是指一个派生类可以同时继承多个基类的特性(成员变量和成员函数)。与单一继承(一个类只继承自一个基类)相比,多继承能更灵活地组合不同类的功能,但也会带来一些复杂性。

    多继承的基本语法

    派生类声明时,在基类列表中用逗号分隔多个基类,语法如下:

    class Derived : access-specifier Base1, access-specifier Base2, ... {
        // 派生类成员
    };
    

    其中 access-specifier 是继承方式(publicprotectedprivate),每个基类可以指定独立的继承方式。

    多继承的示例

    假设我们有两个基类 Student(学生)和 Worker(工人),可以通过多继承创建 StudentWorker(工读生)类,同时拥有两者的特性:

    #include <iostream>
    #include <string>
    using namespace std;
    
    // 基类1:学生
    class Student {
    protected:
        string _school; // 学校
    public:
        Student(string school) : _school(school) {}
        void study() { cout << "在" << _school << "学习" << endl; }
    };
    
    // 基类2:工人
    class Worker {
    protected:
        string _company; // 公司
    public:
        Worker(string company) : _company(company) {}
        void work() { cout << "在" << _company << "工作" << endl; }
    };
    
    // 派生类:工读生(同时继承学生和工人)
    class StudentWorker : public Student, public Worker {
    private:
        string _name; // 姓名
    public:
        // 初始化列表需同时初始化所有基类
        StudentWorker(string name, string school, string company)
            : Student(school), Worker(company), _name(name) {}
        
        void introduce() {
            cout << "我是" << _name << "," << endl;
            study(); // 调用Student的方法
            work();  // 调用Worker的方法
        }
    };
    
    int main() {
        StudentWorker sw("小明", "清华大学", "字节跳动");
        sw.introduce();
        return 0;
    }
    

    输出结果:

    我是小明,
    在清华大学学习
    在字节跳动工作
    

    这个例子中,StudentWorker 同时继承了 Student 的 study() 方法和 Worker 的 work() 方法,实现了功能的组合。

    多继承的问题与解决

    多继承虽然灵活,但会引入一些特殊问题:

    1. 菱形继承(钻石问题)

    当两个基类继承自同一个间接基类,而派生类同时继承这两个基类时,会形成 “菱形” 结构,导致间接基类的成员在派生类中存在多份拷贝,引发二义性。

    class A
    {
    public:
        void print() const{
            cout << "A::print()" << _a << endl;
        }
        double _a;
    };
    
    class B
    : public A
    {
    public:
        double _b;
    };
    
    class C
    : public A
    {
    public:
        double _c;
    };
    
    class D
    : public B
    , public C
    {
    public:
        double _d;
    };

    菱形继承情况下,D类对象的创建会生成一个B类子对象,其中包含一个A类子对象;还会生成一个C类子对象,其中也包含一个A类子对象。所以D类对象的内存布局中有多个A类子对象,访问继承自A的成员时会发生二义性(无论是否涉及A类的数据成员,单纯访问A类的成员函数也会冲突)

    因为编译器需要通过基类子对象去调用,但是不知道应该调用哪个基类子对象的成员函数。

    解决方法:使用虚继承(Virtual Inheritance)
    在基类继承间接基类时,用 virtual 关键字声明,确保间接基类在派生类中只存在一份拷贝:

    class A
    {
    public:
        void print() const{
            cout << "A::print()" << endl;
        }
        double _a;
    };
    
    class B
    : virtual public A
    {
    public:
        double _b;
    };
    
    class C
    : virtual public A
    {
    public:
        double _c;
    };
    
    class D
    : public B
    , public C
    {
    public:
        double _d;
    };

    采用虚拟继承的方式处理菱形继承问题,实际上改变了派生类的内存布局。B类和C类对象的内存布局中多出一个虚基类指针,位于所占内存空间的起始位置,同时继承自A类的内容被放在了这片空间的最后位置。D类对象中只会有一份A类的基类子对象。

    2. 成员名冲突

    当多个基类拥有同名成员时,派生类访问该成员会产生二义性,需显式指定来源。

    class A {
    public:
        void func() { cout << "A::func()" << endl; }
    };
    
    class B {
    public:
        void func() { cout << "B::func()" << endl; }
    };
    
    class C : public A, public B {
    public:
        void callFunc() {
            A::func(); // 显式调用A的func()
            B::func(); // 显式调用B的func()
            // func(); // 错误:二义性
        }
    };
    

    多继承的适用场景

    多继承适合 “组合多个独立功能” 的场景,例如:

    • 一个类需要同时具备多个不相关类的特性(如 “工读生” 同时具备 “学生” 和 “工人” 的特性)。
    • 实现 “接口组合”(在 C++ 中,接口通常是纯虚函数类,多继承多个接口可以组合不同的行为规范)。

    总结

    多继承的核心是让派生类同时拥有多个基类的功能,但也可能带来菱形继承(需用虚继承解决)和成员名冲突(需显式指定来源)等问题。实际开发中应谨慎使用多继承,优先考虑组合(将多个类作为成员对象)而非继承,以降低代码复杂度。

    基类与派生类之间的转换

    一般情况下,基类对象占据的空间小于派生类对象。

    (空继承时,有可能相等)

    1:可否把一个基类对象赋值给一个派生类对象?可否把一个派生类对象赋值给一个基类对象?

    2:可否将一个基类指针指向一个派生类对象?可否将一个派生类指针指向一个基类对象?

    3:可否将一个基类引用绑定一个派生类对象?可否将一个派生类引用绑定一个基类对象?

    引用和指针同理

    派生类对象间的复制控制(重点)

    复制控制函数就是 拷贝构造函数、赋值运算符函数

    原则:基类部分与派生类部分要单独处理

    (1)当派生类中没有显式定义复制控制函数时,就会自动完成基类部分的复制控制操作;

    (2)当派生类中有显式定义复制控制函数时,不会再自动完成基类部分的复制控制操作,需要显式地调用;

    如果Derived类的数据成员申请了堆空间,那么必须手动写出Derived类的复制控制函数,此时就要考虑到基类的复制控制函数的显式调用。

    (如果只是Base类的数据成员申请了堆空间,那么Base类的复制控制函数必须显式定义,Derived类自身的数据成员如果没有申请堆空间,不用显式定义复制控制函数)

    #include <string.h>
    #include <iostream>
    using std::ostream;
    using std::cout;
    using std::endl;
    //继承体系下的复制控制函数
    //这部分代码也需要会写,和之前学习的赋值运算符拷贝构造函数
    //同等的地位
    class Base{
    public:
        Base(const char * p)
        :_pbase(new char[strlen(p) + 1]())
        {
            cout << "Base()" << endl;
            strcpy(_pbase, p);
        }
    
        ~Base(){
            cout << "~Base()" << endl;
            if(_pbase){
                delete [] _pbase;
                _pbase = nullptr;
            }
        }
        //拷贝构造函数
        Base(const Base & rhs)
        :_pbase(new char[strlen(rhs._pbase) + 1]())
        {
            cout << "Base(const Base &)" << endl;
            strcpy(_pbase, rhs._pbase);
        }
        //赋值运算符函数
        Base & operator=(const Base & rhs){
            cout << "Base::operator=" << endl;
            if(this != &rhs){
                delete [] _pbase;
                _pbase = new char[strlen(rhs._pbase) + 1]();
                strcpy(_pbase, rhs._pbase);
            }
            return *this;
        }
    
    //private:
    protected:
        //采取深拷贝
        char * _pbase;
    };
    //第二阶段:派生类中定义一个指针数据成员
    class Derived : public Base{
    public:
        Derived(const char * base, const char * derived)
        :Base(base)
        ,_pderived(new char[strlen(derived) + 1]())
        {
            strcpy(_pderived, derived);
            cout << "Derived(const char *)" << endl;
        }
        ~Derived(){
            cout << "~Derived" << endl;
            if(_pderived){
                delete [] _pderived;
                _pderived = nullptr;
            }
        }
        //需要显示写出派生类的拷贝构造函数和赋值运算符函数
        //为什么在派生类的拷贝构造函数中,会去调用基类的无参构造函数呢?
        //在没有显示写出派生类的拷贝构造函数时,编译器会自动帮助
        //我们调用基类的拷贝构造函数完成对应的操作
        //但是一旦我们显示写出派生类的拷贝构造函数,但是却没有主动调用基类的
        //拷贝构造函数时,那么只能够调用基类的无参构造函数来进行初始化
    #if 0
        Derived(const Derived & rhs)
        :Base(rhs)//调用基类的拷贝构造函数,向上转型
        ,_pderived(new char[strlen(rhs._pderived) + 1]())
        {
            strcpy(_pderived, rhs._pderived);
            cout << "Derived(const Derived &)" << endl;
        }
    
    
        //赋值运算符函数
        //一定需要显式调用基类的赋值运算符函数
        Derived & operator=(const Derived & rhs){
            if(this != &rhs){
                //调用基类的赋值运算符函数
                Base::operator=(rhs);
                delete [] _pderived;
                _pderived = new char[strlen(rhs._pderived) + 1]();
                strcpy(_pderived, rhs._pderived);
            }
            return *this;
        }
     #endif   
    
        friend ostream & operator<<(ostream & os, const Derived & rhs);
    private:
        char * _pderived;
    };
        //输出流运算符一般需要加上const
    //1.不会在函数中被修改数据成员
    //2.可以绑定右值,可以输出右值
    //如果需要去访问函数,那么只能够访问const成员函数
    ostream & operator<<(ostream & os, const Derived & rhs){
        if(rhs._pbase){
            os << rhs._pbase;
        }
        if(rhs._pderived){
            os << "," << rhs._pderived;
        }
        return os;
    }
    
    
    void test(){
        Derived d1("hello", "world");
        cout << d1 << endl;
        Derived d2 = d1;
        cout << d2 << endl;
    }
    void test2(){
        Derived d1("hello", "world");
        Derived d2("c++", "python");
        d1 = d2;
        cout << d1 << endl;
    }
    
    int main()
    {
        test();
        return 0;
    }
    
    

    如果派生类没有定义自己的拷贝构造函数,那么对应的内存图示如上;此时派生类部分会采取浅拷贝的方式,但是随后会自动调用基类的拷贝构造函数完成基类部分的处理

    如果派生类拥有自己的指针数据成员,但是同时没有定义赋值运算符函数,那么便会发生上述现象,会存在内存泄漏以及double free的情况(问题是由于派生类部分的浅拷贝导致的,基类的部分是没有问题的)

    Logo

    这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

    更多推荐