C++ 继承超全详解:核心语法、作用域、默认函数、菱形继承与避坑指南

本文属于 《C++ 进阶篇系统教程》第 1 篇 ,前面我们已经系统吃透了 C++ 基础语法、类和对象、STL 全容器、模板初阶与进阶,今天正式开启面向对象三大特性的核心篇章 ------继承。这是 C++ 进阶的第一道门槛,也是校招面试笔试的绝对高频考点!

面向对象的三大特性是封装、继承、多态 。封装解决了代码模块化和数据安全的问题,而继承解决了代码复用和层次化设计的问题 ------ 让我们可以在已有类的基础上,快速扩展出新的类,不用重复写相同的代码。

但继承的细节非常多,坑也特别多:三种继承方式的区别、同名成员隐藏、构造析构顺序、菱形继承的二义性...... 很多新手学完继承还是一头雾水,面试一问就懵。

这篇文章我们从基础语法到底层原理,结合完整代码示例和面试考点,把继承的所有核心知识点、细节和易错点一次性讲透!

一、什么是继承?为什么要用继承?

1.1 核心概念

继承是一种类与类之间的关系 ,允许我们在一个已有类(基类 / 父类 )的基础上,创建一个新的类(派生类 / 子类 )。派生类会自动拥有基类的所有非私有成员(成员变量和成员函数),并且可以在派生类中添加新的成员,或者重写基类的成员。

继承的本质是 is-a(是一个) 关系:比如 "学生是一个人""老师是一个人",那么Person就是基类,Student和Teacher就是派生类。

1.2 为什么要用继承?------ 解决代码复用问题

如果没有继承,我们要写Student和Teacher两个类,就需要重复写很多相同的代码:

cpp 复制代码
// 没有继承:重复写相同的代码
class Student {
public:
    string _name;
    int _age;
    void ShowInfo() {
        cout << "姓名:" << _name << ",年龄:" << _age << endl;
    }
    int _student_id; // 学生特有
};

class Teacher {
public:
    string _name;
    int _age;
    void ShowInfo() {
        cout << "姓名:" << _name << ",年龄:" << _age << endl;
    }
    int _teacher_id; // 老师特有
};

用继承后,我们把共同的属性和方法抽出来放到基类Person中,派生类只需要写自己特有的部分:

cpp 复制代码
// 用继承:代码复用,只写特有部分
class Person {
public:
    string _name;
    int _age;
    void ShowInfo() {
        cout << "姓名:" << _name << ",年龄:" << _age << endl;
    }
};

// Student 继承自 Person
class Student : public Person {
public:
    int _student_id; // 学生特有
};

// Teacher 继承自 Person
class Teacher : public Person {
public:
    int _teacher_id; // 老师特有
};

int main() {
    Student s;
    s._name = "张三";
    s._age = 18;
    s._student_id = 2026001;
    s.ShowInfo(); // 自动继承基类的方法

    Teacher t;
    t._name = "李四";
    t._age = 35;
    t._teacher_id = 1001;
    t.ShowInfo(); // 自动继承基类的方法

    return 0;
}

二、继承的基本语法与三种继承方式(重点 + 易错点)

2.1 基本语法

cpp 复制代码
class 派生类名 : 继承方式 基类名 {
    // 派生类特有成员
};

继承方式有三种:public(公有继承)、protected(保护继承)、private(私有继承)

如果不写继承方式,class 默认是 private 继承,struct 默认是 public 继承(新手最容易忘的细节)

2.2 三种继承方式的核心区别

继承方式决定了基类成员在派生类中的访问权限,以及派生类对象能否访问基类成员。

核心规则:基类的 private 成员,在任何继承方式下,派生类内部都不能直接访问 ;基类的 public 和 protected 成员,在派生类中的访问权限等于 "继承方式和原权限的最小值"。

一张表讲清楚所有情况:

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

代码示例:验证继承方式的权限

cpp 复制代码
class Person {
public:
    string _name; // public
protected:
    int _age; // protected
private:
    string _id_card; // private
};

// 1. public继承
class Student : public Person {
public:
    void Func() {
        _name = "张三"; //  可以访问(public→public)
        _age = 18;      //  可以访问(protected→protected)
        // _id_card = "123456"; //  不能访问(基类private)
    }
};

// 2. protected继承
class Teacher : protected Person {
public:
    void Func() {
        _name = "李四"; //  可以访问(public→protected)
        _age = 35;      //  可以访问(protected→protected)
        // _id_card = "789012"; //  不能访问
    }
};

// 3. private继承
class Doctor : private Person {
public:
    void Func() {
        _name = "王五"; // 可以访问(public→private)
        _age = 40;      // 可以访问(protected→private)
        // _id_card = "345678"; //  不能访问
    }
};

int main() {
    Student s;
    s._name = "张三"; //  public继承,派生类对象可以访问基类public成员
    // s._age = 18; //protected成员,类外不能访问

    Teacher t;
    // t._name = "李四"; // protected继承,基类public成员变成protected,类外不能访问

    Doctor d;
    // d._name = "王五"; // private继承,基类所有成员变成private,类外不能访问

    return 0;
}

2.3 实际开发中的选择

1.99% 的场景都用 public 继承 :这是最符合 is-a 关系的继承方式,派生类对象可以当作基类对象使用

2.protected 和 private 继承几乎不用:它们表示 "has-a" 或 "用一个" 的关系,不如用组合(把基类对象作为派生类的成员)更清晰

三、继承中的作用域与同名成员隐藏(面试高频 + 易错点)

3.1 核心规则:派生类和基类有独立的作用域

每个类都有自己独立的作用域,派生类的作用域嵌套在基类的作用域之内 。当派生类中有和基类同名的成员时,派生类的成员会隐藏 基类的同名成员 ------ 这就是隐藏规则。

3.2 同名成员变量的隐藏

cpp 复制代码
class Person {
public:
    int _age = 10; // 基类的_age
};

class Student : public Person {
public:
    int _age = 20; // 派生类的_age,隐藏基类的_age
};

int main() {
    Student s;
    cout << s._age << endl; // 输出:20(访问派生类的_age)
    // 如何访问基类的_age?加基类作用域限定符
    cout << s.Person::_age << endl; // 输出:10(访问基类的_age)

    return 0;
}

3.3 同名成员函数的隐藏(最容易踩的坑!)

重点:只要函数名相同,不管参数列表是否相同,派生类的函数都会隐藏基类的所有同名函数 ------这不是重载!重载必须在同一个作用域内。

cpp 复制代码
class Person {
public:
    void Show() {
        cout << "Person::Show()" << endl;
    }
    void Show(int age) {
        cout << "Person::Show(int):" << age << endl;
    }
};

class Student : public Person {
public:
    void Show() {
        cout << "Student::Show()" << endl;
    }
};

int main() {
    Student s;
    s.Show(); // 调用派生类的Show()
    // s.Show(18); // 编译错误,基类的Show(int)被隐藏了,无法直接调用

    // 如何调用基类的Show(int)?加基类作用域限定符
    s.Person::Show(18); // 调用基类的Show(int)

    return 0;
}

3.4 总结

1.派生类的同名成员会隐藏基类的所有同名成员(变量和函数)

2.要访问基类的同名成员,必须加 ** 基类名:: ** 作用域限定符

3.隐藏和重载的区别:重载在同一个作用域,隐藏在不同作用域

四、派生类的默认成员函数(核心 + 面试必考)

我们之前讲过,每个类都有 6 个默认成员函数。在继承中,派生类的默认成员函数会自动处理基类部分,但有很多细节需要注意。

4.1 构造函数

派生类的构造函数必须先调用基类的构造函数,初始化基类部分,再初始化派生类自己的部分。

1.如果派生类的构造函数没有显式调用基类的构造函数,编译器会自动调用基类的默认构造函数

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

cpp 复制代码
class Person {
public:
    // 基类有带参构造函数,没有默认构造函数
    Person(string name, int age)
        : _name(name)
        , _age(age)
    {
        cout << "Person构造函数" << endl;
    }

    string _name;
    int _age;
};

class Student : public Person {
public:
    // 派生类构造函数必须在初始化列表中显式调用基类的带参构造
    Student(string name, int age, int student_id)
        : Person(name, age) // 调用基类构造函数初始化基类部分
        , _student_id(student_id) // 初始化派生类自己的部分
    {
        cout << "Student构造函数" << endl;
    }

    int _student_id;
};

int main() {
    Student s("张三", 18, 2026001);
    // 输出顺序:
    // Person构造函数
    // Student构造函数

    return 0;
}

4.2 析构函数

**析构函数的调用顺序和构造函数完全相反:**先调用派生类的析构函数,再调用基类的析构函数。
重要细节:派生类的析构函数会自动调用基类的析构函数,不需要我们手动调用。如果手动调用基类析构函数,会导致重复析构,程序崩溃。

cpp 复制代码
class Person {
public:
    ~Person() {
        cout << "Person析构函数" << endl;
    }
};

class Student : public Person {
public:
    ~Student() {
        cout << "Student析构函数" << endl;
    }
};

int main() {
    Student s;
    // 输出顺序:
    // Student析构函数
    // Person析构函数

    return 0;
}

4.3 拷贝构造函数

派生类的拷贝构造函数会自动调用基类的拷贝构造函数,初始化基类部分。如果我们显式写派生类的拷贝构造函数,必须在初始化列表中显式调用基类的拷贝构造函数。

cpp 复制代码
class Person {
public:
    Person(string name, int age) : _name(name), _age(age) {}
    Person(const Person& p)
        : _name(p._name)
        , _age(p._age)
    {
        cout << "Person拷贝构造" << endl;
    }

    string _name;
    int _age;
};

class Student : public Person {
public:
    Student(string name, int age, int student_id)
        : Person(name, age)
        , _student_id(student_id)
    {}

    // 显式写派生类拷贝构造,必须调用基类拷贝构造
    Student(const Student& s)
        : Person(s) // 把派生类对象传给基类拷贝构造(切片,后面讲)
        , _student_id(s._student_id)
    {
        cout << "Student拷贝构造" << endl;
    }

    int _student_id;
};

int main() {
    Student s1("张三", 18, 2026001);
    Student s2 = s1;
    // 输出顺序:
    // Person拷贝构造
    // Student拷贝构造

    return 0;
}

4.4 赋值运算符重载

和拷贝构造类似,派生类的赋值运算符重载必须显式调用基类的赋值运算符重载,赋值基类部分。

cpp 复制代码
class Person {
public:
    Person& operator=(const Person& p) {
        if (this == &p) return *this;
        _name = p._name;
        _age = p._age;
        cout << "Person赋值重载" << endl;
        return *this;
    }

    string _name;
    int _age;
};

class Student : public Person {
public:
    Student& operator=(const Student& s) {
        if (this == &s) return *this;
        // 调用基类赋值重载,赋值基类部分
        Person::operator=(s);
        // 赋值派生类自己的部分
        _student_id = s._student_id;
        cout << "Student赋值重载" << endl;
        return *this;
    }

    int _student_id;
};

4.5 总结:派生类默认成员函数的调用顺序

函数 调用顺序 注意事项
构造函数 基类构造 → 派生类构造 必须先调用基类构造
析构函数 派生类析构 → 基类析构 自动调用基类析构,不要手动调用
拷贝构造 基类拷贝构造 → 派生类拷贝构造 显式写时必须调用基类拷贝构造
赋值重载 基类赋值 → 派生类赋值 显式写时必须调用基类赋值

五、赋值兼容规则(切片 / 切割)

赋值兼容规则是 public 继承特有的规则,它允许派生类对象赋值给基类对象、基类指针、基类引用, 这个过程叫做 切片(切割)------ 只取派生类中基类的部分,切掉派生类自己的部分。

cpp 复制代码
class Person {
public:
    string _name;
    int _age;
};

class Student : public Person {
public:
    int _student_id;
};

int main() {
    Student s;
    s._name = "张三";
    s._age = 18;
    s._student_id = 2026001;

    // 1. 派生类对象赋值给基类对象(切片)
    Person p = s;
    cout << p._name << " " << p._age << endl; // 输出:张三 18
    // p._student_id = 123; // 编译错误!p是Person对象,没有_student_id

    // 2. 派生类对象地址赋值给基类指针
    Person* pp = &s;
    cout << pp->_name << " " << pp->_age << endl; // 输出:张三 18

    // 3. 派生类对象赋值给基类引用
    Person& rp = s;
    cout << rp._name << " " << rp._age << endl; // 输出:张三 18

    // 反向不成立:基类对象不能赋值给派生类对象
    // Student s2 = p; // 错误!

    return 0;
}

赋值兼容规则是多态的基础,我们下一篇讲多态的时候会详细展开。

六、菱形继承与虚继承(重难点!)

6.1 什么是菱形继承?

菱形继承是多继承 的一种特殊情况:一个派生类同时继承自两个基类,而这两个基类又继承自同一个基类。

比如:Assistant(助教)既是Student(学生)又是Teacher(老师),而Student和Teacher都继承自Person,这就形成了一个菱形:

6.2 菱形继承的两个致命问题

1.数据冗余 :Assistant对象中会有两份Person的成员(一份来自 Student,一份来自 Teacher)
2.二义性 :访问Person的成员时,编译器不知道该访问哪一份
代码示例:菱形继承的问题

cpp 复制代码
class Person {
public:
    int _age;
};

class Student : public Person {};
class Teacher : public Person {};

class Assistant : public Student, public Teacher {};

int main() {
    Assistant a;
    // a._age = 18; // 编译错误!二义性,不知道访问Student::_age还是Teacher::_age

    // 只能加作用域限定符访问,但数据冗余问题依然存在
    a.Student::_age = 18;
    a.Teacher::_age = 35;
    cout << a.Student::_age << endl; // 输出:18
    cout << a.Teacher::_age << endl; // 输出:35

    // Assistant对象的大小是8字节(两个int),有两份_age,数据冗余
    cout << sizeof(a) << endl; // 输出:8

    return 0;
}

6.3 解决方案:虚继承

C++ 用虚继承 来解决菱形继承的问题。在中间层的继承方式前加virtual关键字,让最顶层的基类(Person)成为虚基类,这样派生类中只会有一份虚基类的成员。

cpp 复制代码
class Person {
public:
    int _age;
};

// 虚继承:加virtual关键字
class Student : virtual public Person {};
class Teacher : virtual public Person {};

class Assistant : public Student, public Teacher {};

int main() {
    Assistant a;
    a._age = 18; // 没有二义性,只有一份_age
    cout << a._age << endl; // 输出:18
    cout << a.Student::_age << endl; // 输出:18
    cout << a.Teacher::_age << endl; // 输出:18

    // 注意:虚继承后对象大小会变大(因为多了虚基表指针)
    // 32位系统下是8字节(4字节虚基表指针 + 4字节_age)
    // 64位系统下是16字节(8字节虚基表指针 + 4字节_age + 4字节对齐)
    cout << sizeof(a) << endl; // 64位输出:16

    return 0;
}

6.4 虚继承的底层原理(面试高频)

虚继承的底层是通过 ** 虚基表指针(vbptr)和虚基表(vbtable)** 实现的:

1.每个虚继承的派生类对象中,会多一个虚基表指针 ,指向自己的虚基表

2.虚基表中存储了虚基类成员相对于当前对象的偏移量

3.访问虚基类成员时,通过虚基表指针找到虚基表,计算出偏移量,然后访问成员

这样,无论有多少个派生类继承自虚基类,最终的派生类对象中只会有一份虚基类的成员,解决了数据冗余和二义性的问题。

6.5 虚继承的注意事项

1.虚继承会增加对象的大小(多了虚基表指针),也会降低访问效率(需要通过虚基表计算偏移量)

2.虚继承的构造函数调用顺序会变化:先调用虚基类的构造函数,再调用中间层的构造函数,最后调用最派生类的构造函数

3.实际开发中尽量避免多继承,更要避免菱形继承,C++ 的很多设计模式都可以替代多继承

七、继承的其他重要细节与易错点

7.1 友元关系不能继承

基类的友元函数 / 友元类,不能访问派生类的私有成员。友元关系是单向的、不可传递的、不可继承的。

cpp 复制代码
class Person {
    friend void ShowPerson(const Person& p);
private:
    int _age = 10;
};

void ShowPerson(const Person& p) {
    cout << p._age << endl; // 友元可以访问
}

class Student : public Person {
private:
    int _student_id = 2026001;
};

int main() {
    Student s;
    ShowPerson(s); // 可以访问s中Person的部分
    // cout << s._student_id << endl; //不能访问Student的私有成员
    return 0;
}

7.2 静态成员变量被所有派生类共享

基类的静态成员变量属于整个类,被所有派生类共享,整个程序中只有一份。

cpp 复制代码
class Person {
public:
    static int _count;
    Person() { _count++; }
};

int Person::_count = 0;

class Student : public Person {};
class Teacher : public Person {};

int main() {
    Student s1, s2;
    Teacher t1;
    cout << Person::_count << endl; // 输出:3
    cout << Student::_count << endl; // 输出:3
    cout << Teacher::_count << endl; // 输出:3
    return 0;
}

八、总结

1.继承的核心是代码复用 ,本质是 is-a 关系,99% 的场景用 public 继承。

2.三种继承方式 决定了基类成员在派生类中的访问权限,基类 private 成员在任何继承方式下都不能直接访问。

3.同名成员隐藏 :派生类的同名成员会隐藏基类的所有同名成员,要访问基类成员必须加作用域限定符。

4.派生类默认成员函数 :构造先基后派,析构先派后基,拷贝构造和赋值重载必须显式调用基类的对应函数。

6.赋值兼容规则 :派生类对象可以赋值给基类对象、指针、引用,这是多态的基础。

6.菱形继承 会导致数据冗余和二义性,用虚继承解决,底层通过虚基表指针和虚基表实现。

相关推荐
L_09071 小时前
【C++】STL— 封装红黑树以实现map 和 set
数据结构·c++
麦兜和小可的舅舅1 小时前
ClickHouse Dist表的Replica选择逻辑深度解析-- Custom Key以及Sample的执行逻辑
c++·clickhouse·distribute·shard
djarmy1 小时前
C 标准库 `<stdio.h>` 完整函数清单(官方标准 + 常用全部函数)
c语言·c++·算法
code_whiter2 小时前
C++10(list)
c++·windows·list
2301_815279522 小时前
实战分享实现 C++ 管理类单例模式:特点与最佳实践
javascript·c++·单例模式
旺仔老馒头.2 小时前
【C++】类和对象(二)
开发语言·c++·后端·类和对象
wefg12 小时前
一些零散的算法
c++·算法
khalil10202 小时前
代码随想录算法训练营Day-48 单调栈02 | 42. 接雨水、84.柱状图中最大的矩形
数据结构·c++·算法·leetcode·单调栈·接雨水
大大杰哥3 小时前
leetcode hot100(3)子串
c++·算法·leetcode