《Essential C++》读书笔记 之 基于对象编程风格
2014-07-13
4.2 什么是Constructors(构造函数)和Destructors(析构函数)
Member Initialization List(成员初值表)
Memberwise initialization(成员逐一初始化)
4.5 Static Class Member(静态的类成员)
4.8 实现一个copy assignment operator
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;ix<elem_cnt; ++ix) 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;ix<length;++ix) 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;ix<elem_cnt; ++ix) 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;ix<vec.size();++ix) 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"<<lt.comp_val()<<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)