第7单元 类和对象(I)
本单元教学目标介绍面向对象程序设计方法的基本原理以及类和对象的概念。
教学要求掌握面向对象的程序设计思想,类和对象的概念,以及类的声明方法和对象的引用。
授课内容
7.1 面向对象的程序设计在面向对象的程序设计技术(OOP,Object Oriented Programming)出现前,程序员们一般采用面向过程的程序设计方法。面向过程的程序设计方法采用函数(或过程)来描述对数据结构的操作,但又将函数与其所操作的数据分离开来。作为对现实世界的抽象,函数和它所操作的数据是密切相关、相互依赖的:特定的函数往往要对特定的数据结构进行操作;如果数据结构发生改变,则必须改写相应的函数。这种实质上的依赖与形式上的分离使得用面向过程的程序设计方法编写出来的大程序不但难于编写,而且难于调试和修改。
面向对象程序设计从所处理的数据入手,以数据为中心而不是以功能为中心来描述系统。数据相对于功能而言具有更强的稳定性。面向对象程序设计与结构化程序设计最大的区别就在于,前者首先关心的是所要处理的数据,而后者首先关心的是功能。
面向对象程序设计是一种围绕真实世界的概念来组织模型的程序设计方法,它采用对象来描述问题空间中的实体。关于对象这一概念,目前还没有统一的定义。一般的认为,对象是包含现实世界物体特征的抽象实体,反映了系统为之保存信息和(或)与之交互的能力。对象是一些属性及服务的封装体,在程序设计领域,可以用“对象=数据+作用于这些数据上的操作”这一公式来表达。
类是具有相同操作功能和相同的数据格式(属性)的对象的集合,可以看作抽象数据类型的具体实现。从外部看,类的行为可以用新定义的操作(方法)加以规定。类是对象集合的抽象,规定了这些对象的公共属性和方法;对象是类的一个实例。例如,苹果是一个类,而放在桌上的那个苹果则是一个对象。对象和类的关系相当于一般的程序设计语言中变量和变量类型的关系。
消息是向某对象请求服务的一种表达方式。对象内有方法和数据,外部的用户或对象对该对象提出的服务请求,可以称为向该对象发送消息。
面向对象的编程方法具有四个基本特征:
1,抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,忽略与主题无关的细节。例如,在设计一个学生成绩管理系统的过程中,考察学生张三这个对象时,我们只关心他的班级、学号、成绩等,而他的身高、体重等信息就可以忽略。抽象包括两个方面,一是过程抽象,二是数据抽象。过程抽象是指任何一个明确定义功能的操作都可被使用者看作单个的实体看待,尽管这个操作实际上可能由一系列更低级的操作来完成。数据抽象定义了数据类型和施加于该类型对象上的操作,并限定了对象的值只能通过使用这些操作修改和观察。
2,封装:封装是面向对象的特征之一,是对象和类概念的主要特性。封装把过程和数据封藏起来,对数据的访问只能通过已定义的界面。面向对象技术的基本概念就是现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。一旦定义了一个对象的特性,则有必要决定这些特性的可见性,即哪些特性对外部世界是可见的,哪些特性用于表示内部状态。通常,应禁止直接访问一个对象的实际表示,只能通过操作接口访问对象,这称为信息隐藏。事实上,信息隐藏是用户对封装性的认识,封装则为信息隐藏提供支持。封装保证了模块具有较好的独立性,使得程序维护修改较为容易。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低限度。
3,继承:继承是一种联结类与类的层次模型。继承允许和鼓励类的重用,提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原来类的特性,新类称为原来类的派生类(子类),而原来类称为新类的基类(父类)。派生类可以从其基类那里继承方法和成员变量,当然也可以对之进行修改或增加新的方法使之更适合特殊的需要。这也体现了大自然中一般与特殊的关系。继承性很好地解决了软件的可重用性问题。
4,多态性:多态性是指允许不同类的对象对同一消息作出响应。例如同样的加法,把两个时间加在一起和把两个整数加在一起的内涵肯定完全不同。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。
面向对象程序设计具有许多优点:开发时间短,效率高,可靠性高,所开发的程序更强壮。由于面向对象编程的编码可重用性,可以在应用程序中大量采用成熟的类库,从而缩短了开发时间,使应用程序更易于维护、更新和升级。继承和封装使得应用程序的修改带来的影响更加局部化。
7.2 类与对象
7.2.1 类的声明
C++类的结构比较复杂,可以将其看成是一种既包含数据又包含函数的数据类型。显然,描述不同编程对象的类应有不同的内部结构,所以在使用类来声明对象前应先说明其结构。声明一个类的一般形式为:
class <类名>
{
private:
<成员表>;
public:
<成员表>;
protected:
<成员表>;
… …
};
其中关键字c1ass指出下面要声明的是一个类,<类名>是程序员为所声明的类起的名字;<成员表>由一个个该类成员的声明组成,这些成员可以是变量(数据成员),也可以是函数(成员函数);关键字private引出的成员叫做私有成员,对私有成员的访问权限制在该类的内部,即只允许该类中的成员函数访问。由于类成员被默认为私有的,所以该关键字可以省略;关键字public引出的成员叫做公有成员,公有成员则允许该类以外的函数访问;而关键字protected引出的成员叫做保护成员,保护成员的访问权限将在第8单元介绍。
[例7-1] 声明一个Person类,用来说明人类对象。
说 明:将Person类的声明放在头文件中。为项目添加头文件的方法与添加源代码文件的方法类似,建立项目后,使用Developer Studio的菜单选项File/New…,在File选项卡中选择C/C++ Header File项,并在对话框右面的File框中填写文件名(可与项目名相同),然后按OK键即可生成一空白头文件。然后输入如下代码。
程 序:
// Example 7-1(Person.h):声明Person类
// 类 Person 的声明
class Person
{
private:
char m_strName[20];
int m_nAge;
int m_nSex;
public:
void Register(char *Name,int Age,char Sex);
void GetName(char *Name);
int GetAge();
char GetSex();
};
作为一个人,可能有许多特征:姓名、性别、年龄、身高、体重、民族、学历、职业…,类Person体现了对人的抽象,它集中了人所具有的共性。同时,在该类中还说明了对一个抽象人的属性进行操作的方法:登录一个人的信息的函数Register();获取一个人的信息的函数GetName()、GetAge()和GetSex()。该例中,将数据成员声明为私有的以阻止外界对它们的随意访问,而成员函数则声明为公有的,它们便是外界访问类中数据成员的统一接口。本例中的private关键字可以省略。另外,在类的声明中,不同访问权限的成员的书写顺序可以任意排列。在这种情况下,要注意使用关键字private,不能随便省略。
在Visual C++程序中,在声明类的数据成员时,通常在其名前加上前缀“m_”,以区别于普通的变量名。
在声明一个类时应注意:
1.类中任何成员都不得用关键字extern,auto和register进行修饰。
2.不得在类声明中对数据成员使用表达式进行初始化。
7.2.2 成员函数的定义从例7-1中可以看出,在类的声明中仅给出了成员函数的原型,函数的定义还需在其他地方(通常每个类的成员函数的定义放在一起构成一个源程序文件)给出。类的成员函数的一般形式为:
<类型> <类名>,,<函数名> (<参数表>)
{
<函数体>
}
其中作用域运算符“::”指出成员函数的类属。没有类属的函数称为公共函数,在前面各单元中用到的函数均为公共函数。
[例7-2] Person类成员函数的声明。
说 明:按例7-1说明的方法添加头文件,输入Person类的声明。然后为项目添加一源代码文件,输入以下程序。
程 序:
// Example 7-2(Person.cpp):Person类成员函数的定义
#include <string.h>
#include,person.h”
void Person:,Register(const char *name,int age,char sex)
{
strcpy(m_strName,name);
m_nAge = age;
m_nSex = (sex = = ‘m’?0:1);
}
void Person::GetName(char *name)
{
strcpy(name,m_strName);
}
int Person:,GetAge()
{
return m_nAge;
}
char Person:,GetSex()
{
return (m_nSex = = 0?’m’:’f’);
}
类中的成员函数有时很简单,这样的函数最适合写成内联成员函数以提高程序的执行效率。对于成员函数来说,除了可以采用关键词inline将其声明为内联函数外,还有一种更简单的方法,就是在类的声明中直接定义成员函数的函数体,这样的函数自动成为内联成员函数。例如,可将类Person的成员函数声明为内联成员函数:
class Person
{
private:
char m_strName[20];
int m_nAge;
int m_nSex;
public:
void Register(const char *name,int age,char sex)
{ strcpy(m_strName,name);
m_nAge = age;
m_nSex = (sex = = ‘m’?0:1);
}
void GetName(char *name)
{ strcpy(name,m_strName);
}
int GetAge()
{ return m_nAge;
}
char GetSex()
{ return (m_nSex = = 0?’m’:’f’);
}
};
类的成员函数与普通函数一样,可以重载,也可以带有缺省参数。
7.2.3 公有成员和私有成员为了实现既要隐藏数据,又要为外界使用数据提供接口的封装目的,通常是将类中的数据成员声明为私有的而将成员函数声明为公有的。然而,在设计一个具体类时,各成员的访问权限还应根据实际需要而定。一般说来,将类中的数据成员声明成公有的将难以保证该成员的安全性,而将类的中成员函数声明成私有时还是非常必要的。例如,类中的一个只供其成员函数调用的成员函数就应当声明为私有的。
习惯上将具有全局作用域的类声明放在一个头文件中,将成员函数的定义放在一个另一个源程序文件中,以提高编程的灵活性。
7.2.4 对象对象是类的实例。从技术上讲,一个对象就是一个具有某种类类型的变量。与普通变量一样,对象也必须先经声明才可以使用。声明一个对象的一般形式为:
<类名> <对象1>,<对象2>,…
例如语句
Person person1,person2;
声明了两个名为personl和person2的Person类的对象。
在程序中使用一个对象,通常是通过对体现对象特征的数据成员的操作实现的。当然,由于封装性的要求,这些操作又是通过对象的成员函数实现的。具体来说:
1.成员函数访问同类中的数据成员,或调用同类中的其他成员函数,可直接使用数据成员名或成员函数名,可参看例7-2中各成员函数的定义;
2.在对象外访问其数据成员或成员函数需使用运算符“.”访问对象的成员,例如
nAge = person1.GetAge();
3.直接访问一个对象中的私有成员则属于非法操作,将导致编译错误;
4.同类对象之间可以整体赋值。例如
person1 = person2;
5.对象用作函数的参数时属于赋值调用;函数可以返回一个对象。
[例7-3] 人事资料的输入输出。
说 明:按例7-1说明的方法添加头文件,输入Person类的声明。然后为项目添加一源代码文件,输入以下程序。
程 序:
// Example 7-3:人事资料的输入和输出
#include <iostream.h>
#include,person.h”
void main()
{
void OutPersonData(Person);
char name[20],sex;
int age;
Person person1,person2;
cout <<,Enter a person’s name,age and sex:”;
cin >> name >> age >> sex;
person1.Register(name,age,sex);
person2.Register(“Zhang3”,19,‘m’);
cout <<,person1,\t”;
OutPersonData(person1);
cout <<,person2,\t”;
OutPersonData(person2);
person2 = person1;
cout <<,person3,\t”;
OutPersonData(person2);
}
void OutPersonData(Person person)
{
char name[20];
person.GetName(name);
cout << name << ‘\t’ << person.GetAge() << ‘\t’ << person.GetSex() << endl;
}
输 入:
Enter a person’s name,age and sex:Wang2 20 ‘f’
输 出:
Personl: Wang2 20 f
Person2: Zhang3 19 m
Person3: Wang2 20 f
7.3 构造函数与析构函数有几类特殊的成员函数,它们决定了类的对象如何创建、初始化、复制和撤消。这就是下面要介绍的构造函数和析构函数。
构造函数(Constructor)定义了创建对象的方法,提供了初始化对象的一种简便手段。构造函数的声明格式为
<类名> (<参数表>);
即构造函数与类同名,且没有返回值类型。构造函数既可在类外定义,也可作为内联函数在类内定义,也允许重载。
[例7-4] 为类Person增加构造函数。
程 序:
// Example 7-4:为类Person增加构造函数
#include <string.h>
class Person
{
Private:
char m_strName[20];
int m_nAge;
int m_nSex;
public:
person(const char *name,int age,char sex) //构造函数
{
strcpy(m_strName,name);
m_nAge = age;
m_nSex = (sex ==’m’?0:1);
}
void Register(char *Name,int Age,char Sex);
void GetName(char *Name);
int GetAge();
char GetSex();
};
在创建一个对象时,系统自动调用对象所属类的构造函数。如果定义了带有参数的构造函数,就可以在声明对象时利用实参对对象进行初始化。例如,当遇到声明
Person personl(“Zhang3”,19,‘f’);
时,编译器就调用构造函数
Person,,Person(const char *,int,char);
来创建对象person1并用实参初始化其数据成员。注意,遇到带有关键字extern修饰的外部对象声明时不调用构造函数,因为该声明属于对外部已经声明过的对象的引用性声明,这一点和外部变量的声明相同。
对于不需通过参数初始化的成员变量,也可以使用下面的形式进行初始化:
<类名>::<构造函数>(<参数表>):<变量1>(<初值1>),…,<变量n>(<初值n>)
{
… …
}
例如
MyClass::MyClass(),m_bAlready(FALSE),m_nCount(0)
{
}
对象具有变量的性质,一个类的对象也可以作为另一个类的数据成员。对象成员的初始化也可通过上述方法进行,其实质是对其构造函数的调用。
应当说明的是,如果在类中没有定义任何构造函数的话,系统会为自动地为它定义一个形如
<类名>:,<类名>();
的缺省构造函数。这种不带任何参数且函数体为空的构造函数就叫作缺省的构造函数。如果在类声明中已经声明、定义了构造函数,则系统不再提供缺省的构造函数。
与构造函数相对应,析构函数(Destructor)用于撤消一个对象。析构函数的声明格式为:
<类名>::~<类名>();
当一个对象的生存期结束时,系统将自动地调用析构函数来撤消该对象,返还它所占据的内存空间。
定义析构函数时应注意:
1.析构函数名与类名相同,只是它的前边须冠以波浪号“~”以与构造函数区别开来。
2.析构函数不得带有任何参数,即其参数表必须为空,即使关键字void也不允许有。因此,析构函数不能重载。
3.析构函数不得返回任何值,即使关键字void也不允许有。
在析构函数中不得调用C++的库函数exit(),这是因为该函数会做一些清理工作,包括撤消对象,困此,在析构函数中调用该函数时,该函数为撤消对象又会调用析构函数,从而形成无限递归。如果必须在析构函数中终止整个程序的运行,则应调用库函数abort ()。
析构函数一般用来作一些销毁对象前的扫尾工作,如用delete运算符释放动态分配的存储等。
与构造函数一样,如果在类的声明中没有显式地定义析构函数,则系统将自动产生一个形如:
<类名>::~<类名>()
{
}
的缺省析构函数,其函数体为空。
7.4 对象与指针
可以声明指向对象的指针,方法与声明指向变量的指针相同。例如
Person personl(“Zhang3”,19,‘f’);
Person *ptr = &person1;
通过指针访问对象的成员要用运算符“->”,例如:
ptr->GetAge();
在第6单元中介绍的new运算符可以用来动态建立一个对象,如
Person *pPerson = new Person;
当然,用new建立的对象要用delete释放:
delete pPerson;
用new建立一个对象时,可调用类的构造函数。例如
Person *pPerson = new Person(“Zhang3”,19,‘f’);
当对象接收到一个消息时,便会调用相应的成员函数。这时,系统会向该成员函数传递一个隐含的参数,即指向该对象自身的指针,即this指针。一般来说,this指针用途不大,因为在成员函数中可以直接使用本对象内部的所有成员变量和成员函数。但在某些场合中调用其他类的成员函数时,可能需要传送本对象的地址。在Windows程序设计中这种情况很多,如第8单元中,说明设备环境变量时就需要用到窗口对象的this指针。
除了指向对象的指针外,也可以说明对对象的引用。使用对象指针和对象引用的主要目的是为了在函数间传递对象时提高程序的运行效率。
自学内容
7.5 const对象与const成员函数
与普通变量一样,可使用关键字const修饰对象。C++规定,对于const对象,只能访问其中也用const修饰的成员函数,即const成员函数。 C++规定,在const成员函数中不得修改类中的任何数据成员的值。例如
class MyClass
{
int x;
public:
MyClass(int a = 0),x(a)
{
}
int NormalFunc()
{
return ++x;
}
int ConstFunc() const
{
return x+1;
}
};
其中成员函数ConstFunc()就是const成员函数。请注意修饰符const的位置。在其他地方(如主函数中)声明了一个MyClass类的const对象:
const MyClass ConstObj(3);
则调用
int i = ConstObj.ConstFunc();
合法,而调用
int j = ConstObj.NormalFunc();
非法,会导致编译错误。但是,如果声明一个MyClass类的普通对象,则无论成员函数是否为const均可调用。因此,如果一个类的对象可能被声明为const对象,则应将不改动数据成员的那些成员函数声明为const的。
7.6 MFC的CString类
Microsoft提供了一个基础类库MFC(Microsoft Foundation Class),其中包含许多用来开发C++应用程序的类。CString类和下节要介绍的CTime类、CTimeSpan类,以及第8单元要介绍的CFile类都在MFC中。
CString类提供了非常丰富的字符串操作,比第3单元介绍的字符型数组及字符串处理函数要方便得多。CString对象的字符串的长度是可变的,如果在程序中改变了字符串的内容,CSting类会自动调整所需的内存。因此,使用CSting类要比使用简单的字符型数组安全。
CString类的声明放在afx.h里,所以如果要使用该类,应在源程序前加上文件包含预处理命令:
#include <afx.h>
CString是MFC中已定义好的类,可在程序中直接声明CString类的对象。例如
CString name,comment;
CString类的数据成员均为私有成员,作为CString类对象的使用者,无需关心其具体设置。就使用CString类的对象而言,只需注意其方法(成员函数)和运算即可。
CString类的特色之一是可将一些常用运算符直接用于于其对象。可用于CString对象的运算符有:
= // 给CString对象赋一个新值
+ // 连接两个字符串并返回一个新字符串
+= // 把一个新字符串连接到一个已经存在的字符串的末端
>,<,==,>=,<=,!= // 各种比较运算(大小写敏感)
[] // 将CSting对象看作数组,取指定位置的字符
CString类的成员函数很多,这里只介绍其中最常用的一些。关于CString类的详细说明,可使用Developer Studio的帮助参看MSDN中的有关资料。
1.截取字符串的一部分构成新字符串
CString Mid(int nFirst,int nCount) const;
CString Left(int nCount) const;
CString Right(int nCount) const;
这3个成员函数均为const成员函数,用法基本相同。Mid()用于取字符串中从nFirst位置开始的nCount个字符构造新串(注意串中第一个字符的位置为0);Left()函数用于取字符串的左端nCount个字符构造新串,而Right()函数用于取字符串的右端nCount个字符构造新串。
2.查看字符串信息
TCHAR GetAt(int nIndex) const; // 返回指定位置的字符
int GetLength( ) const; // 返回字符串中的字符数
BOOL IsEmpty( ) const; // 测试是否空字符串
int Find(TCHAR ch) const; // 返回指定字符在串中位置
int Find(LPCTSTR lpszSub) const; // 返回指定子字符串在串中位置注意最后两个成员函数为重载的查找函数,前者用于在CString对象中查找一个字符,如果成功则给出该字符的位置,否则返回(1;后者用于在CString对象中查找一个字符串。
3.转换字符串
void MakeUpper( ); // 将字符串中所有字符换成大写
void MakeLower( ); // 将字符串中所有字符换成小写
void MakeReverse( ); // 将字符串中各字符的顺序倒转
void Empty( ); // 将字符串中所有字符删除
4.修改字符串的内容
void SetAt( int nIndex,TCHAR ch );
int Insert( int nIndex,TCHAR ch )
int Delete( int nIndex,int nCount = 1 )
int Replace( TCHAR chOld,TCHAR chNew );
int Replace( LPCTSTR lpszOld,LPCTSTR lpszNew );
这组成员函数用于修改字符串的内容。其中SetAt()用于替换指定位置上的字符;Insert()用于在指定位置添加一个字符;Delete()用于删除指定位置上的一个或多个字符。在使用这些函数时要注意,位置nIndex必须小于字符串的长度。最后两个重载的成员函数Replace()分别用于替换字符串中的字符或子字符串。
5.格式化输出
void Format(LPCTSTR lpszFormat,..,);
该成员函数用于根据格式lpszFormat,用其他数据构造一个字符串。其中省略号“…”是输出参数表,每个参数可以是一个变量或表达式。例如
int x = 0,double y = 0.36;
CString s;
s.Format(“Variable x = %d,y = %lf”,x,y);
结果是CString对象(字符串)s的内容为“Variable x = 0,y = 0.36”。参数lpszFormat 称为格式字符串,由要输出的文字和数据格式说明组成。文字说明中除了可以使用字母、数字、空格和一些数学符号以外,还可以使用一些转义字符表示特殊的含义。转义字符以反斜杠“\”开头,后面跟一个字符或者3位8进制数。常用的转义字符见表7-1。
如果除了字符串之外还要输出一些数据,则在格式字符串中还要包含对数据格式的说明。数据格式说明以百分号“%”开头,格式为
%<数据输出宽度说明><格式符>
其中常用的格式说明符见表7-2。
表7-1,常用的转义字符转义字符
含 义
\n
\r
\t
\f
\b
\\
\'
\"
\0
\nnn
换行符回车符制表符换页符退格符反斜杠单引号双引号
0
码值为nnn的ASCII码,nnn表示3位8进制数
表7-2 常用的格式说明符格式符
含 义
c
d
ld
u
lu
x
f
lf
g
e
s
%
输出数据为字符型输出数据为整数输出数据为长整数输出数据为无符号整数输出数据为无符号长整数按16进制格式输出整型数据输出数据为浮点型输出数据为双精度型选用%f或%e格式中输出宽度较短的一种格式,不输出无意义的0
按指数形式输出浮点数据输出数据为字符串数组此处输出一个“%”号
其中数据输出宽度说明可以没有,这时表示按数据的实际数值输出。如果规定了输出宽度,则在该宽度范围内数值数据(如各种整型和浮点型数据)输出值向右对齐,字符串向左对齐。如果输出数据是浮点型或者双精度型,还可以定义输出时的小数位数,例如%8.2f表示输出数据共占8位,其中小数部分占2位,小数点1位,符号和整数部分占5位。数据格式说明要和后面的输出数据项一一对应,即第一个数据格式说明对应第一个要输出的数据,第二个格式说明符要对应第二个要输出的数据,以此类推。
C++的数据格式说明是相当复杂的,上面所举仅其大概,更详细的说明可以参看MSDN中的有关说明。
参数“...”表示此处有若干个数据需要输出。这里的数据的个数和类型必须与格式字符串中的数据格式说明项按顺序一一对应。需要输出的数据也可以是一个表达式或者函数调用,表示直接输出计算结果或函数值。
我们在第3单元中介绍了用字符型数组存放文字信息的方法,以及字符串处理库函数。其实,这些内容都是C++从C语言那里继承下来的,称为零结尾字符数组。由于零结尾字符数组结构简单,应用广泛,所以在C++程序中的应用仍然很多。因此,Microsoft在设计MFC时也考虑了CString类与零结尾字符串的兼容性。表现在如果某函数的参数被说明为LPCTSTR,或const char *类型,则既可以使用零结尾字符串作为实参,也可以使用CString对象作为实参。
如果我们正在编写一个带字符串参数的函数,则可在设计时作出选择。下面是一些编程的规则:
如果函数不改变字符串的内容,而且您又愿意使用字符串处理库函数(如strcpy()等)的话,则可使用const char *参数;
如果函数不改变字符串的内容,但希望在函数内使用CString的成员函数,则可使用const CString &参数;
如果函数要改变字符串的内容,可使用CString &参数。
7.7 结构体类型
C++中有一个结构体类型,其声明和使用方法与类非常相似。结构体类型的声明方法如下:
struct <结构体类型名>
{
<结构体类型的成员变量声明语句表>
};
例如,可声明一个表示日期的结构体类型:
struct date
{
int da_year;
char da_mon;
char da_day;
};
即一个日期类型的变量有3个成员变量,年份(da_year)、月份(da_mon)和日(da_day)。
在声明好结构体类型以后,就可以声明该类型的变量了:
struct <结构体类型名> <变量表>;
注意在定义时不能忘记结构体类型说明符struct,这一点与类不同。例如变量声明语句
struct date yesterday,today,tomorrow;
声明了3个日期类型的变量,yesterday、today和tomorrow。
结构体类型的变量可用作函数的参数或者函数的返回值。对结构体类型变量的成员变量的引用方法与类相同:
<结构体类型变量名>.<成员变量名>
例如:
today.da_year = 2000;
today.da_mon = 3;
today.da_day = 21;
实际上,结构体类型是C++从C语言那里继承下来的内容。C语言的结构体类型比较简单,只有成员变量。而C++的结构体类型和类一样,可以有数据成员,也可以有成员函数。在C++中,结构体类型与类的唯一区别是,如果不明确声明,则类的成员均为私有的;而结构体类型的成员在没有明确声明的情况下均为公有的。
在C++中,使用结构体类型主要是为了兼容一些从C语言继承下来的函数库。
调试技术
7.8 如何在程序中使用MFC类库如果要在程序中使用CString等MFC类,除了要在程序首部加上文件包含命令:
#include <afx.h>
外,还需在Developer Studio的菜单选项“project/Settings…”的“General”选项卡设置“Microsoft Foundation Classes”项。可选项有三种,分别为“Not Using MFC(不使用MFC)”、“Use MFC in a Shared DLL(以动态链接库方式用MFC)”和“Use MFC in a Static Library(以静态库方法使用MFC)”,后两种选项均可使用。
程序设计举例
[例7-5] 声明一个职工档案类。
程 序:
// Example 7-5:声明职工档案类
#include <afx.h>
// 声明工资类
class Salary
{
float m_fWage; // 基本工资
float m_fSubsidy; // 岗位津贴
float m_fInsurance; // 劳保福利
float m_fRent; // 房租
float m_fCostOfElec; // 电费
float m_fCostOfWater; // 水费
float m_fCostOfHeating; // 取暖费
public:
void SetWage(float fWage){ // 填写基本工资
m_fWage = fWage;
}
void SetSubsidy(float fSubsidy){ // 填写岗位津贴
m_fSubsidy = fSubsidy;
}
void SetInsurance(float fInsurance){ // 填写劳保福利
m_fInsurance = fInsurance;
}
void SetCostOfWater(float fCostOfWater){ // 填写水费
m_fCostOfWater = fCostOfWater;
}
void SetCostOfElec(float fCostOfElec){ // 填写电费
m_fCostOfElec = fCostOfElec;
}
void SetCostOfHeating(float fCostOfHeating){ // 填写取暖费
m_fCostOfHeating = fCostOfHeating;
}
float GetWage(){ // 查看基本工资
return m_fWage;
}
float GetSubsidy(){ // 查看岗位津贴
return m_fSubsidy;
}
float GetInsurance(){ // 查看劳保福利
return m_fInsurance;
}
float GetCostOfWater(){ // 查看水费
return m_fCostOfWater;
}
float GetCostOfElec(){ // 查看电费
return m_fCostOfElec;
}
float GetCostOfHeating(){ // 查看取暖费
return m_fCostOfHeating;
}
float RealSum(){ // 计算实发工资
return m_fWage + m_fSubsidy+m_fInsurance
-m_fRent-m_fCostOfElec-m_fCostOfWater-m_fCostOfHeating;
}
};
// 定义职务类型
enum Position
{
MANAGER, // 经理
ENGINEER, // 工程师
EMPLOYEE, // 职员
WORKER // 工人
};
// 声明档案类
class Employee
{
CString m_sDepartment; // 工作部门
CString m_sName; // 姓名
CTime m_tBirthdate; // 出生日期
Position m_nPosition; // 职务
CTime m_tDateOfWork; // 参加工作时间
Salary m_Salary; // 工资
public:
Employee(LPCTSTR lpszDepart,LPCTSTR lpszName,CTime tBirthdate,
Position nPosition,CTime tDateOfWork); // 构造函数
void SetWage(float fWage){ // 填写基本工资
m_Salary.SetWage(fWage);
}
void SetSubsidy(float fSubsidy){ // 填写岗位津贴
m_Salary.SetSubsidy(fSubsidy);
}
void SetInsurance(float fInsurance){ // 填写劳保福利
m_Salary.SetInsurance(fInsurance);
}
void SetCostOfWater(float fCostOfWater){ // 填写水费
m_Salary.SetCostOfWater(fCostOfWater);
}
void SetCostOfElec(float fCostOfElec){ // 填写电费
m_Salary.SetCostOfElec(fCostOfElec);
}
void SetCostOfHeating(float fCostOfHeating){ // 填写取暖费
m_Salary.SetCostOfHeating(fCostOfHeating);
}
float RealSalary(){ // 查看实发工资
return m_Salary.RealSum();
}
void ShowMessage(); // 打印职工信息
};
// 职工档案类的成员函数定义
Employee::Employee(LPCTSTR lpszDepart,LPCTSTR lpszName,
CTime tBirthdate,Position nPosition,CTime tDateOfWork)
{
m_sDepartment = lpszDepart;
m_sName = lpszName;
m_tBirthdate = tBirthdate;
m_nPosition = nPosition;
m_tDateOfWork = tDateOfWork;
}
void Employee::ShowMessage()
{
cout <<,Depart:,<< m_sDepartment << endl;
cout <<,Name:,<< m_sName << endl;
cout <<,Birthdate:,<< m_tBirthdate.Format(“%B %d,%Y”) << endl;
switch(m_nPosition)
{
case MANAGER:
cout <<,Position:,<<,MANAGER” <<endl;
break;
case ENGINEER:
cout <<,Position:,<<,ENGINEER” <<endl;
break;
case EMPLOYEE:
cout <<,Position:,<<,EMPLOYEE” <<endl;
break;
case WORKER:
cout <<,Position:,<<,WORKER” <<endl;
break;
}
cout <<,Date of Work:,<< m_tDateOfWork.Format(“%B %d,%Y”) << endl;
}
分 析,由于CString和CTime等MFC类的声明在头文件<afx.h>中,所以需要用文件包含命令将其包含在程序中。同样,在声明了工资类以后,就可以在声明职工档案类时使用工资类的对象作为成员变量。
由于工资类和职工档案类的所有数据成员均为私有成员(缺省状态),只能由其成员函数访问。因此,我们为这两个类编写了若干成员函数以访问它们的数据成员。由于大多数成员函数的操作都很简单,所以采用了内联方式编写。
声明职工档案类后,就可以用其声明整个企业的职工档案:
// 声明职工档案数组
#define MAX_EMPLOYEE 1000
int EmpCount;
Employee EmployeeList[MAX_EMPLOYEE];
职工档案数组EmployeeList共有MAX_EMPLOYEE个元素,每个元素可以用来存放一个职工的档案。当然,某个企业的职工人数可能发生变化,所以我们另外设置了一个变量EmpCount,用来存放实际的职工人数。
建立职工档案可以使用Employee类的构造函数实现,例如:
CTime birthdate = CTime(1974,10,2,0,0,0);
CTime date_of_work = CTime(1999,12,19,0,0,0);
EmployeeList[EmpCount] = Employee(“办公室”,“张三”,birthdate,
EMPLOYEE,date_of_work);
EmpCount++;
单元上机练习题目
1.现实世界中的物理实体和逻辑概念均可以看作是对象。分析你周围的一个对象,试用C++ 类来描述它。
2.编程验证CString类,CTime类和CTimeSpan类各成员函数的使用方法。
3,为一个小型图书馆的管理系统声明图书卡类、借书证类和借书记录类,设计其成员函数,包括书卡录入函数和借书证录入函数、借书登记函数和还书处理函数等。设计好的类可自编主函数验证之。
本单元教学目标介绍面向对象程序设计方法的基本原理以及类和对象的概念。
教学要求掌握面向对象的程序设计思想,类和对象的概念,以及类的声明方法和对象的引用。
授课内容
7.1 面向对象的程序设计在面向对象的程序设计技术(OOP,Object Oriented Programming)出现前,程序员们一般采用面向过程的程序设计方法。面向过程的程序设计方法采用函数(或过程)来描述对数据结构的操作,但又将函数与其所操作的数据分离开来。作为对现实世界的抽象,函数和它所操作的数据是密切相关、相互依赖的:特定的函数往往要对特定的数据结构进行操作;如果数据结构发生改变,则必须改写相应的函数。这种实质上的依赖与形式上的分离使得用面向过程的程序设计方法编写出来的大程序不但难于编写,而且难于调试和修改。
面向对象程序设计从所处理的数据入手,以数据为中心而不是以功能为中心来描述系统。数据相对于功能而言具有更强的稳定性。面向对象程序设计与结构化程序设计最大的区别就在于,前者首先关心的是所要处理的数据,而后者首先关心的是功能。
面向对象程序设计是一种围绕真实世界的概念来组织模型的程序设计方法,它采用对象来描述问题空间中的实体。关于对象这一概念,目前还没有统一的定义。一般的认为,对象是包含现实世界物体特征的抽象实体,反映了系统为之保存信息和(或)与之交互的能力。对象是一些属性及服务的封装体,在程序设计领域,可以用“对象=数据+作用于这些数据上的操作”这一公式来表达。
类是具有相同操作功能和相同的数据格式(属性)的对象的集合,可以看作抽象数据类型的具体实现。从外部看,类的行为可以用新定义的操作(方法)加以规定。类是对象集合的抽象,规定了这些对象的公共属性和方法;对象是类的一个实例。例如,苹果是一个类,而放在桌上的那个苹果则是一个对象。对象和类的关系相当于一般的程序设计语言中变量和变量类型的关系。
消息是向某对象请求服务的一种表达方式。对象内有方法和数据,外部的用户或对象对该对象提出的服务请求,可以称为向该对象发送消息。
面向对象的编程方法具有四个基本特征:
1,抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,忽略与主题无关的细节。例如,在设计一个学生成绩管理系统的过程中,考察学生张三这个对象时,我们只关心他的班级、学号、成绩等,而他的身高、体重等信息就可以忽略。抽象包括两个方面,一是过程抽象,二是数据抽象。过程抽象是指任何一个明确定义功能的操作都可被使用者看作单个的实体看待,尽管这个操作实际上可能由一系列更低级的操作来完成。数据抽象定义了数据类型和施加于该类型对象上的操作,并限定了对象的值只能通过使用这些操作修改和观察。
2,封装:封装是面向对象的特征之一,是对象和类概念的主要特性。封装把过程和数据封藏起来,对数据的访问只能通过已定义的界面。面向对象技术的基本概念就是现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象。一旦定义了一个对象的特性,则有必要决定这些特性的可见性,即哪些特性对外部世界是可见的,哪些特性用于表示内部状态。通常,应禁止直接访问一个对象的实际表示,只能通过操作接口访问对象,这称为信息隐藏。事实上,信息隐藏是用户对封装性的认识,封装则为信息隐藏提供支持。封装保证了模块具有较好的独立性,使得程序维护修改较为容易。对应用程序的修改仅限于类的内部,因而可以将应用程序修改带来的影响减少到最低限度。
3,继承:继承是一种联结类与类的层次模型。继承允许和鼓励类的重用,提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原来类的特性,新类称为原来类的派生类(子类),而原来类称为新类的基类(父类)。派生类可以从其基类那里继承方法和成员变量,当然也可以对之进行修改或增加新的方法使之更适合特殊的需要。这也体现了大自然中一般与特殊的关系。继承性很好地解决了软件的可重用性问题。
4,多态性:多态性是指允许不同类的对象对同一消息作出响应。例如同样的加法,把两个时间加在一起和把两个整数加在一起的内涵肯定完全不同。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题。
面向对象程序设计具有许多优点:开发时间短,效率高,可靠性高,所开发的程序更强壮。由于面向对象编程的编码可重用性,可以在应用程序中大量采用成熟的类库,从而缩短了开发时间,使应用程序更易于维护、更新和升级。继承和封装使得应用程序的修改带来的影响更加局部化。
7.2 类与对象
7.2.1 类的声明
C++类的结构比较复杂,可以将其看成是一种既包含数据又包含函数的数据类型。显然,描述不同编程对象的类应有不同的内部结构,所以在使用类来声明对象前应先说明其结构。声明一个类的一般形式为:
class <类名>
{
private:
<成员表>;
public:
<成员表>;
protected:
<成员表>;
… …
};
其中关键字c1ass指出下面要声明的是一个类,<类名>是程序员为所声明的类起的名字;<成员表>由一个个该类成员的声明组成,这些成员可以是变量(数据成员),也可以是函数(成员函数);关键字private引出的成员叫做私有成员,对私有成员的访问权限制在该类的内部,即只允许该类中的成员函数访问。由于类成员被默认为私有的,所以该关键字可以省略;关键字public引出的成员叫做公有成员,公有成员则允许该类以外的函数访问;而关键字protected引出的成员叫做保护成员,保护成员的访问权限将在第8单元介绍。
[例7-1] 声明一个Person类,用来说明人类对象。
说 明:将Person类的声明放在头文件中。为项目添加头文件的方法与添加源代码文件的方法类似,建立项目后,使用Developer Studio的菜单选项File/New…,在File选项卡中选择C/C++ Header File项,并在对话框右面的File框中填写文件名(可与项目名相同),然后按OK键即可生成一空白头文件。然后输入如下代码。
程 序:
// Example 7-1(Person.h):声明Person类
// 类 Person 的声明
class Person
{
private:
char m_strName[20];
int m_nAge;
int m_nSex;
public:
void Register(char *Name,int Age,char Sex);
void GetName(char *Name);
int GetAge();
char GetSex();
};
作为一个人,可能有许多特征:姓名、性别、年龄、身高、体重、民族、学历、职业…,类Person体现了对人的抽象,它集中了人所具有的共性。同时,在该类中还说明了对一个抽象人的属性进行操作的方法:登录一个人的信息的函数Register();获取一个人的信息的函数GetName()、GetAge()和GetSex()。该例中,将数据成员声明为私有的以阻止外界对它们的随意访问,而成员函数则声明为公有的,它们便是外界访问类中数据成员的统一接口。本例中的private关键字可以省略。另外,在类的声明中,不同访问权限的成员的书写顺序可以任意排列。在这种情况下,要注意使用关键字private,不能随便省略。
在Visual C++程序中,在声明类的数据成员时,通常在其名前加上前缀“m_”,以区别于普通的变量名。
在声明一个类时应注意:
1.类中任何成员都不得用关键字extern,auto和register进行修饰。
2.不得在类声明中对数据成员使用表达式进行初始化。
7.2.2 成员函数的定义从例7-1中可以看出,在类的声明中仅给出了成员函数的原型,函数的定义还需在其他地方(通常每个类的成员函数的定义放在一起构成一个源程序文件)给出。类的成员函数的一般形式为:
<类型> <类名>,,<函数名> (<参数表>)
{
<函数体>
}
其中作用域运算符“::”指出成员函数的类属。没有类属的函数称为公共函数,在前面各单元中用到的函数均为公共函数。
[例7-2] Person类成员函数的声明。
说 明:按例7-1说明的方法添加头文件,输入Person类的声明。然后为项目添加一源代码文件,输入以下程序。
程 序:
// Example 7-2(Person.cpp):Person类成员函数的定义
#include <string.h>
#include,person.h”
void Person:,Register(const char *name,int age,char sex)
{
strcpy(m_strName,name);
m_nAge = age;
m_nSex = (sex = = ‘m’?0:1);
}
void Person::GetName(char *name)
{
strcpy(name,m_strName);
}
int Person:,GetAge()
{
return m_nAge;
}
char Person:,GetSex()
{
return (m_nSex = = 0?’m’:’f’);
}
类中的成员函数有时很简单,这样的函数最适合写成内联成员函数以提高程序的执行效率。对于成员函数来说,除了可以采用关键词inline将其声明为内联函数外,还有一种更简单的方法,就是在类的声明中直接定义成员函数的函数体,这样的函数自动成为内联成员函数。例如,可将类Person的成员函数声明为内联成员函数:
class Person
{
private:
char m_strName[20];
int m_nAge;
int m_nSex;
public:
void Register(const char *name,int age,char sex)
{ strcpy(m_strName,name);
m_nAge = age;
m_nSex = (sex = = ‘m’?0:1);
}
void GetName(char *name)
{ strcpy(name,m_strName);
}
int GetAge()
{ return m_nAge;
}
char GetSex()
{ return (m_nSex = = 0?’m’:’f’);
}
};
类的成员函数与普通函数一样,可以重载,也可以带有缺省参数。
7.2.3 公有成员和私有成员为了实现既要隐藏数据,又要为外界使用数据提供接口的封装目的,通常是将类中的数据成员声明为私有的而将成员函数声明为公有的。然而,在设计一个具体类时,各成员的访问权限还应根据实际需要而定。一般说来,将类中的数据成员声明成公有的将难以保证该成员的安全性,而将类的中成员函数声明成私有时还是非常必要的。例如,类中的一个只供其成员函数调用的成员函数就应当声明为私有的。
习惯上将具有全局作用域的类声明放在一个头文件中,将成员函数的定义放在一个另一个源程序文件中,以提高编程的灵活性。
7.2.4 对象对象是类的实例。从技术上讲,一个对象就是一个具有某种类类型的变量。与普通变量一样,对象也必须先经声明才可以使用。声明一个对象的一般形式为:
<类名> <对象1>,<对象2>,…
例如语句
Person person1,person2;
声明了两个名为personl和person2的Person类的对象。
在程序中使用一个对象,通常是通过对体现对象特征的数据成员的操作实现的。当然,由于封装性的要求,这些操作又是通过对象的成员函数实现的。具体来说:
1.成员函数访问同类中的数据成员,或调用同类中的其他成员函数,可直接使用数据成员名或成员函数名,可参看例7-2中各成员函数的定义;
2.在对象外访问其数据成员或成员函数需使用运算符“.”访问对象的成员,例如
nAge = person1.GetAge();
3.直接访问一个对象中的私有成员则属于非法操作,将导致编译错误;
4.同类对象之间可以整体赋值。例如
person1 = person2;
5.对象用作函数的参数时属于赋值调用;函数可以返回一个对象。
[例7-3] 人事资料的输入输出。
说 明:按例7-1说明的方法添加头文件,输入Person类的声明。然后为项目添加一源代码文件,输入以下程序。
程 序:
// Example 7-3:人事资料的输入和输出
#include <iostream.h>
#include,person.h”
void main()
{
void OutPersonData(Person);
char name[20],sex;
int age;
Person person1,person2;
cout <<,Enter a person’s name,age and sex:”;
cin >> name >> age >> sex;
person1.Register(name,age,sex);
person2.Register(“Zhang3”,19,‘m’);
cout <<,person1,\t”;
OutPersonData(person1);
cout <<,person2,\t”;
OutPersonData(person2);
person2 = person1;
cout <<,person3,\t”;
OutPersonData(person2);
}
void OutPersonData(Person person)
{
char name[20];
person.GetName(name);
cout << name << ‘\t’ << person.GetAge() << ‘\t’ << person.GetSex() << endl;
}
输 入:
Enter a person’s name,age and sex:Wang2 20 ‘f’
输 出:
Personl: Wang2 20 f
Person2: Zhang3 19 m
Person3: Wang2 20 f
7.3 构造函数与析构函数有几类特殊的成员函数,它们决定了类的对象如何创建、初始化、复制和撤消。这就是下面要介绍的构造函数和析构函数。
构造函数(Constructor)定义了创建对象的方法,提供了初始化对象的一种简便手段。构造函数的声明格式为
<类名> (<参数表>);
即构造函数与类同名,且没有返回值类型。构造函数既可在类外定义,也可作为内联函数在类内定义,也允许重载。
[例7-4] 为类Person增加构造函数。
程 序:
// Example 7-4:为类Person增加构造函数
#include <string.h>
class Person
{
Private:
char m_strName[20];
int m_nAge;
int m_nSex;
public:
person(const char *name,int age,char sex) //构造函数
{
strcpy(m_strName,name);
m_nAge = age;
m_nSex = (sex ==’m’?0:1);
}
void Register(char *Name,int Age,char Sex);
void GetName(char *Name);
int GetAge();
char GetSex();
};
在创建一个对象时,系统自动调用对象所属类的构造函数。如果定义了带有参数的构造函数,就可以在声明对象时利用实参对对象进行初始化。例如,当遇到声明
Person personl(“Zhang3”,19,‘f’);
时,编译器就调用构造函数
Person,,Person(const char *,int,char);
来创建对象person1并用实参初始化其数据成员。注意,遇到带有关键字extern修饰的外部对象声明时不调用构造函数,因为该声明属于对外部已经声明过的对象的引用性声明,这一点和外部变量的声明相同。
对于不需通过参数初始化的成员变量,也可以使用下面的形式进行初始化:
<类名>::<构造函数>(<参数表>):<变量1>(<初值1>),…,<变量n>(<初值n>)
{
… …
}
例如
MyClass::MyClass(),m_bAlready(FALSE),m_nCount(0)
{
}
对象具有变量的性质,一个类的对象也可以作为另一个类的数据成员。对象成员的初始化也可通过上述方法进行,其实质是对其构造函数的调用。
应当说明的是,如果在类中没有定义任何构造函数的话,系统会为自动地为它定义一个形如
<类名>:,<类名>();
的缺省构造函数。这种不带任何参数且函数体为空的构造函数就叫作缺省的构造函数。如果在类声明中已经声明、定义了构造函数,则系统不再提供缺省的构造函数。
与构造函数相对应,析构函数(Destructor)用于撤消一个对象。析构函数的声明格式为:
<类名>::~<类名>();
当一个对象的生存期结束时,系统将自动地调用析构函数来撤消该对象,返还它所占据的内存空间。
定义析构函数时应注意:
1.析构函数名与类名相同,只是它的前边须冠以波浪号“~”以与构造函数区别开来。
2.析构函数不得带有任何参数,即其参数表必须为空,即使关键字void也不允许有。因此,析构函数不能重载。
3.析构函数不得返回任何值,即使关键字void也不允许有。
在析构函数中不得调用C++的库函数exit(),这是因为该函数会做一些清理工作,包括撤消对象,困此,在析构函数中调用该函数时,该函数为撤消对象又会调用析构函数,从而形成无限递归。如果必须在析构函数中终止整个程序的运行,则应调用库函数abort ()。
析构函数一般用来作一些销毁对象前的扫尾工作,如用delete运算符释放动态分配的存储等。
与构造函数一样,如果在类的声明中没有显式地定义析构函数,则系统将自动产生一个形如:
<类名>::~<类名>()
{
}
的缺省析构函数,其函数体为空。
7.4 对象与指针
可以声明指向对象的指针,方法与声明指向变量的指针相同。例如
Person personl(“Zhang3”,19,‘f’);
Person *ptr = &person1;
通过指针访问对象的成员要用运算符“->”,例如:
ptr->GetAge();
在第6单元中介绍的new运算符可以用来动态建立一个对象,如
Person *pPerson = new Person;
当然,用new建立的对象要用delete释放:
delete pPerson;
用new建立一个对象时,可调用类的构造函数。例如
Person *pPerson = new Person(“Zhang3”,19,‘f’);
当对象接收到一个消息时,便会调用相应的成员函数。这时,系统会向该成员函数传递一个隐含的参数,即指向该对象自身的指针,即this指针。一般来说,this指针用途不大,因为在成员函数中可以直接使用本对象内部的所有成员变量和成员函数。但在某些场合中调用其他类的成员函数时,可能需要传送本对象的地址。在Windows程序设计中这种情况很多,如第8单元中,说明设备环境变量时就需要用到窗口对象的this指针。
除了指向对象的指针外,也可以说明对对象的引用。使用对象指针和对象引用的主要目的是为了在函数间传递对象时提高程序的运行效率。
自学内容
7.5 const对象与const成员函数
与普通变量一样,可使用关键字const修饰对象。C++规定,对于const对象,只能访问其中也用const修饰的成员函数,即const成员函数。 C++规定,在const成员函数中不得修改类中的任何数据成员的值。例如
class MyClass
{
int x;
public:
MyClass(int a = 0),x(a)
{
}
int NormalFunc()
{
return ++x;
}
int ConstFunc() const
{
return x+1;
}
};
其中成员函数ConstFunc()就是const成员函数。请注意修饰符const的位置。在其他地方(如主函数中)声明了一个MyClass类的const对象:
const MyClass ConstObj(3);
则调用
int i = ConstObj.ConstFunc();
合法,而调用
int j = ConstObj.NormalFunc();
非法,会导致编译错误。但是,如果声明一个MyClass类的普通对象,则无论成员函数是否为const均可调用。因此,如果一个类的对象可能被声明为const对象,则应将不改动数据成员的那些成员函数声明为const的。
7.6 MFC的CString类
Microsoft提供了一个基础类库MFC(Microsoft Foundation Class),其中包含许多用来开发C++应用程序的类。CString类和下节要介绍的CTime类、CTimeSpan类,以及第8单元要介绍的CFile类都在MFC中。
CString类提供了非常丰富的字符串操作,比第3单元介绍的字符型数组及字符串处理函数要方便得多。CString对象的字符串的长度是可变的,如果在程序中改变了字符串的内容,CSting类会自动调整所需的内存。因此,使用CSting类要比使用简单的字符型数组安全。
CString类的声明放在afx.h里,所以如果要使用该类,应在源程序前加上文件包含预处理命令:
#include <afx.h>
CString是MFC中已定义好的类,可在程序中直接声明CString类的对象。例如
CString name,comment;
CString类的数据成员均为私有成员,作为CString类对象的使用者,无需关心其具体设置。就使用CString类的对象而言,只需注意其方法(成员函数)和运算即可。
CString类的特色之一是可将一些常用运算符直接用于于其对象。可用于CString对象的运算符有:
= // 给CString对象赋一个新值
+ // 连接两个字符串并返回一个新字符串
+= // 把一个新字符串连接到一个已经存在的字符串的末端
>,<,==,>=,<=,!= // 各种比较运算(大小写敏感)
[] // 将CSting对象看作数组,取指定位置的字符
CString类的成员函数很多,这里只介绍其中最常用的一些。关于CString类的详细说明,可使用Developer Studio的帮助参看MSDN中的有关资料。
1.截取字符串的一部分构成新字符串
CString Mid(int nFirst,int nCount) const;
CString Left(int nCount) const;
CString Right(int nCount) const;
这3个成员函数均为const成员函数,用法基本相同。Mid()用于取字符串中从nFirst位置开始的nCount个字符构造新串(注意串中第一个字符的位置为0);Left()函数用于取字符串的左端nCount个字符构造新串,而Right()函数用于取字符串的右端nCount个字符构造新串。
2.查看字符串信息
TCHAR GetAt(int nIndex) const; // 返回指定位置的字符
int GetLength( ) const; // 返回字符串中的字符数
BOOL IsEmpty( ) const; // 测试是否空字符串
int Find(TCHAR ch) const; // 返回指定字符在串中位置
int Find(LPCTSTR lpszSub) const; // 返回指定子字符串在串中位置注意最后两个成员函数为重载的查找函数,前者用于在CString对象中查找一个字符,如果成功则给出该字符的位置,否则返回(1;后者用于在CString对象中查找一个字符串。
3.转换字符串
void MakeUpper( ); // 将字符串中所有字符换成大写
void MakeLower( ); // 将字符串中所有字符换成小写
void MakeReverse( ); // 将字符串中各字符的顺序倒转
void Empty( ); // 将字符串中所有字符删除
4.修改字符串的内容
void SetAt( int nIndex,TCHAR ch );
int Insert( int nIndex,TCHAR ch )
int Delete( int nIndex,int nCount = 1 )
int Replace( TCHAR chOld,TCHAR chNew );
int Replace( LPCTSTR lpszOld,LPCTSTR lpszNew );
这组成员函数用于修改字符串的内容。其中SetAt()用于替换指定位置上的字符;Insert()用于在指定位置添加一个字符;Delete()用于删除指定位置上的一个或多个字符。在使用这些函数时要注意,位置nIndex必须小于字符串的长度。最后两个重载的成员函数Replace()分别用于替换字符串中的字符或子字符串。
5.格式化输出
void Format(LPCTSTR lpszFormat,..,);
该成员函数用于根据格式lpszFormat,用其他数据构造一个字符串。其中省略号“…”是输出参数表,每个参数可以是一个变量或表达式。例如
int x = 0,double y = 0.36;
CString s;
s.Format(“Variable x = %d,y = %lf”,x,y);
结果是CString对象(字符串)s的内容为“Variable x = 0,y = 0.36”。参数lpszFormat 称为格式字符串,由要输出的文字和数据格式说明组成。文字说明中除了可以使用字母、数字、空格和一些数学符号以外,还可以使用一些转义字符表示特殊的含义。转义字符以反斜杠“\”开头,后面跟一个字符或者3位8进制数。常用的转义字符见表7-1。
如果除了字符串之外还要输出一些数据,则在格式字符串中还要包含对数据格式的说明。数据格式说明以百分号“%”开头,格式为
%<数据输出宽度说明><格式符>
其中常用的格式说明符见表7-2。
表7-1,常用的转义字符转义字符
含 义
\n
\r
\t
\f
\b
\\
\'
\"
\0
\nnn
换行符回车符制表符换页符退格符反斜杠单引号双引号
0
码值为nnn的ASCII码,nnn表示3位8进制数
表7-2 常用的格式说明符格式符
含 义
c
d
ld
u
lu
x
f
lf
g
e
s
%
输出数据为字符型输出数据为整数输出数据为长整数输出数据为无符号整数输出数据为无符号长整数按16进制格式输出整型数据输出数据为浮点型输出数据为双精度型选用%f或%e格式中输出宽度较短的一种格式,不输出无意义的0
按指数形式输出浮点数据输出数据为字符串数组此处输出一个“%”号
其中数据输出宽度说明可以没有,这时表示按数据的实际数值输出。如果规定了输出宽度,则在该宽度范围内数值数据(如各种整型和浮点型数据)输出值向右对齐,字符串向左对齐。如果输出数据是浮点型或者双精度型,还可以定义输出时的小数位数,例如%8.2f表示输出数据共占8位,其中小数部分占2位,小数点1位,符号和整数部分占5位。数据格式说明要和后面的输出数据项一一对应,即第一个数据格式说明对应第一个要输出的数据,第二个格式说明符要对应第二个要输出的数据,以此类推。
C++的数据格式说明是相当复杂的,上面所举仅其大概,更详细的说明可以参看MSDN中的有关说明。
参数“...”表示此处有若干个数据需要输出。这里的数据的个数和类型必须与格式字符串中的数据格式说明项按顺序一一对应。需要输出的数据也可以是一个表达式或者函数调用,表示直接输出计算结果或函数值。
我们在第3单元中介绍了用字符型数组存放文字信息的方法,以及字符串处理库函数。其实,这些内容都是C++从C语言那里继承下来的,称为零结尾字符数组。由于零结尾字符数组结构简单,应用广泛,所以在C++程序中的应用仍然很多。因此,Microsoft在设计MFC时也考虑了CString类与零结尾字符串的兼容性。表现在如果某函数的参数被说明为LPCTSTR,或const char *类型,则既可以使用零结尾字符串作为实参,也可以使用CString对象作为实参。
如果我们正在编写一个带字符串参数的函数,则可在设计时作出选择。下面是一些编程的规则:
如果函数不改变字符串的内容,而且您又愿意使用字符串处理库函数(如strcpy()等)的话,则可使用const char *参数;
如果函数不改变字符串的内容,但希望在函数内使用CString的成员函数,则可使用const CString &参数;
如果函数要改变字符串的内容,可使用CString &参数。
7.7 结构体类型
C++中有一个结构体类型,其声明和使用方法与类非常相似。结构体类型的声明方法如下:
struct <结构体类型名>
{
<结构体类型的成员变量声明语句表>
};
例如,可声明一个表示日期的结构体类型:
struct date
{
int da_year;
char da_mon;
char da_day;
};
即一个日期类型的变量有3个成员变量,年份(da_year)、月份(da_mon)和日(da_day)。
在声明好结构体类型以后,就可以声明该类型的变量了:
struct <结构体类型名> <变量表>;
注意在定义时不能忘记结构体类型说明符struct,这一点与类不同。例如变量声明语句
struct date yesterday,today,tomorrow;
声明了3个日期类型的变量,yesterday、today和tomorrow。
结构体类型的变量可用作函数的参数或者函数的返回值。对结构体类型变量的成员变量的引用方法与类相同:
<结构体类型变量名>.<成员变量名>
例如:
today.da_year = 2000;
today.da_mon = 3;
today.da_day = 21;
实际上,结构体类型是C++从C语言那里继承下来的内容。C语言的结构体类型比较简单,只有成员变量。而C++的结构体类型和类一样,可以有数据成员,也可以有成员函数。在C++中,结构体类型与类的唯一区别是,如果不明确声明,则类的成员均为私有的;而结构体类型的成员在没有明确声明的情况下均为公有的。
在C++中,使用结构体类型主要是为了兼容一些从C语言继承下来的函数库。
调试技术
7.8 如何在程序中使用MFC类库如果要在程序中使用CString等MFC类,除了要在程序首部加上文件包含命令:
#include <afx.h>
外,还需在Developer Studio的菜单选项“project/Settings…”的“General”选项卡设置“Microsoft Foundation Classes”项。可选项有三种,分别为“Not Using MFC(不使用MFC)”、“Use MFC in a Shared DLL(以动态链接库方式用MFC)”和“Use MFC in a Static Library(以静态库方法使用MFC)”,后两种选项均可使用。
程序设计举例
[例7-5] 声明一个职工档案类。
程 序:
// Example 7-5:声明职工档案类
#include <afx.h>
// 声明工资类
class Salary
{
float m_fWage; // 基本工资
float m_fSubsidy; // 岗位津贴
float m_fInsurance; // 劳保福利
float m_fRent; // 房租
float m_fCostOfElec; // 电费
float m_fCostOfWater; // 水费
float m_fCostOfHeating; // 取暖费
public:
void SetWage(float fWage){ // 填写基本工资
m_fWage = fWage;
}
void SetSubsidy(float fSubsidy){ // 填写岗位津贴
m_fSubsidy = fSubsidy;
}
void SetInsurance(float fInsurance){ // 填写劳保福利
m_fInsurance = fInsurance;
}
void SetCostOfWater(float fCostOfWater){ // 填写水费
m_fCostOfWater = fCostOfWater;
}
void SetCostOfElec(float fCostOfElec){ // 填写电费
m_fCostOfElec = fCostOfElec;
}
void SetCostOfHeating(float fCostOfHeating){ // 填写取暖费
m_fCostOfHeating = fCostOfHeating;
}
float GetWage(){ // 查看基本工资
return m_fWage;
}
float GetSubsidy(){ // 查看岗位津贴
return m_fSubsidy;
}
float GetInsurance(){ // 查看劳保福利
return m_fInsurance;
}
float GetCostOfWater(){ // 查看水费
return m_fCostOfWater;
}
float GetCostOfElec(){ // 查看电费
return m_fCostOfElec;
}
float GetCostOfHeating(){ // 查看取暖费
return m_fCostOfHeating;
}
float RealSum(){ // 计算实发工资
return m_fWage + m_fSubsidy+m_fInsurance
-m_fRent-m_fCostOfElec-m_fCostOfWater-m_fCostOfHeating;
}
};
// 定义职务类型
enum Position
{
MANAGER, // 经理
ENGINEER, // 工程师
EMPLOYEE, // 职员
WORKER // 工人
};
// 声明档案类
class Employee
{
CString m_sDepartment; // 工作部门
CString m_sName; // 姓名
CTime m_tBirthdate; // 出生日期
Position m_nPosition; // 职务
CTime m_tDateOfWork; // 参加工作时间
Salary m_Salary; // 工资
public:
Employee(LPCTSTR lpszDepart,LPCTSTR lpszName,CTime tBirthdate,
Position nPosition,CTime tDateOfWork); // 构造函数
void SetWage(float fWage){ // 填写基本工资
m_Salary.SetWage(fWage);
}
void SetSubsidy(float fSubsidy){ // 填写岗位津贴
m_Salary.SetSubsidy(fSubsidy);
}
void SetInsurance(float fInsurance){ // 填写劳保福利
m_Salary.SetInsurance(fInsurance);
}
void SetCostOfWater(float fCostOfWater){ // 填写水费
m_Salary.SetCostOfWater(fCostOfWater);
}
void SetCostOfElec(float fCostOfElec){ // 填写电费
m_Salary.SetCostOfElec(fCostOfElec);
}
void SetCostOfHeating(float fCostOfHeating){ // 填写取暖费
m_Salary.SetCostOfHeating(fCostOfHeating);
}
float RealSalary(){ // 查看实发工资
return m_Salary.RealSum();
}
void ShowMessage(); // 打印职工信息
};
// 职工档案类的成员函数定义
Employee::Employee(LPCTSTR lpszDepart,LPCTSTR lpszName,
CTime tBirthdate,Position nPosition,CTime tDateOfWork)
{
m_sDepartment = lpszDepart;
m_sName = lpszName;
m_tBirthdate = tBirthdate;
m_nPosition = nPosition;
m_tDateOfWork = tDateOfWork;
}
void Employee::ShowMessage()
{
cout <<,Depart:,<< m_sDepartment << endl;
cout <<,Name:,<< m_sName << endl;
cout <<,Birthdate:,<< m_tBirthdate.Format(“%B %d,%Y”) << endl;
switch(m_nPosition)
{
case MANAGER:
cout <<,Position:,<<,MANAGER” <<endl;
break;
case ENGINEER:
cout <<,Position:,<<,ENGINEER” <<endl;
break;
case EMPLOYEE:
cout <<,Position:,<<,EMPLOYEE” <<endl;
break;
case WORKER:
cout <<,Position:,<<,WORKER” <<endl;
break;
}
cout <<,Date of Work:,<< m_tDateOfWork.Format(“%B %d,%Y”) << endl;
}
分 析,由于CString和CTime等MFC类的声明在头文件<afx.h>中,所以需要用文件包含命令将其包含在程序中。同样,在声明了工资类以后,就可以在声明职工档案类时使用工资类的对象作为成员变量。
由于工资类和职工档案类的所有数据成员均为私有成员(缺省状态),只能由其成员函数访问。因此,我们为这两个类编写了若干成员函数以访问它们的数据成员。由于大多数成员函数的操作都很简单,所以采用了内联方式编写。
声明职工档案类后,就可以用其声明整个企业的职工档案:
// 声明职工档案数组
#define MAX_EMPLOYEE 1000
int EmpCount;
Employee EmployeeList[MAX_EMPLOYEE];
职工档案数组EmployeeList共有MAX_EMPLOYEE个元素,每个元素可以用来存放一个职工的档案。当然,某个企业的职工人数可能发生变化,所以我们另外设置了一个变量EmpCount,用来存放实际的职工人数。
建立职工档案可以使用Employee类的构造函数实现,例如:
CTime birthdate = CTime(1974,10,2,0,0,0);
CTime date_of_work = CTime(1999,12,19,0,0,0);
EmployeeList[EmpCount] = Employee(“办公室”,“张三”,birthdate,
EMPLOYEE,date_of_work);
EmpCount++;
单元上机练习题目
1.现实世界中的物理实体和逻辑概念均可以看作是对象。分析你周围的一个对象,试用C++ 类来描述它。
2.编程验证CString类,CTime类和CTimeSpan类各成员函数的使用方法。
3,为一个小型图书馆的管理系统声明图书卡类、借书证类和借书记录类,设计其成员函数,包括书卡录入函数和借书证录入函数、借书登记函数和还书处理函数等。设计好的类可自编主函数验证之。