文章目录
须知
💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力!
👍 点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力!
🚀 分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对C++感兴趣的朋友,让我们一起进步!
接上篇:【C++进阶篇】像传承家族宝藏一样理解C++继承-CSDN博客
前言
在C++编程中,继承是面向对象编程(OOP)的一项核心特性,它让我们能够通过创建类的层次结构来实现代码重用,提高开发效率。然而,继承并不总是那么简单,尤其是在我们深入到更复杂的场景时。本文将带领你从基础继承走向更为进阶的继承应用,探讨C++中继承的多个高级概念,帮助你在理解继承机制的同时,避免常见的陷阱,提升代码的可维护性和可扩展性。
6. 继承与友元、静态成员
6.1 继承与友元
在 C++ 中,友元是一种特殊机制,它允许指定的非成员函数或者其他类访问类的私有成员和保护成员。然而,友元关系不能继承,也就是说,基类友元不能访问子类私有和保护成员,反之亦然。
6.1.1 友元函数定义
如果基类定义了一个友元函数,该友元函数只能访问基类的私有和保护成员,而不能访问派生类的私有或保护成员。反之,如果友元函数在派生类中定义,它也无法访问基类的私有和保护成员。
示例代码:
#include<iostream>
#include<string>
using namespace std;class Student; // 前向声明 Student 类class Person
{
public:// 声明友元函数,友元函数需要访问 Person 和 Student 的私有成员friend void ShowData(const Person& s, const Student& t);protected:string _name="Jack";
};class Student : public Person
{
public:// 声明友元函数,友元函数需要访问 Person 和 Student 的私有成员friend void ShowData(const Person& s, const Student& t);protected:int _stuNum=1234567890;
};// 定义友元函数 ShowData
void ShowData(const Person& s, const Student& t)
{cout << s._name << endl; // 访问 Person 类的 _name 成员cout << t._stuNum << endl; // 访问 Student 类的 _stuNum 成员
}int main()
{Person p;Student s;ShowData(p, s);return 0;
}
输出:
Jack
1234567890
在以上代码中,ShowData
函数既是 Person
类的友元,又是Student类的友元,它可以访问 Person
的保护成员 _name,也可以访问Student的保护成员_stuNum
。
6.2 继承与静态成员
C++ 中的静态成员在继承关系中具有一些特殊的行为。无论继承了多少次,基类中的静态成员在整个继承体系中始终只有一个实例。派生类可以共享访问基类中的静态成员。
6.2.1 静态成员的继承与访问
所有的派生类都只共享一个基类的静态成员,有且仅是唯一的。
示例代码:
#include<iostream>
#include<string>
using namespace std;class Person
{
public:Person()//Person类的构造函数{++_scout;}
protected:string _name = "";
public:static int _scout ;//基类中的静态成员变量
};
int Person::_scout = 0;//所有派生类共享的静态变量class Student :public Person
{
public:
protected:int _age = 18;
};class Graduate : public Student
{protected:string _seminarCourse; // 研究科目
};int main()
{Student s1;//派生类实例化出对象时会自动基类的构造函数Student s2;Student s3;Graduate s4;cout << " 人数 :" << Person::_scout << endl;Student::_scout = 0;//Person::_scout = 0;cout << " 人数 :" << Person::_scout << endl;return 0;
}
输出:
人数 :4
人数 :0
在以上代码中,_scout 是 Person 类的静态成员,用于统计创建的 Person 对象数量。由于 Student 类继承自 Person,因此 Student 也可以访问 _scout。无论是通过 Person::_scout 还是 Student::_scout,它们都指向同一个静态成员。
- 第一次输出:输出
4
,因为一共创建了 4 个对象(s1
,s2
,s3
,s4
),每个对象的创建都导致_scout
增加。 - 第二次输出:输出
0
,因为在程序中已经通过Student::_scout = 0;
将静态成员_scout
重置为 0。
图解步骤说明
-
类
Person
:Person
类有一个静态变量_scout
,每当Person
类或派生类的对象被创建时,这个静态变量会增加 1。
-
类
Student
:Student
类是Person
类的派生类。当创建Student
对象时,Person
类的构造函数被调用,从而导致_scout
的自增。
-
类
Graduate
:Graduate
类继承自Student
类,因此创建Graduate
对象时,Person
类的构造函数同样会被调用,使_scout
继续增加。
-
main
函数:- 在
main
函数中,我们创建了3
个Student
对象(s1
,s2
,s3
)和一个Graduate
对象(s4
)。每个对象的创建都会使_scout
增加 1。 - 然后,通过
Person::_scout
输出_scout
的值。
- 在
7. 复杂的菱形继承及虚拟继承
7.1 菱形继承问题
菱形继承是 C++ 多重继承中的一种特殊情况。当一个类从两个基类继承,而这两个基类又有共同的基类时,就会形成一个菱形结构。菱形继承会导致基类的多次实例化,进而引发数据冗余和二义性问题
7.1.1 菱形继承基本结构
菱形继承是指一种特殊的多重继承关系,其中两个子类分别继承自同一个父类,而另一个子类同时继承自这两个子类。其形状类似菱形,因此得名“菱形继承”。
简单展示一下:
class A{
public:int _a;
};
class B:public A{};
class C:public A{};
class D :public B , public C{};
图结构:
在上述代码中,派生类B和C都继承了A,且D又继承了B和C间接导致D继承了A,存在两份A。这就导致了数据冗余和访问的二义性。
7.2 菱形继承的二义性问题
在上述代码中,假如B类中存在与A类相同的成员变量名,使用D构造出一个对象,当D访问该变量名时,无法确定访问哪一个。
示例代码:
int main()
{D d;d._a;//error:对“_a”的访问不明确return 0;
}
编译器报出错误:对“_a”的访问不明确,编译器无法确定_a是从B还是C中继承过来的,这种二义性问题在实际开发中会带来严重的维护和理解困难。
7.3 解决方案:虚拟继承
虚拟继承可以解决菱形继承中的数据冗余和二义性问题。通过虚拟继承,派生类会共享同一个虚基类的实例,从而避免基类被多次实例化。
7.3.1 什么是虚拟继承?
虚拟继承通过在继承时使用
virtual
关键字,指示编译器在继承关系中只生成一个基类实例,从而解决数据冗余和二义性问题。
示例代码:
#include <iostream>
using namespace std;class A {
public:A() { cout << "A's constructor" << endl; }virtual void show() { cout << "A's show()" << endl; }
};class B : virtual public A {
public:B() { cout << "B's constructor" << endl; }void show() override { cout << "B's show()" << endl; }
};class C : virtual public A {
public:C() { cout << "C's constructor" << endl; }void show() override { cout << "C's show()" << endl; }
};class D : public B, public C {
public:D() { cout << "D's constructor" << endl; }void show() override { cout << "D's show()" << endl; }
};int main() {D d; // 创建 D 的对象d.show();return 0;
}
输出:
A's constructor
B's constructor
C's constructor
D's constructor
D's show()
解释
- 构造顺序:在
D
的构造过程中,A
的构造函数只调用一次,因为B
和C
都通过虚继承来继承A
。因此,A
的构造函数先于B
和C
被调用。 show
方法:由于D
重写了show
方法,因此调用d.show()
输出的是D's show()
。
7.4 虚基表(VBTable)与虚基类指针(VBPTR)
在虚拟继承中,编译器会在每个虚基类对象中加入一个指向虚基表(VBTable)的指针,即虚基类指针(VBPTR),用于存储偏移量信息。
7.4.1 虚基表的工作机制
虚基表中存储的是虚基类相对于派生类对象的偏移量。通过虚基类指针,派生类对象可以在运行时计算出虚基类在内存中的实际位置。
示例内存布局:
0x005EF75C
处存储了D
对象的起始地址。- 虚基类指针(
VBPTR
)指向虚基表(VBTable
)。 - 虚基表中的偏移量帮助定位虚基类
A
在D
对象内存中的实际位置。
7.4.2 偏移量的用途
偏移量的设计让编译器能够在运行时调整虚基类的位置,确保派生类在访问基类成员时能够定位到唯一的基类实例。
在虚拟继承中,虚基表中的偏移量解决了菱形继承中的访问问题,使得派生类 D
能够直接访问基类 A
的成员,而不会再有二义性。
int main() {
D d;
d._a = 7; // true:通过虚基表解决了二义性
return 0;
}
D对象通过虚基表定位到A的唯一实例,访问A类的成员变量 _a 合法。
7.5 虚拟继承优缺点
7.5.1 优点:
- 避免冗余:虚拟继承解决了多重继承中的冗余问题,确保多个派生类共享同一个基类实例。
- 消除二义性:解决了菱形继承中的二义性问题,使得派生类可以明确继承父类。
- 控制构造函数:通过虚拟继承,派生类避免了多次调用同一基类的构造函数。
- 更清晰的继承结构:通过共享基类实例,继承关系变得更加清晰。
7.5.2 缺点:
- 增加复杂性:虚拟继承增加了类层次结构的复杂度,使得代码更难理解和维护。
- 性能开销:虚拟继承引入了额外的内存和时间开销,可能影响性能。
- 构造顺序不可控:虚拟继承的构造顺序是固定的,可能不符合实际需求。
- 成员访问复杂:通过虚拟继承访问基类成员时可能需要显式指定,增加了访问的复杂性。
- 不必要的共享:某些情况下虚拟继承可能导致不必要的基类共享,增加了复杂性和开销。
8.虚拟继承与传统继承对比
8.1 定义和区别
8.2 构造函数和析构函数的差异
8.3 访问基类成员的方式
8.4 内存和性能影响
8.5 二义性问题
8.6 代码示例对比
8.6.1 传统继承示例:
class A {
public:void show() { cout << "A's show()" << endl; }
};class B : public A {
public:void show() { cout << "B's show()" << endl; }
};class C : public A {
public:void show() { cout << "C's show()" << endl; }
};class D : public B, public C {// 这里会产生二义性问题,无法确定调用哪个类的 show()
};
8.6.2 虚拟继承示例:
class A {
public:void show() { cout << "A's show()" << endl; }
};class B : virtual public A {
public:void show() { cout << "B's show()" << endl; }
};class C : virtual public A {
public:void show() { cout << "C's show()" << endl; }
};class D : public B, public C {// 虚拟继承消除了二义性,D 共享 A 的唯一实例
};
8.7 虚拟继承最佳实践
8.7.1 小心使用多层次的虚拟继承
虚拟继承可以解决菱形继承的问题,但如果继承层次过多,代码的可读性和维护性会大幅降低。因此,在设计类层次结构时,应尽量保持清晰和简洁。
8.8 实际项目中的案例
假设我们在开发一个多层次的企业管理系统,系统需要表示不同层级的员工(如员工、经理、总经理等),并且系统中有多个部门(如财务、技术等)。这些层级的员工都有一些共同的功能,如工作、考勤等。
#include <iostream>
using namespace std;class Person {
public:void work() {cout << "Person working!" << endl;}
};class Employee : virtual public Person {
public:void attendMeeting() {cout << "Employee attending meeting." << endl;}
};class Manager : virtual public Person {
public:void manageTeam() {cout << "Manager managing team." << endl;}
};class Director : public Employee, public Manager {
public:void makeDecision() {cout << "Director making decision." << endl;}
};int main() {Director d;d.work(); // 正常调用work(),不会出现二义性问题d.attendMeeting();d.manageTeam();d.makeDecision();return 0;
}
8.8.1 解释和分析
-
虚拟继承:在
Employee
和Manager
类中,Person
类是虚拟继承的,这意味着 Employee 和 Manager 共享同一个Person
类的实例。因此,Director
类在继承Employee
和Manager
时,只会有一个Person
类实例。 -
避免二义性:由于虚拟继承确保了
Person
类的唯一实例,Director
类调用work()
时不再存在二义性问题,编译器明确知道应该调用哪个Person
类的work()
方法。
8.8 总结
总的来说,虚拟继承能够解决传统多重继承中存在的重复继承和二义性问题,尤其适用于复杂的继承关系。尽管虚拟继承增加了代码的复杂性和性能开销,但它提供了一个更清晰的多重继承解决方案,尤其是在菱形继承场景中。
9. 继承的总结与反思
9.1. 继承的最佳实践与建议
9.1.1 首选组合而非继承
在设计时,优先考虑使用组合来实现功能,而不是过度依赖继承。组合提供了更高的灵活性,可以避免继承中的一些复杂问题,如多重继承带来的二义性。
9.1.2 尽量使用虚拟继承解决菱形继承问题
在多重继承的情况下,使用虚拟继承来消除基类实例的冗余和二义性,尤其是在菱形继承结构中,这能够避免多个基类副本带来的问题。
9.1.3 避免深层次的继承链
尽量避免继承链过深的情况。深度继承容易导致代码难以理解,且修改基类时可能会影响到多个派生类,增加维护成本。适时考虑替换为接口或组合模式。
9.1.4 适当使用抽象类和纯虚函数
多态章节设计此内容,下篇文章将详细展开叙述。
通过抽象类和纯虚函数,我们可以确保派生类实现特定功能。抽象类提供了一种强制约束,要求派生类按照规范进行实现。
9.1.5 清晰的设计与文档
在设计继承关系时,确保类和方法的职责明确,避免继承结构的模糊性。文档化每个类的职责和继承关系,以便后续开发人员理解和维护。
9.2 继承的优势与应用
9.2.1 代码复用
继承允许派生类复用基类的代码,这样我们可以将公共的功能放在基类中,而将特有的功能放在派生类中,避免了代码的重复编写。
8.2.2 扩展性
继承使得系统能够随着需求变化灵活扩展。当我们需要增加新的功能或新类型的对象时,可以通过继承新的类并在其中添加额外的功能来实现,而不必修改已有的代码。
9.2.3 多态性
继承为多态提供了基础,尤其是当基类指针或引用指向派生类对象时,能够实现运行时动态绑定,从而根据对象的真实类型调用适当的方法。
9.3 继承的类型和特性
9.3.1 单继承
单继承是最简单的继承方式,一个子类从一个父类继承。这种方式结构清晰,容易理解,但在某些复杂场景下可能无法满足需求。
9.3.2 多重继承
多重继承是一个类继承多个父类。它虽然能够提供更丰富的功能,但也带来了问题,尤其是二义性问题,即派生类可能会从多个父类继承相同的成员,这会导致编译器无法决定应该使用哪个成员。
9.3.3 虚拟继承
虚拟继承通过引入共享基类的机制解决了多重继承中的菱形继承问题(即多个派生类从同一基类派生)。虚拟继承确保派生类只会拥有一个基类实例,消除了冗余和二义性问题。
9.3.4 抽象类与纯虚函数
继承还可以与抽象类和纯虚函数一起使用,来实现接口(接口类)和多态行为。抽象类不能直接实例化,只能作为派生类的基类,从而强制派生类实现特定的行为。
9.4 继承的常见问题与反思
9.4.1 二义性问题
在多重继承中,派生类如果继承了多个父类并且父类有相同成员(方法或属性),编译器就无法判断应该调用哪个父类的成员。为了解决这个问题,我们可以使用作用域解析符来明确指定父类的成员,或者使用虚拟继承来确保基类的唯一性。
9.4.2 构造函数与析构函数的调用顺序
继承中的构造函数和析构函数调用顺序是有规则的:
- 构造函数:从最基类到最派生类依次调用。
- 析构函数:从最派生类到最基类依次调用。
这个顺序确保了派生类能够在构造时初始化继承的基类部分,在析构时释放基类的资源。
9.4.3 父类构造函数的调用
派生类的构造函数会自动调用父类的构造函数,但如果父类没有默认构造函数,或者需要特殊参数时,派生类的构造函数需要显式调用父类构造函数。
9.4.4 访问控制的影响
基类的成员(如变量和方法)的访问控制(public、protected、private)会影响派生类的访问权限。虽然派生类可以继承基类的成员,但如果基类的成员是私有的,派生类无法直接访问。
9.4.5 继承的代码复杂性
虽然继承能够带来代码复用,但过度使用继承,尤其是多重继承,会使代码变得复杂、难以理解和维护。特别是当继承关系非常深时,可能会出现难以追踪的问题。因此,应该尽量避免深层次的继承链条,提倡使用组合和接口代替复杂的继承关系。
10. 最后
继承是面向对象设计中非常强大的工具,能够帮助我们简化代码,促进代码复用和扩展。然而,继承也伴随着一定的复杂性,尤其是多重继承、虚拟继承和访问控制等问题。我们在使用继承时,应该清晰地定义类的职责和关系,尽量避免复杂的继承结构,采用适当的设计模式和编程技巧,以保证代码的可读性、可维护性和灵活性。
继承本身并不是“坏”的,而是要根据具体的应用场景和设计需求来合理使用,避免不必要的复杂性。
路虽远,行则将至;事虽难,做则必成
亲爱的读者们,下一篇文章再会!!!