[C++语法] 继承 (用法详解)

1. 继承的概念及定义

1.1 继承的概念

核心概念

继承是面向对象中实现代码复用的核心机制,它允许基于已有类创建新类,保留原有特性并扩展新功能,形成类的层次结构。
举例说明

我们在设计Student(学生)和Teacher(教师)类时,两者都有大量重复的属性和方法:

  • 共同属性:姓名、地址、电话、年龄

  • 共同方法:身份认证(identity)

同时,它们也有各自特有的部分:

  • 学生特有:学号、学习行为

  • 教师特有:职称、授课行为

例如这样,就会使代码重复部分很多,显得冗余:

cpp 复制代码
class Student 
{
    public:
        string name;
        string address;
        string phone;
        int age;
  
        void identity() 
        { 
            cout << "我是学生" << endl; 
        }
    
        // 学生特有
        string studentId;
        void study() 
        { 
            cout << "学习中..." << endl; 
        }
};

class Teacher 
{
    public:
        string name;      // 重复!
        string address;   // 重复!
        string phone;     // 重复!
        int age;          // 重复!
    
        void identity() // 重复逻辑!
        { 
            cout << "我是老师" << endl; 
        }  
    
        // 老师特有
        string title;
        void teach() 
        { 
            cout << "授课中..." << endl; 
        }
};

通过继承,可以将共性提取到基类Person(人员)中,再由StudentTeacher继承,实现:

  1. 消除冗余:公共代码只写一次

  2. 层次清晰:建立"人员→学生/教师"的逻辑关系

  3. 易于扩展:新增人员类型只需继承基类

cpp 复制代码
// 1. 提取公共部分作为基类(如姓名,地址,手机号,年龄等)
class Person 
{
public:
    string name;
    string address;
    string phone;
    int age;
    
    void identity() 
    { 
        cout << "身份验证" << endl; 
    }
};

// 2. 学生类继承Person(如学号、学习状态、学生身份验证等)
class Student : public Person 
{
public:
    // 特有成员
    string studentId;
    
    void study() 
    { 
        cout << "学习中..." << endl; 
    }
    
    void student_identity()   
    { 
        cout << "我是学生" << endl; 
    }
};

// 3. 教师类继承Person(如职称、授课状态、教师身份验证等)
class Teacher : public Person 
{
public:
    // 特有成员
    string title;
    
    void teach() 
    { 
        cout << "授课中..." << endl; 
    }
    
    void teacher_identity() 
    { 
        cout << "我是老师" << endl; 
    }
};

总结一下继承的核心思想

  • 基类(Person):包含所有角色的公共属性和行为

  • 派生类(Student/Teacher):继承基类的公共特性,添加各自独有的特性


1.2 继承定义

1.2.1 定义格式

下面我们看到Person是基类,也称作父类。Student是派生类,也称作子类。(因为翻译的原因,所以 既叫基类/派生类,也叫父类/子类)

下面这个是上面的例子中复制过来的,大家可以看一下格式:

cpp 复制代码
class Student : public Person 

1.2.2 继承基类成员访问方式的变化

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式public >protected> private

  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。

  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实 际中扩展维护性不强。

1.3 继承类模板

cpp 复制代码
// 基类
class 基类名 
{
public:
    数据类型 公共变量;
    
    返回值类型 公共函数() 
    {
        // 函数实现
    }
};

// 派生类(继承基类)
class 派生类名 : 继承方式 基类名 
{
public:
    数据类型 特有变量;
    
    返回值类型 特有函数() 
    {
        // 函数实现
    }
};

2. 基类和派生类间的转换

public继承的派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。

基类对象不能赋值给派生类对象。

cpp 复制代码
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;
 
     // ⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的 
     Person pobj = sobj;
 
     //2.基类对象不能赋值给派⽣类对象,这⾥会编译报错 
     sobj = pobj;
 
     return 0;
}

3. 继承中的作用域

3.1 隐藏规则:

  1. 在继承体系中基类和派生类都有独立的作用域。

  2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。 (在派生类成员函数中,可以使用基类::基类成员显示访问)

  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(比如同样是swap,就算派生类返回类型为int,基类为double的情况也会被隐藏)

  4. 注意在实际中在继承体系里面最好不要定义同名的成员。

cpp 复制代码
#include <iostream>
using namespace std;

// 基类:车辆
class Vehicle 
{
public:
    string brand = "通用车辆";
    
    // 显示车辆信息
    void display() 
    {
        cout << "车辆品牌: " << brand << endl;
    }
    
    void start() 
    {
        cout << "车辆启动" << endl;
    }
};

// 派生类:汽车
class Car : public Vehicle 
{
public:
    string brand = "宝马";  // 与基类同名变量,隐藏基类的brand
    
    // 与基类同名函数,隐藏基类的display函数
    void display() 
    {
        cout << "汽车品牌: " << brand << endl;  // 这里访问的是Car::brand
        cout << "车辆品牌: " << Vehicle::brand << endl;  // 使用作用域解析访问基类成员
    }
    
    void start()  // 隐藏基类的start函数
    {
        cout << "汽车点火启动" << endl;
    }
};

int main() 
{
    Car myCar;
    
    // 1. 变量隐藏演示
    cout << "直接访问brand: " << myCar.brand << endl;  // 输出"宝马"
    
    // 2. 访问基类的隐藏变量
    cout << "访问基类brand: " << myCar.Vehicle::brand << endl;  // 输出"通用车辆"
    
    // 3. 函数隐藏演示
    myCar.display();  // 调用Car的display函数
    
    // 4. 访问基类的隐藏函数
    myCar.Vehicle::display();  // 调用Vehicle的display函数
    
    // 5. 自动调用隐藏的函数
    myCar.start();  // 调用Car的start函数
    myCar.Vehicle::start();  // 调用Vehicle的start函数
    
    return 0;
}

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

4.1 4个常见默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。

  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。

  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的 operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域

  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。

  5. 派生类对象初始化先调用基类构造再调派生类构造。

  6. 派生类对象析构清理先调用派生类析构再调基类的析构。

  7. 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// 基类
class Base 
{
public:
    // 1. 构造函数(没有默认构造函数)
    Base(int baseValue) : _baseValue(baseValue) 
    {
        cout << "Base构造函数,baseValue = " << _baseValue << endl;
    }
    
    // 2. 拷贝构造函数
    Base(const Base& other) : _baseValue(other._baseValue) 
    {
        cout << "Base拷贝构造函数,baseValue = " << _baseValue << endl;
    }
    
    // 3. 赋值运算符
    Base& operator=(const Base& other) 
    {
        if (this != &other) 
        {
            _baseValue = other._baseValue;
            cout << "Base赋值运算符,baseValue = " << _baseValue << endl;
        }
        return *this;
    }
    
    // 4. 析构函数(不加virtual,验证规则7)
    ~Base() 
    {
        cout << "Base析构函数" << endl;
    }
    
protected:
    int _baseValue;
};

// 派生类
class Derived : public Base 
{
public:
    // 1. 派生类构造函数必须调用基类构造函数
    Derived(int baseValue, int derivedValue) 
        : Base(baseValue),  // 显示调用基类构造函数
          _derivedValue(derivedValue) 
    {
        cout << "Derived构造函数,derivedValue = " << _derivedValue << endl;
    }
    
    // 2. 派生类拷贝构造函数必须调用基类拷贝构造函数
    Derived(const Derived& other) 
        : Base(other),  // 调用基类拷贝构造函数,完成基类部分的拷贝
          _derivedValue(other._derivedValue) 
    {
        cout << "Derived拷贝构造函数" << endl;
    }
    
    // 3. 派生类operator=必须调用基类operator=
    //    注意:派生类的operator=隐藏了基类的operator=
    Derived& operator=(const Derived& other) 
    {
        if (this != &other) 
        {
            // 显式调用基类的operator=
            Base::operator=(other);  // 使用作用域解析调用基类operator=
            
            _derivedValue = other._derivedValue;
            cout << "Derived赋值运算符" << endl;
        }
        return *this;
    }
    
    // 4. 派生类析构函数
    //    注意:析构函数名被编译器特殊处理为destructor()
    //    基类析构函数不加virtual时,和派生类析构函数构成隐藏关系
    ~Derived() 
    {
        cout << "Derived析构函数" << endl;
        // 派生类析构完成后,会自动调用基类析构函数
    }
    
private:
    int _derivedValue;
};

int main() 
{
    cout << "=== 测试构造函数调用顺序 ===" << endl;
    // 规则1和5:先调用基类构造,再调用派生类构造
    Derived d1(100, 200);
    
    cout << "\n=== 测试拷贝构造函数 ===" << endl;
    // 规则2:派生类拷贝构造必须调用基类拷贝构造
    Derived d2(d1);
    
    cout << "\n=== 测试赋值运算符 ===" << endl;
    // 规则3:派生类operator=必须调用基类operator=
    Derived d3(300, 400);
    d3 = d1;
    
    cout << "\n=== 测试析构函数调用顺序 ===" << endl;
    {
        // 创建一个局部对象,观察析构顺序
        Derived d4(500, 600);
        // 规则6:先调用派生类析构,再调用基类析构
    } // d4离开作用域,析构
    
    cout << "\n=== main函数结束,对象d1,d2,d3析构 ===" << endl;
    // 规则4和6:析构顺序与构造顺序相反
    return 0;
}

运行结果:

5. 继承与友元

友元关系不能继承,也就是说基类友元不能

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Student;

class Person
{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name; // 姓名
};

class Student : public Person
{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    int _stuNum; // 学号
};

void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}

int main()
{
    Person p;
    Student s;
    Display(p, s);
    return 0;
}

6. 继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有⼀个这样的成员。无论派生出多少个派生类,都只有⼀个static成员实例。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

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;
    
    // 这里的运行结果可以看到非静态成员_name的地址是不一样的
    // 说明派生类继承下来了,父派生类对象各有一份
    cout << &p._name << endl;
    cout << &s._name << endl;
    
    // 这里的运行结果可以看到静态成员_count的地址是一样的
    // 说明派生类和基类共用同一份静态成员
    cout << &p._count << endl;
    cout << &s._count << endl;
    
    // 公有的情况下,父派生类指定类域都可以访问静态成员
    cout << Person::_count << endl;
    cout << Student::_count << endl;
    
    return 0;
}

7. 多继承及其菱形继承问题

7.1 继承模型

单继承:一个派生类只有一个直接基类时称这个继承关系为单继承

多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后⾯。

菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

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()
{
    // 编译报错:error C2385: 对"_name"的访问不明确
    Assistant a;
    a._name = "peter";
    
    // 需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
    
    return 0;
}

8. 继承和组合

8.1 继承和组合

• public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

• 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

• 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用 (white-box reuse)。术语"白箱"是相对可视性而言:在继承方式中,基类的内部细节对派生类可 见。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依 赖关系很强,耦合度高。

• 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse), 因为对象的内部细节是不可见的。对象只以"黑箱"的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。

• 优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太 那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

// Tire(轮胎)和Car(车)更符合has-a的关系
class Tire 
{
protected:
    string _brand = "Michelin"; // 品牌
    size_t _size = 17; // 尺寸
};

class Car 
{
protected:
    string _colour = "白色"; // 颜色
    string _num = "陕ABIT00"; // 车牌号
    Tire _t1; // 轮胎
    Tire _t2; // 轮胎
    Tire _t3; // 轮胎
    Tire _t4; // 轮胎
};

// Car和BMW/Benz更符合is-a的关系
class BMW : public Car 
{
public:
    void Drive() { cout << "好开-操控" << endl; }
};

class Benz : public Car 
{
public:
    void Drive() { cout << "好坐-舒适" << endl; }
};

template<class T>
class vector {};

// stack和vector的关系,既符合is-a,也符合has-a
// 方式1:使用继承 (is-a)
template<class T>
class stack1 : public vector<T> {};

// 方式2:使用组合 (has-a)
template<class T>
class stack2
{
public:
    vector<T> _v;
};

int main()
{
    return 0;
}

学会了就给博主点个赞呗?(✪ω✪)

---------(如有问题,欢迎评论区提问)---------

相关推荐
lxl13073 小时前
C++算法(1)双指针
开发语言·c++
TT哇3 小时前
【实习 】银行经理端两个核心功能的开发与修复(银行经理绑定逻辑修复和线下领取扫码功能开发)
java·vue.js
逝水如流年轻往返染尘3 小时前
Java中的数组
java
淀粉肠kk3 小时前
C++11列表初始化:{}的革命性进化
c++
java1234_小锋3 小时前
Java高频面试题:BIO、NIO、AIO有什么区别?
java·面试·nio
zhooyu3 小时前
C++和OpenGL手搓3D游戏编程(20160207进展和效果)
开发语言·c++·游戏·3d·opengl
HAPPY酷3 小时前
C++ 和 Python 的“容器”对决:从万金油到核武器
开发语言·c++·python
用户8307196840823 小时前
Java IO三大模型(BIO/NIO/AIO)超详细总结
java
sheji34163 小时前
【开题答辩全过程】以 基于SSM的花店销售管理系统为例,包含答辩的问题和答案
java