分类: C/C++
2011-08-03 21:43:09
C++虽然采取了不少有效的措施(如设private保护)以增加数据的安全性,但是有些数据却往往是共享的,人们可以在不同的场合通过不同的途径访问同一个数据对象。
有时在无意之中的误操作会改变有关数据的状况,而这是人们所不希望出现的。
既要使数据能在一定范围内共享,又要保证它不被任意修改,这时可以使用const,即把有关的数据定义为常量。
9.6.1 常对象
在定义对象时指定对象为常对象。
常对象必须要有初值,如
Time const t1(12,34,46); //t1是常对象
这样,在所有的场合中,对象t1中的所有成员的值都不能被修改。
凡希望保证数据成员不被改变的对象,可以声明为常对象。
定义常对象的一般形式为
类名 const 对象名[(实参表列)];
也可以把const写在最左面:
const 类名 对象名[(实参表列)];
二者等价。
如果一个对象被声明为常对象,则不能调用该对象的非const型的成员函数(除了由系统自动调用的隐式的构造函数和析构函数)。
例如,对于例9.7中已定义的Time类,如果有
const Time t1(10,15,36); //定义常对象t1
t1.get_time( ); //企图调用常对象t1中的非const型成员函数,非法
这是为了防止这些函数会修改常对象中数据成员的值。
不能仅依靠编程者的细心来保证程序不出错,编译系统充分考虑到可能出现的情况,对不安全的因素予以拦截。
现在,编译系统只检查函数的声明,只要发现调用了常对象的成员函数,而且该函数未被声明为const,就报错,提请编程者注意。
引用常对象中的数据成员很简单,只需将该成员函数声明为const即可。
如
void get_time( ) const ; //将函数声明为const
这表示get_time是一个const型函数,即常成员函数。
常成员函数可以访问常对象中的数据成员,但仍然不允许修改常对象中数据成员的值。
有时在编程时有要求,一定要修改常对象中的某个数据成员的值,ANSI C++考虑到实际编程时的需要,对此作了特殊的处理,对该数据成员声明为mutable,如
mutable int count;
把count声明为可变的数据成员,这样就可以用声明为const的成员函数来修改它的值。
9.6.2 常对象成员可以将对象的成员声明为const,包括常数据成员和常成员函数。
1. 常数据成员其作用和用法与一般常变量相似,用关键字const来声明常数据成员。
常数据成员的值是不能改变的。
有一点要注意: 只能通过构造函数的参数初始化表对常数据成员进行初始化。
如在类体中定义了常数据成员hour:
const int hour; //声明hour为常数据成员
不能采用在构造函数中对常数据成员赋初值的方法。
在类外定义构造函数,应写成以下形式:
Time::Time(int h):hour(h){} //通过参数初始化表对常数据成员hour初始化
常对象的数据成员都是常数据成员,因此常对象的构造函数只能用参数初始化表对常数据成员进行初始化。
2. 常成员函数 前面已提到: 一般的成员函数可以引用本类中的非const数据成员,也可以修改它们。
如果将成员函数声明为常成员函数,则只能引用本类中的数据成员,而不能修改它们,例如只用于输出数据等。
如
void get_time( ) const ; //注意const的位置在函数名和括号之后
const是函数类型的一部分,在声明函数和定义函数时都要有const关键字,在调用时不必加const。
常成员函数可以引用const数据成员,也可以引用非const的数据成员。
const数据成员可以被const成员函数引用,也可以被非const的成员函数引用。
具体情况可以用书中表9.1表示。
怎样利用常成员函数呢?
(1) 如果在一个类中,有些数据成员的值允许改变,另一些数据成员的值不允许改变,则可以将一部分数据成员声明为const,以保证其值不被改变,可以用非const的成员函数引用这些数据成员的值,并修改非const数据成员的值。
(2) 如果要求所有的数据成员的值都不允许改变,则可以将所有的数据成员声明为const,或将对象声明为const(常对象),然后用const成员函数引用数据成员,这样起到“双保险”的作用,切实保证
(3) 如果已定义了一个常对象,只能调用其中的const成员函数,而不能调用非const成员函数(不论这些函数是否会修改对象中的数据)。
这是为了保证数据的安全。
如果需要访问对象中的数据成员,可将常对象中所有成员函数都声明为const成员函数,但应确保在函数中不修改对象中的数据成员。
不要误认为常对象中的成员函数都是常成员函数。
常对象只保证其数据成员是常数据成员,其值不被修改。
如果在常对象中的成员函数未加const声明,编译系统把它作为非const成员函数处理。
还有一点要指出: 常成员函数不能调用另一个非const成员函数。
9.6.3 指向对象的常指针
将指针变量声明为const型,这样指针值始终保持为其初值,不能改变。
如Time t1(10,12,15),t2; //定义对象
Time * const ptr1; //const位置在指针变量名前面,规定ptr1的值是常值
ptr1=&t1; //ptr1指向对象t1,此后不能再改变指向
ptr1=&t2; //错误,ptr1不能改变指向
定义指向对象的常指针的一般形式为类名 * const 指针变量名;也可以在定义指针变量时使之初始化,如将上面第2,3行合并为
Time * const ptr1=&t1; //指定ptr1指向t1
请注意: 指向对象的常指针变量的值不能改变,即始终指向同一个对象,但可以改变其所指向对象(如t1)的值。
如果想将一个指针变量固定地与一个对象相联系(即该指针变量始终指向一个对象), 可以将它指定为const型指针变量。
往往用常指针作为函数的形参,目的是不允许在函数执行过程中改变指针变量的值,使其始终指向原来的对象。
9.6.4 指向常对象的指针变量
为了更容易理解指向常对象的指针变量的概念和使用,首先了解指向常变量的指针变量,然后再进一步研究指向常对象的指针变量。
下面定义了一个指向常变量的指针变量
ptr: const char *ptr;
注意const的位置在最左侧,它与类型名char紧连,表示指针变量ptr指向的char变量是常变量,不能通过ptr来改变其值的。
定义指向常变量的指针变量的一般形式为const 类型名 *指针变量名;说明:
(1) 如果一个变量已被声明为常变量,只能用指向常变量的指针变量指向它,而不能用一般的(指向非const型变量的)指针变量去指向它。
(2) 指向常变量的指针变量除了可以指向常变量外,还可以指向未被声明为const的变量。
此时不能通过此指针变量改变该变量的值。
如果希望在任何情况下都不能改变c1的值,则应把它定义为const型。
(3) 如果函数的形参是指向非const型变量的指针,实参只能用指向非const变量的指针,而不能用指向const变量的指针,这样,在执行函数的过程中可以改变形参指针变量所指向的变量(也就是实参指针所指向的变量)的值。
如果函数的形参是指向const型变量的指针,在执行函数过程中显然不能改变指针变量所指向的变量的值,因此允许实参是指向const变量的指针,或指向非const变量的指针。
使用形参和实参的对应关系见书中表9.2。
以上的对应关系与在(2)中所介绍的指针变量和其所指向的变量的关系是一致的: 指向常变量的指针变量可以指向const和非const型的变量,而指向非const型变量的指针变量只能指向非const的变量。
以上介绍的是指向常变量的指针变量,指向常对象的指针变量的概念和使用是与此类似的,只要将“变量”换成“对象”即可。
(1) 如果一个对象已被声明为常对象,只能用指向常对象的指针变量指向它,而不能用一般的(指向非const型对象的)指针变量去指向它。
(2) 如果定义了一个指向常对象的指针变量,并使它指向一个非const的对象,则其指向的对象是不能通过指针来改变的。
如果希望在任何情况下t1的值都不能改变,则应把它定义为const型。
(3) 指向常对象的指针最常用于函数的形参,目的是在保护形参指针所指向的对象,使它在函数执行过程中不被修改。
请记住这样一条规则: 当希望在调用函数时对象的值不被修改,就应当把形参定义为指向常对象的指针变量,同时用对象的地址作实参(对象可以是const或非const型)。
如果要求该对象不仅在调用函数过程中不被改变,而且要求它在程序执行过程中都不改变,则应把它定义为const型。
(4) 如果定义了一个指向常对象的指针变量,是不能通过它改变所指向的对象的值的,但是指针变量本身的值是可以改变的。
9.6.5 对象的常引用
过去曾介绍: 一个变量的引用就是变量的别名。
实质上,变量名和引用名都指向同一段内存单元。
如果形参为变量的引用名,实参为变量名,则在调用函数进行虚实结合时,并不是为形参另外开辟一个存储空间(常称为建立实参的一个拷贝), 而是把实参变量的地址传给形参(引用名),这样引用名也指向实参变量。
例9.8 对象的常引用。
#include
using namespace std;
class Time
{public:
Time(int,int,int);
int hour;
int minute;
int sec;
};
Time::Time(int h,int m,int s) //定义构造函数
{hour=h;
minute=m;
sec=s;
}
void fun(Time &t)
{t.hour=18;}
int main( )
{Time t1(10,13,56);
fun(t1);
cout<<t1.hour<<endl;
return 0;
}
如果不希望在函数中修改实参t1的值,可以把引用变量t声明为const(常引用),函数原型为void fun(const Time &t); 则在函数中不能改变t的值,也就是不能改变其对应的实参t1的值。
在C++面向对象程序设计中,经常用常指针和常引用作函数参数。
这样既能保证数据安全,使数据不能被随意修改,在调用函数时又不必建立实参的拷贝。
用常指针和常引用作函数参数,可以提高程序运行效率。
9.6.6 const型数据的小结表9.3
9.7 对象的动态建立和释放
用前面介绍的方法定义的对象是静态的,在程序运行过程中,对象所占的空间是不能随时释放的。
但有时人们希望在需要用到对象时才建立对象,在不需要用该对象时就撤销它,释放它所占的内存空间以供别的数据使用。
这样可提高内存空间的利用率。
在第7章7.1.7节中介绍了用new运算符动态地分配内存,用delete运算符释放这些内存空间。
这也适用于对象,可以用new运算符动态建立对象,用delete运算符撤销对象。
如果已经定义了一个Box类,可以用下面的方法动态地建立一个对象:
new Box; 编译系统开辟了一段内存空间,并在此内存空间中存放一个Box类对象,同时调用该类的构造函数,以使该对象初始化(如果已对构造函数赋予此功能的话)。
但是此时用户还无法访问这个对象,因为这个对象既没有对象名,用户也不知道它的地址。
这种对象称为无名对象,它确实是存在的,但它没有名字。
用new运算符动态地分配内存后,将返回一个指向新对象的指针的值,即所分配的内存空间的起始地址。
用户可以获得这个地址,并通过这个地址来访问这个对象。
需要定义一个指向本类的对象的指针变量来存放该地址。
如Box *pt; //定义一个指向Box类对象的指针变量pt
pt=new Box; //在pt中存放了新建对象的起始地址
在程序中就可以通过pt访问这个新建的对象。
如
cout<<pt->height; //输出该对象的height成员
cout<<pt->volume( ); //调用该对象的volume函数,计算并输出体积
C++还允许在执行new时,对新建立的对象进行初始化。
如
Box *pt=new Box(12,15,18);
这种写法是把上面两个语句(定义指针变量和用new建立新对象)合并为一个语句,并指定初值。
这样更精炼。
新对象中的height,width和length分别获得初值12,15,18。
调用对象既可以通过对象名,也可以通过指针。
用new建立的动态对象一般是不用对象名的,是通过指针访问的,它主要应用于动态的数据结构,如链表。
访问链表中的结点,并不需要通过对象名,而是在上一个结点中存放下一个结点的地址,从而由上一个结点找到下一个结点,构成链接的关系。
在执行new运算时,如果内存量不足,无法开辟所需的内存空间,目前大多数C++编译系统都使new返回一个0指针值。
只要检测返回值是否为0,就可判断分配内存是否成功。
ANSI C++标准提出,在执行new出现故障时,就“抛出”一个“异常”,用户可根据异常进行有关处理。
但C++标准仍然允许在出现new故障时返回0指针值。
当前,不同的编译系统对new故障的处理方法是不同的。
在不再需要使用由new建立的对象时,可以用delete运算符予以释放。
如
delete pt; //释放pt指向的内存空间
这就撤销了pt指向的对象。
此后程序不能再使用该对象。
如果用一个指针变量pt先后指向不同的动态对象,应注意指针变量的当前指向,以免删错了对象。
在执行delete运算符时,在释放内存空间之前,自动调用析构函数,完成有关善后清理工作。
9.8 对象的赋值和复制
9.8.1 对象的赋值
如果对一个类定义了两个或多个对象,则这些同类的对象之间可以互相赋值,
或者说,一个对象的值可以赋给另一个同类的对象。
这里所指的对象的值是指对象中所有数据成员的值。
对象之间的赋值也是通过赋值运算符“=”进行的。
本来,赋值运算符“=”只能用来对单个的变量赋值,现在被扩展为两个同类对象之间的赋值,
这是通过对赋值运算符的重载实现的。
实际这个过程是通过成员复制来完成的,即将一个对象的成员值一一复制给另一对象的对应成员。
对象赋值的一般形式为
对象名1 = 对象名2;
注意对象名1和对象名2必须属于同一个类。
例如
Student stud1,stud2; //定义两个同类的对象
┆
stud2=stud1; //将stud1赋给stud2
通过下面的例子可以了解怎样进行对象的赋值。
例9.9 对象的赋值。
#include
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;
box2=box1; //将box1的值赋给box2
cout<<″The volume of box2 is ″<<box2.volume( )<<endl; return 0;
}
运行结果如下:
The volume of box1 is 11250
The volume of box2 is 11250
说明: (1) 对象的赋值只对其中的数据成员赋值,而不对成员函数赋值。
(2) 类的数据成员中不能包括动态分配的数据,否则在赋值时可能出现严重后果。
9.8.2 对象的复制
有时需要用到多个完全相同的对象。
此外,有时需要将对象在某一瞬时的状态保留下来。
这就是对象的复制机制。
用一个已有的对象快速地复制出多个完全相同的对象。
如Box box2(box1); 其作用是用已有的对象box1去克隆出一个新对象box2。
其一般形式为
类名 对象2(对象1);
用对象1复制出对象2。
可以看到: 它与前面介绍过的定义对象方式类似,但是括号中给出的参数不是一般的变量,而是对象。
在建立对象时调用一个特殊的构造函数——复制构造函数(copy constructor)。
这个函数的形式是这样的:
//The copy constructor definition.
Box::Box(const Box& b)
{height=b.height; width=b.width; length=b.length; }
复制构造函数也是构造函数,但它只有一个参数,这个参数是本类的对象(不能是其他类的对象), 而且采用对象的引用的形式(一般约定加const声明,使参数值不能改变,以免在调用此函数时因不慎而使对象值被修改)。
此复制构造函数的作用就是将实参对象的各成员值一一赋给新的对象中对应的成员。
回顾复制对象的语句Box box2(box1); 这实际上也是建立对象的语句,建立一个新对象box2。
由于在括号内给定的实参是对象,因此编译系统就调用复制构造函数(它的形参也是对象), 而不会去调用其他构造函数。
实参box1的地址传递给形参b(b是box1的引用),因此执行复制构造函数的函数体时,将box1对象中各数据成员的值赋给box2中各数据成员。
如果用户自己未定义复制构造函数,则编译系统会自动提供一个默认的复制构造函数,其作用只是简单地复制类中每个数据成员。
C++还提供另一种方便用户的复制形式,用赋值号代替括号,如
Box box2=box1; //用box1初始化box2
其一般形式为
类名 对象名1 = 对象名2;
可以在一个语句中进行多个对象的复制。
如
Box box2=box1,box3=box2;
按box1来复制box2和box3。
可以看出: 这种形式与变量初始化语句类似,请与下面定义变量的语句作比较:
int a=4,b=a;
这种形式看起来很直观,用起来很方便。
但是其作用都是调用复制构造函数。
请注意对象的复制和9.8.1节介绍的对象的赋值在概念上和语法上的不同。
对象的赋值是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。
而对象的复制则是从无到有地建立一个新对象,并使它与一个已有的对象完全相同(包括对象的结构和成员的值)。
可以对例9.7程序中的主函数作一些修改:
int main( )
{Box box1(15,30,25); //定义box1
cout<<″The volume of box1 is ″<<box1.volume( )<<endl; Box box2=box1,box3=box2; //按box1来复制box2,box3
cout<<″The volume of box2 is ″<<box2.volume( )<<endl;
cout<<″The volume of box3 is ″<<box3.volume( )<<endl;
}
执行完第3行后,3个对象的状态完全相同。
请注意普通构造函数和复制构造函数的区别。
(1) 在形式上类名(形参表列); //普通构造函数的声明,如Box(int h,int w,int len);
类名(类名& 对象名); //复制构造函数的声明,如Box(Box &b);
(2) 在建立对象时,实参类型不同。
系统会根据实参的类型决定调用普通构造函数或复制构造函数。
如
Box box1(12,15,16); //实参为整数,调用普通构造函数
Box box2(box1); //实参是对象名,调用复制构造函数
(3) 在什么情况下被调用
普通构造函数在程序中建立对象时被调用。
复制构造函数在用已有对象复制一个新对象时被调用,在以下3种情况下需要克隆对象:
① 程序中需要新建立一个对象,并用另一个同类的对象对它初始化,如前面介绍的那样。
② 当函数的参数为类的对象时。
在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,这就是按实参复制一个形参,系统是通过调用复制构造函数来实现的,这样能保证形参具有和实参完全相同的值。
如
void fun(Box b) //形参是类的对象
{ }
int main( )
{Box box1(12,15,18);
fun(box1); //实参是类的对象,调用函数时将复制一个新对象b
return 0;
}
③ 函数的返回值是类的对象。
在函数调用完毕将返回值带回函数调用处时。
此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。
如
Box f( ) //函数f的类型为Box类类型
{Box box1(12,15,18);
return box1; //返回值是Box类的对象
}
int main( )
{Box box2; //定义Box类的对象box2
box2=f( ); //调用f函数,返回Box类的临时对象,并将它赋值给box2
}
以上几种调用复制构造函数都是由编译系统自动实现的,不必由用户自己去调用,读者只要知道在这些情况下需要调用复制构造函数就可以了。
9.9 静态成员如果有n个同类的对象,那么每一个对象都分别有自己的数据成员,不同对象的数据成员各自有值,互不相干。
但是有时人们希望有某一个或几个数据成员为所有对象所共有。
这样可以实现数据共享。
在第7章中曾介绍过全局变量,它能够实现数据共享。
如果在一个程序文件中有多个函数,在每一个函数中都可以改变全局变量的值,全局变量的值为各函数共享。
但是用全局变量的安全性得不到保证,由于在各处都可以自由地修改全局变量的值,很有可能偶一失误,全局变量的值就被修改,导致程序的失败。
因此在实际工作中很少使用全局变量。
如果想在同类的多个对象之间实现数据共享,也不要用全局对象,可以用静态的数据成员。