0%

关于C++中构造函数的理解

在C++中,有一种特殊的成员函数也就是构造函数,只要创建类类型的新对象时,都要执行构造函数。构造函数的工作是保证每个对象的数据成员具有合适的初始值。下面主要想说三个问题:默认构造函数、初始化列表、显式构造函数。

一、默认构造函数(Default Constructor)

在《C++ Primer》中,关于默认构造函数有这么一种说法:只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。我们可以将默认构造函数分成两类:trival和nontrival,所谓trival就是没什么用的,而nontrival才有意义,是编译器所的需要的那种,必要的话由编译器合成出来。有四种情况会产生nontrival的默认构造函数。

  1. 带有default constructor的member class object
  2. 带有default constructor的base class
  3. 带有virtual function的class。也就是说vptr是需要由构造函数初始化的,当然这是由编译器完成的。
  4. 带有virtual base class的class
    当然如果用户自己定义了构造函数,那么编译器将不会再自动生成。但是也不是什么也不做,而是在上面几种情况下将必要的操作插入构造函数代码中。举个例子。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A{
public:
  A(){cout<<"A";}
};

class B: public A{
public:
  B(){cout<<"B";}
};
//输出AB
//B的构造函数被扩充如下
B::B(){
  A();
  cout<<"B";
}

如果没有默认构造函数会有什么影响呢?其实默认构造函数的最大好处就是自动调用。当没有提供默认构造函数的时候,自动调用的情况就都出现问题了。比如动态分配class数组。

二、初始化列表(Initialization List)

初始化列表就是形如下面的初始化方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A{
private:
  int x;
public:
  A(int a):x(a){}
};

class B{
private:
  A y;
  int z;
public:
  B(int a, int b):y(a),z(b){}
};

其中内置数据类型直接赋值,如果成员是类的话,则调用相应的构造函数。那么初始化列表与在函数体内直接赋值有什么区别呢?比如下面两个函数。

1
2
3
4
5
6
B(int a, int b):y(a),z(b){
}
B(int a, int b){
  y = A(a);
  z = b;
}

区别在于第二个函数会先产生一个临时性的A object,然后将它初始化,之后以一个赋值运算符将临时性object指定给y,随后再摧毁那个临时性的object。以下是两个函数的可能的内部扩张结果。

1
2
3
4
5
6
7
8
9
10
11
12
B(int a, int b){
  y.A::A(a);
  z = b;
}
B(int a, int b){
  y.A::A();
  A tmp = A(a);
  y = tmp;
  tmp.A::~A();

  z = b;
}

值得注意的一点是,成员的初始化顺序并不初始化列表中的顺序,而是成员在类中声明的顺序。另外有时候初始化列表并非一个可选方案,而是必选。主要有下面四种情况。

  1. 当初始化一个reference member时;
  2. 当初始化一个const member时;
  3. 当调用一个base class 的 constructor,而它拥有一组参数时
  4. 当调用一个member class 的 constructor,而它拥有一组参数时。

三、显式构造函数(explicit constructor)

按照默认规定,只有一个参数的构造函数定义了一个隐式转换,将该构造函数对应数据类型的数据转换为该类对象。比如

1
2
3
4
5
6
7
8
9
class A{
private:
  int x;
public:
  A(int a){...}
};

A a(2);
A a = 2; //隐式转换

但有时候这种转换会带来错误和误解,此时就可以使用explicit修饰构造函数,也就是显式构造函数。

1
2
3
4
5
6
7
8
9
class A{
private:
  int x;
public:
  explicit A(int a){...}
};

A a(2); //OK
A a = 2; //编译不通过