揭秘C++继承机制:从基础到菱形继承全解析----《Hello C++ Wrold!》(13)--(C/C++)
文章目录
- 前言
- 继承的概念
- 继承的定义
- 继承关系和访问限定符对访问权限的影响
- 基类和派生类对象赋值转换
- 继承中的作用域
- 派生类的默认成员函数
- 构造函数
- 赋值操作符重载
- 析构函数
- 继承与友元
- 继承与静态成员
- 复杂的菱形继承及菱形虚拟继承
- 单继承
- 多继承
- 菱形继承
- 虚拟继承
- 继承和组合的区别
- 作业部分
前言
在面向对象编程的世界里,继承机制犹如一座桥梁,连接着抽象与具体,实现了代码的优雅复用和逻辑的自然延伸。作为一名C++开发者,深刻理解继承机制不仅能让我们写出更加简洁高效的代码,更能帮助我们建立起清晰的软件架构思维。
继承不仅仅是语法层面的一种特性,它更是一种思维方式——通过层次化的类设计,我们可以将现实世界中"是一个(is-a)"的关系完美地映射到代码中。从简单的单继承到复杂的多重继承,从访问权限控制到虚函数机制,继承贯穿了C++面向对象编程的方方面面。
在这篇博客中,我将带领大家系统性地探索C++继承机制的各个细节。我们将从基础概念出发,逐步深入到菱形继承、虚拟继承等高级话题。无论你是:
刚接触面向对象编程的新手
希望巩固C++基础的初级开发者
想要深入理解继承底层机制的中高级工程师
这里都有适合你的内容。我会通过大量代码示例和内存布局图示,将抽象的概念具象化,帮助你真正"看到"继承在内存中是如何工作的。
特别值得一提的是,我们将重点讨论实际开发中常见的陷阱和最佳实践,比如:
如何避免隐藏(hiding)带来的困惑
何时使用虚拟继承
继承与组合的选择标准
菱形继承问题的解决方案
理解这些内容不仅能帮助你在面试中游刃有余,更能让你在日常开发中写出更健壮、更易维护的代码。
让我们开始这段探索C++继承机制的旅程吧!相信通过这篇博客,你将对C++面向对象设计有更深刻的认识,并能将这些知识灵活运用到实际项目中。如果在阅读过程中有任何疑问,欢迎随时留言讨论。
继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。
其中的父类也叫做基类,子类叫做派生类
继承的定义
eg:class Teacher : public Person
{//里面就是正常的类的定义
}
前面的这个Teacher是派生类,Person是基类 public是继承方式
继承关系和访问限定符对访问权限的影响
1.基类里面是private的东西对于派生类是不可见的(也就是继承不给派生类)
2.继承关系和访问限定符取小的那个
比如:基类中用protected修饰,继承关系用public 最后在派生类中还是当protected的来用
3.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过
最好显示的写出继承方式
eg:class Derived1 : Base {}; // 等价于 class Derived1 : private Base {};
引申:1.protected和private的区别就出来了,虽然都只能在类内用,但是一个可以让派生类用,一个不能
2.一般常用的就是基类的
public/protected
成员用public
方式继承给派生类
基类和派生类对象赋值转换
1.派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用(也叫做切片或者切割,也就是父类只要自己有的那一部分)
比如:Person是基类 s是一个子类的对象 Person p1 = s;Person* p2 = &s;//这里的p3就只指向父类有的那一部分Person& p3 = s; //这里是不会产生临时变量的
2.基类对象不能赋值给派生类对象(强转都不行)
继承中的作用域
1.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 去显示访问父类)
编译器在子类的域里面搜索名字的顺序是:先子类,再父类,最后全局域
访问父类eg:s.Person::func()
2… 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。(不管参数)
比如父类 void text() 和子类的int text(int a)也构成隐藏 如果 没有显示访问父类的话,直接用text();会报错,因为编译器默认使用的子类的 (和编译器会挑最符合的函数进行使用区分--那个一般是关于重载的,比如有无const&)
3.在继承体系中基类和派生类都有独立的作用域
总结:在继承体系里面最好不要定义同名的成员
易混点:两个fun构成什么关系?a、隐藏/重定义 b、重载 c、重写/覆盖 d、编译报错答案:a
class Person
{
public:void fun(){}
};
class Student : public Person
{
public:void fun(int i){}
};
容易看成是重载,但是注意,同一作用域内才谈重载
派生类的默认成员函数
这里的话其实就是探讨派生类中就基类成员怎么处理
构造函数
1.基类成员的初始化要么调用自己的默认构造函数,要么就只能在基类的构造函数的初始化列表显示调用(给基类成员的构造函数传参)
tip:1.如果要调用的是基类的拷贝构造函数的话,就必须在构造函数的初始化列表里显示调用
2.是普通构造函数还是拷贝构造函数的初始化列表看具体情况而定
class Student : public Person { public :Student(const char* name): Person(name )}
初始化列表初始的顺序取决于变量声明的顺序,所以:派生类先初始化的父类的,之后才初始化的自己的成员变量
引申:在父类的构造函数里面++静态成员变量就可以统计一共有多少个派生类了
赋值操作符重载
派生类的operator=必须要调用基类的operator=完成基类的复制
引申:编译器会对析构函数名进行特殊处理,处理成destrutor(),也就是所以的析构函数在编译器看来名字都是一样的
Student& operator=(const Student& s){if (this != &s){Person::operator=(s);//注意这里要用父类的,不然就死循环了_id = s._id;}return *this;}引申:Student s;&s._num这样的话是&(s._num)
析构函数
编译器设置了:派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象清理顺序是先清理派生类成员再清理基类成员(不支持手动调用哈)
原因:害怕程序员在基类被析构后还去访问子类里面含的基类造成未定义的行为
继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
子类友元也不能访问基类私有和保护成员(除非保护成员继承给子类了)
继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子
类,都只有一个static成员实例 ,在派生类中不会单独拷贝一份。
复杂的菱形继承及菱形虚拟继承
单继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承
菱形继承是多继承的一种特殊情况
这个继承的话,就存在数据冗余和二义性的问题
也就是一个子类继承了两次基类,有两组基类的成员(假设基类里有个age,那么子类里就有两个age)
解决方法:1.指定自己在用的age是哪个中间基类传下来的,但是这样没有解决数据冗余的问题
a.Student::age = 10; a.Teacher::age = 40;
2.引入虚拟继承
虚拟继承
作用:专门用来解决菱形继承的二义性和数据冗余的问题
使用方法:在继承方式前面加一个
virtual
比如:class Person {}; class Student : virtual public Person {}; class Teacher : virtual public Person//两个中间基类都加上了virtual {}; class Assistant : public Student, public Teacher {};
虚拟继承的原理:就是在中间基类里面存了虚基表指针(存储位置为a),然后虚基表指针指向的地方(具体什么地方要看编译器)存储了a距离基类成员位置的偏移量
继承和组合的区别
什么时候用继承?什么时候用组合?
public继承是一种
is-a
的关系。也就是说每个派生类对象都是一个基类对象。例子:狗是动物
组合是一种
has-a
的关系,每个B对象中都有一个A对象。例子:汽车有发动机
如果既能
is-a
又能has-a
的话,就用组合(因为尽量少去用继承)这两者的区别就是:
继承是白箱复用,派生类和基类间的依赖关系很强,耦合度高
组合是黑箱复用,组合类之间没有很强的依赖关系,耦合度低
class C
{};// 继承
class D : public C
{};// 组合
class E
{
private:C _cc;
};
作业部分
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base2, public Base1 { public: int _d; };
//如果这里先Base1再Base2的话,就是p1==p3了(谁先继承,存储位置谁就在上面)int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}
结果是p1!=p3==p2引申:指针可以比较大小,但是比较的是指针指向的地址的高低
class A {
public:A(const char* s) { cout << s << endl; }~A() {}
};class B :virtual public A
{
public:B(const char* sa, const char* sb) :A(sa) { cout << sb << endl; }
};class C :virtual public A
{
public:C(const char* sa, const char* sb) :A(sa) { cout << sb << endl; }
};class D :public B, public C
{
public:D(const char* sa, const char* sb, const char* sc, const char* sd) :B(sa, sb), C(sa, sc), A(sa)
//这里的A不能删除,不然就会去调用A的默认构造,但是A没得{cout << sd << endl;}
};int main() {D* p = new D("A", "B", "C", "D");delete p;return 0;
}
最后的打印顺序是ABCD
注意:A是打印了一次,只有D对象里面A才被构造,那几个都是virtual了的