在编写一个类时,有时候我们需要为类编写构造函数、拷贝构造函数、赋值函数、析构函数。那么这些函数在何时被调用呢?这篇文章将通过实验进行验证。
(编译器Visual Studio 2012)
一、首先来看下面的代码:
#ifndef _CLASS_CONSTRUCTOR_H_
#define _CLASS_CONSTRUCTOR_H_
#include<iostream>
using namespace std;
class Class_Constructor
{
public:
Class_Constructor() { cout<<"无参数的构造函数,member的初始值为:"<<member<<endl; }
Class_Constructor(int i) : member(i)
{
cout<<"有参数的构造函数,member的初始值为:"<<member<<endl;
}
Class_Constructor(const Class_Constructor& obj)
{
member = obj.member;
cout<<"拷贝构造函数,member的初始值为:"<<member<<endl;
}
Class_Constructor& operator=(const Class_Constructor& obj)
{
member = obj.member;
cout<<"赋值函数,member的值为:"<<member<<endl;
return *this;
}
~Class_Constructor() { cout<<"析构函数"<<endl; }
int member;
};
#endif
在上面的代码中,我们定义了一个名为Class_Constructor的类,并为该类定义了默认构造函数、带参数的构造函数、拷贝构造函数、赋值函数、析构函数。然后我们使用一下这个类,可以看出这些函数都是在什么时候被调用的。代码如下:
#include"Class_Constructor.h"
int main()
{
//用默认构造函数进行初始化
Class_Constructor obj1;
//用有一个参数的构造函数进行初始化
Class_Constructor obj2(99);
//用拷贝构造函数进行初始化
Class_Constructor obj3 = obj2;
Class_Constructor obj4(obj1);
//调用赋值函数
obj1 = obj2;
return 0;
}
在上面的代码中,我们用默认构造函数初始化obj1,用带参数的构造函数初始化obj2,用拷贝构造函数初始化obj3、obj4,最后用赋值函数为obj1重新赋值。运行得到的结果如下结果:
通过上面例子不难看出,obj1是由默认构造函数进行的初始化,由于我们并未给obj1.member赋值,它的值是未定义的;obj2由带参数的构造函数进行初始化;obj4由拷贝构造函数进行初始化;obj3的初始化看起来很像赋值运算,但他实际并没有调用赋值函数,而是由拷贝构造函数进行初始化;再次对obj1赋值时,才调用了赋值函数;最后在程序退出之前,析构函数被调用。
二、进一步深入的例子
我们现在将Class_Constructor类修改如下:
#ifndef _CLASS_CONSTRUCTOR_H_
#define _CLASS_CONSTRUCTOR_H_
#include<iostream>
using namespace std;
static int index = 0;
class Class_Constructor
{
public:
Class_Constructor()
{
index++;
m_index = index;
cout<<m_index<<"——>无参数的构造函数,member的初始值为:"<<member<<endl;
}
Class_Constructor(int i) : member(i)
{
index++;
m_index = index;
member = 2;
cout<<m_index<<"——>有参数的构造函数,member的初始值为:"<<member<<endl;
}
Class_Constructor(const Class_Constructor& obj)
{
index++;
m_index = index;
member = obj.member;
cout<<m_index<<"——>拷贝构造函数,member的初始值为:"<<member<<endl;
}
Class_Constructor operator=(const Class_Constructor obj)
{
member = obj.member;
cout<<m_index<<"——>赋值函数,member的值为:"<<member<<endl;
return *this;
}
~Class_Constructor() { cout<<m_index<<"——>析构函数"<<endl; }
int member;
int m_index;
};
#endif
在上面的代码中,我们做了三个改动:
1、新加了一个名为index的全局变量,用来表示被创建对象的序号,并保存在每个对象的m_index字段。
2、在带有一个参数的构造函数中,增加了一条赋值语句,对member重新赋值。
3、将operator=函数的入参和返回值都改为非引用的形式。
使用修改后的Class_Constructor类的代码如下:
#include"Class_Constructor.h"
int main()
{
//用默认构造函数进行初始化
Class_Constructor obj1;
//用有一个参数的构造函数进行初始化
Class_Constructor obj2(99);
//用拷贝构造函数进行初始化
Class_Constructor obj3 = obj2;
Class_Constructor obj4(obj1);
cout<<"---------------"<<endl;
//调用赋值函数
obj1 = obj2;
return 0;
}
执行结果如下图:
通过上面代码,可以得到如下结论:
1、初始化列表的执行顺序,先于函数体内部的程序。
2、如果函数的入参对象或返回对象,不是对象的引用形式,程序得到的将是入参对象或返回对象的副本。
三、总结
三、总结
1、初始化列表的执行顺序,先于函数体内部的程序。
2、赋值形式的初始化,其实是调用了拷贝构造函数。可见赋值和初始化是两个完全不同的过程。
3、如果函数的入参对象或返回对象,不是对象的引用形式,程序得到的将是入参对象或返回对象的副本。
4、我们在编写拷贝构造函数、赋值函数时,函数的入参应尽量用const的引用形式。这样做既可以表明函数的意图,又可以减少对象的拷贝。
5、赋值函数的返回类型也应尽量用引用形式。这样做可以让自定义的赋值运算符=的表现力更像内置运算符=,同时可以减少对象的拷贝。