Download presentation
Presentation is loading. Please wait.
Published byΠελάγιος Στεφανόπουλος Modified 5年之前
1
第3章C++面向对象程序设计 3.1 类和对象 作业1 3.2 继承和派生类 作业2 3.3 多态和虚函数 3.4 运算符重载 作业3
3.5 输入输出流库 作业4
2
3.1类和对象 3.1.1类的定义 定义类的一般格式如下: class <类名>
{ private: [<私有数据和函数>] public: [<公有数据和函数>] }; <各个成员函数的实现> 类中的数据和函数是类的成员,分别称为数据成员和成员函数。 public类成员,是公有的,能被外面的程序访问; private类成员,是私有的,只能由类中的函数所使用,而不能被外面的程序所访问。 <各个成员函数的实现>是类定义中的实现部分,这部分包含所有在类体中声明的函数的定义。 当类的成员函数的函数体在类的外部定义时,必须由作用域运算符“::”来通知编译系统该函数所属的类。
3
3.1类和对象 定义类时应注意: (1) 不允许对所定义的数据成员进行初始化,例如类CMeter中,下面的定义是错误的:
class CMeter { ... private: int m_nPos = 10; ... }; (2) 在“public:”或“private:”后面定义的所有成员都是公有或私有的,直到下一个“public:”或“private:”出现为止。 (3) 关键字public和private可以在类中出现多次,且前后的顺序没有关系; (4) 关键字protected(保护)也可修饰成员的类型,它与private两者基本相似,但在类的继承时有所不同。 (5) 数据成员的类型可以是整型、浮点型、字符型、数组、指针和引用等,也可以是另一个类的对象。 (6) 尽量将类单独存放在一个文件中或将类的声明放在.h文件中而将成员函数的实现放在与.h文件同名的.cpp文件中。
4
3.1类和对象 3.1.2 对象的定义 定义对象,格式:<类名> <对象名表>
定义的对象既可是一个普通对象,也可是一个数组对象或指针对象。 对象的成员就是该对象的类所定义的成员。对象成员有数据成员和成员函数,其表示方式如下: <对象名>.<成员名> <对象名>.<成员名>(<参数表>) 前者用来表示数据成员,后者用来表示成员函数。“.”是一个运算符,用来表示对象的成员。 指向对象的指针的成员表示如下: <对象指针名>-><成员名> <对象指针名>-><成员名>(<参数表>) “->”与“.”运算符的区别是:“->”用来表示指向对象的指针成员,“.”用来表示一般对象成员。前者表示数据成员,后者表示成员函数。 下面的两种表示是等价的: (*<对象指针名>).<成员名> 这对于成员函数也适用。另外,引用对象的成员表示与一般对象的成员表示相同。
5
3.1类和对象 3.1.3构造函数和析构函数 构造函数 为了能给数据成员自动设置某些初始值,就要使用类的特殊成员函数——构造函数。构造函数的最大特点是在对象建立时它会被自动执行,用于变量、对象的初始化代码一般放在构造函数中。 构造函数必须与相应的类同名,可以带参数,也可以不带参数,可以重载。例如: class CMeter { public: CMeter(int nPos = 10) // 构造函数 { m_nPos = nPos; } ... 构造函数CMeter(int nPos=10)中,nPos被设置了10,构造函数中相应实参没有被指定时,使用此缺省值。由于构造函数的参数只能在定义对象时指定。 CMeter oMeter; 和 CMeter oMeter(10);
6
3.1类和对象 析构函数 与构造函数相对应的是析构函数。析构函数是另一种特殊的C++成员函数,它只是在类名称前面加上一个“~”符号。每一个类只有一个析构函数,没有任何参数,也不返回任何值。例如: class CMeter { public: ... ~CMeter( ) { } // 析构函数 } 析构函数一般在下列两种情况下被自动调用: (1) 当对象定义在一个函数体中,函数调用结束后,析构函数被自动调用。 (2) new为对象分配动态内存,用delete释放对象时,析构函数被自动调用。
7
3.1类和对象 默认构造函数和析构函数 类定义时,如果没有定义任何构造函数,编译器自动生成一个不带参数的默认构造函数,格式如下:
<类名>::<默认构造函数名>() { } 按构造函数的规定,默认构造函数名同类名。默认构造函数的这样格式也可由用户定义在类体中。在程序中定义一个对象而没有指明初始化时,则编译器便按默认构造函数来初始化该对象。 默认构造函数对对象初始化时,则将对象的所有数据成员都初始化为零或空。 如果一个类中没有定义析构函数,编译系统生成一个默认析构函数,格式: <类名>::~<默认析构函数名>() 默认析构函数名与该类的类名同名。是一个空函数。
8
3.1类和对象 构造函数的重载 [例Ex_ConDefault] 带默认参数的构造函数。
#include <iostream.h> class CDate { public: CDate(int year = 2002, int month = 7, int day = 30) { nYear = year; nMonth = month; nDay = day; cout<<nYear<<"-"<<nMonth<<"-"<<nDay<<endl; } // 其他公共成员 private: int nYear, nMonth, nDay; }; void main() { CDate day1; CDate day2(2002, 8); 运行结果为:
9
3.1类和对象 拷贝构造函数 [例Ex_ConCopy] 拷贝构造函数的使用。 #include <iostream.h>
class CDate {public: CDate(int year = 2002, int month = 7, int day = 30) { cout<<"调用构造函数"<<endl; nYear = year; nMonth = month; nDay = day; cout<<nYear<<"-"<<nMonth<<"-"<<nDay<<endl; } CDate(const CDate &ymd) { cout<<"调用拷贝构造函数"<<endl; cout<<ymd.nYear<<"-"<<ymd.nMonth<<"-"<<ymd.nDay<<endl; private: int nYear, nMonth, nDay; }; void main() { CDate day1(2002, 8); CDate day2(day1);
10
3.1类和对象 [例Ex_ConCopyDefault] 默认拷贝构造函数的使用。 #include <iostream.h>
class CDate {public: CDate(int year = 2002, int month = 7, int day = 30) { cout<<"调用构造函数"<<endl; nYear = year; nMonth = month; nDay = day; } void output() { cout<<nYear<<"-"<<nMonth<<"-"<<nDay<<endl; } private: int nYear, nMonth, nDay; }; void main() { CDate day1(2002, 8); CDate day2(day1); // 调用默认的拷贝函数 day1.output(); day2.output(); } 运行结果为: 调用构造函数
11
3.1类和对象 3.1.4对象成员初始化 在实际应用中往往需要多个类,这时就可能把一个已定义类的对象作为另一个类的成员。为了能对这些对象成员进行初始化,构造函数定义格式: <类名>::<构造函数名>(形参表):对象1(参数表), 对象2(参数表), …, 对象n(参数表) { } 对象1、对象2、…、对象n就是该类使用的其他类的对象,冒号“:”后面的列表称为成员初始化列表。 说明: (1)对象成员必须初始化,但不能将对象成员直接在构造函数体内进行初始化。 (2)对象成员初始化时,必须有相应的构造函数,且多个对象成员的构造次序不是按初始化成员列表的顺序,而是按各类声明的先后次序进行的。 (3)对象成员初始化也可在类的外部进行,但必须与构造函数在一起。 (4)事实上,成员初始化列表也可用于类中的普通数据成员的初始化。
12
3.1类和对象 3.1.5静态成员 静态数据成员 是同一个类中所有对象共享的成员,不是某一对象的成员。用静态数据成员可以节省内存,是所有对象所公有的,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,它的值是可以更新的。静态数据成员是静态存储的,具有静态生存期。定义静态数据成员: (1) 使用关键词static声明静态数据成员。 (2) 对静态数据成员进行初始化。静态数据成员要分配空间,不能在类声明中进行初始化。静态数据成员初始化在类的外部进行,它的格式如下: <数据类型><类名>::<静态数据成员名>=<值> 静态成员函数 静态成员函数属于类的静态成员,不是对象成员。对静态成员的引用不需要用对象名。 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。如果静态成员函数中要引用非静态成员时,可通过对象来引用。 公有的静态成员函数既可以有通过相应的对象访问,也可以通过其所属的类名来引用。
13
3.1类和对象 3.1.6友元 [例Ex_FriendFunc] 友元函数的使用。 #include <iostream.h>
class CPoint {public: CPoint() { m_x = m_y = 0; } CPoint( unsigned x, unsigned y ) { m_x = x; m_y = y; } void Print() { cout << "Point(" << m_x << ", " << m_y << ")"<< endl; } friend CPoint Inflate(CPoint &pt, int nOffset); // 声明一个友元函数 private: unsigned m_x, m_y; }; CPoint Inflate ( CPoint &pt, int nOffset ) // 友元函数的定义 { CPoint ptTemp = pt; ptTemp.m_x += nOffset; // 直接改变私有数据成员m_x和m_y ptTemp.m_y += nOffset; return ptTemp; } void main() { CPoint pt( 10, 20 ); pt.Print(); pt = Inflate(pt, 3); // 调用友元函数 pt.Print(); }
14
3.1类和对象 3.1.7常类型 常对象 常对象是指对象常量,定义格式:<类名> const <对象名>
常指针和常引用 常指针也是使用关键字const来修饰的。有三种形式。 第一种形式是将const放在指针变量的类型之前,表示声明一个指向常量的指针。此时,在程序中不能通过指针来改变它所指向的数据值,但可以改变指针本身的值。 第二种形式是将const放在指针定义语句的指针名前,表示指针本身是一个常量,称为指针常量或常指针。因此,不能改变这种指针变量的值,但可以改变指变量所指向的数据值。 第三种形式是将const在上述两个地方都加,表示声明一个指向常量的指针常量,指针本身的值不可改变,而且它所指向的数据的值也不能通过指针改变。
15
3.1类和对象 [例Ex_ConstPara] 常参数的函数传递。 #include <iostream.h>
class COne { public: void print(const int *p, int n) // 使用常参数 { cout<<"{"<<*p; for (int i = 1; i<n; i++) cout<<", "<<*(p+i); cout<<"}"<<endl; } private: }; void main() { int array[6] = {1, 2, 3, 4, 5, 6}; COne one; one.print(array, 6);
16
3.1类和对象 常成员函数 [例Ex_ConstFunc] 常成员函数的使用。 #include <iostream.h>
class COne { public: COne(int a, int b) { x = a; y = b; } void print(); void print() const; // 声明常成员函数 private: int x, y; }; void COne::print() { cout<<x<<", "<<y<<endl; } void COne::print() const { cout<<"使用常成员函数:"<<x<<", "<<y<<endl; } void main() { COne one(5, 4); one.print(); const COne two(20, 52); two.print(); }
17
3.1类和对象 常成员函数可以理解成是“只读”函数,既不能更改数据成员的值,也不能调用那些引起数据成员值变化的成员函数,只能调用const成员函数。例如: class CDate { public: CDate( int mn, int dy, int yr );// 构造函数 int getMonth() const; // 常成员函数 void setMonth( int mn ); // 一般成员函数 int month; // 数据成员 }; int CDate::getMonth() const { return month; // 不能修改数据成员的值,只有一个返回值 } void CDate::setMonth( int mn ) { month = mn; // 可以使用赋值等语句,修改数据成员的值
18
3.1类和对象 常数据成员 类型修饰符const不仅可以说明成员函数,也可以说明数据成员。类中声明const数据成员时,只能通过成员初始化列表的方式来生成构造函数对数据成员初始化。例如: [例Ex_ConstData] 常数据成员的使用。 #include <iostream.h> class COne { public: COne(int a):x(a),r(x) // 常数据成员的初始化 { } void print(); const int &r; // 引用类型的常数据成员 private: const int x; // 常数据成员 static const int y; // 静态常数据成员 }; const int COne::y = 10; // 静态数据成员的初始化 void COne::print() { cout<<"x = "<<x<<", y = "<<y<<", r = "<<r<<endl; } void main() { COne one(100); one.print(); } 该程序的运行结果为: x = 100, y = 10, r = 100
19
3.1类和对象 3.1.8 this指针 对一个对象调用成员函数时,编译器将对象的地址赋给this指针,再调用成员函数,每次成员函数存取数据成员时,由隐含作用this指针。可以用*this来标识调用该成员函数的对象。 [例Ex_This] this指针的使用。 #include <iostream.h> class COne { public: COne() { x = y = 0; } COne(int a, int b) { x = a; y = b; } void copy(COne &a); //对象引用作函数参数 void print() {cout<<x<<" , "<<y<<endl; } private: int x, y; }; void COne::copy(COne &a) { if (this == &a) return; *this = a; } void main() { COne one, two(3, 4); one.print();one.copy(two); one.print(); }
20
3.1类和对象 3.1.9 类的作用域和对象的生存期 1. 类的作用域 类的作用域是指在类的定义中由一对花括号所括起来的部分。
1. 类的作用域 类的作用域是指在类的定义中由一对花括号所括起来的部分。 类作用域中可以定义变量,也可以定义函数。类作用域中定义的变量不能使用auto,register和extern等修饰符,只能用static修饰符,而定义的函数也不能用extern修饰符。在类作用域中的静态成员和成员函数还具有外部的连接属性。 文件作用域中可以包含类作用域,类作用域小于文件作用域。一般地,类作用域中可包含成员函数的作用域。 具体地讲,某个类A中某个成员M在下列情况下具有类A的作用域: (1) 成员M出现在类A的某个成员函数中,并且该成员函数没有定义同名标识符。 (2) 成员M出现在a.M或A::M表达式中,其中a是A的对象。 (3) 成员M出现在pa->M这样的表达式中,其中pa是一个指向A类对象的指针。
21
3.1类和对象 不同存储的对象生存期不同。所谓对象的生存期是指对象从被创建开始到被释放为止的时间。按生存期的不同对象分为三种:
2. 对象的生存期 不同存储的对象生存期不同。所谓对象的生存期是指对象从被创建开始到被释放为止的时间。按生存期的不同对象分为三种: (1) 局部对象:对象被定义时调用构造函数,该对象被创建,当程序退出定义该对象所在的函数体或程序块时,调用析构函数,释放该对象。 (2) 静态对象:当程序第一次执行所定义的静态对象时,该对象被创建,当程序结束时,该对象被释放。 (3) 全局对象:当程序开始时,调用构造函数创建该对象,当程序结束时调用析构函数释放该对象。 局部对象被定义在函数体或程序块内,作用域小,生存期短。静态对象被定义在文件中,作用域比较大,生存期也比较大。全局对象被定义在某个文件中,作用域在包含该文件的整个程序中,作用域是最大的,生存期也是最长的。
22
作业 P368~370:1~18偶数题
23
3.2继承和派生类 3.2.1单继承 公有继承(public)
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的。例如: class CStick : public CMeter { int m_nStickNum; // 声明一个私有数据成员 public: void DispStick(); // 声明一个公有成员函数 }; // 注意分号不能省略 void CStick:: DispStick() { m_nStickNum = GetPos(); // 调用基类CMeter的成员函数 cout<<m_nStickNum<<’ ’; } 从基类CMeter派生的CStick类除具有CMeter所有公有成员和保护成员外,还有自身的私有数据成员m_nStickNum和公有成员函数DispStick()。 注意:派生类中或派生类的对象可以使用基类的公有成员(包括保护成员),例如CStick的成员函数DispStick中调用了基类CMeter的GetPos函数,oStick对象调用了基类的StepIt成员函数;但基类或基类的对象却不可以使用派生类的成员。
24
3.2继承和派生类 私有继承(private) 私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。 [例Ex_ClassPrivateDerived] 派生类的私有继承示例。 #include <iostream.h> class CMeter {public: CMeter(int nPos = 10) { m_nPos = nPos; } ~CMeter() { } void StepIt(){ m_nPos++; } int GetPos(){ return m_nPos; } protected: void SetPos(int nPos) { m_nPos = nPos; } private: int m_nPos; };
25
3.2继承和派生类 class CStick : private CMeter // 从CMeter派生,私有继承
{ int m_nStickNum; // 声明一个私有数据成员 public: void DispStick(); // 声明一个公有成员函数 void SetStick(int nPos) { SetPos(nPos); } int GetStick() { return GetPos(); } }; void CStick:: DispStick() { m_nStickNum = GetPos(); // 调用基类CMeter的成员函数 cout<<m_nStickNum<<' '; } void main() { CMeter oMeter(20); CStick oStick; cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetStick()<<endl; oMeter.StepIt(); oStick.DispStick(); }
26
3.2继承和派生类 保护继承(protected)
特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。 注意,一定要区分清楚派生类的对象和派生类中的成员函数对基类的访问是不同的。例如,在公有继承时,派生类的对象可以访问基类中的公有成员,派生类的成员函数可以访问基类中的公有成员和保护成员。在私有继承和保护继承时,基类的所有成员不能被派生类的对象访问,而派生类的成员函数可以访问基类中的公有成员和保护成员。
27
3.2继承和派生类 3.2.2派生类的构造函数和析构函数 派生类的构造函数和析构函数被执行时,基类相应的构造函数和析构函数也会被执行。
注意,派生类对象在建立时,先执行基类的构造函数,然后执行派生类的构造函数。但对于析构函数来说,其顺序刚好相反,先执行派生类的析构函数,而后执行基类的析构函数。 需要注意的是,如果在对派生类进行初始化时,需要对其基类设置初值,则可按下列格式进行: <派生类名>(总参表):<基类1>(参数表1), <基类2>(参数表2), …, <基类n>(参数表n), 对象成员1(对象成员参数表1), 对象成员2(对象成员参数表2), …, 对象成员n(对象成员参数表n) { ... } 构造函数总参表后面给出的是需要用参数初始化的基类名、对象成员名及各自对应的参数表,基类名和对象成员名之间的顺序可以是任意的,且对于使用默认构造函数的基类和对象成员,可以不列出基类名和对象成员名。这里所说的对象成员是指在派生类中新声明的数据成员,它属于另外一个类的对象。对象成员必须在初始化列表中进行初始化。
28
3.2继承和派生类 3.2.3多继承 在类的继承中,允许一个派生类继承多个基类,这种多继承的方式可使派生类具有多个基类的特性,大大提高了程序代码的可重用性。 多继承下派生类的定义是按下面的格式: class <派生类名> : [<继承方式1>] <基类名1>,[<继承方式2>] <基类名2>,... { [<派生类的成员>] }; 其中的继承方式还是前面的三种:public、private和protected。 例如: class A {...} class B class C:public A,private B 派生类C的成员包含了基类A中成员和B中成员以及该类本身的成员。 允许一个基类有多个派生类以及从一个基类的派生类中再进行多个层次的派生。
29
3.2继承和派生类 3.2.4虚基类 [例Ex_Conflict] 基类成员调用的二义性。
#include <iostream.h> class A { public: int x; A(int a = 0) { x = a; } }; class B1 : public A int y1; B1( int a = 0, int b = 0):A(b) { y1 = a; } class B2 : public A int y2; B2( int a = 0, int b = 0):A(b) { y2 = a; }
30
3.2继承和派生类 class C : public B1, public B2 { public: int z;
C(int a, int b, int d, int e, int m):B1(a,b), B2(d,e) { z = m; } void print() { cout<<"x = "<<x<<endl; // 编译出错的地方 cout<<"y1 = "<<y1<<", y2 = "<<y2<<endl; cout<<"z = "<<z<<endl; } }; void main() { C c1(100,200,300,400,500); c1.print();
31
3.2继承和派生类 派生类B1和B2都从基类A继承,这时在派生类中就有两个基类A的拷贝。当编译器编译到“cout<<”x = “<<x<<endl;”语句时,因无法确定成员x是从类B1中继承来的,还是从类B2继承来,产生了二义性,从而出现编译错误。 解决这个问题的方法之一是使用域作用运算符“ ::”来消除二义性,例如若将print()函数实现代码变为: void print() { cout<<"B1::x = "<<B1::x<<endl; cout<<"B2::x = "<<B2::x<<endl; cout<<"y1 = "<<y1<<", y2 = "<<y2<<endl; cout<<"z = "<<z<<endl; } 重新运行,结果为: B1::x = 200 B2::x = 400 y1 = 100, y2 = 300 z = 500 使用虚基类的目的是在多重派生的过程中,使公有的基类在派生类中只有一个拷贝,从而解决上述这种二义性问题。
32
3.2继承和派生类 [例Ex_VirtualBase] 虚基类的使用。 #include <iostream.h>
class A { public: int x; A(int a = 0) { x = a; } }; class B1 : virtual public A int y1; B1( int a = 0, int b = 0):A(b) { y1 = a; } void print(void) { cout<<"B1: x = "<<x<<", y1 = "<<y1<<endl; } }; class B2 : virtual public A int y2; B2( int a = 0, int b = 0):A(b) { y2 = a; } { cout<<"B2: x = "<<x<<", y2 = "<<y2<<endl; }
33
3.2继承和派生类 class C : public B1, public B2 {public: int z;
C(int a, int b, int d, int e, int m):B1(a,b), B2(d,e) { z = m; } void print() { B1::print(); B2::print(); cout<<"z = "<<z<<endl; } }; void main() { C c1(100,200,300,400,500); c1.print(); c1.x = 400; c1.print() } 运行结果为: B1: x = 0, y1 = 100 B2: x = 0, y2 = 300 z = 500 B1: x = 400, y1 = 100 B2: x = 400, y2 = 300
34
3.2继承和派生类 从程序中可以看出: (1) 声明一个虚基类的格式如下: virtual <继承方式><基类名>
(1) 声明一个虚基类的格式如下: virtual <继承方式><基类名> 声明虚基类与声明派生类一道进行,写在派生类名的后面。 (2) 在派生类B1和B2中只有基类A的一个拷贝,改变成员x的值时,由基类B1和B2中的成员函数输出的成员x的值是相同的。 (3) 由虚基类经过一次或多次派生出来的派生类,在其每一个派生类的构造函数的成员初始化列表中必须给出对虚基类的构造函数的调用,如果未列出,则调用虚基类的默认构造函数,在这种情况下,虚基类的定义中必须要有默认的构造函数。 (4)程序中,类C的构造函数尽管分别调用了其基类B1和B2的构造函数,由于虚基类A在类C中只有一个拷贝,所以编译器无法确定应该由类B1的构造函数还是由类B2的构造函数来调用基类A的构造函数。在这种情况下,在执行类B1和B2的构造函数都不调用虚基类A的构造函数,而是在类C的构造函数中直接调用虚基类A的默认构造函数。若将A的构造函数改为: A(int a = 100) { x = a; } 则成员x的初始值为100。当然,不能变成: A(int a) { x = a; } 因为类A中没有声明默认构造函数,因此会出现编译错误。可以是: A():x(0) { }
35
作业 P370~372:19~27偶数题 P397:实验5
36
3.3多态和虚函数 3.3.1虚函数 [例Ex_VirtualFunc] 虚函数的使用。
#include <iostream.h> class CShape { public: virtual float area() // 将area定义成虚函数 { return 0.0; } }; class CTriangle:public CShape CTriangle(float h, float w) { H=h; W=w; float area() { return (float)(H * W * 0.5); private: float H, W;
37
3.3多态和虚函数 class CCircle:public CShape { public: CCircle(float r)
{ R=r; } float area() { return (float)( * R * R); } private: float R; }; void main() { CShape *s[2]; s[0] = new CTriangle(3,4); cout<<s[0]->area()<<endl; s[1] = new CCircle(5); cout<<s[1]->area()<<endl; } 运行结果为: 6
38
3.3多态和虚函数 说明: (1) 虚函数在重新定义时参数的个数和类型必须和基类中的虚函数完全匹配。
(1) 虚函数在重新定义时参数的个数和类型必须和基类中的虚函数完全匹配。 (2) 虚函数所具备的上述功能,只有通过基类指针才可得到。虚函数在用对象名和成员运算符以正常方式调用时,不能达到其功能。例如: CShape ss; ss.area(); 将得到0.0。 (3) 如果不使用new来创建相应的派生类对象,也可使用下列方法来实现: void main() { CShape *p1, *p2; CTriangle tri(3, 4); CCircle cir(5); p1 = &tri; p2 = ○ cout<<p1->area()<<endl; cout<<p2->area()<<endl; } (4) 虚函数必须是类的一个成员函数,不能是友元函数,也不能是静态的成员函数。 (5)可把析函数定义为虚函数,但不能将构造函数定义为虚函数。通常在释放基类中和其派生类中的动态申请的存储空间时,也要把析构函数定义为虚函数,以便实现撤消对象时的多态性。
39
3.3多态和虚函数 3.3.2纯虚函数和抽象类 定义一个基类时,有时会遇到情况:无法定义基类中虚函数的具体实现,其实现完全依赖于其不同的派生类。例如,一个“形状类”由于没有确定的具体形状,因此其计算面积的函数也就无法实现。这时可将基类中的虚函数声明为纯虚函数。 声明纯虚函数的一般格式为: virtual <函数类型><函数名>(<形数表>) = 0; 与一般虚函数不同的是:在纯虚函数的形参表后面多了个“= 0”。把函数名赋于0,本质上是将指向函数的指针的初值赋为0。虚函数不能有具体的实现代码。 抽象类是指至少包含一个纯虚函数的特殊的类。它本身不能被实例化,也就是说不能声明一个抽象类的对象。必须通过继承得到派生类后,在派生类中定义了纯虚函数的具有实现代码,才能获得一个派生类的对象。 [例Ex_VirtualFunc] 虚函数的使用。 #include <iostream.h> class CShape { public: virtual float area() = 0; // 将area定义成纯虚函数 };
40
3.3多态和虚函数 class CTriangle:public CShape { public:
CTriangle(float h, float w) { H = h; W = w; } float area() // 在派生类定义纯虚函数的具体实现代码 { return (float)(H * W * 0.5); } private: float H, W; }; class CCircle:public CShape CCircle(float r) { R = r; } float area() // 在派生类定义纯虚函数的具体实现代码 { return (float)( * R * R); } float R;
41
3.3多态和虚函数 void main() { CShape *pShape; CTriangle tri(3, 4);
cout<<tri.area()<<endl; pShape = &tri; cout<<pShape->area()<<endl; CCircle cir(5); cout<<cir.area()<<endl; pShape = ○ } 运行结果为: 6
42
3.4运算符重载 3.4.1运算符重载的语法 为了重载运算符,必须定义一个特殊的函数,以便通知编译器,遇到该重载运算符时调用该函数,并由该函数来完成该运算符应该完成的操作。这种函数称为运算符重载函数,通常是类的成员函数或是友元函数,运算符的操作数通常也是该类的对象。 定义一个运算符重载函数函数名必须以operator开头,一般形式如下: <函数类型><类名>::operator <重载的运算符>(<形参表>) { … } // 函数体 重载的运算符必须是一个合法的运算符,如“+”、“-”、“*”、“/”、“++”等。 [例Ex_Complex] 运算符的简单重载。 #include <iostream.h> class CComplex { public: CComplex(double r = 0, double i = 0) { realPart = r; imagePart = i; } void print() {cout<<"该复数实部 = "<<realPart<<", 虚部 = "<<imagePart<<endl;} CComplex operator + (CComplex &c); // 重载运算符+ CComplex operator + (double r); // 重载运算符+ private: double realPart; // 复数的实部 double imagePart; };
43
3.4运算符重载 CComplex CComplex::operator + (CComplex &c) // 参数是CComplex引用对象 { CComplex temp; temp.realPart = realPart + c.realPart; temp.imagePart = imagePart + c.imagePart; return temp; } CComplex CComplex::operator + (double r) // 参数是double类型数据 temp.realPart = realPart + r; temp.imagePart = imagePart; return temp; } void main() { CComplex c1(12,20), c2(50,70), c; c = c1 + c2; c.print(); c = c1+ 20; } 运行结果为: 该复数实部 = 62, 虚部 = 90 该复数实部 = 32, 虚部 = 20
44
3.4运算符重载 还需要说明的是: (1) 当用成员函数实现双目运算符的重载时,运算符的左操作数一定是对象,右操作数作为调用运算符重载函数的参数,参数可以是对象、对象的引用或是其他类型的参数。例如,若有表达式“c = 20 + c1”,则编译器必将“20 + c1”解释为“20.operator + (c1)”,显然出现编译错误。但实际应用时,这种运算操作是存在的,解决这个问题的办法是将运算符重载为友元函数(在后面讨论)。 (2) 不是所有的运算符都可以重载。在C++中不允许重载的运算符除三目运算符“?:”外,还有成员操作符“.”、成员指针操作符“*”、作用域操作符“::”以及sizeof运算符。 (3) 只能对C++中已定义了的运算符进行重载,而且当重载一个运算符时,该运算符的操作数个数、优先级和结合性是不能改变的。
45
3.4运算符重载 3.4.2友元重载 实现运算符重载的方法有两种:用类的成员函数来实现和通过类的友元函数来实现。这里来用友元函数实现重载的方法。 友元重载方法既可用于单目运算符,也可以用于双目运算符。它们的一般格式如下: friend <函数类型>operator <重载的运算符>(<形参>)// 单目运算符重载 { … } // 函数体 friend <函数类型>operator <重载的运算符>(<重载的运算符>(<形参1, 形数2>) // 双目运算符重载 对于单目运算符的友元重载函数来说,只有一个形参,形参类型可能是类的对象,也可能是引用,这取决于不同的运算符。对于“++”、“--”,这个形参类型是类的引用,对于“-”(负号运算符),形参类型可以是类的引用,也可以是类的对象。对于双目运算符的友元重载函数来说,它有两个形参。这两个形参中必须有一个是类的对象 需要说明的是,有些运算符是不能重载为友元函数的,它们是:=、()、[]和->。
46
3.4运算符重载 3.4.3转换函数 类型转换是将一种类型的值映射为另一种类型的值。类型转换包含自动隐含和强制转换的两种方法。转换函数是实现强制转换的手段之一,它是类中定义的一个非静态成员函数,其一般格式为; class <类名> { public: operator <类型>(); … } <类型>是要转换后的数据类型,可以是基本的数据类型,也可以是导出的的数据类型。operator和<类型>一起构成了转换函数名。这个转换函数的作用是将“class<类名>”声明的类对象转换成<类型>指定的数据类型。 [例Ex_Money] 转换函数的使用。 #include <iostream.h> #include <string.h> typedef char* string; class CMoney CMoney(double a = 0.0) { amount = a; } operator string (); private: double amount; };
47
3.4运算符重载 CMoney::operator string ()
{ string basestr[15] = {"分", "角", "元", "拾", "佰", "仟", "万", "拾", "佰", "仟", "亿","拾", "佰", "仟", "万"}; string datastr[10] = {"零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"}; static char strResult[80]; double temp, base = 1.0; int n = 0; temp = amount * 100.0; strcpy(strResult, "金额为: "); if (temp < 1.0) strcpy (strResult, "金额为: 零元零角零分"); else { while (temp>= 10.0) { // 计算位数 base = base * 10.0; temp = temp / 10.0; n++; } if (n>=15) strcpy(strResult, "金额超过范围!");
48
3.4运算符重载 else { temp = amount * 100.0; for (int m=n; m>=0; m--)
{ int d = (int)(temp / base); temp = temp - base*(double)d; base = base / 10.0; strcat(strResult, datastr[d]); strcat(strResult, basestr[m]); } return strResult; void main() { CMoney money( ); cout<<(string)money<<endl; 运行结果是: 金额为: 壹万贰仟叁佰肆拾壹亿贰仟叁佰肆拾伍万陆仟柒佰捌拾玖元壹角贰分
49
3.4运算符重载 3.4.4赋值运算符的重载 相同类型的对象之间可以直接相互赋值,不是所有的同类型对象都可以这么操作的。对象的成员中有数组或动态的数据类型时,就不能直接相互赋值,否则在程序的编译或执行过程中出现编译或运行错误。例如: class Cdemo { public: CDemo(char *s) { ps = new char[strlen(s) + 1]; strcpy(ps, s); } ~CDemo() { if (ps) delete[] ps; } void print() { cout<<ps<<endl; } private: char *ps; }; void main() { CDemo d1("Key"), d2("Mouse"); d1 = d2; } 程序运行到“d1 = d2”时发生运行错误。因此,必须重载“=”运算符,
50
3.4运算符重载 [例Ex_Evaluate] 赋值运算符的重载。 #include <iostream.h>
#include <string.h> class Cdemo { public: CDemo& operator = (CDemo &a) // 赋值运算符重载 { if (ps) delete[] ps; if (a.ps) { ps = new char[strlen(a.ps) + 1]; strcpy(ps, a.ps); } else ps = 0; return *this; } private: char *ps; }; void main() { CDemo d1("Key"), d2("Mouse"); d1 = d2; d1.print(); }
51
3.4运算符重载 需要说明的是: (1) 赋值运算符重载函数operator = ()的返回类型是CDemo&,注意它返回是类的引用而不是对象。这是因为,C++要求赋值表达式左边的表达式是左值,它能进行诸如下列的运算: int x, y = 5; (x = y)++; // 结果x为6 由于引用的实质就是对象的地址,所以通过引用当然可以改变对象的值。而如果返回的类型是类的对象,则被认为是一个常量,不能出现在等号左边作为左值(也就是说不能被改变)。 (2) 赋值运算符不能重载为友元函数,只能是一个非静态成员函数。 (3) 赋值运算符是唯一的一个不能被继承的运算符函数。
52
3.4运算符重载 3.4.5增1和减1运算符的重载 增1“++”和减1“--”运算符是单目运算符,有前缀和后缀运算符两种。为了区分这两种运算符,在重载时将后缀运算符视为双目运算符。即 obj++或obj-- 被看作为: obj++0或obj--0 [例Ex_IncrementAndDecrement] “++”和“—”运算符的重载。 #include <iostream.h> class CCounter { public: CCounter() { unCount =0; } CCounter operator ++(); CCounter operator ++(int); void print() { cout<<unCount<<endl; } private: unsigned unCount; };
53
3.4运算符重载 CCounter CCounter::operator ++() // 前缀++运算符 { unCount++;
return *this; } CCounter CCounter::operator ++(int) // 后缀++运算符 { CCounter temp; temp.unCount = unCount++; return temp; void main() { CCounter demo1; for(int i=0; i<8; i++) demo1++; demo1.print(); CCounter demo2; for(i=0; i<8; i++) ++demo2; demo2.print(); 运行结果为: 8
54
作业 P372~373:28~32 P402:实验6
55
3.5输入输出流库 3.5.1概述 输入输出操作是由“流”来处理的。数据从一个位置到另一个位置的流动抽象为“流”。数据从键盘流入到程序中时,这样的流称为“输入流”,数据从程序中流向屏幕或磁盘文件时,这样的流称为“输出流”。 当流被建立后就可以使用一些特定的操作从流中获取数据可向流中添加数据。从流中获取数据的操作称为“提取”操作,向流中添加数据的操作称为“插入”操作。 C++提供了四个预定义的标准流对象:cin、cout、cerr和clog。当在程序中包含了头文件“iostream.h”,编译器调用相应的构造函数,产生这四个标准流对象,在程序中就可以直接使用它们了。 cin是istream类的对象,用处理标准输入,即键盘输入。cout是ostream类的对象,用处理标准输出,即屏幕输出。cerr和clog都是ostream类的对象,用来处理标准出错信息,并将信息显示在屏幕上。在这四个标准流对象中,除了cerr不支持缓冲外,其余三个都带有缓冲区。 标准流通常用提取运算符“>>”和插入运算符“<<”来进行输入输出操作的,而且系统还会自动地完成数据类型的转换。由于前面已讨论过cin和cout的用法,对于cerr和clog也可同样使用,因此这里就不再重复了。 ios istream ostream iostream streambuf 图3.1 C++的输入输出流库
56
3.5输入输出流库 3.5.2流的格式控制和错误处理 使用格式控制成员函数 在ios类中控制输入输出的成员函数有:
int ios::width(); // 返回当前的宽度设置 int ios::width(int); // 设置宽度并返回上一次的设置 int ios::precision(); // 返回当前的精度设置 int ios::precision(int); // 设置精度并返回上一次的设置 char ios::fill(); // 返回当前空位填充的字符 char ios::fill(char);// 设置空位填充的字符并返回上一次的设置 long ios::setf(long); // 设置状态标志并返回上一次的状态标志 long ios::unsetf(long); // 消除状态标志并返回上一次的状态标志 long ios::flags(); // 返回当前的状态标志 long ios::flags(long); // 设置状态标志并返回上一次的状态标志
57
3.5输入输出流库 [例Ex_FormatFunc] 使用格式控制成员函数。 #include <iostream.h>
void main() { int nNum = 12345; double dNum = ; char *str[] = {"This", "is", "a Test!"}; cout.setf(ios::oct|ios::showbase|ios::showpos); cout<<nNum<<"\t"<<dNum<<endl; cout.setf(ios::hex|ios::scientific|ios::uppercase); cout<<nNum<<"\t"<<dNum<<endl; cout.fill('*'); // 设置填充符号为* for (int i=0; i<3; i++) { cout.width(12); cout<<str[i]<<" "; } cout<<endl; cout.setf(ios::left); // 设置标志:左对齐 for (i=0; i<3; i++) }
58
3.5输入输出流库 使用格式算子 格式算子是一个对象,可以直接被插入符或提取符操作。
59
3.5输入输出流库 [例Ex_Formator] 使用格式算子。 #include <iostream.h>
#include <iomanip.h> void main() { int nNum = 12345; double dNum = ; char *str[] = {"This", "is", "a Test!"}; cout<<setiosflags(ios::oct|ios::showbase|ios::showpos); cout<<nNum<<"\t"<<dNum<<endl; cout<<setiosflags(ios::hex|ios::scientific|ios::uppercase); cout<<nNum<<"\t"<<dNum<<endl; cout<<setfill('*'); // 设置填充符号为* for (int i=0; i<3; i++) cout<<setw(12)<<str[i]<<" "; cout<<endl; cout<<setiosflags(ios::left); // 设置标志:左对齐 for (i=0; i<3; i++) }
60
3.5输入输出流库 流的错误处理 在输入输出过程中,一旦发现操作错误,流就会将发生的错误记录下来。可以使用提供的错误检测功能,检测和查明错误发生的原因和性质,然后调用clear()函数清除错误状态,使流能够恢复处理。 在ios类中,定义了一个公有枚举成员io_state来记录各种错误的性质: enum io_state { goodbit = 0x00, // 正常 eofbit = 0x01, // 已达到文件尾 failbit = 0x02, // 操作失败 badbit = 0x04 // 非法操作 }; 在ios类中又定义了检测上述流状态的下列成员函数: int ios::rdstate(); // 返回当前的流状态 int ios::bad(); // 如果badbit位被置位,返回非0 void ios::clear(int); // 清除错误状态 int ios::eof(); // 返回非0表示提取操作已到文件尾 int ios::fail(); // 如果failbit位被置位,返回非0 int ios::good(); // 操作正常时,返回非0
61
3.5输入输出流库 [例Ex_ManipError] 检测流的错误。 #include <iostream.h>
void main() { int i, s; char buf[80]; cout<<"输入一个整数:"; cin>>i; s = cin.rdstate(); cout<<"流状态为:"<<hex<<s<<endl; while (s) { cin.clear(); cin.getline(buf, 80); cout<<"非法输入,重新输入一个整数:"; }
62
3.5输入输出流库 3.5.3使用输入输出成员函数 输入操作的成员函数 (1) 使用get()和getline()函数
int get(); istream& get( char& rch ); istream& get( char* pch, int nCount, char delim = '\n' ); 第一种形式是从输入流中提取一个字符,并转换成整型数值。第二种形式是从输入流中提取字符到rch中。第三种形式是从输入流中提取一个字符串并由pch返回,nCount用来指定提取字符的最多个数,delim用来指定结束字符,默认时是‘\n’。 getline()原型如下: istream& getline( char* pch, int nCount, char delim = '\n' ); 它是用来从输入流中提取一个输入行,并把提取的字符串由pch返回,nCount和delim的含义同上。 用get()函数提取字符串时,由于遇到换行符就会结束提取,此时换行符仍保留在缓冲区中,当下次提取字符串时就不会正常,而getline()在提取字符串时,换行符也会被提取,但不保存它。因此,当提取一行字符串时,最好能使用函数getline()。
63
3.5输入输出流库 (2) 使用read()函数 read()函数可以读取字符或字符串,可以读取字节流。原型:
istream& read( char* pch, int nCount ); istream& read( unsigned char* puch, int nCount ); istream& read( signed char* psch, int nCount ); read()函数的这几种形式都是从输入流中读取由nCount指定数目的字节并将它们放在由pch或puch或psch指定的数组中。 [例Ex_Read] read()函数的使用。 #include <iostream.h> void main() { char data[80]; cout<<"请输入:"<<endl; cin.read(data, 80); data[cin.gcount()] = '\0'; cout<<endl<<data<<endl; }
64
3.5输入输出流库 输出操作的成员函数 ostream类中用于输出单个字符或字节的成员函数是put()和write(),它们的原型如下:
ostream& put( char ch ); ostream& write( const char* pch, int nCount ); ostream& write( const unsigned char* puch, int nCount ); ostream& write( const signed char* psch, int nCount ); 例如: char data[80]; cout<<"请输入:"<<endl; cin.read(data, 80); cout.write(data,80); cout<<endl;
65
3.5输入输出流库 3.5.4提取和插入运算符重载 C++中的一个最引人注目的特性是允许用户重载“>>”和“<<”运算符,以便用户利用标准的输入输出流来输入输出自己定义的数据类型(包括类),实现对象的输入输出。 重载这两个运算符时,虽然可使用别的方法,但最好将重载声明为类的友元函数,以便能访问类中的私有成员。 [例Ex_ExtractAndInsert] 提取和插入运算符的重载。 #include <iostream.h> class CStudent { public: friend ostream& operator<< ( ostream& os, CStudent& stu ); friend istream& operator>> ( istream& is, CStudent& stu ); private: char strName[10]; // 姓名 char strID[10]; // 学号 float fScore[3]; // 三门成绩 };
66
3.5输入输出流库 ostream& operator<< ( ostream& os, CStudent& stu )
{ os<<endl<<"学生信息如下:"<<endl; os<<"姓名:"<<stu.strName<<endl; os<<"学号:"<<stu.strID<<endl; os<<"成绩:"<<stu.fScore[0]<<",\t"<<stu.fScore[1]<<",\t" <<stu.fScore[2]<<endl; return os; } istream& operator>> ( istream& is, CStudent& stu ) { cout<<"请输入学生信息"<<endl; cout<<"姓名:"; is>>stu.strName; cout<<"学号:"; is>>stu.strID; cout<<"三门成绩:"; is>>stu.fScore[0]>>stu.fScore[1]>>stu.fScore[2]; return is; } void main() { CStudent one; cin>>one; cout<<one; }
67
3.5输入输出流库 3.5.5文件流概述 文件是保存在存储介质上一系列数据的集合。有两种含义,一种指一个外部设备。另一种指一个磁盘文件,存放在磁盘上的文件,每个文件都有一个文件名。 将文件看作是由连续的字符的数据顺序组成的。根据文件中数据的组织方式,可分为文本文件和二进制文件。文本文件中每一个字节用以存放一个字符的ASCII码值,而二进制文件是将数据用二进制形式存放在文件中,它保持了数据在内存中存放的原有格式。 无论是文本文件还是二进制文件,都需要用“文件指针”来操纵。一个文件指针总是和一个文件所关联的,文件打开时,文件指针指向文件的开始,随着对文件的处理,文件指针不断地在文件中移动,并一直指向最新处理的字符(字节)位置。 文件处理有两种方式,一种称为文件的顺序处理,从文件的第一个字符(字节)开始顺序处理到文件的最后一个字符(字节),文件指针也相应地从文件的开始位置到文件的结尾。另一种称为文件的随机处理,在文件中通过C++相关的函数移动文件指针,并指向所要处理的字符(字节)位置。按照这两种处理方式,可将文件相应地称为顺序文件和随机文件。
68
3.5输入输出流库 C++提供了文件操作的文件流库,如图。ifstream类是从istream类公有派生而来,用来支持从输入文件中提取数据的各种操作。ofstream类是从ostream公有派生而来,用来实现把数据写入到文件中的各种操作。fstream类是从iostream类公有派生而来,提供从文件中提取数据或把数据写入到文件的各种操作。filebuf类从streambuf类派生而来,用来管理磁盘文件的缓冲区,应用程序中一般不涉及该类。 图3.2 C++的文件流库 ios istream ostream iostream streambuf filebuf ofstream ifstream fstream
69
3.5输入输出流库 3.5.6顺序文件操作 文件的打开和关闭 打开或创建一个指定的文件需要下列两个步骤:
(1) 声明一个ifstream、ofstream或fstream类对象。例如: ifstream infile; // 声明一个输入(读)文件流对象 ofstream outfile; // 声明一个输出(写)文件流对象 fstream iofile; // 声明一个可读可写的文件流对象 (2) 使用文件流类的成员函数打开或创建一个指定的文件,使得该文件与声明的文件流对象联系起来,这样对流对象的操作也就是对文件的操作。例如: infile.open(“file1.txt”); outfile.open(“file2.txt”); iofile.open("file3.txt",ios::in | ios::out); 上述这两步操作也可合为一步进行,即在声明对象时指定文件名。例如: ifstream infile(“file1.txt”); ofstream outfile(“file2.txt”); fstream iofile("file3.txt",ios::in | ios::out);
70
3.5输入输出流库 ifstream、ofstream或fstream的函数原型如下:
ifstream( const char* szName, int nMode = ios::in, int nProt = filebuf::openprot ); void ifstream::open( const char* szName, int nMode = ios::in, int nProt = filebuf::openprot ); ofstream( const char* szName, int nMode = ios::out, int nProt = filebuf::openprot ); void ofstream::open( const char* szName, int nMode = ios::out, int nProt = filebuf::openprot ); fstream( const char* szName, int nMode, int nProt = filebuf::openprot ); void fstream:;open( const char* szName, int nMode, int nProt = filebuf::openprot ); szName用来指定要打开的文件名,mode指定文件的访问方式。prot用来指定文件的共享方式,默认时是filebuf::openprot,表示DOS兼容的方式。
71
3.5输入输出流库 文件的读写 [例Ex_File] 将文件内容保存在另一文件中,并将内容显示在屏幕上。 void main()
{ fstream file1; // 定义一个fstream类的对象用于读 file1.open("Ex_DataFile.txt", ios::in); if (!file1) { cout<<"Ex_DataFile.txt不能打开!\n"; return; } fstream file2; // 定义一个fstream类的对象用于写 file2.open("Ex_DataFileBak.txt", ios::out | ios::trunc); if (!file2) { cout<<"Ex_DataFileBak.txt不能创建!\n"; file1.close(); char ch; while (!file1.eof()) { file1.read(&ch, 1); cout<<ch; file2.write(&ch, 1); } file2.close(); // 不要忘记文件使用结束后要及时关闭 }
72
3.5 输入输出流库 3.5.7 随机文件操作 提供在文件中来回移动文件指针和非顺序地读写文件的能力,能快速地检索、修改和删除文件中的信息。
顺序文件和随机文件都是以顺序字符流的方式将信息写在磁盘等存储介质上,其区别仅在于文件的访问和更新的方法。以随机的方式访问文件时,文件中的信息在逻辑上组织成定长的记录格式。 定长的记录格式是指文件中的数据被解释成同一种类型的信息的集合。这样就可以通过逻辑的方法,将文件指针直接移动到所读写的数据的起始位置,来读取数据或者将数据直接写到文件的这个位置上。 以随机的方式读写文件时,必须首先打开文件。文件以读(in)或写(out)的模式打开时,文件指针指向文件的第一个字符(字节),而以追加的模式(app)打开后,文件指针指向文件的尾部。 根据具体的读写操作提供seekg和seekp函数将文件指针移动到指定的位置。原型: istream& seekg( long pos ); istream& seekg( long off, ios::seek_dir dir ); ostream& seekp( long pos ); ostream& seekp( long off, ios::seek_dir dir ); pos指定文件指针的绝对位置。off指定文件指针的相对偏移时,文件指针的最后位置还依靠dir的值。dir值可以是: ios::beg 从文件流的头部开始 ios::cur 从当前的文件指针位置开始 ios::end 从文件流的尾部开始
73
作业 P374~375:41~51偶数题 P407:实验7
Similar presentations