构造函数是类必备的成员函数,它进行着类的数据成员的初始化工作。如果我们没有定义构造函数,那声明类的对象时会调用系统自动生成的默认构造函数,这个函数什么也不做,通常这样做是有风险的,因为在声明对象时如果不对对象内的数据成员初始化的话,它们的值将是不确定的,这将导致不可预料的灾难。所以为了保险起见,任何时候都要定义一个默认的构造函数对对象数据成员进行初始化。我们定义一个存储数据的类list进行讲解:
默认构造函数
class list
{
public:
list(); //默认构造函数
~list(); //析构函数,先不考虑
int length() { return _length; }; //返回长度
void display(); //打印数组
private:
int _length; //list对象中data数组的长度
int* data; //指向数组的指针
};
list::list()
{
_length = 0;
data = nullptr;
}
类中的各项数据成员和函数见注释,这个类中我们只定义一个默认构造函数,它构造一个空的list。通常如果数据成员较少的话我们采用成员初始化列表(member initialization list)的方式在构造函数声明或定义的时候初始化,。
//list::list():_length(0),data(NULL) {}; //声明的时候
list() :_length(0), data(nullptr) {}; //定义的时候
带参数的构造函数
但我们经常会需要对对象进行特定的初始化,如构造具有num个值为value的对象,而不使用默认构造函数。这时我们需要对构造函数进行重载(c/c++语言支持),我们定义一个接受两个参数的构造函数,一个参数num指明数组大小,一个参数value指明值。
class list
{
~
~
list(int num, int value = 0);
~
~
}
list::list(int num, int value)
{
if (num <= 0)
return;
_length = num;
data = new int[num];
for (int i = 0; i < num; i++)
data[i] = value;
}
这里我们将第二个参数设为具有默认值的参数(将形参直接赋值),这样我们可以使用只传如一个参数的构造函数,这时第二个参数默认为0,它所构造的是一个存储num个值为0的对象。效果见下面代码
int main()
{
list s1(4);
list s2(4, 5);
s1.display();
s2.display();
getchar();
}
输出:
元素为:
0 0 0 0
元素为:
5 5 5 5
拷贝构造函数(copy constructor和copy assignment operator)
除此之外我们也可以使用另一个对象构造,将对象构造为数据成员与另一个对象一样,比如我们经常见到这样的使用形式:
vector<int> vec1 = { 1,2,3,4,5 };
vector<int> vec2(vec1);
vector<int> vec3;
vec3 = vec1;
我们先定义一个vec1,然后使用vec1构造vec2和vec3。这三个对象各自的数据成员的值完全相同。我们自己定义的类list也可以这样使用,看下面代码
int main()
{
list s(4,5);
list s2(s);
list s3 = s;
s2.display();
s3.display();
getchar();
}
输出:
元素为:
5 5 5 5
元素为:
5 5 5 5
此时我们并未对list定义任何新的构造函数,这时s2和s3使用的是系统自动生成的copy constructor和copy assignment operator,这非常方便,我们不用编写额外的代码就可以使用这种构造方式,但有时这样使用是有风险的,我们看下面代码:
int main()
{
list s(4,5);
list s2(s);
list s3 = s;
delete &s;
s2.display();
s3.display();
getchar();
}
我们可以尝试编译这段代码,会发现程序编译成功并不会提示警告,但运行后会发现程序错误,错在哪了呢?我们去看一下list内的数据成员,发现data是一个指向数组的指针,而我们在构造完s2和s3后,s2._length=s3._length=s._length,s2.data=s3.data=s.data,注意!这里的data为指针。也就是说此时s2和s3内的data指向s中的data指向的数组。而之后我们将s删除,s中data指向的数组被删除,这时s2和s3内的data指向的数组不存在,那我们再对s2和s3操作便出现错误!所以当类中存在指针时我们不可以使用系统自动生成的copy constructor和copy assignment operator,这时候我们需要自己定义copy constructor和copy assignment operator。见下面代码:
class list
{
public:
list() :_length(0), data(nullptr) {};
list(int num, int value = 0);
list(const list& l); //copy constructor
~list();
int length() { return _length; };
void display();
list& operator=(const list& l); //copy assignment operator
private:
int _length;
int* data;
};
list::list(const list & l)
{
_length = l._length;
data = new int[_length]; //新开辟一块内存
for (int i = 0; i < _length; i++)
data[i] = l.data[i];
}
list & list::operator=(const list & l)
{
if (&l == this) //如果l是本对象则直接返回
return*this;
_length = l._length;
data = new int[_length]; //新开辟一块内存
for (int i = 0; i < _length; i++)
data[i] = l.data[i];
return *this;
}
由此可知当定义包含指针数据成员的类时,我们也需要为它定义copy constructor和copy assignment operator。当对其中指针进行赋值时,我们新开辟一块内存。另外还有一个需注意的地方,我们看下面代码:
int main()
{
list s=3;
s.display();
}
请问此时s调用的是constructor还是assignment operator呢,答案是调用constructor,大家可以运行一下,会发现输出和调用list s(3)相同
元素为:
0 0 0
总结
综上,构造函数主要有:
- 默认构造函数
- 带参数的构造函数
- copy constructor和copy assignment operator
需要注意:
- 当我们定义类时,我们必须为类定义默认构造函数
- 当数据成员较少时可以使用初始化成员列表,这样可以提高效率
- 如果数据成员不包含指针,可以使用系统默认的copy constructor和copy assignment operator
- 当数据成员内包含指针时,需要自己定义copy constructor和copy assignment operator