【C++】继承

目录

一、继承的概念及定义

[1.1 继承的概念](#1.1 继承的概念)

[1.2 继承定义与访问方式](#1.2 继承定义与访问方式)

[1.3 继承类模板](#1.3 继承类模板)
二、基类和派生类的转换
三、继承中的作用域
四、派生类的默认成员函数
五、继承与友元
六、继承与静态成员
七、多继承及其菱形继承问题

[7.1 继承模型](#7.1 继承模型)

[7.2 虚继承](#7.2 虚继承)
八、继承和组合
九、实践建议


一、继承的概念及定义

1.1 继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。

继承的优势

  • 代码复用:避免重复定义相同成员
  • 层次结构:体现现实世界的层次关系
  • 扩展性:在基类基础上添加新功能

示例:没有继承时的冗余设计

cpp 复制代码
class Student {
public:
    void identity() { /* ... */ }
    void study() { /* ... */ }
protected:
    string _name = "peter";
    string _address;
    string _tel;
    int _age = 18;
    int _stud;  // 学号
};

class Teacher {
public:
    void identity() { /* ... */ }
    void teaching() { /* ... */ }
protected:
    string _name = "张三";
    int _age = 18;
    string _address;
    string _tel;
    string _title;  // 职称
};

使用继承优化后的设计

cpp 复制代码
class Person {
public:
    void identity() {
        cout << "void identity()" << _name << endl;
    }
protected:
    string _name = "张三";
    string _address;
    string _tel;
    int _age = 18;
};

class Student : public Person {
public:
    void study() { /* ... */ }
protected:
    int _stud;  // 学号
};

class Teacher : public Person {
public:
    void teaching() { /* ... */ }
protected:
    string _title;  // 职称
};

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

1.2 继承定义与访问方式

继承定义格式

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

// 示例
class Student : public Person {
public:
    int _stud;   // 学号
    int _major;  // 专业
};

继承方式与访问限定符

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 在派生类中不可见 在派生类中不可见 在派生类中不可见

重要规则

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的
  2. 基类protected成员为继承而设计,在派生类中可以访问
  3. 访问方式 = Min(成员在基类的访问限定符,继承方式),public > protected > private
  4. class默认private继承,struct默认public继承
  5. 实际开发中主要使用public继承

示例演示

cpp 复制代码
class Person {
public:
    void Print() {
        cout << _name << endl;
    }
protected:
    string _name;  // 姓名
private:
    int _age;      // 年龄
};

class Student : public Person {
protected:
    int _stunum;   // 学号
    // _name在Student中为protected成员
    // _age在Student中不可见
};

1.3 继承类模板

类模板也可以被继承,在继承类模板时需要注意模板实例化的问题:

cpp 复制代码
namespace bit {
    template<class T>
    class stack : public std::vector<T> {
    public:
        void push(const T& x) {
            // 基类是类模板时,需要指定类域
            std::vector<T>::push_back(x);
        }
        
        void pop() {
            std::vector<T>::pop_back();
        }
        
        const T& top() {
            return std::vector<T>::back();
        }
        
        bool empty() {
            return std::vector<T>::empty();
        }
    };
}

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

二、基类和派生类的转换

  • public继承的派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI(Run-Time Type Information)的 dynamic_cast 来进行识别后进行安全转换。
cpp 复制代码
class Person {
protected:
    string _name;  // 姓名
    string _sex;   // 性别  
    int _age;      // 年龄
};

class Student : public Person {
public:
    int _No;       // 学号
};

int main() {
    Student sobj;
    
    // 1. 派生类对象可以赋值给基类的指针/引用
    Person* p = &sobj;     // 切片
    Person& rp = sobj;     // 切片
    
    // 2. 派生类对象可以赋值给基类对象(调用拷贝构造)
    Person pobj = sobj;    // 切片
    
    // 3. 基类对象不能赋值给派生类对象
    // sobj = pobj;        // 错误!
    
    return 0;
}

三、继承中的作用域

隐藏规则

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问(隐藏)
  3. 成员函数的隐藏只需要函数名相同就构成隐藏
  4. 可以使用基类::基类成员显式访问被隐藏的成员
cpp 复制代码
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;              // 访问派生类的_num
    }
protected:
    int _num = 999;        // 学号 - 隐藏基类的_num
};

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

函数隐藏示例

cpp 复制代码
class A {
public:
    void fun() {
        cout << "func()" << endl;
    }
};

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

int main() {
    B b;
    b.fun(10);            // 正确
    // b.fun();           // 错误!A::fun()被隐藏
    b.A::fun();           // 正确,显式调用
    return 0;
}

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

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

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。需要注意的是派生类的 operator= 隐藏了基类的 operator=,所以显式调用基类的 operator=,需要指定基类作用域。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类的析构。
  7. 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成 destructor(),所以基类析构函数不加 virtual 的情况下,派生类析构函数和基类析构函数构成隐藏关系。
cpp 复制代码
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) {
        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:
    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) {
        cout << "Student& operator=(const Student& s)" << endl;
        if (this != &s) {
            Person::operator=(s);          // 调用基类operator=
            _num = s._num;
        }
        return *this;
    }
    
    ~Student() {
        cout << "~Student()" << endl;
        // 自动调用基类析构函数
    }

protected:
    int _num;
};

int main() {
    Student s1("jack", 18);
    Student s2(s1);        // 拷贝构造
    Student s3("rose", 17);
    s1 = s3;              // 赋值操作
    
    return 0;
}

实现不能被继承的类

cpp 复制代码
// 方法1:C++98 - 构造函数私有化
class Base {
private:
    Base() {}            // 构造函数私有,派生类无法调用
public:
    static Base* Create() { return new Base; }
};

// 方法2:C++11 - 使用final关键字
class Base final {       // final修饰,不能被继承
public:
    void func() { cout << "Base::func" << endl; }
};

class Derive : public Base {  // 错误!Base是final类
    void func4() { cout << "Derive::func4" << endl; }
};

五、继承与友元

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

cpp 复制代码
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的友元
    // cout << s._stuNum << endl;   // 错误!不能访问Student的protected成员
}

// 解决方案:让Display也成为Student的友元
class Student : public Person {
    friend void Display(const Person& p, const Student& s);
protected:
    int _stuNum;
};

六、继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员:

cpp 复制代码
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;
}

七、多继承及其菱形继承问题

7.1 继承模型

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

cpp 复制代码
class Person {};
class Student : public Person {};
class PostGraduate : public Student {};

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

cpp 复制代码
class Student {};
class Teacher {};
class Assistant : public Student, public Teacher {};

菱形继承 :多继承的特殊情况,存在数据冗余和二义性问题

cpp 复制代码
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";           // 错误!二义性
    a.Student::_name = "xxx";       // 解决二义性
    a.Teacher::_name = "yyy";       // 但数据冗余问题仍在
    
    return 0;
}

7.2 虚继承

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

cpp 复制代码
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";    // 正确!没有二义性
    return 0;
}

虚继承的构造函数处理

cpp 复制代码
class Person {
public:
    Person(const char* name) : _name(name) {}
    string _name;
};

class Student : virtual public Person {
public:
    Student(const char* name, int num) 
        : Person(name), _num(num) {}
protected:
    int _num;
};

class Teacher : virtual public Person {
public:
    Teacher(const char* name, int id)
        : Person(name), _id(id) {}
protected:
    int _id;
};

class Assistant : public Student, public Teacher {
public:
    Assistant(const char* name1, const char* name2, const char* name3)
        : Person(name3)                    // 虚基类由最终派生类初始化
        , Student(name1, 1)
        , Teacher(name2, 2) {}
protected:
    string _majorCourse;
};

八、继承和组合

继承 vs 组合

特性 继承 组合
关系 is-a关系 has-a关系
复用类型 白箱复用 黑箱复用
耦合度
封装性 破坏封装 保持封装

继承示例

cpp 复制代码
class Car {
protected:
    string _colour = "白色";
    string _num = "陕ABIT00";
};

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

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

组合示例

cpp 复制代码
class Tire {                      // 轮胎类
protected:
    string _brand = "Michelin";
    size_t _size = 17;
};

class Car {                       // 汽车类
protected:
    string _colour = "白色";
    string _num = "陕ABIT00";
    Tire _t1, _t2, _t3, _t4;     // has-a关系
};

设计原则

  1. 优先使用组合:组合的耦合度低,代码维护性好
  2. 适合继承时用继承:类之间是is-a关系且需要多态时
  3. 避免过度设计:根据实际需求选择合适的关系

九、实践建议

  • 优先使用public继承
  • 避免菱形继承,如必须使用则用虚继承
  • 优先考虑组合而非继承
  • 注意继承中的隐藏规则和作用域
  • 正确实现派生类的默认成员函数
相关推荐
权泽谦2 小时前
用 Python 做一个天气预报桌面小程序(附源码 + 打包与部署指导)
开发语言·python·小程序
ftpeak2 小时前
《Rust+Slint:跨平台GUI应用》第八章 窗体
开发语言·ui·rust·slint
森语林溪2 小时前
大数据环境搭建从零开始(十七):JDK 17 安装与配置完整指南
java·大数据·开发语言·centos·vmware·软件需求·虚拟机
lsx2024063 小时前
HTML 音频(Audio)详解
开发语言
woshihonghonga3 小时前
【动手学深度学习】
开发语言·python
威风的虫3 小时前
ES6 数组方法:告别循环,拥抱函数式编程
开发语言·前端·javascript
乱舞八重击(junluoyu)3 小时前
1.PagedAtteion算法
c++
码界筑梦坊3 小时前
240-基于Python的医疗疾病数据可视化分析系统
开发语言·python·信息可视化·数据分析·毕业设计·echarts
2301_803554523 小时前
C++ 锁类型大全详解
开发语言·c++