目录
前言
继承
继承方式
基类和派生类对象赋值转换
继承中的作用域
派生类的默认成员函数
继承与友元
继承与静态成员
菱形继承与虚拟继承
虚拟继承
虚拟继承解决数据冗余和二义性的原理
继承和组合
小结
C++三大特性: 封装, 继承, 多态
也是面向对象的三大特性
说明 : 本文程序都是在vs2017下的x86下调试运
简单来说, C++允许一个类继承另一个类的全部类容, 提高了代码的复用性, 但破坏了封装性,增加了耦合性, 那么一个类怎样继承另一个类呢?
格式 :class 派生类名 : 继承方式 基类1 , 继承方式 基类2... {
};
如下:
#include<iostream> using namespace std; #include<string> class Base { int m_b; public: Base() :m_b(10) {} void Func(){ cout << "Base::Func()" << endl; } }; class Derive : public Base { int m_d; public: Derive() :m_d(20) {} void func() { cout << "Derive::func()" << endl; } }; int main(){ Base b; Derive d; system("pause"); return 0; }我们在VS下调试可以看到, 对象d中确实继承了Base类的成员变量和成员函数
前面在定义派生类时, 用到了继承方式, 在程序中我写的是public, 其实还有private和protected共三种继承方式, 这三个单词也是类中的访问限定符. 类的访问限定符 戳链接( ̄︶ ̄)↗
public, private和protected在作为继承方式时, 是对派生类继承来的基类成员进一步限定
继承方式基类public成员基类private成员基类protected成员public派生类中为public派生类中存在但不可见派生类中为protectedprivate派生类中为private派生类中存在但不可见派生类中为privateprotected派生类中为protected派生类中存在但不可见派生类中为protected说明 : 1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里还是类外都不能去访问它。这里的类内不能访问是指不能直接访问, 但可以通过继承来的接口函数访问. 2. protected成员的存在就是为了继承, 没有继承关系时和private没有区别 ,当基类中的成员不想在类外直接访问,但又想让继承后其派生类里可以访问, 就应该定义为protected成员protected成员就可以理解为可以被继承的private成员. 3. 基类成员在派生类中的访问权限 = Min(基类中的访问权限, 继承方式) , public>protected>private 4. 继承时也可以不写继承方式, class默认是private继承, struct默认是public继承
1. 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割. 意思是把派生类中基类那部分切下来成为一个独立的基类, 如下图
2. 基类对象和引用 不能赋值给派生类对象
基类的指针可以通过强制类型转换赋值给派生类的指针, 但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI的dynamic_cast 来进行识别后进行安全转换
例如下面的例子 :
#pragma once #include<iostream> #include<string> using namespace std; class Person { string m_name; bool m_sex; int m_age; public: Person(string name, bool sex, int age) :m_name(name), m_sex(sex), m_age(age) {} Person(Person& x) :m_name(x.m_name), m_sex(x.m_sex), m_age(x.m_age) {} void Print_Peson() { cout << "姓名: " << m_name << " 性别: "; m_sex ? cout << "男" : cout << "女"; cout << " 年龄: " << m_age; } }; class Student : public Person { int m_stunum; public: Student(string name, bool sex, int age, int stunum) :m_stunum(stunum), Person(name, sex, age) {} void Print_Student() { Print_Peson(); cout << " 学号: " << m_stunum << endl; } }; #include<iostream> #include"class.h" using namespace std; void test1() { Student* Sp = new Student("张三", true, 20, 1901); Person* Pp = Sp;//给指针 Person& Pr = *Sp;//给引用 Person a = *Sp;//给对象 Pp->Print_Peson(); cout << endl; Pr.Print_Peson(); cout << endl; a.Print_Peson(); cout << endl; } void test2() { Person* Pp = new Person("李四", false, 19); Student* Sp = (Student*)Pp; Sp->Print_Student(); } int main() { test1(); test2(); system("pause"); return 0; }可以看到, 结果是正如前面所说的, 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用, 基类的指针可以通过强制类型转换赋值给派生类的指针, 但不能访问派生类自己的成员, 可以看到访问到了非法内存
我们知道, 类本身就是一个作用域, 那么当一个类继承了另一个类时, 作用域又将如何变化呢?
1. 继承关系发生后, 基类和派生类还是有各自独立的作用域
2. 当基类和子类有重名成员时, 派生类中将屏蔽掉基类的重名成员, 这种现象叫隐藏, 也叫重定义 解释: 1). 重名, 对变量来说, 只需要变量名重名, 与变量类型无关, 对函数来说, 只需要函数名重名, 与返回值与参数列表无关 2). 屏蔽, 指的是, 当在派生类中直接访问重名成员时, 访问的都是派生类的
3. 事实上基类的重名成员只是被隐藏了, 我们还是可以通过 基类类名::重名成员 这种方式来访问
4. 在实际继承关系中, 最好不要定义重名成员
来看个例子
#include<iostream> using namespace std; class Base { public: int m_data; Base():m_data(10){} int func() { cout << "Base的func()" << endl; return 0; } }; class Derive : public Base { public: double m_data; Derive() :m_data(20.0) {} void func() { cout << "Derive的func()" << endl; } }; int main() { Derive d; cout << d.m_data << endl; d.func(); cout << d.Base::m_data << endl; d.Base::func(); system("pause"); return 0; }就算一个啥成员也没有的类, 其实也有6个默认成员函数, 就是我们不定义, 编译器会自己生成的6个函数
那么, 当继承关系发生后, 派生类中的默认成员函数会不会有啥特别的操作? 答案是, 会的, 如下
1. 派生类默认构造会先调基类的默认构造初始化基类的成员, 如果基类没有默认构造(没有无参构造), 那么派生类构造必须要以初始化列表的形式调用基类构造
2. 派生类的默认构造函数会先调基类的构造构造, 先完成基类的拷贝, 再完成自己的
3. 派生类的默认operator=会先调基类的operator=, 先完成基类的赋值, 再完成自己的
4.派生类析构会先调派生类的析构, 再调基类的析构
问题 :
实现一个不能被继承的类
方法一: C++11 新添加了final关键字, 直接在定义类时加在类名后面, 如下:
class A final{ };方法二: 将类的构造函数或者析构函数私有化, 这个类将不能被继承
缺点, 当类的构造函数或析构函数私有化之后, 这个类就只能在堆上创建对象了, 私有化构造函数或析构函数戳链接( ̄︶ ̄)↗
如下:
构造函数私有化, 两个静态成员函数用于在堆上创建对象
class T1 { T1(int val):b(val) { } T1(T1& x):b(x.b){ } public: static T1* newT1_p(int val = 0) { return new T1(val); } static T1& newT1(int val = 0) { return *new T1(val); } };析构函数私有化, 需要有一个public的函数调用析构, 最后由我们手动调用
class T2 { ~T2() { delete this; } public: int b; T2(int val = 0) :b(val) { } void Destroy() { this->~T2(); } };友元不会被继承
也就是说, 不管是基类的友元类还是基类的友元函数, 都不是派生类的友元
基类中有static静态成员 , 不管怎样被继承, 继承多少,个 几重继承, 继承体系中都只有这一份静态成员
多继承就是一个派生类继承了多个基类, 菱形继承是多继承的特殊情况
图示 :
当一个子类继承的多个父类中, 有两个以上的父类是同一个爷爷类(父类的的父类, 这里为了区分, 叫爷爷类), 则此时就发生了菱形继承, 如上图, D类中就有两份A类的内容, 造成了数据冗余性和二义性问题
冗余性: 在D中有两份A类的数据 二义性: 访问D中A类的数据使, 不知道要访问从B中继承来的, 还是从C中继承来的 二义性测试, 如下代码
#include<iostream> using namespace std; class A { int m_a; }; class B : public A { int m_b; }; class C : public A { int m_c; }; class D : public B, public C{ int m_c; }; int main() { D d; d.m_a = 10; cout<<d.m_a<<endl; system("pause"); return 0; }错误提示, 访问不明确, 因为编译器也不知道到底要访问B继承的还是A继承的
为了解决菱形继承中的数据冗余性和二义性的问题, C++中提供了虚拟继承
在继承基类时加virtual 关键字, 如下 , 就说派生类, 虚拟继承了基类
class 派生类 : virtual 继承方式 基类, ...{
};
我们把上面B和C继承A改为虚拟继承, 再次测试如下
#include<iostream> using namespace std; class A { public: int m_a; }; class B :virtual public A { public: int m_b; }; class C :virtual public A { public: int m_c; }; class D : public B, public C{ public: int m_c; }; int main() { D d; d.m_a = 10; cout << d.m_a << endl; system("pause"); return 0; }可以看到, 没有二义性的问题, 实际上现在, D类中只有1份A类的成员, 二义性, 冗余性都被解决了
实际在添加virtual关键字之后, B和C就是虚拟继承A了, 虚拟继承和普通继承就有一点区别, 虚拟继承之后子类中多了一个隐藏的虚基表, 放着虚基类指针, 作用是标识自己的父类, 也就是B和C中多了一个虚基表, 这个表中的虚基类指针指向自己的父类A, 当D继承B和C时, 发现B和C的基类都是A, 那么D在继承时也会只继承一份A
所以, 虚拟继承一般只用于菱形继承, 因为引入的虚基表会带来额外开销
优先使用对象组合,而不是类继承。
哈哈哈, 不推荐使用可不代表就不需要掌握啦, 该会还得会
参考 : https://www.cnblogs.com/nexiyi/archive/2013/06/16/3138568.html
继承和组合的比较:
面向对象系统中功能复用的两种最常用技术是类继承和对象组合(object composition)。正如我们已解释过的,类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。
但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性” 。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。
对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。
对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象 (而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。
这导出了我们的面向对象设计的第二个原则:优先使用对象组合,而不是类继承。
来看个例子
#include<iostream> using namespace std; class A { public: int m_a; }; class B { public: A m_A; int m_b; }; class C { public: B m_B; int m_c; }; int main() { C c; c.m_B.m_A.m_a = 10; c.m_B.m_b = 20; c.m_c = 30; system("pause"); return 0; }调试, 看一下对象的值, 可以看到这种组合方式和继承一样都得到两一个类的全部内容
1.在实际应用中, 一般不建议多继承, 一定不要菱形继承
2. 继承提高了代码复用性(白箱风格复用), 但在一定程度上破坏了封装性, 增加了耦合性
3. 优先使用对象组合(黑箱风格复用), 而不是继承, 对象组合要求被组合的对象具有良好定义的接口. 对象组合耦合度低, 封装性完整, 代码维护性好
4. 能继承能组合尽量用组合, 但有些情况只能用继承, 例如三大特性之一, 多态的实现
