【C++】多态:C++编程的魔法师(1)
一、继承和组合
1、继承
在上篇文章中,已经详细介绍过继承:
https://blog.csdn.net/hffh123/article/details/143824040?spm=1001.2014.3001.5501
2、组合:
通俗来讲就是一个类的对象作为另一个类的成员,这种关系就是组合。
class A { }; class B { protected: A a; };
3、组合和继承的相同与不同
(1)、组合和继承都是复用的体现。
(2)、继承是白箱复用,组合是黑箱复用。白就是指看不见,黑指看得见。
(3)、继承父类的子类是可以在类内部使用保护成员的,但是组合中,被作为成员的类对象不能使用自己的保护成员,因为相当于在类外面。
(4)、因为我们要遵循“高内聚,低耦合”,所以能用组合就用组合。这就表明组合的耦合性比继承低。耦合性越低,可维护性就越好,可维护性指迭代更新等等。
举例:
比如A类有100个成员,其中10个为公有成员,90个为保护成员,因为组合访问不了保护成员,A类成员修改时,对组合关系影响就远远小于继承关系。
4、知识点介绍
(1)public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。 (2)组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。 (3)优先使用对象组合,而不是类继承 。 (4)继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。 (5)对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被 封装。 (6)实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合
二、多态的概念:
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
(一)、虚函数的概念(关键字:virtual)
虚函数:即被virtual修饰的类成员函数称为虚函数:class Person { public: virtual void fun() { cout << "调用父类的fun" << endl; } };
(二)、虚函数的重写(也叫覆盖)
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表(与参数名无关)完全相同),称子类的虚函数重写了基类的虚函数。class Person { public: virtual void fun() { cout << "调用父类的fun" << endl; } }; class Student :public Person { public: virtual void fun() { cout << "调用子类fun" << endl; } };
如上,子类的fun函数与父类的fun函数就是重写关系。
特别注意:
隐藏关系只需要子类中的函数的函数名与父类的函数名相同即可构成隐藏,所以说重写是隐藏的一种特殊情况。要注意两者的区别。
(三)、构成多态的条件
(1)、多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。(通俗来讲就是,指向父类调用父类,指向子类调用子类)。(2)、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(构成重写的提条件,函数名相同,参数的类型和参数顺序相同,返回值相同,可以说重写是隐藏的一种特殊情况,但要注意区分两者)。(3)、必须通过父类的指针或者引用调用虚函数。具体我们看下述代码:class Person { public: virtual void fun() { cout << "调用父类的fun" << endl; } }; class Student :public Person { public: virtual void fun() { cout << "调用子类fun" << endl; } }; int main() { Person p; Student s; Person* pp = &p; pp->fun(); pp = &s; pp->fun(); return 0; }
运行结果:
解释:满足构成多态的三个条件,所以父类指针pp,指向父类的对象就调用父类的fun,指向子类的对象就调用子类的fun。
(四)、虚函数重写的两个特殊情况
1、协变:
协变:派生类重写基类虚函数时,与基类虚函数返回值类型可以不同,但这两个返回类型需要时继承关系。且基类虚函数返回类型为父类对象的指针或者引用,派生类虚函数返回类型为子类对象的指针或者引用。具体看下述代码:class A { }; class B :public A { }; class Person { public: virtual A* fun() { cout << "调用父类的fun" << endl; return new A; } }; class Student :public Person { public: virtual B* fun() { cout << "调用子类fun" << endl; return new B; } }; int main() { Person p; Student s; Person* pp = &p; pp->fun(); pp = &s; pp->fun(); return 0; }
运行结果:
解释:
2、析构函数的重写
首先我们先看如下代码:class Person { public: virtual void fun() { cout << "调用父类的fun" << endl; } ~Person() { cout << "父类析构" << endl; } }; class Student :public Person { public: virtual void fun() { cout << "调用子类fun" << endl; } ~Student() { cout << "子类析构" << endl; } }; int main() { /*Person p; Student s;*/ /*Person* pp = &p; pp->fun(); pp = &s; pp->fun();*/ Person* P1 = new Person; delete P1; Person* P2 = new Student; delete P2; return 0; }
运行结果:
问题:我们会发现第二种赋值兼容的情况 delete调用的是父类的析构,没有调用到子类的析构,就可能造成一些内存泄漏的情况。解决:我们发现 P1和 P2都是父类的指针,又想调用指定的函数,我们就可以想到用多态来解决问题,也就是 析构函数的多态,根据多态的三个规则,继承关系,父类指针已经满足了,只差重写的条件,而析构函数都没有返回值和参数,所以就只需要满足两个条件:(1)、被virtual修饰成虚函数(2)、父类和子类的析构函数名字相同。根据小编上一篇文章,我们提到,析构函数底层都会将处理为 destructor()函数,所以函数名相同的条件也有了,此时我们就只需要用 virtual关键字修饰析构函数,析构函数就可以构成重写了,结合上述的条件也就构成多态关系了。加上 virtual关键字后,子类和父类的析构函数构成多态,而 P2指针指向的是子类对象,所以 delete会调用子类的析构函数。
3、重写的注意点:
若父类函数没写virtual关键字,就不构成多态。若父类写了关键字,子类没写关键字,也会构成多态。
(五)、重载、覆盖(重写)、隐藏(重定义)的对比
(六)、C++11引入的两个关键字:
1、final:用一个问题来解释
问题:实现一个类,让这个类不能被继承:
解决1:将父类的构造函数私有化,派生类就实例不出对象。
解决2:将该类用final关键字进行修饰后,该类就不能被继承,被final修饰的类称为最终类。
若final修饰虚函数,则该虚函数不能被重写。
(注意final修饰函数的位置):
2、override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
注意:是检查虚函数(被virtual修饰),不是普通函数
例如:我将上述代码中有重写关系的fun函数,在子类中改个名字,并用该关键词修饰,则会报错: