云计算百科
云计算领域专业知识百科平台

【C++进阶】继承

  

目录

  ​编辑

本节学习目标

1. 继承的概念及定义

1.1 什么是继承

1.2 继承定义语法

基类成员在派生类的访问规则

1.3 继承模板类(模板父类)

2. 基类与派生类对象转换(切片 / 切割 slicing)

3. 继承体系下作用域与隐藏(name‑hiding)

同名成员变量隐藏示例

同名成员函数隐藏(重点选择题)

4. 派生类的默认 6 个成员函数

派生类默认成员函数执行规则

4.2 禁止类被继承两种写法

5. 继承与友元

6. 继承与静态成员

7. 多继承、菱形继承与虚继承 virtual

7.1 单继承、多继承

7.2 菱形继承(钻石继承)—— 多继承特殊坑

7.3 虚继承 virtual 解决菱形继承

8. 继承与组合

8.1 is‑a vs has‑a

📔本篇核心考点总结

本节学习目标

  • 理解继承的概念,解决代码冗余复用问题,分清父类 (基类)/ 子类 (派生类)
  • 掌握三种继承方式public / protected / private,掌握基类成员在派生类中的访问规则
  • 理解派生类对象切片,基类派生类指针引用转换
  • 继承体系作用域、成员隐藏(name‑hiding),区分隐藏和重载
  • 派生类 6 个默认成员函数:构造、拷贝构造、赋值重载、析构的执行规则
  • final 禁止继承,继承与友元、继承与静态成员
  • 理解多继承、菱形继承带来二义性与数据冗余,掌握虚继承virtual解决菱形问题
  • 分清is‑a继承 和has‑a组合,理解白箱复用、黑箱复用,开发优先组合
  • 1. 继承的概念及定义

    1.1 什么是继承

    继承是面向对象实现代码复用最重要机制。 在保留原有类(基类 / 父类)全部特性的基础之上,扩展出新的类(派生类 / 子类)。

    场景:学生 Student、老师 Teacher。两者都包含姓名、地址、电话、年龄、身份认证identity();学生独有学号、学习;老师独有职称、授课。 不使用继承,两份类中重复写相同成员,代码大量冗余。

    把公共属性、方法抽出来放到Person父类;Student、Teacher继承 Person,直接复用父类成员,只写自己独有的内容。

    #include <iostream>
    #include <string>
    using namespace std;

    //父类(基类)
    class Person
    {
    public:
    void identity()
    {
    cout << "void identity() " << _name << endl;
    }
    protected:
    string _name = "张三";
    string _address;
    string _tel;
    int _age = 18;
    };

    //子类(派生类) public继承Person
    class Student : public Person
    {
    public:
    void study()
    { }
    protected:
    int _stuid; //学号
    };

    class Teacher : public Person
    {
    public:
    void teaching()
    { }
    protected:
    string _title; //职称
    };

    int main()
    {
    Student s;
    Teacher t;
    s.identity(); //复用父类方法
    t.identity();
    return 0;
    }

    1.2 继承定义语法

    class 派生类名 : 继承方式 基类名
    {
    //派生类自己新增的成员
    };

    继承三种方式:public、protected、private

    基类成员在派生类的访问规则
  • 基类的 private 私有成员:无论哪种继承方式,在派生类内部都不可见。
  • 注意:不是没有继承到子类对象中,只是语法禁止访问,对象内存里面依然存在这份数据。 如果希望子类可以访问,但是外部不能访问,成员设置为protected,protected 就是为继承场景诞生。

  • 简单口诀:派生类中访问权限 = min(基类成员权限,继承方式) 权限优先级:public > protected > private
  • 表格

    基类成员public 继承protected 继承private 继承
    public public protected private
    protected protected protected private
    private 不可见 不可见 不可见
  • 语法细节:
    • 使用class定义类,默认继承方式是 private;
    • 使用struct定义类,默认继承方式是 public;
    • ✅工程开发绝大多数场景只用public继承;protected/private继承极少使用,扩展性差。

    示例演示三种继承:

    class Person
    {
    public:
    void Print() { cout << _name << endl; }
    protected:
    string _name;
    private:
    int _age;
    };

    // class Student : protected Person;
    // class Student : private Person;
    class Student : public Person
    {
    protected:
    int _stunum;
    };

    1.3 继承模板类(模板父类)

    如果父类是类模板,子类继承的时候,必须带上基类模板域,否则编译器找不到基类成员。 例:stack 继承 vector,模拟栈。

    namespace bit
    {
    template<class T>
    class stack : public std::vector<T>
    {
    public:
    void push(const T& x)
    {
    // 必须写 vector<T>::push_back(x);
    // push_back(x); //直接写会编译报错,模板按需实例化找不到名字
    vector<T>::push_back(x);
    }
    void pop()
    {
    vector<T>::pop_back();
    }
    const T& top()
    {
    return vector<T>::back();
    }
    bool empty()
    {
    return vector<T>::empty();
    }
    };
    }

    int main()
    {
    bit::stack<int> st;
    st.push(1);
    st.push(2);
    st.push(3);
    while(!st.empty())
    {
    cout << st.top() << " ";
    st.pop();
    }
    return 0;
    }

    2. 基类与派生类对象转换(切片 / 切割 slicing)

    只针对public 公有继承

  • 派生类对象可以赋值给基类对象、基类指针、基类引用
  • 切片(切割):把派生类对象里面属于基类的那一部分切出来,交给基类。派生类独有的成员直接被切掉丢弃。

    class Person
    {
    protected:
    string _name;
    string _sex;
    int _age;
    };

    class Student : public Person
    {
    public:
    int _No; //学号
    };

    int main()
    {
    Student sobj;

    //1.派生类 → 基类指针
    Person* pp = &sobj;
    //2.派生类 → 基类引用
    Person& rp = sobj;
    //3.派生类赋值给基类对象(发生对象切片,调用基类拷贝构造)
    Person pobj = sobj;

    // ❌基类对象不能直接赋值给派生类对象,编译报错
    // sobj = pobj;

    return 0;
    }

    • 基类普通对象不能直接赋值给派生类对象。
    • 基类指针如果本来指向派生类对象,可以强制转换为派生类指针;安全转换后面多态章节学习dynamic_cast。

    3. 继承体系下作用域与隐藏(name‑hiding)

  • 基类、派生类拥有相互独立的作用域。
  • 成员隐藏(遮蔽):派生类和基类出现同名成员,派生类成员会屏蔽基类同名成员。
  • 注意:只要函数名字一样就构成隐藏,参数不一样也隐藏,不是重载!重载只发生在同一个作用域!

  • 如果需要在子类访问被隐藏的基类成员,使用 基类::成员名显式指定类域。
  • 工程编码建议:尽量不要写同名成员,可读性差容易踩坑。

    同名成员变量隐藏示例

    class Person
    {
    protected:
    string _name = "小李子";
    int _num = 111; //身份证号
    };

    class Student : public Person
    {
    public:
    void Print()
    {
    cout << "姓名:" << _name << endl;
    cout << "身份证号:" << Person::_num << endl; //显式指定基类域访问被隐藏成员
    cout << "学号:" << _num << endl;
    }
    protected:
    int _num = 999; //学号,和父类_num同名,发生隐藏
    };

    int main()
    {
    Student s1;
    s1.Print();
    return 0;
    }

    同名成员函数隐藏(重点选择题)

    class A
    {
    public:
    void fun()
    {
    cout << "A::fun()" << endl;
    }
    };

    class B : public A
    {
    public:
    //函数名字相同,参数不同,构成【隐藏】,不是重载!
    void fun(int i)
    {
    cout << "B::fun(int i)" << i << endl;
    }
    };

    int main()
    {
    B b;
    b.fun(10); //调用B::fun(int)
    // b.fun(); //编译报错,子类作用域把父类fun()隐藏了!
    b.A::fun(); //想要调用父类版本,必须显式指定类域
    return 0;
    }

    考点:重载必须同一个类同一个作用域;父子类之间同名函数,不管参数是否一样,都是隐藏。

    4. 派生类的默认 6 个成员函数

    编译器在满足条件时自动生成 6 个默认成员函数:构造、拷贝构造、赋值运算符重载、析构、取地址两个重载。

    派生类默认成员函数执行规则

  • 派生类构造函数:初始化列表阶段必须调用基类构造函数初始化继承过来的基类部分; 如果基类没有无参默认构造,派生类必须在初始化列表显式调用对应基类构造。
  • 对象构造顺序:先基类构造,再派生类构造。

  • 派生类拷贝构造:必须调用基类拷贝构造,完成基类部分拷贝。
  • 派生类 operator = 赋值重载:必须调用基类的赋值重载;派生类会隐藏基类的 operator=,需要显式基类::operator=(s)调用。
  • 派生类析构函数:派生类析构执行完毕,编译器自动调用基类析构;
  • 对象析构顺序:先派生类析构,再基类析构,和构造顺序相反。

  • 析构函数名字会被编译器特殊处理为destructor(),基类和派生类析构函数名字不一样,普通继承下构成隐藏,不是重写;多态章节 virtual 虚析构才会重写。
  • 完整示例代码:

    #include <iostream>
    #include <string>
    using namespace std;

    class Person
    {
    public:
    Person(const char* name = "peter")
    :_name(name)
    {
    cout << "Person()" << endl;
    }

    Person(const Person& p)
    :_name(p._name)
    {
    cout << "Person(const Person& p)" << endl;
    }

    Person& operator=(const Person& p)
    {
    cout << "Person operator=(const Person& p)" << endl;
    if(this != &p)
    {
    _name = p._name;
    }
    return *this;
    }

    ~Person()
    {
    cout << "~Person()" << endl;
    }
    protected:
    string _name;
    };

    class Student : public Person
    {
    public:
    //派生类构造:初始化列表调用基类构造
    Student(const char* name, int num)
    : Person(name)
    , _num(num)
    {
    cout << "Student()" << endl;
    }

    //派生类拷贝构造,调用基类拷贝构造
    Student(const Student& s)
    : Person(s)
    , _num(s._num)
    {
    cout << "Student(const Student& s)" << endl;
    }

    Student& operator=(const Student& s)
    {
    cout << "Student& operator=(const Student& s)" << endl;
    if(this != &s)
    {
    //显式调用基类赋值重载,派生类会隐藏基类operator=
    Person::operator=(s);
    _num = s._num;
    }
    return *this;
    }

    ~Student()
    {
    cout << "~Student()" << endl;
    //编译器自动在函数末尾调用 ~Person(),不需要手写
    }
    protected:
    int _num;
    };

    int main()
    {
    Student s1("jack",18);
    Student s2(s1);
    Student s3("rose",17);
    s1 = s3;
    return 0;
    }

    4.2 禁止类被继承两种写法

  • C++11 final关键字修饰类,该类不能被继承(推荐)
  • class Base final
    {
    public:
    void func5() { cout << "Base::func5" << endl; }
    protected:
    int a = 1;
    };
    // class Derive : public Base{}; //编译报错,Base被final禁止继承

  • C++98 旧方式:把基类构造函数设置为 private。派生类实例化对象必须调用基类构造,基类构造私有子类无法访问,子类不能实例化。
  • 5. 继承与友元

    ❗友元关系不会继承。基类的友元函数,不能直接访问派生类的 protected/private 成员。

    #include <iostream>
    #include <string>
    using namespace std;

    class Student;
    class Person
    {
    public:
    friend void Display(const Person& p, const Student& s);
    protected:
    string _name;
    };

    class Student : public Person
    {
    protected:
    int _stuNum;
    };

    void Display(const Person& p, const Student& s)
    {
    cout << p._name << endl;
    // cout << s._stuNum << endl; //编译报错!友元不继承,不能访问Student的_stuNum
    }

    int main()
    {
    Person p;
    Student s;
    // Display(p,s); //报错
    return 0;
    }

    解决:需要把 Display 也设置为 Student 的友元。

    6. 继承与静态成员

    基类里面的 static 静态成员,整个继承体系之内只有唯一一份,所有派生类对象共享同一份静态成员。 可以通过基类名、派生类名访问 public 静态成员。

    #include <iostream>
    #include <string>
    using namespace std;

    class Person
    {
    public:
    string _name;
    static int _count;
    };
    int Person::_count = 0;

    class Student : public Person
    {
    protected:
    int _stuNum;
    };

    int main()
    {
    Person p;
    Student s;

    cout << &p._name << endl; //普通成员每个对象独立一份,地址不同
    cout << &s._name << endl;

    cout << &p._count << endl;
    cout << &s._count << endl; //静态成员地址完全相同,共享同一份

    cout << Person::_count << endl;
    cout << Student::_count << endl;
    return 0;
    }

    7. 多继承、菱形继承与虚继承 virtual

    7.1 单继承、多继承

    • 单继承:一个派生类只有一个直接基类。

    • 多继承:一个派生类拥有两个及以上直接基类。

    对象内存排布:继承列表从左到右,先继承的基类放在内存低地址,后继承放后面,派生类自己成员放最后。

    7.2 菱形继承(钻石继承)—— 多继承特殊坑

    继承关系:Assistant助理同时继承Student和Teacher;Student、Teacher 都继承 Person。

    问题:

  • 数据冗余:Assistant 对象里面会存在两份 Person 基类数据(一份来自 Student 路径,一份来自 Teacher 路径);
  • 二义性:直接访问Assistant._name编译器不知道走哪一条继承路径,编译报错。
  • #include <iostream>
    #include <string>
    using namespace std;

    class Person
    {
    public:
    string _name;
    };

    class Student : public Person
    {
    protected:
    int _num; //学号
    };

    class Teacher : public Person
    {
    protected:
    int _id; //职工编号
    };

    //助理,多继承 Student、Teacher
    class Assistant : public Student, public Teacher
    {
    protected:
    string _majorCourse;
    };

    int main()
    {
    Assistant a;
    // a._name = "peter"; //编译报错,二义性!两份_name不知道选哪一个
    //强制指定路径可以访问,但是不能解决数据冗余
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
    return 0;
    }

    7.3 虚继承 virtual 解决菱形继承

    在中间层继承的时候写virtual public Person虚继承;公共父类 Person 在子类对象里面只保存唯一一份,消除冗余和二义性。

    #include <iostream>
    #include <string>
    using namespace std;

    class Person
    {
    public:
    string _name;
    };

    //虚继承
    class Student : virtual public Person
    {
    protected:
    int _num;
    };

    //虚继承
    class Teacher : virtual public Person
    {
    protected:
    int _id;
    };

    class Assistant : public Student, public Teacher
    {
    protected:
    string _majorCourse;
    };

    int main()
    {
    Assistant a;
    a._name = "peter"; //不再二义性,只有唯一一份Person
    return 0;
    }

    ⚠️注意:虚继承会引入虚基表,底层实现复杂,有一定性能开销;工程尽量不要设计菱形继承。Java 直接取消多继承从根源规避这个问题。

    扩展:C++ 标准 IO 库ios_base就是典型虚继承菱形结构。

    8. 继承与组合

    8.1 is‑a vs has‑a

  • public 继承:is‑a 是一个
  • Student is‑a Person;BMW is‑a Car。 继承属于白箱复用:基类内部细节对子类可见;父子类耦合度很高,基类改动会强烈影响子类。

  • 组合:has‑a 拥有一个
  • Car has‑a Tire(汽车拥有轮胎)。把别的类作为自己的成员变量。 组合属于黑箱复用,只使用对方公开接口,内部细节不可见;耦合低,维护性更好。

    ✨开发经验:优先使用组合;只有满足 is‑a 语义再用继承;实现多态必须使用继承。

    示例对比 stack 两种实现:

    //方式1:继承 vector is‑a(不推荐)
    template<class T>
    class stack : public std::vector<T>
    {};

    //方式2:组合 has‑a(推荐,容器适配器真实实现思路)
    template<class T>
    class stack
    {
    public:
    std::vector<T> _v;
    };

    📔本篇核心考点总结

  • 继承实现类层次代码复用;三种继承方式public/protected/private;基类 private 成员子类永远不可见;口诀min(成员权限,继承方式)。开发只用 public 继承。
  • 派生类对象切片:派生类可以赋值给基类指针 / 引用 / 对象;基类不能直接赋值给派生类。
  • 父子类同名成员构成隐藏 name‑hiding,不是重载,重载仅限同一个作用域;基类::成员访问被隐藏成员。
  • 默认成员函数:构造先基类后派生类;析构先派生类后基类;拷贝构造、赋值重载要处理基类部分。final 修饰类禁止继承。
  • 友元不能继承;static 静态成员继承体系共享同一份。
  • 多继承会带来菱形继承:两份公共基类,数据冗余 + 访问二义性;virtual虚继承解决;尽量避免菱形继承。
  • is‑a 用继承,has‑a 优先用组合;组合耦合低,优先组合。
  • 面试简答

    Q:基类 private 成员子类能不能继承?能不能访问?

    A:内存上继承到子类对象,但是语法不可访问;子类需要访问就设置为 protected。

    Q:成员隐藏和重载区别?

    A:重载在同一个作用域,函数名相同参数不同;父子不同作用域只要名字相同就是隐藏,和参数无关。

    Q:菱形继承问题怎么解决?

    A:中间层使用 virtual 虚继承;最好设计上避免菱形继承。

    Q:继承和组合怎么选?

    A:is‑a 是继承;has‑a 优先组合,组合耦合更低。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【C++进阶】继承
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!