C++构造函数
在建立一个对象时,通常最需要立即做的工作是初始化对象,如对数据成员赋初值。为了解决对象初始化的问题,C++提供了构造函数来处理对象的初始化。
(一)
构造函数是一种特殊的成员函数,与其它成员函数不同,它不需要人为调用,而是建立对象时自动被执行。C++规定构造函数的名称与类的名称相同,并且不能指定返回类型。
比如:
Data (int i,int j) //构造函数
{
a=i;b=j;
}
Data () //构造函数
{
a=1;b=2;
}
定义对象的形式:Data A(1,2); //定义Data对象A,调用构造函数初始化
Data A; //构造函数没有形参时候
1)构造函数初始化列表
构造函数可以包含一个构造函数初始化列表:
类名(形参)
:构造函数初始化列表 //初始化阶段
{ //计算阶段
函数体
}
构造函数分为两个阶段进行:初始化阶段与计算阶段。初始化阶段由构造函数初始化列表组成,计算阶段由构造函数函数体的所有语句组成,初始化阶段先于普通的计算阶段。使用初始化列表的构造函数初始化数据成员;而没有定义初始化列表的构造函数版本在函数体中对数据成员赋值。
不管成员是否在构造函数函数初始化列表中显式地初始化,类的成员对象初始化总是发生在计算阶段之前。
注意:构造函数和其他成员函数一样,可以定义在类的内部或者外部,但是构造函数的初始化列表只在构造函数的定义中而不是在函数原型声明中指定。
关于构造函数初始化列表的几点说明:
a)有时必须使用构造函数初始化列表
如果没有为类类型的数据成员提供初始化表,编译器会隐式使用该成员的默认构造函数。如果没有那个类的默认构造函数,编译器回报错。这种情况下,必须提供初始化列表。
2)构造函数重载与带默认参数的构造函数
Point(){x=y=0;}
Point(int a,int b):x(a),y(b){}
Point m,n(1,2);
系统根据对象建立的形式确定对应的构造函数。
Point(int a=0,int b=0):x(a),y(b){} //带默认参数的构造函数
Point k ,m(1),n(1,2);
定义k时候没有给出实参,默认为构造函数中的默认参数;同理m(1),形参a=1,b=0。
从上面可以看到在构造函数中使用默认参数方便且有效,它提供了建立对象时的多个选择,作用相当于多个重载的构造函数。
3)默认构造函数
定义对象时没有提供初始化式,就会使用默认构造函数,定义默认构造函数的一般形式:
类名()
{
函数体
}
任何类有且只有一个默认构造函数。如果定义的类中没有显示定义任何构造函数,编译器会自动为该类生成默认构造函数,称为合成默认构造函数。合成默认构造函数使用与变量初始化相同的规则来初始化成员。具有类类型的成员通过运行各自的默认构造函数来进行初始化。通常在默认构造函数中给成员提供的初始值应该指出该对象是“空的”,即按二进制位置0。
一个类哪怕只定义一个构造函数,编译器也不会再生成默认构造函数,换言之,如果为类定义一个带参数的构造函数,还想要无参数的构造函数,就必须自己定义它。
一般地,任何一个类都应该定义一个默认构造函数。因为很多情况下,默认构造函数是编译器隐式调用的。下面是一个例子:
class Data{
public:
Data(string str):s1(str){}
private:
string s1;
};
class Data1{
public:
Data1(){} //错误,Data对象没有合适的默认构造函数可用
private:
Data one;
};
只要Data1类的构造函数初始化列表中没有形如one("hello")之类的初始化式,则Data1类的构造函数总是错的。因为Data1类的构造函数试图使用Data的默认构造函数,但Data没有默认构造函数。
4)隐式类型转换(其他类型转换为类类型)
class Data{
public:
Data(const string& str=""):s1(str){}
void SetString(const Data& r){s1=r.s1;}
private:
string s1;
}
Data one;
string str="hello";
one.SetString(str);
编译器使用接收string实参的Data构造函数从str生成一个新的Data对象。
可以禁止由构造函数定义的隐式转换,方法是通过将构造函数声明为explicit,来防止在需要隐式转换的上下文使用构造函数:
class Data{
public:
explicit Data(const string& str=""):s1(str){}
void SetString(const Data& r){s1=r.s1;}
private:
string s1;
}
one.SetString(str); //错误,构造函数必须是显式的
one.SetString(Data(str)); //正确,显式地构造对象
C++关键字explicit用来修饰类的构造函数,指明该构造函数是显式的。explicit关键字只能用于类的内部构造函数声明上,在类定义外部不能重复它。
一般地,除非有明显的理由想要定义隐式转换,否则单形参构造函数应该为explicit。将构造函数设置为explicit可以避免错误,如果真需要转换,可以显式构造对象。
(二)复制构造函数
只有单个形参,而且该形参是对本类类型对象的引用常量,这样的构造函数称为复制构造函数:
类名(const 类名 & obj)
{
}
例子:
class Point{
public:
Point():x(0),y(0){} //默认构造函数
Point(const Point& r):x(r.x).y(r.y){} //复制构造函数
Point(int a,int b):x(a),y(b){} //带参数的构造函数
Point(const string& str); //带参数的构造函数
private:
int x,y;
}
Point:Point(const string& srt)
{ //从“x,y”形式的字符串中解析出x和y
char buf[100];
int loc=str.find(','),ylen=str.size()-loc-1;
str.copy(buf,loc,0);buf[loc]=0;x=atoi(buf);
str.copy(buf,ylen,loc+1);buf[ylen]=0;y=atoi(buf);
}
复制构造函数有且只有一个本类类型对象的引用形参,通常使用const限定。形参声明为引用类型可以减少时间和空间开销,使用const是必需的,因为复制构造函数只是复制对象,没有必要改变传递过来的对象的值。复制构造函数的功能是利用一个已知的对象来初始化一个被创建的同类的对象。
与复制构造函数对应的对象的定义形式为:
类名 对象名(类对象)
Point b(1,2);
Point c(b);
1)合成复制构造函数
每个类必须有一个复制构造函数。如果没有定义复制构造函数,编译器会自动合成一个,称为合成复制构造函数。与合成默认构造函数不同,即使定义了其他构造函数,也会合成复制构造函数
2)何时使用复制构造函数
a)用一个对象显式或隐式初始化另一个对象时,即复制初始化时。
C++支持两种初始化形式,复制初始化与直接初始化。复制初始化使用等号(=),而直接初始化将初始化式放在圆括号中。
当用于类类型对象时,复制初始化与直接初始化是有区别的:直接初始化直接调用与实参匹配的构造函数,而复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将临时对象复制到正在创建的对象。
Point direct; //直接调用默认构造函数
Point d(1,3); //直接调用Point(int a,int b)构造函数
Point copy=Point(); //复制初始化,调用默认构造函数
Point d1="lzb"; //复制初始化,调用Point(const string &str)构造函数
b)函数参数按值传递对象时或函数返回对象时
当函数形参为对象类型,而非指针或引用类型时,函数调用按值传递对象,即编译器调用复制构造函数产生一个实参对象副本传递到函数中。
类似地,当以对象类型作为返回值时候,编译器调用复制构造函数产生一个return语句中的值的副本返回到调用函数。
Point x,y,c;
c=fun(x,y);
c)根据元素初始化式列表初始乎数组元素时
如果没有为类类型数组提供元素初始化式,则将使用默认构造函数初始化每一个元素。然而,如果使用常规的大括号的数组初值列表形式来初始化数组时,则使用复制初始化来初始化每一个元素。
总的来说,正是有了复制构造函数,函数才能传递对象与返回对象,对象数组才能用初值列表的形式初始化。
3)深复制与浅复制
浅复制就是仅仅复制指针变量;深复制是复制指针对应的内容。
(三)总结
1)构造函数的作用是初始化数据成员。如果类的数据成员是const对象,类类型对象,那这些对象的初始化只能在初始化列表中进行。
2)构造函数决定了对象定义时的形式。
3)类有重载的构造函数,或者类带默认参数的构造函数时候,需要注意不能有冲突的构造函数形式。
4)只要自己定义了任何形式的构造函数,则编译器不会合成默认构造函数。如果需要默认构造函数,那需要显式定义无参数的构造函数。
5)除非自己定义复制构造函数,否则类总是会合成一个复制构造函数。复制构造函数的形式为:
类名(const 类名 &obj)
当用一个对象初始化另一个对象,函数参数使用对象,函数返回使用对象或运算中出现临时对象时,要求类必须要有复制构造函数(无论合成还是自定义的)。
6)单个参数的构造函数形式为:
类名(const 指定数据类型是&obj)
可以实现将指定数据类型转换为类类型。
7)对象赋值和对象复制概念上,实现上,形式上是不同的。
对象赋值是对一个已经存在的对象赋值,因此必须先定义被赋值的对象,才能进行赋值。而对象的复制则是从无到有建立一个新对象,并且使它与一个已有对象完全相同(包括对象的结构与成员值)。
对象的赋值是因为类重载了赋值运算符,因为任何类默认都会重载赋值运算符,因此任何类的的对象都可以赋值。对象的复制是因为类有复制构造函数,因此任何类的对象在定义时都可以使用复制初始化,函数形成与返回可以是类对象。
Data a,b;
a=b; //对象赋值
Data c(a),d=b; //对象复制
Data fun(Data x,Data y); //函数原型,对象复制