由于在内置类型中,初始化和赋值不进行区分并不会造成很大的影响,所以让我很大程度上忽略了他们的差别。
直到C++primer中对于构造函数提出了一句话,构造函数有一个初始化部分和一个函数体,在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。让我第一次开始思考初始化和赋值操作之间的区别。
实际上,我们要注意很重要的一点,我们平时的对于构造函数中使用的“赋值”写法实际上并没有对于类内的成员进行初始化,如果类内的成员是已经定义默认构造函数的非内置类型,像string类型,那么我们即使用的是“赋值”写法,那么我们没有进行初始化操作也没有问题,因为默认构造函数会帮助我们完成构造过程,但是如果是没有定义默认构造函数的内置类型,编译器是是不负责初始化的。而对于一些非内置类型的话,用赋值的写法理论上也没有问题,因为实际上也会被初始化。但是为了避免出错,我们都建议用初始化列表的方式先进行初始化。
首先我们看构造函数的两种写法
“赋值”写法:
//例1:构造函数的一种写法,虽然合法但比较草率,没有使用构造函数初始值
Sales_data::Sales_data(const string &s,unsigned cnt,double price){
bookNo=s;
units_sold=cnt;
revenue=cnt*price;
}
然后是初始化列表初试化类对象
//例2:
Sales_data(const std::string &s,unisigned n,double p):bookNo(s),units_sold(n),revenue(p*n){}
对比两种构造函数的写法,在第一种情况下,如果类内的成员都是内置类型的话,是不会进行初始化的,而在第二种情况下,用初始化列表的情况下(实际上我们可以看得到就是我们在使用初始化列表的情况下,用括号进行初始化,对于一些非内置类型的其实是调用了各个类型的复制构造函数来进行初始化的,这些类型不经过初始化是无法使用的,因而我们说对于这些成员,初始化和赋值之间的区别是巨大的,而对于一般的内置类型,初始化和赋值是没有明显区别的)
当我们在定义变量时习惯立即对其进行初始化,而非先定义再赋值
string foo="helloworld";//定义并初始化
string bar;//默认初始化成空string对象
bar="helloworld";//为bar赋一个新值
就对象的数据成员而言,初始化和赋值也有类似的区别,如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化。
实际上对于一些特定变量(const变量和引用型变量),对于这些变量,是不允许赋值操作的:
class ConstRef{
...//省略掉其余的成员
private:
int i;
const int ci;
int &ri;
};
//构造函数部分,这样写显然是要报错的,因为const和引用对象是无法赋值的,只能进行初始化
<pre name="code" class="cpp">ConstRef::ConstRef(int ii){
i=ii;
ci=ii;
ri=i;
}
所以在这种情况下,我们就只能列表初始化的方式来书写构造函数,即
ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(ii){}
实际上,我们在一些情况下对比上面的两个例子中的两个构造函数,就可以发现赋值和初始化两个操作的区别在构造函数中激化的是比较严重的,上面提到的const和引用变量是第一种例子,实际上还有一种情况我们在讲合成得默认构造函数的局限性的时候也有提到。
在C++primer P236页中曾经提到合成得默认构造函数的局限性的时候,提到了第三点,当对于类A,如果程序员定义了构造函数,也就是编译器此时并不会生成合成默认构造函数,在没有显式指出默认构造函数的时候,如果我们在类B中将类A作为类B的成员的时候,那么对于类B就无法生成合成的默认构造函数,因为在这种嵌套的方式定义的B类中,B类中要想生成合成默认构造函数,实际上是必须要依赖于默认构造函数的。
实际上,如果我们要在B类中写构造函数,如果想要采用上面例1的方式,用等号来处理的话,是不可以的!因为就像上文所说的,如果像上面例1的方式来处理,那么在函数体之前需要对类B(包含类B中的类A)进行初始化的(注意,不是赋值),而这种初始化是依赖于默认构造函数的!所以我们如果在此时使用所谓“赋值”的写法企图达到初始化的目的是不可能的。而没有进行初始化的相关成员是无法使用的,所以这种时候就只能用列表初始化的方式来书写构造函数。如下:
class A {
public:
A(int p) { ... }
//省略了一些成员
};
class B {
A m;//同样的省略了一些成员
public:
B();
};
//此时对于类B就只能用列表初始化的方式来写构造函数
B::B(): m(2)
{
...
}
在上面我们就可以看到列表初始化的好处所在,因为列表初始化是依赖于复制构造函数的,而不管我们有没有在类中定义这个类的复制构造函数,类中都会自己生成一个默认复制构造函数,而如果我们想要用”赋值“的方式妄图实现所谓的初始化的话,其实这种初始化时依赖于默认构造函数的(也就是没有参数的构造函数,或者是那种在参数表中指明了自己可设可不设参数的构造函数),因此在程序员自己设置了任何一个有参数的默认构造函数且并没有自己定义默认构造函数的时候就会出现问题。
以上是推荐我们使用列表初始化方式进行初始化的原因之一
其二在于如果我们采用赋值的方式妄图进行初始化的话,对于一些非内置类型进行的步骤就是先进行初始化后进行赋值,而如果仅仅是进行列表初始化的话,就只会进行初始化这一种个操作,而并不会进行赋值,故首先效率上列表初始化时是会更高。对于内置类型虽然是没有区别,但是如果在构造函数中先在列表那里写初始化,再在函数体中写内置类型的操作,是有些奇怪的,因此为了统一性,推荐统一使用列表初始化方式进行成员的初始化。