第九章-----类
这里写目录标题
1、类定义
-
类定义包含两部分:类头(由关键字class及后面的类名构成)、类体(由一对花括号包围起来);类定义后面必须接一个分号或一列声明。
class Screen{/*.......*/}; class Screen{/*........*/}myScreen,yourScreen;
-
类体定义了类成员表,包括数据成员和成员函数。
-
每个类定义引入一个不同的类类型;即使来年各个类类型拥有完全相同的成员表。
class First{ int memi; double memd; }; class Second{ int memi; double memd; }; class First obj1; Second obj2=obj1;//错误,obj1和obj2类型不同
-
类体定义了一个域,在类体中的类成员声明把这些成员名字引入到它们的类的域中;如果两个类有同名的成员也不会出错。
-
如何引用类类型呢?
- 指定关键字class,后面紧跟类名。上例中obj1的声明就是这种方式
- 只指定类名。上例中obj2的声明就是这种方式
-
数据成员
- 类数据成员的声明方式同变量声明相同;
- 类数据成员可以是任意类型;
- 除静态(static)数据成员外,数据成员不能再类体中被显式的初始化。
#include <string> #include <vector> class Screen{ string _screen; string::size_type _cursor;//string::size_type:用来存放string的索引值 short _height,_width; vector<int> ivec; };
-
成员函数
-
类成员函数被声明在类体中,也可以被定义在类体中。
class Screen{ public: void home() { _cursor=0; } void move(int,int); char get() { return _Screen[_cursor]; } bool checkRange(int,int); //... };
-
成员函数名在类域之外是不可见的,我们可以通过“点(.)或箭头( -> )成员访问操作符"引用成员函数;
-
类成员函数拥有访问该类的公有和私有成员的特权,而普通函数只能访问类的公有成员;一般而言,一个类的成员函数对另一个类的成员没有访问特权。
-
成员函数可以是重载函数,但一个成员函数只能重载自己类的其他成员函数。
-
-
成员访问:关键字public、private、protected被称为访问限定符。
- 公有成员(public member):在程序的任何地方都可以被访问;实行信息隐藏的类将其public成员限制在成员函数上,这种函数定义了可以被一般程序用来操纵该类类型对象的操作。
- 私有成员(private member):只能被成员函数和类的友元访问;实行信息隐藏的类把其数据成员声明为private。
- 被保护成员(protected member):对派生类就像public成员一样,对其他程序则表现的想private。
//例 class Screen{ public: void home(){ _cursor = 0; } char get(){ return _screen[_cursor]; } char get(int, int); void move( int, int ); //.... private: string _screen; string::size_type _cursor; short _height,width; };
注:一个类可以包含多个public、private、protected区;如果没有指定访问限定符,则缺省情况下,在类体的开始左括号后面的区是private区。
-
友元:允许一个类授权其它的函数访问它的非公有成员
-
友元声明以关键字friend开头,只能出现在类的声明中;且不受类体中被声明的public、private、protected区的影响。
-
我们把所有友元声明组织起来放在类头之后;
class Screen{ friend istream& operator>>(istream&,Screen&); friend ostream& operator<<(ostream&,const Screen&); public: //....类的其他部分 };
-
-
一个类不能有自身类型的数据成员,因为在该类体结束前,该类尚未定义;但是一个类可以用指向自身类型的指针或引用作为数据成员。
2、类对象
-
类的定义(如Screen)不会引起存储区的分配,只有当定义一个类的对象时(如Screen myScreen),系统才会分配存储区。
-
一个对象可以被同一类类型的另一个对象初始化或赋值。
class Screen{ public: //.... private: string _screen; string::size_type _cursor; short _height,width; };//类的定义不会分配内存 Screen myScreen;//定义一个Screen类的对象,此时分配内存 Screen bufScreen = myScreen;//相当于进行了以下操作 //bufScreen._height = myScreen._height; //bufScreen._width = myScreen._width; //bufScreen._cursor = mySyin'yongcreen._cursor; //bufScreen._screen = myScreen._screen;
-
点成员访问操作符(“.”)用于类对象或引用的成员访问,箭头访问操作符(->)用于类对象的指针的成员访问。
-
如果我们想通过一个非成员函数访问私有数据成员,我们不能直接引用其数据成员,只能通过公有成员函数来引用。
//例 class Screen{ public: int height(){ return _height; } int width(){ return _width; } //... private: string _screen; string::size_type _cursor; short _height,width; }; bool isEqual( Screen& s1, Screen* s2 ) {//如果不相等,返回false,否则返回true if(s1.height() != s2->height()) || s1.width() != s2->width() ) return false; return true; }
3、类成员函数
-
在类定义中定义的函数被自动作为inline函数处理。
-
两行及以上的成员函数最好被定义在类体之外;但必须先在类体中声明该函数,且该类体必须出现在该函数定义之前;且该函数在定义时,必须在函数名前加类名限定修饰。
//例 #include <iostream> class Screen{ public: bool checkRange(int,int);//先声明该函数 //.... }; bool Screen::checkRange(int row,int col)//定义该函数时要在函数名前加类名限定修饰 { if(row < 1 || row > _height || col < 1 || col > _width)//成员函数可直接引用私有数据成员 { cerr<< "Screen coordinates("<<row<<", "<<col<<")out of bounds.\n"; return false; } return true; }
-
成员函数可以引用任何一个类成员,无论该成员是私有的还是公有的;也可以直接访问它所属类的成员,无需使用点或箭头成员访问操作符。
-
构造函数 即初始化成员函数,用来初始化类对象;构造函数的名字必须与类名相同。
//例 class Screen{ public: Screen(int hi = 8,int wid = 40,char bkground = '#');//缺省构造函数声明,设定缺省值 }; //构造函数的定义 Screen::Screen(int hi,int wid,char bk): _height( hi ),//用hi初始化_height _width( wid ),//用wid初始化_width _cursor( 0 ),//初始化_cursor为0 _screen( hi*wid,bk)//_screen的大小为hi*wid,所有位置用bk的字符值初始化 { //... } //定义的Screen对象 Screen s1;//Screen(8,40,‘#’) Screen *ps = new Screen(20);//20覆盖了缺省值hi,Screen(20,40,'#')
-
const和volatile成员函数
-
只有被声明为const的成员函数才能被一个const类对象调用;关键字const被放在成员函数的参数表和函数体之间;对于类体之外定义的const成员函数,我们必须在它的定义和声明中同时指定关键字const。
class Screen{ public: char get() const { return _screen[_cursor]; } bool isEqual(char ch) const;//声明 char get(int x,int y); char get(int x,int y) const; char poll() volatile;//将poll函数声明为volatile //... }; char Screen::poll() volatile //定义poll函数 { //... } bool Screen::isEqual(char ch) const//类体之外的const成员函数定义 { return ch==_screen[_cursor]; } int main() { const Screen s1; Screen s; char ch = s1.get(0,0);//调用const成员函数 ch = s.get(0,0);//调用非const成员 }
-
一般来说,任何一个类如果希望广泛使用,就应该把那些不修改类数据成员的成员函数声明为const成员函数。
-
const成员函数可以被相同参数表的非const成员函数重载,此时,类对象的常量性决定了调用哪个函数。如上例程序
-
当构造函数执行结束、类对象已经被初始化时,类对象的常量性就被建立起来了;析构函数一被调用,常量性就消失;所以一个const类对象“从构造完成时刻到析构开始时刻”这段时间被认为是const。
-
若一个类对象的值可能被修改的方式是编译器无法控制和检测的,则可以把它声明为volatile;对于一个volatile类对象,只有volatile成员函数、构造函数、析构函数可以被调用。
-
-
mutable数据成员
-
被声明为const的成员函数不能修改任何一个数据成员;除非该数据成员被声明为mutable(易变的);
-
数据成员声明为mutable后,任何const成员函数都可以修改该数据成员。
-
声明方式:将关键字mutable放在类成员表中的数据成员声明之前。
class Screen{ public: //...成员函数 private: string _screen; mutable string::size_type _cursor;//mutable成员 short _height,_width; };
-
4、隐含的this指针
- 每个类成员函数都含有一个指向被调用对象的指针,这个指针被称为this。
- 在非const成员函数中,它的类型是指向"该类类型"的指针;在const成员函数中,是指向"const类类型"的指针;在volatile成员函数中,是指向"volatile类类型"的指针。
5、静态类成员
静态数据成员:
-
在类体中的数据成员声明前面加上关键字“static”,就使该数据成员成为静态的。
-
对于非静态数据成员,每个类对象都有自己的拷贝;而静态数据成员只有一份,由该类类型的所有对象共享访问。
-
一般的,静态数据成员在该类定义之外被初始化;在初始化时,静态数据成员的名字必须被其类名限定修饰。
-
与全局对象一样,对于静态数据成员,在程序中也只能提供一个定义(初始化)。
-
"整型的const静态数据成员"可以在类体中用以常量值初始化,但该成员仍然必须要被声明在类定义之外;
class Account{ //... private: static const int nameSize = 16 ; static const char name[nameSize] ; }; const int Account::nameSize;//必需的成员声明 const char Account::name[nameSize] = "Saving Account";//类体外初始化静态数据成员
-
静态数据成员的类型可以是其所属类,而非static数据成员只能被声明为该类的对象的指针或引用。
-
静态数据成员可以被作为类成员函数的缺省实参,而非static成员不能。
class Bar{ public: int abc(int = var);//缺省实参 //.... private: static int var; static Bar mem1;//ok Bar *mem2;//ok,该类的对象的指针 Bar mem3;//错误 };
静态成员函数:
- 静态成员函数的声明:在类体中的函数声明前加上关键字“static”,并且不能声明为const或volatile;出现在类体外的函数定义不能指定关键字static。
- 静态成员函数没有this指针。
- 若某个成员函数访问了非静态数据成员,则该成员函数不能声明为static,否则会编译错误。
6、指向类成员的指针
-
函数指针不能被赋值为成员函数的地址,即使返回类型和参数表完全匹配。
-
指向成员函数的指针必须与向其赋值的函数类型匹配,三个方面都要匹配:参数的类型和个数、返回类型、所属的类类型。
class Screen{ public: int height() { return _height; } //... private: int _height; //... }; //定义一个指向height()的指针 int (Screen::*pfi)()=&Screen::height;//pfi为指向成员函数的指针 int Screen::*pt = &Screen::_height;
-
指向数据成员的指针定义,需要考虑 成员类型、所属类类型。
-
通过成员函数的指针调用成员函数:
int (Screen::*pfi)() = &Screen::height; Screen& (Screen::*pfs)(const Screen&) = &Screen::copy; Screen myScreen,*bufScreen; //直接调用成员函数 if(myScreen.height() == bufScreen->height()) bufScreen->copy(myScreen); //通过成员指针调用成员函数 if( (myScreen.*pfi)() == (bufScreen->*pfi)() ) (bufScreen->*pfs)(myScreen); /*注意:(myScreen.*pfi)()和(bufScreen->*pfi)()中,第一对括号必须要,不能写成myScreen.*pfi();否则会被解释为myScreen.*(pfi())*/
-
静态类成员的指针:
class Account{ public: static void reiseInterest(double incr); static double interest(){ return _interestRate; } double amount(){ return _amount; } private: static double _interestRate; double _amount; string _owner; }; inline void Account::raiseInterest(double incr) { _interestRate += incr; } //&_interstRate的类型是double*,而不是double Account::* ; double *pd = &Account::_interestRate;
7、联合:一种节省空间的类
-
定义:一个联合中的数据成员在内存区中的存储是互相重叠的;每个数据成员都在相同的内存地址开始;分配给联合的存储区数量是“最大的数据成员所占的内存数”;同一时刻只有一个成员可以被赋值。
union TokenValue{ char _cval; int _ival; char *_sval; double _dval; };
-
union的成员可以被声明为公有、私有或保护的;缺省情况下,union的成员都是公有成员。
-
union不能有静态数据成员或是引用成员;如果一个类类型定义了构造函数、析构函数或拷贝赋值操作符,则它不能成为union的成员类型。
-
在定义union时,union的名字是可选的;若union没有名字,且后面也没有跟着对象定义,则称为匿名union。
8、位域:一种节省空间的成员
-
定义:一种特殊的类数据成员,可以被声明用来存放特定数目的位;位域必须是有序数据类型;可以有符号也可以无符号。
-
定义方式:位域标识符后面有一个冒号,冒号后面是一个常量表达式来指定位数。也可用bitset进行定义。
class File{ public: unsigned int mode:2; unsigned int modified:1; unsigned int prot_owner:3; //... };
-
对于位域的访问方式与其他类数据成员相同。
-
取址操作符(&)不能应用在位域上,所以没有能指向类的位域的指针;位域也不能是类的静态成员。
第十章–类的初始化、赋值和析构
1、类的初始化
-
通过缺省构造函数初始化
-
显式初始化表:根据数据成员被声明的顺序,这些值按位置解析。
缺点:只能被应用在所有数据成员都是公有的类的对象上(即显示初始化表不支持使用数据封装和抽象数据类型)。
class Data{ public: int ival; char *ptr; }; int maix() { Data local1={0,0};//local1.ival = 0,local1.ptr = 0 Data local2 = {1024,"Anna Livia"};//local2.ival = 1024,local2.ptr = "Anna Livia" }
2、类的构造函数
-
前言
-
构造函数与类同名;一个类可以声明多少个构造函数没有限制,只要每个构造函数的参数表是唯一的即可。
-
在类对象首次被使用前,构造函数将被应用在该对象上。
-
成员初始化表:由逗号分开的成员名及初值的列表。
格式:该初始化表被放在参数表和构造函数体之间,由冒号开始。
class Account{ public: Account() const;//错误 Account() volatile;//错误 Account(const char* name,double opening_bal):_balance(opening_bal)//构造函数,初始化 { //... } //... private: char *_name; unsigned int _acct_nmbr; double _balance; };
-
构造函数不能用const或者volatile关键字来声明。
-
一个const类对象在“从其构造函数完成到析构函数开始”这段时间内才被认为是const的,对volatile对象也一样。
-
关键字explicit修饰符通知编译器不要提供隐式转换;且只能应用在构造函数上。
class Account{ public: explicit Account(const char*,double = 0.0); //... };
-
-
缺省构造函数:指不需要用户指定实参就能被调用的构造函数。
//以下每个都是缺省构造函数 Account::Account(){/*...*/} iStack::iStack(int size = 0){/*...*/} Complex::Complex(double re = 0.0,double im = 0.0){/*...*/}
-
拷贝构造函数
-
用一个类对象初始化该类的另一个对象被称为缺省按成员初始化;一个类对象想该类的另一个对象作拷贝是通过依次拷贝每个非静态数据成员来实现的。
-
拷贝构造函数有一个指向类对象的引用作为形式参数(传统上声明为const)
class Account{ public: Account(const Account &rhs):_balance(rhs._balance)//拷贝构造函数初始化 { //... } }; Account acct2(acct1);//调用拷贝构造函数
-
3、类的析构函数
-
前言
-
析构函数:为生命期即将结束的类对象返还相关的资源或者自动释放资源。
-
析构函数是一个特殊的由用户定义的成员函数,当该类的对象离开了它的域(每一个函数的退出点),或者delete表达式应用到一个该类的对象的指针上时,析构函数会自动被调用。
-
析构函数的名字是在类名前加上波浪线(~),它不返回任何值也没有任何参数;因为不能指定任何参数,所以它不能被重载。
class Account{ public: Account(); explicit Account(const char*,double = 0.0); Account(const Account&);//拷贝构造函数 ~Account();//析构函数声明 private: char *_name; unsigned int _acct_nmbr; double _balance; }; inline Account::~Account()//析构函数定义 { delete [] _name;//释放_name指向的内存空间,而另外两个数据成员是按值传递的,所以无需析构 return _acct_nmbr(_acct_nmbr); }
-
一般的,如果一个类的数据成员是按值传递的,则无需析构函数。
-
当类对象的指针或引用离开域时(被引用的对象还没有结束生命期),析构函数不会被调用。
-
4、类对象数组和vector
-
类对象数组
-
定义方式与内置类型数组的定义方式相同。
-
初始化方式:
Account table[]={"Piglet","Eeyloe","Tigger"}; /*定义了三个元素的数组,三个元素依次用构造函数初始化 Account("Piglet",0.0); Account("Eeyloe",0.0); Account("Tigger",0.0);*/ Account table1[]={ Account("Piglet",1000.0), Account("Eeyloe",999.0), Account("Tigger",999.9) };//用多个实参初始化
-
-
类对象vector
- 定义方式与内置类型vector的定义方式相同。
- 初始化通过拷贝构造函数进行。
5、成员初始化表
-
const和引用数据成员必须是在成员初始化表中被初始化,否则会产生编译错误。
class ConstRef{ public: ConstRef(int ii); private: int i; const int ci; int &ri; }; ConstRef::ConstRef(int ii) :ci(ii),ri(i)//const和引用数据成员必须在成员初始化表中被初始化 { i=ii;//第一个被赋值,因为i在类中第一个被声明。 }
-
初始化的顺序不是由名字在初始化表中的顺序决定,而是由成员在类中的声明顺序决定。
第十一章–重载操作符和 用户定义的转换
1、操作符重载
-
重载的操作符在类体中被声明,声明方式通普通成员函数一样,不过它的名字包含关键字operator,以及紧随其后的一个预定义操作符(该操作符必须来自C++预定义操作符的一个子集)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CrrKI8zA-1589382331453)(E:\重邮硕士\学习笔记\C++\预定义操作符表.jpg)]
-
类成员与非成员
-
如果一个重载操作符是类成员,那么只有当跟它一起被使用的左操作数是该类的对象时,它才会被调用;如果该操作符的左操作数必须是其他的类型,那么重载操作符必须是名字空间成员(全局重载操作符)。
-
C++要求,赋值(=)、下标([ ])、调用(())和成员访问箭头(->)操作符必须被定义为类成员操作符;任何把这些操作符定义为名字空间成员的定义都会被标记为编译时刻错误。
class Text{ public: Text(const char * = 0); Text(const Text &); bool operator==(const char*) const; bool operator==(const string &) const; bool operator==(const Text &) const; }; string flower; if(Text("tulip")==flower)//调用Text::operator==();
-
-
重载操作符的名字
- 不能声明一个没有出现在上表中的重载操作符。
- 只能为类类型或枚举类型的操作数定义重载操作符。
- 操作符预定义的操作数个数必须被保留(例如一元的操作符不能被定义为二元操作符)。
2、友元
-
友元声明以关键字friend开始,它只能出现在类定义中。
-
只有当一个类的定义已经被看到时,它的成员函数才能被声明为另一个类的友元;如果Screen类必须把Window类的成员函数声明为友元,而Window类必须把Screen类的成员函数声明为友元,则可以把整个Window类声明为Screen类的友元。
class Window;//声明 class Screen{ friend class Window; //... };
3、操作符=
第十二章------类模板
1、类模板定义
template <class Type>
class Queen{
public:
Queen();//构造函数
~Queen();//析构函数
Type& remove();
void add(const Type &);
bool is_empty();
bool is_full();
private:
//...
};
//下面依次生成int、复数和string类型的Queen类
Queen<int> qi;
Queen<complex<double>> qc;
Queen<string> qs;
-
类模板的定义和声明都以关键字template开头,关键字后是一个用逗号分隔的模板参数表,用尖括号(<>)括起来;模板的类型参数由关键字“class"或关键字“typename”及其后的标识符构成。
-
模板的非类型参数由一个普通参数声明构成。
template<class Type,int size> class Buffer;
-
模板类型的前向声明和类模板定义中,模板参数的名字可以不同。
//所有的三个QueenItem声明都引用同一个类模板 template <class T> class QueenItem; template <class U> class QueenItem; //模板的真真的定义 template <class Type> class Queen{ //... };
-
类模板名的每次出现都是以下形式:类名<类型标识符>。
2、类模板实例化
- 从通用的类模板定义中生成类的过程被称为模板实例化 。
- 当一个类模板类型的对象被定义时,类模板被实例化。(因为定义对象时,需要计算出该对象所需的内存空间)
- 当一个类模板类型的指针或引用被定义时,只有当检查这个指针或引用所指的对象时,类模板才会被实例化。
- 模板成员函数的类型被调用该模板成员函数的对象的类型定义。
3、非类型参数的模板实参
-
类模板参数(即template< >中的参数)可以是一个非类型模板参数;
template <int hi,int wid>//非类型模板参数 class Screen{//...};
-
定义给非类型模板参数的实参表达式必须是一个常量表达式,即它必须在编译时被计算出来。
Screen <8,24> ancientScreen; template <int *ptr> class Bufptr{//...}; Bufptr<new int[24]> bp;//错误,new表达式调用结果的指针值只有在运行时刻才能知道
- 名字空间域中任何对象的地址(即便该对象不是const类型)是一个常量表达式,但局部对象的地址不是常量表达式。
-
在模板实参的类型和非类型模板实参的类型之间允许进行一些转换,能被允许的转换集是“函数是参上被允许的转换”的子集。
4、类模板中的友元声明
有三种友元声明可以出现在类模板中:
-
非模板友元类或友元函数。
class Foo{ void bar(); }; template <class T> class QueueItem{ friend class foobar; friend void foo(); friend void Foo::bar(); }//...
-
绑定的友元类模板或函数模板。
template <class Type> class foobar{//...}; template <class T> void foo(QueueItem<T>); template<class Type> class Queue{ void bar(); }; template <class Type> class QueueItem{ friend class foobar<Type>; friend void foo<T>(QueueItem<T>);//foo<Type> 可用来指定该友元声明所引用的是函数模板foo()的实例;如果少了“<Type>”,则友元会被声明为非模板函数。 friend void Queue<Type>::bar();//int型的Queue实例只是int型的QueueItem实例的友元 };
- 在一个模板可以被用在一个类模板的友元声明中之前,它的声明或定义必须先被给出;
-
非绑定友元类模板或函数模板。
template <class Type> class QueueItem{ template <class T> friend class foobar; template <class T> friend void foo (QueueItem<T>); template <class T> friend void Queue<T>::bar(); };
5、类模板的静态数据成员
template <class Type>
class QueueItem{
//...
private:
static QueueItem *free_list;
static const unsigned QueueItem_chunk;
};
template <class T>
QueueItem<T> *QueueItem<T>::free_list=0;
template <class T>
const unsigned QueueItem<T>::QueueItem_chunk=24;
-
静态数据成员的模板定义必须出现在类模板的定义之外,以关键字“template”开头,后面是类模板参数表“”,静态数据成员的名字前需要加上前缀“QueueItem::”,表明该成员属于类模板"QueueItem"。
-
静态数据成员的模板定义不会引起任何内存被分配,只有对静态数据成员的某个特定的实例才会分配内存。
-
一个静态数据成员的实例在被引用的时候,总要通过一个特定的类模板实例。
int ival0=QueueItem::QueueItem_chunk;//错误的 int ival1=QueueItem<string>::QueueItem_chunk;//ok int ival2=QueueItem<int>::QueueItem_chunk;//ok
6、类模板的嵌套类型
-
在类模板Queue的私有区中嵌入类模板QueueItem的定义,则只有类模板Queue和Queue的友元才可以访问它。
template <class T> class Queue{ //... private: class QueueItem{ public: QueueItem(Type val):item(val),next(0){//...} Type item; QueueItem *next; }; //因为QueueItem是一个嵌套类型,所以可以省略QueueItem之后的模板实参<Type> QueueItem *front,*back; };
-
当外围类模板被实例化时,它的嵌套类不会自动被实例化;只有当上下文环境确实需要嵌套类的完整类型时,嵌套类才会被实例化。比如当解引用front和back时。
-
公有的嵌套类型可以被用在类定义之外;且使用嵌套类型时可以不用使用外围类模板参数 。
template <class T> class Q{ public: enum QA{empty,full};//嵌套的枚举类型,且为公有的 QA status; }; int main() { Q<double> qd; Q<int> qi;; qd.status=Q::empty;//错误,未指明Q是哪个实例 qd.status=Q<double>::empty;//正确 int val1=Q<double>::empty; int val2=Q<nt>::empty; }
第十三章 ------类继承和子类型
1、继承层次结构
-
继承关系通过类派生表来指定;在单继承下,它的一般形式为:
class 派生类名 : access-level 基类名 {…};
其中:access-level是public、private、protected中的一个;基类必须在事先定义好。
class Query{...}; class AndQuery : public Query{...}; class OrQuery : public Query{...}; class NotQuery : public Query{...}; class NameQuery : public Query{...};
-
派生类继承了其基类的数据成员和成员函数。
-
如果基类和派生类共享相同的公有接口,则派生类被称作基类的子类型。
-
多态性:指基类的指针或引用可以指向其任意派生类的能力。
//pquery可以指向任何从Query派生的类型 void eval(const Query *pquery) { pquery->eval(); } //下面的类都是Query的派生类 int main() { AndQuery aq; NotQuery notq; OrQuery *oq=new OrQuery; NameQuery nq("Botticelli"); eval(&aq); eval(¬q); eval(oq); eval(&nq); }
-
一个对象的多态操纵行为要求"通过指针或引用来访问该对象“。
-
C++支持以下几种方式支持多态性:
-
通过一个隐式转换,从“派生类指针或引用”到“其共有的基类类型的指针或引用”;
Query *pquery = new NameQuery("Glass");
-
通过虚拟函数机制:
pquery->eval();
-
通过dynamic_cast和typeid操作符;
if(NameQuery *pnq = dynamic_cast <NameQuery*>(pquery))...
-
-
一个类的protected区域中的数据成员和成员函数,虽然对于一般程序仍然是不可访问的,但是对于派生类却是可用的。(放在基类的private区域中的项只被提供给基类自己,而不是任何一个派生类。)
-
每个派生类的公有接口都是由“通过继承得到的基类的公有成员”和“派生类自己的公有成员”构成。
2、基类成员访问
- 每个基类代表了一个由该基类的非静态数据成员组成的子对象;派生类对象由其基类子对象以及“由派生类的非静态数据成员构成的派生部分”组成 。
- 在派生类中,继承得到的基类子对象的成员可以被直接访问,就好像他们是派生类的成员一样;(继承链的深度不会限制对这些成员的访问,也不会增加访问开销)
- 基类指针只能访问在该类中被声明(或继承)的数据成员和成员函数;包括虚拟成员函数,而与它可能指向的实际对象无关。
3、基类和派生类的构造
- 派生类由一个或多个基类子对象以及派生类部分构成。
- 构造函数的调用顺序:
- 基类构造函数;如果有多个基类,则按基类在类派生表中出现的顺序,而不是他们在成员初始化表中的顺序调用。
- 成员类对象构造函数;如果有多个成员类对象,则构造函数的调用顺序是对象在类中被声明的顺序。
- 派生类构造函数。
- 析构函数:派生类的析构函数调用顺序与它的构造函数调用顺序相反。
4、基类和派生类虚拟函数
-
当一个成员函数为非虚拟的时候,通过一个类对象(指针或引用)而被调用的该成员函数,就是该类对象的静态类型中定义的成员函数;当成员函数是虚拟的时候,通过一个类对象(指针或引用)而被调用的该成员函数,是在该类对象的动态类型中被定义的成员函数。
-
要把对象声明为虚拟的,只需要指定关键字“virtual”。
class Query{ public: virtual void eval(); };
-
虚拟函数机制只在使用指针和引用时才会如预期般的起作用。
-
第一次引入虚拟函数的基类时,必须在类声明中指定“virtual”关键字,如果定义被放在类的外面,则不能在定义中再次指定关键字“virtual”。
-
一个虚拟函数只是提供了一个可被子类型改写的接口,但其本身并不能通过虚拟机之被调用,这就是纯虚拟函数。纯虚拟函数声明后面紧跟赋值0。
class Query{ public: virtual void eval() = 0; };
-
包含(或继承)一个或多个纯虚拟函数的类被编译器识别为抽象基类。
-
纯虚拟函数可以通过虚拟机制被调用,也可以被静态调用。
inline void AndQuery::eval() { BinaryQuery::print();//静态调用 };
-
虚拟函数的缺省实参不是在运行时刻决定的,而是在编译时刻根据被调用函数的对象的类型决定的。
class base{ public: virtual int foo(int ival=1024){ cout<<"base::foo()--ival:"<<ival<<endl; return ival; } }; class derived:public base{ public: virtual int foo(int ival = 2048){ cout<<"derived::foo()--ival:"<<ival<<endl; return ival; } }; derived *pd=new derived; base *pb=pd; int val = pb->foo();//调用的是base中的缺省实参,val=1024 int val1 = pd->foo();//调用的是派生类中的缺省实参,val1=2048
第十四章-----多继承和虚拟继承
1、
第十五章----C++中继承的用法
1、
第十六章----iostream库
1、前言
-
输入输出操作是由istream(输入流)和ostream(输出流)类提供的。(iostream类同时从istream和ostream派生,允许双向输入输出)
- cin:标准输入,从用户终端读取数据
- cout:标准输出,向用户终端写数据
- cerr:标准错误,导出程序错误消息的地方
-
对文件的读写(fstream头文件包含了iostream头文件)
- 所需头文件:#include
- ifstream:把一个文件绑到程序上用来输入
- ofstream:把一个文件绑到程序上用来输出
- fstream:把一个文件绑到程序上用来输入和输出
-
内存输入/输出:当流被附着在程序内存中的一个字符串上时,我们可以用iostream输入和输出操作符来对它进行读写,通过定义一下三种类型来定义一个iostream字符串对象:
-
所需头文件:#include
-
istringstream:从istream派生,从一个字符串中读取数据
-
ostringstream:从ostream派生,写入到一个字符串中
-
stringstream:从iostream派生,从字符串中读取,或者写入到字符串中
-
1、输出操作符“<<”
- 输出操作符可以接受任何内置数据类型的实参,包括const char*,以及标准库string和complex类类型。
- endl是一个ostream操作符,它把一个换行符插入到输出流中,然后刷新ostream缓冲区。
- 操作符“<<”:从左往右结合。
- 输出操作符的优先级高于条件操作符。
- 输出时应用“boolalpha”操作符,可以将bool类型输出为true或false而不是1或0;“noboolalpha”操作符可以解除该状态。
2、输入操作符“>>”
-
有两种情况会使一个istream对象被计算为false:
- 读到文件结束(此时我们已经正确的读完文件中所有的值)
- 遇到一个无效的值,比如3.14159(小数点非法)、1e-1(字符文字e非法)。
-
预定义的输入操作符可以接受任何的内置数据类型,包括C风格字符串以及标准库string和complex类类型。
-
缺省情况下,所有空白字符都被抛弃掉;如果我们希望读入空白字符,有两种方法:
-
使用istream的get( )成员函数(ostream的put( )成员函数一般与get( )配合使用)
char ch; while(cin.get(ch)) cout.put(ch);
-
使用noskipws操作符。
-
-
**setw( )**把长度等于或大于bufSize的字符串分成最大长度为:bufSize-1 的两个或多个字符串,并在每个字符串的末尾放一个空字符。
-
setw( )所需头文件:#include ;
char buf[bufSize]; while (cin>>setw(bufSize)>>buf)
-
3、其他输入/输出操作符
-
istream成员函数**get( )**一次读入一个字节;**getline( )**一次读入一块字节,或者由一个换行符作为结束,或者由某个用户定义的终止字符作为结束。
-
get( )函数有三种形式:
-
get(char &ch):从输入流中提取一个字符,包括空白字符,并将它存储在ch中;它返回被应用的istream对象。
-
get( )的第二个版本也从输入流读入一个字符;但其返回类型是int而不是char,返回文件结尾的标志(end-of-file),该标志通常用-1表示。
-
get( )的第三种形式:get (char *sink, streamsize size, char delimiter=’\n’);
其中,sink代表一个字符数组,用来存放被读取到的字符;size表示可以用istream中读入的字符的最大数目;delimiter表示如果遇到它就结束读取字符的动作。delimiter本身不会被读入,而是留在istream中,作为istream的下一个字符
返回值是被调用的istream对象。
-
-
istream的成员函数gcount( )函数返回实际被读入的字符个数;
-
istream成员函数**ignore( )**从被调用的istream对象中读入一个字符并丢弃掉,但我们也可以显式指定长度和delimiter:
- 原型:ignore( streamsize length = 1, int delim = traits::eof )
- ignore( )从istream中读入并丢弃lenth个字符,或者遇到delimiter之前包含dilimiter在内的所有字符,或者遇到文件结尾。
- 返回当前被应用的istream对象。
-
getline( )函数:
- 原型:getline( char *sink, streamsize size, char delimiter = ‘\n’);
- 返回被调用的istream对象。
-
ostream成员函数**write( )**可以输出某个长度的字符序列,包括内含的空字符。
-
原型:write (const char *sink, streamsize length );
其中,length指定要显示的字符个数
-
返回当前被调用的ostream类对象。
-
-
istream成员函数**read( )**从输入流中提取size个连续的字节,并将其放在地址从addr开始的内存中。
- 原型:read( char *addr, streamsize size );
- 返回当前被调用的istream类对象。
4、重载输出操作符“<<”
-
输出操作符是一个双目操作符,它返回一个ostream引用。其重载定义的通用框架:
ostream& operator <<(ostream& os, const ClassType &object) { //准备对象的特定逻辑 //成员的实际输出 os << //...; //返回ostream对象 return os; }
第一个实参是ostream对象的引用;第二个一般是一个特定类类型的const引用;返回类型时一个ostream引用,且它的值总是该输出操作符所应用的ostream对象。
-
所有输出操作符必须定义为非成员函数;当输出操作符要求访问非公有成员时,必须将它声明为该类的友元。
#include <iostream> class WordCount{ friend ostream& operator<<(ostream&, const WordConst&); public: WordCount(string word,int cnt = 1); //... private: string word; int occurs; }; ostream& operator<<(ostream& os,const WordCount& wd) { //输出格式:<occurs> word os << "<" << wd.occurs << ">" << wd.word; return os; }
5、重载输入操作符“>>”
#include <iostream>
#include "WordCount.h"
class WordCount{
friend ostream& operator<<(ostream&, const WordCount&);
friend istream& operator>>(istream&, const WordCount&);
};
istream& operator>>(istream& is,WordCount &wd)
{
//读入格式为:<2> string
int ch;
if((ch=is.get())!='<')//读入‘<’符号;如果不存在则设置istream为失败状态并退出
{
is.setstate(ios_base::failbit);
return is;
}
int occrus;
is >> occurs;
while(is && (ch=is.get())!='>');//读入‘>’符号
is >> wd.word;//读入字符串string
return is;
}
6、条件状态
-
为了判断流对象是否处于正常状态,我们通常测试它的真值:
if(!cin)
-
为了读入未知数目的元素,我们一般如下编写while循环:
while(cin>>word)
-
可通过以下四个成员函数监视流的状态:
- 如果一个流遇到文件结束符,则**eof( )**返回true。
- 如果该流由于某种未定义的方式被破坏了,则**bad( )**返回true
- 如果操作不成功,比如打开一个文件流对象失败或遇到一种无效的输入格式,则**fail( )**返回true
- 如果其他条件都不为true,则**good( )**返回true
-
显式的修改流对象的条件状态有两种方式:
-
使用**clear( )**成员函数,可以把条件状态复位到一个显式的值。
-
使用**setstate( )**成员函数。
//用setstate()为istream对象增加一个失败的条件 if((ch=is.get())!='<') { is.setstate(ios_base::failbit); return is; }
-
所有可用的条件值:ios_base::badbit;ios_base::eofbit;ios_base::failbit;ios_base::goodbit;
-
如果cin由于某种异常返回false,我们通过clear( )可以将它重新置于正常的状态,以便重新读取其他输入。
cin.clear();//将cin重新设为正常 cin.clear(ios_base::goodbit);//同上
注:clear的缺省值为goodbit值。
-
-
**rdstate( )**成员函数可以显式的访问iostream类对象的状态。
ios_base::iostate old_state = cin.rdstate(); cin.clear(); process_input(); cin.clear(old_state);//将cin重置为原来的状态
7、string流
-
ostringstream类向一个string插入字符,istringstream类从一个string对象读取字符,而stringstream类可以用来支持读和写两种操作。
- 所需头文件:#include ;
-
ostringstream类对象buf随输入的字符而增长,以便容纳所有的字符:
#include <string> #include <sstream> #include <fstream> string read_file_into_string() { ifstream ifile("alice.txt"); ostringstream buf; char ch; while(buf&&ifile.get(ch)) buf.put(ch); return buf.str(); }
- 成员函数**str( )**返回与ostringstream类对象相关联的string对象。
-
ostringstream对象也可以用来支持符合string的自动格式化,即由多种数据类型构成的字符串。
int main() { int ival=1024; int *pival=&ival; double dval=3.14159; double *pdval=&dval; ostringstream format_message; //把值转换成string表示 format_message<<"ival:"<<ival<<"ival's address:"<<pival<<'\n' <<"dval:"<<dval<<"dval's address:"<<pdval<<endl; string msg=format_message.str(); cout<<"size of message string:"<<msg.size()<<"message:"<<msg<<endl; //将数值字符串转换成算术值 }
-
istringstream由一个string对象构造而来,它可以读取该string对象。其用法是将数值字符串转换成算术值。
int main() { int ival=1024; int *pival=&ival; double dval=3.14159; double *pdval=&dval; ostringstream format_string; //创建一个字符串,存储每个值并用空格分隔 format_string<<ival<<" "<<pival <<" "<<dval<<endl; //提取出被存储起来的ascii值,一次存放在四个对象中 istringstream input_istring(format_string.str()); input_istring >> ival>>pival>>dval>>pdval; }
8、格式状态
- 可以使用成员函数setf( )和unsetf( ),直接设置或解除格式状态标志。
表中的*可以不理:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tiZFdeQT-1589382331455)(E:\重邮硕士\学习笔记\C++\格式操作符1.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MyjdLNlG-1589382331457)(E:\重邮硕士\学习笔记\C++\格式操作符2.jpg)]
&ifile.get(ch))
buf.put(ch);
return buf.str();
}
- 成员函数**str( )**返回与ostringstream类对象相关联的string对象。
3. ostringstream对象也可以用来支持符合string的自动格式化,即**由多种数据类型构成的字符串**。
```c++
int main()
{
int ival=1024;
int *pival=&ival;
double dval=3.14159;
double *pdval=&dval;
ostringstream format_message;
//把值转换成string表示
format_message<<"ival:"<<ival<<"ival's address:"<<pival<<'\n'
<<"dval:"<<dval<<"dval's address:"<<pdval<<endl;
string msg=format_message.str();
cout<<"size of message string:"<<msg.size()<<"message:"<<msg<<endl;
//将数值字符串转换成算术值
}
-
istringstream由一个string对象构造而来,它可以读取该string对象。其用法是将数值字符串转换成算术值。
int main() { int ival=1024; int *pival=&ival; double dval=3.14159; double *pdval=&dval; ostringstream format_string; //创建一个字符串,存储每个值并用空格分隔 format_string<<ival<<" "<<pival <<" "<<dval<<endl; //提取出被存储起来的ascii值,一次存放在四个对象中 istringstream input_istring(format_string.str()); input_istring >> ival>>pival>>dval>>pdval; }
8、格式状态
- 可以使用成员函数setf( )和unsetf( ),直接设置或解除格式状态标志。
表中的*可以不理:
[外链图片转存中…(img-tiZFdeQT-1589382331455)]
[外链图片转存中…(img-MyjdLNlG-1589382331457)]