【C++】深入理解 C++ 中的继承进阶:多继承、菱形继承及其解决方案

个人主页: 起名字真南的CSDN博客

个人专栏:


目录

  • C++继承机制详解与代码示例
  • [📌1. 继承的基本概念](#📌1. 继承的基本概念)
  • [📌 2. 继承示例:`Student`和`Teacher`继承`Person`](#📌 2. 继承示例:StudentTeacher继承Person)
  • [📌 3. 继承类模板示例](#📌 3. 继承类模板示例)
  • [📌 4. 基类和派生类间的转换](#📌 4. 基类和派生类间的转换)
  • [📌 5. 菱形继承与虚继承](#📌 5. 菱形继承与虚继承)
  • [📌 6. 虚继承的实现](#📌 6. 虚继承的实现)
  • [📌 7. 继承与组合的区别](#📌 7. 继承与组合的区别)
  • [📌 总结](#📌 总结)

C++继承机制详解与代码示例

继承是面向对象编程(OOP)中的重要概念之一,通过继承,C++允许我们复用现有类的代码,并在此基础上进行扩展,以构建层次化的类结构。本文将结合详细的代码示例,讲解C++中的继承机制,包括单继承、多继承、菱形继承、虚继承、模板类继承等。学习这些概念将有助于我们在实际开发中设计更高效、可复用的代码结构。


📌1. 继承的基本概念

继承(inheritance)是指一个类可以获得另一个类的属性和方法,从而实现代码的复用。通过继承,派生类(也称子类)不仅能继承基类(也称父类)的成员和行为,还可以扩展或修改这些行为。C++中继承形成了"从一般到特殊"的层次结构,并且有以下几种访问控制方式:

  • public继承 :基类的publicprotected成员在派生类中分别保持为publicprotected
  • protected继承 :基类的public成员变为protectedprotected成员保持不变。
  • private继承 :基类的publicprotected成员都变为private

示例代码展示了如何通过继承减少重复代码,提升代码复用率。


📌 2. 继承示例:StudentTeacher继承Person

在下面的示例中,我们定义了一个Person类,包含了一些基本的个人信息。然后我们通过继承创建Student类和Teacher类,分别表示学生和老师。这些类除了继承Person类的基本信息外,还包含各自特有的属性和方法。

cpp 复制代码
class Person {
public:
    void identity() {
        cout << "身份认证:" << _name << endl;
    }
protected:
    string _name = "默认名字";
    string _address;
    string _tel;
    int _age = 18;
};

class Student : public Person {
public:
    void study() {
        cout << _name << "在学习" << endl;
    }
protected:
    int _stuid;  // 学号
};

class Teacher : public Person {
public:
    void teaching() {
        cout << _name << "在授课" << endl;
    }
protected:
    string _title;  // 职称
};

在该示例中:

  • Person类包含了姓名、地址、电话、年龄等个人信息。
  • Student类继承了Person类,同时增加了学号(_stuid)和study()方法,用于学生的学习行为。
  • Teacher类同样继承自Person类,增加了职称(_title)和teaching()方法,用于表示老师的授课行为。

这种设计避免了在StudentTeacher中重复定义姓名、年龄等成员变量,使代码更加简洁。


📌 3. 继承类模板示例

C++支持模板类继承,通过继承标准库的容器类,我们可以轻松地扩展这些容器类的功能。以下示例展示了一个栈(Stack)类模板,分别继承自std::vectorstd::liststd::deque,从而实现栈的基本操作。

cpp 复制代码
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();
    }
    bool empty() {
        return std::vector<T>::empty();
    }
};

在这个模板类示例中,Stack类继承了std::vector模板类,并添加了push()pop()empty()方法:

  • push:在栈顶添加一个元素;
  • pop:移除栈顶的元素;
  • empty:判断栈是否为空。

模板类在继承时需要特别注意类域的使用。在上例中,vector<T>::push_back(x)明确指出调用vector模板类的成员函数push_back,以确保模板实例化时找到正确的成员函数。这种方式提供了简便的接口,使得Stack类直接具备了向量的功能而无需重写。


📌 4. 基类和派生类间的转换

在C++中,基类指针或引用可以指向派生类对象,从而通过基类指针或引用调用派生类对象的基类成员,这种机制称为"切片"或"切割"。以下代码示例展示了这种转换的用法:

cpp 复制代码
Student sobj;
Person* pp = &sobj;       // 基类指针指向派生类对象
Person& rp = sobj;        // 基类引用指向派生类对象

这种转换称为"向上转换"(upcasting),因为派生类对象可以转换为基类类型。这在多态性设计中非常有用,可以使代码更加通用。相反,基类对象不能直接转换为派生类对象,但可以通过强制类型转换来实现。这种转换称为"向下转换"(downcasting),需要开发者确保安全性,可以使用dynamic_cast来进行运行时类型检查。

注意:向下转换必须确认基类指针实际指向派生类对象,否则会引发运行时错误。


📌 5. 菱形继承与虚继承

C++支持多继承,即一个类可以继承自多个基类。然而,如果多个基类继承自相同的祖先类,就会导致菱形继承问题。如下所示,Assistant类继承了TeacherStudent,而这两个类都继承自Person

cpp 复制代码
class Person {
public:
    string _name = "莱昂纳多";
    int _num = 111;
};

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

class Assistant : public Teacher, public Student {
protected:
    string _major;
};

此时,Assistant类将拥有两份Person的成员变量,导致数据冗余。此外,访问_name等成员变量时会引起二义性问题。通过虚继承可以避免这一问题。


📌 6. 虚继承的实现

虚继承(virtual inheritance)是解决菱形继承问题的一种方法。通过虚继承,派生类可以确保祖先类的成员只有一份,从而消除了菱形继承中的数据冗余和访问二义性问题。以下代码展示了虚继承的用法:

cpp 复制代码
class Person {
public:
    string _name = "莱昂纳多";
    int _num = 111;
};

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

class Assistant : public Teacher, public Student {
protected:
    string _major;
};

int main() {
    Assistant a;
    a._name = "小李";  // 仅有一个 _name,避免二义性
    cout << "Name: " << a._name << endl;
    return 0;
}

通过在StudentTeacher类中使用virtual public继承Person,虚继承确保了Assistant类仅保留一份Person的成员变量,这样既解决了数据冗余问题,又避免了访问时的二义性。


📌 7. 继承与组合的区别

在面向对象设计中,继承组合是两个不同的设计思路:

  • 继承(is-a关系) :用于表示派生类是基类的一种特殊类型。继承常用于表示类之间的层次结构,例如Car类和BMW类。
  • 组合(has-a关系) :用于表示类之间的包含关系。组合常用于表示类之间的拥有关系,例如Car类包含多个Tire对象(轮胎)。

组合优于继承,通常能降低类与类之间的耦合性,使代码更加灵活。仅当派生类确实是基类的一种"特殊类型"时,才考虑使用继承。

例如,在以下代码中,Tire类表示轮胎,Car类组合了多个Tire对象,因为车和轮胎是拥有关系,而不是层次关系。

cpp 复制代码
class Tire {
protected:
    string _brand = "Michelin";  // 轮胎品牌
};

class Car {
protected:
    string _color = "白色";  // 车颜色
    Tire _tire1, _tire2, _tire3, _tire4;  // 4个轮胎
};

在该示例中,CarTire是组合关系,Car对象拥有四个`Tire

对象,说明两者间的关系是has-a`,更适合组合关系,而非继承关系。


📌 总结

C++的继承机制提供了代码复用和层次结构的基础,但其灵活性也带来了复杂性。本文介绍了C++继承的多种使用方式和注意事项,如模板类的继承、基类与派生类的转换、菱形继承和虚继承的使用等。菱形继承可能导致数据冗余和访问冲突,虚继承可以解决这一问题,但会增加实现的复杂性。因此,在设计中要谨慎使用继承,尽量优先选择组合关系,以降低耦合性,提高代码的可维护性和复用性。


相关推荐
守护者1702 分钟前
JAVA学习-练习试用Java实现“使用Arrays.toString方法将数组转换为字符串并打印出来”
java·学习
源码哥_博纳软云4 分钟前
JAVA同城服务场馆门店预约系统支持H5小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
禾高网络5 分钟前
租赁小程序成品|租赁系统搭建核心功能
java·人工智能·小程序
学会沉淀。11 分钟前
Docker学习
java·开发语言·学习
如若12312 分钟前
对文件内的文件名生成目录,方便查阅
java·前端·python
ragnwang28 分钟前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
初晴~43 分钟前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
黑胡子大叔的小屋1 小时前
基于springboot的海洋知识服务平台的设计与实现
java·spring boot·毕业设计
ThisIsClark2 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉2 小时前
【jvm】内存泄漏与内存溢出的区别
jvm