Essential C++中文版(Chap4:基于对象的编程风格)

来源:互联网 发布:剑侠情缘网络经典版 编辑:程序博客网 时间:2024/06/05 00:09

Chapter4:基于对象的编程风格:Object-Based Programming

  虽然我们尚未撰写自己的class,但是自第一章起,我们已经广泛运用了许多classes:string、vector、提供输入及输出功能的各种iostream  classes…从本章开始,我们会设计并实现属于我们自己的classes.
    根据过去的种种使用经验,我们明白了classes的哪些相关事务呢?首先,在使用class之前,由于它并非程序语言本身内建,所以必须先让程序知道它.通常我们会含人某个头文件以完成这件事:=

#include <string>string pooh[4] =    {"winnie", "robin", "eeyore", "piglet"};

class名称被视为一个型别(type)名称,就像内建型别int、double一样.class object的初始化做法有很多种:

#include <vector>string dummy("dummy");vector<string> svec1(4);vector<string> svec2(4, dummy);vector<string> svec3(pooh, pooh+4);

  每个class都会提供一组操作函数,让我们施行于其objects身上.这些操作函数包括具名函数,如size()和empty(),以及重载运算符,如equality(相等)和assignment(赋值)运算特等等.
  通常我们并不知道class的实现内容,例如string是在每次我们要求计算其大小时才去计算呢?或是它将自己的大小存储在每个object之中昵?vector的元素究竟是存储在vector对象内呢?还是存在其它地方,再通过vector对象中的指针加以寻址呢?这些我们都不知道.
  一般而言,class由两部分组成:一组公开的(public)操作函数和运算符,以及一组私有的(private)实现细节.这些操作函数和运算符被称为class's member function(成员函数),并代表这个class的公开接口.身为classes的用户,只能取用其公开接口.这也就是我们使用string、vector的方式,例如,针对string's member function size(),我们只知其原型声明(prototype),亦即:参数列为void,返回整数值.
    class的privatc实现细目可由member functions的定义式以及与此class相关的任何数据组成.例如,假设string class object的size()每次被调用,都会重新计算其字符串长度,那么就不需要任何相关敦据来存储这份信息--size()定义式中可能利用for循环之类的走访方式,取得字符串长度.但如果string clatss object欲存储其字符串长度,就必须在每个class object中定义private data member(私有的数据成员).并在size()定义式中将该值返回.每当字符串长度有所更动时,这份data member都必须同步更新.
  class用户通常不会关心此等实现细节.身为一个用户,我们只利用其公开接口来进行编程,这种情形下,只要接口没有更动,即使实现细节重新打造,所有的应用程序代码亦不需变动.
    这一章,我们的境界将从class的使用提升至class的设计与实现,那正是C++程序员的主要工作.

4.1  如何实现一个class

  好的,该从何处着手呢?一般来说,我们会从所谓的抽象化(abstraction)开始。想想stack这个例子.stack是计算机科学中十分基础的一个抽象概念,它允许我们叠放许多数值,并以后进先出(last-in. frst-out,LIFO)的顺序取出.我们以pushing方式将新数值叠放到堆栈内,并以popping方式取出stack内最后一个被pushed的数值,用户通常还会要求其它操作行为,如询问stack的空间是否已满full),或是否为空(empty),或询问stack的元素数目(size).stack也可能提供检视(peeking)能力,观察stack内最后一个被pushed的数值,
  我们应该让stack存放哪一类型的元素呢?通用型stack应该可以存放各种型别的元素.如果把stack定义为class template,便可以达到这个目的,不过,class template是第六章讨论的课题,而现在才第四章而已,所以本章只定义stack的non-template版本,其中存放string元素.class的声明式以关键词class开始,其后接着一个class名称(可任意命名):
    class Stack;

  此句只是作为Stack class的前置声明(forward declaration),只是将class名称告诉编译器,并未提供此class的任何其它信息(如class支持的操作行为及所包含的data members等等)。前置声明使我们得以进行类指针( class pointer)的定义,或以此class作为数据型别:

//ok:以下这种写法,必须先有class的前置声明才行Stack *pt = 0;void process(const Stack);

  接下来,在定义实际的Stack class object或取用Stack的任何一个member之前,必须先定义class本身。class定义式的骨干看起来是这个样子:

class Stack{public:    // ...public interfaceprivate:    //...private的实现部分};

  class定义式由两部分组成:class的声明,以及紧接在声明之后的主体.主体部分由一对大括号括住,并以分号结尾.主体内的两个关键词publicprivate.用来标示每个区段的“members存取权限”.public members可以在程序的任何地方被取用,private members只能在member function或是class friend内被取用.稍后我会解释friend在c++语言中的意义,以下是Stack class的起始定义:

class Stack{public://任何操作函数如果执行成功,就返回true//pop和peek会将字符串内容置于elem内bool push( const string&);bool pop(string  &elem);bool peek( string &elem);bool empty();bool full();//size()定义于class本身内//其它members则仅仅只是声明int size() {return _stack.size();}private:vector<string> _stack;);

  这一份Stack提供了本节一开始找出的6个操作行为。其元素被存储于名为_stack的string vector内。我的写码习惯是在data member之前加上下划线。以下说明如何定义并使用Stack classobject:

void fill_stack(Stack &stack, istream &is = cin){string str;while(is >> str && != stack.full())stack.push(str);cout << "Read in" << stack.size() << "elements\n";}

  所有member functions都必须在class主体内进行声明.至于是否要同时进行定义,可自由决定,如果要在class主体内定义,这个member functions会自动地被视为inline函数,例如size()即是Stack的一个inline member.要在class主体之外定义member functions.必须使用特殊的语法,目的在于分辨该函数究竟属于哪一个class.如果希望该函数为inline,应该在最前面指定关键词inline:

inline boolStack::empty() {return _stack.empty()};bool Stack::pop(string &elem){if (empty())return false;elem = _stack.back();_stack.pop_back();return true;}

  上述的Stack::empty()   ------[前面有这种用法vactor<string>::iterator iter = svec.begin()]

告诉编译器(或程序读者)说,empty()是Stack class(而非vector或string或……)的一个member。 class名称之后的两个冒号(Stack::)是所谓的class scope resolution(类范围决议)运算符
  对inline函数而言,定义于class主体内或主体外,并没有什么分别,然而就像non-member inlincfunction -样,它也应该被置于头文件中.class定义式及其inlinc member fuction通常都会被放在与class同名的头文件中.例如Stack class的定义和其empty函数定义都应该置于Stack.h头文件中.此即用户想要使用Stack时应该含入的文件.non-inline member function应该在程序代码文件中定义,该文件通常和class同名,其后接着扩展名.C, .cc, .cpp或.cxx。以Visual C++为例,它使用扩展名cpp.

  以下便是Stack member fanctions的定义。full()会将目前的元素数目拿来和底层vcctor的max_size()数值(此即vector的容量)做比较。push()则是在_stack未满的前提下将元素插入.

inline bool Stack::full(){return _stack.size() == _stack.max_size();}bool Stack::peek(string &elem){if (empty())return false;elem = _stack.back();return true;}bool Stack::push(const string &elem){if (full())return false;_stack.push_back(elem);return true;}

  虽然我们已经提供用户一整组操作行为,但还未能完成Stack的完整定义.下一节我们会看到如何提供极为特殊的所谓初始化函数和终止函数,它们分别被称为construdor(构造函数)和destructar(析构函数)。

4.2  什么是Constructors(构造函数)和Destructors(析构函数)

  每个数列都很适合设计为class. 一个数列的class object可以表现出该数列在某范围内的元素。默认情形下,起始位置为1。例如:
  Fibonacci  fib1(7, 3); 便定义出拥有7个元素的Fibonacci object,起始位置为3;
  Pell  pel(10); 则定义出具有10个元素的Pell object1起始位置为默认值1;
  Fibonacci  fib2(fib1); 定义一个Fibonacci object fib2,并以fib1作为fib2的初值。换句话说,fib2是fibl的副本。
  每个class都必须记住它自己的长度--数列的元素数目--和起始位置.但起始位置不得为零值或负值。所以,我以整数存储长度及起始位置,此刻我再定义第三个member _next,用来记录迭代(iterate)操作的下一个元素:

class Triangular{public:    // ...private:    int _length;    int _beg_pos;    int _next;};

  每个Triangular class object内都拥有这些data members.当我写下:
  Triangular tri( 8, 3 );
时.tri内台一份_length(初值8),一份_beg_pos(初值3),一份next(初值2,因为vector的第三个元素的索引值为2).注意,它没有包含实际上用来存储triangular数列元素的vector.为什么?因为我们不希望在每个class objects中都复制一份这个vector:所有class objects共享一份vector便已足够。我们会在4.5节看到如何达成这个目的。
  这些data members如何被初始化呢?嗅,魔法不会自动产生;编译器不会自动为我们处理。如果我们提供一个或多个特别的初始化函数,编译器就会在每次class object被定义出来时,调用适当的函数加以处理.这些特别的初始化函数称为constructors(构造函数)。
  constructors的函数名称必须与class名称相同。语法规定,constructor不应指定返回型别,亦不需返回任何值.它可以被重载(overloaded)。例如,Triangular class可能有三个constructors:

class Triangular{public:    //一组重载的constructors    Triangular();//default costructors    Triangular(int len);    Triangular(int len, int beg_pos);    // ...};

  一旦class object被定义出来,编译器便自动根据获得的参数,挑选出应被调用的constructor。
例如:Triangular t; 会对t施行default constructor(译注:无需任何参数的constructor)。而:

Triangular t2(10, 3); 会调用带有两个参数的constructor。括号内的值舍被视为传给constructor的参数。
Triangular t3=8;  //译注:请注意,这究竟是调用constructor抑或
                        //           assignment operaor呢?答案是constructor
会调用带有单一参数的conslructor.出乎意料的是,以下程序代码无法成功定义一个Triangular object:
Triangular  t5();  //实际结果出乎意料之外
  此行将t5定义为一个函数,其参数表是空的,返回Triangular object.很显然这是个奇怪的解释.为什么它会被这样解释呢?因为C++必须兼容于C。对C而言,t5之后带有小括号,会使t5被视为函数.正确(符合我们意图)的t5声明方式,应该和先前的t样:
Triangular t5;  // ok   

  最简单的constructor是所谓的default constructoro它不需要任何引数(arguments)。这意谓着两种情况,第一,它不接受任何参数:

Triangular::Triangular() {   //default   constructor    _length=1;    _beg_pos=1;    _next=0;}

第二,它为每个参数提供了默认值:

class Triangular{public:    //也是defualt constructor    Triangular(int len = 1, int bp = 1);    //...};
Triangular::Triangular(int len, int bp){    _length = len > 0 ? len : 1;    _beg_pos = bp > 0 ? bp : 1;    _next =  _beg_pos - 1;}

  由于我们为两个整数提供了默认值,所以default constructor同时支持原本的3个constructors:

Triangular tri1;  //Triangular:: Triangular (1, 1);Triangular tri2(12);  //Triangular:: Triangular(12, 1);  Triangular tri3(8 ,3);  //Triangular:: Triangular(8, 3);

Member Initialization List(成员初值表)

  constructor定义式的第二种初始化语法,是所谓的member initialization list(成员初始化表):

Triangular::Triangular(const Triangular &rhs)    : _length  ( rhs._length  ) ,      _beg_pos (  rhs ._beg_pos  ) ,  _next (  rhs ._beg_pos-1 ){}//空的

  Mamber initialization list紧接在参数表最后的冒号后面,是个以逗号为分隔的列表。其中,欲赋值给member的数值被置于member名称后面的小括号中;这使它们看起来像是在调用constructor。
  就本例而言(译注:本例没有member abjects),第一种和第二种constructor定义方式是等价的,并没有谁优于谁的问题.
  Member initialization Iist主要用来将参数传给member class object的constructor。假设我们重新定义Triangular,令它包含一个string member:

class Triangular{public:         // ...private :    string _name;    int _next , _length, _beg_pos;};  

为了将_name的初值传给string constructor,必须以member initialization list完成,像这样:

Triangular::Triangular(int len, int bp)    : _name("Triangular"){    _length = len > 0 ? len : 1;    _beg_pos = bp > 0 ? bp : 1;    _next =  _beg_pos - 1;}

  和constructor对立的是destructor。所谓destructor乃是用户自行定义的一个class member。一旦某个class提供有destructor,当其objects结束生命时,便会自动调用destructor处理善后.destructor主要用来释放在constructor中或对象生命周期中配置的资源.
  destructor的名称有严格规定:class名称再加上‘~’前导符号.它绝对不会有返回值,也没有任何参数。正由于其参数表是空的,所以也绝不可能被重载(overloaded)。
  考虑以下的Matrix class.其constructor使用new表达式从heap中配置double救组所需的空间.其destructor则负责释放这些内存:

class Matrix{public:Matrix(int row, int col):_row(row), _col(col){    //constructor进行资源的配置    _pmat = new double[row * col];}~Matrix(){    //destructor进行资源的释放    delete [] _pmat;}private:int _row, _col;double *_pmat;};

  于是,我们通过Matrix本身的constructor和destructor,完成了heap内存的自动管理例如下面这段叙述:

{    Matrix mat(4, 4);    //此处施行constructor        //...    //此处施行destructor}

  编译器会在mat被定义出来的下一刻,暗暗施行Matrix  constructor。于是,_pmat被初始化为一个指针,指向程序自由空间(free store)中的一块内存,代表一个具有16个double元素的数组.语句区段结束之前,编译器又会暗暗施行Matrix destructor,于是释放_pmat所寻址的那块具有16个doulole元素的数组。Matrix的用户不需要知道内存管理细节。这种写法有点类似标准程序库的容器(containers)设计。
  destructor并非绝对必要.以我们的Triangular为例,3个dala members皆以储值(by value)方式来存放,这些members在class object被定义之后便已存在,并在class object结束其生命时被释还。因此,Triangular destructor没什么事好做。我们没有义务非得提供destrudor不可。事实上,c++编程最难的部分之一,便是了解何时需要定义destructor而何时不需要。

Memberwise Initialization(成员逐一初始化)
  默认情形之下,当我们以某个class object作为另一个object的初值,例如:

Triangular tri1(8);Triangular tri2 = tri1;

时,class data members会被依次复制.本例中的_length、_beg_pos,_next都会依次地从tri1复制到tri2。此即所谓的default memberwise  initialization(默认的成员逐一初始化操作).
  在Triangular例中,default memberwise initialization会正确复制所有的data members,我们不必特意做其它事.但对先前介绍的Matrix class而言,default memberwise initialization并不适当。看看下面这段程序代码:

{Matrix mat(4, 4);//此处.constructor发生作用{Matrix mat2 = mat;//此处,进行default memberwise initialization//…在这里使用mat2//此处,mat2的destructor发生作用}      //在这里使用mat  //此处,mat的destructor发生作用}

其中,default memberwise initialization会将mat2的_pmat设为mat的_pmat值:
  mat2._pmat = mat._pmat;
这会使得两个对象的_pmat都寻址到heap内的同一个数组.当Matrix destructor施行于mat2身上时,该数组空间便被释放。不幸的是,此时mat的_pmat仍旧指向那个数组,而你知道,对空间已被释放的数组进行操作,是非常严重的错误行为。
  这个问题应该如何修正呢?本例中我们必须改变这种“成员逐一初始化”的行为模式。我们可以通过"为Matrix提供另一个copy constructor“达到目的。这里的“我们”指的是Matrix设计者;至于Matrix的用户,应该完全不知道这些问题的存在。
  如果Matrix设计者提供了一个copy constructor,它就可以改变”成员逐一初始化”的默认行为模式。客户端虽然需要重新编译,但至少其程序代码不必有任何更动.
  这个copy constructor看起来会像什么样子呢?其唯一参数是一个const reference,指向(代表)一个Matrix object:

Matrix::MatriX(const Matrix &rhs){    //这里应该写些什么呢?}

  其内容又该如何实现呢?我们可以产生一个独立的数组副本,这样便可以使某个对象的析构操作不致于影响到另一个对象:

Matrix::Matrix(cosnt Matrix &rhs):_row(rhs.row), _col(rhs._col){        //对rhs._pmat所寻址之数组产生一份完全副本int elem_cnt = _row * _col;_pmat = new double[elem_cnt];for (int ix = 0 ; ix < elem_cnt; ++ix)_pmat[ix]= rhs._pmat[ix];}

  当我们设计class时,必须问问自己,在此class之上进行“成员逐一初始化”的行为模式是否适当?如果答案肯定,我们就不需要另外提供copy constructor.但如果答案是否定,我们就必须另行定义copy constructor,并在其中撰写正确的初始化操作.
  如果有必要为某个class撰写copy constructor,那么同样有必要为它撰写copy assignment operator(参阅4.8节)。欲更进一步了解constructot和destructor的特性,请参阅[LIPPMAN98]第14章,以及[LIPPMAN96a]第2章与第5章。

4.3  何谓mutable(可变)和const(不变)

  看看下面这个小函数:

int  sum(  const  Triangular  &trian ){    int  beg_pos = trian.beg_pos();    int  length  = trian.length();    int sum = 0;    for (int ix = 0; ix < length; ++ix)        sum += trian.elem(beg_pos + ix);    return sum;}

  trian是个cons reference参数,因此,编译器必须保证trian在sum()之中不会被修改.但是.sum()所调用的任何一个member function都有可能更动trian的值.为了确保trian之
值不被更动,编译器必须确保beg_pos(),length(),elem()都不会更动其调用者.编译器如何得知这项信息呢?是的,class设计者必须在member function身上标注const,以此告诉编译器:这个membet function不会更动class object的内容:

class Triangular{public://以下是const member functionsint length() const {return _length;}int beg_pos() const {return _beg_pos;}int elem(int pos) cosnt;//以下是non-const member functionsbool next(int &val);void next_reset() {_next = _beg_pos - 1;}// ...private:int _length, _beg_pos, _next;static vector<int> _elems;};

  const修饰词紧接于函数参数表之后。凡是在class主体以外定义者,如果它是一个const member funclion,那就必须同时在声明式与定义式中都指定const.例如:

int Triangular::elem(int pos) const    {return _elem[pos - 1];}

  虽然编译器不会为每个函数进行分析,决定它究竟是const还是non-const,但它会检查每个声明为const的member function,看看它们是否真的没有更动class object内容,例如,假设我们将以下的next()声明为const member function,就会产生编译错误,因为很显然它会更改其调用者的值.

bool Triangular::next(int &value) const{if (_next < _beg_pso + _length - 1){//错误,改动了_next的值value = _elems[_next++];return true;}return false;}

  下面这个class,val()并不直接修改_val,但它却会返回一个non-const reference指向val。那么,val()可被声明为const吗?

class val_class {public :   val_class (const BigClass &v)   :_val(v) {}//这样没有问题吗?BigClass&  val() const {return _val;}private :    BigClass _val;};

  不,这会产生问题(译注:但语法层面正确),返回一个non-conse reference指向_val.实际上等子将_val开放出去,允许程序在其它地方加以修改,由于member functions可以根据const与否而重载,因此有个方法可以解决这个问题:提供两份定义,一为const版本,一为non-const版本。例如:    .

class val_class {public :const BigClass& val() const {return _val;}BigClass& val() {return _val;}// ...};

  non-const class ohject会调用non-const版的val()(译注:于是对象内容被改变也没有关联),const class object则会调用const版的val()译注:那就不可能改变对象的内容).这样就完全没问题了.举个例子:

void  example( const BigClass  *pbc,  BigClass  &rbc){    pbc->val();  //这会调用const版本   rbc.val();    //这会调用non-const版本}

  设计class时,鉴定其const member functions是一件很重要的事.如果你忘了这么做,要知道,没有一个const reference class参数可以调用公开接口中的non-const成分(译注:但目前许多编译器对此类情况都只给予警告)。用户也许会因此大声咒骂。将const加到class内并非易事,特别是如果某个member function被广泛使用之后.

Mutable Data Member(可变的数据成员)
  以下是sum()的另一种做法,藉由next()和next_reset()两个member functions对trian元素进行迭代:

int sum(const Triangular &trian){if (!trian.length())return 0;int val, sum = 0;trian.next_reset();while(trian.next(val))sum += val;return sum;}

  这段程序会通过编译吗?不,至少现在不行.trian是个const object,而next_reset()和next()都会更动_next值,它们都不是const member functions.但它们却被trian调用,于是造成错误.如果我们很希望采用sum()的这份实现代码,next()和next_reset()势必得改为const.但它们真的改变了_next的值呀!唔,我们可以做一个很好的区别.
  检讨一下,_length和_beg_pos提供了数列的抽象属性.如果我们改变trian的长度或起始位置,形同改变其性质,和未改变前的状态不再相同。然而_next只是用来让我们得以实现出iterator机制,它本身不属于数列抽象概念的一环,改变_next的值,从意义上来说,不能视为改变class object的状态,或说不算是破坏了对象的常数性(constness).关键词mutable可以让我们做出这样的声明,只要将_next标示为mutable,我们就可以宣称:对_next所做的改变并不会破坏class object的常数性.

class Triangular{public :bool next( int &val)const;void next_reset{)  const  { _next= _beg_pos -1;}private :mutable int _nextint _beg_pos;int _length;};

  现在,next()和next_reset()既可以修改next的值,又可以被声明为const member functions。这么一来,前述的sum()实现内容就没有问题了。

4.4 什么是this指针

我们得设计一个copy()成员函数,才能够以Triangular class object作为另一个Triangular class object的初值.假设有以下两个对象:
Triangular tr1(8);

Triangular tr2(8, 9);

当我们调用:tr1.copy(tr2)时,会将tr2的长度及起始位置赋值给tr1。 copy()必须返回被复制出来的对象.本例之中,tr1不仅是复制的目标,也用来接受复制的结果。这该如何完成呢?以下是copy()的一份实现内容:

Triangular& Triangular::copy(const Triangular &rhs){_length = rhs._length;_beg_pos = rhs._beg_pos;_next = rhs._next;return ?? //应该返回什么呢}

其中rhs(译注:right hand side的缩写)被绑定至tr2。而以下这个赋值操作:
  _length = rhs._length; 

中,_length寻址至tr1内的相应成员,我们还需要一种可以寻址至trl整个对象的方法,所谓this指针便扮演这样的角色.
  this指针在member functions内用来寻址其调用者(一个对象)。本例之中,this指向tr1.这是怎么被办到的呢?内部运行过程是,编译器自动将this指针加到每一个member functions的参数表中,于是copy()被转换为以下形式: 

//伪码(Pseudo Code):member functions被转换后的结果Triangular& Triangular::copy(Triangular *this, const Triangular &rhs){this->_length = rhs._length;this->_beg_pos = rhs._beg_pos;this->_next = rhs._next;}

整个转换过程还需另一个配合:每次调用copy()都需提供两个参敦.为达此目的,原始的调用方式:
    tr1.copy(tr2);

会被转换为:
    //内部的程序代码转换
    //tr1变成由this指针所寻址的对象
    copy( &trl, tr2);
  在member functions内.this指针可以让我们取用其调用者的一切.如果想在copy()之中返回tr1.只要简单地提领this指针即可,像这样:
    //返回由this指针所寻址的对象
    return *this;
  欲以一个对象复制出另一个对象,先确定两个对象是否相同是个好习惯.这必须再次运用this指针:

Triangular& Triangular::copy(const Triangular &rhs){//检查两个对象是否相同if (this != &rhs){_length = rhs._length;_beg_pos = rhs._beg_pos;_next = rhs._next;}return *this;}

4.5   Static Class Member(静态的类成员)

  第二章曾经利用一个容器(container)来存储Fibonacci数列元素.这个容器是以局部静态( local static) vector实现完成的。现在,我们的class也只需要唯一一个容器来存储数列元素.关键词static再次为我们解决了问题--虽然,它用于class内时的意义颇有不同
  static(静态)data members用来表示唯一一份可共享的members,它可以在同型的所有对象中被存取.例如以下这份定义式,我们声明_elems是Triangular class的一个static data member:

class Triangular{public :    // ...privater :    static vector<int> _elems;};

对class而言,static data members只有唯一一份实体,因此,我们必须在程序代码文件中提供其清楚的定义.这种定义看起来很像全局对象(global object)的定义方式,唯一的差别是,其名称必须附上class scope运算符(::):

//以下置放于程序代码文件中,例如Triangular.cppvector<int>  Triangular:: _elems;//也可以为它指定初值:int Triangular::_initial_size = 8;

  如果要在class member functions内存取static data members,其方式有如存取一般(non-static)数据成员:

Triangular::Triangular(int len, int beg_pos):_length( len > 0 ? len : 1), _beg_pos( beg_pos > 0 ? beg_pos : 1){_next= _beg_pos - l;int elem_cnt = _beg_pos + _length - 1;if(_elems.size() < elem_cnt)gen_elements( elem_cnt );}

  像buf_size这类的const static  int data memhers,可以在声明时为它们明白指定初值:

class intBuffer{  public:    // ...private :    static const int _buf_size = 1024;    int _buffer[_buf_size];};

Static Member Function(静态成员函数)
  考虑以下的is_elem().给予某值,它会依据该值是否在Triangular数列之内而返回true或false:

bool  Triangular::is_elem{ int value ){if (!_elems.size() ||    _elemf[_elems.size() -1]  <  value)gen_elems_to_value(value);vector<int>::iterator found_it;vector<int>::iterator end_it = _elems.end();found_it = find(_elem.begin(), end_it, value);return found_it != end_it;}

  一般情形下,member function必须通过其类的某个对象来调用。这个对象会被绑定至该member function的this指针身上。通过存储于每个对象中的this指针,member function才能够存取存储于每个对象中的non-static data members,
  然而.上述的is_elem()并不存取任何non-static data member。它的运行和任何对象都没有任何关联,因而应该可以很方便地以一般non-member function的方式来调用.但是我们不能这样写:

if (is_elem(8)) ...

因为这样一来就没有办法让编译器或程序阅读者知道我们想调用的究竟是哪一个is_elem().class scope运算符可以解决这种令人混淆的问题:

if(Triangular::is_elem(8))...

  于是static memher function便可以在这种“与任何对象都无瓜葛”的情形之下被调用.注意,member funaions只有在“不存取任何non-static members”的条件下才能够被声明为static,声明方式是在声明式之前加上关键词static:

class Triangular {public :    static bool is_elem(  int  );    static void gen_ements(int length );    static void gen_elems_to_value (  int  value  ) ;        static void display (  int  length,  int  beg_pos.  ostream &os  =  cout);    //...private :    static const int _max_elems = 1024;    static vector<int> _elems;};

  当我们在class主体外部进行member functions的定义时,不需重复加上关键词static(这个规则也适用于static data members)。

4.6打造一个Iterator Class

    为了说明如何对class进行运算符重载操作,让我们体验一下如何实现一个iterator class,我们必须提供以下操作方式: 

Triangular trian(1, 8);Triangular::iterator it = trian.begin(),end_it = train.end();while(it != end_it){cout << *it << ' ';++it; }

    为了让上述程序代码得以运行,我们必须为此iterator class定义!=,*,++等运算符.这应如何办到呢?我们可以采用像定义member functions那样的形式来定义运算符.运算符函数看起来很像普通函数,唯一的差别是它不需指定名称,只需在运算符符号之前加上关键词operator即可。例如:

class Triangular_iterator{public ://为了不要在每次存取元素时都执行-1操作,//此处将_index的值设为index-1Triangular_iterator(int index) :_index(index - l){}bool operator==( const Triangular_iterator&) const;bool operator!=( const rriangular_iterator&) const;int operator*() const;Triangular_iterator& operator++();//前置(prefix)版Triangular_iterator operator++(int);//后置(postfix)版private :void  check_integrity()  const;int _index;};  

    Triangular_iterator维护一个索引值,用以索引Triangular中用来存储数列元素的那个static data member,也就是_elems。为了达到这个目的.Triangular必须赋予Triangular_iterator'S member functions特殊的存取权限。我们会在4.7节看到如何通过friend机制给予这种特殊权限,如果两个Triangular_iterator对象的_index相等,我们便说这两个对象相等:

inline bool Triangular_iterator::operator==(const Triangular_iterator &rhs) cosnt    {return _index == rhs._index;}

    所谓运算符,可以直接旌行于其class objects身上

if (tran1 == trian2) ...

    如果我们希望将运算符施于指针所指的对象身上,就得先提领该指针,取出其所指对象:

if (*ptri1 == *ptri2) ...

    任何运算符如果和另一个运算符性质相反,我们通常会以后者实现出前者,例如:

inline bool Triangular_iterator::operator!=(const Triangular_iterator &rhs) cosnt    {return !(*this == rhs);}

    以下是运算符重载的规则:
●不可以引入新的运算符,除了.,*,::,?:4个运算符,其它的运算符皆可被重载。
●运算符的操作数(operand)数目不可改变。每个二元运算符都需要两个操作数,每个一元运算符都需要恰好一个操作数.因此,我们无法定义出一个equality运算符,并令它接受两个以上或两个以下的操作数.
●运算符的优先级(precendence)不可改变.例如,除法的运算优先级永远高于加法.
●运算符函数的参数列中,必须至少有一个参数为class型别.也就是说,我们无法为诸如指针之类的non-class型别,重新定义其原已存在的运算符,当然更无法为它引进新运算符.
    运算符的定义方式,就像member functions一样:

inline int Triangular_iterator::operator*() const{    check_integrity();    retun Triangular::_elems[_index];}

    但也可以像non-member functions-样:

inline intoperator*(const Triangular_iterator &rhs){    rhs.check_integrity();    //注意:如果这是一个non-member function,就不具有取用non-public members的权利    return Triangular::_elmes[_index];}

    non- member运算符的参数列中,一定会比相应的member运算符多出一个参数,也就是this指针,对member运算符而言,这个this指针隐喻代表左侧操作数.
    下面这个check_integrity() member funcLion可以确保_index不大于_max_elems,并确保_elems存储了必要的元素.
    接下来我们必须提供increment(递增)运算符的前置(++trian)和后置(trian++)两个版本.前序版的参数表是空的: 

inline Triangular_iterator& Triangular_iterator::operator++(){    //前序版本    ++_index;    check_integrity();        return *this;}

    后置版的参数表原本也应该是空的,然而重载规则要求,参数表必须独一无二,与众不同.因此C++语言想出一个变通办法,要求后置版得有一个int参数

inline Triangular_iterator& Triangular_iterator::operator++(int){    //后置版本    Triangular_iterator tmp = *this;    ++_index;    check_integrity();        return tmp;}

    increment【递增)或deaement(递减)运算符的前置及后置版本都可直接施行于其class objecrs
之上:

++it;    //前置版it++;    //后置版  

    令人生疑的是,对后置版而言,其唯一的那个int参数从何发生,又到哪里去了呢?事实的真相是,编译器会自动为后置版产生一个int引数(参数)(译注:其值必为0).用户不必为此烦恼.
    接下来我们要做的,便是为Triangular提供一组begin()/end() member functions,并支持前述的iterator定义.这需要用到稍后才讨论到的所谓嵌套型别(nested rypes).
    首先看看我们必须对Triangular做的修正:

class Triangular{public ://以下这么做,可以让用户不必知晓iterator class的实际名称typedef Triangular_iterator iterator;Triangular_iterator begin() const{return Triangular_iterator(_beg_pos);}Triangular_iterator end() const{return Triangular_iterator(_beg_pos + _length)};//...private :int _beg_pos;int _length;};

嵌套型别( Nested Types)
    typedef可以为某个型别设定另一个不同的名称.其通用形式为。

typedef existing_type new_nae;

    其中的existing_type可以是任何一个内建型别.复合型别,或class型别.在倒子中,令iterator等同于Triangular_iterator,以简化萁使用形式.以下是定义一个iterator object的语法;

Triangular::iterator it = trian.begin();    

    我们得使用class scope运算将来导引编译器,让它在面对iterator这个字眼时,检视Triangular内部提供的定义.如果我们仅仅只是写:

iterator it = trian.begin();

    编译器就不知道在面对iterator这个字时该检视Triangular的内容,于是以上声明出现错误。
    如果将iterator嵌套置于每个“提供iterator抽象观念的class内”,我们就可以提供多个定义,有着相同的名称.但是这样的声明语法有些复杂;

Fibonacci:: iterator fit = fib.begin();Pell:: iterator pit = pel.begin();vector<int>::iterator vit = _elems.begin();string::iterator sit = file_name.begin();

4.7  合作关系必须建立在友谊的基础上   

    以下的non-member operator*()会直接取用 Triangular的 private _elems以及Triangular_iterator的private check_integrity():

inline int operator*(const Triangular_iterator &rhs){    ths.check_integrity();    return Triangular::_elems[rhs.index()];}

    为什么上述程序直接取用private members却可以通过编译呢?因为任何class都可以将其它functions或classes指定为友元(friend)。而所谓friend,具备了与class member function相同的存取权限,可以存取class的private member·为了让operator*()通过编译,不论Triangular或Triangular_iterator都必须将operator*()声明为“友元”:

class Triangular{friend int operator*( consc Triangular_iterator &rhs);// ...};class Triangular_iterator{friend int  operator*( const Triangular_iterator &rhs);//...};

    只要在某个函数的原型(prototype)之前加上关键词friend,就可以将它声明为某个class的friend.这份声明可以出现在class定义式的任意位置上,不受private或public的影响.如果你希望将数个重载函数都声明为某个class的friend,你必须明白地为每个函数加上关键词friend.
    Triangular_iterator  内的  operator*()  和  check_integrity()都需要直接取用Triangular的private members,因此,我们将两者都声明为Triangular的friend:   

class Triangular{    friend  int  Triangular_iterator::operator*();    friend void Triangular_iterator::check_integrity();};

    为了让上述定义成功通过编译,我们必须在上述两行之前,先提供Triangular_iterator的定义给Triangular知道,否则编译器就没有足够的信息可以确定上述两个函数原型是否正确,也无法确定它们是否的确是Triangular_iterator的member function。
    我们也可以令chss A与class B建立friend关系,藉此让class A的所有member functions都成为class B的friend。例如:

class Triangular{    friend class Triangular_iterator;};

    如果以这种形式来声明cIasses间的友谊,就不需要在友谊声明之前先显现class的定义,不过我们也并非一定得以friend方式达到目的.举个例于,仔细看看以下的check_integrity()定义:

inline  void  Triangular_iterator::check_integrity(){if( _index >= Triangular::_max_elems )throw iterator_overflow();if( _index >= Triangular::_elems.size() )Triangular::gen_elements(_index + 1);}

    如果Triangular提供一个public member function用来取得max_elems.以及提供另一个public member function用来返回_elems的当前大小,那么check_integrity()就不再需要授予任何人友谊.像这样:

class Triangular{public :static int elem_size(){return _elems.size();}static int max_elems(){return _max_elems;}};//友谊关系不再需要inline  void  Triangular_iterator::check_integrity(){if( _index >= Triangular::_max_elems )throw iterator_overflow();if( _index >= Triangular::_elems.size() )Triangular::gen_elements(_index + 1);}

    友谊关系的建立,通常是为了效率考虑,侧如,在某个non-member运算符函数中进行Point和Matrix的乘法运算.如果我们仅仅只是希望进行某个data memher的读取和写入,那么,为它提供具有public存取权限的inlinc函数,是建立友谊关系之外的一个替代方案.

4.8实现一个copy assignmcnt operator 

    情况下,当我们将某个class object赋值给另一个,像这样:

Triangular tri1(8), tri2(8, 9);tri1 = tri2;

    class data members会被依次复制过去.在我们的例子中,_length, _beg_pos,_next都会从tri2被复制到tri1去,这称为default memberwise copy(默认的成员逐一复制操作).
    以Triangular为例,default memberwise copy即已足够,我们不需另做其它事情.但是面对4.2节的Matrix class,这种default memberwise copy行为便不正确(析构函数出现了问题,内存管理部分),其原因已在4.2节的default memberwise
initiaIization主题中探讨过.
    Matrix需要一个copy constructor和一个copy assignment operator.以下便是我们为Matrix的copy assignment operator所作的定义:

Matrix& Matrix::operator=(const Matrix &rhs){if( this != &rhs){_row = rhs._row;_col = rhs._col;int elem_cnt = _row * _col;delete [] _pmat;  //在指针变量前面加一个括号,表示对数组空间的操作_pmat = new double[elem_cnt];for (int ix = 0; ix < elem_cnt; ++ix)_pmat[ix] = rhs._pmat[ix];}return *this;}

    只要cIass设计者明白提供了copy assignment operator(像上面那样),它就会被用来取代default memberwise copy行为。客户端的程序代码不必有任何更动。

4.9  实现一个function object

    我们已经在3.6节看到了标准程序库事先定义的function objects。本节教你如何实现自己的function object.所谓function object乃是一种“提供有function call运算符”的class。
    当编译器在编译过程中遇到函数调用,例如:

lt( ival );

时候,lt可能是函数名称,可能是函数指针,也可能是一个提供了function call运算符的function object。如果lt是个clas object,编译器会在内部将此语句转换为:

lt.operator( ival );  //内部转换结果

    function call运算符可接受任意数目的参数:零个,一个、两个或更多。举个例子,它可以被用来支持Matrix的多维下标(subscripting)操作,因为语言所提供的subscript运算符仅能接受一个参数。
    现在就让我来实现一个functian call运算符,测试传人值是否小于某个指定值。我将此class命名为LessThan,其每个对象都必须被初始化为某个基值。此外,我也提供该基值的读取及写人操作。以下便是我的实现内容:

class LessThan{public :LessThan(int val) : _val(val){}int comp_val()const {return _val;}void comp_val(int nval){_val = nval;}bool operator()(int _value)const;private :int _val;};

    其中的function call运算符实现如下

inline bool LessThan::operator()(int value)const {return value <  _val;}

    定义LessThan object的方式和定义一般对象并没有两样:

LessThan lt10(10);

    将function call运算符施行于对象身上,我们便可以调用functioncall运算符:

int count_less_than(const vector<int> &vec, int comp){LessThan lt(comp);int count = 0;for (int ix = 0; ix < vec.size(); ++ix)if (lt(vec[ix]))++count;return count;}  

    通常我们会把function object当作参数传给泛型算法,例如:

void print_less_than(const vector<int> &vec,                     int comp, ostream &os = cout){LessThan lt(comp);vector<int>::const_iterator iter = vec.begin();vector<int>::const_iterator it_end = vec.end();os << "elements less than" << lt.comp_val() << end1;while((iter = find_if(iter, it_end, lt)) != it_end){os << *iter << ' ';++iter;}}   


4.10  将iostream运算符重载

    我们常常会希望对某个class object进行读取和写入操作,如果我们想显示trian对象的内容,可能会希望这样写:

cout << trian << endl;

    为了支持上述形式,我们必须另外提供一份重载的output运算符:

ostream& operator<<(ostream &os, const Triangular &rhs){    rhs.display(rhs.length(), rhs.beg_po(), os);    return os;  //返回了os,可以连续的<<}

    传人函皴的ostream对象又被原封不动地返回.如此一来,我们便得以连接多个output运算符.参数列中的两个对象皆以传址(by referance)方式传人,其中的ostream对象并未声明为const,因为每个output操作都会更动ostream对象的内部状态.至于rhs这种将被输出的对象,就会被声明为const-因为这里之所以使用传址方式,是基于效率考虑而非为了修改其对象内容。

    为什么不把output运算符设计为一个member function呢?因为作为一个member function,其左侧操作数必须是隶属同一个class之下的对象,如果output运算符被设计为tri class member function,那么tri objects就必须被置于output运算符的左侧:

tri << cout << '\n';

    这种奇怪的形式必定对chrss用户造成困惑!

4.11  指针:指向Class Member Functions

    支持Fibonaoci,Pell,Lucas,Square种种数列的classes,基本上和Triangular class大同小异,只不过用来产生数列元素的算法不同罢了,第5章我会将这些类组织起来,成为一个面向对象的类体系.本节讨论的重点在于实现出一个通用的数列类,称为num_sequence,其对象可同时支持上述数列.以下是测试用main():

int main(){num_sequence ns;const int pos = 8;for (int ix = 1; ix < num_senquence::num_of_sequence(); ++ix){ns.set_sequence(num_senquence::ns_type(ix));int elem_val =ns.elem(pos);display(cout, ns, pos, elem_val);}}  

    其中的ns便是一个通用型数列对象.在for循环每次迭代的过程中,我们利用se t_sequence(),根据ns_type()返回的不同数列的代码,将ns置新设值.num_of_sequence()返回当前支持的数列种类数目.num_of_sequence()和ns_type皆为inline static member function.elem()会返回特定位置的元素值.程序编译并执行后,产生如下的输出结果:

The element at position 8 for fibonacci sequence ia 21The element at position 8 for pell sequenca is 40aThe alement at position 8 for lucas sequenca is 47The element at position 8 for triangular sequence is 36The element at position 8 for square sequence is 64The element at position 8 for pantagonal sequance is 92

    num_sequence的设计关键在于,pointer to member function(指向成员函数之指针)机制的运用.这种指针看起来和pointer to non-membar function(2.8节介绍过)极为相似.两着皆必须指定其返回型别和参数表.不过.pointer to member function还得指定它所指出的究竟是哪一个class.例如:

void (num_sequence::*pm)(int) = 0;

    便是将pm声明为一个指针,指向num_sequence`s member function.后者的返回型别必须是void,且只接受单一参数,参数型别为int.pm的初始值为0,表示它当前并不指向任何member function。

    如果这样的语法过于复杂,我们可以通过typedef加以简化。例如

typedef void (num_sequence::*PtrType)(int);PtrType pm = 0;

    这便是将PtrType声明为一个指针,指向num_sequence`s member function,后者的返回型别是void,只接受一个int参数。这种声明方式和先前的方式完全相同。注意,6个数列,除了元素的算法不同之外,其余都相同。num_sequence提供以下6个member functions,每一个都可由PtrType指针加以寻址:

class num_sequence{public :    typedef void (num_sequence::*PtrType)(int);    //_pmf可寻址下列任何一个函数   void fibonacci(int);    void pell(int);    // ...private :    PtrType _pmf;};

    为了取得某个member function的地址,我们对函数名称施以address-of(取址)运算符.注意函数名称之前必须先以class scope运算符加以修饰,至于返回型别和参数表皆不须指明.举个例子,如果要定义一个指针,并令它指向member function f ibonacci(),我们可以这么写:

PtyType pm - &num_sequence::fibonacci;

    同样的道理,如果要指定pm的值,可以这么写:

pm = &num_sequence::Triangular;

    每当调用set_sequence()时,我们便指定_pmf值,令其指向前述6个member functions之一。为求简化,我们可以将这6个member functions的地址存储在一个siatic array中.为了避免重复计算每个数列元素,我们还维护一个static vector,内放6个vectors,分别存储各个数列:

class num_sequence{public :    typedef void(num_sequence:: *PtrType)(int);    // ...    private :    vector<int>* _elem;   //指向目前所用的vector    PtrType _pmf;  //指向目前所用的算(用以计算数列元素)    static const int num_seq = 7;    static PtrType func_tbl[num_seq];    static vector<vector<int> > seq;};

    上述所有定义之中,莫复杂的莫过于seq了:

static vector<vector<int> > seq;

    其意义是,seq是个vector,其中每个元紊又是一个vector(代表一个数列),用来存放int元素。如果我们忘了在两个“>”符号之间加上空白,像这样:

static vector<vector<int>> seq;  //无法编译成功

    就无法编译成功。这是基于所谓的maximal munch编译规则,此规则要求,每个符号序列(symbol sequence)总是以“合法符号序列”中最长的那个解释之.因为>>,是个合法的运算符序列,因此,如果两个“>”符号之间没有空白,这两个符号必定会被合在一起看待.同样道理,如果我们写下 a+++p,在maximal munch规则之下,它必定会被解释为:a++ + p
    接下来,我们还必须提供每个static data members的定义.由于PtrType是个嵌套型别,所以在num_sequence之外对它所做的任何取用操作,都必须以class scope运算符加以修饰。至于num_seq的值,已经在class定义式中指定好了,这儿不需再重复指定.

num_sequence::PtrTypenum_sequence::func_tbl[num_seq] ={0, &num_sequence::fibonacci, &num_sequence::pell, // ... };

   如果你对嵌套型别的语法感到困惑,可以利用typedef加以隐藏:

typedef num_sequence::PtrType PtrType;PtrType num_sequence::func_tbl[num_seq] = ...

     _elem和_pmf在set_sequence()内一起被设定。设定好后,_elem措向存有数列元素的vector,_pfm则指向产生数列元素的member function。set_sequence()的实际做法,要到5.3节才介绍。
    pointer to member function和pointer to function的一个不同点是,前者必须通过同类的对象加以调用,而该对象便是此member function内的this指针所指之物。假设有以下定义;

num_sequence  ns ;num_sequence *pns = &ns;PtrType  pm  =  &mucLsequence::fibonacci;

    为了经由ns调用_pmf.我们这样写:

(ns.*pm)(pos);

    其中的“ .* ”符号是“pointer to member selection运算符”,系针对class object运行-我们必须为它加上外围小括号,才能正确运行.至于针对pointer to member object运行的“pointer to member selection运算符”,其符号是“ ->* ”:

(pns->*pm)(pos);

    以上便是截至目前所讨论的实现方法.第五章有更多设计细节,谈到如何让每个num_sequence对象在其生命期的任意时刻,都能知晓自身的数列究竟是什么种类,然后我们会看到如何通过面向对象(object-oriented)编程风格,将多重型别的操作方式更加简化。

原创粉丝点击