一、对象的赋值和复制
1、对象的赋值
如果对一个类定义了两个或多个对象,则这些同类的对象之间可以互相赋值,或者说,一个对象的值可以赋给另一个同类的对象。这里所指的对象的值是指对象中所有数据成员的值。对象之间的赋值也是通过赋值运算符"="进行的。本来,赋值运算符"="只能用来对单个的变量赋值,现在被扩展为两个同类对象之间的赋值,这是通过对赋值运算符的重载实现的(关于运算符的重载将在第4章中介绍)。实际上这个过程是通过成员复制(memberwise copy)来完成的,即将一个对象的成员值一一复制给另一对象的对应成员。
对象赋值的一般形式为:对象名1=对象名2;
注意:对象名l和对象名2必须属于同一个类。例如Student studl,stud2;//定义两个同类的对象stud2=studl;//将studl各数据成员的值赋给stud2通过下面的例子可以了解怎样进行对象的赋值。
例9 对象的赋值。#include <iostream>using namespace std;class Box{ public:Box(int=10,int=10,int=10); //声明有默认参数的构造函数int volume();private:int height;int width;int length; };Box::Box(int h,int w,int len){ height=h;width=w;length=len; }int Box::volume(){ return(height*width*length); }//返回体积的值int main(){ Box box1(15,30,25),box2; //定义两个对象box1和box2 cout<<"The volume of box1 is "<<box1.volume()<<endl; //输山boxl的体积box2=box1; //将box的值财给1box2 cout<<"The volume of box2 is "<<box2.volume()<<endl; //输出box2的体积return 0; }运行结果如下: The volume Of boxl is 11250 The volume Of box2 is 11250说明:(1)对象的赋值只对其中的数据成员赋值,不对成员函数赋值。数据成员是占存储空间的,不同对象的数据成员占有不同的存储空间,赋值的过程是将一个对象的数据成员在存储空间的状态复制给另一对象的数据成员的存储空间。而不同对象的成员函数是同一个函数代码段,不需要、也无法对它们赋值。(2)类的数据成员中不能包括动态分配的数据,否则在赋值时可能出现严重后果(在此不作详细分析,只需记住这一结论即可)。
2、对象的复制
有时需要用到多个完全相同的对象:其一般形式为:类名对象2(对象1); //用对象l复制出对象2。
有这就是对象的复制机制。用一个已有的对象快速地复制出多个完全相同的对象。
如:Box box2(boxl);// 其作用是用已有的对象boxl去克隆出一个新对象box2。
可以看到:它与前面介绍过的定义对象方式类似,但是括号中给出的参数不是一般的变量,而是对象。在建立一个新对象时调用一个特殊的构造函数——复制构造函数(copy constructor)。这个函数的形式是这样的:
//The copy constructor definitionBox::Box(const Box&b){ height=b.height;width=b.width;length=b.length;}
说明: 1、复制构造函数也是构造函数,但它只有一个参数,这个参数是本类的对象(不能是其他类的对象),而且采用对象的引用的形式(一般约定加const声明,使参数值不能改变,以免在调用此函数时因不慎而使对象值被修改)。此复制构造函数的作用就是将实参对象的各数据成员值一一赋给新的对象中对应的数据成员。2、复制对象的语句 Box box2(boxl);这实际上也是建立对象的语句,建立一个新对象box2。由于在括号内给定的实参是对象,因此编译系统就调用复制构造函数(它的形参也是对象),而不会去调用其他构造函数。实参boxl的地址传递给形参b(b是boxl的引用),因此执行复制构造函数的函数体时,将boxl对象中各数据成员的值赋给borg中各数据成员。3、用户可以在声明类时定义复制构造函数,如果用户自己未定义复制构造函数,则编译系统会自动提供一个默认的复制构造函数,其作用只是简单地复制类中每个数据成员。4、还提供另一种方便用户的复制形式,用赋值号代替括号.其一般形式为:类名对象名1=对象名2;如:Box box2=box1;//用boxl初始化box2可以在一个语句中进行多个对象的复制,但是其作用都是调用复制构造函数。
5、对象的复制和1节介绍的对象的赋值在概念上和语法上的不同。
请注意普通构造函数和复制构造函数的区别。(1)在形式上类名(形参表列); //普通构造函数的声明,如Box(int,int w,int leu);类名(类名&对象名);//复制构造函数的声明,如Box(Box &b);(2)在建立对象时,实参类型不同。系统会根据实参的类型决定调用普通构造函数或复制构造函数。如Boxboxl(12,15,l6);//实参为整数(普通数据类型),调用普通构造函数Boxbox2(boxl); //实参是对象名(类数据类型),调用复制构造函数(3)在什么情况下被调用普通构造函数在程序中建立对象时被调用。
复制构造函数在用已有对象复制一个新对象时被调用,在以下3种情况下需要克隆对象:①程序中需要新建立一个对象,并用另一个同类的对象对它初始化。②函数的参数为类的对象时,在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。③函数的返回值是类的对象。在函数调用完毕需要将返回值(对象)带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。
二、静态成员
学习C语言时已了解全局变量,它能够实现数据共享。如果在一个程序文件中有多个函数,在每一个函数中都可以改变全局变量的值,全局变量的值为各函数共享。但是用全局变量时安全性得不到保证,由于在各处都可以自由地修改全局变量的值,很有可能偶一失误,全局变量的值就被修改,导致程序的失败。因此在实际工作中很少使用全局变量。如果想在同类的多个对象之间实现数据共享,也不要用全局对象,可以用静态的数据成员。
1、静态数据成员
静态数据成员是一种特殊的数据成员。它以关键字static开头。
静态数据成员定义一般形式: static 数据类形静态数据成员
静态数据成员赋值一般形式:数据类型类名::静态数据成员名=初值;//只能在类体外进行初始化。
静态数据成员引用一般形式:类名::静态数据成员名 // 通过类名引用静态数据成员对象.静态数据成员名 //通过对象名引用静态数据成员
例:class Box{ public:int volume();private:static int height;//把height定义为静态的数据成员int width;int length; };
例10 引用静态数据成员。#include <iostream>using namespace std;class Box{ public:Box(int,int);int volume();static int height; //把height定义为公用的静态的数据成员int width;int length; };Box::Box(int w,int len) //通过构造函数对width和length赋初值{ width=w;length=len; }int Box::volume() { return(height*width*length); }int Box::height=10;//对静态数据成员height初始化
int main(){ Box a(15,20),b(20,30);cout<<a.height<<endl; //通过对象名a引用静态数据成员cout<<b.height<<endl; //通过对象名b引用静态数据成员cout<<Box::height<<endl; //通过类名引用静态数据成员cout<<a.volume()<<endl; //调用volume函数,计算体积,输出结果return 0; }
说明:(1)如果只声明了类而未定义对象,则类的一般数据成员是不占内存空间的,只有在定义对象时,才为对象的数据成员分配空间。静态数据成员不属于某一个对象,在为对象所分配的空间中不包括静态数据成员所占的空间。静态数据成员是在所有对象之外单独开辟空间。只要在类中定义了静态数据成员,即使不定义对象,也为静态数据成员分配空间,它可以被引用。在一个类中可以有一个或多个静态数据成员,所有的对象共享这些静态数据成员,都可以引用它。(2)静态数据成员不随对象的建立而分配空间,也不随对象的撤销而释放(一般数据成员是在对象建立时分配空间,在对象撤销时释放)。静态数据成员是在程序编译时被分配空间的,到程序结束时才释放空间。(3)静态数据成员可以初始化,但只能在类体外进行初始化。注意:不能用参数初始化表对静态数据成员初始化。如在定义Box类中这样定义构造函数是错误的:Box(int h,int w,int len):height(h){} //错误,height是静态数据成员如果未对静态数据成员赋初值,则编译系统会自动赋予初值0。(4)静态数据成员既可以通过对象名引用,也可以通过类名来引用。注意:在上面的程序中将height定义为公用的静态数据成员,所以在类外可以直接引用,可以看到在类外可以通过对象名引用公用的静态数据成员,也可以通过类名引用静态数据成员如上例中:a.height//通过对象名a引用静态数据成员。Box:: height//通过
如果静态数据成员被定义为私有的,则不能在类外直接引用,而必须通过公用的成员函数引用。(5)有了静态数据成员,各对象之间的数据有了沟通的渠道,实现数据共享,因此可以不使用全局变量。全局变量破坏了封装的原则,不符合面向对象程序的要求。(6)公用静态数据成员与全局变量的不同,静态数据成员的作用域只限于定义该类的作用域内(如果在一个函数中定义类,那么其中静态数据成员的作用域就是此函数内)。在此作用域内,可以通过类名和域运算符"::"引用静态数据成员,而不论类对象是否存在。
2、静态成员函数
静态成员函数定义一般形式: static 数据类型静态成员函数
静态数据成员函数引用一般形式:类名::静态函数成员名// 通过类名引用静态数据成员对象.静态函数成员名 //通过对象名引用静态数据成员
作用:与静态数据成员不同,静态成员函数的作用不是为了对象之间的沟通,而是为了能处理静态数据成员。
静态成员函数与非静态成员函数的根本区别是:非静态成员函数有this指针,而静态成员函数没有this指针。由此决定了静态成员函数不能访问本类中的非静态成员数据。
说明:静态成员函数可以直接引用本类中的静态数据成员,因为静态数据成员同样是属于类的,可以直接引用。在程序中,静态成员函数主要用来访问静态数据成员,而不访问非静态成员。并不是绝对不能引用本类中的非静态成员,只是不能进行默认访问,因为无法知道应该去找哪个对象。如果一定要引用本类的非静态成员,应该加对象名和成员运算符"."。
例11 静态成员函数的应用。#include <iostream>using namespace std;class Student //定义Student类{ public:Student(int,int,int); //定义构造函数void total();static float average(); //声明静态成员函数private:int num;int age;float score;static float sum; //静态数据成员static int count;}; //静态数据成员Student::Student(int m,int a,int s){ num=m;age=a;score=s; }void Student::total() //定义非静态成员函数{ sum+=score; //累加总分count++; } //计已统计的人数 Static float Student::average() //定义静态成员函数{ return(sum/count); } //使用静态数据成员
float Student::sum=0; //对公用静态数据成员初始化int Student::count=0; //对公用静态数据成员初始化
int main(){ Student stud[3]={ //定义对象数组并初始化Student(1001,18,70),Student(1002,19,79),Student(1005,20,98) };int n;cout<<"please input the number of students:";cin>>n; //输入需要求前面多少名学生的平均成绩for(int i=0;i<n;i++) //调用n次total函数stud[i].total();cout<<"The average score of "<<n<<" students is "<<stud[0].average()<<endl;//使用对象名调用静态成员函数,也可通过类名调用:Student::average()return 0; }
运行结果为 please inputthe numberOfstudents:3/ the average score of3 students is 82.3333
说明:(1)在主函数中定义了stud对象数组,为了使程序简练,只定义它含3个元素,分别存放3个学生的数据(每个学生的数据包括学号、年龄和成绩)。程序的作用是先求用户指定的n名学生的总分,然后求平均成绩(n由用户输入)。(2)在Student类中定义了两个静态数据成员sum(总分)和count(累计需要统汁的学生人数),这是由于这两个数据成员的值是需要进行累加的,它们并不是只属于某一个对象元素,而是由各对象元素共享的,可以看出:它们的值是在不断变化的,而且无论对哪个对象元素而言,都是相同的,而且始终不释放内存空间。(3)total是公有的成员函数,其作用是将一个学生的成绩累加到snm中。公有的成员函数可以引用本对象中的一般数据成员(非静态数据成员),也可以引用类中的静态数据成员。score是非静态数据成员,sum和count是静态数据成员。(4)average是静态成员函数,它可以直接引用私有的静态数据成员(不必加类名或对象名),函数返回成绩的平均值(5)在main函数中,引用total函数要加对象名(今用对象数组元素名),引用静态成员函数average函数要用类名或对象名。(6)请思考:如果不将average函数定义为静态成员函数行不行?程序能否通过编译?需要作什么修改?为什么要用静态成员函数?请分析其理由。(7)如果想在average函数中引用stud[l]的非静态数据成员score,应该怎样处理?有人在上面的程序基础上将静态成员函数average改写为float Student::average() //定义静态成员函数{ cout<<stud[1].score<<endl; //引用非静态数据成员return(sum/count);}结果发现在编译时出错。可以将average函数的定义改为floatStudent::average(Studentstu) //函数参数为对象{ cout<<stu.score<<endl;//通过对象名引用非静态数据成员return(sum/count);以上是在例11基础上顺便说明静态成员函数引用非静态数据成员的方法,以帮助理解。但是在程序中最好养成这样的习惯:只用静态成员函数引用静态数据成员,而不引用非静态数据成员。这样思路清晰,逻辑清楚,不易出错。
三、友元
友元定义:友元可以访问与其有好友关系的类中的私有成员。友元包括友元函数和友元类。定义格式:friend 友元函数和友元类友元说明:在一个类中可以有公用的(public)成员和私有的(pnvate)成员,我们曾用客厅比喻公用部分,用卧室比喻私有部分。在类外可以访问公用成员,只有本类中的函数可以访问本类的私有成员。现在,我们来补充介绍——个例外——友元(friend)。
1、友元函数
如果在本类以外的其他地方定义了一个函数(这个函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数),在对本类进行声明时在类体中用friend对该函数进行声明,此函数就称为本类的友元函数。一个类的友元函数可以访问这个类中的私有成员。
①将普通函数声明为友元函数通过下面的例子可以了解友元函数的性质和作用。
例12 友元函数的简单例子。#include <iostream>using namespace std;class Time{ public:Time(int,int,int);friend void display(Time &); //声明display函数为Time类的友元函数private: //以下数据是私有数据成员int hour;int minute;int sec; };Time::Time(int h,int m,int s) //定义构造函数,给hour,minute,sec赋初值{ hour=h;minute=m;sec=s; }void display(Time &t) //这是友元函数,形参t是Time类对象的引用 { cout<<t.hour<<":"<<t.minute<<":"<<t.sec<<endl; }int main(){ Time t1(10,13,56); display(t1); //调用display函数,实参t1是Time类对象return 0; }
程序输出结果如下:10:23:56
注意: 1、 display是一个在类外定义的且未用类Time作限定的函数,它是非成员函数,不属于任何类。它的作用是输出时间(时、分、秒)。如果在Time类的定义体中未声明display函数为friend函数,它是不能引用Time中的私有成员hour,minute,sec。2、由于声明了display是Time类的friend函数,所以display函数可以引用Time中的私有成员hour,minute,sec。但注意在引用这些私有数据成虽时,必须加上对象名。因为display函数不是Time类的成员函数,不能默认引用Time类的数据成员,必须指定要访问的对象。
②友元成员函数
friend函数不仅可以是一般函数(非成员函数),而且可以是另一个类中的成员函数。例13 友元成员函数的简单应用。在本例中除了介绍有关友元成员函数的简单应用外,还将用到类的提前引用声明,请读者注意。请阅读下面的程序:
#include <iostream>using namespace std;class Date; //对Date类的提前引用声明class Time //定义Time类{ public:Time(int,int,int);void display(const Date&);//display是成员函数,形参是Date类对象的引用private:int hour;int minute;int sec; };class Date //声明Date类{ public: Date(int,int,int); friend void Time::display(const Date &);//声明Time类中的display函数为本类的友元成员函数private: int month;int day;int year; };Time::Time(int h,int m,int s)//定义类Time的构造函数{ hour=h;minute=m;sec=s; }void Time::display(const Date &da) //display函数的作用是输出年、月、日和时、分、秒{ cout<<d.month<<"/"<<d.day<<"/"<<d.year<<endl;//引用Date类对象中私有数据cout<<hour<<":"<<minute<<":"<<sec<<endl; } //引用本类对象中的私有数据Date::Date(int m,int d,int y) //类Date的构造函数{ month=m;day=d;year=y; }int main(){ Time t1(10,13,56); //定义Time类对象t1Date d1(12,25,2004); //定义Date类对象dlt1.display(d1); //调用tl中的display函数,实参是Date类对象dl return 0; }
运行时输出: 12/25/2004 (输出Date类对象dl中的私有数据) l0:13:56 (输出Time类对象11中的私有数据)
注意在本程序的主函数中调用友元函数防问有关类的私有数据方法:(1)在函数名display的前面要加display所在的对象名(t1);(2)display成员函数的实参是Date类对象d1,否则就不能访问对象dl中的私有数据;(3)在Time::display函数中引用Date类私有数据时必须加上对象名,如d.month。
③一个函数(包括普通函数和成员函数)可以被多个类声明为"朋友",这样就可以引用多个类中的私有数据
2、友元类
声明友元类的一般形式为:friend 类名;
例:在A类的定义体中用以下语句声明B类为其友元类:friend B:
说明:1、友元的关系是单向的而不是双向的。如果声明了B类是A类的友元类,不等于A类是B类的友元类,A类中的成员函数不能访问B类中的私有数据。2、友元的关系不能传递,友元利弊的分析:面向对象程序设计的一个基本原则是封装性和信息隐蔽,而友元却可以访问其他类中的私有成员,不能不说这是对封装原则的一个小的破坏。但是它能有助于数据共享,能提高程序的效率,在使用友元时,要注意到它的副作用,不要过多地使用友元,只有在使用它能使程序精练,并能大大提高程序的效率时才用友元。也就是说,要在数据共享与信息隐蔽之间选择一个恰当的平衡点。
四、类模板
允许使用函数模板,对于功能相同而数据类型不同的一些函数,不必一一定义各个函数,可以定义一个可对任何类型变量进行操作的函数模板,在调用函数时,系统会根据实参的类型,取代函数模板中的类型参数,得到具体的函数。这样可以简化程序设计。
类模板一般定义形式: template <class虚拟类型参数>//声明一个模板,虚拟类型名为numtype class 类模板名 { 类体定义 }请将此类模板和类定义作一比较,可以看到有两处不同:(1)声明类模板时要增加一行: template<class虚拟类型参数>(2)原有的类型名换成虚拟类型参数名numtype。在建立类对象时,如果将实际类型指定为int型,编译系统就会用int取代所有的numtype,如果指定为float型,就用float取代所有的numtype。这样就能实现"一类多用"。(3)由于类模板包含类型参数,因此又称为参数化的类。如果说类是对象的抽象,对象是类的实例,则类模板是类的抽象,类是类模板的实例。利用类模板可以建立含各种数据类型的类。(4)类模板的引用:类模板名<实际类型名> 对象名(实参表列);
例14 声明一个类模板,利用它分别实现两个整数、浮点数和字符的比较,求出大数和小数。#include <iostream>using namespace std;template<class numtype> //定义类模板class Compare{ public:Compare(numtype a,numtype b){ x=a;y=b;}numtype max(){ return (x>y)?x:y;} //引用C语言中条件运算符numtype min(){return (x<y)?x:y;}private:numtype x,y; };
int main(){ Compare<int> cmp1(3,7); //定义对象cmpl,用于两个整数的比较cout<<cmp1.max()<<" is the Maximum of two inteder numbers."<<endl;cout<<cmp1.min()<<" is the Minimum of two inteder numbers."<<endl<<endl;Compare<float> cmp2(45.78,93.6); //定义对象cmp2,用于两个浮点数的比较cout<<cmp2.max()<<" is the Maximum of two float numbers."<<endl;cout<<cmp2.min()<<" is the Minimum of two float numbers."<<endl<<endl;Compare<char> cmp3('a','A');//定义对象cmp3,用于两个字符的比较cout<<cmp3.max()<<" is the Maximum of two characters."<<endl;cout<<cmp3.min()<<" is the Minimum of two characters."<<endl;return 0;}
运行结果如下: 7 is the Maximum Of two integers 3 is the Minimum Of two integers. 93.6 is the Maximum Of two float numbers. 45.78 is the Minimum OftWO float numbers. a is the Maximum Of two characters. A is the Minimum Of two characters.
可以这样声明和使用类模板:(1)先写出一个实际的类(如本节开头的Compare int)。由于其语义明确,含义清楚,一般不会出错。(2)将此类中准备改变的类型名(如int要改变为float或char)改用一个自己指定的虚拟类型名(如上例中的numtype)。(3)在类声明前面加入一行,格式为template<class虚拟类型参数>如 template<classnumtype> //注意本行末尾无分号class Compare{ …};//类体
(4)用类模板定义对象时用以下形式:类模板名<实际类型名>对象名;类模板名<实际类型名> 对象名(实参表列);(5)如果在类模板外定义成员函数,应写成类模板形式: template<class虚拟类型参数>函数类型类模板名<虚拟类型参数>::成员函数名(函数形参表列){ …}
说明:(1)类模板的类型参数可以有一个或多个,每个类型前面都必须加class,如template<class Tl,class T2>class someclass{ …};在定义对象时分别代人实际的类型名,如someelass<int,double> obj;(2)和使用类一样,使用类模板时要注意其作用域,只能在其有效作用域内用它定义对象。如果类模板是在A文件开头定义的,则A文件范围内为有效作用域,可以在其中的任何地方使用类模板,但不能在B文件中用类模板定义对象。(3)模板可以有层次,一个类模板可以作为基类,派生出派生模板类。