《Essential C++》读书笔记 之 基于对象编程风格
2014-07-13
4.1 如何实现一个class
4.2 什么是Constructors(构造函数)和Destructors(析构函数)
Constructor
Member Initialization List(成员初值表)
Destructor
Memberwise initialization(成员逐一初始化)
4.3 何谓mutable(可变)和const(不变)
Mutable Data Member(可变的数据成员)
4.4 什么是this指针
4.5 Static Class Member(静态的类成员)
4.6 打造一个Iterator Class
嵌套型别(Nested Types)
4.7 合作关系必须建立在友谊的基础上
4.8 实现一个copy assignment operator
4.9 实现一个fuction object
4.11 指针:指向Class Member Functions
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<string> _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 Triangular 2 { 3 public: 4 //default constructors 5 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 Triangular 2 { 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 initialization 5 Matrix mat2=mat; 6 //...这里使用mat2 7 //此处,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;ixix) 8 _pmat[ix]=rhs._pmat[ix]; 9 }
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;ixix) 8 sum+=trian.elem(beg_pos+ix); 9 return sum; 10 }
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 fuctions 10 int length() const { return _length; } 11 int beg_pos() const { return _beg_pos; } 12 int elem( int pos ) const; 13 14 //以下是non-const member fuctions 15 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<int> _elems; 24 };
const修饰词紧接于函数参数表之后。凡在class主题外定义者,如果它是一个const member function,那必须同时在声明式与定时式都指定const。如下代码:
1 int Triangular::elem(int pos) const 2 { 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 //添加mutable 10 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拷贝给tr1 5 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 Triangular 2 { 3 public: 4 static bool is_elem(int); 5 //... 6 private: 7 static vector<int> _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 ) const 3 { return _index == rhs._index; }
运算符的定义方式:
- 可以像member functions一样:
1 inline int Triangular_iterator:: 2 operator*() const 3 { 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 member 4 return Triangular::_elems[ _index ]; //直接取用private member 5 }
因为任何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 Triangular 2 { 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 Triangular 2 { 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。但对于4.2节的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;ixix) 12 _pmat[ix]=rhs._pmat[ix]; 13 } 14 return *this; 15 }
4.9 实现一个fuction object
返回
我们已经在3.6节看到了标准程序库定义的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<int>&vec,int comp) 2 { 3 LessThan lt(comp); 4 int count=0; 5 for(int ix=0;ixix) 6 if(lt(vec[ix])) 7 ++count; 8 return count; 9 }
通常我们会把function object当作参数传给泛型算法,例如:
1 void print_less_than(const vector<int>&vec,int comp, ostream &os=cout) 2 { 3 LessThan lt(comp); 4 vector<int>::const_iterator iter=vec.begin(); 5 vector<int>::const_iterator it_end=vec.end(); 6 7 os<<"elements less than"<endl; 8 while((iter=find_if(iter,it_end,lt))!=it_end) 9 { 10 os<<*iter<<' '; 11 ++iter; 12 } 13 }
4.11 指针:指向Class Member Functions
返回
pointer to member function(指向成员函数之指针)机制的运用 ,这种指针看起来和pointer to non-member 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;
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=&ns 2 3 //以下写法和pns->Fibonacci(pos)相同 4 (pns->*pm)(pos)