须知
💬 欢迎讨论:如果你在学习过程中有任何问题或想法,欢迎在评论区留言,我们一起交流学习。你的支持是我继续创作的动力! 👍 点赞、收藏与分享:觉得这篇文章对你有帮助吗?别忘了点赞、收藏并分享给更多的小伙伴哦!你们的支持是我不断进步的动力! 🚀 分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对C++感兴趣的朋友,让我们一起进步!
接上篇:【C++进阶篇】像传承家族宝藏一样理解C++继承-CSDN博客
前言
在C++编程中,继承是面向对象编程(OOP)的一项核心特性,它让我们能够通过创建类的层次结构来实现代码重用,提高开发效率。然而,继承并不总是那么简单,尤其是在我们深入到更复杂的场景时。本文将带领你从基础继承走向更为进阶的继承应用,探讨C++中继承的多个高级概念,帮助你在理解继承机制的同时,避免常见的陷阱,提升代码的可维护性和可扩展性。
在 C++ 中,友元是一种特殊机制,它允许指定的非成员函数或者其他类访问类的私有成员和保护成员。然而,友元关系不能继承,也就是说,基类友元不能访问子类私有和保护成员,反之亦然。
如果基类定义了一个友元函数,该友元函数只能访问基类的私有和保护成员,而不能访问派生类的私有或保护成员。反之,如果友元函数在派生类中定义,它也无法访问基类的私有和保护成员。
示例代码:
#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
。
C++ 中的静态成员在继承关系中具有一些特殊的行为。无论继承了多少次,基类中的静态成员在整个继承体系中始终只有一个实例。派生类可以共享访问基类中的静态成员。
基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员 。无论派生出多少个子
类,都只有一个 static 成员实例 。
所有的派生类都只共享一个基类的静态成员,有且仅是唯一的。
示例代码:
#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
的值。菱形继承是 C++ 多重继承中的一种特殊情况。当一个类从两个基类继承,而这两个基类又有共同的基类时,就会形成一个菱形结构。菱形继承会导致基类的多次实例化,进而引发数据冗余和二义性问题
菱形继承是指一种特殊的多重继承关系,其中两个子类分别继承自同一个父类,而另一个子类同时继承自这两个子类。其形状类似菱形,因此得名“菱形继承”。
简单展示一下:
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。这就导致了数据冗余和访问的二义性。
在上述代码中,假如B类中存在与A类相同的成员变量名,使用D构造出一个对象,当D访问该变量名时,无法确定访问哪一个。
示例代码:
int main()
{
D d;
d._a;//error:对“_a”的访问不明确
return 0;
}
编译器报出错误:对“_a”的访问不明确,编译器无法确定_a是从B还是C中继承过来的,这种二义性问题在实际开发中会带来严重的维护和理解困难。
虚拟继承可以解决菱形继承中的数据冗余和二义性问题。通过虚拟继承,派生类会共享同一个虚基类的实例,从而避免基类被多次实例化。
虚拟继承通过在继承时使用
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()。
在虚拟继承中,编译器会在每个虚基类对象中加入一个指向虚基表(VBTable)的指针,即虚基类指针(VBPTR),用于存储偏移量信息。
虚基表中存储的是虚基类相对于派生类对象的偏移量。通过虚基类指针,派生类对象可以在运行时计算出虚基类在内存中的实际位置。
示例内存布局:
0x005EF75C
处存储了 D
对象的起始地址。VBPTR
)指向虚基表(VBTable
)。A
在 D
对象内存中的实际位置。偏移量的设计让编译器能够在运行时调整虚基类的位置,确保派生类在访问基类成员时能够定位到唯一的基类实例。
在虚拟继承中,虚基表中的偏移量解决了菱形继承中的访问问题,使得派生类 D
能够直接访问基类 A
的成员,而不会再有二义性。
int main() { D d; d._a = 7; // true:通过虚基表解决了二义性 return 0; }
D对象通过虚基表定位到A的唯一实例,访问A类的成员变量 _a 合法。
virtual
关键字来声明,它用于解决多重继承中的菱形继承问题。在虚拟继承中,子类与其他派生类共享父类的唯一实例。虚拟继承确保在继承链中只有一个基类实例。D
继承了类 B
和类 C
,那么 B
和 C
会各自调用基类 A
的构造函数(重复调用)。D
)来调用。这是因为虚拟继承确保了所有的派生类共享一个基类实例。D
类继承了多个虚拟继承的基类,访问基类成员时可能需要通过作用域解析符(::
)明确指定。D
同时继承自 B
和 C
,而 B
和 C
都继承自 A
,那么在访问 A
的成员时,编译器无法判断应该访问哪个副本。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()
};
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 的唯一实例
};
虚拟继承可以解决菱形继承的问题,但如果继承层次过多,代码的可读性和维护性会大幅降低。因此,在设计类层次结构时,应尽量保持清晰和简洁。
has-a
关系)替代继承(is-a
关系),那么优先选择组合,这样可以降低代码的耦合度。假设我们在开发一个多层次的企业管理系统,系统需要表示不同层级的员工(如员工、经理、总经理等),并且系统中有多个部门(如财务、技术等)。这些层级的员工都有一些共同的功能,如工作、考勤等。
#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;
}
Employee
和 Manager
类中,Person
类是虚拟继承的,这意味着 Employee 和 Manager 共享同一个 Person
类的实例。因此,Director
类在继承 Employee
和 Manager
时,只会有一个 Person
类实例。
Person
类的唯一实例,Director
类调用 work()
时不再存在二义性问题,编译器明确知道应该调用哪个 Person
类的 work()
方法。
总的来说,虚拟继承能够解决传统多重继承中存在的重复继承和二义性问题,尤其适用于复杂的继承关系。尽管虚拟继承增加了代码的复杂性和性能开销,但它提供了一个更清晰的多重继承解决方案,尤其是在菱形继承场景中。
在设计时,优先考虑使用组合来实现功能,而不是过度依赖继承。组合提供了更高的灵活性,可以避免继承中的一些复杂问题,如多重继承带来的二义性。
在多重继承的情况下,使用虚拟继承来消除基类实例的冗余和二义性,尤其是在菱形继承结构中,这能够避免多个基类副本带来的问题。
尽量避免继承链过深的情况。深度继承容易导致代码难以理解,且修改基类时可能会影响到多个派生类,增加维护成本。适时考虑替换为接口或组合模式。
多态章节设计此内容,下篇文章将详细展开叙述。
通过抽象类和纯虚函数,我们可以确保派生类实现特定功能。抽象类提供了一种强制约束,要求派生类按照规范进行实现。
在设计继承关系时,确保类和方法的职责明确,避免继承结构的模糊性。文档化每个类的职责和继承关系,以便后续开发人员理解和维护。
继承允许派生类复用基类的代码,这样我们可以将公共的功能放在基类中,而将特有的功能放在派生类中,避免了代码的重复编写。
继承使得系统能够随着需求变化灵活扩展。当我们需要增加新的功能或新类型的对象时,可以通过继承新的类并在其中添加额外的功能来实现,而不必修改已有的代码。
继承为多态提供了基础,尤其是当基类指针或引用指向派生类对象时,能够实现运行时动态绑定,从而根据对象的真实类型调用适当的方法。
单继承是最简单的继承方式,一个子类从一个父类继承。这种方式结构清晰,容易理解,但在某些复杂场景下可能无法满足需求。
多重继承是一个类继承多个父类。它虽然能够提供更丰富的功能,但也带来了问题,尤其是二义性问题,即派生类可能会从多个父类继承相同的成员,这会导致编译器无法决定应该使用哪个成员。
虚拟继承通过引入共享基类的机制解决了多重继承中的菱形继承问题(即多个派生类从同一基类派生)。虚拟继承确保派生类只会拥有一个基类实例,消除了冗余和二义性问题。
继承还可以与抽象类和纯虚函数一起使用,来实现接口(接口类)和多态行为。抽象类不能直接实例化,只能作为派生类的基类,从而强制派生类实现特定的行为。
在多重继承中,派生类如果继承了多个父类并且父类有相同成员(方法或属性),编译器就无法判断应该调用哪个父类的成员。为了解决这个问题,我们可以使用作用域解析符来明确指定父类的成员,或者使用虚拟继承来确保基类的唯一性。
继承中的构造函数和析构函数调用顺序是有规则的:
这个顺序确保了派生类能够在构造时初始化继承的基类部分,在析构时释放基类的资源。
派生类的构造函数会自动调用父类的构造函数,但如果父类没有默认构造函数,或者需要特殊参数时,派生类的构造函数需要显式调用父类构造函数。
基类的成员(如变量和方法)的访问控制(public、protected、private)会影响派生类的访问权限。虽然派生类可以继承基类的成员,但如果基类的成员是私有的,派生类无法直接访问。
虽然继承能够带来代码复用,但过度使用继承,尤其是多重继承,会使代码变得复杂、难以理解和维护。特别是当继承关系非常深时,可能会出现难以追踪的问题。因此,应该尽量避免深层次的继承链条,提倡使用组合和接口代替复杂的继承关系。
10. 最后
继承是面向对象设计中非常强大的工具,能够帮助我们简化代码,促进代码复用和扩展。然而,继承也伴随着一定的复杂性,尤其是多重继承、虚拟继承和访问控制等问题。我们在使用继承时,应该清晰地定义类的职责和关系,尽量避免复杂的继承结构,采用适当的设计模式和编程技巧,以保证代码的可读性、可维护性和灵活性。
继承本身并不是“坏”的,而是要根据具体的应用场景和设计需求来合理使用,避免不必要的复杂性。
路虽远,行则将至;事虽难,做则必成
亲爱的读者们,下一篇文章再会!!!