第五章 类与对象 ( 本教案改变了教学次序 )
从本章起将进入面向对象程序设计学习的实质阶
段,是面向对象的第一个重要特性 —封装性。
封装( Encapsulation) 是面向对象程序设计最
基本的特性,把数据(属性)和函数(操作)合成一
个整体,这在计算机世界中是用类与对象实现的。本
章将引入 C++的类( class)和对象( object)的概念,
建立, 函数也可以是数据类型的成员, 的思想。
下面对类和对象的概念再进行说明:
类是对现实世界中客观事物的抽象描述,将具有
相同属性的一类事物称为某个类,例如将在路上跑的
各种各样的汽车抽象出它们相同的属性,称为 汽车类,
而 宝马汽车 是汽车类的一个实例,宝马就是 对象 。
类与对象
从计算机角度来看,类首先是一种复杂的数据类
型,它是将不同类型的 数据 和与这些数据有关的 操作
封状在一起的集合体,因此,定义类就是定义一种新
的数据类型,就跟我们在 C语言中定义结构体相类似,
只不过类除了数据定义之外,还可以定义这些数据的
操作函数 。类不仅具有 封装性,还具有 隐藏性,类的
一些属性可以设置为不被外界操作,所以对外部来说,
这些数据有不可见性,外面只有通过类的操作函数才
能操作类的内部数据。数据隐藏对于一个团队合作开
发一个大型程序是非常有意义的。
学习面向对象程序设计的目的是要解决实际问题,
只有具备了从实际问题中抽象出类和对象的能力,才
能说初步掌握了面向对象的程序设计的方法。
第五章 类与对象
5.1 类与对象
5.5 类的作用域
5.6 全局对象与类接口
5.4 静态成员
5.3 友元
5.2 构造函数和析构函数
5.1 类与对象
5.1.3对象的创建与使用
5.1.1 C++类的定义
5.1.2成员函数的定义
5.1.1 C++类的定义
在一个面向对象的 C++程序是由三部分组成的:
1、类的定义
2、类的实现 —成员函数的定义
3、主函数的实现
首先我们来看看类在 C++中是如何定义的:
class 类名 {
private:
私有数据和函数成员的声明或实现;
public:
公有数据和函数成员的声明或实现;
protected:
保护数据和函数成员的声明或实现;
};
5.1.1 C++类的定义
在 C++中定义一个圆的类,可以这样表述:
class Circle {
private:
float mfR; // 数据成员,半径是私有的数据
public, // 四个公开的成员函数
void SetRadius(float mR); // 设置圆的半径
float GetRadius(); // 返回圆的半径
void OutputArea(); // 求面积
void OutputCircum(); // 求周长
} ; //最后的分号不可少,这是一条说明语句
上面的表述中,关键字 class是 数据类型说明符,指出下
面说明的是类。标识符 Circle是 圆 这个类的 类型名 。花括号中
是构成类体的一系列的成员,关键字 private和 public是一种
访问限定符, private表示其后所列为私有成员,就是说外部
不可以对这些成员进行直接访问,而 public表示其后所列为
公共成员,就是说可以在外部对这些成员进行访问。
5.1.1 C++类的定义
访问限定符( access specifier) 有三种:
public(公共的), private(私有的) 和
protected(保护的),其中后两种说明的成员是
不能从外部进行访问的。每种说明符可在类体中使
用多次。它们的 作用域 是从该说明符出现开始到下
一个说明符之前或类体结束之前结束。
如果在类体起始点无访问说明符,系统 默认定
义为私有 ( private)。
访问说明符 private(私有的)和 protected(保
护的)体现了类具有 封装性( Encapsulation),
实现数据成员的隐藏特性。 private和 protected的
区别在以后的类的继承特性中说明。
5.1.1 C++类的定义
在类的定义中, 引进了 成员函数 (member function)或
函数成员,也就是说函数也成了数据 (类 )中的一员 。 类把 数据
( 事物的属性 ) 和 函数 ( 事物的行为 ——操作 ) ( 也称为 方
法 ) 封装为一个整体 。 在圆的类定义中, 我们注意到,
一个 数据成员 半径 fmR被说明成 私有, 而四个 函数成员
SetRadius,GetRadius,OutputArea,OutputCirum被说明
成 公有的 ; 也就是说如果从外部对这个数据成员 fmR进行操
作的话,只能通过这四个公有函数来完成,这时候数据受到了良
好的保护 。 公有函数集定义了类的 接口 ( interface) 。
类是一种数据类型, 定义时系统并不为类分配存储空间, 所
以不能对类的数据成员初始化 。 当然类中的任何数据成员也
不能使用关键字 extern,auto或 register限定其存储类型 。
成员函数 可以 直接使用 类定义中的 任一成员, 可以 处理
数据成员, 也可 调用函数成员 。
5.1.2 成员函数的定义
在前面的小结中, 只对成员函数作了一个 声
明, 或者讲只给出了函数的 原型, 并 没有 对函
数的具体操作进行 定义 。 函数定义通常在类的
说明之后进行, 其格式如下:
返回值类型 类名,:成员 函数名 (参数表 )
{
//函数体部分;
}
其中运算符,,:”称为 作用域运算符 (scope
resolution operator),也可称为 范围运算符 。
它指出该函数是属于哪一个类的成员函数 。
5.1.2 成员函数的定义
类 Circle的 函数成员 可以如下定义:
void Circle::SetRadius(float mR)
{
fmR = mR;
}
float Circle::GetRadius()
{
return fmR;
}
void Circle::OutputArea()
{
cout<<“圆的面积为,”<<PI*mfR*mfR<<endl;
}
void Circle::OutputCircum()
{
cout<<“圆的周长为,”<<2.0*PI*mfR<<endl;
}
5.1.2 成员函数的定义
如果 成员函数 较短,我们可以直接在类里面定义,这时候
的成员函数称为 内联成员函数。 上面圆类可以如下定义:
class Circle {
private:
float mfR; // 数据成员,半径是私有的数据
public, // 四个公开的成员函数
void SetRadius(float mR)
{ fmR = mR;
}
float GetRadius()
{ return fmR;
}
void OutputArea()
{ cout<<“圆的面积为,”<<PI*mfR*mfR<<endl;
}
void OutputCircum()
{ cout<<“圆的周长为,”<<2.0*PI*mfR<<endl;
}
} ;
5.1.2 成员函数的定义
如果想在类外面将 成员函数 定义为 内联函数,我们可以在
函数定义时候使用关键字 inline。如下面所示:
inline void Circle::SetRadius(float mR)
{ fmR = mR;
}
inline float Circle::GetRadius()
{
return fmR;
}
inline void Circle::OutputArea()
{
cout<<“圆的面积为,”<<PI*mfR*mfR<<endl;
}
inline void Circle::OutputCircum()
{
cout<<“圆的周长为,”<<2.0*PI*mfR<<endl;
}
前面我们讲过, 类 是一种 数据类型, 定义某一个 类, 只是
声明一种新的数据类型, 告诉编译系统该数据类型的结构形式,
计算机并没有分配内存, 只有在定义 对象 之后, 系统才为该 对
象 分配内存 。 对象是类的实例化 ( instance), 如用 汽车类
定义 宝马车 一个 对象 。 正如在前几章讲的定义一个数据类型的
变量一样, int i。
创建类的对象可以有两种常用方法 。 第一种是 直接定义 类
的实例 ——对象,定义格式:
类名 对象名列表; 如 // Circle cs1,cs2;
这个定义创建了 Circle类的两个对象 cs1和 cs2,同时为
它们 分配 了属于它们的 存储块, 用来 存放数据 和对这些数据实
施操作的 成员函数 ( 代码 ) 。 与变量定义一样, 一个对象只在
定义它的域中有效 。
第二种是采用 动态创建类的对象的方法, 将在以后中学习 。
所谓动态指在程序 运行时 建立对象 。 而前一种是在 编译时 ( 程
序运行前 ) 建立 。
5.1.3 对象的创建与使用
内存中对象的空间布局
数据区
对象 1
数据区
对象2
数据区
对象n
......
各对象的代码区共用的方案
公共程序代码区
( 静态存储区 )
原则上,对象在内存中的存储形式和结构一样,同类的每
个对象都被分配一段能保存其所有成员的存储单位,但为了节
省内存,在创建对象时只为每个对象的数据成员分配内存,而
成员函数只是一次性存放在静态存储区中,为所有对象所共享。
这是因为同一个类中的成员函数的代码无论属于哪个对象,在
整个程序的运行期间都不会改变,如果为每一个对象也都分配
相应的成员函数代码区,对系统来说是极大的浪费,所以采用
对象成员函数共享的方式来为同类对象分配内存空间。
数据成员
函数成员
5.1.3 对象的创建与使用
对象创建了以后,对象提供的数据成员和函数就可以被使用,由于类
和结构定义有相似之处,我们一样采用成员访问 运算符”,”和 指向运算符
,->”来存取,其格式如下:
对象名,数据成员名
对象名,成员函数名(参数表)
对象指针名 ->数据成员名
对象指针名 ->成员函数名(参数表)
注意:这些成员必须是公有的成员,只有公有成员才能在对象的外面
对它进行访问 。 如上面定义,Circle cs1,cs2;
cs1.SetRadius(7.5); cs2.SetRadius(123);
cout<<“圆 1的半径:” <<cs1.GetRadius
<<“圆 2的半径:” <<cs2.GetRadius<<endl;
cs1.OutputArea(); // 输出圆 1的面积
cs2.OutputCirum(); // 输出圆 2的周厂
cout<<“圆 1的半径:” <<cs1.fmR<<endl;
最后一句错了,因为这里对象 fmR数据成员是私有的,在外部是不能
访问的。
提问:
编程设计一个日期类,具体要求如下,// t5-1.cpp
( 1)日期类名为 Date;
( 2)设置日期
( 3)输出明天日期
( 4)返回年份
#include <iostream.h>
class Rectangle {
private:
int length=10;
int width = 20;
public:
void Set(int len,int wid);
int area();
}
void Set(int len,int wid)
{ length = len;
width = wid;
}
int area()
{ return length * width;
}
void main()
{ Rectangle aa,bb;
aa.Set(20,30);
cout <<,面积是:” << aa.area() << endl;
bb.length = 40;
bb.width = 50;
cout <<,面积是:” << bb.area() << endl;
}
指出该程序中存在的错误并改正
5.2 构造函数和析构函数
定义对象时,按现在已学过的知识无法进行初
始化,即无法对数据成员进行赋初值的过程。数据
成员,从封装的目的出发,应该多为私有的,要对
它们进行赋值,看来必须用一个公有函数来进行,
在圆类定义中 SetRadius就是这样一个函数。但对象
的初始化,要求该函数应该在且仅在定义对象时自
动执行一次,否则就不是初始化了。显然 SetRadius
不能满足这样的要求。在 C++程序设计语言中,每个
对象在创建时候都会自动调用一个初始化函数,并
且只能调用一次,该函数称为 构造函数 。
5.2 构造函数和析构函数
5,2,1 构造函数的定义与使用
5,2,3 复制构造函数的定义
5,2,4 成员对象与构造函数
5,2,2 析构函数的定义
5,2,5 构造和析构函数的调用
对于对象的 初始化,采用 构造函数( constructor) 。
当需要对对象进行初始化时,总是编写一个或一组构造函数。
构造函数是特殊的公有成员函数,其特征如下:
1,函数名与类名相同 。
2,构造函数无函数返回类型说明 。 注意是没有而不是
void,即什么也不写, 也 不可写 void! 实际上 构造函数有隐
含的返回值, 返回的就是构造函数所创建的对象 。
3,在程序运行时, 当新的对象被建立, 该对象所属的类
的构造函数自动被调用, 在该对象生存期中也只调用这一次 。
4,构造函数可以重载 。 严格地讲, 说明中可以有多个构
造函数, 它们由不同的参数表区分, 系统在自动调用时按一
般函数重载的规则选一个执行 。
5.2.1 构造函数的定义与使用
5.2.1 构造函数的定义与使用
5,构造函数可以在类中定义, 也可以在类外定义 。
6,如果类说明中没有给出构造函数,则 C++编译器自
动给出一个 缺省 的构造函数,
类名 (void)
{
}
但 只要我们定义了一个构造函数,系统就不会自动生成
缺省的构造函数 。 缺省的构造函数,也可以由程序员自己来
编,只要构造函数是无参的或者只要各参数均有缺省值的,
C++编译器都认为是缺省的构造函数,并且缺省的构造函数
只能有一个 。
如果对象的数据成员 全为公有的,在缺省构造函数方式
下,也可以在对象名后加,=”加,{}”,在花括号中顺序填
入全体数据成员的初始值,
5.2.1 构造函数的定义与使用
为了更加能说明问题, 下面我们以学生类 Student来说明:
class Student { // 5-2-1.cpp
private:
int iNo;
char sName[10];
float fEng,fMath,fCmp;
public,
Student(int no,char *sn,float e,float m,float c)
{
iNO = no;
strcpy(sName,sn);
fEng = e; fMath = m; fCmp = c;
cout<<“学生的构造函数被调用” <<endl;
}
float GetAvg();
float GetTotal();
} ;
Student std1(1001,“zhang”,77,89,82);
Student std2(1002,“wang”,87,92,78);
5.2.2 析构函数的定义
当一个对象定义时,C++自动调用构造函数建立该对象
并进行初始化,那么当一个对象的生命周期结束时,C++也
会自动调用一个函数 注销 该对象并进行善后工作,这个特殊
的成员函数即 析构函数( destructor),
1,析 构函数名与类名相同,但在前面加上字符 ‘ ~’,

~Student() 。
2,析构函数无函数返回类型,与构造函数在这方面是
一样的。但析构函数不带任何参数。
3,一个类有一个也只有一个析构函数,这与构造函数
不同。析构函数可以缺省。
4,对象注销时,系统自动调用析构函数。
~Student()
{ cout<<“析构函数被调用, <<endl;
}
5.2.3 复制构造函数
同一个类的对象在内存中有完全相同的结构, 如果作为
一个 整体进行复制 或称拷贝是完全可行的 。 这个拷贝过程 只
需要拷贝数据成员, 而 函数成员是共用的 ( 只有一份拷贝 ) 。
在建立对象时可用同一类的另一个对象来初始化该对象, 这
时 所 用 的 构 造 函 数 称 为 复 制 构 造 函 数 ( Copy
Constructor) 。 其应用格式如下:
类名,:类名 ( const 类名 & 引用名 )
对于学生类, 我们定义它的复制构造函数如下
Student (Student & ss)
{ iNo = ss.iNo;
Strcpy (sName,ss.sName);
fEng = ss.fEng;
fMath = ss.fMath;
fCmp = ss.fCmp;
}
Student std3=std1;
上面是在类内部定义的, 在类外面定义如下:
Student:,Student (Student & ss)
5.2.3 复制构造函数
这里必须注意复制构造函数的参数 ——同类( class)的对
象采用的是 引用 的方式。如果把一个真实的类对象作为参数传
递到复制构造函数,会引起 无穷递归 。所以必须将复制构造函
数的参数定义为一个类的对象的引用。
通常情况下,这种按成员语义支持已经足够。但在某些情
况下,它对类与对象的安全性和处理的正确性还不够,这时就
要求类的设计者提供特殊的 复制构造函数 和 赋值操作符 的定义。
系统会自动提供,称为 缺省的按成员语义支持的 复制构造
函数,每个类成员被依次复制,亦称为 缺省的按成员 初始化 。
按成员作复制是通过依次复制每个数据成员实现的,赋值运算
符, =”称缺省的 按成员赋值操作符,同类对象之间可以用, =”
直接拷贝 。 Student std3=std1; 或者 Student
std3(std1);
在类定义中如果没有显式给出复制构造函数时,并不是不
用复制构造函数,而是由系统自动调用缺省的复制构造函数。
5.2.3 复制构造函数
复制构造函数并不只是在同类的一个对象去初始化该类
的另一个对象时使用, 它还在另二个方面使用:
1,当函数的 形参 是类的 对象, 调用函数时, 进行形参
与实参结合时使用 。 这时要在内存新建立一个局部对象, 并
把实参复制到新的对象中 。 理所当然也调用复制构造函数 。
2.当函数的 返回值 是类 对象, 函数执行完成返回调用者
时使用 。 理由也是要建立一个临时对象中, 再返回调用者 。
为什么不直接用要返回的局部对象呢? 因为 局部对象在离开
建立它的函数时就消亡了, 不可能在返回调用函数后继续生
存, 所以在处理这种情况时, 编译系统会在调用函数的表达
式中创建一个 无名临时对象, 该临时对象的 生存周期 只在函
数调用处的 表达式 中 。 所谓 return 对象, 实际上是 调用复
制构造函数把该对象的值拷入临时对象 。 如果返回的是变量,
处理过程类似, 只是不调用复制构造函数 。
范例
范例,设计一个程序,定义一个矩形类,包括数据成员和函数成员。要
求有构造函数、析构函数,完成赋值、修改、显示等功能的接口,并编
写 main函数测试,要求用一个对象初始化另一对象。
[分析 ] 要确定一个矩形(四边都是水平或垂直方向,不能倾斜),只
要确定其左上角和右下角的 x和 y坐标即可,因此应包括四个数据成员,
left,right,top,bottom,即左右上下四个边界值。由构造函数对数据
成员赋值,赋值函数完成未初始化的矩形赋值,修改函数可以修改各数
据成员,显示函数则给出该矩形参数。
class Rectangle {
private:
int left,top,right,bottom;
public:
Rectangle(int l=0,int t=0,int r=0,int b=0);
//缺省构造函数必须在此指定缺省实参
~ Rectangle(){ }; //析构函数,在此函数体为空
void Assign(int l,int t,int r,int b);
void Show();
};
将该部分内容存放在 rect.h文件中
范例#include <iostream.h>
#include,rect.h”
// 构造函数,带缺省参数,缺省值为全 0,在声明中指定
Rectangle::Rectangle(int l,int t,int r,int b) {
left = l; top = t;
right = r; bottom = b;
}
void Rectangle::Assign(int l,int t,int r,int b) {
left = l; top = t;
right = r; bottom = b;
}
void Rectangle::Show() {
cout<<”左上角 (”<<left<<”,”<<top<<”)”<<’\n’;
cout<<”右下角 (”<<right<<”,”<<bottom<<”)”<<’\n’;
}
void Rectangle::Draw(CDC * pDC) {
pDC->Rectangle(left,top,right,bottom );
}
// 将上述内容保存为 rect.cpp
范例
#include <iostream.h>
#include,rect.h”
void main()
{
Rectangle rect;
rect.Show();
rect.Assign(100,200,300,400);
rect.Show();
Rectangle rect1(0,0,200,200);
rect1.Show();
Rectangle rect2( rect1) ;
rect2.Show();
}
5.2.4 成员对象与构造函数
在定义类的对象时不仅要对对象进行初始化, 还要 先 对成
员对象进行初始化 。 对成员对象初始化, 必须调用该成员对象
的构造函数来实现 。
C++中对含对象成员的类对象的构造函数有 特殊 的格式:
类名,:构造函数名 (参数总表 ):对象成员 1(参数表 1),对象
成员 2(参数表 2),…… 对象成员 n(参数表 n) {……}
冒号后用逗号隔开的为要初始化的对象成员,附在后面的参
数表 1,…,参数表 n依次为调用相应对象成员所属的构造函数
时的实参表。这些表中的参数通常来自冒号前的参数总表。
含有成员对象的类的构造函数:
#include <iostream.h> // 程序文件 5-2-4.cpp
#include <string>
class Student {
public:
class studentID {
private:
long value;
public:
studentID(long id=0) {
value=id;
cout<<“赋给学生的学号:”
<<value<<endl;
}
~studentID() {
cout<<“删除学号:”
<<value<<endl;
}
}; //学号类定义,注意分号
private:
studentID id;
char name[20];
public:
Student (char* sname=“no name”,long sid=0):id(sid) {
//sname 现暂看作字符串,char*是指向字符的指针类型
cout<<“学生名:” <<sname<<endl;
strcpy(name,sname);
}
}
如果定义,Student ss(“朱明”,08002132); 运行结果为:
赋给学生的学号,08002132
学生名:朱明
删去学号,08002132
在 student构造函数头部的冒号表示要对象成员的构造
函数进行调用。但注意:如果构造函数在类外定义,那么 在类
中的构造函数的声明中,冒号及冒号以后部分必须略去。并且
类外面参数的缺省值去掉。
*以下供学生阅读:
不用这种格式将无法把学号传递给学号类对象
student(char*sname,long sid=0){
//sname 现暂看作字符串,char*是指向字符的指针类型
cout<<"学生名,"<<sname<<endl;
strcpy(name,sname);
studentID id(sid);
} //A 目的是将学号转给学号类对象,但未达目的
5.2.4 成员对象与构造函数
5.2.4 成员对象与构造函数
这段程序希望通过构造函数中的 A行进行初始化,把学生名“朱明”和
学号 08002132赋给对象 SS。运行结果:
赋给学生的学号,0 //首先调用对象 ss的对象成员 id的构造函数
学生名:朱明 //对象 ss的数据成员 name
赋给学生的学号,08002132
//ss构造函数中建立的 局部对象 id中数据成员 value初值
删去学号,08002132 //局部对象 id析构
删去学号,0 //对象成员 id析构
这表明构造函数中的 A行并没有把学号 08002132赋给对象 ss中的对象
成员 id,而是在 构造函数中构造了一个名字也为 id的 studentID局部对
象,它只在构造函数中生存,构造函数返回时该局部对象被析构。而对
象 ss中的对象成员 id则因为构造函数中没有指定对它进行初始化的值,
所以系统按缺省方式调用了 StudentID()建立了对象成员 id,所以有第
一条输出“赋给学生的学号,0“。 A行也可以写为,
studentID:,studentID(sid);
结果一样,无法把学号传过去,这时构造函数建立的是一个无名的
studentID局部对象。
5.2.4 成员对象与构造函数
对于不含对象成员的类对象的初始化,也
可以套用以上的格式,把部分只需要直接赋初
值的变量初始化写在冒号的右边:
类名,:构造函数名 (参数表 ):变量 1(初值
1),……,变量 n(初值 n)
{ ……
}
当然也可以把一部分变量重新放回花括号中
的函数体。 冒号以后部分实际是函数体的一部
分,所以在构造函数的声明中,冒号及冒号以
后部分必须略去。
5.2.5 构造函数和析构函数的调用
对于不同作用域的对象类型, 构造函数和析构函数的调用
如下:
1,对全局定义的对象, 当程序进入入口函数 main之前对象
就已经定义, 这时要调用构造函数 。 整个程序结束时调用析构
函数 。
2,对于局部定义的对象, 每当程序控制流到达该对象定义
处时, 调用构造函数 。 当程序控制走出该局部域时, 则调用析
构函数 。
3,对于静态局部定义的对象, 在程序控制首次到达该对象
定义处时, 调用构造函数 。 当整个程序结束时调用析构函数 。
5.2.5 构造函数和析构函数的调用
在正确定义了构造函数和析构函数的前提下,在一
个健康的程序中,每个创建的对象必然有一个而且
只有一个撤消动作 。请读者根据下面程序执行结果,
注意每个对象创建和撤消的对应关系:
例 5.6演示了对象创建和撤消的对应关系。请参看
VC++平台上的演示 。
注意,先建立的对象后撤销 。
本例可见构造函数和析构函数使用频繁,最好在类
说明之外定义构造函数和析构函数,以确保一个函
数拷贝,避免代码膨胀。
提问:
class Point { // t5-2.cpp
int x,y;
public:
Point(int ux=0,int uy=0)
{ x = ux; y = uy;
}
int getx() { return x; }
int gety() { return y; }
};
class Circle {
private:
Point p;
int r;
public:
构造函数
float area()
{ return 3.1415*r*r;
}
};
void main()
{ Circle a(50,50,10),b;
cout <<,A 面积是:” << a.area << endl;
cout <<,B 面积是:” << b.area << endl;
b = a;
cout << endl;
cout <<,B 面积是:” << b.area << endl;
}
Circle(int ux=0,int uy=0,int ur=0):p(ux,uy),r(ur) { }
5.3 友元
类具有封装性, 类中的私有成员一般只能通过
该类中的成员函数才能访问, 而程序中其他函数是
无法直接访问私有成员的 。 类的封装机制带来的好
处是明显的, 但若绝对不允许外部函数访问类的私
有成员的话, 确实也有很多不便之处, 如果频繁地
通过成员函数来访问的话, 过多的参数传递, 类型
检查, 会影响程序的运行效率 。 为了解决该问题,
C++引进 友元 ( friend) 函数, 允许在类外的普通
函数访问该类中的任何成员, 就象成员函数一样 。
友元函数 是一个说明在类体中的 普通函数, 格式:
friend 类型说明 友元函数名 ( 参数表 ) ;
5.3 友元
class Girl { // 5-3.cpp
char *name,*dial;
prublic:
Girl(char *n,char *d) {
name = new char[strlen(n)+1]; strcpy(name,n);
dial = new char[strlen(d)+1]; strcpy(dial,d);
}
friend void disp(Girl &);
~Girl() {
delete name; delete dial;
}
}
void disp(Girl &x)
{ cout<<“Girl name is:” <<x.name<<“,tel is:”<<x.dial<<endl;
}
main()
{ Girl e(“Susan”,“020-84118888”);
disp(e);
}
// 该程序的运行结果是 Girl name is,Susan,tel is,020-84118888
5.3 友元
成员函数用做友元函数:
可以将一个类的成员函数说明为另外一个类的友元函数, 这样,
通过友元函数可以使一个类对象直接访问另一个类的所有成员 。,
定义格式,friend 类型 所在类名,:友元函数名 ( 参数表 ) ;
Class Y;
Class X {
private:
int x;
public:
X(int a=0) { x=a;}
int GetX() { return x;}
void SetX(Y &); // void SetX(int a) {x=a;}
}
Class Y {
private:
int y;
public:
Y(int b=0) { y=b;}
int GetY() { return y;}
friend void X::SetX(Y &s);
}
5.3 友元
void X::SetX(Y &s)
{ x=s.y;
}
main()
{ X x(5);
Y y(10);
cout<<“X=“<<x.GetX()<<,Y=“<<y.GetY()<<endl;
x.SetX(y); // x.SetX(y.GetY())
cout<<“X=“<<x.GetX()<<,Y=“<<y.GetY()<<endl;
}
如果不用友元函数,我们可以看出需要两次函数调用,而友元函数只需要一次。
成员函数作为友元函数时应注意以下几点:
1、友元函数作为一个类的成员函数时,除应当在它所在类定义中说明外,还应当
在在另一个类中有关键字 friend声明它的友元关系。声明格式如上
2、友元函数在引用声明它是友元函数的类的私有成员时,引用参数必须是友元类
对象。
3、一个类的成员函数做另一个类的友元函数时,必须先定义它,在上面例子中,
先声明 class Y,然后定义类 X
友元函数注意点,
1,友元函数是一种说明在类体内的普通函数, 不是类的成员
函数, 在函数体中访问对象的成员, 必须用对象名加运算符,,”加
对象成员名 。 这一点和一般函数一样 。 但友元函数可以 访问 类中的
所有成员 ( 公有的, 私有的, 保护的 ), 一般函数只能访问类中的
共有成员 。
2,友元函数不受类中的访问权限关键字限制, 可以把它放在类的
公有, 私有, 保护部分, 但结果一样 。
3,某类的友元函数的作用域并非该类作用域 。 如果该友元函数是
另一类的成员函数, 则其作用域为另一类的作用域, 否则与一般函
数相同 。
友元函数破坏了面向对象程序设计类的封装性, 所以友元函
数如不是必须使用, 则 尽可能少用 。 或者用其他手段保证封装性 。
友元类
友元还有 友元类 概念:整个类可以是另一个类的友元 。 友元
类每个成员函数都是另一个类的友元函数, 都可访问另一个类中
的保护或私有数据成员 。 定义方法如下:
class A {
……
friend class B; //声明 B为 A的友元类
……
};
举例,5-3-1.cpp
#include <iostream.h> // 5-3-1.cpp
Class X {
private:
int x;
static int y;
public:
void Set(int i) {x=i;}
void Display(){cout<<“x=“<<x<<“,y=“<<y<<endl;}
friend class Y;
};
友元类
class Y {
private:
X a;
public:
Y(int i,int j);
void Display();
};
int X::y = 10;
Y::Y(int i,int j)
{ a.x = i; X::y=j; }
void Y::Display()
{ cout<<“x=“<<a.x<<“,y=“<<X::y<<endl;
}
void main()
{ X b; b.Set(15); b.Display();
Y c(16,19); c.Display(); b.Display();
}
结果,x=15,y=10
x=16,y=19
x=15,y=19
友元类
友元类注意事项:
1、友元关系是单向的,若 Y是类 X的友元,必须
在类 X中声明类 Y是类 X的友元,但类 X不是类
Y的友元,若想是,还必须在类 Y的定义中用
friend声明类 X是类 Y的友元。
2、友元关系不具有传递性,若 X是类 Y的友元,
而类 Y是类 Z的友元,并不表示类 X就是类 Z的
友元。
当一个类要和另外一个类协同工作时,使一
个类成为类一个类的友元类很有用,这是友元
类中的每一个成员函数都是对方类的友元函数
5.4 静态成员
在 C++中定义一个类, 就相当与定义一种新的数
据类型, 每当说明一个类的对象时, 系统就为该对
象分配内存, 以便存放对象中所有成员数据, 在有
些应用中, 希望程序中若干个同类的对象共享某个
数据, 这时候可以将共享数据成员在类中用关键字
static修饰为 静态类成员 ( static class member) 。
虽然使用 static修饰说明, 但与函数中的静态变量有
明显差异 。 类的静态成员为其 所有对象共享, 不管
有多少对象, 静态成员只有一份存于公用内存中 。
5.4.1 静态数据 5.4.2 静态函数成员
5.4.1 静态数据
在类定义中,用关键字 static修饰的数据成员为 静
态数据成员 。该类产生的所有对象共享系统为静态成
员分配的一个存储空间,而这个存储空间是在编译时
分配的,在定义对象时不再为静态成员分配空间。静
态数据实际上是该类所有对象所共有的,它更像在面
向过程程序设计时的全局变量,可提供同一类的所有
对象之间信息交换的捷径,正因为静态数据成员不属
于类的某一特定对象,而是 属于整个类 的,所以使用
时可用以下格式:
类名,:静态数据成员名
静态数据的初始化, 是在类体外进行的, 格式如下:
数据类型 类名,:静态数据成员 =初值
5.4.1 静态数据
卖西瓜作为例子来说明静态数据成员的使用
王婆卖瓜,每卖出一个西瓜,要计算该西瓜的重量,还要计算所卖出的所
有西瓜的总重量和总个数。
分析:
用面向对象的程序设计思想来进行数据组织,我们要处理的数据对象
为西瓜,所以以西瓜作为类来设计,每个西瓜的数据成员包括西瓜的重量,
累计重量和累计个数,由于累计重量和累计个数是属于所有西瓜的,所以
可以将他们设计为静态的数据成员。还有,西瓜类的成员函数包括卖瓜和
退瓜,我们可以用构造函数和释放函数来模拟,显示单个瓜重和总的函数,
所以我们可以定义瓜类如下:
class watermelon { // 5-4-1.cpp
private:
float weight;
static float total_weight; // 静态数据成员
static float total_number; // 静态数据成员
public:
watermelon(float w) {
weight = w;
total_weight += weight;
total_number ++;
}
5.4.1 静态数据
~watermelon() {
total_weight -= weight;
total_number --;
}
void display() { cout<<“瓜重为:” <<weight<<end; }
static void total_display() {
cout<<“总瓜重为:” <<total_weight<<endl;
cout<<“总数量为:” <<total_number<<endl; }
};
float watermelon::total_weight = 0; // 静态成员数据初始化 A行
int watermelon::total_number = 0;
void main()
{ watermelon w1(3.5f);
w1.display();
watermelon::total_display();
watermelon w2(6.3f);
w2.display();
watermelon::total_display();
watermelon w3(5.6f);
w3.display();
watermelon::total_display();
w2.~watermelon();
watermelon::total_display();
}
5.4.1 静态数据
静态数据成员说明:
上例中 A行是对静态数据成员数据作定义性说明并初始化,
必须在文件作用域中作一次并只能做一次说明, 只有在这时 C++
在编译时为静态数据成员分配存储空间并初始化 。 C++静态数据
成员缺省的初值为 0,所以 A行中, =0”是可以省去的 。 特别要注
意不管静态变量是私有或公有, 定义性说明均有效 。
静态数据成员虽然具有全局变量的一些特性, 但受到访问权
限的约束 。 建议 静态成员说明为 私有 的, 从而保证面向对象程序
设计的封装性 。 如果说明为公有的, 它会带来与全局变量同样的
副作用 。
系统对每个类的每个静态成员只有一个实体, 而不是像其他
成员那样, 对每个对象都有它的一个实体, 这样才能保证公用数
据的一致性 。
5.4.2 静态函数成员
函数成员也可说明为静态的, 同样它与该类的对象无关 。 是属于整个
类的 。 严格地讲, 在逻辑上该函数成员只有一个拷贝 。 它的定义如下:
static 数据类型 函数名 ( 参数表 )
如上面程序中 static void total_display();
静态函数成员的调用, 在对象之外可以采用下面的方式:
类名,:函数名 watermelon.total_display();
与静态数据成员相反, 为使用方便, 静态函数成员多为 公有 的 。 因它
是独立于具体对象而存在的, 所以一般是用静态成员函数来访问静态数据
成员, 如果要访问非静态数据成员, 因为数据不确定 ( C++系统不知应取
哪一个对象的数据 ) 而不能运行, 所以必须要通过对象名来访问 。
在上面的例子中, 如果 static void display(); 说明为静态函数
那么在定义时程序应改为:
static void display(watermelon &w)
{ cout<<“瓜重为:, <<w.weight<<end;
}
5.4.2 静态函数成员
如果静态成员函数 在类定义之外定义 时, 则 不能
在定义时再加 static,这一点与友元函数类似 。 因为
static不属于数据类型组成部分 。
因为 C++在产生类的对象时, 为了减少对象所占
空间, 物理上将同一类的所有对象的成员函数只保
留一个拷贝, 所以一般情况下定义静态函数不能取
得明显好处, 只有逻辑上的优点 。 反而在使用上变
得不方便, 通常是没有必要去定义静态成员函数的 。
5.5 类的作用域
类定义的花括号之间区域叫做 类作用域,在类中说明 的成员变量,其
可见性在该类域内。类域是介于文件域和函数域之间的作用域,类体内可以
定义函数,因此类域比函数域大,一个文件可以包含若干的类,所以类域比
文件域小,另外,在类域中定义的变量不能用 register,extern等修饰符,
类中定义的函数也不能用 extern。
1、嵌套类:在一个类中定义的类称为嵌套类,包含嵌套类的类称为外围类
class Outer {
private:
……
public:
class Inner {
private:
int a,b;
……
};
……
};
定义嵌套类的目的是为了隐藏类名,限制该类的创建对象的范围,从而减少
全局标识符,提高类的抽象能力。
5.5 类的作用域
嵌套类说明:
1,从作用域角度来看, 嵌套类被隐藏在外围类中, 所以该类名只
能在外围类中使用, 如果在外围类作用域外使用, 则需加名字限定
2,从访问权限角度来看, 嵌套类名和外围类的的成员名具有相同
的访问权限, 一般设置嵌套类为 public
3,从嵌套类和外围类的关系角度来看, 嵌套类的成员不是外围类
的成员, 友元也是, 嵌套类中的成员函数不能访问外围类中的成员,
反之也是 。
4,嵌套类中的友元不能访问外围类的成员 。 例子:
Class A {
private:
int a;
B bb;
public:
class B {
private:
5.5 类的作用域
int b;
public:
B(int i) {b=i;}
void print() {cout<<b<<endl; }
};
A(int i,int j):bb(i) {a=j;}
void print() {
cout<<a<<“,”;
bb.print();
}
};
Void main()
{ A a(19,15);
A::B b(10);
a.print();
b.print();
}
程序结果是:
15,19
10
5.5 类的作用域
2、局部类:在一个函数体内定义的类为局部类,局部类只能
在它的函数体内使用,超过该函数体则不可见。
定义局部类还应注意:
在局部类中不能说明为静态成员
局部类中所有成员函数都必须定义在函数体内
5.6 全局对象与类接口
建立对象的顺序就是调用构造函数的过程的顺序 。 工程文
件中各 C++文件编排的顺序不同, 结果也会有所不同 。
在 Windows的 C++编程中,入口函数 Winmain()并非
第一个执行的程序函数。在标准的 Windows编程( MFC)中,
有一个必不可少的但又只能有一个的类 CWinApp的对象,它
必须被定义为 全局量,由它建立 Windows应用程序的主线程,
这些工作必须在进入入口函数之前完成。所有 全局对象 都 在
入口函数运行之前被构造 。即全局对象是第一批构造的对象。
如果是单文件程序,全局对象可以按照定义的先后顺序确定
建立的先后顺序。但是多文件程序,各文件分别编译、连接,
因为编译器不能控制文件的连接顺序,所以不能决定不同文
件中全局对象之间的构造顺序。 千万不要在 全局对象之间引
用, 因为这 隐含了两个全局对象建立的先后次序, 会引起运
行中不可预料的错误。
5.6 全局对象与类接口
第二,进一步讨论 类接口 。在类中一般数据成员置为私有,
保证不被外部程序直接访问,而函数则必须有一部分成员置为
公有,专供类外程序语句访问,包括创建本类对象的构造函数
和撤消该对象的析构函数,这些函数亦称 接口函数 。其余成员
函数是开发类时 故意对外隐蔽 起来的操作,而这些往往是最复
杂最关键的部分,越是复杂越是关键就越能体现出使用类封装
的必要和优越。类中故意的隐藏也为以后的升级扩展留下了余
地,只要接口不变,内部再变,也不必修改原来的程序,就象
MFC(微软基础类)升级后,由 MFC底层类所编的程序完全不
必修改,自动升级。封装有助于编程人员在处理好简单接口后,
集中精力处理高层次开发的工作,MFC能被广泛使用也正是有
此优点。用户界面等的编程是底层的十分复杂和困难的事,
MFC给解决了。
作业:
1,设计一个类用来计算某个学生
3门课的平均值,对学生的描述
除了成绩之外,还有学生的姓
名,编程显示 5个学生的姓名和
平均成绩,及总平均值,要求
用到构造函数。
2,已知两个点的坐标,求两个点
之间的距离,要求设计一个点
类,求距离函数用友元函数的
方法实现。
3,设计一个个人通讯录,考虑为
它定义哪些数据成员和成员函
数。
#include <iostream.h>
class M {
int m1,m2;
public:
M(int I,int j) {
m1 = I; m2 = j;
}
void Sum(M a,M b) {
m1 = a.m1 + b.m1;
m2 = a.m2 + b.m2;
}
void Print() {
cout <<,m1 =, << m1
<<,,m2 =, << m2 << endl;
}
};
void main()
{ M a(3,8); M b(a); M c=a;
c.Sum(a,b); c.Print();
}