构造函数
对象初始化的意义是在对象创建的时候给予它初始的值,如果不初始化,则对象的值未定义,使用未定义的对象会发生意想不到的结果,甚至会导致严重的错误,因此我们在创建一个对象时一定要对其初始化:
-
对于内置类型,初始化方式有直接初始化、默认初始化、拷贝初始化和列表初始化。
-
而对于自定义类型对象的初始化是通过构造函数来实现的,构造函数有默认构造函数对应内置类型的默认初始化、有参构造函数对应内置类型的直接初始化、拷贝构造函数对应内置类型的拷贝初始化。构造函数的任务就是初始化类对象的数据成员,无论何时,只要类的对象被创建,就会执行构造函数。
为了方便说明以下的知识,我们先定义Sales_data类。
//_bookNo是一个isbn编号
//_units_sold是书的销量
//_revenue是总销售额
struct Sales_data{
string _bookNo;
unsigned _sold=0; //类内初始值
double revenue=0.0; //类内初始值
};
1.合成默认构造函数
Sales_data total; //默认初始化,调用默认构造函数
在Sales_data类中,我们没有定义构造函数,但上述对象仍成功创建,并且调用了默认构造函数。如果我们没有显式的定义构造函数,那么编译器就会为了我们隐式的定义一个默认构造函数,称为合成的默认构造函数。
①合成的默认构造函数初始化的规则:
- 如果存在类内的初始值,用类内初始值来初始化成员。
- 如果没有类内初始值且是内置类型,采用内置函数的默认初始化规则来初始化该成员。(内置函数的初始化规则:默认初始化的内置类型变量如果定义在所有函数体之外,则值为0;如果定义在任何块之内则是未定义的,使用未定义的变量会发生未知的错误。)
- 如果没有类内初始化,且是个类类型,则调用这个类的默认构造函数来初始化。
例如:上述的Sales_data类,_bookNo没有类内初始值,且是个类类型,调用string类的默认构造函数初始化为一个空字符串。_units_sold和revenue是内置类型且存在类内初始值,则用类内初始值初始化,由于这两个对象在块内,内置类型如果没有类内初始化且在块内则会未定义,因此我们必须对其类内初始化。
合成的默认构造函数只适合非常简单的类,对于一个普通的类来说,必须定义它自己的默认构造函数。
②必须定义自己的默认构造函数的原因:
-
类不包含任何构造函数的情况下才会定义合成构造函数,一旦定义了其他构造函数如拷贝构造或有参构造函数,那么除非我们再定义一个自己的默认构造函数,我们的类就会没有默认构造函数,就不能默认初始化。
-
如上述例子所说,如果定义在块中的内置类型或复合类型的对象被默认初始化,它们的值就是未定义的,用户创建类的对象时就会得到未定义的值。
-
如果类的某些成员对象的类型本身是个没有默认构造函数的类,默认合成构造函数将无法对这个类类型的数据成员进行初始化。在上述例子中,string类型有自己的默认构造函数,因此不会发生这个问题,但不能保证其他类也有自己的默认构造函数。
2.定义构造函数
对于我们的Sales_data类,我们将定义下面四种不同的构造函数来得到一个新的Sales_data类:
struct Sales_data{
Sales_data()=default; //自定义的默认构造函数
Sales_data(const string &s):_booKNo(s){} //有参构造函数
Sales_data(const string &s,unsigned n,double p):
_bookNo(s),_sold(n),_revenue(p){} //有参构造函数
Sales_data(const Sales_data &); //拷贝构造函数
string _bookNo;
unsigned _sold=0; //类内初始值
double revenue=0.0; //类内初始值
}
2.1默认构造函数
由于我们已经定义了其他的构造函数,因此,如果我们不定义自己的默认构造函数,编译器不会为我们合成一个合成默认构造函数,因此,我们必须定义自己的默认构造函数。
//自定义的默认构造函数
Sales_data()=default; //显式的定义合成的默认构造函数
该构造函数不接受任何实参,所以它是一个默认构造函数。我们想自定义的默认构造函数和合成的默认构造函数一致,因此用 =default来显式的定义合成的默认构造函数。
合成的默认构造函数等价于如下的默认构造函数:
//等价于合成的默认构造函数
Sales_data():_sold(0),_revenue(0){}
上述是用构造函数的初始值列表来构造的,下节将详细说明。
2.2有参构造函数
构造函数接受参数,且不是只有const Sales_data&一个参数(这个是拷贝构造函数),称为有参构造函数。
Sales_data(const string&s):_bookNo(s){}
Sales_data(const string &s,unsigned n,double p)
_bookNo(s),_sold(n),_revenue(p*n){}
冒号和花括号之间的代码的部分称为构造函数的初始值列表,负责为新创建的对象的一个或几个数据成员赋初始值。
构造函数的初始值列表的初始化规则:
- 初始值列表里的成员将用括号里的值初始化成员。
- 被初始值列表忽略的成员将采用和合成默认构造一样的规则来构造。
例如:
Sales_data(const string&s):_bookNo(s){}
//_bookNo在初始值列表内,其他两个数据成员不在,等价于
Sales_data(const string&s):
_bookNo(s),_sold(0),_revenue(0){}
构造函数使用类内初始值是个好选择,这样保证为成员赋予了一个正确的值,构造函数不应该轻易覆盖类内初始值。
2.3拷贝构造函数
假设类的名字为T,则T的拷贝构造函数的形式为:
T(const T&){
//函数体
}
拷贝构造函数通过传参方式,用另一个类对象来初始化,就像内置类型的拷贝初始化一样。
Sales_data(const Sales_data&rhs):
_bookNo(rhs._bookNo),
_sold(rhs._sold),
_revenue(rhs._revenue){}
3.构造函数的初始化和赋值问题
①如果没有在构造函数的初始值列表中显式的初始化成员,则该成员将在构造函数体之前执行默认初始化,如:
#include<iostream>
using namespace std;
class A {
public:
A(int v) {
cout << _a << " " << _b << endl;
_a=1;
_b=1;
cout << _a << " " << _b << endl;
}
//构造函数初始值列表没有初始化_a和_b,在执行有参构造之前会默认初始化_a和_b.
//然后构造函数执行了函数体对_a和_b进行赋值。
private:
int _a;
int _b;
};
int main() {
A a(1);
return 0;
}
打印结果为:
可以看出,_a和_b的值都是未初始化的。构造函数是对未初始化的数据成员进行了赋值操作。
②相比于初始化,先定义、再赋值有以下缺点:
- 如果成员是const或者引用的话,必须将其初始化,否则会发生错误。
如:
#include<iostream>
using namespace std;
class A {
public:
A(int v) {
cout << _a << " " << _b << endl;
_a=1;
_b=1;
cout << _a << " " << _b << endl;
}
//构造函数初始值列表没有初始化_a和_b,在执行有参构造之前会默认初始化_a和_b.
//然后构造函数执行了函数体对_a和_b进行赋值。
private:
const int _a;
int &_b;
};
int main() {
A a(1);
return 0;
}
执行上述代码会得到如下错误:
- 初始化和赋值事关底层效率问题,先定义再赋值的效率比直接初始化低不少。(赋值操作先清除赋值号左侧对象的值,再创建一个临时对象,将创建的临时对象赋值给赋值号左侧的对象)
③成员初始化顺序**
成员初始化顺序与它们在类中定义的出现顺序一致:第一个成员先被初始化,然后第二个以此类推。构造函数初始值列表中的初始值的前后位置不同不会影响实际的初始化顺序。
如:
#include<iostream>
using namespace std;
class X{
int i;
int j;
public:
X(int val):j(val),i(j){}
//错误:i在j之前被定义,不能用j初始化i,虽然不会报错,但i的值是未定义的。
};
int main() {
X a(1);
return 0;
}
从执行结果可以看出i的值实际是未定义的,因为i使用未定义的j的值初始化的。
4.委托构造函数
一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说把它自己的一些职责委托给了其他构造函数。
例如:
#include<iostream>
using namespace std;
class A {
int _a;
int _b;
public:
A(int val) :_a(val), _b(val) {}
A() :A(1) {} //默认构造函数全部委托给了有参构造函数
void print(){
cout << _a << " " << _b << endl;
}
};
int main() {
A a;
a.print();
return 0;
}
打印结果为: