实验内容
一、继承访问权限测试
设计类A具有public, protected, private等不同属性的成员函数或变量;
类B通过public, protected, private等不同方式继承A,在类B的成员函数中测试访问A的成员函数或变量;
在类B中添加public, protected, private等不同属性的成员函数或变量,在外部测试访问B的各个成员函数或变量;
B以private方式继承A,尝试把A中的部分public成员提升为public。
二、友元类继承测试
设计类A含有私有变量a,在类A中友元给类C;
设计类B继承A,添加私有变量b;在类C中测试访问类B的成员变量a, b;
设计类D继承C,在D的成员函数中测试访问类A的成员变量a,类B的成员变量a, b。
三、多态性综合运用
一般多态性函数:输入输出参数完全一样,在父类中添加virtual;
特殊多态性函数:输入或输出参数在子类中是父类的指针或基类的引用,在子类中对于的是子类的指针或子类的引用;
析构函数的多态性;
多继承,注意什么情况需要虚继承;
设计矢量图,运用多继承设计组合图形,要求具备创建不同类型矢量图、选择图形、移动图形、用不同颜色显示图形(表示选中与否),用vector或数组管理图形。
什么是面向对象和面向过程?
面向过程就是分析解决问题的步骤,然后用函数把这些步骤一步一步的进行实现,在使用的时候进行一一调用就行了,注重的是对于过程的分析。面向对象则是把构成问题的事进行分成各个对象,建立对象的目的也不仅仅是完成这一个个步骤,而是描述各个问题在解决的过程中所发生的行为。
面向对象和面向过程的区别?
面向过程的设计方法采用函数来描述数据的操作,但将函数与操作的数据进行分离开来。
面向对象的设计方法是将对象和数据进行封装起来成为一个整体。
面向过程以设计步骤为过程,后期难以维护。
面向对象设计以数据为中心,数据相对与功能来说具有较强的稳定性更加易于维护。
面向对象的三大特征
封装:
对于封装来说就是数据与具体操作实现的代码都放在某个对象的内部,使这些代码的具体细节不被外界发现,只留下一些接口供外部来使用,而不能一任何的形式来对象内部的实现。使用封装能够隐藏具体的实现的细节,使代码更加易于维护并且保证了系统的安全性。
继承:
继承机制是面向对象程序设计使代码进行复用的最重要的手段,他允许程序员在保证类原有类特性的基础上进行扩展来增加功能。这样新产生的类就被称为派生类,继承就可以表现面向对象机制的的层次结构。
多态:
多态简单点说就是“一个接口,多种实现”,就是同一种事物表现出的多种形态。多态在面向对象语言中是指:接口多种的不同实现方式。也就是复用相同接口,实现不同操作。
什么是继承?
继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。其继承的过程,就是从一般到特殊的过程。
通过继承创建的新类称为“子类”或“派生类”。被继承的类称为“基类”、“父类”或“超类”。要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现。在某些 OOP 语言中,一个子类可以继承多个基类。但是一般情况下,一个子类只能有一个基类,要实现多重继承,可以通过多级继承来实现。
Son继承Father的内容,对于Son我们称为派生类,也俗称子类,对于Father我们称为基类,也俗称父类,可以看到Son是以public的权限继承基类的,如果不写public的话,默认是以private的方式去继承基类
#include#include #include using namespace std; class Father { private: int money; protected: int room_key; public: void it_skill(void) { cout<<"father's it skill"< money = money; } }; class Son : public Father { private: int toy; public: void play_game(void) { int m; cout<<"son paly game"< 上述代码中,子类不能直接去访问父类的private成员,也是需要通过父类提供的public接口来访问,执行结果如下 :
10 father's it skill son paly game什么是友元?
友元顾名思义是作为朋友的一个成员,他的访问权限比较大,友元可以分为友元类,友元函数,他们可以访问类中的被private修饰的私有成员,友元的修饰符是用friend修饰。
友元全局函数 includeusing namespace std; class person{ friend void visit();只需要在类中声明时添加一个friend修饰符 public: person(int age){ m_age=age; } private: int m_age; }; void visit(){ person p1(52); cout<<"年龄是:"< 友元类 #includeusing namespace std; class person{ friend class student; public: person(){ m_age=20; } private: int m_age; }; class student{ public: void visit(){ cout< 友元成员函数 #include#include using namespace std; class person; class student{ public: void visit(); person *m_p; }; class person{ friend void student::visit(); public: person(){ m_age=20; } private: int m_age; }; void student::visit(){ m_p=new person; cout< m_age<
什么是多态?
多态性(polymorphisn)是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
继承有3种形式:私有继承、保护继承、公有继承,缺省的继承方式是私有继承。
私有继承:将基类保护和公有的成员变量作为派生类的私有变量,不能在类外访问;
保护继承:将基类保护和公有的成员变量作为派生类的保护变量,不能在类外访问但是可以被继承;
公有继承:将基类保护和公有的成员变量作为派生类的公有变量,可以在类外访问
派生类对象构造方式
.先调用基类构造函数,构造基类部分成员变量,再调用派生类构造函数构造派生类部分的成员变量。2.基类部分成员的初始化方式在派生类构造函数的初始化列表中指定。3.若基类中还有成员对象,则先调用成员对象的构造函数,再调用基类构造函数,最后是派生类构造函数。析构顺序和构造顺序相反。见下:
#includeusing namespace std; class Test { public: Test() { cout<<"Test::Test()"< 结果如下:
不论哪种继承方式,派生类都是显示的继承类基的保护成员变量和函数和公有成员变量和函数,继承方式只是限定在派生类中这两种成员变量的访问方式(即访问权限)。私有的成员变量和函数也被继承到派生类中,但是不能被访问,它是隐藏的,在派生类中不可见。
派生类继承基类,除基类的构造函数和析构函数不外,其他的所有都继承。
继承是“is-a”的关系。派生类是基类的一种,例如:学生继承与人,学生是人的一种,但人不一定都是学生
基类和派生中同名成员的关系
派生类从基类中继承过来的成员(函数、变量)可能和派生类部分成员(函数、变量)重名。1.前面提到,派生类从基类中继承了基类作用域,所以同成员名变量可以靠作用域区分开(隐藏)。2.同名成员函数则有三种关系:重载、隐藏和覆盖。
1.重载
函数重载有三个条件,一函数名相同,二形参类型、个数、顺序不同,三相同作用域。根据第三个条件,可知函数重载只可能发生在一个类中,见下:
class base { public: base(int a) { ma = a; } void show() { cout<<"base show 1"<其中,两个show函数构成函数重载
2. 函数隐藏在派生类中将基类中的同名成员方法隐藏,要想在派生类对象中访问基类同名成员得加上基类作用域。
#includeusing namespace std; class base { public: base(int a) { ma = a; } void show() { cout<<"base show 1"< 结果如下:
3. 函数覆盖
基类、派生类中的同名方法 函数头相同(参数、返回值),且基类中该方法为虚函数,则派生类中的同名方法将基类中方法覆盖。这里涉及到了虚函数的问题,在后续进行讲解。函数隐藏和函数覆盖都是发生在基类和派生类之间的,可以这么理解:基类和派生类中的同名函数,除去是覆盖的情况,其他都是隐藏的情况。
引用和指针
基类对象和派生类对象
派生类对象可以赋值给基类对象,基类对象不可以赋值给基类对象
对于基类对象和派生类对象,编译器默认支持从下到上的转换,上是基类,下是派生类。
基类指针(引用)和派生类指针(引用)
基类指针(引用)可以指向派生类对象,但只能访问派生类中基类部分的方法,不能访问派生类部分方法,派生类指针(引用)不可以指向基类对象,解引用可能出错,因为派生类的一些方法可能基类没有
编译器只支持从上到下的转换,即只能允许基类指针去指向派生类类对象。
以上对于方法的访问都是基于指针的类型。我们可以看一下基类和派生类的大小,以及基类、派生类的指针(引用)的类型。
#include#include using namespace std; class base { public: base(int a = 1) { ma = a; } void show() { cout<<"base show 1"< 运行结果如下:
base类和Derive类的大小就是他们各自包含的成员变量的总大小,Derive类继承了base类中的成员变量,所以要比base类大4个字节。在上面提到,这里的方法调用都是依据指针的类型,所以我们可以看到 对基类指针p解引用得到的类型只和指针本身的类型相关。
虚函数
当base*指向Derive对象时,而base类中含有虚函数时,基类和派生类大小、基类和派生类指针(引用)的类型。
#include#include using namespace std; class base { public: base(int a = 1) { ma = a; } virtual void show() { cout<<"base show 1"< 运行结果如下:
当base类中有虚函数时,不论是base类还是Derive类,它们的大小都增加了4个字节。并且当base*指向Derive对象时,*base的类型却变为Derive,不再和指针本身的类型相关
虚函数指针
base 和Derive类增加的4个字节就是虚函数指针的大小,每一个类只要有虚函数(包括继承而来的),它就有且只有一个虚函数指针,类的大小就是总的成员变量的大小加上一个虚函数指针的大小。虚函数指针指向的是一张虚表,里面是这个类所有虚函数的地址,一个类对应一张虚函数表,而虚函数指针存在于每一个对象中,并且永远占据对象内存的前四个字节。
以含有虚函数的base类为例,下面是它的内存布局:
class base { public: base(int a = 1) { ma = a; } virtual void show() { cout<<"base show 1"<
虚函数表
虚函数表又称为“虚表”,它在编译期间就已经确定,在程序运行时就会被装载到只读数据段,在整个程序运行期间都会一直存在。一个类实例化的多个对象,它们 的虚函数指针指向的是同一张虚表。
以上base类中虚函数指针vfptr指向的虚函数表——vftable如下所示:
虚表的合并
base类的派生类Derive类定义:
class Derive : public base { public: Derive(int b = 2):base(b) { mb = b; } void show() { cout<<"derive show 1"<根据“虚函数指针永远占据对象的前四个字节”原则,那么正确的内存布局见下:
相应地,派生类的虚函数表也有变化。如果派生类中实现了同名覆盖函数,则派生类虚表中该同名覆盖函数的地址会将基类该同名方法的地址覆盖。派生类中如果没有实现覆盖,则里面的同名函数地址还是基类方法的地址。Derive类中对show方法实现了同名方法覆盖,则它的虚函数表为:



