【C++游记】子承父业——乃继承也

发布于:2025-08-29 ⋅ 阅读:(16) ⋅ 点赞:(0)

 

枫の个人主页

你不能改变过去,但你可以改变未来

算法/C++/数据结构/C

Hello,这里是小枫。C语言与数据结构和算法初阶两个板块都更新完毕,我们继续来学习C++的内容呀。C++是接近底层有比较经典的语言,因此学习起来注定枯燥无味,西游记大家都看过吧~,我希望能带着大家一起跨过九九八十一难,降伏各类难题,学会C++,我会尽我所能,以通俗易懂、幽默风趣的方式带给大家形象生动的知识,也希望大家遇到困难不退缩,遇到难题不放弃,学习师徒四人的精神!!!故此得名【C++游记

 话不多说,让我们一起进入今天的学习吧~~~  

一、继承的概念及核心价值

1.1 继承的本质:类层次的代码复用

继承(inheritance)允许在保持原有类(基类/父类)特性的基础上,扩展新的方法(成员函数)和属性(成员变量),生成新类(派生类/子类)。它区别于函数层次的复用,是面向对象程序设计中"由简单到复杂"认知过程的体现,能有效减少代码冗余。

1.2 继承的优化:一个爸爸

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

// 基类:提取Student和Teacher的共性
class Person
{
public:
    // 身份认证(复用)
    void identity() {
        cout << _name << "身份认证通过" << endl;
    }
protected:
    string _name = "张三";  // 姓名(复用)
    string _address;        // 地址(复用)
    string _tel;            // 电话(复用)
    int _age = 18;          // 年龄(复用)
};

// 学生类:继承Person,仅定义独有成员
class Student : public Person
{
public:
    void study() {
        cout << _name << "(学生)学习中" << endl;
    }
protected:
    int _stuid; // 学生独有:学号
};

// 教师类:继承Person,仅定义独有成员
class Teacher : public Person
{
public:
    void teaching() {
        cout << _name << "(教师)授课中" << endl;
    }
protected:
    string _title; // 教师独有:职称
};

// 测试:复用基类方法,调用派生类独有方法
int main()
{
    Student s;
    Teacher t;
    
    // 复用基类identity()方法
    s.identity(); 
    t.identity(); 
    
    // 调用派生类独有方法
    s.study();    
    t.teaching(); 
    
    return 0;
    // 输出结果:
    // 张三身份认证通过
    // 张三身份认证通过
    // 张三(学生)学习中
    // 张三(教师)授课中
}

二、继承的定义格式与访问权限控制

2.1 继承的定义格式

派生类定义需指定"基类"和"继承方式",语法格式如下:

class 派生类名 : 继承方式 基类名
{
    // 派生类成员(独有属性/方法)
};

其中:

  • 基类/派生类:基类(Base Class)也称父类,派生类(Derived Class)也称子类(因翻译差异两种称呼通用);
  • 继承方式:包括public(公继承)、protected(保护继承)、private(私有继承);
  • 默认继承方式:使用class定义类时默认private继承,使用struct时默认public继承,建议显式指定继承方式。

2.2 继承方式对成员访问权限的影响

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 派生类中不可见 派生类中不可见 派生类中不可见
  1. 基类private成员"不可见":指成员虽被继承到派生类对象中,但语法限制派生类(类内/类外)均无法直接访问,需通过基类的public/protected接口间接访问;
  2. protected限定符的意义:若基类成员需"类外不可访问、派生类内可访问",则定义为protected,这是专为继承设计的访问限定符;
  3. 实际应用建议:几乎只使用public继承,protected/private继承会限制派生类扩展性,维护成本高。

三、基类与派生类的对象转换规则

3.1 允许的转换:向上转换(派生类→基类)

派生类对象可以赋值给基类的指针、引用或对象,此过程称为"切片/切割"——即仅取派生类中基类对应的部分,丢弃派生类独有成员。实例如下:

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

class Person
{
protected:
    string _name = "张三"; // 姓名
    int _age = 18;         // 年龄
public:
    void showInfo() {
        cout << "姓名:" << _name << ",年龄:" << _age << endl;
    }
};

class Student : public Person
{
public:
    int _No = 2024001; // 学号(独有)
    void showStuInfo() {
        cout << "学号:" << _No << ",姓名:" << _name << endl;
    }
};

int main()
{
    Student sobj;
    // 1. 派生类对象 → 基类指针
    Person* pp = &sobj;
    pp->showInfo(); // 调用基类方法,仅访问基类成员
    
    // 2. 派生类对象 → 基类引用
    Person& rp = sobj;
    rp.showInfo(); // 调用基类方法
    
    // 3. 派生类对象 → 基类对象(调用基类拷贝构造)
    Person pobj = sobj;
    pobj.showInfo(); // 仅包含基类成员
    
    return 0;
    // 输出结果:
    // 姓名:张三,年龄:18
    // 姓名:张三,年龄:18
    // 姓名:张三,年龄:18
}

3.2 禁止的转换:向下转换(基类→派生类)

基类对象不能直接赋值给派生类对象,因为基类不包含派生类的独有成员,无法完成派生类对象的完整初始化,编译会直接报错。实例如下:

int main()
{
    Person pobj;
    Student sobj;
    // sobj = pobj; // 编译报错:error C2679: 二进制"=": 没有找到接受"Person"类型的右操作数的运算符
    return 0;
}

四、继承中的作用域与隐藏规则

基类和派生类拥有独立的作用域,当两者存在同名成员时,会触发"隐藏"规则(也称"重定义"),这是继承中易混淆的核心知识点之一。

4.1 隐藏规则的核心内容

  • 基类和派生类的作用域相互独立;
  • 派生类与基类有同名成员时,派生类成员会屏蔽基类同名成员的直接访问,需通过基类名::基类成员显式访问;
  • 成员函数的隐藏:仅需"函数名相同"即构成隐藏,无需参数列表或返回值匹配(与"重载"区分:重载要求同一作用域、参数列表不同);
  • 实际建议:继承体系中尽量不定义同名成员,避免混淆。

4.2 隐藏规则实例演示

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

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

class Student : public Person
{
public:
    void Print()
    {
        cout << "姓名:" << _name << endl;          // 无隐藏,访问基类_name
        cout << "身份证号:" << Person::_num << endl;// 显式访问基类_num(隐藏)
        cout << "学号:" << _num << endl;            // 访问派生类_num(隐藏基类)
    }
protected:
    int _num = 999; // 派生类:学号(与基类_num同名,触发隐藏)
};

int main()
{
    Student s1;
    s1.Print();
    // 输出结果:
    // 姓名:小李子
    // 身份证号:111
    // 学号:999
    return 0;
}

4.3 常见误区:函数隐藏 vs 函数重载

函数隐藏与重载的核心区别在于"作用域":隐藏发生在不同作用域(基类vs派生类),重载发生在同一作用域。实例如下:

#include <iostream>
using namespace std;

class A
{
public:
    void fun() { cout << "A::func()" << endl; } // 基类函数
};

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

int main()
{
    B b;
    b.fun(10);  // 正确:调用B::fun(int)
    // b.fun();  // 编译报错:B::fun(int)隐藏了A::fun()
    b.A::fun(); // 正确:显式访问基类fun()
    return 0;
}

五、派生类的默认成员函数实现规则

C++类有6个默认成员函数(构造、析构、拷贝构造、赋值重载、取地址重载、const取地址重载),其中前4个在派生类中需特殊处理,核心原则是"先初始化基类,后清理派生类"。

5.1 派生类默认成员函数的核心规则

  • 构造函数:派生类构造必须调用基类构造初始化基类部分;若基类无默认构造(无参/全缺省),需在派生类构造的初始化列表显式调用基类构造;
  • 拷贝构造函数:派生类拷贝构造必须调用基类拷贝构造完成基类部分的拷贝初始化;
  • 赋值运算符重载:派生类赋值重载必须调用基类赋值重载完成基类部分的赋值;
  • 析构函数:派生类析构函数执行完后,编译器会自动调用基类析构函数(先清理派生类,后清理基类)。

5.2 派生类默认成员函数实现示例

#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) {
        if (this != &p) {
            _name = p._name;
        }
        cout << "Person::operator=" << endl;
        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) {
        if (this != &s) {
            Person::operator=(s); // 调用基类赋值
            _num = s._num;
        }
        cout << "Student::operator=" << endl;
        return *this;
    }
    
    // 派生类析构:编译器自动调用基类析构
    ~Student() {
        cout << "~Student()" << endl;
    }
protected:
    int _num; // 学号
};

// 测试:初始化与清理顺序
int main()
{
    Student s1("jack", 18); // 构造顺序:Person() → Student()
    Student s2(s1);         // 拷贝构造顺序:Person拷贝 → Student拷贝
    Student s3("rose", 17);
    s1 = s3;                // 赋值顺序:Person赋值 → Student赋值
    
    return 0; 
    // 析构顺序:~Student() → ~Person()(与构造顺序相反)
}

六、继承中的特殊场景处理

6.1 继承与友元:友元关系不可继承

基类的友元不能访问派生类的私有/保护成员,友元关系仅存在于声明它的类与友元之间,不传递给派生类。爸爸的朋友不是我的朋友。

6.2 继承与静态成员:静态成员全局唯一

基类定义的static成员,在整个继承体系中仅存在一份实例,派生类与基类共享该静态成员。

七、多继承与菱形继承问题解析

7.1 多继承的概念

  • 单继承:一个派生类仅有一个直接基类(如:A:public B);
  • 多继承:一个派生类有多个直接基类(如:A:public B,public C);

7.2 菱形继承的问题

菱形继承是多继承的特殊情况,表现为"一个派生类的两个直接基类继承自同一个间接基类",会导致数据冗余二义性

7.3 菱形虚拟继承:解决数据冗余和二义性

C++通过虚拟继承(virtual inheritance)解决菱形继承问题,在直接基类继承间接基类时使用virtual关键字,使间接基类在继承体系中只保留一份实例。

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

// 间接基类
class Person {
public:
    string _name; // 姓名
};

// 虚拟继承:Student -> Person
class Student : virtual public Person {
protected:
    int _num; // 学号
};

// 虚拟继承:Teacher -> Person
class Teacher : virtual public Person {
protected:
    int _id; // 职工号
};

// 派生类:Assistant继承Student和Teacher
class Assistant : public Student, public Teacher {
protected:
    string _major; // 主修课程
};

int main() {
    Assistant a;
    a._name = "peter"; // 正确:无二义性(仅一份_name)
    return 0;
}

虚拟继承的实现原理:

虚拟继承通过引入"虚基表"和"虚基指针"实现,派生类对象中不再直接包含间接基类的成员,而是通过指针访问共享的间接基类实例,从而解决数据冗余问题。注意:虚拟继承仅在菱形继承场景使用,普通继承无需使用,否则会增加内存和时间开销!

八、继承与组合的选择原则

继承和组合都是代码复用的重要方式,各有适用场景,核心区别在于:

  • 继承:体现"is-a"关系(如"学生是一个人"),是一种强耦合关系,派生类依赖基类的实现,基类变化会影响派生类;
  • 组合:体现"has-a"关系(如"汽车有一个发动机"),是一种弱耦合关系,类通过包含其他类的对象实现功能,不依赖其内部实现。

8.1 组合的优势与应用场景

实际开发中优先使用组合,因为它具有更低的耦合度和更高的灵活性。示例如下:

// 发动机类
class Engine {
public:
    void run() { cout << "发动机启动" << endl; }
    void stop() { cout << "发动机关闭" << endl; }
};

// 汽车类(组合发动机,体现"has-a"关系)
class Car {
private:
    Engine _engine; // 组合发动机对象
public:
    void drive() {
        _engine.run(); // 使用发动机功能
        cout << "汽车行驶中" << endl;
    }
    void park() {
        _engine.stop(); // 使用发动机功能
        cout << "汽车已停车" << endl;
    }
};

int main() {
    Car c;
    c.drive();
    c.park();
    return 0;
}

九、总结

  1. 继承的核心价值是实现类层次的代码复用,减少冗余;
  2. 访问权限由基类成员限定符和继承方式共同决定,实际开发中优先使用public继承;
  3. 基类与派生类的转换遵循"向上兼容"原则,向下转换需谨慎;
  4. 同名成员会触发隐藏规则,继承体系中应避免定义同名成员;
  5. 派生类默认成员函数需正确处理基类部分的初始化与清理;
  6. 菱形继承问题通过虚拟继承解决,但应尽量避免复杂的多继承结构;
  7. 设计时优先考虑组合,仅在必要时使用继承,遵循"高内聚低耦合"原则。

十、结语

今日C++到这里就结束啦,如果觉得文章还不错的话,可以三连支持一下。感兴趣的宝子们欢迎持续订阅小枫,小枫在这里谢谢宝子们啦~小枫の主页还有更多生动有趣的文章,欢迎宝子们去点评鸭~C++的学习很陡,时而巨难时而巨简单,希望宝子们和小枫一起坚持下去~你们的三连就是小枫的动力,感谢支持~


网站公告

今日签到

点亮在社区的每一天
去签到