欢迎光临
我们一直在努力

【C++】继承

前言 C++有三大特性——封装、继承、多态,是面向对象的基石。此前模拟实现string、vector、list等容器时,我们也就体会到封装的价值,迭代器本身属于三大特性中的封装,所有会感到string、vector、list的结构很相似,但底层天差地别,这就在于把底层复杂的细节全部屏蔽掉,然后用相似的迭代器来访问,这就是封装带来的便利之处。 前面我们模拟实现过string、vector、list、stack、queue的底层结构,那这篇博客就来细讲C++三大特性之一的继承。

继承

  • 一、继承的概念及定义
      • 1.1 无继承的痛点:代码冗余
      • 1.2 继承的解决方案:抽离公共部分
  • 二、继承的基础语法
    • 2.1 继承的定义格式
    • 2.2 继承方式与成员访问权限
    • 2.3 类模板的继承
  • 三、基类与派生类的类型转换
  • 四、继承的核心坑点:作用域与隐藏
  • 4.1 类域的隐藏规则
    • 4.2 经典面试题:函数隐藏 vs 重载
  • 五、派生类的默认成员函数
    • 5.1 构造函数
    • 5.2 析构函数
    • 5.3 拷贝构造 / 赋值重载
    • 5.4 总代码
  • 六、继承的特殊场景
    • 6.1 不能被继承的类
    • 6.2 继承与友元
    • 6.3 继承与静态成员
  • 七、多继承与菱形继承(C++ 的坑)
    • 7.1 多继承的基本概念
    • 7.2 菱形继承的问题
    • 7.3 虚继承解决菱形继承(不推荐)
    • 7.4 多继承中指针偏移问题
    • 7.5 IO库中的菱形虚拟继承
  • 八、继承 vs 组合(设计原则)
      • 设计原则:优先使用组合
    • 总结

一、继承的概念及定义

C 语言的复用停留在函数层级,而 C++ 的继承实现了类层级的复用 —— 在保留原有类(基类)成员的基础上,扩展新成员生成派生类,贴合 “从简单到复杂” 的认知逻辑。

1.1 无继承的痛点:代码冗余

以Student和Teacher类为例,二者包含大量重复的成员(姓名、地址、身份验证等),仅少数成员不同:

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

// 学生类
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; // 独有
};

重复代码不仅增加开发量,还会导致后续维护成本翻倍(比如修改身份验证逻辑需改两处)。

1.2 继承的解决方案:抽离公共部分

将重复成员抽离为Person基类,Student和Teacher通过继承复用这些成员,仅需定义独有部分:

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

// 基类:封装学生/教师的公共成员
class Person
{
public:
void identity() {
cout << "身份验证:" << _name << endl;
}
protected:
string _name = "yuuki";
string _address;
string _tel;
int _age = 18;
};

// 派生类:学生(public继承Person)
class Student : public Person
{
public:
void study() { /* 学习逻辑 */ }
protected:
int _stuid; // 独有成员
};

// 派生类:教师(public继承Person)
class Teacher : public Person
{
public:
void teaching() { /* 授课逻辑 */ }
protected:
string _title; // 独有成员
};

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

输出结果:

身份验证:yuuki
身份验证:yuuki

二、继承的基础语法

2.1 继承的定义格式

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

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 基类(父类):被继承的类(如Person);
  • 派生类(子类):基于基类扩展的类(如Student/Teacher);
  • 继承方式:public/protected/private(实际开发优先用public)。

2.2 继承方式与成员访问权限

基类成员有public/protected/private三种访问权限,不同继承方式会改变派生类中基类成员的访问权限,核心规则如下(记重点即可):

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

核心规则说明
1 基类private成员:无论哪种继承方式,派生类中不可访问(仅基类自身可访问);
2 基类protected成员:派生类可访问,外部不可访问;
3 class默认继承方式为private,struct默认为public;
4 实际开发仅用public继承(protected/private继承扩展性差)。

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

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

// public继承(推荐)
class Student : public Person
{
public:
void Test() {
Print(); // 可访问(基类public→派生类public)
_name = "Tom"; // 可访问(基类protected→派生类protected)
// _age = 20; // 不可访问(基类private)
}
protected:
int _stuid;
};

2.3 类模板的继承

继承模板类时,需通过类模板名<类型>::指定基类域(编译器无法自动推导):

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

namespace yuuki
{
// 继承std::vector模板类实现栈
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x) {
vector<T>::push_back(x); // 必须指定vector<T>域
}
void pop() {
vector<T>::pop_back();
}
const T& top() {
return vector<T>::back();
}
bool empty() {
return vector<T>::empty();
}
};
}

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

三、基类与派生类的类型转换

public继承下的类型转换是面试高频考点,核心规则:

  • 派生类对象 → 基类指针 / 引用:直接支持(称为 “切片”—— 切出派生类中的基类部分);
  • 基类对象 → 派生类对象:不支持(基类不含派生类的独有成员);
  • 基类指针 → 派生类指针:需强制类型转换(仅当基类指针指向派生类对象时安全)。
  • #include<iostream>
    using namespace std;

    class Person // 基类
    {
    virtual void func() {} // 虚函数(为dynamic_cast做准备)
    protected:
    string _name;
    int _age;
    };

    class Student : public Person // 派生类
    {
    public:
    int _stuid;
    };

    int main()
    {
    Student sobj;
    // 1. 派生类对象 → 基类指针/引用(切片)
    Person* pp = &sobj;
    Person& rp = sobj;
    Person pobj = sobj; // 切片赋值

    // 2. 基类对象 → 派生类对象(报错)
    // sobj = pobj;

    // 3. 基类指针 → 派生类指针(安全场景)
    Student* ps1 = dynamic_cast<Student*>(pp); // pp指向sobj,转换成功
    cout << ps1 << endl; // 非空地址

    // 3. 基类指针 → 派生类指针(不安全场景)
    Person pobj2;
    pp = &pobj2;
    Student* ps2 = dynamic_cast<Student*>(pp); // pp指向基类对象,转换失败
    cout << ps2 << endl; // 空地址

    return 0;
    }

    四、继承的核心坑点:作用域与隐藏

    4.1 类域的隐藏规则

    继承体系中,基类和派生类有独立作用域,若出现同名成员,派生类成员会 “隐藏” 基类成员:

  • 同名成员变量:优先访问派生类的;
  • 同名成员函数:仅函数名相同就隐藏(无需参数 / 返回值一致);
  • 访问被隐藏的基类成员:需加基类名::。
  • 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
  • #include<iostream>
    using namespace std;

    class Person // 基类
    {
    protected:
    string _name = "yuuki";
    int _num = 18; // 身份证号
    };

    class Student : public Person // 派生类
    {
    public:
    void Print() {
    cout << "姓名:" << _name << endl; // 复用基类_name
    cout << "学生编号:" << _num << endl; // 访问派生类_num(隐藏基类)
    cout << "身份证号:" << Person::_num << endl; // 访问被隐藏的基类_num
    }
    protected:
    int _num = 999; // 学生编号(与基类_num同名)
    };

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

    输出结果:

    姓名:yuuki
    学生编号:999
    身份证号:18

    4.2 经典面试题:函数隐藏 vs 重载

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

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

    int main()
    {
    B b;
    b.func(10); // 正常调用B::func(int)
    // b.func(); // 报错!A::func()被B::func(int)隐藏,无法直接访问
    b.A::func(); // 正确:显式访问基类被隐藏的函数
    return 0;
    }

    结论:A 和 B 的func是隐藏关系(而非重载)—— 重载要求函数在同一作用域,而隐藏是不同作用域的同名函数。

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

    派生类的 6 个默认成员函数(构造、拷贝构造、赋值重载、析构等),需遵循 “先基类、后派生类” 的规则:

    5.1 构造函数

    • 派生类构造必须先调用基类构造,初始化基类成员;
    • 若基类无默认构造(无参 / 全缺省),派生类需在初始化列表显式调用基类构造。

    class Person
    {
    public:
    // 基类无默认构造(必须传参)
    Person(const char* name) : _name(name) {
    cout << "Person构造" << endl;
    }
    protected:
    string _name;
    };

    class Student : public Person
    {
    public:
    // 派生类构造:先调用Person(name),再初始化_stuid
    Student(const char* name, int stuid)
    : Person(name) // 显式调用基类构造(必须)
    , _stuid(stuid)
    {
    cout << "Student构造" << endl;
    }
    protected:
    int _stuid;
    };

    int main()
    {
    Student s("Tom", 1001); // 输出:Person构造 → Student构造
    return 0;
    }

    5.2 析构函数

    • 派生类析构执行完毕后,编译器自动调用基类析构(保证 “先析构派生、后析构基类”);
    • 析构函数名会被编译器统一处理为destructor(),因此基类析构不加virtual时,派生类析构会隐藏基类析构。

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

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

    int main()
    {
    Student s; // 析构顺序:Student析构 → Person析构
    return 0;
    }

    5.3 拷贝构造 / 赋值重载

    • 拷贝构造:派生类需先拷贝基类部分,再拷贝自身成员;
    • 赋值重载:派生类需先调用基类的operator=,再赋值自身成员。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    class Person
    {
    public:
    Person(const char* name = "yuuki") : _name(name) {}
    // 基类拷贝构造
    Person(const Person& p) : _name(p._name) {
    cout << "Person拷贝构造" << endl;
    }
    // 基类赋值重载
    Person& operator=(const Person& p) {
    if (this != &p) _name = p._name;
    cout << "Person赋值重载" << endl;
    return *this;
    }
    protected:
    string _name;
    };

    class Student : public Person
    {
    public:
    // 派生类拷贝构造
    Student(const Student& s)
    : Person(s) // 拷贝基类部分
    , _stuid(s._stuid)
    {
    cout << "Student拷贝构造" << endl;
    }
    // 派生类赋值重载
    Student& operator=(const Student& s) {
    if (this != &s) {
    Person::operator=(s); // 调用基类赋值重载
    _stuid = s._stuid;
    }
    cout << "Student赋值重载" << endl;
    return *this;
    }
    protected:
    int _stuid = 1001;
    };

    int main()
    {
    Student s1;
    Student s2 = s1; // 拷贝构造:Person拷贝构造 → Student拷贝构造
    Student s3;
    s3 = s1; // 赋值重载:Person赋值重载 → Student赋值重载
    return 0;
    }

    5.4 总代码

    #define _CRT_SECURE_NO_WARNINGS
    #include<iostream>
    using namespace std;

    class Person
    {
    public:

    /*默认构造第一种情况*/

    //// 默认构造(有初始化)
    //Person(const char* name = "YUUKI")
    //:_name(name)
    //{
    //cout << "Person()" << endl;
    //}

    /*默认构造第二种情况,需要子类帮助*/

    // 默认构造(无初始化)
    Person(const char* name)
    :_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:
    // 默认生成的构造函数的行为
    // 1、内置类型->不确定
    // 2、自定义类型->调用默认构造
    // 3、继承父类成员看做一个整体对象,调用父类的默认构造

    // 子类默认构造函数,
    Student(const char* name, int num, const char* addrss)
    // 将Person看做一个整体
    :Person(name)// 错误写法 -> :_name(name)
    ,_num(num)
    ,_addrss(addrss)
    {}

    // 报错:因为父类和子类改成隐藏,因重复调用子类,导致栈溢出
    /*Student& operator=(const Student& s)
    {
    operator=(s);
    }*/

    ~Student()
    {
    // 错误写法: ~Person();
    // 原因:
    // 1. 语法定义,先子类后父类。如果写成上面,就成了先父类再子类
    // 2. 子类析构完后,编译器会自动掉用父类的析构
    }
    protected:
    // 无缺省值
    /*int _num;
    string _addrss;*/

    // 有缺省值
    int _num = 18;
    string _addrss = "广东佛山市"; // 自定义类型会调用自动生成的构造
    };

    int main()
    {
    Student s1("yuuki", 18, "广东佛山市");
    Student s2(s1);

    Student s3("YUUKI", 28, "广东深圳市");
    s1 = s3; // 不需要在子类写赋值运算符,只需要父类里写即可

    return 0;
    }

    方法:将父类看成一个类型,与其他类型一起编写,可更好理解

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    六、继承的特殊场景

    6.1 不能被继承的类

    • 方法 1(C++98):将基类构造函数设为私有(派生类无法调用构造,无法实例化);
    • 方法 2(C++11):用final关键字修饰基类(直接禁止继承)。

    // 方法2:final修饰(推荐)
    class Base final
    {
    public:
    void func() { cout << "Base::func()" << endl; }
    };

    // class Derive : public Base {}; // 报错!Base被final修饰,不能继承

    6.2 继承与友元

    友元关系不能继承—— 基类的友元无法直接访问派生类的私有 / 保护成员,需在派生类中重新声明友元:

    class Student; // 前向声明

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

    class Student : public Person
    {
    public:
    friend void Display(const Person& p, const Student& s); // 重新声明友元
    protected:
    int _stuid = 1001;
    };

    // 友元函数:可访问Person和Student的保护成员
    void Display(const Person& p, const Student& s) {
    cout << p._name << endl;
    cout << s._stuid << endl;
    }

    int main()
    {
    Person p;
    Student s;
    Display(p, s); // 输出:yuuki → 1001
    return 0;
    }

    6.3 继承与静态成员

    基类的静态成员在整个继承体系中只有一份(所有派生类共享):

    class Person
    {
    public:
    static int _count; // 静态成员:统计对象数量
    };
    int Person::_count = 0; // 静态成员类外初始化

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

    int main()
    {
    Person::_count++;
    Student::_count++;
    Teacher::_count++;
    cout << Person::_count << endl; // 输出:3(三者共享_count)
    return 0;
    }

    七、多继承与菱形继承(C++ 的坑)

    7.1 多继承的基本概念

    • 单继承:一个派生类只有一个基类(推荐使用);
    • 多继承:一个派生类有多个基类(易出问题,尽量避免);
    • 菱形继承:多继承的特殊情况(A→B、A→C、B+C→D),会导致数据冗余和二义性(D 对象中有两份 A 的成员)。
    • 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    7.2 菱形继承的问题

    class Person { public: string _name; }; // 基类
    class Student : public Person { public: int _stuid; }; // 派生类1
    class Teacher : public Person { public: string _title; }; // 派生类2
    class Assistant : public Student, public Teacher { public: int _id; }; // 菱形顶点

    int main()
    {
    Assistant a;
    // a._name = "Tom"; // 报错!二义性:_name来自Student还是Teacher?
    a.Student::_name = "Tom"; // 显式指定,解决二义性(但数据冗余仍存在)
    a.Teacher::_name = "Jerry";
    return 0;
    }

    7.3 虚继承解决菱形继承(不推荐)

    通过virtual关键字实现虚继承,可消除数据冗余和二义性,但底层实现复杂、性能损耗大,实战中建议避免设计菱形继承:

    class Person { public: string _name; };
    class Student : virtual public Person { public: int _stuid; }; // 虚继承
    class Teacher : virtual public Person { public: string _title; }; // 虚继承
    class Assistant : public Student, public Teacher { public: int _id; };

    int main()
    {
    Assistant a;
    a._name = "Tom"; // 正常访问(仅一份_name)
    return 0;
    }

    7.4 多继承中指针偏移问题

    选择以下选项:()
    A: p1 == p2 == p3 B: p1 < p2 < p3 C: p1 == p3 != p2 D: p1 != p2 != p3

    class Base1 { public: int _b1;};
    class Base2 { public: int _b2;};
    class Derive : public Base1, public Base2 { public: int _d;};

    int main()
    {
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;

    return 0;
    }

    7.5 IO库中的菱形虚拟继承

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    八、继承 vs 组合(设计原则)

    特性继承(is-a 关系)组合(has-a 关系)
    关系 派生类是一个基类(如 Student 是 Person) 类包含另一个类(如 Car 包含 Engine)
    封装性 破坏基类封装(派生类可访问基类保护成员) 高封装(被组合类的细节不可见)
    耦合度 高(基类修改会影响派生类) 低(被组合类修改不影响组合类)
    复用方式 白箱复用(基类细节可见) 黑箱复用(仅通过接口访问)

    设计原则:优先使用组合

    • 若类之间是 “is-a” 关系(如 Student 是 Person),用继承;
    • 若类之间是 “has-a” 关系(如 Car 有 Engine),用组合;
    • 若两者皆可,优先选组合(降低耦合,提升代码可维护性)。

    总结

  • 继承的核心是代码复用,实战中优先用public继承;
  • 继承的核心坑点是同名成员隐藏,需通过基类::访问被隐藏成员;
  • 派生类默认成员函数需遵循 “先基类、后派生类” 的规则;
  • 多继承(尤其是菱形继承)易出问题,尽量避免;
  • 设计类时,优先用组合而非继承(降低耦合)。
  • 继承是 C++ 多态的基础,但滥用会导致代码臃肿、难以维护 —— 理解继承的规则,更要理解 “何时不用继承”,才是面向对象设计的关键。

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

    评论 抢沙发

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