类的四个主要特征:抽象、封装、继承、多态
封装性:一、将有关的数据和操作代码封装在一个对象中;二、隐蔽内部细节,只留少量接口,用以接收外部消息
继承 inheritance:利用一个类建立一个新的类,解决了软件重用问题,是类最重要的一个特征
多态:由继承而产生的相关的不同的类,其对象对同一消息会作出不同反应,增加了程序的灵活性。(例狗和鱼是动物的子类,狗的呼吸事件使用肺,鱼的是用肺)
类的private私有成员只能被成员函数访问,而不能被外界直接访问(类名.变量名)。
类是对象的抽象,对象是类的具体实例(instance)
抽象的作用是表示同一类事物的本质。
面向对象开发的五个过程:
OOA Object Oriented Analysis 面向对象分析
OOD Object Oriented Design 面向对象设计
OOP Object Oriented Programming 面向对象编程
OOT Object Oriented Test 面向对象测试
OOSM Object Oriented Soft Maintenance 面向对象维护
类的大小:
对象的大小等于数据成员所占空间,而不包含函数代码。函数代码是对象公用的。
class Time
{
int hour,minute,sec;
void set()
{
cin>>hour,minute,sec;
}
}
cout<<sizeof(Time);
结果应为12 12 = 3*4
类的实现、信息隐蔽
公用程序函数是用户使用类的公用接口,通过成员函数对数据成员进行操作称为类的实现,类的公用接口与私有实现的分离形成了信息隐蔽。
类库
类的声明与定义往往放在两个文件中,类声明放在头文件中,成员函数定义放在主模块中。
实际工作中,并不是把一个类声明为一个头文件,而是将若干常用的功能相近的类声明集中在一起,形成类库。
类库包括两部分:
(1)类声明头文件
(2)已经编译过的成员函数的定义,它是目标文件,这样用户能看到头文件中类的声明和成员函数的原型声明,却看不到定义成员函数的源代码,保护了软件开发商的利益。
构造函数
构造函数在建立对象时自动执行,名字与类名同,用来处理对象的初始化。
类的数据成员是不能在声明时初始化的,如下面的写法是错误的
class T
{
int a = 5; //错误
}
数据成员的赋值
(1)定义对象时赋值: Time t1 = {14,20,56};
(2)建立对象后,用成员函数赋值
(3)构造函数赋值
(4)构造函数的参数初始化表(在构造函数声明后面)
Box::Box(int h, int w, int len):height(h),width(w),length(len){}
相当于
Box::Box(int h, int w, int len):
{
height = h;
width = w;
length = len;
}
析构函数
类名前加 ~ 符号,当对象生命期结束时在,自动执行析构函数。具体情况
(1)函数中局部自动对象,在函数调用结束时,析构。如果函数被多次调用,则每次调用时都调用构造函数。
(2)static局部对象,在函数调用结束时对象不释放,因此也不调用析构函数,只在main函数结束或调用exit结束函数时,才调用static局部对象的析构函数。
(3)如果定义了一个全局对象,在程序流程离开其作用域时(main结束或exit退出),调用其析构函数。
(4)如果用new运算符动态建立了一个对象,则在delete释放对象时,调用析构函数
析构函数的作用
(1)并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新的对象。
(2)执行用户希望在最后一次使用对象之后所执行的任何操作。
析构函数没有函数参数,没有返回值值,没有函数类型,故也不能被重载。
先构造的后析构,后构造的先析构。
对象数组
定义:Student Stu[3] = {Student(1001,18,87), Student(1002,19,76), Student(1003,18,72)}
使用:cout << Stu[0].num;
常对象与常成员函数:
Time const t1(12,34,56);
常对象的数据成员都不能被改变,注意:常对象不能调用非const的成员函数。
常数据成员的值不能被改变,故只能利用构造函数的参数初始化表对其进行初始化
常成员函数用const修饰,只能引用本类中的数据成员,而不能修改它。
静态数据成员、成员函数
静态数据成员、静态成员函数属于类,而不是具体的对象。
静态数据成员在所有对象中的值相等,只占一份内存单元。
静态成员函数只能操作静态数据成员。
友元函数
如果在本类以外的其他地方定义了一个函数,(该函数可以是或不是某类的成员函数),在类体中用friend对其进行声明,此函数即为本类的友元函数。
友元函数可以访问本类的私有成员。
例1:普通函数作为友元函数
#include <iostream>
using namespace std;
class Time
{
public:
Time(int,int,int); //构造函数
friend void display(Time &); //友元函数,引用形参
private:
int hour,minute,sec;
}
Time::Time(int h,int m,int n):hour(h),minute(m),sec(n)); //构造函数 参数初始化表
void display(Time &t) //定义友元函数
{
cout<<t.hour<<t.minute<<t.sec;
}
int main()
{
Time t(10,23,45);
display(t);
return 0;
}
例2:友元成员函数,另一个类的成员函数作为本类的友元函数
class Date; //提前引用声明
class Time
{
Time(int,int,int);
display(Data &);
}
class Date
{
Date(int,int,int);
friend void Time::display(Date &); //display是Time类的成员函数,也是本类的友元函数
}
两个类互相用到对方,故需要进行提前引用声明,否则编译报错。
一个函数可以被多个类声明为“友元”,这样就可以引用多个类中的私有数据。
友元类
友元类中的所有函数都是本类的友元函数,可以访问本类的所有成员。
声明的一般形式:friend 类名。
类模版
与函数模版类似,用于类体相同,而数据成员类型不同的情况。
template <class numtype> //声明一个类模版,numtype为虚拟的类型名
class Compare
{
public:
Compare(numtype a, numtype b){x=a;y=b;} //构造函数
numtype max();
numtype min();
private:
numtype x,y;
}
numtype Compare <numtype>::max()
{ return (x>y)?x:y; }
numtype Compare <numtype>::min()
{ return (x<y)?x:y; }
//创建对象时,注意要加上<int> <float>等类型标志
Compare <int> cmp;
Compare <int> cmp(3,7);
类模版中可以定义多个不同的类型参数
template <class T1, class T2) //T1,T2 为两种变量类型
class someclass
{...}
定义对象:
someclass <int,double> obj;
************************************************************************************************************************************
二、类的继承
基类、派生类的继承方式:
public:基类的公用成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类私有
protected:基类的公用成员和保护成员在派生类中成了保护成员,其私有成员仍为基类私有
private:默认,基类的公用成员和保护成员在派生类中成了私有成员,其私有成员仍为基类私有
保护成员:不能被外界引用,但可以被派生类的成员引用。
虽然派生类外不能通过派生类对象调用私有基类的公用成员函数,但可以通过派生类的成员函数调用私有基类的公用成员函数。
class Student
{
public:
Student(int a,string b,char c) //基类构造函数
{num=a;name=b;sex=c;}
void get_value();
void display();
private:
int num;
string name;
char sex;
}
class Student1: private Student
{
public:
void display1();
private:
int age;
string addr;
}
void Student1::display1()
{
display(); //派生类可以调用私有基类的公用成员函数,输出三个成员的值
}
void main()
{
Student1 stu1;
stu1.display(); //错误,私有基类(private继承)的公用成员函数在派生类中是私有函数,派生类对象不能调用私有基类的函数
stu1.display1();
stu1.age = 18; // 错误,age是派生类的私有成员,外界不可访问,只能通过成员函数访问。
}
派生类的构造函数和析构函数
基类的构造函数不可被继承,所以在设计派生类时不仅要考虑派生类所新增数据成员的初始化,还要考虑基类数据成员的初始化。
解决思路是在执行派生类的构造函数时,调用基类的构造函数。
一般形式:
派生类构造函数名(总参数表列):基类构造函数名(参数表列)
{
派生类中新增数据成员初始化语句
}
注意:
(1)总参数表列中不仅包括新增数据成员,还要包括基类构造函数用到的数据成员。
(2)派生类构造函数名后面的参数表列中,包括参数类型和参数名(int n),而基类名后面的参数表列中只有参数名而无类型名(a,b,c),因为这里不是定义基类构造函数,而是调用基类构造函数,因此这些参数是实参而不是形参。它们可以是常量、全局变量和派生类构造函数总参数表中的参数。
(3)派生类构造函数会将前面3个参数传递给基类构造函数的形参。
class Student1: private Student
{
public:
Student1(int a,string b,char c,int n,string s):Student(a,b,c);
void display1();
private:
int age;
string addr;
}
有子对象的派生类的构造函数
派生类中有 对象 数据成员时(数据成员里有其它类的对象 Student monitor),派生类的构造函数有三个任务:
(1)初始化基类数据成员
(2)初始化子对象数据成员 (!)
(3)初始化派生类数据成员
一般形式:
派生类构造函数名(总参数表列):基类构造函数名(参数表列),子对象名(参数表列)
{派生类中新增数据成员初始化语句。}
注:这里的总参数表列包括基类构造函数所需数据成员,子对象对应类的构造函数所需数据成员,派生类构造函数所需数据成员
派生类构造函数的执行顺序是
(1)调用基类构造函数
(2)调用子对象构造函数
(3)执行派生类的构造函数本身
多层派生时的构造函数
派生类2构造函数名(总参数表列):基类构造函数名(参数表列),派生类1构造函数名(参数表列) (从根向下)
{派生类中新增数据成员初始化语句。}
派生类中的析构函数
派生类不能继承基类的析构函数,派生类析构函数会自动调用基类的析构函数。
执行顺序从下往上,先执行派生类析构函数,再调用子对象的析构函数,最后执行基类析构函数。
多重继承:
一个派生类继承两个或多个基类。class D: public A, protected B, private C;
构造函数:
派生类构造函数名(总参数表列): 基类1构造函数(参数表列), 基类2构造函数(参数表列), 基类3构造函数(参数表列)
{派生类中新增数据成员初始化语句。}
同名覆盖:派生类新增加的数据成员会覆盖基类的同名成员。成员函数在函数名、参数个数、参数类型相同的情况下也会覆盖,否则为函数重载。
如果C继承的A,B两个类中有同名成员,怎么处理呢?
例:
C类中的数据成员: int A::a; int a::a1; int B::a; int B::a2; int a;
成员函数: void A::display(); void B::display(); void show();
类A、B中都有数据成员a、成员函数display,但它们代表不同的存储单元,可以分别存放不同的数据。
类C如何访问a、display呢? 解决方法:加上基类名
c1.A::a = 3;
c1.A::display();
虚基类
虚基类不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的,因为一个基类可以在生成一个派生类时是虚基类,而在生成另一个派生类时不是派生类。
一般形式: class 派生类名: virtual 继承方式 基类名
经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说基类成员只保留一次。
(否则,如果类D继承自类B、类C,而类B、类C又是继承自类A的情况下,类D中会存在两份类A的数据成员)
最后的派生类不仅要负责对其直接基类进行初始化,还要负责对虚基类进行初始化。编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其它派生类对虚基类的构造函数的调用,以保证虚基类的数据成员不会被多次初始化。
************************************************************************************************************************************
三、多态性
多态:一个事物有多种形态。
多态性:具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。
在面向对象方法中一般这样描述:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为。
如果一种语言只支持类而不支持多态,是不能称为面向对象的,只能说是基于对象。
函数的重载、运算符重载等都是多态现象。
社会中的例子:学校校长通知9月1日开学。不同的对象会作出不同反应:学生准备返校上课;教师要备课;后勤部门要准备教室、宿舍
如果不利用多态性,校长就要分别给学生、老师、后勤部门发通知,规定每一种人接到通知后怎么做,显然是件十分复杂细致的工作。
多态性分为两类:静态多态性和动态多态性
函数重载、运算符重载实现的多态性属于静态多态性,在程序编译时就能够决定调用哪一个函数,故静态多态性又称编译时的多态性,是通过函数的重载实现的(运算符重载实质上也是函数重载)。
动态多态性是在程序运行过程中才动态地确定操作所针对的对象,又称运行时的多态性,是通过虚函数实现的。
虚函数
虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类的同名函数。
class Student
{
public:
Student(int a,string b,char c) //基类构造函数
void display(){cout<<num<<name<<sex;}
private:
int num;
string name;
char sex;
}
class Graduate: private Student
{
public:
void display(){cout<<age<<addr;}
private:
string addr;
}
void main()
{
Student stu(1001,"LI",87.5);
Graduate gra(2001,"Ta",90,"london");
Student *pt = &stu;
pt->display();
pt=&gra; //改变指针指向
pt->display();
}
结果:两次执行的都是基类的display函数,为什么呢?
原因:本来,基类指针是用来指向基类对象的,如果用它指向派生类对象,则进行指针类型转换,将派生类对象的指针先转换为基类的指针,所以基类指针指向的是派生类对象
中的基类部分,因此无法用基类指针去调用派生类对象中的成员函数。
解决方法:
(1)直接用派生类的对象调用成员函数,gra.display
(2)再定义一个指针,指向派生类对象,Graduate *p2 = &gra; gra->display();
(3)虚函数,将基类中display的定义改为 virtual void display() 即可。
派生类中的虚函数取代了基类中原有的虚函数,因此使基类指针指向派生类对象后,调用虚函数时就会调用派生类的虚函数了。
虚函数是很有用处的:
在类的继承中,基类的成员函数可能并不适用于派生类,在派生类中新建一个函数是一个选择,但如果派生层次多,就要起很多函数名,不方便。如果采用同名函数,又会发生
同名覆盖。
利用虚函数就能很好的解决这个问题,当把某个成员函数声明为虚函数后,允许在其派生类中对该函数进行重新定义赋予新的功能,并且可以通过指向基类的指针指向同一类族
中不同类的对象,从而调用其中的同名函数。
由虚函数实现的动态多态性就是:同一类族中不同类的对象,对同一函数作出不同的响应。
当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。因此,在派生类中重新定义虚函数时,可以不加virtual,习惯上是加的以使程序清晰。
通过虚函数与指向基类对象的指针变量的配合使用,就能方便的调用同一类族中不同类的同名函数,只要先用基类指针指向即可。该指针指向不同类的对象,就会调用该对象的同名函数。
函数重载处理的是同一层次上同名函数问题,而虚函数处理的是不同派生层次上同名函数问题。同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的。
静态关联与动态关联
确定调用具体对象的过程成为关联(binding),这里是指把将一个函数名与一个类对象捆绑在一起,建立关联。一般来说,关联是吧一个标识符和一个存储地址联系起来。
编译系统要根据已有的信息,对同名函数的调用作出判断。例如函数的重载,系统是根据参数的个数和类型的不同去找与之匹配的函数的。
静态关联:函数重载和通过对象名调用的函数,在编译时即可确定函数所属的类,称static binding,又称早期关联。
动态关联:通过虚函数实现的绑定,只有在程序运行过程中,才能把要调用的虚函数和类对象关联起来,故称动态关联,也称滞后关联,又称运行时的多态性。
在什么情况下应当声明虚函数
注意事项:
(1)只能用virtual声明类的成员函数,使它成为虚函数,而不能将类外的普通函数声明为虚函数。因为虚函数的作用是允许在派生类中对基类的虚函数重新定义。显然,它只能用于类的继承层次中。
(2)一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同参数和返回值类型的同名函数。(这句没用吧,不是系统默认把派生类的属性定位virtual吗)
根据什么考虑把一个成员函数定义为虚函数
(1)首先考虑该类是否会作为基类,然后看该成员函数是否会被派生类修改。
(2)考虑对成员函数的调用是通过对象名还是基类指针或引用去访问,如果是通过基类指针或引用去访问的,则应当声明为虚函数。
(3)有时,定义虚函数时,其函数体可能是空的,其作用只是定义一个虚函数名,具体功能由派生类实现。
纯虚函数
基类中某一成员函数定义为虚函数,并非基类本身的需要,而是考虑到派生类的需要,因此在基类中预留了一个函数名,具体功能由派生类实现。
可以写成这样 virtual float area() const {return 0;}
为了简化,可以写成 virtual float area() const = 0; //
后面这种被“初始化”为0的虚函数形式就被成为纯虚函数。
纯虚函数没有函数体,后面的“=0”并不表示返回值为0,而只起形式上的作用,告诉编译系统这是纯虚函数。这是一个纯虚函数的声明语句,后面要加分号。
抽象类 abstract class
不用来定义对象而只作为一种基本类型用作继承的类成为抽象类,作为基类时又被成为抽象基类 abstract base class。凡是包含纯虚函数的类都是抽象类,纯虚函数不能被调用,包含纯虚函数的类是无法建立对象的。如果派生类对所有纯虚函数进行了定义,那么这个派生类就不是抽象类了,可以用来创建对象。
如果声明了一个类,一般用它来定义对象。但有些类不用来生成对象。定义它的目的是用它作基类去建立派生类,在此基础上根据需要定义出功能各异的派生类。
虚析构函数
当派生类的对象从内存中撤销时一般先调用派生类的析构函数,然后调用基类的析构函数。
但如果用new运算符建立了临时对象,若基类中有析构函数,并且定义了一个指向该基类的指针变量。在程序用带指针参数的delete运算符撤销对象时,系统会只执行基类的析构函数,而不执行派生类的析构函数。
解决方法:将基类的析构函数声明为虚函数。