深度揭秘C++面向对象编程中继承的核心概念
作者:Aomnitrix
一.继承
1.继承与面向对象
我们知道C语言是面向过程的编程语言,C++在C语言的基础上进化出了面向对象的模型。而继承就是面向对象的重要属性。继承使得我们在一个基础属性上能够加以拓展,用来描述很多具有相同基本属性又各有不同的事务。
2.继承方式访问权限
继承方式与访问权限相同都有三种:public、private、protected继承,再结合基类的三种访问权限,在派生类中就有九种不同的情况,可以总结为:基类为私有的成员在派生类一律不可见,其余权限按照与继承方式相比较小的来。
3.切片(赋值转换)
在实际应用场景中我们常用基类代表几种事务共同的基本属性,派生类用来代表自己独特的属性,这就会导致派生类对象一定是包含父类对象的,所以子类对象可以赋给父类对象/指针/引用,过程会将子类中独特属性的成员切出,剩余赋给父类对象,所以称为切片,不仅如此基类的指针可以通过强制类型转换赋值给派生类的指针,代码演示如下:
class Person { protected: string _name; string _sex; int _age; }; class Student : public Person { public: int _No; }; void Test() { Student sobj; // 子类对象可以赋值给父类对象/指针/引用 Person pobj = sobj; Person* pp = &sobj; Person& rp = sobj; // 基类的指针可以通过强制类型转换赋值给派生类的指针 pp = &sobj; Student * ps1 = (Student*)pp; // 这种情况转换时可以的。 ps1->_No = 10; pp = &pobj; Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问 ps2->_No = 10; } int main() { Test(); return 0; }
4.作用域
如果在基类和派生类定义了同名函数就会构成隐藏/重定义,子类成员将屏蔽父类对同名成员的直接访问,如下所示;
class A { public: A(int num = 0) :_num(num) {} void func() { cout << _num << endl; } private: int _num; }; class B : public A { public: B(int val = 1) :_val(val) {} void func() { cout << _val<< endl; } private: int _val; }; int main() { B b; b.func(); b.A::func(); return 0; }
5.默认成员函数
继承这里遵循着一个规则:构造先父后子,析构先子后父。因为派生类中一定包含基类的对象所以派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员,如果基类没有默认的构造函数(无参、全缺省、编译器默认生成),则必须在派生类构造函数的初始化列表阶段显示调用。
派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
派生类的operator=必须要调用基类的operator=完成基类的复制。
基类的析构函数调用后会自动调用基类的析构函数,不能在派生类显示调用不然无法保证析构先子后父的顺序。
6.友元与静态函数
友元关系不会继承,静态函数不会多次实例化。
7.解决菱形继承的二义性与数据冗余
菱形继承是多继承的一种特殊情况,我们在编写代码时应要避免菱形继承:
如上图所示就是一个菱形继承,这样会导致A类中会有两份P类中的成员,这样不仅会浪费空间,而且会产生二义性,如果我通过A对象访问P中的成员,因为A类中有两份P中的成员所有会有歧义。为了解决此问题,出现了虚拟继承。如下图所示:
那虚继承是如何完成的呢:在d类中建立虚基表,将原来冗余的对象放在d对象空间最下面,B、C原来存储A的位置存储当前位置与空间最下面A对象的偏移量,保证指定B或者C对象访问_a时都能访问到。
8.继承与组合
与继承相比组合的耦合度更低,更适合我们使用!继承又称为白箱复用,组合称为黑盒复用,因为使用组合的话是无法看到类内部的设计。
到此这篇关于深度揭秘C++面向对象编程中继承的核心概念的文章就介绍到这了,更多相关C++继承内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!