基于对象的编程风格
一般而言class由两部分组成:一组 公开的(public)操作函数和运算符_,以及一组 私有的(private)实现细节 。这些操作函数和运算符称为class的 member function(成员函数),并代表这个class的公开接口。身为class的用户,只能访问其公开接口。这就是我们使用 string、 vector的方式。例如,针对 string 的member function size() ,我们只知道其原型声明(prototype),即:参数列表 void, 返回数值。
Class的 private实现细节可由member function的定义以及与此class相关的任何数据组成。
Class 用户通常不会关心实现细节,身为用户,我们只利用其公开接口来进行编程。只要接口没有更改,即使实现细节重新打造,所有的应用程序代码也不需变动。
本章将从class的使用提升至class的设计与实现。这正是C++程序员的主要工作。
4.1 如何实现一个Class
以stack实现为例,讲解实现class的所需完成的工作。
Stack是一个抽象概念,它允许叠放许多数值,并以后进先出(last-in, first-out, LIFO)。我们以pushing 方式将新数值叠放到栈中,并以popping 方式取出最后一个pushed的数值。同时用户会要求其他的操作行为,如查询stack空间是否已满(full),或是否为空(empty), 或询问元素个数(size)。也可能提供查看(peeking)能力,观察stack内最后一个pushed的数值。
(如果把stack定义为class template,可以存放各种类型的元素。但那是第六章讨论的课题,本章只定义non-template版本,存放string元素。)
class的声明以关键字class开始,其后接一个class名称(可任意命名):
class Stack;
该句只是作为Stack class的前置声明(forward declaration),将class名称告诉编译器。这可以让我们进行类指针的定义,或以此作为数据类型:
Stack *pt = 0; // 定义一个类指针
void process( const Stack& ); //以Stack作为数据类型
在定义实际的Stack object 或访问 Stack的member之前,必须先定义class。class定义的骨干如下:
class Stack{
public:
// ... public 接口
private:
// ... private 的实现部分
};
class定义由两部分组成:class声明,以及紧接在声明之后的主体。
public和private用来标示每个块的“member访问权限”。public member可以在程序的任何地方被访问,private member只能在member function或是class friend内被访问。以下是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本身中,
// 其他member则仅仅只是声明。
int size() {return _stack.size(); }
private:
vector<string> _stack;
}
这一份Stack提供了本节一开始找出的六个操作行为。其元素被储存在名为 _stack 的 string vector内。
所有 member function 都必须在 class 主体内声明。至于是否要同时进行定义,可自由决定。
如果在主体内定义,这个 member function 自动被视为 inline 函数。在主体外定义必须使用特殊语法,用以分辨属于哪个class。如果希望该函数为inline,应在最前面指定关键字inline:
inline bool
Stack::empty()
{
return _stack.empty();
}
bool
Stack::pop( string &elem)
{
if ( empty() )
return false;
elem = _stack.back();
_stack.pop_back();
return true;
}
上述的
Stack::empty()
告诉编译器说,empty() 是 Stack class 的一个member。class名称后的两个冒号(Stack::)即所谓的class scope resolution(类作用域解析)运算符。
对inline函数,定义在主体内还是主体外没分别,跟non-member inline function一样,也应该放在头文件中。class定义以及其inline member function 通常都会被放在与class同名的头文件中。如 Stack class 的定义和empty() 函数定义都应该放在Stack.h头文件中。此即用户使用Stack时应该包含的文件。
non-inline member function应该在程序代码文件中定义,该文件通常和class同名,其后接着扩展名 .C、.cc、.cpp 或 .cxx。
以下便是 Stack member function 的定义。 full() 会将目前的元素个数拿来和底层vector的max_size()数值作比较。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的完整定义。下一节将会讲解极为特殊的初始化函数和终止函数,它们分别成为constructor(构造函数) 和deconstructor(析构函数)。
4.2 构造函数和析构函数
以Triangular数列为例,
class Triangular{
public:
// ...
private:
int _length; // 元素个数
int _beg_pos; // 起始位置
int _next; // 下一个迭代目标
};
data member 如何被初始化?提供一个或多个特别的初始化函数,编译器在每次class object被定义时,调用适当函数加以处理。这些特别的初始化函数称为 constructor(构造函数)。
constructor 的名称必须与class名称相同。语法规定,constructor 不应指定返回类型,也不用返回任何值。。它可以被重载(overloaded)。例如, Triangular class 可能有三个 constructor :
class Triangular{
public:
// 一组重载的constructor
Triangular(); // default constructor
Triangular( int len );
Triangular( int len, int beg_pos );
private:
int _length; // 元素个数
int _beg_pos; // 起始位置
int _next; // 下一个迭代目标
};
class object 定义出来后,编译器便自动根据获得的参数,挑选出应被调用的 constructor 。
例如:
Triangular t;
Triangular t2( 10, 3 );
Triangular t3 = 8; //注意, 这里是调用的constructor 而不是 assignment operator
但是,以下程序;代码无法定义一个Triangular object。
Triangular t5();
这里将吧 t5 定义为一个函数,这是为了兼容C。对C而言, t5 之后带有小括号,会使 t5 被视为函数。正确的声明方式,应该和 t 一样:
Triangular t5;
最简单的constructor是所谓的default constructor。它不需要任何参数(argument)。这意味着两种情况。第一,它不接受任何参数:
Triangular::Triangular()
{ // default constructor
_length = 1;
_beg_pos = 1;
_next = 0;
}
第二,这种情况更常见,它为每个参数提供默认值:
class Triangular{
public:
// 也是default constructor
Triangular( int len = 1, int beg_pos = 1 );
};
Triangular::Triangular(int len, int bp)
{
_length = len > 0 ? len : 1;
_beg_pos = bp > 0 ? bp : 1;
_next = _beg_pos - 1;
}
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)
{} // 是的,空的。
Member initialization list 紧接在参数列表最后的冒号后面,是个以逗号分隔的列表。
主要用于将参数传给member class object 的constructor。
class Triangular{
public:
// ...
private:
string _name;
int _length, _beg_pos, _next;
};
为了将_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,当其object结束生命时,便会自动调用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 )
{
_pmat = new double[ row * col];
}
~Matrix()
{
delete [] _pmat;
}
private:
int _row, _col;
double* _pmat;
};
编译器会在 class object被定义的下一刻,应用Matrix constructor。于是_pmat 被初始化为一个指针,指向程序空闲空间(free store )中的一块内存,代表一个具有16个double元素的数组。语句块结束之前,编译器又会暗暗应用Matrix destructor,于是释放_pmat 所指向的数组。
destructor并非绝对必要。
Memberwise Initialization(成员逐一初始化)
默认情形下,当我们以某个class object作为另一个object的初值,例如:
Triangular tril( 8 );
Triangular tri2 = tri1;
class data member 会依次复制。本例中的_length、_beg_pos、 _next 都会依次从tri1复制tri2。此即default memberwise initialization (默认的成员逐一初始化操作)。
但是对先前介绍的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值。
这会使两个对象的_pmat 都知道heap内的同一个数组。当 Matrix destructor应用于mat2 上,该数组空间便被释放。不幸的是,此时 mat 的 _pmat 仍指向那个数组,这是非常严重的错误行为。
修正方法时:改变这种行为模式,可以通过“为Matrix提供另一个copy constructor” 达到目的。(这是Matrix设计者的工作,用户不需考虑。)
如果提供了一个copy constructor,它就可以改变“成员逐一初始化”的默认行为模式。客户端虽然需要重新编译,但至少其源代码不必有任何更改。
例子:
Matrix::Matrix(const 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 ];
}
copy constructor唯一参数是const reference,指向(代表)一个Matrix object。
以上可以产生一个独立的数组复本,这样便可以使某个对象的析构操作不致于影响到另一个对象。
如果有必要为class编写copy constructor,同样有必要为它编写copy assignment operator(4.8节介绍)。
4.3 mutable 和 const
为了确保调用的member function 不会更改调用者。class设计者标注 const :这个member function不会改变调用者的内容。
class Triangular{
public:
// 以下是 const member function
int length() const { return _length; }
int beg_pos() const { return _beg_pos; }
int elem( int pos ) const;
// non-const member function
bool next();
void next_reset() { _next = _beg_pos -1; }
private:
int _length; // 元素个数
int _beg_pos; // 起始位置
int _next; // 下一个迭代目标
static vector<int> _elems;
};
注意:凡是在class主体以外定义者,如果它是const member function,那就必须同时在声明与定义者指定const。
下面这个class,val() 并不直接修改_val,但却会返回一个non-const reference指向_val。
class val_class {
public:
val_class( const BigClass &v)
: _val (v) {}
BigClass& val() const { return _val;}
private:
BigClass _val;
}
这会产生问题(但语法层面正确),这会把_val 开放出去,允许程序在其他地方加以修改。由于member function可以根据const与否进行重载,因此有个方法:提供两份定义,一是const版本,二是non-const版本。
class val_class {
public:
const BigClass& val() const { return _val;}
BigClass& val() { return _val; }
// ...
}
non-const class object 会调用non-const版的val()(对象内容被改变也没关系) ,const class object 则会调用const版的val() (不可能改变对象的内容)
例子:
void example ( const BigClass *pbc, BigClass &rbc )
{
pbc->val();
rbc.val();
}
Mutable Data Member(可变的数据成员)
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 function 。
_length 和 _beg_pos 提供了数列的抽象属性。但是_next 是用来实现出Iterator机制的,并不属于数列抽象概念的部分。从意义上来说,改变_next 的值不算改变了class object的状态或者说 不算改变了对象的常量性(constness)。关键字 mutable 可以让我们做出这样的声明。只要将_next 标示为mutable,就可以说:对_next 所做的改变并不会破坏class object 的常量性。
class Triangular{
public:
bool next();
void next_reset() { _next = _beg_pos -1; }
// ...
private:
int _length; // 元素个数
int _beg_pos; // 起始位置
mutable int _next; // 下一个迭代目标
};
4.4 this指针
this是member function内用来指向其调用者(一个对象)。可以让我们访问其调用者的一切。
以copy为例
Triangular& Triangular::
copy( cosnt Triangular &rhs)
{
if( this != &rhs )
{
_length = rhs._length;
_beg_pos = rhs._beg_pos;
_next = rhs._next;
}
return *this;
}
4.5 静态类成员
static(静态) data member 用来表示唯一的、可共享的member。它可以在同一类的所有对象中被访问。对于class而言,static data member 只有唯一的一份实体,因此我们必须在程序代码文件中提供清楚的定义。在class member function内访问 static data member ,方式跟一般(non-static)数据成员一样。
static Member Function(静态成员函数)
当成员函数不访问任何non-static data member,它的工作跟任何对象都没有任何关联,因而应该可以很方便地以一般non-member function的方式来调用,但为了让编译器或程序阅读者知道我们想调用的究竟是哪一个,class scope运算符可以解决这种令人混淆的问题:
class Triangular{
public:
static bool is_elem( int);
static void gen_elements( 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 function时,无须重复加上关键字 static(这个规则同样适用于static data member):
void Triangular::
gen_elems_to_value( int value )
{
int ix = _elems.size();
if( !ix ){
_elems.push_back( 1 );
ix = 1;
}
while ( _elems[ix-1] < value && ix < _max_elems )
{
++ix;
_elems.push_back( ix*(ix+1)/2 );
}
if ( ix==_max_elems )
cerr << "Triangular Sequence: oops: value too large "
<< value << " -- exceeds max size of "
<< _max_elems << endl;
}
4.6 打造一个Iterator Class
本节是为了说明如何进行运算符重载操作,以实现一个Iterator class为例。
class Triangular_iterator
{
public:
Triangular_iterator( int index ) : _index(index -1) {}
bool operator==( const Triangular_iterator& ) const;
bool operator!=( const Triangular_iterator& ) const;
int operator*() const;
Triangular_iterator& operator++();
Triangular_iterator operator++( int );
private:
void check_integrity() const;
int _index;
};
Triangular_iterator 维护一个索引值,用以索引Triangular中用来储存数列元素的那个static data member,也就是_elems。
inline bool Triangular_iterator::
operator==( const Triangular_iterator &rhs ) const
{
return _index == rhs._index;
}
任何运算符如果和另一个运算符性质相反,我们通常会以后者实现出前者,例如:
inline bool Triangular_iterator::
operator!=( const Triangular_iterator &rhs ) const
{
return !(_index == rhs._index);
}
运算符重载规则:
1.不可以引入新的运算符。除了.、 .*、 ::、 ?: 四个运算符,其他的运算符都可被重载。
2.运算符的操作数(operand)个数不可改变。
3. 运算符的优先级(precedence)不可改变。
4. 运算符函数的参数列表中,必须至少有一个参数为class类型。
运算符的定义方式,就像member function一样。
但也可以像non-member function 一样:
但是这样就无法访问non-public member 的权力。所以该运算符的参数列表中会比相应的member运算符多出一个参数,也就是this指针。对member而言,这个this指针隐式代表左操作数。
对于increment(递增)运算符的前置和后置两个版本,后置版原本应该是空的,但是重载规则要求,参数列表必须独一无二,所以,C++设计了一个变通办法,要求后置版得有一个int参数:
inline Triangular_iterator& Triangular_iterator::
operator++()
{ // 前置版本
++_index;
check_integrity();
return *this;
}
inline Triangular_iterator Triangular_iterator::
operator++( int )
{ // 后置版本
Triangular_iterator tmp = *this;
++_index;
check_integrity();
return tmp;
}
注意:对于后置版,编译器会自动为后置版产生一个int参数(其值必为0)。
接下来就是提供begin() / end() member function,并支持前述Iterator定义。
class Triangular{
public:
typedef Triangular_iterator iterator;
iterator begin() const
{
return iterator( _beg_pos );
}
iterator end() const
{
return iterator( _beg_pos+_length );
}
// ...
private:
int _beg_pos;
int _length;
// ...
};
嵌套类型(Nested Type )
typedef 可以为某个类型设定另一个不同的名称。其通用形式:
typedef existing_type new_name;
4.7 合作关系必须建立在友谊的基础上
以下的non-member operator*() 会直接访问private ;
inline int operator*( const Triangular_iterator &rhs )
{
rhs.check_integrity();
return Triangular::_elems[ rhs.index() ];
}
上述程序是无法通过编译的。有两种方案可以解决:
一是,在class设计时,将operator*() 函数指定为friend,这可以使其具备class member function相同的访问权限;
class Triangular{
friend int operator*(const Triangular_iterator &rhs);
// ..
};
class Triangular_iterator{
friend int operator*(const Triangular_iterator &rhs);
//...
};
二是在在class设计时提供public的接口
class Triangular{
public:
static int elem_size() { return _elems.size(); }
static int max_elems() { reutn _max_elems; }
// ...
};
补充:
我们也可以令class A 与 class B 建立 friend 关系,借此让class A 的所有member function都可以成为class B 的friend。例如:
class Triangular {
// 以下造成Triangular_iterator的所有 member function 都成为Triangular的friend
friend class Triangular_iterator;
// ...
};
4.8 实现一个copy assignment operator
默认情况下 ,当我们将某个class object赋值给另一个,例如:
Triangular tri1(8), tri2( 8, 9);
tri1 = tri2;
class data member 会被依次复制过去,这称之为default memberwise copy(默认的成员逐一复制操作)。
这对Triangular够用,但是对4.2节的Matrix class 行为就不正确了;
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;
}
4.9 实现一个function object
所谓的function object 乃是一种“提供有function call运算符”的class。
当编译器在编译过程中遇到函数调用,例如:
lt(ival);
lt 可能是函数名称,可能是函数指针,也可能是一个提供了function call “()” 运算符的function object。
例:
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;
}
将function call运算符应用于对象身上,便可以调用function call 运算符:
int count_less_than( const vector<int> &vec,
int comp, ostream &os = cout)
{
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() << endl;
while (( iter = find_if( iter, it_end, lt )) != it_end )
{
os << *iter << ' ';
++iter;
}
}
以下小程序,用来练习上述两个函数:
int main()
{
int ia[16] = { 17, 12, 44, 9, 18, 45, 6, 14,
23, 67, 9, 0, 27, 55, 8, 16};
vector<int> vec(ia, ia+16);
int comp_val = 20;
cout << "Number of elements less than "
<< comp_val << " are "
<< count_less_than( vec, comp_val ) << endl;
print_less_than( vec, comp_val );
return 0;
}
4.10 重载iostream 运算符
我们 常常希望对某个class object 进行读取和写入操作。例:
Triangular tri(6, 3);
cout << tri << '\n';
Triangular tri2;
cin >> tri2;
cout << tri2;
我们需要提供重载的运算符
ostream& operator<< ( ostream &os, const Triangular &rhs)
{
os << "( " << rhs.beg_pos() << ", "
<< rhs.length() << " )";
rhs.display( rhs.length(), rhs.beg_pos(), os);
return os;
}
istream& operator>> ( istream &is, Triangular &rhs)
{
char ch1, ch2;
int bp, len;
is >> ch1 >> bp
>> ch2 >> len;
rhs.beg_pos( bp );
rhs.length( len );
rhs.next_reset();
return is;
}
4.11 指针,指向Class Member Function
pointer to member function (指向成员函数的指针)和pointer to non-member function 极为相似。两者都需要指定其返回类型和参数列表。不过,pointer to member function还需指明它所指的class。例:
void (num_sequence::*pm)(int) = 0;
上述将pm 声明为一个指针,指向num_sequence 的member function,后者的返回类型必须是void ,并只接受单一参数,参数类型为int。pm的初始值为0,表示它目前并不指向任何member function。
我们还可以通过typedef 加以简化:
typedef void (num_sequence::*PtrType) (int);
Ptrtype pm = 0;
将PtrType 声明为一个指针。
六个数列除了元素的算法不同外,其余都相同。num_sequence 提供了六个member function,每一个都可由PtrType指针加以定位:
class num_sequence{
public:
typedef void (num_sequence::*PtrType) (int);
// _pmf 可指向下列任何一个函数
void fibonacci (int);
void pell (int);
void lucas (int);
void triangular (int);
void sequare (int);
void pentagonal (int);
private:
PtrType _pmf; // 指向目前计算数列元素的算法
vector<int>* _elem; // 指向目前所用的vector
static const int num_sequence = 7;
static PtrType func_tbl[ num_sequence ];
static vector<vector<int> > seq;
};
为了取得某个member function 的地址,我们对函数名称应用address-of(取址)运算符。
PtrType pm = &num_sequence::fibonacci;
num_sequence::Ptrtype
num_sequence::func_tbl[ num_seq] =
{
0,
&num_sequence::fibonacci,
&num_sequence::pell,
&num_sequence::lucas,
&num_sequence::triangular,
&num_sequence::sequare,
&num_sequence::pentagonal,
};
注意:
static vector<vector<int>> seq; // 无法编译成功
这是基于maximal munch 编译规则。此规则要求,每个符号序列总是以“合法符号序列”中最长的那个解释。因为>> 是合法运算符序列,因此如果没有空白,这两个符号必定会被合在一起看待。例:
a+++p;
a++ +p;
两者等价。
pointer to member function和 pointer to function 的一个不同点是,前者必须通过同一类的对象加以调用,而该对象便是此member function内的this指针所指之物。
假设:
num_sequence ns;
num_sequence *pns = &ns;
Ptrtype pm = &num_sequence::fibonacci;
ns.fibonacci(pos);
(ns.*pm)(pos);
pns->fibonacci(pos);
(pns->*pm)(pos);
上述效果一样。
本文详细介绍了C++中类的实现过程,包括如何创建一个类、构造函数和析构函数的作用,以及mutable关键字、this指针的使用。此外,还讨论了静态成员、迭代器类的构建以及运算符重载,特别是与对象合作的friend函数和functionobject的概念。文章强调了成员函数的访问权限、拷贝构造函数和赋值运算符的重要性,并提供了实例来说明如何实现这些功能。
40

被折叠的 条评论
为什么被折叠?



