当前位置: 首页 > news >正文

第六讲:类、对象的简单应用及类封装、隐蔽性

第六讲:类、对象的简单应用及类封装、隐蔽性

本讲基本要求

掌握:类成员函数的简单应用,用类来设计程序。
理解:公用接口、私有接口、类中的方法、“消息”的概念。
重点、难点: 类成员函数的简单应用,用类来设计程序。

一、类和对象的简单应用举例(前两个为成员数据;后两个为成员函数)

例1 最简单的例子。
#includc <iostream>
using namespace std;
class Time     //定义Time类
    { public: //数据成员为公用的
        int hour;
        int minute;
        int sec; };
int main()
  { Time t1;     //定义t1为Time类对象
    cin>>t1.hour;//输入设定的时间
    cin>t1.minute;
    cin>>t1.sec;
    cout<<t1.hour<<":"<<t1.minute<<":"<<tl.sec<<endl; //输出时间
    return O;
  }

运行情况如下:
    12 32 43/ (输入时、分、秒)
   12:32:43 (输出时间)

注意:
    (1)在引用数据成员hour,minute,sec时不要忘记在前面指定对象名。
    (2)不要错写为类名,如写成Time.hour,Time.minute,Time.sec是不对的。因为类是一种抽象的数据类型,并不是一个实体,也不占存储空间,而对象是实际存在的实体,是占存储空间的,其数据成员是有值的,可以被引用的。
    (3)如果删去主函数的3个输入语句,即不向这些数据成员赋值,则它们的值是不可预知的。读者可以上机试一下。

例2 引用多个对象的成员。
    若有多个对象,需要分别引用多个对象中的数据成员,可以写出如下程序:

(1)程序(a)
#include<iostream>
using namespace std;
class Time
    { public:
      int hour;
      int minute;
      int sec;
     };
int main()
  { Time tl;      //定义对象tl
     cin>>t1.hour;//向n的数据成员输入数据
     cin>>t1.minute;
     cin>>t1.sec;
     cout<<t1.hour<<":"<<t1.nunute<<":"<<t1.sec<<endl;//输出tl中数据成员的值
    Time t2;       //定义对象t2
     cin>>t2.hour; //向t2的数据成员输入数据
     cin>>t2.minute;
     cin>>t2.sec;
     cout<<t2.hour<<":"<<t2.minute<<":"<<t2.sec<<endl;//输出t2中数据成员的值
    return O;}

运行情况如下:
   10 32 43/
   10:32:43
   22 32 43/
   22:32:43

    程序是清晰易懂的,但是在主函数中对不同的对象一一写出有关操作,会使程序冗长,如果有10个对象,那么主函数会有多长呢?这样会降低程序的清晰性,使阅读困难。为了解决这个问题,可以使用函数来进行输入和输出。见程序(b)。

(2)程序(b)
#include<iostream>
using namespace std;
class Time
    { public:
      int hour;
      int mimlte;
      int sec;
    };
int main()
  { void set_time(Time&); //函数声明
    void show_time(Time&);//函数声明
    Time t1;      //定义tl为Time类对象
    Set_time(t1); //调用set_time函数,向t1对象中的数据成员输入数据
    show_time(t1);//调用show3ime函数,输出d对象中的数据
    Time t2;      //定义t2为Time类对象
    set_time(t2); //调用set_tillle函数,向[2对象中的数据成员输入数据
    show_time([2];//调用show_time函数,输出t2对象中的数据
    return O;
  }
void set_nme(Time&t) //定义函数settlme,形参t是Time类型的引用变量
   { cin>>t.hour;   //输入设定的时间
     cin>>t.minue;
     cin>>t.sec;
   }
void show time(Time&t)//定义函数show_time,形参t是引用变量
   { cout<<t.hour<<":"<<t.minute<<":"<<t.sec<<endl;//输出对象中的数据
   }

运行情况与程序(a)相同。

    本程序在类外定义函数set_time和show_time。函数set_time用来给数据成员赋值,函数show_time用来显示数据成员的值。函数的形参t是Time类对象的引用变量,当主函数调用函数set_time(t1)时,由于set_time函数中的形参t是引用变量,因此它与实参tl共占同一段内存单元(所以说t是u的别名),,执行set_time函数相当于执行以下语句,向tl中的hour,minute和sec输入数值。
    cit>>t1.hour:
    cin>>t1.minute:
    cin>>t1.sec:
    调用show_time(t1)时,输出对象t1中的数据。用t2作实参时情况也一样。
注意:在程序中对t1和t2的定义是分别用两个语句完成的,并未写在一行上。C语言要求所有的声明必须集中写在本模块的开头,因此熟悉c语言的程序编写人员往往养成一个习惯,把所有声明集中写在本模块的开头。但是在C++编程中并不提倡这样做。在C++中,声明是作为语句处理的,可以出现在程序中的任何位置。因此,C++的编程人员习惯不把声明写在开头,而是用到时才进行声明(如同本程序那样),这样程序比较清晰,阅读方便。

(3)程序(c)
    可以对上面的程序作一些修改,数据成员的值不再由键盘输入,而在调用函数时由实参给出,并在函数中使用默认参数。将程序(b)第8行以下的部分修改为:

int main()
  { void set_time(Time&,int hour=0,int minute=O,int sec=0);//函数声明
    void show_time(Time&);    //函数声明
    Time tl;
     set_ttme(t1,12,23,34);//通过实参传递时、分、秒的值
     show_time(t1);
   Time t2;
     set_time(t2);            //使用默认的时、分、秒的值
     show_time(t2);
   return 0;
  }
void set_time(Time&t,int hour,int minute,int sec)
   { t.hour=hour;
     t.minute:minute;
     t.Sec=sec;
void show time(Time&t)
   { cout<<t.hour<<":"<<t.minute<<":"<<t.sec<<endl; }

    
程序运行时的输出为
   12:23:34 (t1中的时、分、秒)
   0:0:0 (t2中的时、分、秒)

     程序中只在main函数中声明set time函数时指定了默认参数,在定义set_time函数时不必重复指定默认参数。如果在定义函数时也指定默认参数,其值应与函数声明时一致,如果不一致,编译系统以函数声明时指定的默认参数值为准,在定义函数时指定的默认参数值不起作用。例如将定义settime函数的首行改为:    void set_time(Time&t,int hour=9,int minute=30,int sec=O)?
在编译时上行指定的默认参数值不起作用,程序运行结果仍为:
      12:23:34
     0:0:0

   以上两个程序中定义的类都只有数据成员,没有成员函数,这显然没有体现出使用类的优越性。之所以举这两个例子,主要想从最简单的情况开始逐步熟悉有关类的使用。在下面的例子中,类体中就包含了成员函数。

例3 将例2的程序改用含成员函数的类来处理。
#include<lostream>
using namespace std;
class Time
    { public:
        void set_time();  //公用成员函数
        void show_time(); //公用成员函数
      private:            //数据成员为私有
        int hour;
        int minute;
        int sec;
      }
int main()
  { Time tl;       //定义对象tl
    t1.set_time(); //调用对象t1的成员函数set_time,向t1数据成员输入数据
    t1.show_time();//调用对象t1成员函数show_time,输出d的数据成员的值
    Time t2;       //定义对象t2
    t2.set_time(); //调用对象t2成员函数set_time,向t2数据成员输入数据
    t2.show_time();//调用对象t2成员函数show_time,输出12的数据成员的值
    return 0;
   }
void Time::set_time() //在类外定义set_time函数
   { cin>>hour;
     cin>>minute;
     cin>>sec;
   }
void Time::show_time() //在类外定义show_time函数
{ cout<<hour<<":"<<minute<<":"<<sec<<endl; }

运行情况与例2.2中的程序(a)相同。

注意:

     (1)在主函数中调用两个成员函数时,应指明对象名(t1,t2)。虽然都是调用同一个display函数,但是结果是不同的。函数t1.display()只能引用对象tl中的数据成员,t2.display()只能引用对象t2中的数据成员。尽管tl和t2都属于同一类,但t1的成员函数只能访问11中的成员,而不能访问t2中的成员。反之亦然。
     (2)在类外定义函数时,应指明函数的作用域(如void Time::set_time())。在成员函数引用本对象的数据成员时,只需直接写数据成员名,这时C++系统会把它默认为本对象的数据成员。也可以显式地写出类名并使用域运算符。如上面最后一个函数的定义也可以写成:
     void Time::show_time()
        { cout<<Time::hour<<":"<<Time::minute<<":"<<Time::sec<<endl;// 加了类名限定
         }
在执行时,会根据this指针的指向,输出当前对象中的数据成员的值。
     (3)应注意区分什么场合用域运算符“::”,什么场合用成员运算符“.”,不要搞混。
例如在主函数中调用set_time函数,不能写成
     Time::set_time(); //错误
类型是抽象的,对象是具体的。定义成员函数时应该指定类名,因为定义的是该类中的成员函数,而调用成员函数时应该指定具体的对象名。后面不是跟域运算符“::”,而是跟成员运算符“.”。如.
     t1.set_time();或t2.set_time();

例4 找出一个整型数组中的元素的最大值。

    这个问题可以不用类的方法来解决,现在用类来处理,读者可以比较不同方法的特点。 可以编写出以下程序:

#include<iostream>
using namespace std;
class Array_max           //声明类
    { public:            //以下3行为成员函数原型声明
       void set_value(); //对数组元素设置值
       void max_value(); //找出数组中的最大元素
       void show_value();//输出最大值
      private:
       int array[1O];     //整型数组
       int max;          //max用来存放最大值
};
void Array_max::set_value()//成员函数定义,向数组元素输入数值
   { int i;
     for(i=0;i<lO;i++)
        cin>>array[i];}
void Array_max::max_value()//成员函数定义,找数组元素中的最大值
   { int i;
     max=array[O];
     for(i=1;i<10;i++)
        if(array[i]>max)max=array[i];}
void Array max::show_value() //成员函数定义,输出最大值
   { cout<<"max="<<max;}
int main()
  { Array_max arrmax;   //定义对象arrmax
    arrmax.set_value(); //调用armlax的set_value函数,向数组元素输入数值
    arrmax.max_value{); //调用arrmax的max_value函数,找出数组元素中的最大值
    arrmax.show_value();//调用arrmax的show_value函数,输出数组元素中的最大值
    returm 0;
   }

运行结果如下:
     12 12 39 -34 17 134 0 45 -91 76,/ (输入10个元素的值)
     max=134 (输入10个元素中的最大值)

    请注意成员函数定义与调用成员函数的关系,定义成员函数只是设计了一组操作代码,并未实际执行,只有在被调用时才真正地执行这一组操作。
    可以看出:主函数很简单,语句很少,只是调用有关对象的成员函数,去完成相应的 操作。在大多数情况下,主函数中甚至不出现控制结构(判断结构和循环结构),而在成 员函数中使用控制结构。在面向对象的程序设计中,最关键的工作是类的设计。所有的 数据和对数据的操作都体现在类中。只要把类定义好,编写程序的工作就显得很简单了。

二、类的封装性和信息隐蔽

1、公用接口与私有实现的分离

    1、C++通过类来实现封装性,把数据和与这些数据有关的操作封装在一个类中,或者说,类的作用是把数据和算法封装在用户声明的抽象数据类型中。在面向对象的程序设计中,在声明类时,一般都是把所有的数据指定为私有的,使它们与外界隔离,把需要让外界调用的成员函数指定为公用的,外界通过公用的函数来实现对数据的操作。外界与对象惟一的联系渠道就是调用公用的成员函数。这样就使类与外界的联系减少到最低限度。
    2、在声明了一个类以后,用户主要是通过调用公用的成员函数来实现类提供的功能(例如对数据成员赋值,显示数据成员的值,对数据进行加工等)。因此,公用成员函数是用户使用类的公用接口(pubic interface),或者说是类的对外接口。当然并不一定要把所有成员函数都指定为public(公用)的,但非public的成员函数就不是公用接口了。在类外虽然不能直接访问私有数据成员,但可以通过调用公用成员函数来引用甚至修改私有数据成员的值。
    3、用户可以调用公用成员函数来实现某些功能,而这些功能是在声明类时已指定的,用户可以使用它们而不应改变它们。实际上用户往往并不关心这些功能是如何实现的细节,而只需知道调用哪个函数会得到什么结果,能实现什么功能即可。如同使用照相机一样,只需知道按下快门就能照相即可,不必了解其实现细节,那是设计师和制造商的事。
    4、照相机的快门就是公用接口,用户通过使用快门实现照相的目的,但不能改变相机的结构和功能。一切与用户操作无关的部分都封装在机箱内,用户看不见,摸不着,改不了,这就是接口与实现分离。

    通过成员函数对数据成员进行操作称为类的实现,为了防止用户任意修改公用成员函数,改变对数据进行的操作,往往不让用户看到公用成员函数的源代码,显然更不能修改它,用户只能接触到公用成员函数的目标代码。可以看到:类中被操作的数据是私有的,实现的细节对用户是隐蔽的,这种实现称为私有实现(pnvate implementation)。这种“类的公用接口与私有实现的分离”形成了信息隐蔽。
    软件工程的一个最基本的原则就是将接口与实现分离,信息隐蔽是软件工程中一个非常重要的概念。它的好处在于:
    (1)如果想修改或扩充类的功能,只需修改奉类中有关的数据成员和与它有关的成员函数,程序中类外的部分可以不必修改。例如,想在声明的student类中增加一项数据成员“年龄”,只需这样改:

class student
    { private:
       int num;
       smng name;
       int age; //此行是新增的
       char sex;
      public:
       void display()
          { cout<<"num:"<<num<<endl;
            cout<<”nameI"<<name<<endl;
            cout<<"age:”<<age<<endl; //此行是新增的
            cout<<"sex:"<<sex<<endl; }
          };
 student stud;

    注意:虽然类中的数据成员改变了,成员函数display的定义改变了,但是类的对外接口没有改变,外界仍然通过公用的display函数访问类中的数据。程序中其他任何部分均无需修改。当然,类的功能改变了,在调用stud对象的display时,输出该学生的学号、姓名、年龄和性别的值。
    可以看出:当接口与实现(对数据的操作)分离时,只要类的接口没有改变,对私有实现的修改不会影响程序的其他部分。对用户来说,类的实现方法的改变,不会影响用户,只要保持类的接口不变即可。譬如,软件开发商想对以前提供给客户的类库进行修改升级,只要保持类的接口不变,即用户调用成员函数的方法(包括函数参数的类型和个数)不变,用户的程序就不必修改。

    (2)如果在编译时发现类中的数据读写有错,不必检查整个程序,只需检查本类中访问这些数据的少数成员函数。这样,就使得程序(尤其是大程序)的设计、修改和调试都显得方便简单了。

2、类声明和成员函数定义的分离

    如果一个类只被一个程序使用,那么类的声明和成员函数的定义可以直接写在程序的开头,但是如果一个类被多个程序使用,这样做的重复工作量就很大了,效率就太低了。
    在面向对象的程序开发中,一般做法是将类的声明(其中包含成员函数的声明)放在指定的头文件中,用户如果想用该类,只要把有关的头文件包含进来即可,不必在程序中重复书写类的声明,以减少工作量,节省篇幅,提高编程的效率。
    由于在头文件中包含了类的声明,因此在程序中就可以用该类来定义对象。由于在类体中包含了对成员函数的声明,在程序中就可以调用这些对象的公用成员函数。为了实现上一节所叙述的信息隐蔽,对类成员函数的定义一般不放在头文件中,而另外放在一个文件中。

例如,可以分别写两个文件:
//student.h (这是头文件,在此文件中进行类的声明)

class student            //类声明
    { public:
        void display(); //公用成员函数原型声明
      private:
        int num;
        char name[20];
        char sex;
    };
//student.cpp //在此文件中进行函数的定义
#include<iostream>
#include"student.h"     //不要漏写此行,否则编译通不过
void Student::display() //在类外定义display类函数
   { cout<<"num:"<<num<<endl;
     cout<<"name:"<<name<<endl;
     cout<<"sex:"<<sex<<endl;}
为了组成一个完整的源程序,还应当有包括主函数的源文件:
//main.cpp 主函数模块

#include<iostream>
#include"student.h" //将类声明头文件包含进来
int main()
  { Student stud; //定义对象
    stud.display(); //执行stud对象的display函数
    retum 0; }

    这是一个包括3个文件的程序,组成两个文件模块:一个是主模块main.cpp,一个是student.cpp。在主模块中又包含头文件student.h。在预编译时会将头文件student.h中的内容取代#include"student.h"行。请注意:由于将头文件student.h放在用户当前目录中,因此在文件名两侧用双撇号包起来("student.h")而不用尖括号(<student.h>),否则编译时会找不到此文件。
    可以按照对多文件程序的编译和运行方法对程序进行编译和连接。C++编译系统对两个源文件main.cpp和student.cpp分别进行编译,得到两个目标程序main.obj和student.obj(目标文件的后级在不同的C++编译系统中是不同的,例如在GCC中,后缀是.o,这里用.obj是对一般目标程序而言的。),然后将它们和其他系统资源连接起来,形成可执行文件main.exe。

    在执行主函数时调用stud中的display函数,输出各数据成员的值。这只是一个程序的框架,在Student类中包括了数据成员nam,name和sex,但是并没有对stud对象中的数据成员赋值。程序虽能通过编译并可以运行,但输出的值是不可预知的、无意义的。请读者对以上程序框架作必要的补充(可以在Student类中增加一个对数据成员赋值的set函数)。
   可能会考虑这样一个问题:如果一个类声明多次被不同的程序所选用,每次都要对包含成员函数定义的源文件(如上面的student.cpp)进行编译,这是否可以改进呢?的确,可以不必每次都对它重复进行编译,而只需编译一次即可。把第一次编译后所形成的目标文件保存起来,以后在需要时把它调出来直接与程序的目标文件相连接即可。这和使用函数库中的函数是类似的。
    这也是把成员函数的定义不放在头文件中的一个好处。如果对成员函数的定义也放在类声明的头文件中,那么,在对使用这些类的每一个程序的每一次编译时都必然包括对成员函数定义的编译,即同一个成员函数的定义会多次被重复编译。只有把对成员函数的定义单独放在另一文件中,单独编译,才能做到不重复编译。
    在实际工作中,并不是将一个类声明做成一个头文件,而是将若干个常用的功能相近的类声明集中在一起,形成类库。类库有两种:一种是C++编译系统提供的标准类库;
    一种是用户根据自己的需要做成的用户类库,提供给自己和自己授权的人使用,这称为自定义类库。在程序开发工作中,类库是很有用的,它可以减少用户自己对类和成员函数进 行定义的工作量。
类库包括两个组成部分:(1)类声明头文件;(2)已经过编译的成员函数的定义,它是目标文件。用户只需把类库装入到自己的计算机系统中(一般装到c++编译系统所在的子目录下),并在程序中用#include命令行将有关的类声明的头文件包含到程序中, 就可以使用这些类和其中的成员函数,顺利地运行程序。
这和在程序中使用c++系统提供的标准函数的方法是一样的,例如用户在调用sin函数时只需将包含声明此函数的头文件包含到程序中,即可调用该库函数,而不必了解sin函数是怎么实现的(函数值是怎样计算出来的)。当然,前提是系统已装了标准函数 库。在用户源文件经过编译后,与系统库(是目标文件)相连接。
    在用户程序中包含类声明头文什,类声明头文件就成为用户使用类库的有效方法和公用接口,用户只有通过头文件才能使用有关的类。用户看得见和接触到的是这个头文件,任何要使用这个类的用户只需包含这个头文件即可。包含成员函数定义的文件就是 类的实现。请特别注意:类声明和函数定义一般是分别放在两个文件中的。
    由于接口与实现分离,就为软件开发商向用户提供类库创造了很好的条件。开发商把用户所需的各种类的声明按类放在不同的头文什中,同时对包含成员函数定义的源文件进行编译,得到成员函数定义的目标代码。软件商向用户提供这些头文件和类的实现的目标代码(不提供函数定义的源代码)。用户在使用类库中的类时,只需将有关头文件包含到自己的程序中,并且在编译后连接成员函数定义的目标代码即可。用户可以看到头文件中类的声明和成员函数的原型声明,但看不到定义成员函数的源代码,更无法修改成员函数的定义,开发商的权益得到保护。
    由于类库的出现,用户可以像使用零件一样方便地使用在实践中积累的通用的或专用的类,这就大大减少了程序设计的工作量,有效地提高了工作效率。

3、面向对象程序设计中的几个名词

    顺便介绍面向对象程序设计中的几个名词:

    1、方法:类的成员函数在面向对象程序理论中被称为“方法”(method),“方法”是指对数据的操作。一个“方法”对应一种操作。显然,只有被声明为公用的方法(成员函数)才能被对象外界所激活。

    2、消息:外界是通过发“消息”来激活有关方法的。所谓“消息”,其实就是一个命令,由程序语句来实现。前面的stud.display();就是向对象stud发出的一个“消息”,通知它执行其中的display“方法”(即display函数)。

    3、对象:客观世界中任何一个事物都可以看成一个对象(object)。对象可大可小;可以是自然体也可以是逻辑体;对象可以嵌套;对象有两个要素:静态特征称为属性(attribute),动态特征称为行为(behavior)。5、控制行为改变的因素称之为消息。要使某一个对象实现某一种行为(即操作),应当向它传送相应的消息。

    上面这个语句涉及3个术语:对象、方法和消息:stud是对象,display()是方法,语句“stud.display();”是消息。
    

转自:http://210.44.195.12/cgyy/text/HTML/text/06.htm 

 

 

 

相关文章:

  • 第七讲:构造函数与析构函数
  • 第八讲:对象数组与指针
  • elk 日志处理的一点思路
  • 第九讲:共用数据的保护与对象的动态处理
  • rtems 4.11 IRQ (arm,beagle)
  • 第十讲:对象的赋值和复制
  • jQuery插件之ajaxFileUpload[转载]
  • 第十一讲:运算符重载与重载函数
  • Google Chrome Frame 自定义渲染方式,调用ActiveX
  • 第十二讲:重载单、双目、插入、提取运算符
  • 第十三讲:不同类型数据间的转换
  • 一)6张表
  • 第十四讲:继承与派生的概念
  • Spring MyBatis Oracle 多数据源
  • 第十五讲:派生类的构造函数和析构函数
  • 收藏网友的 源程序下载网
  • $translatePartialLoader加载失败及解决方式
  • CSS魔法堂:Absolute Positioning就这个样
  • Golang-长连接-状态推送
  • input实现文字超出省略号功能
  • leetcode378. Kth Smallest Element in a Sorted Matrix
  • Phpstorm怎样批量删除空行?
  • PHP的类修饰符与访问修饰符
  • Promise面试题2实现异步串行执行
  • SpiderData 2019年2月25日 DApp数据排行榜
  • Storybook 5.0正式发布:有史以来变化最大的版本\n
  • vue从入门到进阶:计算属性computed与侦听器watch(三)
  • WordPress 获取当前文章下的所有附件/获取指定ID文章的附件(图片、文件、视频)...
  • 浮现式设计
  • 使用Tinker来调试Laravel应用程序的数据以及使用Tinker一些总结
  • 译自由幺半群
  • #14vue3生成表单并跳转到外部地址的方式
  • (1)(1.8) MSP(MultiWii 串行协议)(4.1 版)
  • (4)(4.6) Triducer
  • (8)Linux使用C语言读取proc/stat等cpu使用数据
  • (bean配置类的注解开发)学习Spring的第十三天
  • (备忘)Java Map 遍历
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (力扣题库)跳跃游戏II(c++)
  • (利用IDEA+Maven)定制属于自己的jar包
  • (三十五)大数据实战——Superset可视化平台搭建
  • (十)DDRC架构组成、效率Efficiency及功能实现
  • .naturalWidth 和naturalHeight属性,
  • .NET 2.0中新增的一些TryGet,TryParse等方法
  • .NET 8.0 发布到 IIS
  • .NET MVC之AOP
  • .net MySql
  • .NET Remoting Basic(10)-创建不同宿主的客户端与服务器端
  • .NetCore实践篇:分布式监控Zipkin持久化之殇
  • .net对接阿里云CSB服务
  • .NET框架
  • @vue/cli脚手架
  • [BZOJ2208][Jsoi2010]连通数
  • [codeforces]Recover the String
  • [CSAWQual 2019]Web_Unagi ---不会编程的崽