第九章 面向对象程序设计
基础
2004年 3月
河北建筑工程学院
9.1 面向对象程序设计概述
9.1.1 面向对象是软件方法学的返朴归真
客观世界是由许多具体的事物、抽象的概念、规则等组成的,
我们将任何感兴趣或要加以研究的事、物、概念统称为对象
( Object)。每个对象都有各自的内部状态和运动规律,不
同对象之间通过消息传送进行相互作用和联系就构成了各种
不同的系统。面向传统的结构化方法强调的是功能抽象和模
块化,每个模块都是一个过程。
结构化方法处理问题是以过程为中心。面向对象强调的是功
能抽象和数据抽象,用对象来描述事物和过程。而对象包含
数据和对数据的操作,是对数据和功能的抽象和统一。面向
对象方法处理问题的过程是对一系列相关对象的操纵,即发
送消息到目标对象中,由对象执行相应的操作。
从结构到类
程序 =数据结构 +算法
程序 =(数据结构 +算法)
+ (数据结构 +算法) ……
程序 =对象 +对象 ……
因此面向对象方法是 以对象为中心 的, 这种以对象为中心
的方法更自然, 更直接地反映现实世界的问题空间, 具有独
特的抽象性, 封装性, 继承性和多态性, 能更好地适应复杂
大系统不断发展与变化的要求 。
采用对象的观点看待所要解决的问题, 并将其抽象为系统
是极其自然与简单的, 因为它符合人类的思维习惯, 使得应
用系统更容易理解 。 同时, 由于应用系统是由相互独立的对
象构成的, 使得系统的修改可以局部化, 因此系统更易于维
护 。
软件开发从本质上讲就是对软件所要处理的问题域进行
正确的认识,并把这种认识正确地描述出来。
既然如此, 那就应该 直接面对问题域中客观存在的事物来
进行软件开发, 这就是 面向对象 。 另一方面, 人类在认识世
界的历史长河中形成的普遍有效的思维方法, 在软件开发中
也应该是适用的 。 在软件开发中尽量采用人们在日常生活中
习惯的思维方式和表达方式, 这就是面向对象方法所强调的
基本原则 。 软件开发从过分专业化的方法, 规则和技巧中回
到了客观世界, 回到了人们的日常思维, 所以说面向对象方
法是软件方法学的返朴归真 。
9.1.2 面向对象程序设计语言的四大家族
1,LISP家族
LISP是 50年代开发出来的一种语言,它以表处理为
特色,是一种人工智能语言,70年代以来,在 LISP
基础上开发了很多 LISP家族的面向对象语言。
Simula
Simula语言是 60年代开发出来的, 在 Simula中引入了几个面
向对象程序设计语言中最重要的概念和特性, 即数据抽象,
类和继承性机制 。 Simula67是它具有代表性的一个版本, 70
年代发展起来的 CLU,Ada,Modula-2等语言是在它的基础上
发展起来的 。
Smalltalk
Smalltalk是第一个真正的面向对象程序设计语言, 它体现了
纯粹的 OOP设计思想, 是最纯的 OOP语言 。 它起源于 Simula
语言 。 尽管 Smalltalk不断完善, 但在那个时期, 面向对象程
序设计语言并没有得到广泛的重视, 程序设计的主流是结构
化程序设计 。
C家族
在 19世纪 80年代, C语言成为一种极其流行, 应用非常广泛
的语言 。 C++是在 C语言的基础上进行扩充,
并增加了类似 Smalltalk语言中相应的对象机制 。 它
将, 类, 看作是用户定义类型, 使其扩充比较自然 。
C++以其高效的执行效率赢得了广大程序设计员的青睐,
在 C++中提供了对 C语言的兼容性, 因此, 很多已有的 C
程序稍加改造甚至不加改造就可以重用, 许多有效的算
法也可以重新利用 。 它是一种混合型的面向对象程序设
计语言, 由于它的出现, 才使面向对象的程序设计语言
越来越得到重视和广泛的应用 。
JAVA语言是一种适用于分布式计算的新型面向对象
程序设计语言, 可以看作是 C++语言的派生, 它从 C++语
言中继承了大量的语言成分, 抛弃了 C++语言中冗余的,
容易引起问题的功能, 增加了多线程, 异常处理, 网络
程序设计等方面的支持, 掌握了 C++语言, 可以很快学
会 JAVA 语言 。
面向对象语言
纯粹的面向对象语言
混合型的面向对象语言
强调开发快速原型的能力
运行效率
面
向
对
象
语
言
的
分
类
9.1.3 面向对象程序分析 OOA与设计 OOD的基本步骤
1,标识对象和它们的属性
标识应用系统的对象和它们的属性是面向对象设计方法中
最艰难的工作 。 首先要搞清楚系统要解决的问题到底涉及到
哪些事物以及它们在系统中的作用 。
事
物
的
分
类
客观存在物,包括有形对象和角色对象, 体
现问题的结构特性 。
行为,包括事件对象和交互对象 。 行为是对
象的一部分, 行为依赖于对象 。 它体现问题
的行为特性 。
概念,现实世界中事物和它们行为规律的抽
象, 是识别对象时的一类认识和分析对象 。
标识对象可以从应用系统非形式化描述中的名词来导出 。 对
象标识出来后, 还应注意对象之间的类似之处, 以建立对象
类 。
例
如,
Windows多窗口
…… 窗口 1 窗口 2 窗口 n
抽象其共同属性,
大小、位置、标题
构
造
类
2,标识每个对象所要求的操作和提供的操作
3,建立对象之间的联系和每个对象的接口
面向对象程序设计能支持
软件开发策略有,
编写可重用代码
编写可维护的代码
共享代码
简化已有的代码
9.2 类和对象
类和对象,
? 类是 C++的数据抽象和封装机制,它描述了一组具有
相同属性和行为特征(数据成员和成员函数)的对象
? 对象是类的实例。 类是对一组具有相同特征的对象 的
抽象描述,所有这些对象都是这个类的实例。
类是一种数据类型
对象是类类型的变量
说明,类和对象的关系相当于普通数据类型与其变量的关系。
类和对象的关系,
类是一种;逻辑抽象概念 。 声明一个类只是定义了
一种新的数据类型, 对象说明才真正创建了这种数据
类型的物理实体 。
类的定义,
class 类名 {
private,
// 私 有 数 据 成 员和 成 员函 数
public,
// 公 有 数 据 成 员和 成 员函 数
protected,
// 保护的数据成员和成员函数 };
class是定义类
的关键字
三种
访问
控制
权限
注
意
不
要
丢
掉
有关类定义的几点说明,
类 成 员
的 三 种
访 问 控
制 权 限,
私有成员 private
公有成员 public
保护成员 protected
缺省访问控制
处于类声明中的第
一部分,可省略
通过成员函数或某些特
殊说明的函数访问
一般是成员函数
公有派生类
成员函数
友元
类的成员函数
访问
结构和类的区别是,
在缺省情况下,结构体中的数据成员和成员函
数都是 公有 的,而在类中是 私有 的。
在所有其它方面,结构和类等价。
类定义举例,例:定义日期类
class Tdate //定义日期类
{
public,//定义公有成员函数
void Set(int m,int d,int y); //置日期值
int IsLeapYear(); //判是否闰年
void Print(); //输出日期值
private,//定义私有数据成员
int month;
int day;
int year;
}; //类定义体的结束
成员函数的定义,
成员函数是程序算法实现部分, 是对封装的
数据进行操作的唯一途径 。
成员函数的分类
外联 函数,
内联函数,
在类定义体中声明而在类外
定义的成员函数 。
内联函数是指程序在编译时将函数
的代码插入在函数的每个调用处,
作为函数体的内部扩展
(1) 外联函数
在类外定义成员函数的具体形式为,
返回值类型 类名,:成员函数名 (形式参数表)
{
函数体
}
作用域分隔符
Void Tdate::Set(int
m,int d,int y)
//置日期值
{ month=m;day=d;
year=y;
}
int Tdate::IsLeapYear()
//判是否闰年
{
return
(year%4==0&&year%100!=
0)||(year%400==0);
}
例 2
例 1
void Tdate::Print() //输出日期值
{
cout<<month<<”/”<<day <<”/”<<year<<endl;
}
例 3
(2) 内联成员函数 ( 内部函数, 内置函数 )
① 在类定义体内定义内联函数
class Tdate
{
public,
void Set(int m,int d,int y)
//置日期值
{
month=m; day=d;
year=y;
}
int IsLeapYear()
//判是否闰年
return (year%4==0&&year%100!=0)||(year%400==0);
}
void Print() //输出日期值
{
cout<<month<<”/”<<day<<”/”<<year<<endl;
}
private,
int month;
int day;
int year;
};
② 使用关键字 inline定义内联成员函数
inline void Tdate::Set(int m,int d,int y)
//置日期值
{
month=m; day=d; year=y;
} 或
void inline Tdate::Set(int m,int d,int y)
//置日期值
{
month=m; day=d; year=y;
}
9.2.2 对象
对象是类的实例, 是由数据及其操作所构成的封装体 。 对
象是面向对象方法的主体当一个对象映射为软件实现时由三
部分组成,
私有的数据结构,它用于描述对象的内部状态 。
处理,也称为操作或方法, 它施加于数据结构之上 。
接口,这是对象可被共享的部分, 消息通过接口调用相应
的操作 。 接口规定哪些操作是允许的 。 它不提供操作是如何
实现的信息 。
对象的定义,
(1) 方法一:在定义类的同时直接定义
class location
{
private,
int x,y;
public,
void init(int x0,int
y0);
int Getx( void );
int Gety( void );
}dot1,dot2;
例
(2) 方法二:在使用时定义对象
类名 标识符,...,标识符;
如, location dot1,dot2;
成员的访问
对象对成员的访问形式如下,
对象名,公有成员名
(*对象指针名 ).公有成员名
对象指针名 ->公有成员名
!!!需要注意,只有用 public定义的公有成员 才能使用圆点
操作符访问。对象中的私有成员是类中隐藏的数据,不允许
类外的程序中被直接访问,只能通过该类的公有成员函数来
访问它们。
class Myclock
{
private,
int hour,minute,
second;
public,
void init();
void updata();
void display();
};
Myclock clock,*pclock;
//定义对象 clock和指向 Myclock类
对象的指针 pclock
clock.init();
//通过对象访问公有成员函数
pclock=&clock;
//指针 pclock指向对象 clock
pclock->display();
//通过指针访问成员函数
clock.hour=4;
//错误, 因为对象不能访问其私有
成员
定义时钟类,
9.2.3 名字解析和 this指针
1,名字解析
在调用成员函数时, 通常使用缩写形式, 如上例中的
clock.init()就是 clock.Myclock::init()的缩写, 因此可以定义两
个或多个类的具有相同名字的成员而不会产生二义性 。
2,This指针
当一个成员函数被调用时,自动向它传递一个隐含的参数,
该参数是一个指向接受该函数调用的对象的指针,在程序中
可以使用关键字 this来引用该指针,因此称该指针为 this指针。
This指针是 C++实现封装的一种机制,它将成员和用于操作
这些成员的成员函数连接在一起
例如 Tdate类的成员函数 Set被定义为,
void Tdate::Set(int m,int d,int y) //置日期值
{
month=m; day=d; year=y;
}
注,其中对 month,day和 year的引用表示在该成员函数被调用时,
引用接受该函数调用的对象中的成员 month,day和 year。
例如, 对于下面的语句,
Tdate dd;
dd.Set(5,16,1991);
注,当调用成员函数 Set时,该成员函数的 this指针指向对
象 dd。成员函数 Set中对 month,day和 year的引用表示引用
对象 dd的成员 。
C++编译器所认识的成员函数 Set的定义形式为,
void Tdate::Set(int m,int d,int y) //置日期值
{
this->month=m; this->day=d; this->year=y;
}
注,即对于该成员函数中访问的任何类成员, C++编译器都
认为是访问 this指针所指向对象的成员 。 由于不同的对象调
用成员函数 Set()时, this指针指向不同的对象, 因此, 成员
函数 Set()可以为不同对象的 month,day和 year赋初值 。 使用
this指针, 保证了每个对象可以拥有不同的数据成员, 但处
理这些数据成员的代码可以被所有的对象共享 。
9.3 带缺省参数的函数和函数重载
9.3.1 带缺省参数的函数
如果在函数说明或函数定义中为形参指定一个
缺省值,则称此函数为带缺省参数的函数 。
如果在调用时, 指定了形参相对应的实参, 则形参使
用实参的值, 如果未指定相应的实参, 则形参使用缺省
值, 这为函数的使用提供了很大的便利 。
例如,函数 init 可以被说明为
void init(int x=4);
init(10);
init();
传递给形参
的值为 10
传递给形参的
值为 4
指定了初始值的参数称为缺省参数。
如果函数有多个缺省参数,则缺省参数必须是从 右向左 定
义,并且在一个缺省参数的右边不能有未指定缺省值的参数。
例如,
void fun(int a,int b=1,int c=4,int d=5);
此函数声明语句是 正确 的,
void fun(int a=3,int b=6,int c,int d);
void fun(int a=65,int b=3,int c,int d=3);
在一个缺省参数的右边不
能有未指定缺省值的参数
因为当编译器将实参与形参进行比较时,是从左
到右进行的,如果省去提供中间的实参,编译器
就无法区分随后的实参对应哪个形参。
【 例 9.1】 带缺省参数的函数
#include <iostream.h>
class Tdate
{
public,
void Set(int m=5,int d=16,int y=1991)
//置日期值
{ month=m; day=d; year=y; }
void Print() //输出日期值
{
cout<<month<<”/”<<day<<”/”<<year<<endl;
}
private,
int month;
int day;
int year;
};
void main()
{
Tdate a,b,c;
a.Set(4,12,1996);
b.Set(3);
c.Set(8,10);
a.Print();
b.Print();
c.Print();
}
程序的运行结果为,
4/12/1996
3/16/1991
8/10/1991
9.3.2 函数重载
C++编译系统允许为两个或两个以上的函数取 相同 的
函数名,但是形参的个数或者形参的类型不应相同,编译
系统会根据实参和形参的类型及个数的最佳匹配,自动确
定调用哪一个函数,这就是所谓的函数重载。
函数重载无需特别声明,只要所定义的函数与已
经定义的同名函数形参形式不完全相同,C++编译器
就认为是函数的重载
例如,
#include<iostream.h>
int max(int a,int b)
{if(a>b)
return a;
else
return b;}
float max(float a,float b)
{if(a>b)
return a;
else
return b;}
char * max(char *a,
char *b)
{
if(strcmp(a,b)>0)
return a;
else
return b;
}
这里定义了三个名为 max的函数, 它们的函数原型不同,
C++编译器在遇到程序中对 max函数的调用时将根据 参数形
式进行匹配, 如果找不到对应的参数形式的函数定义, 将
认为该函数没有函数原型, 编译器会给出错误信息 。
C++允许重载函数有数量不同的参数个数。当函数名相
同而参数个数不同时,C++会自动按参数个数定向到正确
的要调用的函数。
【 例 9.2】 重载函数应用举例
#include <iostream.h>
int add(int x,int y)
{
int sum;
void main( )
{
int a,b;
a=add(5,10);
b=add(5,10,20);
cout<<”a=”<<a<<endl;
cout<<”b=”<<b<<endl;
}
sum=x+y;
return sum;
}
int add(int x,int y,int z)
{
int sum;
sum=x+y+z;
return sum;
}
int func(int x);
float func(int x);
程序运行结果为,
a=15
b=35
在使用重载函数时要注意,
① 不可以定义两个具有 相同名称、相同参数类型和
相同参数个数,只是 函数返回值不同 的函数。
以下定义是
C++不允许的
② 如果某个函数参数有缺省值,必须保证其参数缺省后调用形
式不与其它函数混淆。
例如,
int f(int a,float b);
void f(int a,float b,int c=0); (× )
函数缺省参数 c后,其形式与第一个函数
参数形式相同 容易产生二义性
例如,f(10,2.0); 具有二义性
注,类的成员函数同样也可以重载,类的成员函数的重载
与全局函数的重载方法相同
9.4 构造函数和析构函数
9.4.1 构造函数
对象的初始化是指对象数据成员的初始化,在使用
对象前,一定要初始化,
对对象初始化的方法
普通成员函数来初始化
构造函数对对象进行
初始化
使用上的不便
和不安全
1.构造函数
构造函数是一个 与类同名, 没有返回值 的特殊成员函数
一般用于初始化类的数据成员,每当创建一个对象时(包括
使用 new动态创建对象),编译系统就 自动调用 构造函数 。
【 例 9.3】 构造函数的定义, 使用和重载
#include <iostream.h>
class test
{
private,
int num;
float f1;
public,
test();
int getint()
{return num;}
float getfloat()
{return f1;}
};
test(int n,float f);
//参数化的构造函数
test::test()
{
cout<<”Initializing
default”<<endl;
num = 0;
f1 = 0.0;
}
test::test(int n,float f)
{
cout<<“Initializing”<<n<<,,”
<<f<<endl;
num = n;
f1 = f;
}
void main(void)
{
test x;
test y(10,21.5);
}
执行结果为,
Initializing default
Initializing 10,21.5
注,类的构造函数一般是公有的 (public),但有时也声明为
私有的, 其作用是限制创建该类对象的范围, 即只能在本类
和友元中创建该类对象 。
2,带缺省参数的构造函数
【 例 9.4】 带缺省参数的构造函数
#include <iostream.h>
class Tdate{
public,
Tdate(int m=5,int d=16,int y=1990)
{
month=m; day=d; year=y;
cout <<month <<”/” <day
<<”/” <<year <<endl;
}
private,
int month;
int day;
int year;
};
void main()
{
Tdate aday;
Tdate bday(2);
Tdate cday(3,12);
Tdate
dday(1,2,1998);
}
程序的运行结果,
5/16/1990
2/16/1990
3/12/1990
1/2/1998
!!!注意,使用带缺省参数的构造函数时,要注意 避免二义性,所
带的参数个数或参数类型必须有所不同,否则系统调用时会出
现二义性 。
3.缺省构造函数(默认构造函数)
C++规定, 每个类必须有一个构造函数, 没有构造函
数, 就不能创建任何对象 。
若未提供一个类的构造函数, 则 C++提供一个缺省的
构造函数, 该缺省构造函数是个无参构造函数, 它仅负
责创建对象, 而不做任何初始化工作 。
只要一个类定义了一个构造函数 ( 不一定是无参构造
函数 ), C++就不再提供默认的构造函数 。 如果为类定义
了一个带参数的构造函数, 还想要无参构造函数, 则必
须自己定义 。
与变量定义类似, 在用默认构造函数创建对象时, 如果
创建的是全局对象或静态对象, 则对象的位模式全为 0,否
则, 对象值是随机的 。
【 例 9.5】 缺省构造函数
#include <string.h>
class Student{
public,
Student(char* pName)
{
strncpy(name,pName,sizeof(n
ame));
name[sizeof(name)-
1]=’\0’;
}
Student()
{}
//不能省略, 因为在 main()
中创建无参对象 noName
时使用
};
protected,
char name[20];
void main()
{
Student noName;
Student ss(“Jenny”);
}
4,拷贝构造函数(复制构造函数)
(1) 功能,拷贝构造函数的功能是用一个已有的对象来初始化
一个被创建的同类的对象,
(2) 特点,一种特殊的构造函数,具有一般构造函数的所
有特性,其形参是本类对象的引用。
拷贝构造函数的声明形式为,
类名 (类名 &对象名 );
例,class cat
{
private,
int age;
float weight;
char *color;
类 cat的定义
public,
cat();
cat(cat &);
void play();
void hunt();
};
cat::cat(cat &other)
{
age = other.age;
weight=other.weight;
color = other.color;
}
拷贝构造函数的
声明
拷贝构造函数
的定义
调
用
拷
贝
构
造
函
数
有
以
下
三
种
情
况
(1) 用类的一个对象去初始化另一个对象时。
(2) 对象作为函数参数传递时,调用拷贝构造函数
(3) 如果函数的返回值是类的对象,函数调用
返回时,调用拷贝构造函数。
例,cat cat1; cat cat2(cat1);
//创建 cat2时系统自动调用拷贝构造函数
用 cat1初始化 cat2。
例 f(cat a){ } //定义 f函数, 形参为 cat类对象
cat b; //定义对象 b
f(b);//进行 f函数调用时, 系统自动调用拷贝
构造函数
例 cat f() //定义 f函数, 函数的返回值为 cat类的对象
{ cat a;
…
return a;
}
cat b;//定义对象 b
b=f();//调用 f函数, 系统自动调用拷贝构造函数
!!! 注意,
由 C++提供的默认拷贝构造函数只是对对象进行浅拷贝
复制(逐个成员依次拷贝)。
如果对象的数据成员包括指向堆空间的指针,就不能使
用这种拷贝方式,因为两个对象都拥有同一个资源,对象析
构时,该资源将经历两次资源返还,此时必须自定义深拷贝
构造
下面的程序段定义了深拷贝构造函数,
#include<string.h>
class Person
{
public,
Person(char *name)
//构造函数
{
pname=new
char[strlen(name)+1];
if(pname!=0)
//使用 new进行动态内
存分配
{strcpy(pname,name);}
}
Person(Person&p)
// 拷贝构造函数
{
pname=new
char[strlen(p.pname)+
1];
//复制资源
if(pname!=0)
strcpy(pname,p.pname);
// 复制对象空间
}
//其它成员函数
private,
char *pname;
};//类定义的结束
5,构造初始化表
构造函数也可使用构造初始化表对数据成员进行初始化,
例,Circle::Circle(float r)
{radius=r; }
可改写为,
Circle::Circle(float r):radius(r){}
注,成员初始化的次序取决于它们在类定义中的 声明
次序, 与它们在成员初始化表中的次序无关 。
6,类类型和基本数据类型的转换
(1) 构造函数用作类型转换
通过构造函数进行类型转换必须有一个前提,那就是此
类一定要有一个只带一个参数的构造函数,这样才可以
实现从参数类型向该类类型的转换,并且这种转换是隐
式的。
class A
{ …
public,
A();
A(int);
};…
例,f(A a);
//f函数的形参为 A类的对象
f(1);
/*进行 f函数调用时先构造一
个 A类对象, 调用 A::A(int)
进行类型转换, 然后把它传
给函数 f。 */
(2) 类类型转换函数
通过构造函数进行类类型的转换只能从参数类型向类类型转
换,类类型转换函数用来将类类型向基本类型转换。
为类定义一类型转换函数的语法为,
① 在类定义体中声明
operator type();
要转向的基本类型名
注,此函数既没有参数, 又没有返回类型, 但在函数体中
必须返回具有 type类型的一个对象 。
② 定义转换函数的函数体
类名, opertor type()
{
//
return type类型的值
}
③ 使用类型转换函数,
使用类型转换函数与对基本类型进行强制转换时
一样, 就象是一种函数调用过程 。
【 例 9.6】 类型转换函数
#include<iostream.h>
class RMB
{
public,
RMB(double value = 0.0);
//构造函数用作类型转换
operator double(){ return yuan + jf / 100.0; }
//类类型转换函数
void display(){ cout << (yuan + jf / 100.0) << endl; }
RMB::RMB(double value)
{
yuan = value;
jf = ( value – yuan ) * 100 + 0.5;
}
void main()
{
RMB d1(2.0),d2(1.5),d3;
d3 = RMB((double)d1 + (double)d2);
//显式转换
d3 = d1 + d2;
//隐式转换
d3.display();
}
程序执行结果为,
3.5
9.4.2 析构函数
1,析构函数及其作用
功能, 当对象被撤消时,释放该对象占用的内存空间
特点,析构函数的作用与构造函数正好相反,一般情况下,
析构函数执行构造函数的逆操作。在对象消亡时,系统将
自动调用析构函数。析构函数 没有返回值, 没有参数,每
个类只有一个析构函数。析构函数的函数名为类名前加 ~。
析
构
函
数
在
以
下
情
况
下
自
动
被
调
用
(1) 一个动态分配的对象被删除, 即使
用 delete删除对象时, 编译系统会自动
调用析构函数
(2) 程序运行结束时
(3) 一个编译器生成的临时对象不再需
要
2,当用户在程序中手工调用析构函数时, 语法如下,
对象名,类名,:析构函数名
注,而构造函数只能由系统调用,不能用上述方法调用
3,析构函数与构造函数的调用顺序刚好相反
【 例 9.7】 析构函数和构造函数的调用顺序
#include <iostream.h>
#include <string.h>
class Student{
public,
Student(char* pName=”no name”,int ssId=0)
{
strncpy(name,pName,40);
name[39]=’\0’;
id = ssId;
cout <<”Constructing new student
,<<pName <<endl;
}
Student(Student& s) //拷贝构造函数
{
cout <<”Constructing copy of, <<s.name
<<endl;
strcpy(name,”copy of,);
strcat(name,s.name);
id=s.id;
}
~Student()
{
cout <<”Destructing, <<name <<endl;
}
protected,
char name[40];
int id;
};
void fn(Student s)
{
析构函数的定义
cout <<”In function fn()\n”;
//fn函数调用结束时, 析构对象 s
}
void main()
{
Student randy(“”Randy”,1234);
//调用构造函数, 创建对象 randy
Student wang(“wang”,5678);
// 调用构造函数, 创建对象 wang
cout <<”Calling fn()\n”;
运行结果为,
Constructing new
tudent Randy
Constructing new
student wang
Calling fn()
Constructing copy
of Randy
In function fn()
Destructing copy
of Randy
Returned from fn()
Destructing wang
Destructing Randy
fn(randy);
//调用 fn函数,参数传递时
调用拷贝构造函数
cout <<”Returned from fn()\n”;
//主函数调用结束时,
先析构对象 wang,
再析构对象 randy
}
9.5 对象成员、静态成员
9.5.1 对象成员
定义,对象成员也称为类的聚集, 是指在类的定义中数据成
员可以为其它的类对象, 即类对象作为成员 。
如果在类定义中包含有对象成员, 则在创建类对象时先
调用对象成员的构造函数, 再调用类本身的构造函数 。 析构
函数的调用顺序正好相反 。
从实现的角度讲, 实际上是首先调用类本身的构造函数,
在执行本身构造函数的函数体之前, 调用成员对象的构造函
数, 然后再执行类本身构造函数的函数体 。 因此, 在构造函
数的编译结果中包含了对对象成员的构造函数的调用, 至于
调用对象成员的哪一个构造函数, 是由成员初始化表指定的;
当成员初始化表为空时, 则调用对象成员
【 例 9.8】 含有对象成员的类的构造函数和析构函
数的调用顺序
#include <iostream.h>
#include <string.h>
class StudentID{
public,
StudentID(int id=0)
//带缺省参数的构造函数
{
value=id;
cout <<”Assigning
student id, <<value
<<endl;
}
~StudentID()
{
cout
<<“Destructing
id”<<value <<endl;
class Student{
public,
Student(char* pName=“no name”,int
ssID=0):id(ssID)
{
cout <<”Constructing student, <<pName
<<endl;
strncpy(name,pName,sizeof(name));
name[sizeof(name)-1]=’\n’;
}
~Student()
{cout<<“Deconstructing student
"<name<<endl;}
protected,
char name[20];
StudentID id;
//对象成员
};
void main()
{
Student s(“wang”,9901);
程序的运行结果,
Assigning student id 9901
Constructing student wang
Assigning student id 0
Constructing student li
Deconstructing student li
Destructing id 0
Deconstructing student wang
Destructing id 9901
9.5.2 静态成员
定义,在类的定义中, 它的数据成员和成员函数可以声
明成静态的, 即用关键字 static,这些成员就被称为静态
成员 。
特征,不管这个类创建了多少个对象, 而其静态成员只
有一个副本, 此副本被这个类的所有对象共享 。 静态成
员分为 静态数据成员 和 静态成员函数 。
1,静态数据成员
静态数据成员被存放在内存某一单元内,该类的所有对
象都可以访问它。无论建立多少个该类的对象,都只有
一个静态数据的拷贝。由于静态数据成员仍是类成员,
因而具有很好的安全性能。当这个类的第一个对象被建
立时,所有 static数据都被初始化,并且,以后再建立
对象时,就不需再对其初始化。初始化在类体外进行。
格式如下,
〈 类型 〉 〈 类名 〉 ∷ 〈 数据成员名 〉 =〈 初始值 〉
【 例 9.9】 静态数据成员的使用
#include<iostream.h>
class A
{
static int i;
//定义静态数据成员
public,
A(){i++;}
int list(){return i;}
};
int A::i=0;
//请注意静态成员数
据的初始化格式,无
static关键字
程序的执行结果,
3,3,3
void main()
{
A a1,a2,a3;
cout<<a1.list()<<','<<a2
.list()<<','<<a3.list();
//显示均为 3(因为创建
三个对象, 三次使得静
态数据成员加 1)
}
2,静态成员函数
特点,
(1)静态成员函数无 this指针, 他是同类的所有对
象共享的资源, 只有一个共用的副本 。 而一般
的成员函数中都含有一个 this指针, 指向对象自
身 。
(2) 在静态成员函数中访问的基本上是静态数据
成员或全局变量 。
(3) 在调用静态成员函数的前面, 必须缀上对象
名或类名, 经常用类名 。
(4) 静态成员函数的使用虽然不针对某一个特定
的对象, 但在使用时系统中必须已经存在此类
的对象 。
注,由于静态成员函数属于类独占的成员函数, 因
此访问静态成员函数的消息接收者 不是类对象, 而
是类自身 。 C++语言规定使用类作用域符号,:表示
是类身份的对象标识 。
如在上例中在函数 list()前附加声明 static后, 便可
将 main()中的语句, a1.list();” 改写成, A::list();”
的形式, 运行结果相同 。
9.6 友元
友元的分类,友元函数, 友元成员和友元类三种
1.友元函数
友元函数是一种说明在类定义体内的非成员函数。
说明友元函数的方法如下,
friend 〈 返回值类型 〉 〈 函数名 〉 (〈 参数表 〉 )
{〈 函数体 〉 }
说明,
(1) 友元函数是在类中说明的一个函数,它不是该
类的成员函数,但允许访问该类的所有成员。他
是独立于任何类的一般的外界函数。友元并不在
类的范围中,它们也不用成员选择符 (.或 ->)调用,除
非它们是其它类的成员。
(2) 由于友元函数不是类的成员,所以没有 this指
针,访问该类的对象的成员时,必须使用对象名,
而不能直接使用类的成员名。
(3) 虽然友元函数是在类中说明的,但其名字的作
用域在类外,作用域的开始点在说明点,结束点
和类名相同。因此,友元说明可以代替该函数的
函数说明。
(4) 如果在说明友元时给出了该函数的函数体代码,
则它是内联的。
【 例 9.10】 友元函数的定义和使用
#include<iostream.h>
#include<string.h>
class Student
{
private,
char name[10],num[10];
friendvoidshow(Student& st)
//友元函数的声明和定义
{cout<<"Name:"<<st.name<<"\n"; }
public,
Student(char *s1,char *s2)
{ strcpy(name,s1);
strcpy(num,s2);}
};
class Score
{
unsigned int mat,phy,eng;
friend void show_all(Student&,Score*);
//友元函数的声明
public,
Score(unsigned int i1,unsigned int i2,
unsigned int i3):mat(i1),phy(i2),eng(i3)
{}
};
void show_all(Student& st,Score* sc)
{
show(st);
cout<<"Mathematics:"<<sc->mat
<<"\nPhyics:"<<sc->phy
<<"\nEnglish:"<<sc->eng<<endl;
public,
Score(unsigned int i1,unsigned int
i2,unsigned int i3):mat(i1),phy(i2),eng(i3)
{}
};
void show_all(Student& st,Score* sc)
{ show(st);
cout<<"Mathematics:"<<sc->mat
<<"\nPhyics:"<<sc->phy
<<"\nEnglish:"<<sc->eng<<endl;}
void main()
{
Student wang("Wang","9901");
Score ss(72,82,92);
show_all(wang,&ss);
}
程序的运行结果,
Name:Wang
Mathematics:72
Phyics:82
English:92
2,友元成员
另一个类的成员函数可以作为某个类的友元,只
是在声明友元函数时要加上成员函数所在的类名,
称为友元成员。
声明如下,
friend 类名 ∷ 成员函数名
【 例 9.11】 友元成员
#include<iostream.h>
#include<string.h>
class Student;
//声明引用的类名
class Score
{
unsigned int mat,phy,eng;
public,
Score(unsigned int i1,unsigned int i2,
unsigned int i3):mat(i1),phy(i2),eng(i3){}
void show()
{
cout<<"Mathematics:"<<mat<<"\nPhyics
:"<<phy
void show(Student&);
};
class Student
{
friend void Score::show(Student&);
//声明友元成员
char name[10],num[10];
public,
Student(char *s1,char *s2)
{strcpy(name,s1); strcpy(num,s2);}
};
void Score::show(Student& st)
{
cout<<"Name:"<<st.name<<"\n";
show();
}
void main()
{
Student wang("Wang","9901");
Score ss(72,82,92);
ss.show(wang); }
程序的运行结果,
Name:Wang
Mathematics:72
Phyics:82
English:92
3.友元类
某一个类可以是另一个类的友元,这样作为友
元的类中的所有成员函数都可以访问另一个类
中的私有成员。
友元类的说明方式如下,
friend class 〈 类名 〉
【 例 9.12】 友元类
#include<iostream.h>
#include<string.h>
class Student
{
friend class Score;
//声明 Score类为 Student类的友元类
char name[10],num[10];
public,
Student(char *s1,char *s2)
{strcpy(name,s1);
strcpy(num,s2);}
};
class Score
{
unsigned int mat,phy,eng;
public,
Score(undsige int i1,undsige int i2,undsige int
i3),phy(i2),eng(i3)
{}
void show()
{cout<<"Mathematics:"<<mat<<"\nPhyics:"<<
phy<<"\nEnglish:"<<eng<<endl;}
void show(Student&);
};
void Score::show(Student& st)
void Score::show(Student& st)
{
cout<<"Name:"<<st.name<<"\n";
show();
}
void main()
{
Student wang("Wang","9901");
Score ss(72,82,92);
ss.show(wang); }
程序的运行结果是,
Name:Wang
Mathematics:72
Phyics:82
English:92
注,例中由于声明 Score为 Student的友元类, 此
时 Score中的成员函数便可以直接访问 Stuednt对
象的成员 。 这样 Score的成员函数 Show(Student&
st)中的, st,name”的语句才是允许的 。
9.7 类模板和模板类
模板是一种描述函数或类的特性的蓝图 。
模板分为函数模板和类模板, 可以从一个
函数模板生成多个函数或从一个类模板生
成多个类 。 建立一个模板后, 编译器将根
据需要从模板生成多份代码 。
模
板
的
定
义
与
性
质
类模板为类定义一种模式, 使得类
中的某些数据成员, 某些成员函数的
参数, 某些成员函数的返回值, 能取
任意类型 ( 包括系统预定义的和用户
自定义的 )
类
模
板的
定
义
与
性
质
C++编译器根据类模板和特定的数据类型来
产生一个类,即模板类(这是一个类)。
类模板是一个抽象的类,而模板类是实实
在在的类,是由类模板和实际类型结合后
由编译器产生一个类的实例。这个类名就
是抽象类名和实际数据类型的结合
模
板
类
的
定
义
与
性
质
如,
TclassName<int> 整体是一个类名,包括尖
括号和 int。而通过这个类才可以产生对象
(类的实例)。
注,对象是类的是实例,而模板类是类模板的实例
mm,kmjkj
类模板由 C++语言的关键字 template引入, 定义
的语法形式如下,
template <class 类属参数 1,class 类属参数 2,…… >
class name
{ }
形式类属参数表每个类属
参数由关键字 class引入
【 例 9.13】 类模板和模板类的使用
#include <iostream.h>
template<class T>
class tem
{
T *data;
int size;
public,
tem(int);
~tem( )
{delete [ ]data;}
T& operator[ ](int i)
{return data[i];} };
template<class T>
tem<T>::tem(int n)
{
data = new T[n];
size = n;
}
void main( )
{
tem<int> x(5);
int i;
/‘
for(i=0;i<5;i++)
x[i]=i;
for(i=0;i<5;i++)
cout<<x[i]<< ' ';
cout<<'\n';
}
程序的运行结果为
0 1 2 3 4
注,
这个例子中定义了一
个 类模板 tem,它有
一个类型参数 T,类
模板的定义由
template<class T>开
始,下面的类模板定
义体部分与普通类定
义的方法相同,只是
在部分地方用参数 T
表示数据类型。
模板类的每一个非内置函数的定义是一个独立
的模板, 同样以 template<class T>开始, 函数头中
的类名由模板类名加模板参数构成, 如例子中的
tmp<T>。
模板类在使用时必须指明参数, 形式为:模板
类名 <模板实参表 >。 主函数中的 tmp<int>即是如
此, 编译器根据参数 int生成一个实实在在的类即
模板类, 理解程序时可以将 temp<int>看成是一个
完整的类名 。 在使用时 temp不能单独出现, 它总
是和尖括号中的参数表一起出现
上面的例子中模板只有一个表示数据类型的参数,
多个参数以及其它类型的参数也是允许的 。
关于模板类的几点说明,
基础
2004年 3月
河北建筑工程学院
9.1 面向对象程序设计概述
9.1.1 面向对象是软件方法学的返朴归真
客观世界是由许多具体的事物、抽象的概念、规则等组成的,
我们将任何感兴趣或要加以研究的事、物、概念统称为对象
( Object)。每个对象都有各自的内部状态和运动规律,不
同对象之间通过消息传送进行相互作用和联系就构成了各种
不同的系统。面向传统的结构化方法强调的是功能抽象和模
块化,每个模块都是一个过程。
结构化方法处理问题是以过程为中心。面向对象强调的是功
能抽象和数据抽象,用对象来描述事物和过程。而对象包含
数据和对数据的操作,是对数据和功能的抽象和统一。面向
对象方法处理问题的过程是对一系列相关对象的操纵,即发
送消息到目标对象中,由对象执行相应的操作。
从结构到类
程序 =数据结构 +算法
程序 =(数据结构 +算法)
+ (数据结构 +算法) ……
程序 =对象 +对象 ……
因此面向对象方法是 以对象为中心 的, 这种以对象为中心
的方法更自然, 更直接地反映现实世界的问题空间, 具有独
特的抽象性, 封装性, 继承性和多态性, 能更好地适应复杂
大系统不断发展与变化的要求 。
采用对象的观点看待所要解决的问题, 并将其抽象为系统
是极其自然与简单的, 因为它符合人类的思维习惯, 使得应
用系统更容易理解 。 同时, 由于应用系统是由相互独立的对
象构成的, 使得系统的修改可以局部化, 因此系统更易于维
护 。
软件开发从本质上讲就是对软件所要处理的问题域进行
正确的认识,并把这种认识正确地描述出来。
既然如此, 那就应该 直接面对问题域中客观存在的事物来
进行软件开发, 这就是 面向对象 。 另一方面, 人类在认识世
界的历史长河中形成的普遍有效的思维方法, 在软件开发中
也应该是适用的 。 在软件开发中尽量采用人们在日常生活中
习惯的思维方式和表达方式, 这就是面向对象方法所强调的
基本原则 。 软件开发从过分专业化的方法, 规则和技巧中回
到了客观世界, 回到了人们的日常思维, 所以说面向对象方
法是软件方法学的返朴归真 。
9.1.2 面向对象程序设计语言的四大家族
1,LISP家族
LISP是 50年代开发出来的一种语言,它以表处理为
特色,是一种人工智能语言,70年代以来,在 LISP
基础上开发了很多 LISP家族的面向对象语言。
Simula
Simula语言是 60年代开发出来的, 在 Simula中引入了几个面
向对象程序设计语言中最重要的概念和特性, 即数据抽象,
类和继承性机制 。 Simula67是它具有代表性的一个版本, 70
年代发展起来的 CLU,Ada,Modula-2等语言是在它的基础上
发展起来的 。
Smalltalk
Smalltalk是第一个真正的面向对象程序设计语言, 它体现了
纯粹的 OOP设计思想, 是最纯的 OOP语言 。 它起源于 Simula
语言 。 尽管 Smalltalk不断完善, 但在那个时期, 面向对象程
序设计语言并没有得到广泛的重视, 程序设计的主流是结构
化程序设计 。
C家族
在 19世纪 80年代, C语言成为一种极其流行, 应用非常广泛
的语言 。 C++是在 C语言的基础上进行扩充,
并增加了类似 Smalltalk语言中相应的对象机制 。 它
将, 类, 看作是用户定义类型, 使其扩充比较自然 。
C++以其高效的执行效率赢得了广大程序设计员的青睐,
在 C++中提供了对 C语言的兼容性, 因此, 很多已有的 C
程序稍加改造甚至不加改造就可以重用, 许多有效的算
法也可以重新利用 。 它是一种混合型的面向对象程序设
计语言, 由于它的出现, 才使面向对象的程序设计语言
越来越得到重视和广泛的应用 。
JAVA语言是一种适用于分布式计算的新型面向对象
程序设计语言, 可以看作是 C++语言的派生, 它从 C++语
言中继承了大量的语言成分, 抛弃了 C++语言中冗余的,
容易引起问题的功能, 增加了多线程, 异常处理, 网络
程序设计等方面的支持, 掌握了 C++语言, 可以很快学
会 JAVA 语言 。
面向对象语言
纯粹的面向对象语言
混合型的面向对象语言
强调开发快速原型的能力
运行效率
面
向
对
象
语
言
的
分
类
9.1.3 面向对象程序分析 OOA与设计 OOD的基本步骤
1,标识对象和它们的属性
标识应用系统的对象和它们的属性是面向对象设计方法中
最艰难的工作 。 首先要搞清楚系统要解决的问题到底涉及到
哪些事物以及它们在系统中的作用 。
事
物
的
分
类
客观存在物,包括有形对象和角色对象, 体
现问题的结构特性 。
行为,包括事件对象和交互对象 。 行为是对
象的一部分, 行为依赖于对象 。 它体现问题
的行为特性 。
概念,现实世界中事物和它们行为规律的抽
象, 是识别对象时的一类认识和分析对象 。
标识对象可以从应用系统非形式化描述中的名词来导出 。 对
象标识出来后, 还应注意对象之间的类似之处, 以建立对象
类 。
例
如,
Windows多窗口
…… 窗口 1 窗口 2 窗口 n
抽象其共同属性,
大小、位置、标题
构
造
类
2,标识每个对象所要求的操作和提供的操作
3,建立对象之间的联系和每个对象的接口
面向对象程序设计能支持
软件开发策略有,
编写可重用代码
编写可维护的代码
共享代码
简化已有的代码
9.2 类和对象
类和对象,
? 类是 C++的数据抽象和封装机制,它描述了一组具有
相同属性和行为特征(数据成员和成员函数)的对象
? 对象是类的实例。 类是对一组具有相同特征的对象 的
抽象描述,所有这些对象都是这个类的实例。
类是一种数据类型
对象是类类型的变量
说明,类和对象的关系相当于普通数据类型与其变量的关系。
类和对象的关系,
类是一种;逻辑抽象概念 。 声明一个类只是定义了
一种新的数据类型, 对象说明才真正创建了这种数据
类型的物理实体 。
类的定义,
class 类名 {
private,
// 私 有 数 据 成 员和 成 员函 数
public,
// 公 有 数 据 成 员和 成 员函 数
protected,
// 保护的数据成员和成员函数 };
class是定义类
的关键字
三种
访问
控制
权限
注
意
不
要
丢
掉
有关类定义的几点说明,
类 成 员
的 三 种
访 问 控
制 权 限,
私有成员 private
公有成员 public
保护成员 protected
缺省访问控制
处于类声明中的第
一部分,可省略
通过成员函数或某些特
殊说明的函数访问
一般是成员函数
公有派生类
成员函数
友元
类的成员函数
访问
结构和类的区别是,
在缺省情况下,结构体中的数据成员和成员函
数都是 公有 的,而在类中是 私有 的。
在所有其它方面,结构和类等价。
类定义举例,例:定义日期类
class Tdate //定义日期类
{
public,//定义公有成员函数
void Set(int m,int d,int y); //置日期值
int IsLeapYear(); //判是否闰年
void Print(); //输出日期值
private,//定义私有数据成员
int month;
int day;
int year;
}; //类定义体的结束
成员函数的定义,
成员函数是程序算法实现部分, 是对封装的
数据进行操作的唯一途径 。
成员函数的分类
外联 函数,
内联函数,
在类定义体中声明而在类外
定义的成员函数 。
内联函数是指程序在编译时将函数
的代码插入在函数的每个调用处,
作为函数体的内部扩展
(1) 外联函数
在类外定义成员函数的具体形式为,
返回值类型 类名,:成员函数名 (形式参数表)
{
函数体
}
作用域分隔符
Void Tdate::Set(int
m,int d,int y)
//置日期值
{ month=m;day=d;
year=y;
}
int Tdate::IsLeapYear()
//判是否闰年
{
return
(year%4==0&&year%100!=
0)||(year%400==0);
}
例 2
例 1
void Tdate::Print() //输出日期值
{
cout<<month<<”/”<<day <<”/”<<year<<endl;
}
例 3
(2) 内联成员函数 ( 内部函数, 内置函数 )
① 在类定义体内定义内联函数
class Tdate
{
public,
void Set(int m,int d,int y)
//置日期值
{
month=m; day=d;
year=y;
}
int IsLeapYear()
//判是否闰年
return (year%4==0&&year%100!=0)||(year%400==0);
}
void Print() //输出日期值
{
cout<<month<<”/”<<day<<”/”<<year<<endl;
}
private,
int month;
int day;
int year;
};
② 使用关键字 inline定义内联成员函数
inline void Tdate::Set(int m,int d,int y)
//置日期值
{
month=m; day=d; year=y;
} 或
void inline Tdate::Set(int m,int d,int y)
//置日期值
{
month=m; day=d; year=y;
}
9.2.2 对象
对象是类的实例, 是由数据及其操作所构成的封装体 。 对
象是面向对象方法的主体当一个对象映射为软件实现时由三
部分组成,
私有的数据结构,它用于描述对象的内部状态 。
处理,也称为操作或方法, 它施加于数据结构之上 。
接口,这是对象可被共享的部分, 消息通过接口调用相应
的操作 。 接口规定哪些操作是允许的 。 它不提供操作是如何
实现的信息 。
对象的定义,
(1) 方法一:在定义类的同时直接定义
class location
{
private,
int x,y;
public,
void init(int x0,int
y0);
int Getx( void );
int Gety( void );
}dot1,dot2;
例
(2) 方法二:在使用时定义对象
类名 标识符,...,标识符;
如, location dot1,dot2;
成员的访问
对象对成员的访问形式如下,
对象名,公有成员名
(*对象指针名 ).公有成员名
对象指针名 ->公有成员名
!!!需要注意,只有用 public定义的公有成员 才能使用圆点
操作符访问。对象中的私有成员是类中隐藏的数据,不允许
类外的程序中被直接访问,只能通过该类的公有成员函数来
访问它们。
class Myclock
{
private,
int hour,minute,
second;
public,
void init();
void updata();
void display();
};
Myclock clock,*pclock;
//定义对象 clock和指向 Myclock类
对象的指针 pclock
clock.init();
//通过对象访问公有成员函数
pclock=&clock;
//指针 pclock指向对象 clock
pclock->display();
//通过指针访问成员函数
clock.hour=4;
//错误, 因为对象不能访问其私有
成员
定义时钟类,
9.2.3 名字解析和 this指针
1,名字解析
在调用成员函数时, 通常使用缩写形式, 如上例中的
clock.init()就是 clock.Myclock::init()的缩写, 因此可以定义两
个或多个类的具有相同名字的成员而不会产生二义性 。
2,This指针
当一个成员函数被调用时,自动向它传递一个隐含的参数,
该参数是一个指向接受该函数调用的对象的指针,在程序中
可以使用关键字 this来引用该指针,因此称该指针为 this指针。
This指针是 C++实现封装的一种机制,它将成员和用于操作
这些成员的成员函数连接在一起
例如 Tdate类的成员函数 Set被定义为,
void Tdate::Set(int m,int d,int y) //置日期值
{
month=m; day=d; year=y;
}
注,其中对 month,day和 year的引用表示在该成员函数被调用时,
引用接受该函数调用的对象中的成员 month,day和 year。
例如, 对于下面的语句,
Tdate dd;
dd.Set(5,16,1991);
注,当调用成员函数 Set时,该成员函数的 this指针指向对
象 dd。成员函数 Set中对 month,day和 year的引用表示引用
对象 dd的成员 。
C++编译器所认识的成员函数 Set的定义形式为,
void Tdate::Set(int m,int d,int y) //置日期值
{
this->month=m; this->day=d; this->year=y;
}
注,即对于该成员函数中访问的任何类成员, C++编译器都
认为是访问 this指针所指向对象的成员 。 由于不同的对象调
用成员函数 Set()时, this指针指向不同的对象, 因此, 成员
函数 Set()可以为不同对象的 month,day和 year赋初值 。 使用
this指针, 保证了每个对象可以拥有不同的数据成员, 但处
理这些数据成员的代码可以被所有的对象共享 。
9.3 带缺省参数的函数和函数重载
9.3.1 带缺省参数的函数
如果在函数说明或函数定义中为形参指定一个
缺省值,则称此函数为带缺省参数的函数 。
如果在调用时, 指定了形参相对应的实参, 则形参使
用实参的值, 如果未指定相应的实参, 则形参使用缺省
值, 这为函数的使用提供了很大的便利 。
例如,函数 init 可以被说明为
void init(int x=4);
init(10);
init();
传递给形参
的值为 10
传递给形参的
值为 4
指定了初始值的参数称为缺省参数。
如果函数有多个缺省参数,则缺省参数必须是从 右向左 定
义,并且在一个缺省参数的右边不能有未指定缺省值的参数。
例如,
void fun(int a,int b=1,int c=4,int d=5);
此函数声明语句是 正确 的,
void fun(int a=3,int b=6,int c,int d);
void fun(int a=65,int b=3,int c,int d=3);
在一个缺省参数的右边不
能有未指定缺省值的参数
因为当编译器将实参与形参进行比较时,是从左
到右进行的,如果省去提供中间的实参,编译器
就无法区分随后的实参对应哪个形参。
【 例 9.1】 带缺省参数的函数
#include <iostream.h>
class Tdate
{
public,
void Set(int m=5,int d=16,int y=1991)
//置日期值
{ month=m; day=d; year=y; }
void Print() //输出日期值
{
cout<<month<<”/”<<day<<”/”<<year<<endl;
}
private,
int month;
int day;
int year;
};
void main()
{
Tdate a,b,c;
a.Set(4,12,1996);
b.Set(3);
c.Set(8,10);
a.Print();
b.Print();
c.Print();
}
程序的运行结果为,
4/12/1996
3/16/1991
8/10/1991
9.3.2 函数重载
C++编译系统允许为两个或两个以上的函数取 相同 的
函数名,但是形参的个数或者形参的类型不应相同,编译
系统会根据实参和形参的类型及个数的最佳匹配,自动确
定调用哪一个函数,这就是所谓的函数重载。
函数重载无需特别声明,只要所定义的函数与已
经定义的同名函数形参形式不完全相同,C++编译器
就认为是函数的重载
例如,
#include<iostream.h>
int max(int a,int b)
{if(a>b)
return a;
else
return b;}
float max(float a,float b)
{if(a>b)
return a;
else
return b;}
char * max(char *a,
char *b)
{
if(strcmp(a,b)>0)
return a;
else
return b;
}
这里定义了三个名为 max的函数, 它们的函数原型不同,
C++编译器在遇到程序中对 max函数的调用时将根据 参数形
式进行匹配, 如果找不到对应的参数形式的函数定义, 将
认为该函数没有函数原型, 编译器会给出错误信息 。
C++允许重载函数有数量不同的参数个数。当函数名相
同而参数个数不同时,C++会自动按参数个数定向到正确
的要调用的函数。
【 例 9.2】 重载函数应用举例
#include <iostream.h>
int add(int x,int y)
{
int sum;
void main( )
{
int a,b;
a=add(5,10);
b=add(5,10,20);
cout<<”a=”<<a<<endl;
cout<<”b=”<<b<<endl;
}
sum=x+y;
return sum;
}
int add(int x,int y,int z)
{
int sum;
sum=x+y+z;
return sum;
}
int func(int x);
float func(int x);
程序运行结果为,
a=15
b=35
在使用重载函数时要注意,
① 不可以定义两个具有 相同名称、相同参数类型和
相同参数个数,只是 函数返回值不同 的函数。
以下定义是
C++不允许的
② 如果某个函数参数有缺省值,必须保证其参数缺省后调用形
式不与其它函数混淆。
例如,
int f(int a,float b);
void f(int a,float b,int c=0); (× )
函数缺省参数 c后,其形式与第一个函数
参数形式相同 容易产生二义性
例如,f(10,2.0); 具有二义性
注,类的成员函数同样也可以重载,类的成员函数的重载
与全局函数的重载方法相同
9.4 构造函数和析构函数
9.4.1 构造函数
对象的初始化是指对象数据成员的初始化,在使用
对象前,一定要初始化,
对对象初始化的方法
普通成员函数来初始化
构造函数对对象进行
初始化
使用上的不便
和不安全
1.构造函数
构造函数是一个 与类同名, 没有返回值 的特殊成员函数
一般用于初始化类的数据成员,每当创建一个对象时(包括
使用 new动态创建对象),编译系统就 自动调用 构造函数 。
【 例 9.3】 构造函数的定义, 使用和重载
#include <iostream.h>
class test
{
private,
int num;
float f1;
public,
test();
int getint()
{return num;}
float getfloat()
{return f1;}
};
test(int n,float f);
//参数化的构造函数
test::test()
{
cout<<”Initializing
default”<<endl;
num = 0;
f1 = 0.0;
}
test::test(int n,float f)
{
cout<<“Initializing”<<n<<,,”
<<f<<endl;
num = n;
f1 = f;
}
void main(void)
{
test x;
test y(10,21.5);
}
执行结果为,
Initializing default
Initializing 10,21.5
注,类的构造函数一般是公有的 (public),但有时也声明为
私有的, 其作用是限制创建该类对象的范围, 即只能在本类
和友元中创建该类对象 。
2,带缺省参数的构造函数
【 例 9.4】 带缺省参数的构造函数
#include <iostream.h>
class Tdate{
public,
Tdate(int m=5,int d=16,int y=1990)
{
month=m; day=d; year=y;
cout <<month <<”/” <day
<<”/” <<year <<endl;
}
private,
int month;
int day;
int year;
};
void main()
{
Tdate aday;
Tdate bday(2);
Tdate cday(3,12);
Tdate
dday(1,2,1998);
}
程序的运行结果,
5/16/1990
2/16/1990
3/12/1990
1/2/1998
!!!注意,使用带缺省参数的构造函数时,要注意 避免二义性,所
带的参数个数或参数类型必须有所不同,否则系统调用时会出
现二义性 。
3.缺省构造函数(默认构造函数)
C++规定, 每个类必须有一个构造函数, 没有构造函
数, 就不能创建任何对象 。
若未提供一个类的构造函数, 则 C++提供一个缺省的
构造函数, 该缺省构造函数是个无参构造函数, 它仅负
责创建对象, 而不做任何初始化工作 。
只要一个类定义了一个构造函数 ( 不一定是无参构造
函数 ), C++就不再提供默认的构造函数 。 如果为类定义
了一个带参数的构造函数, 还想要无参构造函数, 则必
须自己定义 。
与变量定义类似, 在用默认构造函数创建对象时, 如果
创建的是全局对象或静态对象, 则对象的位模式全为 0,否
则, 对象值是随机的 。
【 例 9.5】 缺省构造函数
#include <string.h>
class Student{
public,
Student(char* pName)
{
strncpy(name,pName,sizeof(n
ame));
name[sizeof(name)-
1]=’\0’;
}
Student()
{}
//不能省略, 因为在 main()
中创建无参对象 noName
时使用
};
protected,
char name[20];
void main()
{
Student noName;
Student ss(“Jenny”);
}
4,拷贝构造函数(复制构造函数)
(1) 功能,拷贝构造函数的功能是用一个已有的对象来初始化
一个被创建的同类的对象,
(2) 特点,一种特殊的构造函数,具有一般构造函数的所
有特性,其形参是本类对象的引用。
拷贝构造函数的声明形式为,
类名 (类名 &对象名 );
例,class cat
{
private,
int age;
float weight;
char *color;
类 cat的定义
public,
cat();
cat(cat &);
void play();
void hunt();
};
cat::cat(cat &other)
{
age = other.age;
weight=other.weight;
color = other.color;
}
拷贝构造函数的
声明
拷贝构造函数
的定义
调
用
拷
贝
构
造
函
数
有
以
下
三
种
情
况
(1) 用类的一个对象去初始化另一个对象时。
(2) 对象作为函数参数传递时,调用拷贝构造函数
(3) 如果函数的返回值是类的对象,函数调用
返回时,调用拷贝构造函数。
例,cat cat1; cat cat2(cat1);
//创建 cat2时系统自动调用拷贝构造函数
用 cat1初始化 cat2。
例 f(cat a){ } //定义 f函数, 形参为 cat类对象
cat b; //定义对象 b
f(b);//进行 f函数调用时, 系统自动调用拷贝
构造函数
例 cat f() //定义 f函数, 函数的返回值为 cat类的对象
{ cat a;
…
return a;
}
cat b;//定义对象 b
b=f();//调用 f函数, 系统自动调用拷贝构造函数
!!! 注意,
由 C++提供的默认拷贝构造函数只是对对象进行浅拷贝
复制(逐个成员依次拷贝)。
如果对象的数据成员包括指向堆空间的指针,就不能使
用这种拷贝方式,因为两个对象都拥有同一个资源,对象析
构时,该资源将经历两次资源返还,此时必须自定义深拷贝
构造
下面的程序段定义了深拷贝构造函数,
#include<string.h>
class Person
{
public,
Person(char *name)
//构造函数
{
pname=new
char[strlen(name)+1];
if(pname!=0)
//使用 new进行动态内
存分配
{strcpy(pname,name);}
}
Person(Person&p)
// 拷贝构造函数
{
pname=new
char[strlen(p.pname)+
1];
//复制资源
if(pname!=0)
strcpy(pname,p.pname);
// 复制对象空间
}
//其它成员函数
private,
char *pname;
};//类定义的结束
5,构造初始化表
构造函数也可使用构造初始化表对数据成员进行初始化,
例,Circle::Circle(float r)
{radius=r; }
可改写为,
Circle::Circle(float r):radius(r){}
注,成员初始化的次序取决于它们在类定义中的 声明
次序, 与它们在成员初始化表中的次序无关 。
6,类类型和基本数据类型的转换
(1) 构造函数用作类型转换
通过构造函数进行类型转换必须有一个前提,那就是此
类一定要有一个只带一个参数的构造函数,这样才可以
实现从参数类型向该类类型的转换,并且这种转换是隐
式的。
class A
{ …
public,
A();
A(int);
};…
例,f(A a);
//f函数的形参为 A类的对象
f(1);
/*进行 f函数调用时先构造一
个 A类对象, 调用 A::A(int)
进行类型转换, 然后把它传
给函数 f。 */
(2) 类类型转换函数
通过构造函数进行类类型的转换只能从参数类型向类类型转
换,类类型转换函数用来将类类型向基本类型转换。
为类定义一类型转换函数的语法为,
① 在类定义体中声明
operator type();
要转向的基本类型名
注,此函数既没有参数, 又没有返回类型, 但在函数体中
必须返回具有 type类型的一个对象 。
② 定义转换函数的函数体
类名, opertor type()
{
//
return type类型的值
}
③ 使用类型转换函数,
使用类型转换函数与对基本类型进行强制转换时
一样, 就象是一种函数调用过程 。
【 例 9.6】 类型转换函数
#include<iostream.h>
class RMB
{
public,
RMB(double value = 0.0);
//构造函数用作类型转换
operator double(){ return yuan + jf / 100.0; }
//类类型转换函数
void display(){ cout << (yuan + jf / 100.0) << endl; }
RMB::RMB(double value)
{
yuan = value;
jf = ( value – yuan ) * 100 + 0.5;
}
void main()
{
RMB d1(2.0),d2(1.5),d3;
d3 = RMB((double)d1 + (double)d2);
//显式转换
d3 = d1 + d2;
//隐式转换
d3.display();
}
程序执行结果为,
3.5
9.4.2 析构函数
1,析构函数及其作用
功能, 当对象被撤消时,释放该对象占用的内存空间
特点,析构函数的作用与构造函数正好相反,一般情况下,
析构函数执行构造函数的逆操作。在对象消亡时,系统将
自动调用析构函数。析构函数 没有返回值, 没有参数,每
个类只有一个析构函数。析构函数的函数名为类名前加 ~。
析
构
函
数
在
以
下
情
况
下
自
动
被
调
用
(1) 一个动态分配的对象被删除, 即使
用 delete删除对象时, 编译系统会自动
调用析构函数
(2) 程序运行结束时
(3) 一个编译器生成的临时对象不再需
要
2,当用户在程序中手工调用析构函数时, 语法如下,
对象名,类名,:析构函数名
注,而构造函数只能由系统调用,不能用上述方法调用
3,析构函数与构造函数的调用顺序刚好相反
【 例 9.7】 析构函数和构造函数的调用顺序
#include <iostream.h>
#include <string.h>
class Student{
public,
Student(char* pName=”no name”,int ssId=0)
{
strncpy(name,pName,40);
name[39]=’\0’;
id = ssId;
cout <<”Constructing new student
,<<pName <<endl;
}
Student(Student& s) //拷贝构造函数
{
cout <<”Constructing copy of, <<s.name
<<endl;
strcpy(name,”copy of,);
strcat(name,s.name);
id=s.id;
}
~Student()
{
cout <<”Destructing, <<name <<endl;
}
protected,
char name[40];
int id;
};
void fn(Student s)
{
析构函数的定义
cout <<”In function fn()\n”;
//fn函数调用结束时, 析构对象 s
}
void main()
{
Student randy(“”Randy”,1234);
//调用构造函数, 创建对象 randy
Student wang(“wang”,5678);
// 调用构造函数, 创建对象 wang
cout <<”Calling fn()\n”;
运行结果为,
Constructing new
tudent Randy
Constructing new
student wang
Calling fn()
Constructing copy
of Randy
In function fn()
Destructing copy
of Randy
Returned from fn()
Destructing wang
Destructing Randy
fn(randy);
//调用 fn函数,参数传递时
调用拷贝构造函数
cout <<”Returned from fn()\n”;
//主函数调用结束时,
先析构对象 wang,
再析构对象 randy
}
9.5 对象成员、静态成员
9.5.1 对象成员
定义,对象成员也称为类的聚集, 是指在类的定义中数据成
员可以为其它的类对象, 即类对象作为成员 。
如果在类定义中包含有对象成员, 则在创建类对象时先
调用对象成员的构造函数, 再调用类本身的构造函数 。 析构
函数的调用顺序正好相反 。
从实现的角度讲, 实际上是首先调用类本身的构造函数,
在执行本身构造函数的函数体之前, 调用成员对象的构造函
数, 然后再执行类本身构造函数的函数体 。 因此, 在构造函
数的编译结果中包含了对对象成员的构造函数的调用, 至于
调用对象成员的哪一个构造函数, 是由成员初始化表指定的;
当成员初始化表为空时, 则调用对象成员
【 例 9.8】 含有对象成员的类的构造函数和析构函
数的调用顺序
#include <iostream.h>
#include <string.h>
class StudentID{
public,
StudentID(int id=0)
//带缺省参数的构造函数
{
value=id;
cout <<”Assigning
student id, <<value
<<endl;
}
~StudentID()
{
cout
<<“Destructing
id”<<value <<endl;
class Student{
public,
Student(char* pName=“no name”,int
ssID=0):id(ssID)
{
cout <<”Constructing student, <<pName
<<endl;
strncpy(name,pName,sizeof(name));
name[sizeof(name)-1]=’\n’;
}
~Student()
{cout<<“Deconstructing student
"<name<<endl;}
protected,
char name[20];
StudentID id;
//对象成员
};
void main()
{
Student s(“wang”,9901);
程序的运行结果,
Assigning student id 9901
Constructing student wang
Assigning student id 0
Constructing student li
Deconstructing student li
Destructing id 0
Deconstructing student wang
Destructing id 9901
9.5.2 静态成员
定义,在类的定义中, 它的数据成员和成员函数可以声
明成静态的, 即用关键字 static,这些成员就被称为静态
成员 。
特征,不管这个类创建了多少个对象, 而其静态成员只
有一个副本, 此副本被这个类的所有对象共享 。 静态成
员分为 静态数据成员 和 静态成员函数 。
1,静态数据成员
静态数据成员被存放在内存某一单元内,该类的所有对
象都可以访问它。无论建立多少个该类的对象,都只有
一个静态数据的拷贝。由于静态数据成员仍是类成员,
因而具有很好的安全性能。当这个类的第一个对象被建
立时,所有 static数据都被初始化,并且,以后再建立
对象时,就不需再对其初始化。初始化在类体外进行。
格式如下,
〈 类型 〉 〈 类名 〉 ∷ 〈 数据成员名 〉 =〈 初始值 〉
【 例 9.9】 静态数据成员的使用
#include<iostream.h>
class A
{
static int i;
//定义静态数据成员
public,
A(){i++;}
int list(){return i;}
};
int A::i=0;
//请注意静态成员数
据的初始化格式,无
static关键字
程序的执行结果,
3,3,3
void main()
{
A a1,a2,a3;
cout<<a1.list()<<','<<a2
.list()<<','<<a3.list();
//显示均为 3(因为创建
三个对象, 三次使得静
态数据成员加 1)
}
2,静态成员函数
特点,
(1)静态成员函数无 this指针, 他是同类的所有对
象共享的资源, 只有一个共用的副本 。 而一般
的成员函数中都含有一个 this指针, 指向对象自
身 。
(2) 在静态成员函数中访问的基本上是静态数据
成员或全局变量 。
(3) 在调用静态成员函数的前面, 必须缀上对象
名或类名, 经常用类名 。
(4) 静态成员函数的使用虽然不针对某一个特定
的对象, 但在使用时系统中必须已经存在此类
的对象 。
注,由于静态成员函数属于类独占的成员函数, 因
此访问静态成员函数的消息接收者 不是类对象, 而
是类自身 。 C++语言规定使用类作用域符号,:表示
是类身份的对象标识 。
如在上例中在函数 list()前附加声明 static后, 便可
将 main()中的语句, a1.list();” 改写成, A::list();”
的形式, 运行结果相同 。
9.6 友元
友元的分类,友元函数, 友元成员和友元类三种
1.友元函数
友元函数是一种说明在类定义体内的非成员函数。
说明友元函数的方法如下,
friend 〈 返回值类型 〉 〈 函数名 〉 (〈 参数表 〉 )
{〈 函数体 〉 }
说明,
(1) 友元函数是在类中说明的一个函数,它不是该
类的成员函数,但允许访问该类的所有成员。他
是独立于任何类的一般的外界函数。友元并不在
类的范围中,它们也不用成员选择符 (.或 ->)调用,除
非它们是其它类的成员。
(2) 由于友元函数不是类的成员,所以没有 this指
针,访问该类的对象的成员时,必须使用对象名,
而不能直接使用类的成员名。
(3) 虽然友元函数是在类中说明的,但其名字的作
用域在类外,作用域的开始点在说明点,结束点
和类名相同。因此,友元说明可以代替该函数的
函数说明。
(4) 如果在说明友元时给出了该函数的函数体代码,
则它是内联的。
【 例 9.10】 友元函数的定义和使用
#include<iostream.h>
#include<string.h>
class Student
{
private,
char name[10],num[10];
friendvoidshow(Student& st)
//友元函数的声明和定义
{cout<<"Name:"<<st.name<<"\n"; }
public,
Student(char *s1,char *s2)
{ strcpy(name,s1);
strcpy(num,s2);}
};
class Score
{
unsigned int mat,phy,eng;
friend void show_all(Student&,Score*);
//友元函数的声明
public,
Score(unsigned int i1,unsigned int i2,
unsigned int i3):mat(i1),phy(i2),eng(i3)
{}
};
void show_all(Student& st,Score* sc)
{
show(st);
cout<<"Mathematics:"<<sc->mat
<<"\nPhyics:"<<sc->phy
<<"\nEnglish:"<<sc->eng<<endl;
public,
Score(unsigned int i1,unsigned int
i2,unsigned int i3):mat(i1),phy(i2),eng(i3)
{}
};
void show_all(Student& st,Score* sc)
{ show(st);
cout<<"Mathematics:"<<sc->mat
<<"\nPhyics:"<<sc->phy
<<"\nEnglish:"<<sc->eng<<endl;}
void main()
{
Student wang("Wang","9901");
Score ss(72,82,92);
show_all(wang,&ss);
}
程序的运行结果,
Name:Wang
Mathematics:72
Phyics:82
English:92
2,友元成员
另一个类的成员函数可以作为某个类的友元,只
是在声明友元函数时要加上成员函数所在的类名,
称为友元成员。
声明如下,
friend 类名 ∷ 成员函数名
【 例 9.11】 友元成员
#include<iostream.h>
#include<string.h>
class Student;
//声明引用的类名
class Score
{
unsigned int mat,phy,eng;
public,
Score(unsigned int i1,unsigned int i2,
unsigned int i3):mat(i1),phy(i2),eng(i3){}
void show()
{
cout<<"Mathematics:"<<mat<<"\nPhyics
:"<<phy
void show(Student&);
};
class Student
{
friend void Score::show(Student&);
//声明友元成员
char name[10],num[10];
public,
Student(char *s1,char *s2)
{strcpy(name,s1); strcpy(num,s2);}
};
void Score::show(Student& st)
{
cout<<"Name:"<<st.name<<"\n";
show();
}
void main()
{
Student wang("Wang","9901");
Score ss(72,82,92);
ss.show(wang); }
程序的运行结果,
Name:Wang
Mathematics:72
Phyics:82
English:92
3.友元类
某一个类可以是另一个类的友元,这样作为友
元的类中的所有成员函数都可以访问另一个类
中的私有成员。
友元类的说明方式如下,
friend class 〈 类名 〉
【 例 9.12】 友元类
#include<iostream.h>
#include<string.h>
class Student
{
friend class Score;
//声明 Score类为 Student类的友元类
char name[10],num[10];
public,
Student(char *s1,char *s2)
{strcpy(name,s1);
strcpy(num,s2);}
};
class Score
{
unsigned int mat,phy,eng;
public,
Score(undsige int i1,undsige int i2,undsige int
i3),phy(i2),eng(i3)
{}
void show()
{cout<<"Mathematics:"<<mat<<"\nPhyics:"<<
phy<<"\nEnglish:"<<eng<<endl;}
void show(Student&);
};
void Score::show(Student& st)
void Score::show(Student& st)
{
cout<<"Name:"<<st.name<<"\n";
show();
}
void main()
{
Student wang("Wang","9901");
Score ss(72,82,92);
ss.show(wang); }
程序的运行结果是,
Name:Wang
Mathematics:72
Phyics:82
English:92
注,例中由于声明 Score为 Student的友元类, 此
时 Score中的成员函数便可以直接访问 Stuednt对
象的成员 。 这样 Score的成员函数 Show(Student&
st)中的, st,name”的语句才是允许的 。
9.7 类模板和模板类
模板是一种描述函数或类的特性的蓝图 。
模板分为函数模板和类模板, 可以从一个
函数模板生成多个函数或从一个类模板生
成多个类 。 建立一个模板后, 编译器将根
据需要从模板生成多份代码 。
模
板
的
定
义
与
性
质
类模板为类定义一种模式, 使得类
中的某些数据成员, 某些成员函数的
参数, 某些成员函数的返回值, 能取
任意类型 ( 包括系统预定义的和用户
自定义的 )
类
模
板的
定
义
与
性
质
C++编译器根据类模板和特定的数据类型来
产生一个类,即模板类(这是一个类)。
类模板是一个抽象的类,而模板类是实实
在在的类,是由类模板和实际类型结合后
由编译器产生一个类的实例。这个类名就
是抽象类名和实际数据类型的结合
模
板
类
的
定
义
与
性
质
如,
TclassName<int> 整体是一个类名,包括尖
括号和 int。而通过这个类才可以产生对象
(类的实例)。
注,对象是类的是实例,而模板类是类模板的实例
mm,kmjkj
类模板由 C++语言的关键字 template引入, 定义
的语法形式如下,
template <class 类属参数 1,class 类属参数 2,…… >
class name
{ }
形式类属参数表每个类属
参数由关键字 class引入
【 例 9.13】 类模板和模板类的使用
#include <iostream.h>
template<class T>
class tem
{
T *data;
int size;
public,
tem(int);
~tem( )
{delete [ ]data;}
T& operator[ ](int i)
{return data[i];} };
template<class T>
tem<T>::tem(int n)
{
data = new T[n];
size = n;
}
void main( )
{
tem<int> x(5);
int i;
/‘
for(i=0;i<5;i++)
x[i]=i;
for(i=0;i<5;i++)
cout<<x[i]<< ' ';
cout<<'\n';
}
程序的运行结果为
0 1 2 3 4
注,
这个例子中定义了一
个 类模板 tem,它有
一个类型参数 T,类
模板的定义由
template<class T>开
始,下面的类模板定
义体部分与普通类定
义的方法相同,只是
在部分地方用参数 T
表示数据类型。
模板类的每一个非内置函数的定义是一个独立
的模板, 同样以 template<class T>开始, 函数头中
的类名由模板类名加模板参数构成, 如例子中的
tmp<T>。
模板类在使用时必须指明参数, 形式为:模板
类名 <模板实参表 >。 主函数中的 tmp<int>即是如
此, 编译器根据参数 int生成一个实实在在的类即
模板类, 理解程序时可以将 temp<int>看成是一个
完整的类名 。 在使用时 temp不能单独出现, 它总
是和尖括号中的参数表一起出现
上面的例子中模板只有一个表示数据类型的参数,
多个参数以及其它类型的参数也是允许的 。
关于模板类的几点说明,