250 likes | 400 Views
第 3 章 C++ 进阶. 3.1 多态和虚函数 3.2 运算符重载 3.3 输入输出流库 3.4 模板. 3.1 多态和虚函数. class CCircle:public CShape {public: CCircle(float r) { R=r; } float area() {return (float)(3.14159265 * R * R);} private: float R; }; void main() { CShape *s[2]; s[0] = new CTriangle(3,4);
E N D
第3章C++进阶 3.1 多态和虚函数 3.2 运算符重载 3.3 输入输出流库 3.4 模板
3.1多态和虚函数 class CCircle:public CShape {public: CCircle(float r) { R=r; } float area() {return (float)(3.14159265 * 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 78.5398 3.1.1虚函数 [例Ex_VirtualFunc] 虚函数的使用。 #include <iostream.h> class CShape { public: virtual float area() { return 0.0;} }; 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; };
3.1多态和虚函数 说明: (1)虚函数在重新定义时参数的个数和类型必须和基类中的虚函数完全匹配,这一点和函数重载完全不同。 (2)虚函数所具备的上述功能,只有通过基类指针才可实现。虚函数在用对象名和成员运算符以正常方式调用时,不能达到其效果。 (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)可把析构函数定义为虚函数,但不能将构造函数定义为虚函数。通常在释放基类及其派生类中的动态申请的存储空间时,也要把析构函数定义为虚函数,以便实现撤消对象时的多态性。
3.1多态和虚函数 3.1.2纯虚函数和抽象类 • 定义一个基类时,会遇到这样的情况:无法定义基类中虚函数的具体实现,其实现完全依赖于其不同的派生类。例如,一个“形状类”由于没有确定的具体形状,因此其计算面积的函数也就无法实现。这时可将基类中的虚函数声明为纯虚函数。 • 声明纯虚函数的一般格式为: virtual <函数类型><函数名>(<形数表>) = 0; • 它与一般虚函数不同的是:在纯虚函数的形参表后面多了个“=0”。把函数名赋于0,本质上是将指向函数的指针的初值赋为0。需要说明的是,纯虚函数不能有具体的实现代码。 • 抽象类是指至少包含一个纯虚函数的特殊的类。它本身不能被实例化,也就是说不能声明一个抽象类的对象。必须通过继承得到派生类后,在派生类中定义了纯虚函数的具有实现代码,才能获得一个派生类的对象。 • 与虚函数使用方法相同,也可以声明指向抽象类的指针,虽然该指针不能指向任何抽象类的对象,但可以通过该指针获得对派生类成员函数的调用。事实上,纯虚函数是一个特殊的虚函数。
3.2运算符重载 3.2.1运算符重载的语法 <函数类型><类名>::operator <重载的运算符>(<形参表>) { … } // 函数体 运算符重载函数的函数是以特殊的关键字operator开始的,因而编译器很容易与其他的函数名区分开来。 重载的运算符必须是合法的运算符。 说明: (1)当用成员函数实现双目运算符的重载时,运算符的左操作数一定是对象,右操作数作为调用运算符重载函数的参数,参数可以是对象、对象的引用或是其他类型的参数。 (2)不是所有的运算符都可以重载。不允许重载的运算符除三目运算符“?:”外,还有成员操作符“.”、成员指针取值操作符“*”、作用域操作符“::”以及sizeof运算符。 (3)只能对已定义了的运算符进行重载,而且当重载一个运算符时,该运算符的操作数个数、优先级和结合性是不能改变的。
3.2运算符重载 [例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; // 复数的虚部 }; CComplex CComplex::operator + (CComplex &c) // 参数是CComplex引用对象 { CComplex temp; temp.realPart = realPart + c.realPart; temp.imagePart = imagePart + c.imagePart; return temp;}
3.2运算符重载 CComplex CComplex::operator + (double r) // 参数是double类型数据 { CComplex temp; 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; c.print(); } 运行结果为: 该复数实部= 62, 虚部= 90 该复数实部= 32, 虚部= 20 对运算符“+”作了两次重载,一个用于实现两个复数的加法,另一个用于实现一个复数与一个实数的加法。当重载一个运算符时,必须定义该运算符要完成的具体操作,而且当运算符重载函数是类的成员函数时,该函数的形参个数要比运算符操作数个数少一个,双目运算符重载的成员函数只有一个参数,单目运算符重载的成员函数没有参数。
3.2运算符重载 3.2.2赋值运算符的重载 对象的成员中有数组或动态的数据类型时,就不能直接相互赋值,否则在程序的编译或执行过程中出现编译或运行错误。例如: 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”时发生运行错误。因此,必须重载“=”运算符,它与其他运算符的重载相同。
3.2运算符重载 [例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(); } 运行结果为: Mouse 赋值运算符不能重载为友元函数,只能是一个非静态成员函数。
3.2运算符重载 3.2.3提取和插入运算符重载 C++允许用户重载“>>”和“<<”运算符,以便用户利用标准的输入输出流来输入输出自己定义的数据类型(包括类),实现对象的输入输出。 重载这两个运算符时最好将重载声明为类的友元函数,以便访问类中的私有成员。 友元重载的一般格式如下: friend <函数类型>operator <重载的运算符>(<形参>) // 单目运算符重载 { … } // 函数体 friend <函数类型>operator <重载的运算符>(<形参1, 形数2>)// 双目运算符重载 { … } // 函数体 对于单目运算符的友元重载函数,只有一个形参,形参类型可以是类的对象,可以是引用,这取决于不同的运算符。对于双目运算符的友元重载函数来说,它有两个形参,这两个形参中必须有一个是类的对象。需要说明的是,=、()、[]和->运算符不能用友元来重载。
3.2运算符重载 [例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]; // 三门成绩 }; 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;}
3.2运算符重载 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; } 运行结果为: 请输入学生信息: 姓名:LiMing 学号:21010212 三门成绩:80 90 75 学生信息如下: 姓名:LiMing 学号:21010212 成绩:80, 90, 75
3.3输入输出流库 3.3.1概述 • 输入输出操作是由“流”来处理的。数据从一个位置到另一个位置的流动抽象为“流”。当流被建立后就可以使用一些特定的操作从流中获取数据或向流中添加数据。从流中获取数据的操作称为“提取”操作,向流中添加数据的操作称为“插入”操作。 • ios类提供对流状态进行设置的功能,是虚基类,其它类都是从这个派生而来的,streambuf不是ios类的派生类,在类ios中只是有一个指针成员,指向streambuf类的一个对象。streambuf类是用来为ios类及其派生类提供对数据的缓冲支持。 • itream和ostream类是ios的公有派生类,前者向流中插入数据的有关操作,后者从流中提取数据的有关操作。iostream类是itream和ostream类公有派生的。 • 四个预定义的标准流对象:cin、cout、cerr和clog。当用户在程序中包含了头文件“iostream.h”,编译器调用相应的构造函数,产生这四个标准流对象,用户在程序中就可以直接使用它们了。其中,cin是istream类的对象,用处理标准输入cout是ostream类的对象,用处理标准输出。cerr和clog都是ostream类的对象,用来处理标准出错信息,并将信息显示在屏幕上。在这四个标准流对象中,除了cerr不支持缓冲外,其余三个都带有缓冲区。 • 标准流通常使用提取运算符“>>”和插入运算符“<<”来进行输入输出操作的,而且系统还会自动地完成数据类型的转换。
3.3输入输出流库 3.3.2cout和cin • 输出流(cout) cout可以输出一个整数、实数、字符及字符串,cout中的插入符“<<”可以连续写多个,每个后面可以跟一个要输出的常量、变量、转义序列符、对象以及表达式等。 (1)width函数 格式:int width(); int width(int); 第一种格式用来获取当前输出数据时的宽度,另一种格式是用来设置当前输出数据时的宽度。 (2)precision函数 格式:int precision(); int precision(int); 这两种格式分别用来获取和设置当前浮点数的有效数字的个数,第二种格式函数还将返回设置前的有效数字的个数。需要说明的是,C++默认的有效数字的个数为6。 (3)fill函数 格式:char fill(); char fill(char); 这两种格式分别用来获取和设置当前宽度内的填充字符,第二种格式函数还将返回设置前的填充字符。
3.3输入输出流库 • 输入流(cin) 格式:cin>> <表达式1 > [>> <表达式2> ...] 提取符“>>”可以连续写多个,每个后面跟一个表达式,该表达式通常是获得输入值的变量或对象。 要求用户从键盘上输入三个整数。输入时,必须在三个数值之间加上一些空格来分隔,空格的个数不限,最后用回车键结束输入;或者在每个数值之后按回车键。 • 格式算子oct、dec和hex 格式算子oct、dec和hex能分别将输入或输出的数值转换成八进制、十进制及十六进制。
3.3输入输出流库 输入输出过程中发现操作错误,流就会将发生的错误记录下来。用户可以使用错误检测功能,检测和查明错误发生的原因和性质,然后调用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 intios::good(); // 操作正常时,返回非0 当输入一个浮点数,会自动进行类型转换。只有键入字符或字符串时,才会产生输入错误,但由于cin有缓冲区,输入的字符或字符串会暂时保存到它的缓冲区中,因此为了能继续提取用户的输入,必须先将缓冲区清空。。
3.3输入输出流库 3.3.4使用输入输出成员函数 • 输入操作的成员函数 数据的输入/输出可以分为三大类:字符类、字符串和数据。 (1)使用get和getline函数 int get(); istream& get( char&rch ); istream& get( char*pch, int nCount, char delim = '\n' ); getline函数原型如下: istream& getline( char* pch, int nCount, char delim = '\n' ); (2)使用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指定的数组中。 • 输出操作的成员函数 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 );
3.3输入输出流库 3.3.5文件流概述 • 文件看作是由连续的字符的数据顺序组成。根据文件中数据的组织方式,可分为文本文件和二进制文件。文本文件中每一个字节用以存放一个字符的ASCII码值,二进制文件是将数据用二进制形式存放在文件中。 • 一个文件指针总是和一个文件所关联的,在文件每一次打开时,文件指针指向文件的开始,随着对文件的处理,文件指针不断地在文件中移动,并一直指向最新处理的字符(字节)位置。 • 文件处理方式,一种称为文件的顺序处理。另一种称为文件的随机处理,并指向所要处理的字符(字节)位置。按照这两种处理方式,可将文件相应地称为顺序文件和随机文件。
3.3输入输出流库 3.3.6顺序文件操作 • 文件的打开和关闭 声明一个ifstream、ofstream或fstream类对象。 使用文件流类的成员函数打开或创建一个指定的文件,使得该文件与声明的文件流对象联系起来,这样对流对象的操作也就是对文件的操作。 ifstream、ofstream或fstream类构造函数中总有一种原型和它的成员函数open功能相同。它们的函数原型如下: 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 );
3.3输入输出流库 • 文件的读写 [例Ex_File] 将文件内容保存在另一文件中,并将内容显示在屏幕上。 #include <iostream.h> #include <fstream.h> 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(); return; } char ch; while (!file1.eof()) { file1.read(&ch, 1); cout<<ch; file2.write(&ch, 1); } file2.close(); // 不要忘记文件使用结束后要及时关闭 file1.close();}
3.3输入输出流库 3.3.7随机文件操作 随机文件可以在文件中来回移动文件指针,从而可以实现非顺序读写文件数据的功能,达到快速检索、修改和删除文件数据的效果。 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从文件流的尾部开始
3.4模板 3.4.1函数重载机制的不足 函数重载方便用户对函数名的记忆,完善了同一个函数的代码功能。允许多个同名的函数存在,同名的各个函数的形参必须有区别:形参的个数不同,或者形参的个数相同,但形参类型有所不同。 如果需要处理所有标准数据类型的参数,则还需要更多的代码进行函数重载。可以看出函数重载有时也显得比较烦琐,解决这个问题就是使用函数模板。 3.4.2函数模板 格式: template <数据类型声明> 函数体 template是一个声明模板的关键字,数据类型声明是使用class关键字来定义类型名称,而函数体与传统的函数定义相似。
3.4模板 [例Ex_FunTemplate] 使用函数模板。 #include <iostream.h> template <class T> // 定义第一个函数模板 T sum(T x, T y, T z) { return x+y+z; } template <class T, class T1> // 定义第二个函数模板 T sum(T x, T1 y) { return x+y; } void main() { cout<<sum(2,'a')<<endl; // 结果为99 cout<<sum('a',2)<<endl; // 结果为'c' cout<<sum(2,5,7)<<endl; // 结果为14 cout<<sum(1.2,5.0,7.5)<<endl; // 结果为13.7 } 运行结果如下: 99 c 14 13.7
3.4模板 3.4.3类模板 template <数据类型声明> class 类名 { ... // 成员的定义}; 注意:大括号后的分号不能省略。 3.4.4标准模板库简介 标准模板库STL(Standard Template Library)是通过STL中的相应运算法则在应用程序的迭代子(iterator)、容器以及其他定义的序列中建立一种统一的标准。从根本上来说,STL是一个基于模板的群体类库,它包含群体类(链表或列表、向量、栈、队列、集合、映象)、算法(排序、查找)以及迭代子。 STL中的群体类是基于模板的,包含线性群体类,也包含非线性群体类。其主要群体类有: deque (双端队列) list (链表、列表) map (映象) multimap(多重映象) multiset (多重集合) set (集合) vector(向量)
3.4模板 • STL的迭代子有顺序和直接访问,顺序迭代子使用++、--等进行指针移动,只能顺序访问群体类中的对象,而直接访问迭代子是通过iterator_traits来直接访问群体类中的某个特定对象。iterator_traits用来指定要访问的属性类型,它可以有: iterator_category (目录类型) value_type (数值类型) difference_type (距离类型,表示两个指针的地址之间的距离) pointer (指针类型) reference (引用类型) • STL的算法是用函数模板实现的,可以实现对不同类型对象的通用操作。算法与STL群体类之间是通过迭代子iterator来进行沟通的。STL的主要算法有: 排序(sort、merge) 查找(find、search) 比较(equal) 集合(includes、set_union、set_difference) 计算(accumulate、partial_sum) 统计(max、min) 管理(swap、fill、replace、copy、unique、rotate、reverse) 堆操作(make_heap、push_heap、pop_heap、sort_heap)