第五章 类与对象
5.1 类与对象
5.5 运算符的重载
5.4 构造函数和析构函数
5.3 引用
5.2 从面向过程到面向对象
5.10 全局对象及其它
5.9面向对象的程序设计和 Windows编程
5.8 结构和联合
5.7 静态成员
5.6 友元
5.1 类与对象
5.1.3对象的创建与使用
5.1.4名字空间域和类域
5.1.1 C++类的定义
5.1.2成员函数的定义
5.1.1 C++类的定义在 C++中,类是一种数据类型。
定义一个类的一般格式为:
class 类名 {
《,private:,
成员表 1;》
,public:
成员表 2;》
,protected:
成员表 3;》
};
5.1.1 C++类的定义例如描述一种商品,在 C++中可以这样表述:
class CGoods{
private,
char Name[21] ;
int Amount ;
float Price ;
float Total_value ;
public,
void RegisterGoods(char*,int,float) ;
void CountTotal(void) ;
void GetName(char*) ;
int GetAmount(void) ;
float GetPrice(void) ;
float GetTotal_value(void) ;};
5.1.1 C++类的定义类把数据 ( 事物的属性 ) 和函数 ( 事物的行为 ——操作 ) 封装为一个整体 。 还应注意到,
四个数据成员被说明成私有的,而六个函数成员被说明成公有的; 这就是说如果从外部对四个数据成员进行操作的话,只能通过六个公有函数来完成,数据受到了良好的保护,不易受副作用的影响 。 公有函数集定义了类的接口
( interface) 。
5.1.2 成员函数的定义在前面的小结中,只对成员函数作了一个声明,或者讲只给出了函数的原型,并没有对函数进行定义 。 函数通常在类的说明之后进行,其格式如下:
返回值类型 类名,:函数名 (参数表 )
{…… }
//函数体其中运算符,,:,称为作用域解析运算符 (scope
resolution operator),它指出该函数是属于哪一个类的成员函数 。 当然也可以在类的定义中直接定义函数 。
5.1.3 对象的创建与使用对象是类的实例 ( instance),正如在前几章称变量是数据类型的实例一样 。 声明一种数据类型只是告诉编译系统该数据类型的结构形式,并没有预定内存,或者讲并没有创建了可用来存放数据的变量 。 类只是一个样板,以此样板可以在内存中开辟出一个个同样结构的实例 ——对象 。
创建类的对象可以有两种常用方法 。 第一种是直接定义类的实例 ——
对象:
CGoods Car;
这个定义创建了 CGoods类的一个对象 Car,同时为它分配了属于它自己的存储块,用来存放数据和对这些数据实施操作的成员函数 ( 代码 ) 。 与变量定义一样,一个对象只在定义它的域中有效 。
第二种是采用动态创建类的对象的方法,将在第七章中学习,当然变量也同样可动态创建 。 所谓动态指在程序运行时建立对象 。 而前一种是在编译时 ( 程序运行前 ) 建立 。
一个样板可以创造出无数相同的物品来,同样,一个类可以创建出无数同样组成的对象来 。
5.1.3 对象的创建与使用有两种方法可存储对象。
数据区代码区对象 1
数据区代码区对象2
数据区代码区对象n
.....

图 5.1各对象完全独立地安排内存的方案数据区对象 1
数据区对象2
数据区对象n
..,..

图 5.2各对象的代码区共用的方案公共代码区
5.1.3 对象的创建与使用
【 例 5.1】 商品类对象应用实例:
#include<iostream.h>
#include<iomanip.h>
#include<string.h>
//省略了类定义
void main( ){
CGoods car ;
char string[21] ;
int number ;
float pr ;
5.1.3对象的创建与使用
cout<<“请输入汽车型号:,;
cin.getline(string,20) ; //输入串长必须小于 20
cout<<“请依次输入汽车数量与单价:,;
cin>>number>>pr ;
car.RegisterGoods(string,number,pr) ;
car.CountTotal() ;
string[0]=?\0? ; //字符串 string清零
car.GetName(string) ; //string赋值 car.Name
cout<<setw(20)<<string<<setw(5)<<car.GetAmount() ; //A
cout<<setw(10)<<car.GetPrice()<<setw(20)<<car.GetTotal_value()<
<endl ; //B
}
成员名
Name[21] ;
Amount ;
Price ;
Total_value ;
RegisterGoods(char*,int,float) ;
CountTotal(void) ;
GetName(char*) ;
GetAmount(void) ;
GetPrice(void) ;
GetTotal_value(void) ;};
minicar
5
2
10
minicar 5
2 10
5.1.4 名字空间域和类域在 C++中支持三种形式的域,局部域 ( local scope),名字空间域 ( namespace scope) 和 类域 ( class scope) 。
名字空间域 是不包含在函数声明、函数定义或类定义内的程序文本部分。换言之,名字空间域可含有函数声明、函数定义或类定义等等,而不能相反包含。程序的最外层的名字空间域被称为全局域
( global scope),或全局名字空间域( global namespace scope)
程序员也可以利用名字空间定义 ( namespace definition) 来定义用户声明的 ( user-declared) 的名字空间,它们嵌套在全局域内 。
用户声明的名字空间定义以关键字 namespace开头,后面是名字空间的名字(标识符)。该名字在它被定义的域中必须唯一,不能与其它实体同名,因为用户声明的名字空间可以不连续,分为多段,但它们是同一个名字空间。
5.1.4 名字空间域和类域名字空间域的引入,主要是为了解决全局名字空间污染
( global namespace pollution)问题,即防止程序中的全局实体名与 C++各种库中声明的全局实体名冲突。
using 声 明 以 关键 字 using 开头,后 面是 被 限定 修 饰 的
( qualified) 名字空间成员名 。
例如:
namespace cplusplus_primer{
namespace Matrixlib{ //名字空间嵌套
class matrix{…… } //名字空间类成员 matrix
…,..
}
}
5.1.4 名字空间域和类域使用 using指示符可以一次性地使库中所有成员都可以直接被使用,而不用限定修饰名。
using cplusplus_primer::Matrixlib::matrix m ;
等同于,using cplusplus_primer::Matrixlib
matrix m ;
5.1.4 名字空间域和类域使用 using指示符可以一次性地使库中所有成员都可以直接被使用,
而不用限定修饰名 。
using指示符以关键字 using开头,后面是关键字 namespace,然后是名字空间名 。
例如:
# include,primer.h” //头文件中定义了 cplusplus_primer名字空间 using
namespace cplusplus_primer; //using指示符使 cplusplus_primer所有成员都成为可见的
……
void func(matrix&m){… } ;// func()为 cplusplus_primer名字空间中的成员
//cplusplus_primer中所有成员可不加限定修饰地被使用
5.1.4 名字空间域和类域类体也定义了一个域称为类域。在类域中说明的标识符仅在该类的类域内有效。必须加上,类名::,作限定修饰。
类的实体 ——对象中的公有成员也可以在对象之外访问,但必须使用成员访问操作符,,”,对象名 +“.”+成员名。
定义类本身的目的就是要实现一个封装性,对外是封闭的,对内是开放的,在程序中并不总是需要用成员访问符之类来引用类成员。多数程序代码本身就在类域中,这些程序可以直接访问类成员。
5.1.4 名字空间域和类域在类域中不仅有数据和函数成员,也可以有类型说明,可以称为类成员,或成员类,其作用域为类域,出了类说明之外无效 。 例如嵌套类
( nested class),
class student { public,
class studentID{ //成员类,嵌套类
int value ;
public,
…… } ;
private,
studentID id ; //嵌套类说明的私有对象
char name[20] ;}
student s1,s2 ;
student::studentID kk ;
5.2 从面向过程到面向对象上世纪六十年代中后期软件危机发生之后,面向过程 ( procedure-oriented ) 的 结 构 化 程 序 设 计
( structured programming,SP) 成为主流 。 结构化程序设计的提出与发展是伴随软件日益庞大和复杂进行的,
但是当软件复杂到一定的程度后,结构化程序设计也不能满足需要 。 当软件规模超过一定的尺度后,采用结构化程序设计,其开发和维护就越来越难控制 。 其 根本的原因 就在于面向过程的结构化程序设计的方法与现实世界 ( 包括主观世界和客观世界 ) 往往都不一致,结构化程序设计的思想往往很难贯彻到底 。
5.2 从面向过程到面向对象在结构化程序设计中,采用的是,自顶向下,逐步细化( divide and conquer,stepwise refinement),的思想。它的具体操作方法是模块化,是按功能来分的,所以也称功能块。也就是从一般事物中抽象出来的操作,在
C++中称为一个函数,一个函数解决一个问题,即实现一个功能或一个操作。当程序规模和复杂性达到一定程度时不可避免地引入大量的全局变量,这时模块化没法坚持到底。比如一个实时管理系统,当管理的规则发生大的变化,
程序的维护往往相当困难,为某一处修改通用的函数往往会影响其它部分,牵一发而动全身。可维护性差成了制约结构化程序设计应用的瓶颈。所以,必须重新进行功能抽象,必须重新建立模块间联系的规则。
5.2 从面向过程到面向对象对象的概念是面向对象技术的核心所在。面向对象技术中的对象就是现实世界中某个具体的物理实体在计算机逻辑中的映射和体现。比如你所拥有的一部移动电话,它是现实世界中的一个实体。它由天线、发射部件、接收部件、显示屏、按键、
专用集成电路芯片及外壳组成;它有着其实在的功能,可以打电话,可以发短消息,可以存储、输入和编辑各种个人信息,
甚至可以上网。这样一个实体可以在计算机世界中映射为一个计算机可以理解、可以操纵、具有前面所叙述的属性和操作的对象。又如你们所拥有的一辆自行车,它由车架、车轮、脚踏和传动机构、变速机构等组成,它具有代步功能,它可以进行变速骑行,特别要强调的是它有一些特征可以把你的这辆自行车与其他自行车区分开来,其中最重要的是钢印号。这些都可以在面向对象的程序中用对象及其属性和操作模拟出来。
5.2 从面向过程到面向对象对象类计 算 机世 界实体抽象类别现实世界客观世界抽象抽象实例化映射 主观世界图 5.3对象,实体与类现实世界中的实体可以抽象出类别的概念。对应于计算机世界就有一个类( class)的概念,因为类是一个抽象的概念的对应体,所以计算机不给它分配内存,只给对象分配内存。 图 5.3表达了计算机世界与现实世界之间的对应关系。
5,3 引用在有关函数的学习中,我们知道 C++函数中参数的传递方式是传值 。 在函数域中为参数重新分配内存,而把实参的数值传递到新分配的内存中 。
它的优点是有效避免函数的副作用,在函数调用中不会无意中修改了实参的值 。 但如果就是要求改变实参的值,怎么办呢? 再者,如果参数是一些简单的数据类型,占据内存不多,重新分配内存问题不大;如果实参是一个复杂的对象,重新分配内存会引起程序执行效率大大下降,怎么办呢?在 C++中有一种新的导出型数据类型 —引用
( reference) 可以解决上面的难题 。 引用又称别名 ( alias) 。
5,3 引用引用是一种非常特殊的数据类型,它不是定义一个新的变量,而是给一个已经定义的变量重新起一个别名,也就是 C++系统不为引用类型变量分配内存空间 。 引用主要用于函数之间的数据传递 。 引用定义的格式为:
类型 &引用变量名 =已定义过的变量名;
例如:
double number ;
double &newnum=number ;
newnum是新定义的引用类型变量,它是变量 number的别名,内存分配见图 5.4。
number称为引用 newnum的关联变量 。,&”(仍读作 ampersand)在这里是引 用 的说 明 符 。 必 须注 意 number和
newnum都是 double类型 。 如在程序中修改了 newnum也就是修改了 number,
两位一体 。
5,3 引用
【 例 5.2】 使用一个函数来交换两个数据 。 内存分配见图 5.5。
#include<iostream.h>
void swap(double & d1,double & d2){
double temp ;
temp=d1 ; d1=d2 ; d2=temp ;}
void main(void){
double x,y ;
cout<<"请输入 x和 y的值 "<<'\n';
cin>>x>>y ;
swap(x,y) ;
cout<<"x="<<x<<'\t'<<"y="<<y<<'\n';}
X
y
d1
d2
temp
1.414
2.718
1.414
2.718
1.414
5,3 引用
【 例 5.3】 采用不同返回方式的求正方形面积函数的比较
#include<iostream.h>
double temp;
double fsqr1(double a){
temp=a*a ; return temp;}
double & fsqr2(double a){
temp=a*a ; return temp;}
void main(){
double x=fsqr1(5.5);//第一种情况
double y=fsqr2(5.5);//第二种情况
cout<<"x="<<x<<'\t'<<"y="<<y<<endl;
}
图 5.6 普通返回图 5.7 引用返回
5,3 引用
【 例 5.4】 统计学生成绩,分数在 80分以上的为 A类,
60分以上,80分以下的为 B类,60分以下为 C类。
程序:E x5_4.cpp
5.4 构造函数和析构函数定义对象时,按现在已学过的知识无法进行初始化,即无法对数据成员进行赋初值的过程。数据成员,从封装的目的出发,应该多为私有的,要对它们进行初始化,看来必须用一个公有函数来进行。同时这个函数应该在且仅在定义对象时自动执行一次,否则就不是初始化了。在 C++程序设计语言中这个函数称为构造函数( constructor)。必须指出:调用构造函数也是建立对象的唯一方法(联合例题,见
5.8)。
5.4 构造函数和析构函数
5,4,1 构造函数的定义与使用
5,4,3 析构函数的定义
5,4,2 拷贝构造函数
5,4,4 成员对象与构造函数
5.4.1 构造函数的定义与使用对于对象的初始化,采用构造函数( constructor)是标准的方法,C++编译器也会自动产生一个缺省的构造函数,不过什么初始化也不做。所以当需要对对象进行初始化时,总是编写一个或一组构造函数。
构造函数是特殊的公有成员函数,其 特征 如下:
1,函数名与类名相同 。
2,构造函数无函数返回类型说明 。 注意是没有而不是 void,即什么也不写,也不可写 void! 实际上构造函数有返回值,返回的就是构造函数所创建的对象,见
5.5节 。
3,在程序运行时,当新的对象被建立,该对象所属的类的构造函数自动被调用,
在该对象生存期中也只调用这一次 。
4,构造函数可以重载 。 严格地讲,说明中可以有多个构造函数,它们由不同的参数表区分,系统在自动调用时按一般函数重载的规则选一个执行 。
5,构造函数可以在类中定义,也可以在类外定义 。
6,如果类说明中没有给出构造函数,则 C++编译器自动给出一个缺省的构造函数,
类名 (void) {};
5.4.1 构造函数的定义与使用下面编写 5.1节中商品类 CGoods的构造函数 。 可以用三个参数来实现对 4个数据成员的初始化:
Cgoods (char* name,int amount,float price){
strcpy(Name,name) ;
Amount=amount ;
Price=price ;
Total_value=price*amount ; }
在实际应用时,也可以只用两个参数:货名和单价,这时构造函数为:
Cgoods (char* name,float price){
strcpy(Name,name) ;
Price=price ;
Amount=0 ;
Total_value=0.0 ;}
这两个构造函数同时被说明 。
5.4.1 构造函数的定义与使用如果定义对象时的格式为:
CGoods Car1(“夏利 2000”,30,98000.0) ;
则调用了 CGoods中的第一个构造函数,相当于自动调用:
CGoods(“夏利 2000”,30,98000.0) ;
如果定义对象时的格式为:
CGoods Car2(“桑塔那 2000”,164000.0) ;
则调用的是第二个构造函数,参数为两个 。
定义对象初始化时也可以把构造函数显式表示出来如:
CGoods Car1= CGoods(“夏利 2000”,30,98000.0);
如果还希望初始化时,不带任何参数,可作如下定义:
CGoods( ){Name[0]=?\0? ; Price=0.0 ;
Amount=0 ; Total_value=0.0 ;}
但是定义对象时不能加括号 。 例如,CGoods Car3,Car4();
5.4.1 构造函数的定义与使用缺省的构造函数,也可以由程序员自己来编,
只要构造函数是无参的或者只要各参数均有缺省值的,C++编译器都认为是缺省的构造函数,并且缺省的构造函数只能有一个 。
5.4.2 拷贝构造函数同一个类的对象在内存中有完全相同的结构,如果作为一个整体进行复制或称拷贝是完全可行的 。 这个拷贝过程只需要拷贝数据成员,而函数成员是共用的 ( 只有一份拷贝 ) 。 在建立对象时可用同一类的另一个对象来初始化该对象,这时所用的构造函数称为拷贝初始化构造函数 ( Copy Constructor) 。
对于 CGoods类,可以定义拷贝构造函数为:
CGoods (CGoods & cgd){
StrCpy (Name,cgd.Name);
Price= cgd.price;
Amount=cgd.Amount;
Total_value=cgd.Total_value;
};
5.4.2 拷贝构造函数通常情况下,这种按成员语义支持已经足够。但在某些情况下,它对类与对象的安全性和处理的正确性还不够,这时就要求类的设计者提供特殊的 拷贝构造函数
( Copy Constructor)和拷贝赋值操作符( Copy
Assignment Operator)的定义。
这里必须注意拷贝构造函数的参数 ——同类
( class)的对象采用的是引用的方式。如果把一个真实的类对象作为参数传递到拷贝构造函数,会引起无穷递归 。 所以必须将拷贝构造函数的参数定义为一个类的对象的引用。
5.4.2 拷贝构造函数这里 成员函数的参数为同一类 ( class) 的对象或它的引用,在函数体内使用参数对象的私有数据成员时,可用对象名加成员访问操作符点号进行 。 从逻辑上讲,每个对象有自己的成员函数,访问同类其他对象的私有数据成员应通过该对象的公有函数,不能直接访问 。 但在物理上只有一个成员函数拷贝,所以直接访问是合理的 。 其他实例可见 5.5节 。 对本对象的数据成员不加点号,参见 6.2节 。 注意,仅在成员函数中可以这样做 。
下面来看一个实例 。 有一个程序段:
CGood Car1(“夏利 2000”,30,98000.00); //调用三个参数的构造函数
CGood Car2= Car1;调用拷贝构造函数
CGood Car3 ( Car1);调用拷贝构造函数这样三个对象的初始化情况完全一样 。
在类定义中如果没有显式给出构造函数时,并不是不用构造函数,而是由系统自动调用缺省的构造函数或缺省的拷贝构造函数 。 如果有程序设计者定义的构造函数 ( 包括拷贝构造函数 ),则按函数重载的规律,调用合适的构造函数 。
5.4.2 拷贝构造函数拷贝构造函数并不只是在同类的一个对象去初始化该类的另一个对象时使用,它还在另二个方面使用:
1,当函数的形参是类的对象,调用函数时,进行形参与实参结合时使用 。 这时要在内存新建立一个局部对象,并把实参拷贝到新的对象中 。 理所当然也调用拷贝构造函数 。
2.当函数的返回值是类对象,函数执行完成返回调用者时使用 。 理由也是要建立一个临时对象中,再返回调用者 。 为什么不直接用要返回的局部对象呢? 因为 局部对象在离开建立它的函数时就消亡了,
不可能在返回调用函数后继续生存,所以在处理这种情况时,编译系统会在调用函数的表达式中创建一个无名临时对象,该临时对象的生存周期只在函数调用处的表达式中 。 所谓 return 对象,实际上是调用拷贝构造函数把该对象的值拷入临时对象 。 如果返回的是变量,处理过程类似,只是不调用构造函数 。
5.4.3 析构函数的定义当一个对象定义时,C++自动调用构造函数建立该对象并进行初始化,那么当一个对象的生命周期结束时,C++也会自动调用一个函数注销该对象并进行善后工作,这个特殊的成员函数即析构函数( destructor):
1,构函数名也与类名相同,但在前面加上字符 ‘ ~?,如
~CGoods( ) 。
2,析构函数无函数返回类型,与构造函数在这方面是一样的 。
但析构函数不带任何参数 。
3,一个类有一个也只有一个析构函数,这与构造函数不同 。 析构函数可以缺省 。
4,对象注销时,系统自动调用析构函数 。
5.4.4 成员对象与构造函数在定义类的对象时不仅要对对象进行初始化,还要对成员对象进行初始化 。 对成员对象初始化,必须调用该成员对象的构造函数来实现 。
【 例 5.5】 含有成员对象的类的构造函数:
程序,Ex5_5.cpp
这段程序希望通过构造函数中的 A行进行初始化,把学生名,朱明,和学号
08002132赋给对象 SS。 运行结果:
赋给学生的学号,0 //对象 ss的对象成员 id中数据成员 value的初值学生名:朱明 //对象 ss的数据成员 name
赋给学生的学号,08002132 //ss构造函数中建立的局部对象 id中数据成员 value
初值删去学号,08002132 //局部对象 id析构删去学号,0 //对象成员 id析构
5.4.4 成员对象与构造函数最后作为对构造函数和析构函数的总结,对于不同作用域的对象类型,构造函数和析构函数的调用如下:
1,对全局定义的对象,当程序进入入口函数 main之前对象就已经定义,这时要调用构造函数 。 整个程序 结束时调用析构函数 。
2,对于局部定义的对象,每当程序控制流到达该对象定 义处时,调用构造函数 。 当程序控制走出该局部域时,则调用析构函数 。
3,对于静态局部定义的对象,在程序控制首次到达该对 象定义处时,调用构造函数 。 当整个程序结束时调用析构函数 。
5.4.4 成员对象与构造函数
【 例 5.6】 演示对象创建和撤消的对应关系 。
程序 Ex5_6.cpp
Initializing 0 0 //全局对象首先建立,调用缺省的构造函数
Entering main //进入入口函数
Initializing 0 0 //用缺省的构造函数建立 com1
Initializing 5.6 7.5 //用带参数的构造函数建立 com2
Copy 0 0 //用拷贝的构造函数建立 com3
0+0i //打印 com3
0+0i //打印 global
5.4.4 成员对象与构造函数
Copy 5.6 7.5 //调用全局函数 fun(),调用拷贝构造函数建立临时对象 com
Entering function //进入全局函数 fun()
Copy 5.6 7.5 //进入 global.assign(),调用拷贝构造函数建立临时对象新 com
Destructor //退出 global.assign(),调用析构函数,清新 com
Exiting function //将退出 fun()
Destructor //退出 fun(),调用析构函数,清 fun()的 com
Exit main //将退出入口函数
Destructor //退出入口函数前,调用析构函数,清 com3
Destructor //退出入口函数前,调用析构函数,清 com2
Destructor //退出入口函数前,调用析构函数,清 com1
Destructor //退出入口函数前,调用析构函数,清 global
5.5 运算符的重载
C++中没有复数类型,我们可以自己来定义一个复数类
( class),同样可以用 +,-,*,/来进行复数的算术运算 。
运算符的重载实际是一种特殊的函数重载,必须定义一个函数,
并告诉 C++编译器,当遇到该重载的运算符时调用此函数 。 这个函数叫做运算符重载函数,通常为类的成员函数 。
定义运算符重载函数的一般格式:
返回值类型 类名,:operator重载的运算符 (参数表 )
{…… }
operator是关键字,它与重载的运算符一起构成函数名。 因函数名的特殊性,C++编译器可以将这类函数识别出来。
5.5 运算符的重载
【 例 5.7】 定义复数类,可完成复数基本运算,并应用它进行复数运算。
程序:E x5_7.cpp
在本例中重载了运算符,+”,,=”,,+=”和,*”,,/”,以及求模 ( 绝对值 ) 的函数 abs(),可以进行复数运算 。 首先来看,+”的重载 。
在做
c=c2+c3;
时,C++编译器把表达式 c2+c3解释为:
c2.operator+(c3) ;
这样一个函数调用过程,函数 c2.operator创建一个局部的 Complex对象 Temp,把出现在表达式中的两个 Complex类对象 c2和 c3的实部之和及虚部之和暂存其内,然后把这个局部对象返回,赋给 Complex类对象 c( 注意这里调用了拷贝构造函数生成一个无名临时对象过渡 ) 。 以上过程可以用图 5.8形象表示 。
5.5 运算符的重载
Temp.Real=Real+c2.Real;
Temp.Image=Image+c3.Image;
c=return(Temp);
Real
Image
c3.Real
c3.Image= +
局部 对象 Temp 当前对象 c2 对象 c3
图 5.8显式说明临时对象的,+”运算符执行过程
5.5 运算符的重载使用引用类型变量作为运算符重载函数的参数,可以提高复数类型运算的效率 。 复数与复数相加的 Operator+成员函数的最终形式:
Complex complex::operator+(const complex &c){
return complex(real+c.real,Image+c.Image) ;
}
这里采用 complex对象的引用而不是对象本身,调用时不再重新分配内存建立一个复制的对象,函数效率会更高。而在引用形式参数类型说明前加 const关键字,表示被引用的实参是不可改变的,
如程序员不当心在函数体中重新赋值了被引用的实参,C++编译器会认为出错。其它运算符也是类似处理。
5.5 运算符的重载在缺省的情况下,C++ 编译器为每个类生成一个缺省的赋值操作,用于同类的两个对象之间的相互赋值,缺省的语义是类成员逐个相互赋值 。 对复数类 complex 如果没有重载赋值运算符 =,复数的赋值语义是:
Complex &Complex::operator = (Complex& c){
real = c.real;
image = c.image
return *this; //参见 6.2节
}
这种 缺省的赋值操作格式对所有类是固定的,这种 缺省的格式对 复数是合适的,但对其他类缺省的赋值可能产生问题,那时需重载。对所有的类对象,赋值运算符,=”即缺省的按成员 拷贝赋值操作符( Copy
Assignment Operator),同类对象之间可以用,=”直接拷贝。本例中重载的 赋值运算符,=”取代了缺省的赋值操作,格式专用 。
5.5 运算符的重载重载的 运算符,+=”标准算法是:
Complex& Complex::operator +=(Complex &
com){
real += com.real;
image += com.image;
return *this; //参见 6.2节
}
5.5 运算符的重载由以上例子与说明,对运算符的重载可做一小结:
1,运算符重载函数的函数名必须为关键字 Operator加一个合法的运算符 。 在调用 该函数时,将右操作数作为函数的实参 。
2,当用类的成员函数实现运算符的重载时,运算符重载函数的参数 ( 当为双目运 算符时 ) 为一个或 ( 当为单目运算符时 ) 没有 。 运算符的左操作数一定是对象,因为重载的运算符是该对象的成员函数,而右操作数是该函数的参数,其类型 并无严格限制 。 C++不允许重载三目运算符 。
3,单目运算符,++”和,--”存在前置与后置问题 。 前置,++”格式为:
返回类型 类名,:operator++(){…… }
而后置,++”格式为:
返回类型 类名,:operator++(int){…… }
后置,++”中的参数 int仅用作区分,并无实际意义,可以给一个变量名,也可以不给变量名 。
4,C++中只有极少数的运算符不允许重载,表 5.1中列出了不允许重载的运算符。
5.5 运算符的重载运算符 运算符名称 禁止重载的理由
,三目条 件运算符
C++中没有定义三目运算符的语法
,成员操作符 为保证成员操作符对成员访问的安全性
:,作用域操作符 该操作符右操作数不是表达式
Sizeof 类型字 长操作符该操作符的操作数为类型名,不是表达式表 5.1C++中不允许重载的运算符
5.5 运算符的重载在本小节中学习了有关运算符重载的基础知识,必须指出的是这只是初步的,由于所学知识有限,重载中不少问题还不能解决 。 如例 5.2中:
c=c+d;
语句,改为
c=d+c;
因为 d不是 Complex的对象,C++编译器将无法找到合适的重载的,+”运算符对应的函数,最终给出出错信息。
5.6 友元在 C++中采用友元 ( friend) 函数允许在类外访问该类中的任何成员,就象成员函数一样 。
友元函数用关键字 friend说明 。 下面用友元函数重载运算符,+”,以实现 c=d+c。
用友元函数重载运算符,+”,实现实数与复数的加法 。
5.6 友元
class Complex {……
friend Complex operator + (double,Complex);
//opration+说明为类 Complex类的有元函数,friend只用于类说明中 };
……
Complex operator + (double d,Complex c){ //注意友元不是成员函数,
也不加 friend
return Complex(d+c.Real,c.Image) ;//友元函数可以直接访问私有成员 }
void main(void){
……
c=d+c1;
c.print();
}
5.6 友元如 d和 c1的初值与例 5.2相同,则输出为:
Real=1.5 Image=1.0
这里 d+c被 C++编译器解释为 operator+( d,c),重载了友元函数 。 从这个例子由此可知友元函数在特定场合可能是必不可少的 。 友元函数重载运算符 +,有三种形式 。 另两个的声明为:
friend Complex operator +(Complex,Complex ) ;
friend Complex operator + (Complex,double ) ;
则无论是复数与复数相加,还是实数与复数相加 ( 不论实数在前还是在后 ) 都可以用该运算符三个重载函数之一 。 再进一步,如果使用友元函数
friend complex operator +(complex c1,complexc2) ;
无论是复数与复数相加,还是实数与复数相加 ( 不论实数在前还是在后 ) 都可以用该运算符重载函数 。 因为有例 5.7所定义的缺省的构造函数,实数会被强制转换为虚部为零的复数 。 d+c1被解释为 operator+(complex(d),c1)。 注意这里的两个参数是传值,在函数内是建立了两个复数对象,而把实参的值传进去,进行运算 。 参见图 5.9。
5.6 友元在这里友元函数可以有两个参数,而对应的成员函数只有一个参数,所以友元函数的使用可以更灵活,更方便 。
使用引用类型变量作为运算符重载函数的参数,以提高复数类型运算的效率和可行性 。 Operator+友元函数的声明可改进为:
friend Complex operator+(const Complex & c1,const Complex & c2)
这里采用 Complex对象的引用而不是对象本身,调用时不再重新分配内存建立一个复制的对象,函数效率会更高 。 加 const,实参只读,可防止实参被修改 。
5.6 友元
【 例 5.7_1】 用友元函数重载运算符,实现复数的运算。
程序 Ex5.7_1.cpp
5.6 友元单目运算符前,++”的成员函数重载方式如下:
Complex Complex::operator++()
{return (++Real,++Image) ;}
采用成员函数方式重载与使用都很方便 。 但采用友元方式则必须使用引用,
因为被施加,++”运算的是一个参数 。 友元函数重载后置,++”如下:
friend Complex operator++(Complex & c,int) //注意友元方式与前者的区别
{return (c.Real++,c.Image++) ;}
采用引用类型,后,++”是直接施加于实参。否则施加于拷贝,而实参不变。
5.6 友元关于友元函数要注意以下几点:
1,友元函数不是类的成员函数,在函数体中访问对象的成员,必须用对象名加运算符
,.”加 对象成员名 。 这一点和一般函数一样 。 但友元函数可以访问类中的所有成员
( 公有的,私有的,保护的 ),一般函数只能访问类中的共有成员 。
2,友元函数不受类中的访问权限关键字限制,可以把它放在类的公有,私有,保护部分,但结果一样 。
3,某类的友元函数的作用域并非该类作用域 。 如果该友元函数是另一类的成员函数,
则其作用域为另一类的作用域,否则与一般函数相同 。
但是友元函数破坏了面向对象程序设计类的封装性,所以友元函数如不是必须使用,则尽可能少用 。 或者用其他手段保证封装性 。
友元还有友元类概念:整个类可以是另一个类的友元 。 友元类的每个成员函数都是另一个类的友元函数,都可访问另一个类中的保护或私有数据成员 。 定义方法如下:
class A{……
friend class B;//声明 B为 A的友元类
…… };
5.7 静态成员由关键字 static修饰说明的类成员,成为静态类成员 ( static class member) 。 虽然使用 static修饰说明,但与函数中的静态变量有明显差异 。 类的静态成员为其所有对象共享,
不管有多少对象,静态成员只有一份存于公用内存中 。
5.7 静态成员
5.7.1 静态数据
5.7.2 静态函数成员
5.7.1 静态数据在类定义中,用关键字 static修饰的数据成员为静态数据成员。该类产生的所有对象共享系统为静态成员分配的一个存储空间,而这个存储空间是在编译时分配的,在定义对象时不再为静态成员分配空间。
静态数据实际上是该类所有对象所共有的,它更像在面向过程程序设计时的全局变量,可提供同一类的所有对象之间信息交换的捷径,正因为静态数据成员不属于类的某一特定对象,而是属于整个类的,所以使用时可用以下格式:
类名,:静态数据成员名
5.7.1 静态数据
【 例 5.8】 用静态数据成员对同一类建立的对象的数量进行计数。
程序 Ex5_8.cpp
5.7.1 静态数据执行程序后输出:
对象数量 =1 //a[0]构造函数产生对象数量 =2 //a[1]构造函数产生对象数量 =3 //a[2]构造函数产生对象数量 =2 //a[2]析构函数产生对象数量 =1 //a[1]析构函数产生对象数量 =0 //a[0]析构函数产生上例中 A行是对静态数据成员数据作定义性说明,必须在文件作用域中作一次并只能做一次说明,只有在这时 C++编译器为静态数据成员分配存储空间 。 C++静态数据成员缺省的初值为 0,所以 A行中,=0”是可以省去的 。
特别要注意不管静态变量是私有或公有,定义性说明均有效 。
静态数据成员虽然具有全局变量的一些特性,但受到访问权限的约束 。 建议静态成员说明为私有的,从而保证面向对象程序设计的封装性 。 如果说明为公有的,它会带来与全局变量同样的副作用 。
5.7.2 静态函数成员函数成员说明为静态,同样将与该类的不同对象无关 。 严格地讲,在逻辑上该函数成员只有一个拷贝 。 静态函数成员的调用,在对象之外可以采用下面的方式:
类名,:函数名与静态数据成员相反,为使用方便,静态函数成员多为公有的 。 在例 5.6中的复数类中的函数成员 print(),如被说明为静态的则可如下表达:
Static void
print(){cout<<”Real=”<<Real<<?\t?<<”Image=”<<Image<<?\n?}
如果静态成员函数只涉及其他静态成员 ( 包括静态数据和静态函数 ),因他们是独立于具体对象而存在的,似乎可以用 complex::print( )来调用 。 但是因为数据不确定 ( C++系统不知应取哪一个对象的数据 ) 而不能运行 。 在本例中 print()函数应改为,
static void print(complex & ob){
cout<<”Real=”<<ob.Real<<?\t?<<”Image=”<<ob.Image<<?\n? ;
}
这里用 complex 的对象的引用为参数,而以具体的 complex对象为实参,这样就能正常运行了 。
5.7.2 静态函数成员如果静态成员函数在类定义之外定义时,则不能在定义时再加 static,这一点与友元函数类似 。 因为 static不属于数据类型组成部分 。
因为 C++在产生类的对象时,为了减少对象所占空间,
物理上将同一类的所有对象的成员函数只保留一个拷贝,
参见图 5.2。 所以一般情况下定义静态函数不能取得明显好处,只有逻辑上的优点 。 反而在使用上变得不方便,通常是没有必要去定义静态成员函数的 。
5.8 结构和联合在 C++中结构( structure)与类几乎是完全一样的类型,差别仅仅在于缺省情况下结构的成员为公有的。在 C语言阶段,结构就已存在,但它只有公有的数据成员。正因为如此,C++程序员仍然使用结构,但是只为结构安排公有的数据成员。当只使用数据成员,而且这些数据成员的类型往往互不相同时,总是采用结构类型,而不采用类。因为这样程序更易读易懂。在程序设计中可以把结构类型的数据作为类的数据成员,
定义结构类型的格式如下,
struct 结构类型名 {
类型名 变量 1;
,类型名 变量 2; … ;》
};//最后的分号不可少
5.8 结构和联合例如下面给出的库存货物 ( inventory),员工 ( employee) 两个结构类型:
struct inventory{
char description[15] ; //货物名称
char no[10] ; //货号
int quantity ; //库存数量
double cost ; //成本
double retail ; //零售价格 } ;
struct employee{
char name[27] ; //员工姓名
char address[30] ; //家庭住址
long int zip ; //邮政编码
long int telenum ; //联络电话
double salary ; //工资 };
5.8 结构和联合结构是一种派生数据类型,定义结构时并不分配存储空间,只有定义了结构类型的变量,编译系统才为结构变量分配存储空间 。 定义变量方法如下:设有两种货物 —汽车和摩托车
inventory car,motor ;
对结构变量的初始化是用花括号中顺序填入结构中的 ( 数据 ) 成员的初始值完成的:
employee emp1={“朱明,,,四牌楼 2号,,210096,
3792666,2430.0},emp2={“沈俊,,,丁家桥 15号,,210009,
3273389,1920.0};
结构变量的访问与类一样,可使用成员访问操作符之一 ——点操作符,对成员函数一个个进行:
变量名,成员名
5.8 结构和联合与同类的对象之间可以 拷贝 一样,同结构类型的变量之间也可以作为 整体相互赋值(拷贝) (因为结构的组成与大小是固定的,
这一点与数组不同),在 C++中,赋值运算符,=”理解为 隐式 拷贝赋值操作符( Copy Assignment Operator)。 如按前面的定义,emp1=emp2是合法的,这时 emp2中的内容成为 emp1的拷贝。但同类型变量不能比较大小。
结构变量也可以作为函数的参数和返回值,结构作为参数也是按值(复制)进行传递的,当然也可以按引用传递。在程序文件中强烈推荐将结构类型的定义放在所有函数的外面,这样程序文件中的各个函数可以按需要在各个函数中声明局部的结构变量。在各函数中定义结构类型,即使两个函数中定义的完全一样,系统也完全认为是两种结构类型。
5.8 结构和联合结构可以嵌套,如上面所定义的员工 ( employee) 结构类型可改为如下定义 ;
struct mail{
char address[30] ; //地址
long int zip ; //邮政编码
long int telenum ; //电话号码 };
struct employee{
char name[25] ; //员工姓名
mail addinfo ; //结构作为成员,嵌套
double salary ; //工资 };
结构成员必须是已定义过的结构,结构成员决不能是结构自身 ( 这会引起无穷递归 ) 。
用连续点号来访问结构变量的结构成员中的成员 。 如有程序段,
employee emp1={“朱明,,,四牌楼 2号,,210096,3792666,2430.0};
cout<<emp1.addinfo.telenum ;
输出为 3792666。
5.9 面向对象的程序设计和 Windows编程在本小节中,我们引入怎样实际实现面向对象的程序设计的概念与方法 。 使读者理解面向对象设计的程序中各对象是怎样协调工作的,以及为什么在 Windows操作系统下才能真正实现面向对象的程序设计 。
5.9 面向对象的程序设计和 Windows编程
5.9.1 面向对象程序的组织与 Windows下的实现
5.9.3 MFC 编程
5.9.2 传统的 Windows 编程
5.9.1 面向对象程序的组织与 Windows下的实现在面向过程的程序设计中有一句名言:
程序 =算法 +数据结构 。
算法实际上就是功能抽象 。 在面向过程的程序设计中程序是模块化的,
模块是分层次的,层与层之间是一个从上往下的调用关系 。 图 5.10给出了这种层次模块的调用关系 。 功能抽象是困难的,而且很难全面,一旦要解决的问题发生一点小变化,功能块就要重编,而一个功能块又被多个上层模块调用 ( 图中称任务块 ),他们的要求有的变了,有的没变,
这就给重编带来极大的困难 。
图 5.11 面向过程程序设计的程序组织
5.9.1 面向对象程序的组织与 Windows下的实现面向对象的程序设计则换了一句名言:
对象 =( 算法 +数据结构 ),
程序 =对象 +对象 +…… +对象 。
这里程序是由一个个封装的对象组成,而对象是由紧密结合在一起的算法和数据结构组成,对象中有数据和对数据的操作,它带来了计算机效率的下降和程序员效率的上升及工作难度的下降 。 那么对象与对象之间怎样建立有效的联系,互相调用的思想明显不行 。 实际上采用的是用消息传递机制来协调各对象的运行,如图 5.11:
图 5.12 面向对象的程序组织
5.9.1 面向对象程序的组织与 Windows下的实现消息是对象之间相互请求或相互协作的途径,是要求某个对象执行其中某个功能操作的规格的说明。消息传递是对象与其外部世界相互关联的唯一途径。对象可以向其他对象发送消息以请求服务,也可以响应其他对象传来的消息,完成自身固有的某些操作,从而服务于其他对象。
因为对象的操作主要用来响应外来消息并为其他对象提供服务,
所以它们也被称作,外部服务,。
面向对象的思想包括消息和方法的概念 。 一般而言,消息是客观世界中对象之间通信的信号,是要求某个对象执行其中某个功能操作的规格说明 。 对象的动作取决于发送给该对象的消息,消息通知对象要求完成的功能 。 也就是说,消息传递完成的是,做什么,的任务,并认为接受消息的对象知道如何去做,对象激活该功能,完成任务 。 从程序设计的角度看,消息功能类似于函数调用 。
5.9.1 面向对象程序的组织与 Windows下的实现方法描述了对象的能力,从程序设计的角度看它是对象实现功能操作的代码段。方法与消息相互对应,每当对象收到一个消息后,除了知道“做什么”外,还必须知道和决定“怎样做”。方法就是对象中决定“怎样做”的操作代码,方法就是实现每条消息具体功能的手段。
面向对象的程序设计并没有给出或指定具体的方法来实现这个“消息传递”机制,只是提出这样一种理念。实际上对 C++而言,这一机制是由操作系统完成的。
5.9.1 面向对象程序的组织与 Windows下的实现
Windows系统支持多个应用程序同时执行,在界面形式上,它支持多个窗口同时活动 。 它的运行机制就是,消息传递,事件驱动
( message based,event driven),。
Windows系统使用事件驱动的编程模式 。 所谓事件的含义非常广泛 。 输入设备的动作,如敲打键盘,按鼠标等会产生一系列的事件 ( 注意不是一个事件 ) 。 操作系统所作的一举一动也被当作某种类型的事件,应用程序也会产生各种事件 。 事件用来标识发生的某件事情 。
Windows系统对于应用程序环境中发生的每一个事件都会以对应的某种消息的形式标识,并放入相应的 Windows建立的消息队列中,然后由对应的应用程序或窗口函数去处理 。
5.9.1 面向对象程序的组织与 Windows下的实现图 5.13 windows 操作系统下的应用程序
5.9.2 传统的 Windows 编程
Windows操作系统是多任务、图形界面的操作系统。
Windows拥有一个图形用户界面 (GUI)。用户可以用键盘和鼠标与显示器上的图形实现直接交互,更方便的是可以直接用鼠标进行拖放操作。
Windows支持多任务操作,可在同一时刻运行多个程序,并可在程序间进行信息交互。每个程序只能在称为“窗口”的屏幕矩形区中实现输出。
Windows的图形设备接口 ( GUI),实现了程序与设备的无关性,
即为 Windows编写的应用程序可以运行于任何具有 Windows设备驱动程序的硬件环境中,方便了使用 。
为使程序员在编写应用程序时实现这些特征,Windows提供了一个应用程序编程接口,称 Windows API( Application
Programming Interface),这是 Windows支持的函数定义、参数定义和消息格式的集合,可供应用程序调用。
5.9.2 传统的 Windows 编程这上千个 API函数包含了各种窗口类和系统资源(内存管理、文件、线程等等)。利用这些函数就可以编出具有 Windows风格的程序。
Windows API也是 Windows操作系统自带的在
Windows环境下运行的软件开发包( SDK)。程序员总是直接或间接引用 API进行应用程序的开发,所以 Windows应用程序就有大致相同的用户界面。
5.9.2 传统的 Windows 编程直接采用 API进行的 C/C++程序设计称为传统的
Windows编程 。 下面给出一个最简单的实例:
【 例 5.9】 一个对话框的简单 Windows程序:
程序:E x5_9.cpp
5.9.2 传统的 Windows 编程入口函数,Windows程序的入口函数是 WinMain()。 在
VC++的集成环境中新建一个工程文件 ( project) 时有一种控制台程序方式 ( Console Application),它是链接器自动去寻找
main( )函数,建立的是 MS_DOS风格的程序 。 而其他程序方式是使链接器去寻找 Winmain()函数作为入口 。
头文件,在 Windows程序中,Windows.h是最主要的包含文件,它包含了其它的 Windows头文件,它定义了 Windows的所有数据类型,数据结构,符号常量和函数原型声明 。
调用约定,请注意 Winmain() 前方的说明中有一个
,APIENTRY”,这是一个宏,它代替的是 _stdcall,它表示按从左到右的顺序压参数入栈,由被调用者把参数弹出栈 ( stack)
入口参数与返回值,对应入口参数有四个固定的参数。
5.9.2 传统的 Windows 编程但是这个程序不是 典型的 Windows程序 。 典型的 Windows程序,入口函数中有一个由 while语句组成的 消息循环,在这个循环中调用了诸如获取消息 ( GetMessage()),键盘消息转换
( TranslateMessage()),派送消息 ( DispatchMessage())
给窗口处理函数 ( window procedure,或 window function)
等 API函数,循环直到取得退出消息 ( WIN-QUIT) 结束 。 在应用程序中还有操作系统的 API函数回调 ( callback) 的窗口处理函数 ( 简称窗口函数 ) 来根据消息值转相应的消息处理,这是由
switch语句组成的多向选择 。 如果不能处理则转缺省的窗口函数 。
请参见图 5.13。
Windows应 用程序有相对固定的程序结 构:入口函数
WinMain()首先进行初始化工作,注册窗口类,创建窗口,然后进行消息循环;要为窗口写一个窗口处理函数,在该函数内用
switch语句分不同消息进行相应处理 。 因此传统的 Windows编程往往有意识的套用一个程序框架,然后在此基础上加以修改以满足新的需要 。
5.9.3 MFC 编程
VC++的微软基础类库( Microsoft Foundation Class
Library,MFC)封装了大部分 API函数,并提供了 一个应用程序框架,简化了和标准了 Windows程序设计,所以用 MFC编写 Windows应用程序也称为标准 Windows程序设计。 MFC
约有 200个类,提供了 Windows应用程序框架和创建应用程序的组件。其中只有 5个核心类对应用程序框架有影响:
Cwinapp,Cdocument,Cview,CFrameWnd和
CDocTemplate。五个之中只有 Cwinapp是必不可少的类,
Cwinapp的对象在应用程序中必须有一个,也只有一个,并是一个全局对象,它建立了应用程序执行的主线程(线程的概念参见本节后的附录)。全局对象是在 MFC初始化之前,也即当 Windows操作系统调用 WinMain( )时,这个对象就先已建立,即主线程已建立。形象地说,执行程序的通路已经开通。
5.9.3 MFC 编程在 MFC编程中,入口函数 WinMain( )被封装在 MFC
的应用程序框架内,已经不用也不可以再定义为另一个
WinMain()函数 。
MFC编程最好的办法是使用 MFC的应用程序向导工具 AppWizard。 AppWizard为程序员提供了一种快捷方便的工具来定制基于 MFC的应用程序框架,程序员只需以此为基础,添加与修改程序代码来实现所需功能。
MFC编程包含三种类型的应用程序:单文档界面( SDI)
应用程序、多文档界面( MDI)应用程序和基于对话框
( Dialog Based)的应用程序,它们都可以由
AppWizard引导建立起来。
5.10 全局对象及其它建立对象的顺序就是调用构造函数的过程的顺序 。 工程文件中各
C++文件编排的顺序不同,结果也会有所不同 。
在 Windows的 C++编程中,入口函数 Winmain()并非第一个执行的程序函数。在标准的 Windows编程( MFC)中,有一个必不可少的但又只能有一个的类 CWinApp的对象,它必须被定义为全局量,由它建立 Windows应用程序的主线程,这些工作必须在进入入口函数之前完成。所有全局对象都在入口函数运行之前被构造。即全局对象是第一批构造的对象。。如果是单文件程序,全局对象可以按照定义的先后顺序确定建立的先后顺序。但是多文件程序,各文件分别编译、连接,因为编译器不能控制文件的连接顺序,所以不能决定不同文件中全局对象之间的构造顺序。
5.10 全局对象及其它第二,进一步讨论 类接口 。在类中一般数据成员置为私有,保证不被外部程序直接访问,而函数则必须有一部分成员置为公有,
专供类外程序语句访问,包括创建本类对象的构造函数和撤消该对象的析构函数,这些函数亦称 接口函数 。其余成员函数是开发类时故意对外隐蔽起来的操作,而这些往往是最复杂最关键的部分,越是复杂越是关键就越能体现出使用类封装的必要和优越。类中故意的隐藏也为以后的升级扩展留下了余地,只要接口不变,内部再变,
也不必修改原来的程序,就象 MFC(微软基础类)升级后,由
MFC底层类所编的程序完全不必修改,自动升级。封装有助于编程人员在处理好简单接口后,集中精力处理高层次开发的工作,
MFC能被广泛使用也正是有此优点。用户界面等的编程是底层的十分复杂和困难的事,MFC给解决了。