什么是构造函数和析构函数
构造函数和析构函数是针对类来说的,别在别的地方用!
通过构造函数我们可以访问类对象的私有成员从而对其这些成员进行初始化(赋值)操作。
定义类的时候,别忘了右大括号和; !!!!!
构造函数
- 为什么会有构造函数呢,就是我们想初始化类里的数据成员,我们就需要提供一个或者多个特别的初始化函数。嗯?为什么是特别的初始化函数,请先放下疑惑,看看这个特别的初始化函数的解释。
- 每次定义类对象的时候,调用特别的初始化函数进行处理,去初始化类对象里的数据成员,这种初始化函数就叫做构造函数
- 构造函数的名称必须和类的名称相同,我类名叫Triangular,那你构造函数的名称就得是Triangular,一会儿我举个例子给你说明!
- C++语法规定,构造函数没有返回类型,所以构造函数的函数体没有return语句,构造函数也不用返回任何值。构造函数可以是重载函数
先放出Triangular类私有成员和公共接口定义。对理解后面的代码有帮助
class Triangular{
public:
//...公共接口定义在下面一块代码
private:
int _length;//Triangular数列的长度(元素个数)
int _beg_pos;//T数列的起始位置(第一个元素应该是下标几?
//从下标几开始?)
int _next;//T数列的下一个迭代目标(我该操作下一个下标对应的元素了)
Class Triangular{
public:
//以下是构造函数的声明和重载。
Triangular();//默认的构造函数
//看见了吗,构造函数函数名和类名相同,没有返回值返回类型。
Triangular(int len);
Triangular(int len,int beg_pos);
//三个重载构造函数
//...
};//别忘了右大括号和; !!!!!
当我定义了一个类对象,编译器会根据我如何定义的类对象去给我挑选并调用对应的重载构造函数去初始化类对象的数据成员。
比如:
Triangular t;
//编译器选择默认的构造函数来调用。
Triangular t2(10,3);
//编译器选择带两个参数的构造函数来调用,其中
//括号内的值会对应选择调用的构造函数的两个形参
Triangular t3=8;
//相当于Triangular t3(8);编译器选择带一个参数的构造函数来调用
//括号内的值对应选择调用的构造函数的一个形参
注意,这种定义类对象方式并试图采用构造函数初始化数据成员是错误的。
Triangular t4();
这行代码的意思是我将t4定义为一个函数。参数表是空的,返回类型是Triangular类。
为嘛是这样的意思?因为C++向下兼容C,对于C而言,t4()被视为一个函数。
所以正确的t4定义(声明)方式应该是Triangular t4;
- 好,接下来我对默认的构造函数,带一个参数的构造函数,带两个的构造函数,把他们的定义挨个解析一下。
//默认的构造函数的定义方式1
//类主体外定义构造函数((注意这时就不要在右大括号后加;了)
Triangular::Triangular()
{
_length=1;
_beg_pos=1;
_next=0;
}
注意,默认的构造函数还有一个定义方式,那就是给每个参数提供了默认值。
class Triangular{
public:
//注意下面这个函数也是默认的构造函数,是另一种声明和定义方式(定义方式2)(下面这个构造函数是属于类的公共接口的函数)
Triangular(int len=1,int bp=1);//声明默认的构造函数
//在声明中就提供了默认参数值,这在语法中可行!这样在函数的定义
//上就不用提供默认参数值了!
//...
};//别忘了右大括号和; !!!!!
//类主体外定义构造函数
Triangular::Triangular(int len,int bp)
{
//注意数列长度和起始位置都必须≥1!
_length=len>0?len:1;//很巧妙的写法,数列长度大于0就把数列长度真实值赋给_length来初始化该成员变量(数据成员),初始化数列长度为真实的数列长度值。
//否则赋给_length默认值1,初始化数列长度为1,让数列长度保持≥1!
_beg_pos=bp>0?bp:1;
//同理,起始位置大于0就把真实起始位置赋给_beg_pos来初始化该成员变量(数据成员)。否则赋给其1来初始化该成员变量(数据成员)
_next=_beg_pos-1;//因为数组的逻辑序列和物理序列相差1,所以从起始位置开始迭代要这么做,对应好下标。
}
- 好的,下面让我们使用构造函数来初始化数据成员!
Triangular tri1;//相当于调用默认的构造函数来初始化数据成员。
//相当于调用了Triangular::Triangular(1,1);
Triangular tri2(12);//相当于调用单参数的构造函数来初始化数据成员
//相当于调用了Triangular::Triangular(12,1);
Tirangular tri3(8,3);//相当于调用双参数的构造函数来初始化数据成员
//相当于调用了Triangular::Triangular(8,3);
- OK,构造函数的声明定义和使用就讲述这么多!
成员初始化列表
- 这是定义构造函数的第二种初始化数据成员的初始化语法。
先以一块代码作为开头讲解这个初始化语法。
//类主体外定义类成员函数(构造函数)
Triangular::Triangular(const Triangular &rhs)
: _length(rhs._length),_beg_pos(rhs.beg_pos),_next(rhs.beg_pos-1)
{};
//对,这个大括号里是空的
//rhs是类对象,这里引用了类对象且不会对类对象做任何修改
好,解读一下,开始是一个构造函数的定义,
紧接着成员初始化列表就在最后的那个单独的冒号后面。
成员初始化列表以逗号分隔,
赋值给数据成员的数值放在了数据成员名称后面的小括号里
OK,这很像在调用构造函数。
注意,最后一个冒号前面要TAB缩进,后面要加一个空格!!!!(格式问题)
成员初始化列表主要用于将参数传给成员类对象的构造函数,举个例子你就明白了
class Triangular{
public:
//...
private:
string _name;//多了个名字字符串
int _next,_length,_beg_pos;
};//别忘了右大括号和; !!!!!
我重新定义T这个类。
然后我以成员初始化列表的形式(构造函数的第二种初始化数据成员的初始化语法)将_name的初值传给构造函数的形参。如
//类主体外定义类成员函数(构造函数)
Triangular::Triangular(int len,int bp)
: _name("Triangular")
{
_length=len>0?len:1;
_beg_pos=bp>0?bp:1;
_next=_beg_pos-1;
}
好了,这下我就把_name这个数据成员给初始化掉了,利用构造函数的定义。也遵从一开始我写的成员初始化列表的代码。
析构函数
- 用户自定义的一个类成员。
- 作用:当某个类有析构函数,在其类对象结束生命周期时,由析构函数处理善后这个类对象。析构函数主要用来释放在构造函数或类对象生命周期中分配的资源(构造函数初始化了数据成员(给了数据成员初值和内存)即为分配给数据成员资源!!)。
- 析构函数的名称:类名称加上~前缀
- 析构函数没有返回值,也没有任何参数,参表是空的,所以不可能有其重载函数。
- 我举个例子来说明析构函数的定义
class Matirx{//注意class小写别搞错了!不是大写!
public:
Matirx(int row,int col)
:int _row(row),_col(col)
//运用成员初始化列表的初始化语法来定义构造函数
//(属于类的公共接口的函数)
{
//构造函数在此进行数据成员的资源的分配。
//(构造函数初始化了数据成员(给了数据成员初值和内存)
//即为分配给数据成员资源!!)
_pmat=new double[row*col];//定义一个指针指向
//通过new分配一个内存空间的矩阵
if(!_pamt)//检查析构函数是否成功分配给数据成员的资源了。
{//走了if证明没有分配成功
cerr<<"不能分配给数据成员内存空间(资源)!";
//我不会这句话的英文!抱歉!
return -1;//出去就完了,还析构什么。
}
//走到了这里证明由构造函数分配给数据成员内存空间和初值了!
}
~Matrix()//析构函数的定义
{
//析构函数进行资源的释放
delete[] _pmat;
//注意空格
//在]和_之间有一个空格。
}
private:
int _row,_col;
double* _pmat;
//注意_pmat被定义成了double型指针(或说数组首元素地址,
//因为_pmat一旦指向了new分配的数组后是自动指向
//new分配的数组首元素地址的!)
};//别忘了右大括号和; !!!!!
好了,我们知道了析构函数的定义,接下来说说析构函数的应用
{
//某函数体(可能是main()函数或其他函数)
Matrix mat(4,4);
//我定义了一个类对象,此处会有构造函数去
//处理我这个类对象里的数据成员初始化的问题
//...(其他代码)
//好了,此处会有析构函数来帮我释放在构造函数或类对象生命周期中
//分配的资源
}
至于我为什么没写出析构函数的细节,那是因为:
- 编译器会在mat这个类对象被定义出的下一刻,自动应用Matrix构造函数,你不用去管。好的,类对象的数据成员_pamt被初始化为一个指针,指向一个具有16个double型元素的数组(也是程序空闲空间的一块内存)。好,在语句块结束前,编译器会自动应用Matrix析构函数,于是该析构函数释放了_pamt所指的那块具有16个double型元素的数组。
- 当然你不要觉得构造函数和析构函数我不定义了,那是不行滴,编译器只是自动调用构造函数和析构函数而非自动定义他们!
- 作为Matirx这个类的用户不需要知道内存管理细节,所以我们看起来这种构造函数析构函数的组合写法像标准库的容器设计。
- 实际上,我们不是每个类都要配析构函数的,以我上述的Triangular类为例,_length,_beg_pos,_next这三个数据成员都是储值的方式存放(局部动态(auto)变量),他们的生命周期就是类对象被定义到类对象结束生命周期,这一段生命周期就是那三个数据成员的生命周期。到了这三个数据成员的生命周期,这三个数据成员自己就释放掉了,是不需要析构函数的!
- 所以啊,得了解何时需要定义析构函数何时不需要定义,这是个C++编程的难点。
成员逐一初始化
什么意思呢,就是当在默认情形下,我们以某个类对象作为另外一个类对象的初值,如
Triangular tri1(8);
Triangular tri2=tri1;
此时类数据成员会被依次复制。这里Triangular类的_length,_beg_pos,_next都会依次从tri1复制到tri2。这就是默认的成员注意初始化操作
- 然而某些情况下不适合用默认的成员逐一初始化操作,比如某些类的类成员有指针对象。
比如:
{
//某函数体内
Matrix mat(4,4);//定义一个Matrix类的类对象
//此处Matrix类的构造函数自动起作用
{
Matirx mat2=mat;
//上面一行进行了默认的成员逐一初始化操作
//mat2的相关操作写在这里
//...
//好,临近mat2类对象的生命周期结束时刻,
//此时mat2的析构函数自动起作用
}
//mat的相关操作写在这里
//...
//好,临近mat类对象的生命周期结束时刻,
//此时mat的析构函数自动起作用
}
注意,因为我们的Matrix类的私有成员有double* _pmat;
其中,默认成员逐一初始化会将mat2的_pmat设为mat的_pmat值:
mat2._pmat=mat._pmat;
这会让两个类对象的_pmat成员都指向了堆内存的同一个数组,当Matrix类的析构函数作用于mat2上的时候数组所占的内存空间就给释放掉了,那你mat的_pmat(mat._pmat)指向谁去?不就成了野指针?
- 怎么解决呢?
由类设计者为Matrix类(或者说为类)提供另一个拷贝构造函数。这样我们就可以解决析构函数第一次作用后,有一个类对象(或说类)的私有成员指针成员,变成了野指针的问题。 此时客户端需要重新编译,但是源代码不用更改。注意,我的拷贝构造函数会产生一个独立的副本,让类对象的私有成员指针成员去指向那个副本**(两个指针分别依次指向产生的两个副本),从而解决两指针指同一块内存空间的问题
如图所示。分开指向两个类对象各自的拷贝构造函数产生的副本,而后两个类对象各自的析构函数释放各自的拷贝构造函数产生的副本。
来,我们来看一看拷贝构造函数**(以Matrix类作为例子)的定义(类主体外定义)
Matrix::Matrix(const Matrix &rhs)//←拷贝构造函数的参表
:_row(rhs._row),_col(rhs._col)
//Matrix类的构造函数(属于类的公共接口的函数)采用成员初始化列表的初始化语法
{
int elem_cnt=_row*_col;
_pmat=new double[elem_cnt];
//这个_pmat私有成员的指针成员会指向那个数组副本。
//对rhs._pmat(rhs是类对象,
//这里引用了类对象且不会对类对象做任何修改)
//_pmat这个类私有成员所指的数组产生一份完全并且独立的数组副本,解决析构函数第一次作用后,
//有一个类对象(或说类)的私有成员指针成员,变成了野指针的问题。
for(int ix=0;ix<elem_cnt;++ix)
{
_pmat[ix]=rhs._pmat[ix];
}//产生_pmat这个私有成员指针成员所指的数组的完全并独立的副本的操作
}
- 所以,在设计类时,必须知道在此类之上进行成员逐一初始化的行为模式是否适当,如果适当,我们就不用提供拷贝构造函数,否则就必须提供拷贝构造函数并在其中编写正确的初始化操作。
- 如果有必要为某个类编写拷贝构造函数,那就同样有必要为这个类编写拷贝赋值操作符。