第3章 类和对象 类是C++语言支持面向对象思想的重要机制,是C++语言实现数据隐藏和封装的基本单元,它将一个数据结构与一个操作紧密地结合起来,是实现面向对象其他特性的基础。类对象是类的实例,用类对象模拟现实世界中的事物比用一般的数据变量更加确切。 3.1类 类是C++语言的数据抽象和封装机制,它描述了一组具有相同属性(数据成员)和行为特征(成员函数)的对象。在系统实现中,类是一种共享机制,它提供了本类对象共享的操作实现。类是代码复用的基本单位,它可以实现抽象数据类型、创建对象、实现属性和行为的封装。例如,在学生中,有小学生、中学生、大学生等不同类型,但在描述时,可找出各种类型学生的共性,将其归为一类,即学生类。 对象是类的实例。类是对一组具有相同特征的对象的抽象描述,所有这些对象都是这个类的实例。 例如,对于学籍管理系统,学生是一个类,而每个具体的学生则是学生类的一个实例。在程序设计语言中,类是一种数据类型,而对象是该类型的变量,变量名即是某个具体对象的标识。类和对象的关系相当于普通数据类型与其变量的关系。类是一种逻辑抽象概念。声明一个类只是定义了一种新的数据类型,声明对象才真正创建了这种数据类型的物理实体。由同一个类创建的各个对象具有完全相同的数据结构,但它们的数据值可能不同。 类提供了完整的解决特定问题的能力,因为类描述了数据结构(对象属性)、算法(对象行为)和外部接口(消息协议)。 在C++语言中,一个类的定义包含数据成员和成员函数两部分内容。数据成员定义该类对象的属性,不同对象的属性值可以不同; 成员函数定义了该类对象的操作,即行为。 3.1.1类的定义 类由三部分组成: 类名、数据成员和成员函数。类定义的一般格式如下所示。 class类名 { private: //私有数据成员和成员函数 public: //公有数据成员和成员函数 protected: //受保护的数据成员和成员函数 }; 下面是有关类定义的几点说明。 (1) class是定义类的关键字,类名是一种标识符,必须符合C++语言标识符的命名规则。一般情况下,类名的首字母大写,以区别于普通的变量和对象。花括号内是类的定义体部分,说明该类的成员,类的成员包括数据成员和成员函数。 (2) 类成员的三种访问控制权限。 类成员有三种访问控制权限,分别是private(私有成员)、public(公有成员)、protected(受保护成员),在每一种访问控制权限下,均可以定义数据成员和成员函数。 ① 私有成员private: 私有成员是在类中被隐藏的部分,它往往是用来描述该类对象属性的一些数据成员,私有成员只能由本类的成员函数或某些特殊说明的函数(如第4章中的友元函数)访问,而类的外部函数无法访问私有成员,实现了访问权限的有效控制,使数据得到有效的保护,有利于数据的隐藏; 使内部数据不被任意地访问和修改,也不会对该类以外的其余部分造成影响; 使模块之间耦合程度降到最低。私有成员若处于类声明中的第一部分,可省略关键字private。 ② 公有成员public: 公有成员对外是完全开放的,公有成员一般是成员函数,它提供了外部程序与类的接口功能,用户通过公有成员访问该类中的数据。 ③ 受保护成员protected: 只能由该类的成员函数、友元、公有派生类成员函数访问的成员。受保护成员与私有成员在一般情况下含义相同,它们的区别体现在类的继承中对产生的新类的影响不同,具体内容将在第5章中介绍。 默认访问控制(未指定private、protected、public访问权限)时,系统默认为私有成员。 类具有封装性,C++语言中的数据封装通过类来实现,外部不能随意访问权限说明为protected和private的成员。 (3) 由于类的公有成员提供了一个类的接口,所以一般情况下,先定义公有成员,再定义保护成员和私有成员,这样可以在阅读时首先了解这个类的接口。当然,类声明中的三种访问控制权限说明可以按任意顺序出现任意次。 (4) 数据成员可以是任何数据类型,但是不能用自动(auto)、寄存器(register)或外部(extern)进行说明。 (5) 注意在定义类时,不允许初始化数据成员,下面的定义是错误的。 class A { private: int n = 0; //错误 int m = 5; //错误 … }; (6) 结构体和类的区别。 C语言中的结构体只有数据成员,无成员函数。C++语言中的结构体可有数据成员和成员函数。在默认情况下,结构体中的数据成员和成员函数都是公有的,而在类中是私有的。从外部可以随意修改结构体变量中的数据,对数据的这种操作是很不安全的,程序员不能通过结构体对数据进行保护和控制; 在结构体中,数据和其相应的操作是分离的,使得程序的复杂性难以控制,而且程序的可重用性不好,严重影响了软件的生产效率。所以,一般仅有数据成员时使用结构体,当既有数据成员又有成员函数时使用类。 注意: 在类定义时不要丢掉类定义的结束标志“;”。 例如定义日期类: class Tdate//定义日期类 { public://定义公有成员函数 void set(int m,int d,int y); //设置日期值 int isLeapYear(); //判断是否是闰年 void print(); //输出日期值 private://定义私有数据成员 int month; int day; int year; }; //类定义体的结束 3.1.2类中成员函数的定义 类的数据成员说明对象的特征,而成员函数决定对象的操作行为。成员函数是程序算法实现的部分,是对封装的数据进行操作的唯一途径。类的成员函数有两种定义方法: 外联定义和内联定义。 1. 外联成员函数(外联函数) 外联定义成员函数是指在类定义体中声明成员函数,而在类定义体外定义成员函数。在类中声明成员函数时,它所带的函数参数可以只指出其类型,而省略参数名; 在类外定义成员函数时必须在函数名之前缀上类名,在函数名和类名之间加上作用域区分符“::”,作用域区分符“::”指明一个成员函数或数据成员所在的类。作用域区分符::前若不加类名,则成为全局数据或全局函数(非成员函数)。 在类外定义成员函数的具体形式如下。 返回值类型 类名::成员函数名(形式参数表) { //函数体 } 如3.1.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); } void Tdate::print()//输出日期值 { cout</* 因为tdate.cpp文件要访问运算符<< 和ostream类对象cout,而这二者都是定义在iostream类中的,所以包含iostream头文件*/ #include "tdate.h"//包含用户自定义的头文件,该文件中提供了Tdate类的定义 using namespace std; 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)); } void Tdate::print() { cout << month << "/" << day << "/" << year << endl; } 类的应用在此例中没有给出,具体参见例3.2。 说明: 头文件tdate.h中前两行 #ifndef Tdate_H//用来避免重复定义 #define Tdate_H//不是类的一部分 的作用是,如果一个程序系统中的多个文件均包含Tdate类,则在编译时可以避免Tdate类中标识符的重复定义。类定义前的这些行可使编译器跳过文件中最后一行#endif//Tdate_H之前的所有行。 除了第一次之外,以后编译器每遇到编译预处理命令#include "tdate.h"则以#ifndef(如果没有定义)开始的命令行测试标识符Tdate_H是否已经定义。如果没有定义,则第二行定义标识符Tdate_H,且它的值为NULL,然后编译器处理文件tdate.h中的其余行。如果以后再一次包含了tdate.h文件,则编译器要处理第一行#ifndef Tdate_H,确定了标识符Tdate_H已经定义,则命令行#endif之前的所有行都被跳过,不进行编译,因此避免了类中标识符的重复定义。名字Tdate_H没有任何特殊的意义,只是在类名的末尾加了_H。 3.2对象 1. 对象的基本概念 现实生活中,任何事物都可以称为对象,它是无所不在的。用面向对象方法开发一个系统时,对象的识别与描述只限定在待开发的软件系统中与系统目标相关的事物。 用面向对象方法开发的软件系统中,对象是类的实例,是属性和服务的封装体。一个对象就是一个实际问题域中的实体,它包含了数据结构和所需的相关操作功能,形成了一个基本程序模块。 对象的属性用于描述对象的静态数据特征。如人类有大脑、五官、四肢,鸟有翅膀、羽毛,树有根、茎、叶等,这些都是描述对象的静态数据特征。对象的属性可用系统的或用户自定义的数据类型来表示,也可以用抽象的数据类型表示。对象属性值的集合称为对象的状态(state)。 对象的服务用于描述对象的动态特征,也称为行为或性能,它是定义在对象属性基础上的一组操作方法(method)的集合。如人类有思维能力、有语言能力、可直立行走等,鸟类有翅膀可以飞行等。对象的服务是响应消息而完成的算法,它体现了对象的行为能力。对象的服务包括自操作和它操作,自操作是对象对其内部的数据属性进行的操作,它操作是对其他对象进行的操作。 当一个对象映射为软件实现时由以下三部分组成。 (1) 私有的数据: 用于描述对象的内部状态。 (2) 处理: 也称为操作或方法,对私有数据进行运算。 (3) 接口: 这是对象可被共享的部分,消息通过接口调用相应的操作。接口规定哪些操作是允许的,但并不提供操作是如何实现的信息。 2. 对象的定义 对象的定义有两种方法,可以在定义类的同时直接定义,也可以在使用时通过类进行定义。 (1) 方法一: 在定义类的同时直接定义。 class Location { public: void init(int x0, int y0); int getX( void ); int getY( void ); private: int x, y; }dot1,dot2; (2) 方法二: 在使用时定义对象。 格式如下: 类名 标识符,…,标识符; 如: Location dot1,dot2; 3. 成员的访问 定义了类及其对象,就可以调用公有成员函数实现对对象内部属性的访问。当然,不论是数据成员,还是成员函数,只要是公有的(public),在类的外部就可以通过类的对象进行访问。对公有成员的调用可以通过以下几种方法来实现。 (1) 通过对象调用成员。 格式如下: 对象名.公有成员 其中,“.”称为对象选择符,简称点运算符。 (2) 通过指向对象的指针调用成员。 格式如下: 指向对象的指针->成员 或 (*对象指针名).公有成员 (3) 通过对象的引用调用成员。 格式如下: 对象的引用.成员 需要注意,只有用public定义的公有成员才能使用点运算符访问。对象中的私有成员是类中隐藏的数据,类的外部不能访问对象的私有成员,只能通过该类的公有成员函数来访问它们。例如定义时钟类: class Clock { public: void init(); void update(); void display(); private: int hour, minute, second; }; int main() { Clock myClock,*pclock; //定义对象myClock和指向myClock类对象的指针pclock myClock.init(); //通过对象访问公有成员函数 pclock = &myClock; //指针pclock指向对象myClock pclock->display(); //通过指针访问成员函数 //myClock.hour = 4;该语句错误,因为对象不能访问其私有成员 return 0; } 【例3.2】对象成员的访问示例。 此例的项目文件中包含了例3.1中的tdate.h头文件和tdate.cpp源程序文件以及下述的ch3_2.cpp文件,共3个文件。在Visual C++2010环境中新建的项目中添加头文件tdate.h,添加源文件tdate.cpp和ch3_2.cpp,该项目的文件结构如图3.1所示。源程序代码如下: // ch3_2.cpp #include #include"tdate.h" using namespace std; void someFunc(Tdate& refs) { refs.print(); //通过对象的引用调用成员函数 if(refs.isLeapYear())//通过对象的引用调用成员函数 cout << "error\n"; else cout << "right\n"; } int main()//类的应用部分 { Tdate s,*pTdate = &s; //pTdate为指向Tdate类对象的指针 s.set(2,15,1998); //通过对象调用成员函数 pTdate -> print(); //通过指向对象的指针调用成员函数 if((*pTdate).isLeapYear())//通过指向对象的指针调用成员函数 cout << "error\n"; else cout << "right\n"; someFunc(s); //对象的地址传给引用 return 0; } 程序的运行结果如图3.2所示。 图3.1例3.1项目文件结构 图3.2例3.2的运行结果 为了提高程序的可读性,类的定义和类的实现存放在不同的文件中,也可以采用例3.3的方式在项目中添加类,系统会自动创建对应的.h头文件和.cpp源文件以简化程序员操作。由于本书中的例题相对比较简单,为方便教材的编写,部分例题仍将类的定义和实现放在同一个文件中。如果程序比较复杂,建议读者将类的定义和实现部分分别放在头文件和对应的源程序文件中。 【例3.3】堆栈Cstack类的实现,该类用于存储字符。 在Visual C++2010环境中新建“Win32控制台应用程序”项目ch3_3,在解决方案资源管理器中右击ch3_3项目名称,在弹出的快捷菜单中选择“添加”→“类”,弹出如图3.3所示的“添加类”对话框,在该对话框中选中“C++类”,单击“添加”按钮,弹出如图3.4所示的“一般C++类向导”对话框,在“类名”文本框中输入Cstack,项目中会自动添加Cstack.h和Cstack.cpp两个文件,单击“完成”按钮。 图3.3“添加类”对话框 图3.4“一般C++类向导”对话框 自动生成的Cstack.h头文件的内容如下: #pragma once class Cstack { public: Cstack(void); ~Cstack(void); }; 自动生成的Cstack.cpp源文件的内容如下: #include "Cstack.h" Cstack::Cstack(void) { } Cstack::~Cstack(void) { } 其中,编译预处理命令“#pragma once”的作用与下述代码段的作用相同,是为了避免重复定义。 #ifndef Tdate_H//用来避免重复定义 #define Tdate_H//不是类的一部分 … #endif 特殊成员函数Cstack(void) 和 ~Cstack(void) 的概念在后续小节中加以讲解。用户在此基础上修改Cstack.h和Cstack.cpp文件即可完成类和成员函数的定义,之后再添加ch3_3.cpp源文件即可完成该程序。完整的源程序代码如下: // Cstack.h #pragma once const int SIZE = 10; //存储的最多字符数 class Cstack { public: Cstack(void); ~Cstack(void); void init(); charpush (char ch); charpop(); private: char stk[SIZE]; intposition; }; // Cstack.cpp #include "Cstack.h" #include using namespace std; Cstack::Cstack(void) { } Cstack::~Cstack(void) { } void Cstack::init() { position = 0; } char Cstack::push(char ch) { if (position == SIZE) { cout<<"\n栈已满 \n"; return 0; } stk [position++] = ch; returnch; } char Cstack::pop() { if (position == 0) { cout<<"\n栈已空"< #include "Cstack.h" using namespace std; int main() { Cstack s; s.init(); char ch; cout<<"请输入字符: "<>ch; while(ch != '#'&& s.push(ch)) cin>>ch; cout<<"\n现在输出栈内数据\n"; while(ch = s.pop()) cout <month=m; this->day=d; this->year=y; } 即对于该成员函数中访问的类的任何数据成员,C++语言编译器都认为是访问this指针所指向对象的成员。由于不同的对象调用成员函数set()时,this指针指向不同的对象,因此,成员函数set()可以为不同对象的month、day和year赋初值。使用this指针,保证了每个对象可以拥有不同的数据成员值,但处理这些数据成员的代码可以被所有的对象共享。 5. 带默认参数的成员函数和重载成员函数 同普通函数一样,类的成员函数也可以是带默认值的函数,其调用规则与普通函数相同。成员函数也可以是重载函数,类的成员函数的重载与全局函数的重载方法相同。 【例3.4】带默认参数的成员函数。 该程序共包括2个源文件Tdate.h和ch3_4.cpp,源程序代码如下: // Tdate.h #include using namespace std; class Tdate { public: void set(int m=5,int d=16,int y=1990)//设置日期值 { month=m; day=d; year=y; } void print()//输出日期值 { cout< using namespace std; class Cube { public: int volume(intht,int wd) { return ht*wd; } int volume(int ht,int wd,int dp) { height=ht; width=wd; depth=dp; return height*width*depth; } private: int height,width,depth; }; int main() { Cube cube1; cout << cube1.volume(10,20) << endl; //调用带2个参数的成员函数 cout << cube1.volume(10,20,30) << endl; //调用带3个参数的成员函数 return 0; } 程序的运行结果如图3.7所示。 图3.7例3.5的运行结果 3.3构造函数和析构函数 当声明一个对象时,对象的状态(数据成员的取值)是不确定的。但对象表达了现实世界的实体,因此,一旦声明对象,必须有一个有意义的初始值。C++语言中有一个称为构造函数的特殊成员函数,它可自动进行对象的初始化,还有一个析构函数在对象撤销时执行清理任务,进行善后处理。 构造函数和析构函数是类中的两个特殊的成员函数,具有普通成员函数的许多共同特性,但还具有一些独特的特性,可以归纳成以下几点。 (1) 它们都没有返回值说明,也就是说定义构造函数和析构函数时不能指出函数返回值的类型,即使是void也不能有。 (2) 它们不能被继承。 (3) 和大多数C++函数一样,构造函数可以有默认参数。 (4) 析构函数可以是虚的(virtual),但构造函数不可以是虚的。 (5) 不可取它们的地址。 (6) 不能用常规调用方法调用构造函数,当使用完全的限定名(带对象名、类名和函数名)时可以调用析构函数。比较特殊的是,在对象数组中,初始化数组元素时可以显式调用构造函数,参见例3.11。 (7) 当定义对象时,编译程序自动调用构造函数; 当删除对象时,编译程序自动调用析构函数。 3.3.1构造函数 对象的初始化是指对象数据成员的初始化,在使用对象前,一定要进行初始化。由于数据成员一般为私有的(private),所以不能直接赋值。对象初始化有以下两种方法。 一种方法是类中提供一个普通成员函数来初始化,但是会造成使用上的不便(使用对象前必须显式调用该函数)和不安全(未调用初始化函数就使用对象)。 另外一种方法是使用构造函数对对象进行初始化。下面具体介绍构造函数及其使用方法。 1. 构造函数(constructor) 构造函数是一个与类同名,没有返回值(即使是void也不可以有,但在函数体内可有无值的return 语句)的特殊成员函数。一般用于初始化类的数据成员,每当创建一个对象时(包括使用new动态创建对象),编译系统就自动调用构造函数。构造函数既可在类外定义,也可作为内联函数在类内定义。 构造函数定义了创建对象的方法,提供了初始化对象的一种简便手段。在类外定义构造函数时,其声明格式为: <类名>::构造函数名(<形式参数表>) 定义了构造函数后,在定义该类对象时可以将参数传递给构造函数来初始化该对象。 一个类可以有多个构造函数,但它们的形式参数的类型和个数不能完全相同,编译器在编译时可以根据参数的不同选择不同的构造函数。 【例3.6】构造函数的定义和调用示例。 项目中包含MyQueue.h、MyQueue.cpp和ch3_6.cpp三个文件。 // MyQueue.h #pragma once class MyQueue { public: MyQueue(void); ~MyQueue(void); void qput(int i); int qget(); private: int q[100]; int sloc,rloc; }; // MyQueue.cpp #include "MyQueue.h" #include using namespace std; MyQueue::MyQueue(void) { sloc=rloc=0; cout<< "queue initialized\n"; } MyQueue::~MyQueue(void) { } void MyQueue::qput(int i) { if(sloc == 100) { cout<< "queue is full\n"; return; } sloc++; q[sloc]=i; } int MyQueue::qget() { if(rloc==sloc) { cout<< "queue is empty\n"; return 0; } rloc++; return q[rloc]; } // ch3_6.cpp #include "MyQueue.h" #include using namespace std; int main() { MyQueue a,b; a.qput(10); b.qput(20); a.qput(20); b.qput(19); cout< using namespace std; TestOverloadConstructor::TestOverloadConstructor(void) { num = 0; f1 = 0.0; cout<<"Initializing default "< using namespace std; Tdate::Tdate(int m,int d,int y) { month=m;day=d;year=y; cout < using namespace std; class Student { public: Student(char* pName) { cout<<"call one parameter constructor"< using namespace std; Test::Test(void) { num = 0; f1 = 0.0; cout<<"Initializing default"< using namespace std; int main() { cout<<"the main function:"< using namespace std; class Test { private: int num; float f1; public: Test (int n); Test (int n, float f); }; Test::Test(int n) { num = n; cout<<"Initializing "< using namespace std; class Person { public: Person(char *na)//构造函数 { cout<<"call constructor"< using namespace std; class RMB { public: RMB(double value = 0.0)//转换构造函数 { yuan = value; jf = ( value - yuan ) * 100 + 0.5; } operator double()//类类型转换函数 { return yuan + jf / 100.0; } void display() { cout << (yuan + jf / 100.0) << endl; } private: unsigned int yuan; unsigned int jf; }; int main() { RMB d1(2.0), d2(1.5), d3; d3 = RMB((double)d1 + (double)d2); //显式转换 d3.display(); d3 = d1 + d2; /* 隐式转换,系统首先会调用类类型转换函数把对象d1和d2隐式转换为double数据类型, 然后进行相加,加完的结果再调用构造函数隐式转换为RMB类类型,赋值给d3*/ d3.display(); return 0; } 程序的运行结果如图3.22所示。 图3.22例3.14的运行结果 【程序解析】 若在转换构造函数前面加上explicit关键字,如explicit RMB(double value=0.0);,则转换构造函数不再进行隐式类型转换,当编译语句“d3=d1+d2;”时会出现如图3.23所示的编译错误提示信息。 图3.23编译错误提示信息 3.3.2析构函数 类的另一个特殊的成员函数是析构函数。析构函数的功能是当对象被撤销时,释放该对象占用的内存空间。析构函数的作用与构造函数正好相反,一般情况下,析构函数执行构造函数的逆操作。在对象消亡时,系统将自动调用析构函数,执行一些在对象撤销前必须执行的清理任务,例如将构造函数中动态分配的内存空间释放掉。 与构造函数相同的是在定义析构函数时,不能指定任何的返回类型,也不能使用void。与构造函数不同的是构造函数可以带参数,可以重载,而析构函数没有参数,每个类只能有一个析构函数。若未显式编写自己的析构函数,编译器会提供一个默认析构函数,析构函数的函数名为类名前加~。 1. 析构函数被自动调用的三种情况 (1) 一个动态分配的对象被删除,即使用delete删除对象时,编译系统会自动调用析构函数。 (2) 某个对象的生命周期结束时。 (3) 一个编译器生成的临时对象不再需要时。 2. 析构函数的手工调用 除对象数组之外,构造函数只能由系统自动调用,而析构函数可以使用下述方法手工调用: 对象名.类名::析构函数名(); 但一般情况下,不显式调用析构函数,而由系统自动调用。 3. 析构函数与构造函数的调用顺序 构造函数和析构函数的调用顺序刚好相反,在同一作用域中先构造后析构。 【例3.15】析构函数和构造函数的调用顺序示例。 // ch3_15.cpp #include using namespace std; class Student{ public: Student(char* pName="no name",int ssId=0) { strncpy_s(name,pName,40); name[39]= '\0'; id = ssId; cout <<"Constructing new student " < using namespace std; class StudentID{ public: StudentID(int id=0)//带默认参数的构造函数 { value=id; cout <<"Assigning student id " < using namespace std; class Student//学生类的定义 { public: Student() { cout<<"constructing student."< using namespace std; classA { public: A() { numbers++; } intgetNumbers() { returnnumbers; } staticintnumbers;//定义静态数据成员 }; intA::numbers=0; //静态数据成员的初始化 intmain() { cout< using namespace std; class Student{ public: Student(char* pName ="no name") { cout <<"create one student\n"; strncpy_s(name,pName,40); name[39]='\0'; numbersOfStudent++; //静态成员: 每创建一个对象,学生人数增加1 cout <<"现有 "< using namespace std; Student* Student::pFirst =0; Student::Student(void) { } Student::Student(char* pName) { cout<<"Construct "<pNext) if(pS->pNext == this){//找到时,pS指向当前结点的前一结点 pS->pNext = pNext;//pNext即this->pNext return; } } // ch3_20.cpp #include #include "Student.h" using namespace std; Student*fn() { cout<<"------In fn()------"< #include using namespace std; class MyString//定义一个字符串类 { public: MyString(char *s)//定义构造函数 { length=strlen(s); //取字符串长度 contents=new char[length+1]; //为字符串分配存储空间 strcpy_s(contents,length+1,s); //字符串复制 } static int set_total_length() { total_length+=length; return total_length; } ~MyString() { delete[]contents; } private: static int total_length; //定义一个静态变量,用来存放所有字符串的总长度 int length; //存放此字符串长度的变量 char * contents; //存放字符串内容 }; int MyString::total_length=0; //给静态变量赋初值 int main() { MyString obj1("the first object"); cout<< obj1.set_total_length()< using namespace std; class Student { public: Student(int id1,char name1[],int score1) { id = id1; score = score1; strcpy_s(name, name1); sum += score1; //求学生成绩总和 number++; //学生人数自增 } static double average() { return sum / number; //求所有学生的平均分 } void display() { cout< using namespace std; classA { public: int set(int k) { i=++k; return i; } private: int i; }; int main() { int(A::*f)(int)=&A::set; Aaa; cout<<(aa.*f)(10)< using namespace std; class A { public: static void set(int k) { i=k;i++; } private: static int i; friend class B; }; class B { public: static void ds(int l) { int *p=&A::i; cout<<*p< using namespace std; Set::Set(void) { } Set::~Set(void) { } Bool Set::Member(int elem) { for(int i=0;ielems[i] = elems[i]; set->card = card; } Bool Set::Equal(Set *set) { if(card != set->card) return False; for(int i = 0; i < card;++i) //判断当前集合的某元素是否是Set所指集合中的元素 if(!set->Member(elems[i])) return False; return True; } void Set::Print() { cout<<"{"; for(int i = 0; i < card; ++i) cout << elems[i]<< ";"; cout<<"}\n"; } void Set::Intersect(Set *set, Set *res)//交集: *this∩*set->*res { res->card = 0; for(int i = 0; i < card; ++i) for(int j = 0; j < set->card; ++j) if(elems[i] == set->elems[j]){ res->elems[res->card++] = elems[i]; break; } } ErrCode Set::Union(Set *set,Set *res)//并集: *set∪*this->*res { set->Copy(res); for(int i = 0; i < card; ++i) if(res->AddElem(elems[i]) == overflow) return overflow; return noErr; } // ch3_25.cpp #include "Set.h" #include using namespace std; // 下面是测试用的程序代码 int main() { Set s1, s2, s3; s1.EmptySet(); s2.EmptySet(); s3.EmptySet(); s1.AddElem(10); s1.AddElem(20); s1.AddElem(30); s1.AddElem(40); s2.AddElem(30); s2.AddElem(50); s2.AddElem(10); s2.AddElem(60); cout<<"s1="; s1.Print(); cout<<"s2="; s2.Print(); s2.RmvElem(50); cout<<"s2-{50}="; s2.Print(); if(s1.Member(20)) cout<<"20 is in s1\n"; s1.Intersect(&s2,&s3); cout<<"s1 intsec s2 ="; s3.Print(); s1.Union(&s2,&s3); cout<<"s1 union s2 ="; s3.Print(); if(!s1.Equal(&s2)) cout<<"s1!=s2\n"; return 0; } 图3.36例3.25的运行结果 程序的运行结果如图3.36所示。 【例3.26】实现一个大小可变的整型数据元素集合,集合可存储的数据元素个数在对象构造时给定,由构造函数为数据元素分配存储空间,在对象被释放时由析构函数释放存储空间。 实现该功能的项目中包含Set.h头文件,Set.cpp和ch3_26.cpp两个源程序文件。程序代码如下: // Set.h #pragma once const int maxCard=16; //集合中元素个数的默认最大值 enum ErrCode {noErr, overflow}; //错误代码 enum Bool {False, True}; //Bool类型定义 class Set { public: Set(int sz=maxCard); ~Set(); Bool Member(int); //判断一个数是否为集合中的元素 ErrCode AddElem(int); //向集合中添加元素 void RmvElem(int); //删除集合中的元素 void Copy(Set *); //把当前集合默认到形参指针指向的集合中 Bool Equal(Set *); //判断两个集合是否相等 void Print(); void Intersect(Set *, Set *); //交集 ErrCode Union(Set *, Set *); //并集 private: int size; //元素的最大个数 int *elems; //存储元素的数组 int card; //集合中元素的个数 }; // Set.cpp #include "Set.h" #include using namespace std; Set::Set(int sz) { card=0; size=sz; elems=new int[size]; } Set::~Set(void) { delete []elems; } Bool Set::Member(int elem) { for(int i=0;isizeelems; set->elems = new int [size]; set->size = size; } for(int i=0;ielems[i]=elems[i]; set->card=card; } Bool Set::Equal(Set *set) { if(card!=set->card) return False; for(int i=0;iMember(elems[i])) return False; return True; } void Set::Print() { cout<<"{"; for(int i=0;i0) cout<sizeelems; res->elems = new int[size]; res->size = size; } res->card=0; for(int i=0;icard;++j) if(elems[i]==set->elems[j]) { res->elems[res->card++]=elems[i]; break; } } ErrCode Set::Union(Set *set, Set *res) { if(res->sizesize) { delete []res->elems; res->elems = new int[size+set->size]; res->size = size+set->size; } set->Copy(res); for(int i=0;iAddElem(elems[i])==overflow) return overflow; return noErr; } // ch3_26.cpp #include"Set.h" #include using namespace std; int main() { Set s1, s2, s3; s1.AddElem(10); s1.AddElem(20); s1.AddElem(30); s1.AddElem(40); s2.AddElem(30); s2.AddElem(50); s2.AddElem(10); s2.AddElem(60); cout<<"s1="; s1.Print(); cout<<"s2="; s2.Print(); s2.RmvElem(50); cout<<"s2-{50}="; s2.Print(); if(s1.Member(20)) cout<<"20 is in s1\n"; s1.Intersect(&s2,&s3); cout<<"s1 intsec s2 ="; s3.Print(); s1.Union(&s2,&s3); cout<<"s1 union s2 ="; s3.Print(); if(!s1.Equal(&s2)) cout<<"s1!=s2\n"; return 0; } 程序的运行结果与例3.25相同。 习题 1. 为什么要引入构造函数和析构函数? 2. 类的公有、私有和保护成员之间的区别是什么? 3. 什么是拷贝构造函数,它何时被调用? 4. 设计一个计数器类,当建立该类的对象时其初始状态为0,考虑为计数器定义哪些成员? 5. 定义一个时间类,能提供和设置由时、分、秒组成的时间,并编写出应用程序,定义时间对象,设置时间,输出该对象提供的时间。 6. 设计一个学生类Student,它具有的私有数据成员是: 注册号、姓名、数学成绩、英语成绩、计算机成绩; 具有的公有成员函数是: 求三门课总成绩的函数sum(); 求三门课平均成绩的函数average(); 显示学生数据信息的函数print(); 获取学生注册号的函数get_reg_num(); 设置学生数据信息的函数set_stu_inf()。 编制主函数,说明一个Student类对象的数组并进行全班学生信息的输入与设置,而后求出每一学生的总成绩、平均成绩、全班学生总成绩最高分、全班学生平均分,并在输入一个注册号后,输出该学生有关的全部数据信息。 7. 模拟栈模型的操作,考虑顺序栈和链栈两种形式。 8. 写出下列程序的运行结果: // ex3_8.cpp #include using namespace std; class Tx { public: Tx(int i,int j); ~Tx(); void display(); private: int num1,num2; }; Tx::Tx(int i,int j=10) { num1=i; num2=j; cout<<"Constructing "<