【c++笔记十一】面向对象三大特征之《封装》与《继承》
2015年2月4日 晴 周三
今天立春,长沙终于见到了久违的太阳。心情好,复习一下知识点,就来说说面向对象的三大特征中的两点:封装和继承。
----------------------------------分割线-------------------------------------
一.封装
其实封装没办法具体的去讲,就像一种保密措施,在实践自己去体会。
该公开的数据,就用public权限;该隐藏的数据,就用private权限。那就看你在设计类的时候,想把什么公开给别人看,想把什么保护起来。
封装之所以是面向对象的三大特征之一,肯定有它自己的作用。第一,便于分工和划分模块。通过把问题全部抽象为类之后,不同程序猿可以负责不同的类模块。其他程序猿在相互使用类的时候不用在意类是具体怎么实现的,只要会用该类提供的接口就可以。这样做可以将一个大型的项目划分成小的模块,不同程序员之间分工合作完成。第二,防止代码不必要的扩展。你可以把自己的想隐藏的代码封装上,别人就无法篡改你的代码,避免了不必要的代码扩展。
二.继承
其实这才是今天要讲的重点,相比封装我觉得继承更重要一点,通过继承我们可以慢慢引入多态(以后再讲)的概念。
1.为什么需要继承?
继承的目的是:代码复用和扩展。
比如,你老爸会修电器,你继承了你老爸的这些技能(代码),你就可以自己灵活的使用这些技能帮同学修电器。除此之外,你还会修电脑(扩展),这是你爸不会的。
用c++语言来表达,你爸和你就是两个不同的类。Father是一个父亲类,该类具有name和age的特征,该类还具有void repair()的功能。Son是儿子类,你也同时具有name age特征,你也会repair()的功能,另外你还包含了void computer()的功能。因为Father和Son类同时具有void repair()的能力,所以Son类不需要自己再去定义这个成员数据,直接从Father那里继承过来就好了。Father类因此称为父类或基类,Son类因此称为子类或派生类。
Son通过继承了Father,免去了再重写void repair()的工作,做到了代码复用。同时Son还能自己具有void computer()的功能,做到了代码扩展。
2.继承的语法
class 子类:权限控制 父类[,权限控制 父类2, ...]{
};
继承方式很简单,在子类之后加上“ : ”,后面跟上父类的名字,并要说明以何种权限继承(该权限也是那三种:public private protected)。注意,子类可以继承多个父类。我们先介绍继承一个父类的形式。
在说语法之前,我们有必要再重新回顾一下权限控制的问题:
我们知道权限有三种:public,private,protected。而这三种的访问权限如下图所示:
我们可以发现,只有private属性权限的成员数据,是不能被子类访问的。再看一张图:
上图说明了,父类中三种权限的成员数据通过三种权限继承给子类之后,子类中这些成员数据的访问权限。
可能看图看的有点头晕,我们一一来说明这三种继承方式:
(1)public方式继承
父类中的公开数据到子类后,是公开的。
父类中的保护数据到子类后,是保护的。
父类中的私有数据到子类后,是隐藏的。
我们可以发现,父类中的公开和保护数据到子类之后访问权限不变,唯有私有数据变成了隐藏数据。这隐藏数据可不同于私有私有数据哦,它是隐藏起来的并且子类无法使用的数据。
我们一起来看个例子吧:
我们在父类中,设置了三种访问权限的repair()函数,我们并且通过public权限继承了Father类。通过Son对象调用这三个来自父类的函数,我们发现,我们却不能访问protected和private属性的父类成员函数了!!!
这就是用public权限继承父类的特性。我们只能使用父类的pubic权限的东西。我们一起来修改程序,想办法让他们都过了!
#include <iostream> using namespace std; class Father{ public: void public_repair(){ cout<<"public_repair()"<<endl; } protected: void protected_repair(){ cout<<"protected_repair()"<<endl; } private: void private_repair(){ cout<<"private_repair()"<<endl; } }; class Son : public Father{ public: void call_protected_repair(){ protected_repair(); } }; int main() { Son sa; sa.public_repair(); sa.call_protected_repair(); return 0; }
我们发现,我们在子类中通过调用成员函数(第27行)的形式,成功调用了父类的protected权限的成员函数。可是我们依然没有办法调用父类的private权限的成员,因为它已经隐藏掉了,根本不让我们使用。
(2)protected方式继承
父类中的公开数据到子类后,是保护的。
父类中的保护数据到子类后,是保护的。
父类中的私有数据到子类后,是隐藏的。
我们发现,公开和保护数据到子类后都是保护的,私有数据依然还是隐藏的!
我们还是举例说明:
#include <iostream> using namespace std; class Father{ public: void public_repair(){ cout<<"public_repair()"<<endl; } protected: void protected_repair(){ cout<<"protected_repair()"<<endl; } private: void private_repair(){ cout<<"private_repair()"<<endl; } }; class Son : protected Father{ public: void call_public_repair(){ public_repair(); } void call_protected_repair(){ protected_repair(); } }; int main() { Son sa; sa.call_public_repair(); sa.call_protected_repair(); return 0; }
因为父类中public和protected权限的repair函数,在子类中都是protected权限的,所以我们只能通过调用成员函数形式调用父类的这两个repair函数。我们依然无法调用父类私有数据。
(3)private方式继承
父类中的公开数据到子类后,是私有的。
父类中的保护数据到子类后,是私有的。
父类中的私有数据到子类后,是隐藏的。
公开和保护数据都变成了私有的,私有数据依然是隐藏的。
该类继承同保护继承类似,都只能通过调用成员函数调用父类的公共和保护数据,还是无法调用父类的私有数据。程序略。
至此,我们可以得出一个结论:无论以何种权限继承父类,子类都无法调用父类的私有成员(父类中的私有数据到子类后都是隐藏的)。
所谓的继承方式,其实就是父类能给子类的最高访问权限。
3.构造函数,拷贝构造函数,赋值运算符的调用顺序问题
凡是牵涉到内存相关操作,我们都要提出来重点讲解一下。
(1)构造函数调用顺序问题
在构建子类对象时,子类一定会先调用父类的构造函数!子类默认调用父类的无参构造函数,子类可以通过初始化列表指定调用父类的构造函数。
上面那一段话,对于我们理解父类和子类构造函数调用顺序问题至关重要。
#include <iostream> using namespace std; class Father{ int age; public: Father(){ cout<<"Father()"<<endl; } Father(int age):age(age){ cout<<"Father(int)"<<endl; } }; class Son : public Father{ public: Son(){ cout<<"Son()"<<endl; } }; int main() { Son sa; return 0; }
Father()有两个构造函数,一个有参数一个无参数。从程序运行结果来看,Son会先调用Father的构造的函数,并且调用的是无参构造函数。
#include <iostream> using namespace std; class Father{ int age; public: Father(){ cout<<"Father()"<<endl; } Father(int age):age(age){ cout<<"Father(int)"<<endl; } }; class Son : public Father{ public: Son():Father(10){ cout<<"Son()"<<endl; } }; int main() { Son sa; return 0; }
注意15行,我们在子类的构造函数的初始化列表中显示的调用 了父类的带参数的构造函数。
顺带提一点,析构函数调用顺序完全和构造函数相反。
(2)拷贝构造函数调用顺序
子类触发拷贝构造函数(不懂的请看我的【c++笔记】)时,默认一定会先调用父类的拷贝构造函数,再调用自己的拷贝构造函数。但一旦子类提供了拷贝构造函数,则不再默认调用父类的拷贝构造函数,需要人为指定调用。
我们通过程序说明上述现象。
#include <iostream> using namespace std; class Father{ int age; public: Father(int age=0):age(age){ cout<<"Father()"<<endl; } Father(const Father& fa){ cout<<"Father(const Father&)"<<endl; age = fa.age; } }; class Son : public Father{ public: Son(){ cout<<"Son()"<<endl; } }; int main() { Son sa; Son sb=sa; return 0; }
从运行结果我们可以发现,子类的确先是调用父类的拷贝构造函数。然后会调用自己的默认拷贝构造函数。
再看一下给子类也定义拷贝构造函数的情况:
#include <iostream> using namespace std; class Father{ int age; public: Father(int age=0):age(age){ cout<<"Father()"<<endl; } Father(const Father& fa){ cout<<"Father(const Father&)"<<endl; age = fa.age; } }; class Son : public Father{ public: Son(){ cout<<"Son()"<<endl; } Son(const Son& so){ cout<<"Son(const Son&)"<<endl; } }; int main() { Son sa; Son sb=sa; return 0; }
我们给父类Father和子类Son同时提供了拷贝构造函数函数。但是因为子类提供了拷贝构造函数就不再调用父类的拷贝构造函数了,所以程序运行结果只调用了子类的拷贝构造函数。
如果在子类定义了拷贝构造函数的情况下,又可以调用父类的拷贝构造函数,我们需要人为的指定去调用父类的拷贝构造函数。做法同样是在,子类的拷贝构造函数的初始化列表中调用父类的!(拷贝构造函数也有初始化列表哦。)
#include <iostream> using namespace std; class Father{ int age; public: Father(int age=0):age(age){ cout<<"Father()"<<endl; } Father(const Father& fa){ cout<<"Father(const Father&)"<<endl; age = fa.age; } }; class Son : public Father{ public: Son(){ cout<<"Son()"<<endl; } Son(const Son& so):Father(so){ cout<<"Son(const Son&)"<<endl; } }; int main() { Son sa; Son sb=sa; return 0; }
(3)赋值运算符调用顺序问题
子类触发赋值运算符时,默认一定会先调用父类的赋值运算符,在调用自己对赋值运算符。但一旦子类提供了赋值运算符,则不再默认调用父类的赋值运算符,需要人为指定调用。
发现没有,和拷贝构造函数的调用顺序是一样的。我们就不再浪费时间验证这种现象了。但是,有一点一定要特别说明一下:如何人为的去调用父类的赋值运算符?重载的赋值运算符可是没有初始化列表的哦!如果要调用父类的赋值运算符,可以这样做:
父类名::operator=(子类对象);
看代码:
#include <iostream> using namespace std; class Father{ int age; public: Father(int age=0):age(age){ cout<<"Father()"<<endl; } Father& operator=(const Father& fa){ cout<<"Father& operator=(const Father&)"<<endl; age = fa.age; } }; class Son : public Father{ public: Son(){ cout<<"Son()"<<endl; } Son& operator=(const Son& fa){ Father::operator=(fa); cout<<"Son& operator=(const Son&)"<<endl; } }; int main() { Son sa; Son sb; sb=sa; return 0; }
注意20行,我们人为的调用了父类的赋值运算符。为什么可以这样去调用父类的赋值运算符呢?这里牵涉到我们的下一个知识点——名字隐藏(namehide)
4.名字隐藏(namehide)
(1)什么是名字隐藏?
当子类中提供和父类同名的数据时,会把父类的数据隐藏掉。这种机制叫做名字隐藏。
注意:名字隐藏,和通过private权限继承父类之后父类的私有数据在子类中的隐藏特性是不一样的。
我们一起来通过代码看看这种名字隐藏现象:
#include <iostream> using namespace std; class Father{ public: void repair(){ cout<<"Father::repair()"<<endl; } }; class Son : public Father{ public: void repair(){ cout<<"Son::repair()"<<endl; } }; int main() { Son sa; sa.repair(); return 0; }
因为子类中重新定义了父类的repair()函数,发生了名字隐藏现象,把父类的repair隐藏掉所以显示出来的是子类的repair()函数。
(2)如何打破名字隐藏?
如果发生了名字隐藏,但是我们一定要用父类的数据可以吗?当然可以啊,回头看看上面我们是怎样显示的调用父类的赋值运算符的。因为子类和父类的赋值运算符重载函数重名了,发生了名字隐藏。
所以,想要打破名字隐藏,我们这样做:
父类名::成员数据名
我们一起通过例子看打破名字隐藏。
#include <iostream> using namespace std; class Father{ public: void repair(){ cout<<"Father::repair()"<<endl; } }; class Son : public Father{ public: void repair(){ cout<<"Son::repair()"<<endl; } }; int main() { Son sa; sa.Father::repair(); return 0; }
仔细看18行:sa.Father::repair(),我们通过这种方法显示调用父类的repair函数。所以如果你想在名字隐藏的情况下用父类的数据就直接在数据名之前加上“父类名::”
5.多继承
多继承是继承中最难的知识点,也是最重要的。只有很好的理解多继承,才能为学习多态打好基础。
(1)什么是多继承?
一个类可以有多个直接父类,叫多继承。
比如,儿子既继承了父亲修电器的技能,又继承了母亲的炒菜技能。现在儿子同时具有修电器和炒菜的能力。这就是多继承。
在说继承的语法时我们提到过这一点。
(2)多继承的语法
class 子类名: 权限 父类1,权限 父类2,...{
};
比如:
class Son : public Father, public Mother{
};
那么多继承的子类,是按照什么顺序调用父类的构造函数呢?
它只和继承的顺序保持一致,与其他无关。我们来看代码:
#include <iostream> using namespace std; class Father{ public: Father(){ cout<<"Father()"<<endl; } }; class Mother{ public: Mother(){ cout<<"Mother"<<endl; } }; class Son : public Father,public Mother{ public: Son(){ cout<<"Son"<<endl; } }; int main() { Son sa; return 0; }
析构函数,拷贝构造函数,赋值运算符的调用顺序和单继承的方式一样。父类之间的顺序只和继承的顺序有关。
(3)多继承数据访问中的问题
非私有继承的子类可以直接调用父类中的除了私有成员之外的其他成员。这一点我们都知道,但是,多继承中有一种数据访问冲突的问题。
先看代码:
Father和Mother都会炒菜,Son同时继承了他们炒菜的技能。可是到Son来炒菜的时候它犯傻了,我到底是用Father的炒菜方式还是用Mother的炒菜方式呢?所以编译器报错了,说cook()函数有歧义!
这就是多继承中,数据访问冲突问题。一旦父类中有同名的成员函数,子类调用这函数的时候就会发生冲突。
那我们如何去解决这种数据访问冲突呢?有两种方法:
A.使用名字隐藏机制避免数据访问冲突。
Son在不知道用Fater还是Mother的炒菜方式(cook())的时候,决定糅合Father和Mother的炒菜方式,开发出属于自己特有的炒菜方式:
#include <iostream> using namespace std; class Father{ public: void cook(){ cout<<"Father::cook"<<endl; } }; class Mother{ public: void cook(){ cout<<"Mother::cook"<<endl; } }; class Son : public Father,public Mother{ public: void cook(){ cout<<"Son::cook()"<<endl; } }; int main() { Son sa; sa.cook(); return 0; }
Son灵活运用名字隐藏机制,定义了自己的cook函数,这样就避免了数据访问冲突的问题。
B.虚继承
a.把父类中所有的共同部分抽取到更高层的类中(父类的父类中)
b.父类用虚继承的方式继承更高层的类
c.最后再让子类继承父类
钻石继承,是我们在多继承中碰到的很有意思的现象。提供抽取父类中共同数据来构造更高层,再通过虚继承来解决数据访问冲突问题。
我们举例说明:Father和Mother炒菜,都是跟Grandfather学的。但是因为Father和Mother没有用心学,炒菜方式并不是我们祖传正宗的方法。如果Son还跟Father和Mother去学炒菜,那家传秘方就要失传了!怎么办呢?爷爷说:“不行,你爸你妈那都是学的虚招,没有学到真本事。孙子来,跟爷爷学真正的炒菜吧!”这样,Son跳过了Father和Mother,直接跟着Grandfather学真本事。
这样,就构成了有趣的菱形继承。我来画张丑图说明一下吧:
因为形状比较像钻石(菱形),所以叫钻石继承(菱形继承)
Father和Mother都是从Grandfather那里虚继承来的。Son自然而且的就会虚继承Father和Mother(一虚则全虚)。
我们再通过这种虚继承的方式来重写程序:
#include <iostream> using namespace std; class Grandfather{ public: void cook(){ cout<<"Grandfather::cook()"<<endl; } }; class Father:virtual public Grandfather{ public: void cook(){ cout<<"Father::cook()"<<endl; } }; class Mother:virtual public Grandfather{ public: void cook(){ cout<<"Mother::cook()"<<endl; } }; class Son : public Father,public Mother{ public: void cook(){ Grandfather::cook(); } }; int main() { Son sa; sa.cook(); return 0; }
我们可以看到第9和15行,Father和Mother虚继承了Grandfather。虽然Son同时继承了Father和Mother,并且他们有共同成员函数cook()函数,但是Son直接绕过了Father和Mother从Grandfather那里学到了cook()。
如果不采用这种虚继承的方式我们能绕过Father 和 Mother吗?
通过这种虚继承,我们可以直接访问更高层的类。所以大家一定要弄清楚这一部分。这也是解决数据访问冲突的一种方法。
----------------------------分割线--------------------------------
继承部分的主要知识点,我们算是讲完了,然我们一起总结一下:
首先,我们认识什么是继承。其次我们了解了继承的语法,特别需要三种继承方式的不同。然后,我们还强调了构造函数,析构函数,拷贝构造函数和赋值运算符的调用顺序问题。顺带的我们又说了什么是名字隐藏。最后再谈到多继承以及多继承中遇到的数据访问冲突问题和解决方法。
如果大家还有什么疑问,或是发现本文的不足,欢迎评论指出。
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。