构造函数主要包括:默认构造函数、普通构造函数、拷贝构造函数三种。
构造函数经常与赋值函数混淆,这里放在一起讲,便于区分。
首先初始化一个简单的类作为例子讲解:
class A {
public:
A() {
v = 1;
printf("默认构造(无参数)\n");
}
A(int t = 1) {
v = 1;
printf("默认构造(有参数,且参数有默认值)\n");
}
A(int t) {
v = t;
printf("普通构造1\n");
}
A(int t,int f) {
v = t;
m = f;
printf("普通构造2\n");
}
//默认构造函数只能有一个,当有参默认构造函数的参数列表与普通构造函数的参数列表一样时,两者也只能存在一个。
A(const A& a) {
this->v = a.v;
printf("拷贝构造\n");
}
A& operator= ( A& a) {
this->v = a.v;
printf("赋值函数\n");
return *this;
}
~A() {
printf("析构函数\n");
}
int v;
int m;
};
①准确的说,默认构造函数就是在调用时不需要显示地传入实参的构造函数。
②一个类只能有一个默认构造函数。
当类中没有定义任何构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空。
此外,用户可自定义默认构造函数如下。
实现形式:
A() {
//可以为空,为空则与编译器生成的默认构造函数相同
}
A(int t = 1) {
v = t;
}
上述两种默认构造函数不能同时存在,否则在调用默认构造函数时会引起二义性。
调用形式:
int main() {
A a;//默认构造函数
return 0;
}
当我们同时定义一个带默认参数的默认构造函数和一个不带参数的默认构造函数之后(一般不这样做,两种构造函数不应被同时定义),在定义一个类对象时,如果不传递参数,会出现二义性错误。因为我们没有传递参数时,系统默认又两种选择,一种是调用无参的默认构造函数,另一种是调用带参默认构造函数并传递默认参数。
class A
{
public:
A()
{
v = 0;
}
A(int r = 0)
{
v = r;
}
int v;
};
int main()
{
A c;//error: call of overloaded ‘Complex()’ is ambiguous
return 0;
}
会报错:error: call of overloaded ‘Complex()’ is ambiguous
1.去掉无参默认构造函数。
class A
{
public:
A(int r = 0)
{
v = r;
}
int v;
};
这时,当我们不给构造函数传递参数时,系统会调用有参默认构造函数并传递默认参数对类数据成员进行初始化。
int main() {
A a;
//相当于
A a(0);//有参默认构造函数
return 0;
}
2.去掉带参默认构造函数的默认参数
class A
{
public:
A()
{
v = 0;
}
A(int r) //去掉带参构造函数的默认参数
{
v = r;
}
int v;
};
此时带参默认构造函数就变成了普通构造函数。
当类中没有定义任何拷贝构造函数时,编译器默认提供一个默认拷贝构造函数,简单的进行成员变量的值的赋值。这里涉及到深拷贝和浅拷贝的知识,将在拷贝构造函数小节详述。
实现形式:
//通过参数列表的不同进行重载
A(int t) {
v = t;
}
A(int t,int f) {
v = t;
m = f;
}
A(int t): v(a) {}//普通构造函数1的参数列表形式
A(int t,int f): v(a), m(f) {}//普通构造函数2的参数列表形式
调用形式:
int main() {
A a(1);//普通构造函数1
A a(1,1);//普通构造函数2
return 0;
}
实现形式:
A(const A& a) {
this->v = a.v;
printf("拷贝构造\n");
}
调用形式(三种情况):
int main() {
A a(1);
A b = a;//拷贝构造函数形式一
A c(a);//拷贝构造函数形式二,最好写成这种形式
return 0;
}
void g(A ret) {
printf("函数作为参数\n");
}
int main() {
A a(1);
g(a);//作为参数传入函数时调用拷贝构造函数
return 0;
}
A f() {
A ret(3);
return ret;
}
int main() {
A a(1);
A b = f();//以值传递方式从函数返回时调用拷贝构造函数
return 0;
}
浅拷贝,即在定义一个类A,使用类似A obj; A obj1(obj);或者A obj1 = obj; 时候,由于没有自定义拷贝构造函数,C++编译器自动会产生一个默认的拷贝构造函数。这个默认的拷贝构造函数采用的是“位拷贝”(浅拷贝),而非“值拷贝”(深拷贝)的方式,如果类中含有指针变量,默认的拷贝构造函数必定出错。
浅拷贝:也就是在对象复制时,只是对对象中的数据成员进行简单的赋值,如果对象中存在动态成员,即指针,浅拷贝就会出现问题,下面代码:
class A
{
public:
A() // 构造函数,p指向堆中分配的一空间
{
m_data = new char(100);
printf("默认构造函数\n");
}
~A() // 析构函数,释放动态分配的空间
{
if (m_data != NULL)
{
delete m_data;
m_data = NULL;
printf("析构函数\n");
}
}
private:
char* m_data; // 一指针成员
};
int main()
{
A a;
A b(a); // 拷贝构造
return 0;
}
分析:由于没有拷贝构造函数,走编译器默认的拷贝构造函数,A b(a); 进行对象析构时,会造成释放同一内存空间2次。
对于深拷贝,针对成员变量存在指针的情况,不仅仅是简单的指针赋值,而是重新分配内存空间,如下:
class A
{
public:
A() // 构造函数,p指向堆中分配的一空间
{
m_pdata = new char(100);
printf("默认构造函数\n");
}
A(const A& r)
{
m_pdata = new char(100); // 为新对象重新动态分配空间
memcpy(m_pdata, r.m_pdata, strlen(r.m_pdata));
printf("深拷贝构造函数\n");
}
~A() // 析构函数,释放动态分配的空间
{
if (m_pdata != NULL)
{
delete m_pdata;
printf("析构函数\n");
}
}
private:
char* m_pdata; // 一指针成员
};
int main()
{
A a;
A b(a); // 拷贝构造
return 0;
}
赋值函数不属于构造函数,它是“=”运算符的重载函数。只有两个已经声明的对象之间(两个已经在内存中存在的对象)才能进行赋值。
实现形式:
A& operator= ( A& a) {
if (this != &a)
{
this->v = a.v;
}
printf("赋值函数\n");
return *this;
}
①返回类型为自身类的引用,使得赋值函数可以联等赋值。
A t2,t3;
t2=t3=t1;//假设t1是已被初始化的对象
②形参为类的引用,可以避免重复调用拷贝构造函数,const可以保护传入对象自身的安全。
调用形式:
int main() {
A a(1);
A b;
b = a;//赋值函数
return 0;
}
//初始化方法1
string a;//调用无参默认构造函数,初始化后对象a内的字符串成员为空。
//初始化方法2
string a = "abc";//先调用普通构造函数生成一个匿名string对象,再调用拷贝构造函数把匿名对象拷贝给a。
//该语句等同于:
string a(string("abc"));
//初始化方法3
string a("abc");//调用普通构造函数
//初始化方法4
string a(b);//调用拷贝构造函数
//初始化方法5
string a = b;//调用拷贝构造函数
//方法6 (a,b已初始化)
a = b;//调用赋值函数