类和对象
对象是对客观事物的抽象,类是对对象的抽象。类是一种抽象的数据类型,其定义为:
class 类名{
}
它们的关系是,对象是类的实例,类是对象的模板。
声明定义
类对象的定义
类是现实世界或思维世界中的实体在计算机中的反映,它将数据以及这些数据上的操作封装在一起。
对象是具有类类型的变量。类和对象是面向对象编程技术中的最基本的概念。[2]
类是对象的抽象,而对象是类的具体实例。类是抽象的,不占用内存,而对象是具体的,占用存储空间。类是用于创建对象的蓝图,它是一个定义包括在特定类型的对象中的方法和变量的软件模板。
1 2 3 4 5 6 7 8 9 | class 类名 { public: 公用的数据和成员函数 protected: 保护的数据和成员函数 private: 私有的数据和成员函数 } |
定义对象方法
1.先声明类类型,然后再定义对象
举例:Student stud1,stud2; //Student是已经声明的类类型
2.在声明类类型的同时定义对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Student//声明类类型 { public://先声明公用部分 void display() { cout<<″num:″<<num<<endl; cout<<″name:″<<name<<endl; cout<<″sex:″<<sex<<endl; } private://后声明私有部分 int num; char name[20]; char sex; } stud1,stud2;//定义了两个Student类的对象 //在定义Student类的同时,定义了两个Student类的对象。 |
3.不出现类名,直接定义对象
1 2 3 4 5 6 7 8 | class//无类名 { private://声明以下部分为私有的 ┆ public://声明以下部分为公用的 ┆ } stud1,stud2;//定义了两个无类名的类对象 |
直接定义对象,在C++中是合法的、允许的,但却很少用,也不提倡用。在实际的程序开发中,一般都采用上面3种方法中的第1种方法。在小型程序中或所声明的类只用于本程序时,也可以用第2种方法。在定义一个对象时,编译系统会为这个对象分配存储空间,以存放对象中的成员。
C++增加了class类型后,仍保留了结构体类型(struct ),而且把它的功能也扩展了。C++允许用struct来定义一个类型。如可以将前面用关键字class声明的类类型改为用关键字struct。
为了使结构体类型也具有封装的特征,C++不是简单地继承C的结构体,而是使它也具有类的特点,以便于用于面向对象程序设计。
用struct声明的结构体类型实际上也就是类。用struct声明的类,如果对其成员不作private或public的声明,系统将其默认为public。
如果想分别指定私有成员和公用成员,则应用private或public作显式声明。
而用class定义的类,如果不作private或public声明,系统将其成员默认为private,在需要时也可以自己用显式声明改变。
如果希望成员是公用的,使用struct比较方便,如果希望部分成员是私有的,宜用class。建议尽量使用class来建立类,写出完全体现C++风格的程序。
编辑
类的成员函数(简称类函数)是函数的一种,它的用法和作用和第4章介绍过的函数基本上是一样的,它也有返回值和函数类型,
它与一般函数的区别只是:
它是属于一个类的成员,出现在类体中。
它可以被指定为private(私有的)、public (公用的)或protected(受保护的)。
在使用类函数时,要注意调用它的权限(它能否被调用)以及它的作用域(函数能使用什么范围中的数据和函数)。
例如私有的成员函数只能被本类中的其它成员函数所调用,而不能被类外调用。
成员函数可以访问本类中任何成员(包括私有的和公用的),可以引用在本作用域中有效的数据。
一般的做法是将需要被外界调用的成员函数指定为public,它们是类的对外接口。
但应注意,并非要求把所有成员函数都指定为public。有的函数并不是准备为外界调用的,而是为本类中的成员函数所调用的,就应该将它们指定为private。
这种函数的作用是支持其它函数的操作,是类中其它成员的工具函数(utility function),类外用户不能调用这些私有的工具函数。
类的成员函数是类体中十分重要的部分。如果一个类中不包含成员函数,就等同于C语言中的结构体了,体现不出类在面向对象程序设计中的作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | classStudent {public: voiddisplay(); //公用成员函数原型声明 private: intnum; stringname; charsex; //以上3行是私有数据成员 }; voidStudent∷display() //在类外定义display类函数 {cout<<″num:″<<num<<endl; //函数体 cout<<″name:″<<name<<endl; cout<<″sex:″<<sex<<endl; } Studentstud1,stud2; //定义两个类对象 |
注意:在类体中直接定义函数时,不需要在函数名前面加上类名,因为函数属于哪一个类是不言而喻的。
但成员函数在类外定义时,必须在函数名前面加上类名,予以限定(qualifed),“∷”是作用域限定符(field qualifier)或称作用域运算符,用它声明函数是属于哪个类的。
如果在作用域运算符“∷”的前面没有类名,或者函数名前面既无类名又无作用域运算符“∷”,
如∷display( ) 或 display( ),则表示display函数不属于任何类,这个函数不是成员函数,而是全局函数,即非成员函数的一般普通函数。
类函数必须先在类体中作原型声明,然后在类外定义,也就是说类体的位置应在函数定义之前,否则编译时会出错。
虽然函数在类的外部定义,但在调用成员函数时会根据在类中声明的函数原型找到函数的定义(函数代码),从而执行该函数。
在类的内部对成员函数作声明,而在类体外定义成员函数,这是程序设计的一种良好习惯。
如果一个函数,其函数体只有2-3行,一般可在声明类时在类体中定义。多于3行的函数,一般在类体内声明,在类外定义。
inline 函数
在类体中定义的成员函数的规模一般都很小,而系统调用函数的过程所花费的时间开销相对是比较大的。调用一个函数的时间开销远远大于小规模函数体中全部语句的执行时间。
为了减少时间开销,如果在类体中定义的成员函数中不包括循环等控制结构,C++系统会自动将它们作为内置(inline )函数来处理。
也就是说,在程序调用这些成员函数时,并不是真正地执行函数的调用过程(如保留返回地址等处理),而是把函数代码嵌入程序的调用点。
这样可以大大减少调用成员函数的时间开销。C++要求对一般的内置函数要用关键字inline声明,但对类内定义的成员函数,可以省略inline,因为这些成员函数已被隐含地指定为内置函数。如
1 2 3 4 5 6 7 8 9 10 | classStudent {public: voiddisplay() {cout<<″num:″<<num<<endl;cout<<″name:″ <<name<<endl;cout<<″sex:″<<sex<<endl;} private: intnum; stringname; charsex; }; |
其中第3行
void display( ) 也可以写成
inline voiddisplay( )
将display函数显式地声明为内置函数。
以上两种写法是等效的。对在类体内定义的函数,一般都省写inline。
应该注意的是:如果成员函数不在类体内定义,而在类体外定义,系统并不把它默认为内置(inline )函数,调用这些成员函数的过程和调用一般函数的过程是相同的。如果想将这些成员函数指定为内置函数,应当用inline作显式声明。如
1 2 3 4 5 6 7 8 9 10 | classStudent { public:inlinevoiddisplay();//声明此成员函数为内置函数 private: intnum; stringname; charsex; }; inlinevoidStudent∷display()//在类外定义display函数为内置函数 {cout<<″num:″<<num<<endl;cout<<″name:″<<name<<endl;cout<<″sex:″<<sex<<endl;} |
在函数的声明或函数的定义两者之一作inline声明即可。
值得注意的是:如果在类体外定义inline函数,则必须将类定义和成员函数的定义都放在同一个头文件中(或者写在同一个源文件中),否则编译时无法进行置换(将函数代码的拷贝嵌入到函数调用点)。
但是这样做,不利于类的接口与类的实现分离,不利于信息隐蔽。虽然程序的执行效率提高了,但从软件工程质量的角度来看,这样做并不是好的办法。
只有在类外定义的成员函数规模很小而调用频率较高时,才将此成员函数指定为内置函数。
用类去定义对象时,系统会为每一个对象分配存储空间。
如果一个类包括了数据和函数,要分别为数据和函数的代码分配存储空间。
按理说,如果用同一个类定义了10个对象,那么就需要分别为10个对象的数据和函数代码分配存储单元。
能否只用一段空间来存放这个共同的函数代码段,在调用各对象的函数时,都去调用这个公用的函数代码。
显然,这样做会大大节约存储空间。C++编译系统正是这样做的,因此每个对象所占用的存储空间只是该对象的数据部分所占用的存储空间,而不包括函数代码所占用的存储空间。如果声明了一个类:
1 2 3 4 5 6 7 8 9 | classTime { public: inthour; intminute; intsec; voidset() {cin>>a>>b>>c;} }; |
可以用下面的语句来输出该类对象所占用的字节数:
cout<<sizeof(Time)<<endl;
输出的值是12。
这就证明了一个对象所占的空间大小只取决于该对象中数据成员所占的空间,而与成员函数无关。
函数代码是存储在对象空间之外的。如果对同一个类定义了10个对象,这些对象的成员函数对应的是同一个函数代码段,而不是10个不同的函数代码段。
需要注意的是:虽然调用不同对象的成员函数时都是执行同一段函数代码,但是执行结果一般是不相同的。
不同的对象使用的是同一个函数代码段,它怎么能够分别对不同对象中的数据进行操作呢?
原来C++为此专门设立了一个名为this的指针,用来指向不同的对象。需要说明:
(1) 不论成员函数在类内定义还是在类外定义,成员函数的代码段都用同一种方式存储。
(2) 不要将成员函数的这种存储方式和inline(内置)函数的概念混淆。
(3) 应当说明:常说的“某某对象的成员函数”,是从逻辑的角度而言的,而成员函数的存储方式,是从物理的角度而言的,二者是不矛盾的。
成员引用
编辑
例如在程序中可以写出以下语句:
stud1.num=1001;//假设num已定义为公用的整型数据成员
表示将整数1001赋给对象stud1中的数据成员num。
其中“.”是成员运算符,用来对成员进行限定,指明所访问的是哪一个对象中的成员。
注意不能只写成员名而忽略对象名。
访问对象中成员的一般形式为
对象名.成员名
不仅可以在类外引用对象的公用数据成员,而且还可以调用对象的公用成员函数,但同样必须指出对象名,如
stud1.display();//正确,调用对象stud1的公用成员函数
display( );//错误,没有指明是哪一个对象的display函数
由于没有指明对象名,编译时把display作为普通函数处理。
应该注意所访问的成员是公用的(public )还是私有的(private )。只能访问public成员,而不能访问private成员,如果已定义num为私有数据成员,下面的语句是错误的:
stud1.num=10101;//num是私有数据成员,不能被外界引用
在类外只能调用公用的成员函数。在一个类中应当至少有一个公用的成员函数,作为对外的接口,否则就无法对对象进行任何操作。
class Time
{
public : //数据成员是公用的
int hour;
int minute;
};Time t,*p;//定义对象t和指针变量p
p=&t;//使p指向对象t
cout<<p->hour;//输出p指向的对象中的成员hour
在p指向t的前提下,p->hour,(*p).hour和t.hour三者等价。
如果为一个对象定义了一个引用变量,它们是共占同一段存储单元的,实际上它们是同一个对象,只是用不同的名字表示而已。
因此完全可以通过引用变量来访问对象中的成员。
如果已声明了Time类,并有以下定义语句:
Time t1; //定义对象t1
Time&t2=t1;//定义Time类引用变量t2,并使之初始化为t1
cout<<t2.hour;//输出对象t1中的成员hour
由于t2与t1共占同一段存储单元(即t2是t1的别名),因此t2.hour就是t1.hour。
应用举例
编辑
最简单的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include<iostream> usingnamespacestd; classTime//定义Time类 {public://数据成员为公用的 inthour; intminute; intsec;}; intmain() { Timet1;//定义t1为Time类对象 cin>>t1.hour;//输入设定的时间 cin>>t1.minute; cin>>t1.sec; //输出时间: cout<<t1.hour<<″:″<<t1.minute<<″:″<<t1.sec<<endl; return0; } |
运行情况如下: 1232 43↙
12:32:43
注意:
(1) 在引用数据成员hour,minute,sec时不要忘记在前面指定对象名。
(2) 不要错写为类名,
如写成
Time.hour,Time.minute,Time.sec是不对的。因为类是一种抽象的数据类型,并不是一个实体,也不占存储空间,而对象是实际存在的实体,是占存储空间的,其数据成员是有值的,可以被引用的。
(3) 如果删去主函数的3个输入语句,即不向这些数据成员赋值,则它们的值是不可预知的。
例2
引用多个对象的成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | (1)程序(a) #include<iostream> usingnamespacestd; classTime {public: inthour; intminute; intsec; }; intmain() {Timet1;//定义对象t1 cin>>t1.hour;//向t1的数据成员输入数据 cin>>t1.minute; cin>>t1.sec; cout<<t1.hour<<″:″<<t1.minute<<″:″<<t1.sec<<endl;//输出t1中数据成员的值 Timet2;//定义对象t2 cin>>t2.hour;//向t2的数据成员输入数据 cin>>t2.minute; cin>>t2.sec; cout<<t2.hour<<″:″<<t2.minute<<″:″<<t2.sec<<endl;//输出t2中数据成员的值 return0; } 运行情况如下: 103243↙ 10:32:43 223243↙ 22:32:43 程序是清晰易懂的,但是在主函数中对不同的对象一一写出有关操作,会使程序冗长。为了 解决这个问题,可以使用函数来进行输入和输出。见程序(b)。 (2)程序(b) #include<iostream> usingnamespacestd; classTime {public: inthour; intminute; intsec; }; intmain() { voidset_time(Time&);//函数声明 voidshow_time(Time&);//函数声明 Timet1;//定义t1为Time类对象 set_time(t1);//调用set_time函数,向t1对象中的数据成员输入数据 show_time(t1);//调用show_time函数,输出t1对象中的数据 Timet2;//定义t2为Time类对象 set_time(t2);//调用set_time函数,向t2对象中的数据成员输入数据 show_time(t2);//调用show_time函数,输出t2对象中的数据 return0; } voidset_time(Time&t)//定义函数set_time,形参t是引用变量 { cin>>t.hour;//输入设定的时间 cin>>t.minute; cin>>t.sec; } voidshow_time(Time&t)//定义函数show_time,形参t是引用变量 { cout<<t.hour<<″:″<<t.minute<<″:″<<t.sec<<endl;//输出对象中的数据 } |
运行情况与程序(a)相同。
3)程序(c)
可以对上面的程序作一些修改,数据成员的值不再由键盘输入,而在调用函数时由实参给出,并在函数中使用默认参数。将程序(b)第8行以下部分改为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | intmain() { voidset_time(Time&,inthour=0,int minute=0,intsec=0);//函数声明 voidshow_time(Time&);//函数声明 Timet1; set_time(t1,12,23,34);//通过实参传递时、分、秒的值 show_time(t1); Timet2; set_time(t2);//使用默认的时、分、秒的值 show_time(t2); return0; } voidset_time(Time&t,inthour,intminute,intsec) { t.hour=hour; t.minute=minute; t.sec=sec; } voidshow_time(Time&t) { cout<<t.hour<<″:″<<t.minute<<″:″<<t.sec<<endl; } |
程序运行时的输出为
12:23:34 (t1中的时、分、秒)
0:0:0 (t2中的时、分、秒)
以上两个程序中定义的类都只有数据成员,没有成员函数,这显然没有体现出使用类的优越性。在下面的例子中,类体中就包含了成员函数。
将例2的程序改用含成员函数的类来处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #include<iostream> usingnamespacestd; classTime {public: voidset_time();//公用成员函数 voidshow_time();//公用成员函数 private://数据成员为私有 inthour; intminute; intsec; }; intmain() { Timet1;//定义对象t1 t1.set_time();//调用对象t1的成员函数set_time,向t1的数据成员输入数据 t1.show_time();//调用对象t1的成员函数show_time,输出t1的数据成员的值 Timet2;//定义对象t2 t2.set_time();//调用对象t2的成员函数set_time,向t2的数据成员输入数据 t2.show_time();//调用对象t2的成员函数show_time,输出t2的数据成员的值 return0; } voidTime∷set_time()//在类外定义set_time函数 { cin>>hour; cin>>minute; cin>>sec; } voidTime∷show_time()//在类外定义show_time函数 { cout<<hour<<″:″<<minute<<″:″<<sec<<endl; } |
运行情况与例2中的程序(a)相同。
注意:
(1) 在主函数中调用两个成员函数时,应指明对象名(t1,t2)。表示调用的是哪一个对象的成员函数。
(2) 在类外定义函数时,应指明函数的作用域(如void Time∷set_time( ))。在成员函数引用本对象的数据成员时,只需直接写数据成员名,这时C++系统会把它默认为本对象的数据成员。也可以显式地写出类名并使用域运算符。
(3) 应注意区分什么场合用域运算符“∷”,什么场合用成员运算符“.”,不要搞混。
找出一个整型数组中的元素的最大值。这个问题可以不用类的方法来解决,用类来处理,读者可以比较不同方法的特点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #include<iostream> usingnamespacestd; classArray_max//声明类 {public://以下3行为成员函数原型声明 voidset_value();//对数组元素设置值 voidmax_value();//找出数组中的最大元素 voidshow_value();//输出最大值 private: intarray[10];//整型数组 intmax;//max用来存放最大值 }; voidArray_max∷set_value()//成员函数定义,向数组元素输入数值 {inti; for(i=0;i<10;i++) cin>>array[i]; } voidArray_max∷max_value()//成员函数定义,找数组元素中的最大值 {inti; max=array[0]; for(i=1;i<10;i++) if(array[i]>max)max=array[i]; } voidArray_max∷show_value()//成员函数定义,输出最大值 {cout<<″max=″<<max;} intmain() {Array_maxarrmax;//定义对象arrmax arrmax.set_value();//调用arrmax的set_value函数,向数组元素输入数值 arrmax.max_value();//调用arrmax的max_value函数,找出数组元素中的最大值 arrmax.show_value();//调用arrmax的show_value函数,输出数组元素中的最大值 return0; } |
运行结果如下:
12 12 39 -34 17134 045 -91 76↙ (输入10个元素的值)
max=134 (输入10个元素中的最大值)
请注意成员函数定义与调用成员函数的关系,定义成员函数只是设计了一组操作代码,并未实际执行,只有在被调用时才真正地执行这一组操作。
可以看出: 主函数很简单,语句很少,只是调用有关对象的成员函数,去完成相应的操作。
在大多数情况下,主函数中甚至不出现控制结构(判断结构和循环结构),而在成员函数中使用控制结构。
在面向对象的程序设计中,最关键的工作是类的设计。所有的数据和对数据的操作都体现在类中。
只要把类定义好,编写程序的工作就显得很简单了。