C++【02】类和对象的进阶

类和对象的进阶

  • 一、构造函数
    • 1.对象初始化
    • 2.构造函数的作用
    • 3.带参数的构造函数
    • 4.用参数初始化表对数据成员的初始化
    • 5.构造函数的重载
    • 6.使用默认参数的构造函数
  • 二、析构函数
    • 1.调用构造函数和析构函数的顺序
  • 三、对象数组
  • 四、对象指针
    • 指向对象成员的指针
    • 指向对象成员函数的指针
    • this 指针
    • 共用数据的保护
  • 常对象
    • 常对象成员
      • 1.常数据成员
      • 2.常成员函数
      • 3.指向常对象的指针
      • 4.指向常对象的指针变量
      • 5.对象的常引用
      • 6.小结
      • 7.对象的动态建立和释放
  • 五、对象的赋值和复制
    • 1.对象的赋值
    • 2.对象的复制
  • 六、静态数据成员
    • 静态数据成员函数
  • 七、友元
    • 友元函数
      • 1.将普通函数声明为友元函数
      • 2.友元成员函数
      • 3.友元类
  • 八、类的模板

一、构造函数

1.对象初始化

在建立一个对象时,常常需要作某些初始化的工作,例如对数据成员赋初值。如果一个数据成员未被赋值,则它的值是不可预知的,因为在系统为它分配内存时,保留了这些存储单元的原状,这就成为了这些数据成员的初始值。这种状况显然是与人们的要求不相符的,对象是一个实体,它反映了客观事物的属性(例如时钟的时、分、秒的值),是应该有确定的值的。
注意: 类的数据成员是不能在声明类时初始化的。

如果一个类中所有的成员都是公用的,则可以在定义对象时对数据成员进行初始化。如
class Time
{public: //声明为公用成员
hour;
minute;
sec;
};
Time t1={14,56,30}; //将t1初始化为14:56:30
这种情况和结构体变量的初始化是差不多的,在一个花括号内顺序列出各公用数据成员的值,两个值之间用逗号分隔。但是,如果数据成员是私有的,或者类中有private或protected的成员,就不能用这种方法初始化。

2.构造函数的作用

为了解决这个问题,C++提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。构造函数的名字必须与类名同名,而不能由用户任意命名,以便编译系统能识别它并把它作为构造函数处理。它不具有任何类型,不返回任何值。构造函数的功能是由用户定义的,用户根据初始化的要求设计函数体和函数参数。
例:

#include 
using namespace std;
class Time
{public:
    Time( )                    //定义构造成员函数,函数名与类名相同
      {hour=0;
       minute=0;
       sec=0;
      }
   void set_time( );           //函数声明
   void show_time( );          //函数声明
private:
   int hour;                  //私有数据成员
   int minute;
   int sec;
};
void Time∷set_time( )       //在类外定义set_time函数
{
 cin>>hour;
 cin>>minute;
 cin>>sec;
}

void Time∷show_time( )       //在类外定义show_time函数
{
 cout<

有关构造函数的使用,有以下说明:
(1)在建立类对象时自动调用构造函数。
(2) 构造函数没有返回值,因此也不需要在定义构造函数时声明类型,这是它和一般函数的一个重要的不同之点。不能写成 void Time(){…}
(3) 构造函数不需用户调用,也不能被用户调用。
不能如调用一般成员函数的方法来调用构造函数,如t1.Time()是错误的。
(4) 在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其他语句,如cout语句。但是一般不提倡在构造函数中加入与初始化无关的内容,以保持程序的清晰。
(5) 如果用户自己没有定义构造函数,则C++系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作。

3.带参数的构造函数

构造函数不带参数,在函数体中对数据成员赋初值。这种方式使该类的每一个对象都得到同一组初值(例如例3.1中各数据成员的初值均为0)。但是有时用户希望对不同的对象赋予不同的初值。
可以采用带参数的构造函数,在调用不同对象的构造函数时,从外面将不同的数据传递给构造函数,以实现不同的初始化。构造函数首部的一般格式为
构造函数名(类型 1 形参1,类型2 形参2,…)
前面已说明: 用户是不能调用构造函数的,因此无法采用常规的调用函数的方法给出实参。实参是在定义对象时给出的。定义对象的一般格式为
类名 对象名(实参1,实参2,…);
列:

Box∷Box(int h, int w, int len)   //在类外定义带参数的构造函数
{height=h;
 width=w;
 length=len;
}

4.用参数初始化表对数据成员的初始化

在构造函数的函数体内通过赋值语句对数据成员实现初始化。C++还提供另一种初始化数据成员的方法——参数初始化表来实现对数据成员的初始化。这种方法不在函数体内对数据成员初始化,而是在函数首部实现。例如例3.2中定义构造函数可以改用以下形式:
Box∷Box(int h,int w,int len):height(h),width(w),length(len){ }
这种写法方便、简练,尤其当需要初始化的数据成员较多时更显其优越性。甚至可以直接在类体中(而不是在类外)定义构造函数。
在一个类中可以定义多个构造函数,以便对类对象提供不同的初始化的方法,供用户选用。这些构造函数具有相同的名字,而参数的个数或参数的类型不相同。这称为构造函数的重载。在第1章中所介绍的函数重载的知识也适用于构造函数。
通过下面的例子可以了解怎样应用构造函数的重载。

5.构造函数的重载

在一个类中可以定义多个构造函数,以便对类对象提供不同的初始化的方法,供用户选用。这些构造函数具有相同的名字,而参数的个数或参数的类型不相同。这称为构造函数的重载。在第1章中所介绍的函数重载的知识也适用于构造函数。
通过下面的例子可以了解怎样应用构造函数的重载。
定义两个构造函数,其中一个无参数,一个有参数。

#include 
using namespace std;
class Box
{public:
      Box( );                                    //声明一个无参的构造函数
      Box(int h,int w,int len):height(h),width(w),length(len){ }
            //声明一个有参的构造函数,用参数的初始化表对数据成员初始化
      int volume( );
 private:
     int height;
     int width;
     int length;
};
Box∷Box( )                                   //定义一个无参的构造函数
{height=10;
 width=10;
 length=10;
};
int Box∷volume( )
{return(height*width*length);
}

int main( )
{
 Box box1;                                    //建立对象box1,不指定实参
 cout<<″The volume of box1 is ″<

在本程序中定义了两个重载的构造函数,其实还可以定义其他重载构造函数,其原型声明可以为
Box∷Box(int h); //有1个参数的构造函数
Box∷Box(int h,int w); //有两个参数的构造函数

6.使用默认参数的构造函数

构造函数中参数的值既可以通过实参传递,也可以指定为某些默认值,这样如果用户不指定实参值,编译系统就使形参取默认值。
在第1章中介绍过在函数中可以使用有默认值的参数。在构造函数中也可以采用这样的方法来实现初始化。
例3.3的问题也可以使用包含默认参数的构造函数来处理。
例3.4 将例3.3程序中的构造函数改用含默认值的参数,长、宽、高的默认值均为10。
在例3.3程序的基础上改写如下:

#include 
using namespace std;
class Box
{public:
     Box(int h=10, int w=10, int len=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;                   //没有给实参 
cout<<″The volume of box1 is ″<

结果:

The volume of box1 is 1000
The volume of box2 is 1500
The volume of box3 is 4500
The volume of box4 is 9000

可以看到: 在构造函数中使用默认参数是方便而有效的,它提供了建立对象时的多种选择,它的作用相当于好几个重载的构造函数。它的好处是: 即使在调用构造函数时没有提供实参值,不仅不会出错,而且还确保按照默认的参数值对对象进行初始化。尤其在希望对每一个对象都有同样的初始化状况时用这种方法更为方便。

说明:
(1) 应该在声明构造函数时指定默认值,而不能只在定义构造函数时指定默认值。因为类的声明是放在头文件中的,用户可以看到。
(2) 程序第5行在声明构造函数时,形参名可以省略。即写成:
Box(int =10, int =10, int =10);
(3) 如果构造函数的全部参数都指定了默认值,则在定义对象时可以给一个或几个实参,也可以不给出实参。
由于不需要实参也可以调用构造函数,因此全部参数都指定了默认值的构造函数也属于默认构造函数
由于一个类只能有一个默认构造函数,同时定义以下两个构造函数是错误的:
Box( );
Box(int =10, int =10, int =10);
(4) 在一个类中定义了全部是默认参数的构造函数后,不能再定义重载构造函数。

二、析构函数

析构函数(destructor)也是一个特殊的成员函数,它的作用与构造函数相反,它的名字是类名的前面加一个“~”符号。在C++中“~”是位取反运算符,析构函数是与构造函数作用相反的函数。
当对象的生命期结束时,会自动执行析构函数。具体地说如果出现以下几种情况,程序就会执行析构函数:
①如果在一个函数中定义了一个对象(它是自动局部对象),当这个函数被调用结束时,对象应该释放,在对象释放前自动执行析构函数。
②static局部对象在函数调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用static局部对象的析构函数。
③如果定义了一个全局对象,则在程序的流程离开其作用域时(如main函数结束或调用exit函数) 时,调用该全局对象的析构函数。
④如果用new运算符动态地建立了一个对象,当用delete运算符释放该对象时,先调用该对象的析构函数。
析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用。程序设计者事先设计好析构函数,以完成所需的功能,只要对象的生命期结束,程序就自动执行析构函数来完成这些工作。
析构函数不返回任何值,没有函数类型,也没有函数参数。因此它不能被重载。一个类可以有多个构造函数,但只能有一个析构函数。
实际上,析构函数的作用并不仅限于释放资源方面,它还可以被用来执行“用户希望在最后一次使用对象之后所执行的任何操作”,例如输出有关的信息。。
一般情况下,类的设计者应当在声明类的同时定义析构函数,以指定如何完成“清理”的工作。如果用户没有定义析构函数,C++编译系统会自动生成一个析构函数,但它只是徒有析构函数的名称和形式,实际上什么操作都不进行。想让析构函数完成任何工作,都必须在定义的析构函数中指定。

1.调用构造函数和析构函数的顺序

在使用构造函数和析构函数时,需要特别注意对它们的调用时间和调用顺序。
在一般情况下,调用析构函数的次序正好与调用构造函数的次序相反: 最先被调用的构造函数,其对应的(同一对象中的)析构函数最后被调用,而最后被调用的构造函数,其对应的析构函数最先被调用。如图3.1示意。
调用析构函数的次序与调用构造函数的次序相反,这是对同一存储类别的对象而言。对象可以在不同的作用域中定义,可以有不同的存储类别。这些会影响调用构造函数和析构函数的时机。
下面归纳一下什么时候调用构造函数和析构函数:
(1) 在全局范围中定义的对象(即在所有函数之外定义的对象),它的构造函数在文件中的所有函数(包括main函数)执行之前调用。但如果一个程序中有多个文件,而不同的文件中都定义了全局对象,则这些对象的构造函数的执行顺序是不确定的。当main函数执行完毕或调用exit函数时(此时程序终止),调用析构函数。
(2) 如果定义的是局部自动对象(例如在函数中定义对象),则在建立对象时调用其构造函数。如果函数被多次调用,则在每次建立对象时都要调用构造函数。在函数调用结束、对象释放时先调用析构函数。
(3) 如果在函数中定义静态(static)局部对象,则只在程序第一次调用此函数建立对象时调用构造函数一次,在调用结束时对象并不释放,因此也不调用析构函数,只在main函数结束或调用exit函数结束程序时,才调用析构函数。
构造函数和析构函数在面向对象的程序设计中是相当重要的。以上介绍了最基本的、使用最多的普通构造函数,在本章第3.8节中将会介绍复制构造函数。

三、对象数组

数组不仅可以由简单变量组成(例如整型数组的每一个元素都是整型变量),也可以由对象组成(对象数组的每一个元素都是同类的对象)。
在日常生活中,有许多实体的属性是共同的,只是属性的具体内容不同。例如一个班有50个学生,每个学生的属性包括姓名、性别、年龄、成绩等。如果为每一个学生建立一个对象,需要分别取50个对象名。用程序处理很不方便。这时可以定义一个“学生类”对象数组,每一个数组元素是一个“学生类”对象。例如
Student stud[50]; //假设已声明了Student类,定义stud数组,有50个元素
在建立数组时,同样要调用构造函数。如果有50个元素,需要调用50次构造函数。在需要时可以在定义数组时提供实参以实现初始化。如果构造函数只有一个参数,在定义数组时可以直接在等号后面的花括号内提供实参。如
Student stud[3]={60,70,78}; //合法,3个实参分别传递给3个数组元素的构造函数
如果构造函数有多个参数,则不能用在定义数组时直接提供所有实参的方法。例如,类Student的构造函数有多个参数,且为默认参数:
Student∷ Student(int=1001,int=18,int=60); //定义构造函数,有多个参数,且为默认参数
如果定义对象数组的语句为
Student stud[3]={1005,60,70};
编译系统会将3个实参分别作为3个元素的第一个实参。
编译系统只为每个对象元素的构造函数传递一个实参,所以在定义数组时提供的实参个数不能超过数组元素个数,如
Student stud[3]={60,70,78,45}; //不合法,实参个数超过对象数组元素个数
那么,如果构造函数有多个参数,在定义对象数组时应当怎样实现初始化呢?方法是: 在花括号中分别写出构造函数并指定实参。如果构造函数有3个参数,分别代表学号、年龄、成绩。则可以这样定义对象数组:
Student Stud[3]={ //定义对象数组
Student(1001,18,87), //调用第1个元素的构造函数,为它提供3个实参
Student(1002,19,76), //调用第2个元素的构造函数,为它提供3个实参
Student(1003,18,72) //调用第3个元素的构造函数,为它提供3个实参
};

#include 
using namespace std;
class Box
{public:
     Box(int h=10,int w=12,int len=15): 
      height(h),width(w),length(len){ }
      //声明有默认参数的构造函数,用参数初始化表对数据成员
        初始化
     int volume( );
 private:
     int height;
     int width;
     int length;
};
int Box∷volume( )
{return(height*width*length);
}

int main( )
{ Box a[3]={                     //定义对象数组
    Box(10,12,15),               //调用构造函数Box,提供第1个元素的实参
    Box(15,18,20),               //调用构造函数Box,提供第2个元素的实参
    Box(16,20,26)                //调用构造函数Box,提供第3个元素的实参
    };
 cout<<″volume of a[0] is ″<

运行结果:

volume of a[0] is 1800
volume of a[1] is 5400
volume of a[2] is 8320

四、对象指针

在建立对象时,编译系统会为每一个对象分配一定的存储空间,以存放其成员。对象空间的起始地址就是对象的指针。可以定义一个指针变量,用来存放对象的指针。如果有一个类:

class Time
{public:
      int hour;
      int minute;
      int sec;
      void get_time( );
};
void Time∷get_time( )
{cout<

在此基础上有以下语句:
Time *pt; //定义pt为指向Time类对象的指针变量
Time t1; //定义t1为Time类对象
pt=&t1; //将t1的起始地址赋给pt
这样,pt就是指向Time类对象的指针变量,它指向对象t1。
定义指向类对象的指针变量的一般形式为
类名 *对象指针名;
可以通过对象指针访问对象和对象的成员。如
*pt pt所指向的对象,即t1。
(*pt).hour pt所指向的对象中的hour成员,即t1.hour
pt->hour pt所指向的对象中的hour成员,即t1.hour
(*pt).get_time ( ) 调用pt所指向的对象中的get_time函数,即t1.get_time
pt->get_time ( ) 调用pt所指向的对象中的get_time函数,即t1.get_time

指向对象成员的指针

对象有地址,存放对象初始地址的指针变量就是指向对象的指针变量。对象中的成员也有地址,存放对象成员地址的指针变量就是指向对象成员的指针变量。
. 指向对象数据成员的指针
定义指向对象数据成员的指针变量的方法和定义指向普通变量的指针变量方法相同。例如
int *p1; //定义指向整型数据的指针变量
定义指向对象数据成员的指针变量的一般形式为
数据类型名 *指针变量名;
如果Time类的数据成员hour为公用的整型数据,则可以在类外通过指向对象数据成员的指针变量访问对象数据成员hour。
p1=&t1.hour; //将对象t1的数据成员hour的地址赋给p1,p1指向t1.hour
cout<<*p1<

指向对象成员函数的指针

注意: 定义指向对象成员函数的指针变量的方法和定义指向普通函数的指针变量方法有所不同。
指向普通函数的指针变量的定义方法:
void (p)( ); //定义p是指向void型函数的指针变量
p=fun; //将fun函数入口地址赋给p,p指向函数fun
(p)( ); //调用fun函数
模仿上面的方法将对象成员函数名赋给指针变量是不可以的
p=t1.get_time; //会出现编译错误
成员函数与普通函数有一个最根本的区别: 它是类中的一个成员。编译系统要求在上面的赋值语句中,指针变量的类型必须与赋值号右侧函数的类型相匹配,要求在以下3方面都要匹配: ①函数参数的类型和参数个数;②函数返回值的类型;③所属的类。
定义指向成员函数的指针变量应该采用下面的形式:
void (Time∷
p2)( ); //定义p2为指向Time类中公用成员函数的指针变量
(Time∷
p2)的括号不可省略,因为()优先级高于*,如无此括号则相当于
void Time∷*(p2( )); //此为返回值为void型指针的函数
定义指向公用成员函数的指针变量的一般形式为
数据类型名 (类名∷*指针变量名)(参数表列);
可以让它指向一个公用成员函数,只需把公用成员函数的入口地址赋给一个指向公用成员函数的指针变量即可。如
p2=&Time∷get_time;
使指针变量指向一个公用成员函数的一般形式为
指针变量名=&类名∷成员函数名; //在VC++系统中也可以不写&,以和C语言用法一致,但建议写C++程序时不省略
例:

#include 
using namespace std;
class Time
{public:
     Time(int,int,int);
     int hour;
     int minute;
     int sec;
     void get_time( );                //声明公有成员函数
};

Time∷Time(int h,int m,int s)  //定义构造函数
{hour=h;
 minute=m;
 sec=s;
}
{cout<get_time( );                  //调用p2所指向对象(即t1)的get_time函数
  void (Time∷*p3)( );              //定义指向Time类公用成员函数的指针变量p3
  p3=&Time∷get_time;              //使p3指向Time类公用成员函数get_time
  (t1.*p3)( );                      //调用对象t1中p3所指的成员函数(即t1.get_time( ))
}

void Time∷get_time( ) //定义公有成员函数
程序运行结果为
10 (main函数第4行的输出)
10:13:56 (main函数第5行的输出)
10:13:56 (main函数第7行的输出)
10:13:56 (main函数第10行的输出)
可以看到为了输出t1中hour,minute和sec的值,可以采用3种不同的方法。
说明:
(1) 从main函数第9行可以看出: 成员函数的入口地址的正确写法是: &类名∷成员函数名。
(2) main函数第8、9两行可以合写为一行:
void (Time∷*p3)( )=&Time∷get_time; //定义指针变量时指定其指向

this 指针

在第2章中曾经提到过: 每个对象中的数据成员都分别占有存储空间,如果对同一个类定义了n个对象,则有n组同样大小的空间以存放n个对象中的数据成员。但是,不同对象都调用同一个函数代码段。
那么,当不同对象的成员函数引用数据成员时,怎么能保证引用的是所指定的对象的数据成员呢?假如,对于例3.6程序中定义的Box类,定义了3个同类对象a,b,c。如果有a.volume( ),应该是引用对象a中的height,width和length,计算出长方体a的体积。如果有b.volume( ),应该是引用对象b中的height,width和length,计算出长方体b的体积。而现在都用同一个函数段,系统怎样使它分别引用a或b中的数据成员呢?
在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this。它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。例如,当调用成员函数a.volume时,编译系统就把对象a的起始地址赋给this指针,于是在成员函数引用数据成员时,就按照this的指向找到对象a的数据成员。例如volume函数要计算heightwidthlength的值,实际上是执行:
(this->height)(this->width)(this->length)
由于当前this指向a,因此相当于执行:
(a.height)(a.width)(a.length)
这就计算出长方体a的体积。同样如果有b.volume( ),编译系统就把对象b的起始地址赋给成员函数volume的this指针,显然计算出来的是长方体b的体积。
this指针是隐式使用的,它是作为参数被传递给成员函数的。本来,成员函数volume的定义如下:
int Box∷volume( )
{return (heightwidthlength);
}
C++把它处理为
int Box∷volume(Box this)
{return(this->height * this->width * this->length);
}
即在成员函数的形参表列中增加一个this指针。在调用该成员函数时,实际上是用以下方式调用的:
a.volume(&a);
将对象a的地址传给形参this指针。然后按this的指向去引用其他成员。
需要说明: 这些都是编译系统自动实现的,编程序者不必人为地在形参中增加this指针,也不必将对象a的地址传给this指针。
在需要时也可以显式地使用this指针。例如在Box类的volume函数中,下面两种表示方法都是合法的、相互等价的。
return(height * width * length); //隐含使用this指针
return(this->height * this->width * this->length); //显式使用this指针
可以用
this表示被调用的成员函数所在的对象,this就是this所指向的对象,即当前的对象。例如在成员函数a.volume( )的函数体中,如果出现this,它就是本对象a。上面的return语句也可写成
return((this).height * (this).width * (this).length);
注意
this两侧的括号不能省略,不能写成
this.height。因为成员运算符“.”优先级高于指针运算符“
”, this.height相当于(this.height),而this.height不是合法的,编译会出错。
所谓“调用对象a的成员函数f”,实际上是在调用成员函数f时使this指针指向对象a,从而访问对象a的成员。在使用“调用对象a的成员函数f”时,应当对它的含义有正确的理解。

共用数据的保护

C++虽然采取了不少有效的措施(如设private保护)以增加数据的安全性,但是有些数据却往往是共享的,人们可以在不同的场合通过不同的途径访问同一个数据对象。有时在无意之中的误操作会改变有关数据的状况,而这是人们所不希望出现的。
既要使数据能在一定范围内共享,又要保证它不被任意修改,这时可以使用const,即把有关的数据定义为常量。

常对象

在定义对象时指定对象为常对象。常对象必须要有初值,如
Time const t1(12,34,46); //t1是常对象
这样,在所有的场合中,对象t1中的所有数据成员的值都不能被修改。凡希望保证数据成员不被改变的对象,可以声明为常对象。
定义常对象的一般形式为
类名 const 对象名[(实参表列)];
也可以把const写在最左面:
const 类名 对象名[(实参表列)];
二者等价。
如果一个对象被声明为常对象,则不能调用该对象的非const型的成员函数(除了由系统自动调用的隐式的构造函数和析构函数)。例如,对于例3.7中已定义的Time类,如果有
const Time t1(10,15,36); //定义常对象t1
t1.get_time( ); //企图调用常对象t1中的非const型成员函数,非法
这是为了防止这些函数会修改常对象中数据成员的值。不能仅依靠编程者的细心来保证程序不出错,编译系统充分考虑到可能出现的情况,对不安全的因素予以拦截。
现在,编译系统只检查函数的声明,只要发现调用了常对象的成员函数,而且该函数未被声明为const,就报错,提请编程者注意。
引用常对象中的数据成员很简单,只需将该成员函数声明为const即可。如
void get_time( ) const; //将函数声明为const
这表示get_time是一个const型函数,即常成员函数。常成员函数可以访问常对象中的数据成员,但仍然不允许修改常对象中数据成员的值。
有时在编程时有要求,一定要修改常对象中的某个数据成员的值(例如类中有一个用于计数的变量count,其值应当能不断变化),ANSI C++考虑到实际编程时的需要,对此作了特殊的处理,对该数据成员声明为mutable,如
mutable int count;
把count声明为可变的数据成员,这样就可以用声明为const的成员函数来修改它的值。

常对象成员

可以将对象的成员声明为const,包括常数据成员和常成员函数。

1.常数据成员

其作用和用法与一般常变量相似,用关键字const来声明常数据成员。常数据成员的值是不能改变的。有一点要注意: 只能通过构造函数的参数初始化表对常数据成员进行初始化。如在类体中定义了常数据成员hour:
const int hour; //声明hour为常数据成员
不能采用在构造函数中对常数据成员赋初值的方法。
Time∷Time(int h)
{hour=h;} //非法,常数据成员不能被赋值
在类外定义构造函数,应写成以下形式:
Time∷Time(int h):hour(h){} //通过参数初始化表对常数据成员hour初始化
常对象的数据成员都是常数据成员,因此常对象的构造函数只能用参数初始化表对常数据成员进行初始化。
在类体中声明了一个数据成员为常数据成员后,该类的所有对象中的该数据成员的值都不可改变,但不同对象中的该数据成员的值可以是不同的(在定义时给出)。

2.常成员函数

前面已提到: 一般的成员函数可以引用本类中的非const数据成员,也可以修改它们。如果将成员函数声明为常成员函数,则只能引用本类中的数据成员,而不能修改它们,例如只用于输出数据等。如
void get_time( ) const; //注意const的位置在函数名和括号之后
const是函数类型的一部分,在声明函数和定义函数时都要有const关键字,在调用时不必加const。常成员函数可以引用const数据成员,也可以引用非const的数据成员。const数据成员可以被const成员函数引用,也可以被非const的成员函数引用。具体情况可以用书中表3.1表示。见教材P90
怎样利用常成员函数呢?
(1) 如果在一个类中,有些数据成员的值允许改变,另一些数据成员的值不允许改变,则可以将一部分数据成员声明为const,以保证其值不被改变,可以用非const的成员函数引用这些数据成员的值,并修改非const数据成员的值。
(2) 如果要求所有的数据成员的值都不允许改变,则可以将所有的数据成员声明为const,或将对象声明为const(常对象),然后用const成员函数引用数据成员,这样起到“双保险”的作用,切实保证了数据成员不被修改。
(3) 如果已定义了一个常对象,只能调用其中的const成员函数,而不能调用非const成员函数(不论这些函数是否会修改对象中的数据)。这是为了保证数据的安全。如果需要访问对象中的数据成员,可将常对象中所有成员函数都声明为const成员函数,但应确保在函数中不修改对象中的数据成员。
不要误认为常对象中的成员函数都是常成员函数。常对象只保证其数据成员是常数据成员,其值不被修改。如果在常对象中的成员函数未加const声明,编译系统把它作为非const成员函数处理。
还有一点要指出: 常成员函数不能调用另一个非const成员函数。

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型指针变量。
往往用常指针作为函数的形参,目的是不允许在函数执行过程中改变指针变量的值,使其始终指向原来的对象。如果函数执行过程中修改了该形参的值,编译系统就会发现错误,给出出错信息,这样比用人工来保证形参的值不被修改更可靠。

4.指向常对象的指针变量

为了更容易理解指向常对象的指针变量的概念和使用,首先了解指向常变量的指针变量,然后再进一步研究指向常对象的指针变量。
下面定义了一个指向常变量的指针变量ptr:
const char *ptr;
注意const的位置在最左侧,它与类型名char紧连,表示指针变量ptr指向的char变量是常变量,不能通过ptr来改变其值的。
定义指向常变量的指针变量的一般形式为
const 类型名 *指针变量名;
说明:
(1)如果一个变量已被声明为常变量,只能用指向常变量的指针变量指向它,而不能用一般的(指向非const型变量的)指针变量去指向它。如
const char c[ ]=“boy”; //定义const型char数组
const char *p1; //定义p1为指向const型的char变量的指针变量
p1=c; //合法,p1指向常变量(char数组的首元素)
char *p2=c; //不合法,p2不是指向常变量的指针变量
(2) 指向常变量的指针变量除了可以指向常变量外,还可以指向未被声明为const的变量。此时不能通过此指针变量改变该变量的值。如
char c1=‘a’; //定义字符变量c1,它并未声明为const
const char *p; //定义一个指向常变量的指针变量p
p=&c1; //使p指向字符变量c1
*p=‘b’; //非法,不能通过p改变c1的值
c1=‘b’; //合法,没有通过p访问c1,c1不是常变量
请注意:定义了指向常变量的指针变量p并使它指向c1,并不意味着把c1也声明为常变量,而只是通过指针变量引用c1时,c1具有常变量的特征,其值不能被改变。在其他情况下,c1仍是一个普通变量,其值是可以改变的。
如果希望在任何情况下都不能改变c1的值,则应把它定义为const型。
如const char c1=‘a’;
(3) 如果函数的形参是指向非const型变量的指针,实参只能用指向非const变量的指针,而不能用指向const变量的指针,这样,在执行函数的过程中可以改变形参指针变量所指向的变量(也就是实参指针所指向的变量)的值。如果函数的形参是指向const型变量的指针,在执行函数过程中显然不能改变指针变量所指向的变量的值,因此允许实参是指向const变量的指针,或指向非const变量的指针。如
const char str[ ]=“boy”; //str是const型数组名
void fun(char *ptr); //函数fun的形参是指向非const型变量的指针
fun(str); //调用fun函数,实参是const变量的地址,非法

5.对象的常引用

过去曾介绍: 一个变量的引用就是变量的别名。实质上,变量名和引用名都指向同一段内存单元。如果形参为变量的引用名,实参为变量名,则在调用函数进行虚实结合时,并不是为形参另外开辟一个存储空间(常称为建立实参的一个拷贝),而是把实参变量的地址传给形参(引用名),这样引用名也指向实参变量。

6.小结

C++【02】类和对象的进阶_第1张图片

7.对象的动态建立和释放

用前面介绍的方法定义的对象是静态的,在程序运行过程中,对象所占的空间是不能随时释放的。但有时人们希望在需要用到对象时才建立对象,在不需要用该对象时就撤销它,释放它所占的内存空间以供别的数据使用。这样可提高内存空间的利用率。
在第1章介绍了用new运算符动态地分配内存,用delete运算符释放这些内存空间。这也适用于对象,可以用new运算符动态建立对象,用delete运算符撤销对象。
如果已经定义了一个Box类,可以用下面的方法动态地建立一个对象:
new Box;
编译系统开辟了一段内存空间,并在此内存空间中存放一个Box类对象,同时调用该类的构造函数,以使该对象初始化(如果已对构造函数赋予此功能的话)。但是此时用户还无法访问这个对象,因为这个对象既没有对象名,用户也不知道它的地址。这种对象称为无名对象,它确实是存在的,但它没有名字。
用new运算符动态地分配内存后,将返回一个指向新对象的指针的值,即所分配的内存空间的起始地址。用户可以获得这个地址,并通过这个地址来访问这个对象。需要定义一个指向本类的对象的指针变量来存放该地址。如
Box *pt; //定义一个指向Box类对象的指针变量pt
pt=new Box; //在pt中存放了新建对象的起始地址
在程序中就可以通过pt访问这个新建的对象。如
cout cout 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运算符时,在释放内存空间之前,自动调用析构函数,完成有关善后清理工作。

五、对象的赋值和复制

1.对象的赋值

如果对一个类定义了两个或多个对象,则这些同类的对象之间可以互相赋值,或者说,一个对象的值可以赋给另一个同类的对象。这里所指的对象的值是指对象中所有数据成员的值。
对象之间的赋值也是通过赋值运算符“=”进行的。本来,赋值运算符“=”只能用来对单个的变量赋值,现在被扩展为两个同类对象之间的赋值,这是通过对赋值运算符的重载实现的。实际这个过程是通过成员复制来完成的,即将一个对象的成员值一一复制给另一对象的对应成员。对象赋值的一般形式为
对象名1 = 对象名2;
注意对象名1和对象名2必须属于同一个类。
例如
Student stud1,stud2; //定义两个同类的对象
stud2=stud1; //将stud1赋给stud2

2.对象的复制

有时需要用到多个完全相同的对象。此外,有时需要将对象在某一瞬时的状态保留下来。这就是对象的复制机制。用一个已有的对象快速地复制出多个完全相同的对象。如
Box box2(box1);
其作用是用已有的对象box1去克隆出一个新对象box2。
其一般形式为
类名 对象2(对象1);
用对象1复制出对象2。
C++还提供另一种方便用户的复制形式,用赋值号代替括号,如
Box box2=box1; //用box1初始化box2
其一般形式为
类名 对象名1 = 对象名2;
可以在一个语句中进行多个对象的复制。如
Box box2=box1,box3=box2;
按box1来复制box2和box3。可以看出: 这种形式与变量初始化语句类似,请与下面定义变量的语句作比较:
int a=4,b=a;
这种形式看起来很直观,用起来很方便。但是其作用都是调用复制构造函数。

六、静态数据成员

如果有n个同类的对象,那么每一个对象都分别有自己的数据成员,不同对象的数据成员各自有值,互不相干。但是有时人们希望有某一个或几个数据成员为所有对象所共有。这样可以实现数据共享。
在C语言中曾介绍过全局变量,它能够实现数据共享。如果在一个程序文件中有多个函数,在每一个函数中都可以改变全局变量的值,全局变量的值为各函数共享。但是用全局变量的安全性得不到保证,由于在各处都可以自由地修改全局变量的值,很有可能偶一失误,全局变量的值就被修改,导致程序的失败。因此在实际工作中很少使用全局变量。
如果想在同类的多个对象之间实现数据共享,也不要用全局对象,可以用静态的数据成员。
所有对象都可以引用它。静态的数据成员在内存中只占一份空间。每个对象都可以引用这个静态数据成员。静态数据成员的值对所有对象都是一样的。如果改变它的值,则在各对象中这个数据成员的值都同时改变了。这样可以节约空间,提高效率。
说明:
(1) 在第2章中曾强调: 如果只声明了类而未定义对象,则类的一般数据成员是不占内存空间的,只有在定义对象时,才为对象的数据成员分配空间。但是静态数据成员不属于某一个对象,在为对象所分配的空间中不包括静态数据成员所占的空间。静态数据成员是在所有对象之外单独开辟空间。只要在类中定义了静态数据成员,即使不定义对象,也为静态数据成员分配空间,它可以被引用。

在一个类中可以有一个或多个静态数据成员,所有的对象共享这些静态数据成员,都可以引用它。
(2) 在学习C语言时曾介绍了静态变量的概念: 如果在一个函数中定义了静态变量,在函数结束时该静态变量并不释放,仍然存在并保留其值。现在讨论的静态数据成员也是类似的,它不随对象的建立而分配空间,也不随对象的撤销而释放(一般数据成员是在对象建立时分配空间,在对象撤销时释放)。静态数据成员是在程序编译时被分配空间的,到程序结束时才释放空间。
(3) 静态数据成员可以初始化,但只能在类体外进行初始化。如
int Box∷height=10; //表示对Box类中的数据成员初始化
其一般形式为
数据类型类名∷静态数据成员名=初值;
不必在初始化语句中加static。
注意: 不能用参数初始化表对静态数据成员初始化。如在定义Box类中这样定义构造函数是错误的:
Box(int h,int w,int len):height(h){ } //错误,height是静态数据成员
如果未对静态数据成员赋初值,则编译系统会自动赋予初值0。
(4) 静态数据成员既可以通过对象名引用,也可以通过类名来引用。
请注意: 在上面的程序中将height定义为公用的静态数据成员,所以在类外可以直接引用。可以看到在类外可以通过对象名引用公用的静态数据成员,也可以通过类名引用静态数据成员。即使没有定义类对象,也可以通过类名引用静态数据成员。
这说明静态数据成员并不是属于对象的,而是属于类的,但类的对象可以引用它。
如果静态数据成员被定义为私有的,则不能在类外直接引用,而必须通过公用的成员函数引用。
(5) 有了静态数据成员,各对象之间的数据有了沟通的渠道,实现数据共享,因此可以不使用全局变量。全局变量破坏了封装的原则,不符合面向对象程序的要求。
但是也要注意公用静态数据成员与全局变量的不同,静态数据成员的作用域只限于定义该类的作用域内(如果是在一个函数中定义类,那么其中静态数据成员的作用域就是此函数内)。在此作用域内,可以通过类名和域运算符“∷”引用静态数据成员,而不论类对象是否存在。

静态数据成员函数

成员函数也可以定义为静态的,在类中声明函数的前面加static就成了静态成员函数。如
static int volume( );
和静态数据成员一样,静态成员函数是类的一部分,而不是对象的一部分。如果要在类外调用公用的静态成员函数,要用类名和域运算符“∷”。如
Box∷volume( );
实际上也允许通过对象名调用静态成员函数,如
a.volume( );
但这并不意味着此函数是属于对象a的,而只是用a的类型而已。
与静态数据成员不同,静态成员函数的作用不是为了对象之间的沟通,而是为了能处理静态数据成员。
前面曾指出: 当调用一个对象的成员函数(非静态成员函数)时,系统会把该对象的起始地址赋给成员函数的this指针。而静态成员函数并不属于某一对象,它与任何对象都无关,因此静态成员函数没有this指针。既然它没有指向某一对象,就无法对一个对象中的非静态成员进行默认访问(即在引用数据成员时不指定对象名)。
可以说,静态成员函数与非静态成员函数的根本区别是: 非静态成员函数有this指针,而静态成员函数没有this指针。由此决定了静态成员函数不能访问本类中的非静态成员。
静态成员函数可以直接引用本类中的静态数据成员,因为静态成员同样是属于类的,可以直接引用。在C++程序中,静态成员函数主要用来访问静态数据成员,而不访问非静态成员。假如在一个静态成员函数中有以下语句:
cout< cout< 但是,并不是绝对不能引用本类中的非静态成员,只是不能进行默认访问,因为无法知道应该去找哪个对象。如果一定要引用本类的非静态成员,应该加对象名和成员运算符“.”。如
cout< 假设a已定义为Box类对象,且在当前作用域内有效,则此语句合法。

七、友元

在一个类中可以有公用的(public)成员和私有的(private)成员。在类外可以访问公用成员,只有本类中的函数可以访问本类的私有成员。现在,我们来补充介绍一个例外——友元(friend)。
友元可以访问与其有好友关系的类中的私有成员。友元包括友元函数和友元类。

友元函数

如果在本类以外的其他地方定义了一个函数(这个函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数),在类体中用friend对其进行声明,此函数就称为本类的友元函数

1.将普通函数声明为友元函数

#include 
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<

由于声明了display是Time类的friend函数,所以display函数可以引用Time中的私有成员hour,minute,sec。但注意在引用这些私有数据成员时,必须加上对象名,不能写成
cout< 因为display函数不是Time类的成员函数,不能默认引用Time类的数据成员,必须指定要访问的对象。

2.友元成员函数

friend函数不仅可以是一般函数(非成员函数),而且可以是另一个类中的成员函数。见例3.13。
例3.13 友元成员函数的简单应用。
在本例中除了介绍有关友元成员函数的简单应用外,还将用到类的提前引用声明。

#include 
using namespace std;
class Date;                 //对Date类的提前引用声明
class Time                  //定义Time类
{public:
     Time(int,int,int);
     void display(Date &);    //display是成员函数,形参是Date类对象的引用
 private:
     int hour;
   int minute;
   int sec;
};

class Date                               //声明Date类
{public:
      Date(int,int,int);
      friend void Time∷display(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(Date &d)       //display的作用是输出年、月、日和时、分、秒
{cout<

运行结果:

12/25/2004                     (输出Date类对象d1中的私有数据)
10:13:56                       (输出Time类对象t1中的私有数据)

在本例中定义了两个类Time和Date。程序第3行是对Date类的声明,因为在第7行和第16行中对display函数的声明和定义中要用到类名Date,而对Date类的定义却在其后面。能否将Date类的声明提到前面来呢?也不行,因为在Date类中的第4行又用到了Time类,也要求先声明Time类才能使用它。为了解决这个问题,C++允许对类作“提前引用”的声明,即在正式声明一个类之前,先声明一个类名,表示此类将在稍后声明。程序第3行就是提前引用声明,它只包含类名,不包括类体。如果没有第3行,程序编译就会出错。
应当注意: 类的提前声明的使用范围是有限的。只有在正式声明一个类以后才能用它去定义类对象。如果在上面程序第3行后面增加一行:
Date d1; //企图定义一个对象
会在编译时出错。因为在定义对象时是要为这些对象分配存储空间的,在正式声明类之前,编译系统无法确定应为对象分配多大的空间。编译系统只有在“见到”类体后,才能确定应该为对象预留多大的空间。
在对一个类作了提前引用声明后,可以用该类的名字去定义指向该类型对象的指针变量或对象的引用变量(如在本例中,定义了Date类对象的引用变量)。这是因为指针变量和引用变量本身的大小是固定的,与它所指向的类对象的大小无关。
请注意程序是在定义Time∷display函数之前正式声明Date类的。如果将对Date类的声明的位置(程序13~21行)改到定义Time∷display函数之后,编译就会出错,因为在Time∷display函数体中要用到Date类的成员month,day,year。如果不事先声明Date类,编译系统无法识别成员month,day,year等成员。
在一般情况下,两个不同的类是互不相干的。在本例中,由于在Date类中声明了Time类中的display成员函数是Date类的“朋友”,因此该函数可以引用Date类中所有的数据。请注意在本程序中调用友元函数访问有关类的私有数据方法:
(1) 在函数名display的前面要加display所在的对象名(t1);
(2) display成员函数的实参是Date类对象d1,否则就不能访问对象d1中的私有数据;
(3) 在Time∷display函数中引用Date类私有数据时必须加上对象名,如d.month。
3. 一个函数(包括普通函数和成员函数)可以被多个类声明为“朋友”,这样就可以引用多个类中的私有数据
例如, 可以将例3.13程序中的display函数不放在Time类中,而作为类外的普通函数,然后分别在Time和Date类中将display声明为朋友。在主函数中调用display函数,display函数分别引用Time和Date两个类的对象的私有数据,输出年、月、日和时、分、秒。

3.友元类

不仅可以将一个函数声明为一个类的“朋友”,而且可以将一个类(例如B类)声明为另一个类(例如A类)的“朋友”。这时B类就是A类的友元类。友元类B中的所有函数都是A类的友元函数,可以访问A类中的所有成员。
在A类的定义体中用以下语句声明B类为其友元类:
friend B;
声明友元类的一般形式为
friend 类名;
关于友元,有两点需要说明:
(1) 友元的关系是单向的而不是双向的。如果声明了B类是A类的友元类,不等于A类是B类的友元类,A类中的成员函数不能访问B类中的私有数据。
(2) 友元的关系不能传递。如果B类是A类的友元类, C类是B类的友元类,不等于C类是A类的友元类
在实际工作中,除非确有必要,一般并不把整个类声明为友元类,而只将确实有需要的成员函数声明为友元函数,这样更安全一些。
关于友元利弊的分析: 面向对象程序设计的一个基本原则是封装性和信息隐蔽,而友元却可以访问其他类中的私有成员,不能不说这是对封装原则的一个小的破坏。但是它能有助于数据共享,能提高程序的效率,在使用友元时,要注意到它的副作用,不要过多地使用友元,只有在使用它能使程序精炼,并能大大提高程序的效率时才用友元。

八、类的模板

将此类模板和前面第一个Compare_int类作一比较,可以看到有两处不同:
(1) 声明类模板时要增加一行
template
(2) 原有的类型名int换成虚拟类型参数名numtype。在建立类对象时,如果将实际类型指定为int型,编译系统就会用int取代所有的numtype,如果指定为float型,就用float取代所有的numtype。这样就能实现“一类多用”。
由于类模板包含类型参数,因此又称为参数化的类。如果说类是对象的抽象,对象是类的实例,则类模板是类的抽象,类是类模板的实例。利用类模板可以建立含各种数据类型的类。
在声明了一个类模板后,怎样使用它?怎样使它变成一个实际的类?
先回顾一下用类来定义对象的方法:
Compare_int cmp1(4,7); // Compare_int是已声明的类
用类模板定义对象的方法与此相似,但是不能直接写成
Compare cmp1(4,7); // Compare是类模板名
Compare是类模板名,而不是一个具体的类,类模板体中的类型numtype并不是一个实际的类型,只是一个虚拟的类型,无法用它去定义对象。必须用实际类型名去取代虚拟的类型,具体的做法是:
Compare cmp(4,7);

你可能感兴趣的:(C++)