《Essential C++》读书笔记 之 基于对象编程风格
2014-07-13
)
4.1 如何实现一个class
class定义由两部分组成:class的声明,以及紧接在声明之后的主体。主体部分由一对大括号括住,并以分号结尾。
主体内的两个关键词public和private。public members可以在程序的任何地方被取用,private members只能在member function或class friend内被取用。
以下是stack class的起始定义:
1 class Stack { 2 public: 3 bool push( const string& ); 4 bool pop( string &elem ); 5 bool peek( string &elem ); 6 7 bool empty(); 8 bool full(); 9 10 //size()定义于class本身内,其它members则仅仅只是声明11 int size() { return _stack.size(); }12 private:13 vector_stack;14 };
以下是如何定义并使用Stack class object:
1 void fill_stack( Stack &stack, istream &is = cin )2 {3 string str;4 while ( is >> str && ! stack.full() )5 stack.push( str );6 7 cout << "Read in " << stack.size() << " elements\n";8 }
所有member function都必须在class主体内进行声明。至于是否要同时进行定义,可自由决定义。
- 如果要在class主体内定义,这个member fuction会自动被视为inline函数。例如size()函数
- 如果要在class主体外定义,必须使用特殊语法,来分辨该函数究竟属于哪一个class。如果希望该函数为inline,应该在最前面指定关键字inline:
inline bool Stack::empty(){ return _stack.empty();}
上述的: Stack::empty()
告诉编译器说,empty()是Stack class的一个member。class名称之后的两个冒号(Stack::)是所谓的class scope resolution(类范围决议)运算符。
- 对inline函数而言,定义于class主体内或主体外,并没有分别。class定义式及其inline member function通常会放在与class同名得头文件中。例如Stack class的定义和其empty()函数都放在Stack.h头文件中。
- non-inline member functions应该在程序代码中定义,该文件通常和class同名,其后接着扩展名.c,.cc,.cpp。
4.2 什么是Constructors(构造函数)和Destructors(析构函数)
Constructor
Constructor(构造函数)的作用是对data member进行初始化。
语法规定: costructors函数名称必须和class名称相同。constructor不应指定返回型别,亦不需返回值。它可以被重载。看以下代码:
1 class Triangular 2 { 3 public: 4 //一组重载constructors 5 Triangular(); //default constructors 6 Triangular(int lin); 7 Triangular(int len, int beg_pos); 8 private: 9 int _length; //元素数目10 int _beg_pos; //起始位置11 int _next; //下一个迭代目标12 }
如何调用构造函数:
1 //对t施行default constructor(无参构造函数) 2 Triangular t; 3 //调用戴两个参数的constructor 4 Triangular t2(10,3); 5 //是调用constructor or assignment operator呢?答案是constructor,调用戴一个参数的constructor 6 Triangular t3=8; 7 8 //该语句t5被定义为一个函数,其参数表为空,返回Triangular object。why? 9 //因为C++必须兼容与C,对C而言,t5之后带有小括号,会被视为函数。10 Triangular t5();
对于默认函数,它不需要任何arguments。这意味着以下两种情况:
- 第一,它不接收任何参数:
1 Triangular::Triangular()2 {3 int _length=1;4 int _beg_pos=1;5 int _next=0;6 }
- 第二,它为每个参数提供了默认值:
1 class Triangular2 {3 public:4 //default constructors5 Triangular(int len=1, int beg_pos=1);
Member Initialization List(成员初值表)
Constructor定义式的第二种初始化语法,是Member Initialization list:
1 Triangular::Triangular(const Triangular &rhs)2 : _length(rhs._length), _beg_pos(rhs._beg_pos),_next(rhs._next)3 {} //是的,空的
Member Initialization List的主要作用是将参数传递给member class object的constructor。假设重新定义Triangular,令它包含一个string member:
1 class Triangular2 {3 public:4 //...5 private:6 string _name;7 int _length, _beg_pos, _next;8 }
为了将_name的初值传给string constructor,必须以member intialization list完成。代码如下:
1 Triangular::Triangular(int len, int bp)2 : _name("Triangularr")3 {4 _length=len>0?len:1;5 _beg_pos=bp>0?bp:1;6 _next=_beg_pos-1;7 }
Destructor
和constructor对立的是destructor。所谓destructor是用户自行定义的一个class member。一旦某个class提供有destructor,当其object结束生命时,便会自动调用destructor处理善后。destructor主要用来释放在constructor中或对象生命周期中配置的资源。
语法规定:Destructors函数名称必须在class名称再加上‘~’前导符号。它绝对不会有返回值,也没有任何参数。正由于起参数表为空,所以也决不可能被重载(overloaded)。
考虑以下的Matrix class。其constructor使用new表达式从heap中配置double数组所需的空间;其destructor则负责释放这些内存:
1 class Matrix 2 { 3 public: 4 Matrix(int row, int col):_row(row),_col(col) 5 { 6 //constructor进行资源配置 7 _pmat=new double[row*col]; 8 } 9 ~Matrix()10 {11 //destructor进行资源释放12 delete [] _pmat;13 }14 private:15 int _row,_col;16 double* _pmat;17 }
Memberwise initialization(成员逐一初始化)
默认情况下,当我们以某个class object作为另一个object的初值,class data members会被依次复制。但对Matrix class而言,default memberwise initialization并不恰当。看以下代码:
1 //此处,constructor发生作用2 Matrix mat(4,4); 3 4 //此处,进行default memberwise initialization5 Matrix mat2=mat;6 //...这里使用mat27 //此处,mat2的destructor发生作用8 9 //此处,mat的destructor发生作用
上述代码地5行,default membrewise initialziation会将mat2的_pmat设为_pmat值:
mat2._pmat=mat._pmat;
这会使得两个对象的_pmat都寻址到heap内的统一个数组。当Matrix destructor施行mat2身上是,该数组空间便被释放。不幸的是mat的_pmat仍然指向那个数组,而你知道,对空间以被释放的数组进行操作,是非常严重的错误行为。
如何避免这样的情况呢?本例中必须改变这种“成员逐一初始化”的行为模式。可以通过“为Matrix提供另一个copy constructor”达到目的。见如下代码:
1 Matrix::Matrix(const Matrix &rhs):_row(rhs._row),_col(rhs._col)2 {3 //对rhx._pmat所寻址之叔祖产生一份完全副本4 int elem_cnt=_row*_col;5 _pmat=new double[elem_cnt];6 7 for(int ix=0;ix
4.3 何谓mutable(可变)和const(不变)
看下面这个函数sum,它调用对象Triangular的member function:
1 int sum(const Triangular &trian) 2 { 3 int beg_pos=trian.beg_pos(); 4 int length=trian.length(); 5 int sum=0; 6 7 for(int ix=0;ix
trian是个const reference参数,因此,编译器必须保证trian在sum()之中不会被修改。但是,sum()所调用的每一个member funciton都有可能更动trian的值。为了确保trian的值不被更动,编译器必须保证beg_pos(),length(),elem()都不会更动其调用者。编译器如何得知这项信息呢?
class的设计这必须在member function身上标注const,以此告诉编译器:这个membr function不会更动class object的内容:
1 class Triangular 2 { 3 public: 4 //一组重载constructors 5 Triangular(); //default constructors 6 Triangular(int lin); 7 Triangular(int len, int beg_pos); 8 9 //以下是const member fuctions10 int length() const { return _length; }11 int beg_pos() const { return _beg_pos; }12 int elem( int pos ) const;13 14 //以下是non-const member fuctions15 bool next( int &val ); 16 void next_reset() { _next = _beg_pos - 1; }17 private:18 int _length; //元素数目19 int _beg_pos; //起始位置20 int _next; //下一个迭代目标21 22 //static data members将于4.5节说明23 static vector _elems;24 };
const修饰词紧接于函数参数表之后。凡在class主题外定义者,如果它是一个const member function,那必须同时在声明式与定时式都指定const。如下代码:
1 int Triangular::elem(int pos) const2 { return _elems[pos-1];}
编译器会检查每个声明为const的member function,看看他们是否真的没有更动class object内容。如下代码:
1 bool Triangular::next(int &value) const 2 { 3 if(_next<_beg_pos+_length-1) 4 { 5 //错误,更动了_next的值 6 value=_elems[_next++]; 7 return true; 8 } 9 return false;10 }
注意:编译器能够区分const版本和non-const版本的函数。如下代码:
1 class val_class 2 { 3 public: 4 val_class(const BigClass &v):_val(v){} 5 const BigClass& val() const{ return _val;} 6 BigClass& val() { return _val;} 7 private: 8 BigClass _val; 9 };10 class BigClass{};
在调用时,const class object会调用const版的val()(那就不可能改变对象的内容了)。non-const class object会调用non-const版的val()。如下代码:
1 void exmpale(const val_class *pbc, val_class &rbc)2 {3 pbc->val(); //这会调用const版本4 rbc.val(); //这会调用non-const版本5 }
Mutable Data Member(可变的数据成员)
以下是函数sum()的另一种做法,用next()和next_reset()两个member function对trian元素进行迭代:
1 int sum(const Triangular &trian) 2 { 3 if(!trian.length()) 4 return 0; 5 int val,sum=0; 6 //Compile error 7 trian.next_reset(); 8 //Compile error 9 while(trian.next(val))10 sum+=val;11 return sum;12 }
上述代码会编译出错。因为trian是个const object,而next()和next_reset()都会更懂_next的值,它们都不是const member function。但他们被train调用,于是造成错误。
于是我们想是否把next()和next_reset()改为const。但它们确实是改变了_next的值呀!
这里,我们要重新认识一下,在class的data member中,哪些应限定为常数性(constness),哪些不是:
- _length和_beg_pos提供了数列的抽象属性。如果我们改变了它们,形同改变了其性质,和未改变的状态不再相同。所以它们应限定为常数性;
- _next只是用来让我们得以实现除iterator机制,它本身不属于数列抽象概念的一环。改变_next的值,从意义上来说,不能视为改变class object的状态,或者说不算破坏了对象的常数性。
只要将_next标识为mutable,我们就可以宣传:对_next所作的改变并不会破坏class object的常数性:
1 class Triangular 2 { 3 public: 4 //添加const 5 bool next( int &val ) const; 6 void next_reset() const { _next = _beg_pos - 1; } 7 //... 8 private: 9 //添加mutable10 mutable int _next; //下一个迭代目标11 int _length; 12 int _beg_pos;13 };
现在,next()和next_reset()既可以修改_next的值,又可以被声明为const member functions。
4.4 什么是this指针
我们得设计一个copy()成员对象,才能够以Triangular class object作为另一个Triangular class object的初值。假设有以下两个对象,将其中一个拷贝给另一个:
1 Triangular tr1(8);2 Triangular tr1(8,9);3 4 ////将tr2拷贝给tr15 tr1.copy(tr2);
函数copy()的实现:
1 Triangular& Triangular::copy(const Triangular &rhs)2 {3 _length=ths._length;4 _beg_pos=rhs._beg_pos;5 _next=rhs._beg_pos-1;6 7 retrun *this; //什么是this指针8 };
其中rhs(right hand side的缩写)被绑定至tr2。而赋值操作中,_length寻址至tr1内的相应成员。这里出现一个问题:如何寻址至tr1对象本身?this指针就是扮演这样的角色。
本例中,this指向tr1。这是如何作到的?内部的过程是,编译器自动将指针夹道每一个member functions的参数表中,于是copy()被转换为以下形式:
1 //伪码(pseudo code):member function被转换后的结果2 Triangular& Triangular::copy(Triangular *this, const Triangular &rhs)3 {4 this->_length=ths._length;5 this->_beg_pos=rhs._beg_pos;6 this->_next=rhs._beg_pos-1;7 8 retrun *this; //什么是this指针9 };
4.5 Static Class Member(静态的类成员)
注意:member functions只有在“不存取任何non-static members”的条件下才能够被声明为static,声明方式是在声明式之前加上关键词static:
1 class Triangular2 {3 public:4 static bool is_elem(int);5 //...6 private: 7 static vector _elems;8 //...9 };
当我们在class主体外部进行member fuctions的定义时,不许要重复加上关键词static。
4.6 打造一个Iteator Class
定义运算符:运算符函数看起来很像普通函数,唯一的差别是它不需指定名称,只需在运算符符号之前加上关键词operator即可。如class Triangular_iterator:
1 class Triangular_iterator 2 { 3 public: 4 //为了不要在每次存取元素时都执行-1操作,此处将_index的值设为index-1 5 Triangular_iterator( int index ) : _index( index-1 ){} 6 7 bool operator==( const Triangular_iterator& ) const; 8 bool operator!=( const Triangular_iterator& ) const; 9 int operator*() const;10 Triangular_iterator& operator++(); //前置(prefix)版11 Triangular_iterator operator++( int ); //后置(prefix)版12 13 private:14 void check_integrity() const; 15 int _index;16 };
Triangular_iterator维护一个索引值,用以索引Triangular中用来存储数列元素的那个static data member,也就是_elems。为了达到这个目的,Triangular必须赋予Triangular_iterator member functions特殊的存取权限。我们会在4.7节看到如何通过friend机制给予这种特殊权限。
如果两个Triangular_iterator对象的_idex相等,我们边说这两个对象相等:
1 inline bool Triangular_iterator::2 operator==( const Triangular_iterator &rhs ) const3 { return _index == rhs._index; }
运算符的定义方式:
- 可以像member functions一样:
1 inline int Triangular_iterator::2 operator*() const3 {4 check_integrity();5 return Triangular::_elems[ _index ]; 6 }
- 也可以像non_member functions一样:
1 inline int operator*(const Triangular_iterator &rhs) 2 {3 rhs.check_integrity();4 //注意:如果这是个non-member function,就不具有取用non-public members的权利5 return Triangular::_elems[ _index ]; 6 }
non-member运算符的参数列中,一定会比相应的member运算符多一个参数,也就是this指针。
嵌套型别(Nested Types)
typedef可以为某个型别设定另一个不同的名称。其通用形式为:
typedef existing_type new_name
其中existing_type可以是人翮一个内建型别、复合型别,或class型别。
下面一个例子,另iterator等同于Triangular_iterator ,以简化其使用形式:
Triangular::iterator it = trian.begin();
上述代码的Triangular::告诉编译器,在Triangular内部检视iterator。
可以将iterator嵌套置于每个“提供iterator抽象观念”的class内。
4.7 合作关系必须建立在友谊的基础上
以下代码的non-member operator*()会直接取用Triangular的private member:why?
1 inline int operator*(const Triangular_iterator &rhs) 2 {3 rhs.check_integrity(); //直接取用private member4 return Triangular::_elems[ _index ]; //直接取用private member5 }
因为任何class都可以将其它的functions或classes指定为友元(friend)。所谓friend,具备类于class member function相同的存取权限。为了让operator*()通过编译,不论Triangular或Triangular_iterator都必须将operator*()声明为“友元”:
1 class Triangular 2 { 3 friend int operator*(const Triangular_iterator &rhs); 4 //... 5 } 6 class Triangular_iterator 7 { 8 friend int operator*(const Triangular_iterator &rhs); 9 //...10 }
只要将某个函数的原型(prototype)之前加上关键词friend,就可以将它声明为某个class的friend。这份声明可以出现在class定义式的任何位置上,不受private和public的影响。
如果你希望将某个重载函数 声明为某个class的friend,你必须明白的为这个函数加上关键词friend。比如:Triangular_iterator内的operator*()需要直接取用Triangular的private members,就需要将它声明为Triangular的friend:
1 class Triangular2 {3 friend int Triangular_iterator::operator*();4 //...
这样,编译器就知道它是Triangular_iterator的member function。
也可以令class A与class B建立friend关系,让class A的所有member functions都成为class B的friend。如下代码:
注意:友谊关系的建立,通常是为了效率的考虑。如果我们仅仅只是希望进行某个data member的读写,那么,为它提供具有public存取前线的inline函数即可。
1 class Triangular2 {3 friend class Trianguar_iterator;4 //...
4.8 实现一个copy assignment operator
默认情况下,当我们将某个class object赋值给另一个,像这样:
Triangular tril(8), tri2(8,9);tri1 = tri2;
class data members会被依次赋值过去。辄被称为default memberwise copy。但对于节的Matrix class,这种default memberwise copy行为便不正确。
Matrix需要一个copy constructor和一个copy assignment operator。以下便是我们为Matrix的copy assignment operator所做的定义:
1 Matrix& Matrix:: 2 operator=(const Matrix &rhs) 3 { 4 if(this!=&rhs) 5 { 6 _row=rhs._row; _col=rhs._col; 7 int elem_cnt=_row*_col; 8 delete [] _pmat; 9 _pmat=new double[elem_cnt];10 11 for(int ix=0;ix
4.9 实现一个fuction object
我们已经在节看到了标准程序库定义的function objects。本节教你如何实现自己的function object。
所谓functon object乃是一种“提供有function call运算符”的class。
当编译器在编译过程中遇到函数调用,例如:
lt(ival);
时,lt可能是函数名称,可能是函数指针,也可能是提供了function call运算符的function object。如果是function object,编译器会在内部将此语句转换为:
lt.operator(ival);
现在我们实现一个function call运算符,测试传入值是否小于某个指定指。我们将此class命名为LessThan:
1 class LessThan 2 { 3 public: 4 LessThan(int val):_val(val){} 5 int comp_val()const { return _val;} //基值的读取 6 void comp_val(int nval) {_val=nval;} //基值的写入 7 8 bool operator()(int _value)const; 9 private:10 int _val;11 };
其中的function call运算符实现如下:
inline bool LessThan::operator()(int value)const{ return value<_val;}
定义和调用function call运算符:
1 int count_less_than(const vector &vec,int comp)2 {3 LessThan lt(comp);4 int count=0;5 for(int ix=0;ix
通常我们会把function object当作参数传给泛型算法,例如:
1 void print_less_than(const vector &vec,int comp, ostream &os=cout) 2 { 3 LessThan lt(comp); 4 vector ::const_iterator iter=vec.begin(); 5 vector ::const_iterator it_end=vec.end(); 6 7 os<<"elements less than"<<
4.11 指针:指向Class Member Functions
pointer to member function(指向成员函数之指针)机制的运用 ,这种指针看起来和pointer to non-member function(介绍过)极为相似。两者皆必须制定其返回型别和参数表。不过,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;
pointer to member function和pointer to function的一个不同点是:前者必须通过同类对象加以调用,而该对象便是此member function内的this指针所指之物:
1 num_sequence ns;2 typedef void(num_sequence::*PtrType)(int);3 PtrType pm=&num_sequence::fibonaaci;4 5 //以下写法和ns.fibonaaci(pos)相同6 (ns.*pm)(pos)
其中的.*符号是个“pointer to member selection运算符”,系针对class object运行。
至于针对pointer to class object运行的“pointer to member selection运算符”,其符号是->*:
1 num_sequence *pns=&ns2 3 //以下写法和pns->Fibonacci(pos)相同4 (pns->*pm)(pos)