——《深度探索C++对象模型》
Default Constructor 的构造操作
当我们没有显式的声明一个构造函数时,编译器会自动帮我们生成;但是这个构造函数的作用极其有限,比如下面这个例子:
class Base{
public:
int _val;
};
int main(){
Base b;
std::cout << b._val << std::endl;
return 0;
}
这里我们实现了一个类Base,并且没有显式的声明构造函数,于是编译器会使用其默认的构造函数对其进行初始化操作,但是_val却是一个未定义的值,因为默认的构造函数并没有对类内的对象进行初始化操作,只是单纯的定义了一个类对象;(对于较严格的编译器,这里甚至会导致错误)
所以书中将这种默认生成的构造函数称为trivial(浅薄无能的) constructor;
下面就一一介绍nontrivial default constructor的四种情况:
"带有Default Constructor" 的 Member Class Object
这里标题的意思就是说一个类中内含了另一个类对象,且该类对象据有默认的构造函数,如下所示:
class Foo {};
class Base {
Foo foo;
};
这里Base类中定义了另一个类Foo,而Foo类具有默认的构造函数,所以编译器默认生成Base类的构造函数可能如下所示:
Base::Base(){
foo.Foo::Foo(); // 调用Foo类的默认构造函数
}
但是这仅限于类的初始化,类内部的成员变量的初始化则需要程序员手动的完成;
下面再将类进行改装:
class Foo {};
class Base {
public:
Foo foo;
int _val;
Base()
:_val{}
{}
};
这里我添加了一个成员变量_val,并在显式的构造函数中对其进行初始化操作;这里你可能会有疑问——不需要在Base类中显式的调用Foo类的默认构造函数吗?
答案是不需要——编译器会自动添加相应的初始化代码到Base类的构造函数中,我们可以这样来验证:
class Foo {
public:
Foo() {
std::cout << "Foo constructor is called." << std::endl;
// 显式的定义构造函数,在被调用时发出信号
}
};
class Base {
public:
Foo foo;
int _val;
Base()
:_val{}
{}
};
int main() {
Base b;
std::cout << b._val << std::endl;
return 0;
}
运行这段代码,发现Foo函数的调用信号被打印,所以上述理论得到验证;
当我们面对多个需要隐式调用的构造函数时,其调用顺序是怎样的呢?
答案是同声明顺序一致,可见下面的例子:
class Foo1 {
public:
Foo1() {
std::cout << "Foo1 constructor is called." << std::endl;
}
};
class Foo2 {
public:
Foo2() {
std::cout << "Foo2 constructor is called." << std::endl;
}
};
class Foo3 {
public:
Foo3() {
std::cout << "Foo3 constructor is called." << std::endl;
}
};
class Base {
public:
Foo3 foo3;
Foo2 foo2;
Foo1 foo1;
int _val;
Base()
:_val{}
{}
};
int main() {
Base b;
std::cout << b._val << std::endl;
return 0;
}
运行后的结果可以发现,其调用顺序取决于在Base类中的声明顺序,而并非其定义顺序;
”带有Default Constructor“ 的 Base Class
与上面的类似,对于没有显式的调用构造函数的对象,编译器会在Base Class中自动填充一个,但是这仅限于类的构造,对于类内部变量的初始化,需要手动完成;
”带有一个Virtual Function“ 的 Class
另有两种情况,也需要合成出default constructor:
- class声明(或继承)一个virtual function
- class派生自一个继承串链,其中有一个或多个的virtual base classes
请看下面这个例子:
class Base {
public:
virtual void Func() = 0;
};
class Derived1 :public Base {
public:
void Func() {
std::cout << "Derived1::Func is called." << std::endl;
}
};
class Derived2 :public Base {
public:
void Func() {
std::cout << "Derived2::Func is called." << std::endl;
}
};
int main() {
Derived1 d1;
Derived2 d2;
d1.Func();
d2.Func();
return 0;
}
对于虚函数类的默认构造函数,需要编译器合成一个virtual function table,其中存放class的virtual functions 地址;
此外,对于每一个class object中(需要是继承属性),还会合成一个额外的pointer member,其中包含相关的class vtbl的地址;
而这两项拓展(由编译器自动完成),会使得对应类调用函数发生变化(此即为多态),通过虚函数指针的不同指向地址,实现函数的多态调用;
”带有一个Virtual Base Class“ 的 Class
这里使用原书中作者给出的例子:
class X { public: int i; };
class A :public virtual X {};
class B :public virtual X { public:double d; };
class C :public A, public B { public:int k; };
void foo(A* const pa) { pa->i = 1024; } // 这里如果是const A* pa,后续的更改操作会报错
int main() {
foo(new A);
foo(new C);
return 0;
}
这里使用指针或引用是为了配合其动态匹配特性,因为对于pa->i的调用无法在编译器决定其具体的调用方式,对此,编译器内部可能会进行如下转变:
void foo (A* const pa) { pa->__vbcX->i = 1024; }
这里的__vbcX表示编译器所产生的指针,指向virtual base class X;而该指针会帮助pa完成动态的函数调用工作,实现运行期的动态匹配;
总结
对于上述的四种情况,C++Standard把那些合成物称为implicit nontrivial default constructors;
注意,被合成出来的constructor只能满足于编译器的需要,其会被编译器自动的完成调用,完成相应的任务;
在合成的default constructor中,只有base class subobjects 和 member class objects 会被初始化;
所有其他的nonstatic data member(如整数、整数指针、整数数组等等)都不会被初始化;因为这些都不是编译器所必需的,相反这些需要程序员手动的初始化;
作者还给出了C++新手的两个常见误解:
- 任何 class 如果没有定义 default constructor,就会被合成出一个来;
- 编译器合成出来的default constructor 会显式设定” class 内每一个 data member 的默认值“;
对于第一条,如果类中定义了一个拷贝构造、移动构造等,编译器就不会提供一个默认构造函数;
对于第二条,参考前文,这里不再赘述;