C++继承从概念到实践指南

作者:袖梨 2026-08-08

处理C++继承从概念到实践指南这类问题时,先确认目标场景,再按步骤核对配置或玩法细节。

1. 继承的概念与定义

继承(Inheritance)是面向对象程序设计(OOP)中实现代码复用的核心机制。它允许我们在保持原有类特性的基础上进行扩展,增加新的成员变量和成员函数,从而产生新的类,称为派生类(Derived Class)。继承体现了面向对象程序设计的层次结构,反映了从简单到复杂的认知过程。

C++继承从概念到实践指南

1.1 为什么需要继承?

在没有继承机制时,如果多个类有共同的属性和行为,我们只能在每个类中重复定义这些成员,导致代码冗余和维护困难。

例如,考虑学生(Student)和教师(Teacher)两个类:

// 没有继承的设计 - 存在大量冗余class Student {public:    void identity() { /* 身份认证 */ }    void study() { /* 学习 */ }protected:    string _name;    string _address;    string _tel;    int _age;    int _stuid;  // 学号};class Teacher {public:    void identity() { /* 身份认证 */ }    void teaching() { /* 授课 */ }protected:    string _name;    string _address;    string _tel;    int _age;    string _title;  // 职称};

可以看到,两个类中都重复定义了姓名、地址、电话、年龄等成员变量,以及 identity() 成员函数。通过继承,我们可以将这些公共成员提取到基类中:

// 使用继承的设计class Person {public:    void identity() {        cout << "身份认证:" << _name << endl;    }protected:    string _name = "张三";    string _address;    string _tel;    int _age = 18;};class Student : public Person {public:    void study() { /* 学习 */ }protected:    int _stuid;  // 学号};class Teacher : public Person {public:    void teaching() { /* 授课 */ }protected:    string _title;  // 职称};

1.2 继承的定义格式

继承的基本语法如下:

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

其中:

基类:也称为父类,是被继承的类

派生类:也称为子类,是继承基类的类

继承方式:public、protected、private,控制基类成员在派生类中的访问权限

1.3 继承方式与访问权限

继承方式会影响基类成员在派生类中的访问权限:

基类成员访问限定符public继承protected继承private继承
publicpublicprotectedprivate
protectedprotectedprotectedprivate
private不可见不可见不可见

重要规则:

  1. 基类的 private 成员在派生类中无论以什么方式继承都是不可见的(但确实被继承到了派生类对象中)
  2. 基类的 protected 成员是为继承而设计的,可以在派生类中访问,但不能在类外访问
  3. 访问权限 = Min(成员在基类的访问限定符,继承方式),其中 public > protected > private
  4. class 默认继承方式是 private,struct 默认继承方式是 public(建议显式指定)
  5. 实际开发中主要使用 public 继承,protected/private 继承较少使用

2. 基类与派生类的转换

在 public 继承中,存在以下转换规则:

2.1 派生类对象赋值给基类

class Person {protected:    string _name;    string _sex;    int _age;};class Student : public Person {public:    int _No;  // 学号};int main() {    Student sobj;    // 1. 派生类对象可以赋值给基类的指针/引用(切片/切割)    Person* pp = &sobj      // 正确    Person& rp = sobj;       // 正确    // 2. 派生类对象可以赋值给基类对象(调用基类的拷贝构造)    Person pobj = sobj;      // 正确    // 3. 基类对象不能赋值给派生类对象    // sobj = pobj;          // 错误:编译报错    // 4. 基类指针/引用可以通过强制类型转换赋值给派生类指针/引用    //    但必须是基类指针指向派生类对象时才安全    Person* p = &sobj    Student* ps = (Student*)p;  // 强制转换,需要谨慎    return 0;}

切片:将派生类对象赋值给基类对象时,只会复制基类部分的数据,派生类特有的部分会被"切掉"。

3. 继承中的作用域

3.1 隐藏规则

在继承体系中,基类和派生类有独立的作用域。当派生类和基类有同名成员时,派生类成员会隐藏基类的同名成员。

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();  // 输出:姓名:小李子                 //      身份证号:111                 //      学号:999    return 0;}

注意:

如果是成员函数的隐藏,只需要函数名相同就构成隐藏(参数不同也构成隐藏)

实际开发中应避免在继承体系中定义同名成员

可以使用基类::成员名显式访问被隐藏的基类成员

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

派生类的6个默认成员函数有其特殊性:

4.1 构造函数

派生类的构造函数必须调用基类的构造函数来初始化基类部分:

class Person {public:    Person(const char* name = "peter")          : _name(name)    {        cout << "Person()" << endl;    }protected:    string _name;};class Student : public Person {public:    Student(const char* name, int num)         : Person(name)  // 必须调用基类构造函数        , _num(num) {        cout << "Student()" << endl;    }protected:    int _num;};

注意:_name不属于子类student成员,属于父类person的成员

初始化列表只能初始化当前类自己定义的成员变量,不能直接初始化父类的成员。

- _num :是 Student 自己的成员,所以可以写 _num(num)

- _name :定义在 Person 父类,Student 无权在自己的初始化列表直接初始化它

2、正确处理父类成员的唯一方式

只能通过调用父类构造函数,由父类自己的初始化列表完成 _name 赋值:

4.2 拷贝构造函数

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

4.3 赋值运算符重载

Student& operator=(const Student& s) {    if (this != &s) {        Person::operator=(s);  // 显式调用基类的operator=,这里需要使用父类的类域,因为名字相同,构成隐藏,访问时需要指明父类域        _num = s._num;    }    return *this;}

4.4 析构函数

派生类析构函数会在调用完成后自动调用基类的析构函数:

~Student() {    cout << "~Student()" << endl;    // 此处会自动调用 ~Person()}

构造和析构顺序:

  1. 派生类对象构造:先调用基类构造,再调用派生类构造
  2. 派生类对象析构:先调用派生类析构,再调用基类析构

5. 继承与友元、静态成员

5.1 继承与友元

友元关系不能继承:基类的友元不能访问派生类的私有和保护成员。

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;      // 正确:可以访问Person的protected成员    // cout << s._stuNum << endl; // 错误:不能访问Student的protected成员    // 需要将Display也声明为Student的友元}

5.2 继承与静态成员

基类定义的静态成员在整个继承体系中只有一份:

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;}

6. 多继承与菱形继承

6.1 单继承与多继承

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

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

6.2 菱形继承问题

菱形继承是多继承的一种特殊情况,会导致数据冗余和二义性问题:

class Person {public:    string _name;};class Student : public Person {protected:    int _num;};class Teacher : public Person {protected:    int _id;};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";  // 数据冗余:两份_name    return 0;}

6.3 虚继承解决方案

使用虚继承可以解决菱形继承的数据冗余和二义性问题:

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";  // 正确:只有一份_name    return 0;}

注意:虽然C++支持多继承和虚继承,但实际开发中应尽量避免设计出菱形继承,因为这会增加代码复杂性和维护难度。

7. 继承与组合

在面向对象设计中,除了继承还有另一种重要的代码复用方式——组合。

7.1 继承 vs 组合

特性继承(is-a)组合(has-a)
关系派生类对象是一个基类对象B对象中有一个A对象
复用类型白箱复用(内部细节可见)黑箱复用(内部细节不可见)
耦合度高(基类改变影响派生类)低(类之间依赖弱)
封装性破坏封装(基类细节对派生类可见)保持封装

7.2 设计原则

  1. 优先使用组合:组合的耦合度低,代码维护性好
  2. 适合继承时用继承:类之间确实是is-a关系时使用继承
  3. 实现多态必须用继承:要实现运行时多态必须使用继承
  4. 既适合继承又适合组合时用组合
// 组合示例:Car has-a Tireclass Tire {protected:    string _brand;    size_t _size;};class Car {protected:    string _colour;    string _num;    Tire _t1, _t2, _t3, _t4;  // 组合关系};// 继承示例:BMW is-a Carclass BMW : public Car {public:    void Drive() { cout << "好开-操控" << endl; }};

8. 继承的最佳实践

  1. 使用public继承:除非有特殊需求,否则使用public继承
  2. 避免菱形继承:多继承会增加复杂度,尽量避免设计菱形继承
  3. 合理使用protected:需要在派生类中访问但不在类外访问的成员设为protected
  4. 注意隐藏问题:避免在派生类中定义与基类同名的成员
  5. 正确调用基类构造函数:在派生类构造函数初始化列表中显式调用基类构造函数
  6. 考虑使用final:C++11的final关键字可以防止类被继承
// 使用final防止继承class Base final {  // 这个类不能被继承public:    void func() { cout << "Base::func" << endl; }};// class Derived : public Base {  // 错误:Base是final的//     // ...// };

总结

继承是C++面向对象编程的核心特性之一,它提供了代码复用的强大机制。通过合理使用继承,我们可以:减少代码冗余,提高代码复用性、建立清晰的类层次结构、实现多态性(配合虚函数)。

相关文章

精彩推荐