第四章 基于对象的编程风格(什么是构造函数和析构函数)

来源:互联网 发布:应用架构 数据架构 编辑:程序博客网 时间:2024/06/05 07:41

4.2什么是构造函数和析构函数

 

每个数列都适合设计为class,一个数列的clas object可以表现出该数列在某范围内的元素。默认情形下,起始的位置为1.例如:

 

Fibonacci fib1(7,3);

便定义了拥有7个元素的Fibonacci object,起始位置为3

pell pel(10);


则定义了具有10个元素的pell object,起始位置默认为1


Fibonacci fib2(fib1);


定义了一个fibonacci object fib2,并以fib1作为fib2的初值。换句话说,fib2是fib1的副本。

每个class都必须记住它自己的长度——数列元素的个数——起始位置。但起始位置不得为0值或者负值。

所以,我以整数存储长度和起始位置。此刻我再定义第三个member

 

_next,用来记录迭代iterate操作的下一个元素:

 

class triangular
{
  public:

  private:
int_length;//元素的个数
int_beg_pos;//元素的位置
int_next;//下一个迭代目标
};


每个triangular class object(Triangular对象例如tri1,tri2........)内都拥有这些data member


当我写下:triangular tri(8,3);


tri对象内含一份_length(初值为8),一份_beg_pos(初值3),一份_next(初值2,因为vector的第三个元素的索引值为2)。

注意:它没有包含实际上用来存储triangular数列元素的vector。

为什么?因为我们不希望在每个class object中都复制一份这个vector;

所有的class object共享一份vector便足够,我们可以再4.5中看到如何实现。


constructor(构造函数的初始化)

 

这些data member如何被初始化呢?编译器不会自动为我们处理。如果我们提供一个或多个初始化函数,编译器就会在每次class object被定义出来时,调用适当的函数加以处理。这些特别的初始化函数被称为constructor(构造函数)。

 

constructor的函数名称必须与class名称相同。语法规定,consrtuctor不应该指定返回类型,亦不用任何返回值。它可以被重载(overload)例如,triangular class可能有三个constuctor:

 

class triangular
{
  public:
//一组重载的constructor
triangular();//defalt constructors
triangular(int len);
triangular(int len,int beg_pos);
//....
};

 

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

 

而triangular t2(10,3);会调用带有两个参数的cunstructor.括号内的值会被视为传给constructor的参数。

 

同样,triangular t3=8;//请注意,这究竟是调用constructor亦或assignment operator?答案是constructor,会带有单一参数的constructor.

 

出人意料的是,以下代码无法成功定义一个triangular object:
triangular t5();//实际结果出人意料
此行将t5定义为一个函数,其参数列表是空的,返回triangular object。很显然这是一个奇怪的解释。为什么它会这样被解释呢?因为c++必须兼容于c,对c而言,t5之后会带有小括号,会使t5被视为函数,正确的声明方式,应该和先前t一样:triangular t5;


最简单的constructor应该是所谓的default construct。

 

它不需要任何参数,这意味着两种情况,第一它不接受任何参数:

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

第二,这种情况更常见,它为每个参数提供了默认值:

class triangular
{
  public:
//也是default constructor
  triangular(int len=1,int bp=1);
//
  triangular::triangular(int len,int bp)
{
  //_lengtn和_beg_pos都必须>=0
  //最好不要相信用户永远是对的这句话
  _length=len>0?len:1;
  _beg_pos=bp>0?bp:1;
  _next=_beg_pos-1;
}


由于我们为两个整数提供了默认值,所以这个default constructor同时支持原本的三个constructor:
triangular tril;//triangular(1,1)
triangular tril2(12);//triangular(12,1)
triangular tril3(8,3);


Member initialization list(成员初始化列表)

 


constructor定义的第二种初始化语法,是所谓的member initialization list(成员初始化列表):
triangular(const triangular &rhs)
:_length(rhs._length),
_beg_pos(rhs._beg_pos),_next(rhs._beg_pos-1)
{}//是的,空的

member initialization list紧接在参数列表最后的冒号后面,是一个以逗号分隔的列表。其中,欲赋值给member的数值被放在member名称后面的小括号中;这使它们看起来像是调用constructor

就本例来讲,第一种和第二种constructor定义的方式是等价的,并没有是谁干扰谁的问题。
member initialization list主要用于将参数传给member class object的constructor,假如我们重新定义constructor,令它包含一个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

 

所谓denstructor乃是用户自己定义的一个class member,一旦某个class提供destructor,当其中object结束生命的时候,便会自动调用destructor处理善后。destructor主要

用来释放在constructor中或对象生命周期中分配的资源。

destructor的名称有严格规定:class名称再加上'~'前缀。它绝对不会有返回值,也没有任何参数,由于其参数列表是空的,所以也绝不可能被重载overload

 

考虑以下的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()
{
  //desstructor进行资源释放
  delete[]_pmat;
}
private:
int _row,_col;
double*_pmat;
};

于是我们通过matrix本身的constructor和destructor,完成了heap的内存自动管理,例如下面这个语句块:
{
  matri mat(4,4);
  //此处应用constructor
  //...
  //此处应用destructor
}

编译器会在mat被定义出来的下一刻,暗暗应用matrix constructor,于是_pmat被初始化为一个指针,指向程序空闲空间free store中的一块内存,代表一个具有16个double元素的数组。语句块结束之前,编译器又会暗暗应用matrix destructor,于是释放_pmat所指向的那块具有16个double元素的数组。matrix的用户不需要知道内存的管理细节,这种写法有点类似于容器的设计。

destructor并非绝对必要,以我们的triangular为例,三个data member皆以储值方式来存放,这些member在class object被定义以后就已存在,并在class object结束其生命时被释放。因此,triangular destructor没有什么事好做。我们没有义务非得提供destructor,事实上,c++编程最难得部分之一,便是了解何时需要定义destructor而何时不需要。

Memberwise initialization(成员逐一初始化)


默认情形下,当我们以某个class object做为另一个object的初值,例如:
triangular tril(8);
triangular tril2=tril;
class data member会依次复制。本例中的_lengeh,_beg_pos,_next都会依次从tril复制到tril2此即所谓的default memberwise initialization (默认成员逐一初始化操作)

在triangular中,default memberwise initialization会正确复制所有的data member,我们不必特意做其他事,但对先前介绍的matrix class而言,default memberwise initilization并不适合。让我们看看下面代码:
{
 matrix mat(4,4);
//此处,constructor发生作用
{
 matrix mat2=mat;
//此处进行default memberwise initialization
//...在这里使用mat2

//此处mat2的destructor发生作用
}
}

其中,default memberwise initialization会将mat2的值设为mat的_pmat的值:

mat2._pmat=mat._pmat;

这会使得两个对象的_pmat都指向heap内的同一个数组。当matrix destructor应用于mat2上,该数组空间会被释放。不幸的是,此时mat的_pmat仍指向那个数组,而你知道,对空间已被释放的数组操作,是非常严重的错误行为。

     这个问题应该如何修正呢?本例中我们必须改变这种“成员逐一初始化”的行为模式。我们可以通过“为matri提供另一个copy constructor”达到目的。

    如果matri设计者提供了一个copy constructor,它就可以改变“成员初始化”的默认行为模式。客户端虽然需要重新编译,但其源代码不必有任何的更改。

    这个copy constructor看起来会像什么样子呢?其唯一的参数是一个const reference,指向(代表)一个matri object:

    matrix::matrix(const string&rhs)

{

//这里应该写一些什么呢?

}

其内容应该如何实现呢?我们可以产生一个独立的复本,这样便可以使某个对象的析构操作不至于影响到另一个对象:
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];

 

}

当我们设计class时,我们必须问问自己,在此class之上进行“成员逐一初始化”的行为模式是否适当?答案如果是肯定,我们就不需要另外提供copy constructor,那么童颜各有必要为它编写copy assianment operator.
0 0
原创粉丝点击