C++继承详解:菱形问题、虚继承原理与组合优先原则

一、继承的概念及定义

1.1 继承的概念

继承是面向对象程序设计中使代码可以复用的一种方法,允许我们在保持原有类的基础上进行扩展,增加成员函数以及成员变量,这样产生新的类叫做派生类(也叫做子类) ,而被继承的类叫做基类(也叫做父类)

下面来看一组代码,在下面的代码中有两个类,TeacherStudent

在这两个类中重复的成员变量和成员函数:身份认证函数、姓名与年龄成员变量。

不同的只有他们各自的学习函数和授课函数,还有学号与职称的成员变量。

cpp 复制代码
class Teacher
{
public:
    void identity() {}	// 身份认证
    void teaching() {}	// 授课

    string _name;  // 姓名
    int _age;      // 年龄

    int _title;    // 职称
};

class Student
{
public:
    void identity() {}	// 身份认证
    void study() {}	// 学习

    string _name;  // 姓名
    int _age;      // 年龄

    int _stuid;    // 学号
};

下面的代码定义一个Person类,将TeacherStudent两个类中重复的部分都放在Person类中,就可以复用这些成员,免去了重复定义的麻烦。

cpp 复制代码
class Person
{
public:
	void identity() {}	// 身份认证
	string _name;  // 姓名
	int _age;	   // 年龄
};

class Student :public Person
{
public:
	void study() {}	// 学习
	int _stuid;	   // 学号
};

class Teacher :public Person
{
public:
	void teaching() {}	// 授课
	int _title;	   // 职称
};

1.2 继承的定义

1.2.1 继承的语法

class 派生类: 继承方式 基类

在上面的代码中,Person类就是基类(也叫做父类) ,而TeacherStudent两个类就是派生类(也叫做子类)

还可以看到需要有继承方式,而继承方式共有三种:

  • 公共继承:public
  • 保护继承:protected
  • 私有继承:private

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

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 派生类中不可见 派生类中不可见 派生类中不可见
  1. 基类private成员在派生类中无论以什么方式继承都不可见。这里的不可见指基类的私有成员还是被继承到了派生类对象中,但是因为语法限制派生类对象无论是类内还是类外都不可访问。
  2. 如果基类成员不想被类外访问,但需要在派生类中访问,可以使用protected方式继承。
  3. 使用class 时默认继承方式是private ,使用struct 时默认继承方式是public,不过最好还是显式写出继承方式。
  4. 实际运用中普遍都是public继承,很少使用另外两种方式,因为在实际中扩展维护性不强。

1.3 继承类模板

cpp 复制代码
template<class T>
class stack : public std::vector<T>
{
public:
	void push(const T& x)
	{
		// 基类是类模板时需要指定类域
		// 因为stack<int>实例化时也实例化vector<int>
		// 但模板是按需实例化,push_back等成员函数未实例化
		vector<T>::push_back(x);
	}

	// ...
};

int main()
{
	stack<int> st;
	st.push_back(1);
	st.push_back(2);
	st.push_back(3);
	return 0;
}

二、基类和派生类间的转换

  • public继承的派生类对象 可以赋值给基类的指针/引用,有个形象的说法叫切片或者切割。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针或引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
cpp 复制代码
class Person
{
protected:
	string _name;
	string _sex;
	int _age;
};

class Student :public Person
{
public:
	int _stuid;
};

int main()
{
	Student s1;
	// 1.派生类对象可以赋值给基类的指针/引用
	Person* pp = &s1;
	Person& pp = s1;
	return 0;
}

三、继承中的作用域

3.1 隐藏规则

  1. 在继承中基类和派生类都有独立的作用域。
  2. 派生类和基类中有同名成员,派生类成员将屏蔽基类同名成员的直接访问,这种情况叫隐藏,可以使用基类::基类成员显式访问。
  3. 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 在实际应用中最好不要定义同名成员。
cpp 复制代码
class Person
{
protected:
	string _name = "张三";
	int _num = 18;	// 年龄
};

class Student :public Person
{
public:
	void Print()
	{
		// 这里_name成员没有重复
		cout << "姓名:" << _name << endl;
		// _num重复,所以需要指定作用域
		cout << "年龄:" << Person::_num << endl;
		cout << "学号:" << _num << endl;
	}
	
	int _num = 999;	  // 学号
};

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

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

4.1 默认成员函数

  1. 派⽣类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显式调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝构造。
  3. 派生类的operator=必要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用后自动调用基类的析构函数清理基类成员。
  5. 派生类对象初始化先调用基类构造再调用派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调用基类析构。
  7. 在多态中一些场景析构函数需要重写,重写的条件之一是函数名相同(在后面多态文章中会解释)。编译器会对析构函数名进行特殊处理为destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏。

五、继承与友元

友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员。

六、继承与静态成员

基类定义了static 成员,那么整个继承体系中只有一个这样的static成员,无论有多少个派生类。

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

7.1 继承模型

  • 单继承:一个派生类只有一个基类时称为单继承。
  • 多继承:一个派生类有两个或以上的直接基类时成为多继承,多继承在内存中的模型是:先继承的基类在前,后继承的基类在后,派生类成员在最后。
  • 菱形继承:是多继承的一种特殊情况,存在数据冗余和二义性的问题,实践中不建议设计菱形继承。

7.2 虚继承

虚继承 是解决多重继承中 菱形继承问题 的关键机制。它通过确保 共享基类(虚基类)在继承体系中只保留一份副本,避免了数据冗余和二义性。

假设有以下继承关系:

cpp 复制代码
class A {
public:
    int value;
};

class B : public A {};   // 普通继承
class C : public A {};   // 普通继承
class D : public B, public C {}; // 多重继承

此时可以在继承前加virtual关键字。

cpp 复制代码
class A {
public:
    int value;
};

class B : virtual public A {}; // 虚继承
class C : virtual public A {}; // 虚继承
class D : public B, public C {};

但虚继承会增加复杂度,仅在必要时使用,虚继承可能引入额外的间接访问开销,多继承可认为是C++的缺陷之一,所以后来例如Java语言等都没有多继承。

八、继承和组合

优先使用组合

  • "组合优于继承" 是面向对象设计的经典原则(《设计模式》中强调)。
  • 原因:组合更灵活、耦合度更低,符合开放-封闭原则(对扩展开放,对修改封闭)。
相关推荐
1024熙12 分钟前
【C++】——lambda表达式
开发语言·数据结构·c++·算法·lambda表达式
mahuifa1 小时前
(2)VTK C++开发示例 --- 绘制多面锥体
c++·vtk·cmake·3d开发
23级二本计科2 小时前
C++ Json-Rpc框架-3项目实现(2)
服务器·开发语言·c++·rpc
rigidwill6662 小时前
LeetCode hot 100—搜索二维矩阵
数据结构·c++·算法·leetcode·矩阵
矛取矛求2 小时前
栈与队列习题分享(精写)
c++·算法
摆烂能手3 小时前
C++基础精讲-02
开发语言·c++
胡乱儿起个名4 小时前
C++ 标准库中的 <algorithm> 头文件算法总结
开发语言·c++·算法
政安晨5 小时前
【嵌入式人工智能产品开发实战】(二十)—— 政安晨:小智AI嵌入式终端代码解读:【C】关于项目中的MQTT+UDP核心通信交互理解
网络·c++·mqtt·网络协议·udp·小智ai·实时打断
六bring个六6 小时前
C++双链表介绍及实现
开发语言·数据结构·c++
Ring__Rain8 小时前
visual studio 常用的快捷键(已经熟悉的就不记录了)
c++·git·visual studio