背景知识
编译器默认生成函数的规则如下:
1.定义一个类时,如果自己没有声明,那么编译器会自动帮助生成一个拷贝构造函数(copy construction),赋值操作符(copy assignment),析构函数(deconstruction)。
2.如果没有声明任何构造函数(包括拷贝构造函数),编译器会帮助声明一个默认构造函数。
构造函数(包括编译器生成的默认构造函数)的执行包括两个阶段:
1.初始化阶段
2.构造函数体内代码执行构造的阶段
构造函数执行的两个阶段非常重要,在初始化阶段,如果类中存在类类型的成员变量,那么会调用这个类类型的成员变量的默认构造函数来初始化这个成员变量,如果是内置类型,那么可能会对内置类型的变量初始化,也可能不会对其初始化(这点和内置类型的变量是全局变量还是局部变量有关系)。
弄明白了初始化阶段的工作后就明白了构造函数体内执行的代码都是赋值操作而不是初始化操作了。因为初始化操作在构造函数体的代码执行之前就已经完成了。我们也可以对初始化阶段的工作进行控制,这就是C++中的构造函数初始化列表。C++中构造函数初始化列表就是控制初始化阶段的,如果没有提供初始化列表,那么会调用成员变量中的默认构造函数来初始化成员变量。
1.构造函数执行的两个阶段
以一个例子来感受下构造函数执行的两个阶段,自定义两个类,让其中一个类作为另外一个类的成员变量:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
class Bitmap
{
public:
//默认的构造函数
Bitmap()
{
std::cout<<"bitmap construction:"<<this<<std::endl;
}
//拷贝构造函数
Bitmap(const Bitmap & rhs)
{
std::cout<<"bitmap copy construction"<<std::endl;
}
//赋值操作符
Bitmap& operator=(const Bitmap & rhs)
{
std::cout<<"operator assignment:"<<this<<" = "<<&rhs<<std::endl;
}
};
class Widget
{
public:
//自定义的构造函数,用来测试构造函数执行的两个过程,为了减少一次Bitmap拷贝构造函数的调用,这里传递引用
Widget(Bitmap &bitmap)
{
b=bitmap;
std::cout<<"Widget(Bitmap &b) construction:"<<&b<<std::endl;
}
//因为自己定义了构造函数,所以编译器不会再为我们生成构造函数了,所以需要自己定义一个默认的构造函数
Widget()
{
std::cout<<"Widget construction:"<<&b<<std::endl;
}
private:
Bitmap b;//成员变量为类类型时在构造函数中才会调用Bitmap的构造函数
};
int main()
{
Bitmap bb;
Widget w1(bb);
return 0;
}
上面的代码执行后的结果如下:
其中:
Bitmap bb; 这行代码执行后打印出bitmap construction:ox28ff2f,表示创建的这个对象bb的地址是0x28ff2f
Widget w1(bb);这行代码执行w1的构造函数包括两个阶段:
1.初始化阶段,调用w1的成员变量Bitmap b的默认构造函数初始化对象b,这时又会调用一次构造函数来创建Widget的成员变量b,它的地址是0x28ff2e。
2.构造阶段,调用w1的第一个构造函数内的执行代码:
b=bitmap;//此时会调用Bitmap的赋值操作符,打印出operator assignment:ox28ff2e=0x28ff2f,这里注意ox28ff2e是初始化阶段生成的对象的地址,0x28ff2f是我们传递给Widget构造函数实参的地址。赋值操作只是将bitmap对象的内容拷贝到b对象中,不会更改b对象的地址。
然后执行输出操作,打印出来的是在初始化阶段创建的对象的地址
如果将Widget中Bitmap对象的初始化工作放到构造函数初始化列表中执行,那么就只会有一次拷贝构造函数的调用,而不会像上面调用一次默认构造函数,再调用一次赋值操作符了。代码如下:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
class Bitmap
{
public:
//默认的构造函数
Bitmap()
{
std::cout<<"bitmap construction:"<<this<<std::endl;
}
//拷贝构造函数
Bitmap(const Bitmap &rhs)
{
std::cout<<"bitmap copy construction:"<<this<<std::endl;
}
//赋值操作符
Bitmap& operator=(const Bitmap & rhs)
{
std::cout<<"operator assignment:"<<this<<" = "<<&rhs<<std::endl;
}
};
class Widget
{
public:
//自定义的构造函数,用来测试构造函数执行的两个过程,为了减少一次Bitmap拷贝构造函数的调用,这里传递引用
Widget(Bitmap& bitmap):b(bitmap)
{
std::cout<<"Widget(Bitmap &b) construction:"<<&b<<std::endl;
}
//因为自己定义了构造函数,所以编译器不会再为我们生成构造函数了,所以需要自己定义一个默认的构造函数
Widget()
{
std::cout<<"Widget construction:"<<&b<<std::endl;
}
private:
Bitmap b;//成员变量为类类型时在构造函数中才会调用Bitmap的构造函数
};
int main()
{
Bitmap bb;
Widget w1(bb);
return 0;
}
输出结果如下:
Bitmap bb;//这条语句会调用默认的构造函数
Widget w1(bb);//因为Widget带有Bitmap参数的构造函数中使用了初始化列表,所以在初始化阶段调用了拷贝构造函数来创建成员变量b,它的地址是ox28ff2e。
2.如果自定义了构造函数,那么一定也要加上默认构造函数
其实上面的例子已经可以说明问题了,就是Widget类中的Bitmap成员变量在初始化阶段自动调用了默认的构造函数,如果一个类中不存在默认的构造函数(即自己定义了构造函数但是忘记添加默认的构造函数),那么这个类是其他类的成员变量时必须在构造函数初始化列表中提供参数进行初始化,否则会由于在初始化阶段需要调用默认构造函数而却没有而导致构造失败。还是写上一个例子,在上面的类中在加上一个Pane类,注意Widget类的构造函数也做了一些改变:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
class Bitmap
{
public:
//默认的构造函数
Bitmap()
{
std::cout<<"bitmap construction:"<<this<<std::endl;
}
//拷贝构造函数
Bitmap(const Bitmap &rhs)
{
std::cout<<"bitmap copy construction:"<<this<<std::endl;
}
//赋值操作符
Bitmap& operator=(const Bitmap & rhs)
{
std::cout<<"operator assignment:"<<this<<" = "<<&rhs<<std::endl;
}
};
class Widget
{
public:
//注意这里没有传递引用了,不然下面Pane类的初始化列表就不能那样写了
Widget(Bitmap bitmap):b(bitmap)
{
std::cout<<"Widget(Bitmap &b) construction:"<<&b<<std::endl;
}
private:
Bitmap b;//成员变量为类类型时在构造函数中才会调用Bitmap的构造函数
};
class Pane
{
public:
//由于Widget没有默认的构造函数,所以需要在Pane的构造函数初始化列表中提供参数进行
//初始化,这里没有为其提供初始化式,会导致构造失败
//把下面的注释解开就是正常的写法了,提供了w类的初始化式
Pane()//:w(Bitmap())
{
std::cout<<"pane construction:"<<this<<std::endl;
}
private :
Widget w;
};
int main()
{
Pane p;
return 0;
}
当Pane不提供初始化式时,即向上面那样,编译器会提示报错:
当将Pane类的初始化列表中的注释去掉后就可以正常构造了,执行结果如下:
3.引用类型,const类型的成员变量必须在初始化列表中进行初始化
由于C++中引用类型和const类型必须在声明的时候就进行初始化,并且它不能再指向其他变量。所以根据上面构造函数初始化阶段的说明,引用类型和const类型必须在初始化列表中初始化,不能写在构造函数内。关于const关键可以参考之前的这篇博客。
template<class T>
class NamedObject
{
public:
//由于str和objectVale为引用或const类型,所以必须在初始化列表中进行初始化
NamedObject(std::string & s,const T &t)
:str(s),objectValue(t)
{
//str=s;//不能在函数体内对引用类型和const类型初始化,函数体内的是赋值操作而不是初始化操作
std::cout<<"const invoke!"<<std::endl;
}
private:
std::string & str;
const T objectValue;
};
4.引用类型,const类型不能使用编译器默认生成的赋值操作符
由于编译器默认生成的赋值操作符只是简单的执行赋值操作,而引用类型或const类型的变量是不能指向其它变量的,所以如果一个类中存在引用或const类型的变量,那么需要自己定义赋值操作符,否则会出现编译错误。同样以上面的那个类为例,必须为上面的那个类自己定义赋值操作符,否则编译都会通不过:
template<class T>
class NamedObject
{
public:
//由于str和objectVale为引用或const类型,所以必须在初始化列表中进行初始化
NamedObject(std::string & s,const T &t)
:str(s),objectValue(t)
{
std::cout<<"const invoke!"<<std::endl;
}
//由于成员变量中存在引用和const类型,所以必须要
//自定义operator=操作符,因为引用和const类型都不能
//重新赋值
NamedObject& operator=(const NamedObject & rhs)
{
std::cout<<"operator= invoke!"<<std::endl;
}
private:
std::string & str;
const T objectValue;
};
int main()
{
//注意const char *只能转换成std::string而不能转换成std::string&,所以下面两步不能合并成一步
std::string s="hello";
NamedObject<int> obj1(s,1);
NamedObject<int> obj2(obj1);
obj2=obj1;
return 0;
}
5.类中存在指针类型的对象
当类中存在指针类型的变量时,需要注意编译器不会自动为其调用类的构造函数,这时需要手动设置指针为NULL或调用new为其动态创建对象。
同时也需要注意编译器自动创建的赋值操作符和拷贝构造函数是否符合自己的要求了。因为编译编译器自动创建的赋值操作符和拷贝构造函数只是简单的进行赋值操作。
下面是一个易懂但是不具备异常安全和自我赋值安全的赋值操作符定义:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
class Bitmap
{
public:
Bitmap()
{
std::cout<<"bitmap construction:"<<this<<std::endl;
}
};
class Widget
{
public:
Widget()
{
pb=new Bitmap();
std::cout<<"Widget construction:"<<this<<std::endl;
}
//注意这种写法是异常不安全和自我赋值不安全的,下面会有改进措施
Widget & operator=(const Widget & w)
{
delete pb;
pb=new Bitmap(*w.pb);//注意.运算符优先级比*运算符高
std::cout<<"operator = "<<std::endl;
return *this;
}
private:
Bitmap *pb;//类型为指针时不会调用Bitmap的构造函数
};
int main()
{
Widget w1;
Widget w2;
w2=w1;
return 0;
}