面向对象程序设计

类的四个主要特征:抽象、封装、继承、多态    

封装性:一、将有关的数据和操作代码封装在一个对象中;二、隐蔽内部细节,只留少量接口,用以接收外部消息

继承 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运算符撤销对象时,系统会只执行基类的析构函数,而不执行派生类的析构函数。

解决方法:将基类的析构函数声明为虚函数。



发布了143 篇原创文章 · 获赞 11 · 访问量 77万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章