欢迎光临
我们一直在努力

C++第十三讲:继承

第十三讲:C++ 继承

继承是面向对象程序设计的核心特性之一,核心目标是实现类级别的代码复用,建立类的层次结构,描述 “is-a(是一种)” 的类间关系。下面从基础语法到进阶坑点,系统梳理所有核心知识点。

一、继承的基本概念

1. 为什么需要继承

没有继承时,相似类会存在大量代码冗余。例如Student和Teacher都有姓名、年龄、地址等属性,都有身份认证方法,但各自独立实现,重复代码多。

继承的核心思路:提取公共成员到基类(父类),派生类(子类)继承基类后,只需编写自己特有的成员。

2. 基本语法与术语

// 基类(父类):存放所有子类的公共成员
class Person {
public:
  void identity() { cout << "身份认证:" << _name << endl; }
protected:
  string _name; // 姓名
  int _age;     // 年龄
};

// 派生类(子类):public方式继承Person
class Student : public Person {
public:
  void study() { cout << _name << "在学习" << endl; }
private:
  int _stuId; // 学号:Student类特有属性
};

class Teacher : public Person {
public:
  void teaching() { cout << _name << "在授课" << endl; }
private:
  string _title; // 职称:Teacher类特有属性
};

  • 基类 / 父类:被继承的类(如Person),存放通用成员

  • 派生类 / 子类:继承基类的类(如Student、Teacher),只扩展特有成员


二、继承方式与成员访问权限

1. 三种继承方式

继承方式决定了基类成员在派生类中的最终访问权限,共有public、protected、private三种。

基类成员权限public 继承后派生类权限protected 继承后派生类权限private 继承后派生类权限
public public protected private
protected protected protected private
private 不可直接访问 不可直接访问 不可直接访问

2. 核心规则(必记)

  • private 成员永远不可直接访问:基类的私有成员会被继承到派生类对象的内存中,但语法上派生类(类内、类外)都无法直接访问。

  • 权限取更小值:派生类中成员的访问权限 = min(基类成员权限, 继承方式),权限等级:public > protected > private。

  • 默认继承方式:class默认private继承,struct默认public继承。工程开发必须显式写出继承方式。

  • protected 的意义:专门为继承设计的访问限定符。类外无法访问,但派生类内可以访问,平衡了封装性和继承复用。

  • 工程惯例:几乎只使用public继承。protected/private继承会降低成员访问权限,扩展性差、耦合度高。


  • 三、基类与派生类的对象转换(赋值兼容规则)

    public继承下,派生类是一种特殊的基类,支持以下类型转换规则:

    1. 支持的安全转换

    Student s;
    // 1. 派生类对象 → 基类指针(安全,无拷贝)
    Person* p = &s; // 指针指向s对象中基类Person的部分
    // 2. 派生类对象 → 基类引用(安全,无拷贝)
    Person& r = s; // 引用绑定s对象中基类Person的部分
    // 3. 派生类对象 → 基类对象(发生切片)
    Person p2 = s; // 调用基类拷贝构造,只拷贝基类部分,丢弃派生类部分

    • 切片(切割):派生类对象赋值给基类对象时,会 “切掉” 派生类特有的成员,只保留基类部分。

    2. 不支持的转换

    Person p;
    Student s = p; // 编译报错!基类对象不能直接赋值给派生类对象

    3. 强制转换

    基类指针 / 引用如果实际指向派生类对象,可以强制转换为派生类指针 / 引用,但直接强转不安全。多态场景下推荐使用dynamic_cast进行安全的向下转型。


    四、继承中的作用域与同名隐藏

    1. 核心规则

    • 基类和派生类拥有独立的作用域。

    • 同名隐藏:派生类中如果定义了和基类同名的成员,派生类成员会屏蔽基类的同名成员,直接访问默认访问派生类的。

    • 访问基类同名成员:必须显式指定类域 基类名::成员名。

    2. 函数隐藏的特点

    函数隐藏只看函数名相同,和参数列表、返回值无关。

    ⚠️ 注意和函数重载的区别:重载发生在同一个作用域,隐藏发生在不同作用域(基类和派生类)。

    3. 经典例题解析

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

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

    int main() {
      B b;
      b.fun(10); // 调用B::fun(int),正常
      b.fun();   // 编译报错!B的fun隐藏了A的fun,找不到无参版本
      b.A::fun();// 显式指定类域,调用A::fun(),正常
      return 0;
    }

    结论:A::fun()和B::fun(int)是隐藏关系,不是重载。

    4. 工程建议

    继承体系中尽量不要定义同名成员,避免混淆和误用。


    五、派生类的默认成员函数

    类有 6 个默认成员函数,继承体系中,派生类的默认函数会自动处理基类部分,规则如下:

    1. 构造函数

    • 派生类构造函数必须先调用基类构造函数,初始化基类的成员。

    • 如果基类有默认构造函数,派生类可以不显式调用,编译器自动调用。

    • 如果基类没有默认构造函数,派生类必须在初始化列表中显式调用基类构造。

    2. 拷贝构造函数

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

    3. 赋值运算符重载

    • 派生类的operator=会隐藏基类的operator=。

    • 必须显式调用基类的赋值运算符:基类::operator=(派生类对象)。

    4. 析构函数

    • 派生类析构函数执行完后,编译器自动调用基类析构函数。

    • 调用顺序:构造函数「先基类,后派生类」;析构函数「先派生类,后基类」。

    • 析构函数也构成隐藏:编译器会把所有析构函数名统一处理为destructor(),因此基类和派生类析构函数是隐藏关系。

    5. 完整代码示例

    class Person {
    public:
      // 构造函数
      Person(const char* name = "peter") : _name(name) {
          cout << "Person()" << endl;
      }
      // 拷贝构造函数
      Person(const Person& p) : _name(p._name) {
          cout << "Person(const Person&)" << endl;
      }
      // 赋值运算符重载
      Person& operator=(const Person& p) {
          cout << "Person& operator=(const Person&)" << 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&)" << endl;
      }
      // 赋值运算符:显式调用基类赋值
      Student& operator=(const Student& s) {
          cout << "Student& operator=(const Student&)" << endl;
          if (this != &s) {
              Person::operator=(s); // 调用基类赋值
              _num = s._num;
          }
          return *this;
      }
      // 析构函数
      ~Student() { cout << "~Student()" << endl; }
    protected:
      int _num;
    };


    六、特殊主题

    1. 不能被继承的类

    方法 1:C++98 构造函数私有化

    派生类必须调用基类构造函数,基类构造私有则派生类无法调用,无法实例化对象。

    class NonInherit {
    private:
      NonInherit() {} // 构造函数私有
    };

    方法 2:C++11 final 关键字

    用final修饰类,直接禁止该类被继承。

    class Base final { // final修饰,不能被继承
    public:
      void func() {}
    };

    2. 友元与继承

    友元关系不能继承。基类的友元函数只能访问基类的成员,不能访问派生类的私有 / 保护成员。

    class Student;
    class Person {
      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;   // 合法:Person的友元可以访问Person的成员
      cout << s._stuNum << endl; // 编译报错!友元关系不继承,不能访问Student的保护成员
    }

    3. 静态成员与继承

    基类定义的静态成员,整个继承体系只有一份,所有基类、派生类对象共享同一个静态成员。

    class Person {
    public:
      static int _count; // 静态成员
    };
    int Person::_count = 0;

    class Student : public Person {};

    int main() {
      Person p;
      Student s;
      // 地址相同,说明是同一个静态成员
      cout << &p._count << endl;
      cout << &s._count << endl;
      // 都可以通过类名访问
      cout << Person::_count << endl;
      cout << Student::_count << endl;
      return 0;
    }


    七、多继承与菱形继承

    1. 基本概念

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

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

      class A {};
      class B {};
      class C : public A, public B {}; // 多继承

    2. 菱形继承(多继承的经典问题)

    定义

    两个派生类继承同一个基类,最派生类同时继承这两个派生类,形成菱形结构。

      Person(公共基类)
      /     \\
    Student   Teacher(中间类)
      \\     /
      Assistant(最派生类)

    核心问题
  • 数据冗余:Assistant对象中会有两份Person的成员(分别来自 Student 路径和 Teacher 路径)。

  • 二义性:访问Person的成员时,编译器不知道走哪条继承路径,报错 “访问不明确”。

  • 临时解决二义性

    显式指定访问哪个基类路径的成员,但数据冗余问题仍然存在。

    Assistant a;
    a.Student::_name = "张三"; // 显式指定Student路径的_name
    a.Teacher::_name = "李四"; // 显式指定Teacher路径的_name

    3. 虚继承(解决菱形继承问题)

    语法

    在继承方式前加virtual关键字,让中间类虚继承公共基类。

    class Person { public: string _name; };

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

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

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

    效果
    • 最派生类中只有一份公共基类(Person)的成员,彻底解决数据冗余。

    • 访问公共基类成员不再有二义性。

    注意事项
  • 虚继承底层通过虚基表指针实现,有额外的内存和性能开销。

  • 虚基类的构造由最派生类负责调用,保证只构造一次。

  • 工程建议:尽量避免设计菱形继承结构,多继承本身会大幅增加代码复杂度。


  • 八、继承 vs 组合

    1. 核心区别

    关系语义复用类型封装性耦合度
    继承 is-a(是一种) 白箱复用:基类内部对派生类可见 破坏基类封装
    组合 has-a(有一个) 黑箱复用:对象内部不可见 保持封装

    2. 设计原则

    优先使用对象组合,而非类继承。

    • 组合耦合度低、维护性好,更灵活。

    • 只有当类之间是明确的is-a关系,且需要实现多态时,才使用继承。

    3. 示例

    • Car和Tire(轮胎):是 has-a 关系,用组合。

    • BMW和Car:是 is-a 关系,用继承。

    • stack和vector:既可以继承也可以组合,推荐用组合。


    核心知识点总结

  • 继承核心是代码复用,描述 is-a 关系;class 默认私有继承,struct 默认公有继承。

  • 基类 private 成员不可直接访问;成员最终权限取基类权限和继承方式的更小值。

  • public 继承下,派生类对象可赋值给基类指针 / 引用 / 对象(发生切片),反向不可行。

  • 同名成员构成隐藏(不是重载);访问基类同名成员必须显式加类域。

  • 构造顺序:先基类后派生类;析构顺序:先派生类后基类;拷贝构造和赋值要显式调用基类版本。

  • 菱形继承存在数据冗余和二义性,可用虚继承解决,但工程上尽量避免菱形结构。

  • 面向对象设计原则:优先使用组合,其次使用继承。

  • 赞(0)
    未经允许不得转载:171主机测评 » C++第十三讲:继承
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址