在C++中当你创建一个空类时,编译器会默认声明一个default构造函数,copy构造函数,一个copy assignment操作符,一个析构函数。注意,编译器只声明,只有当这些函数被调用时,他们才会被创建。
举个栗子,当你写下
class Empty() {};
实际上在编译器中为你实现了这样的代码
class Empty { Empty(){} Empty(const Empty& rhs){} ~Empty(){} Empty& operator=(const Empty& rhs){} };
1.构造函数
1.默认构造函数
所谓默认构造函数,不仅仅是指编译器自己为你声明的构造函数,准确的说,默认构造函数应该是在创建类对象时不给对象传入任何参数的情况下,类对象调用的构造函数称为默认构造函数。
那么默认构造函数分为两种,无参构造函数,带默认参数的构造函数(所有参数都有默认值)。举个栗子
class empty { }; class non_pat { public: non_pat(){} }; class pat { public: pat(int _a = 0,double _b = 1.0) { a = _a; b = _b; } private: int a; double b; }; int main() { empty e; non_pat n; pat p;//pat p(0,1.0); return 0; }
在main函数中创建empty类对象时,调用编译默认创建的默认构造函数。non_pat和pat类对象时,都会调用本类的默认构造函数,pat对象没有传入参数,而我们手动实现的构造函数是有两个参数的,那他是否执行编译器默认创建的构造函数呢?不是的,只要我们手动声明了一个构造函数,编译器就不再为我们创建default构造函数。在pat类中,虽然没有无参构造函数,但是形参是有默认值的,当我们不传入参数时,实际上传入默认形参。可以认为我们实际上写下了了注释部分的代码。
2.构造函数做了什么?
构造函数负责控制对象的初始化过程。实际上,构造函数做了两个阶段的工作。第一阶段为初始化阶段,负责为类对象分配空间并将类的数据成员初始化。第二阶段为计算阶段,负责调用构造函数函数体内的赋值操作,为类对象的数据成员赋值。我们通过代码来研究第一阶段,由于第二阶段执行函数体内的操作,所以我们先写一个构造函数函数体内误操作的类。
class Init { public: Init() { } int i; double d; }; int main() { Init init; std::cout << init.i << "," << init.d << std::endl; }
测试结果为618788,1.00043e-307.显然,没有赋值阶段,我们的类对象依然在内存中获得了空间,并且数据成员全部被赋予了随机值。但是显然我们的测试用例不够说服力,C++的内置类型使用随机值初始化,那如果数据成员本身就是一个类呢?我们再做测试
class pat { public: pat(int _a = 0,double _b = 1.0) { a = _a; b = _b; }int a; double b; }; class Init { public: Init() { } int i; double d; pat p; }; int main() { Init init; std::cout << init.i << "," << init.d << std::endl; std::cout << init.p.a << "," << init.p.b << std::endl; }
测试结果为
4259624,1.45552e-305
0,1
通过测试结果我们可知,当类的数据成员有类类型时,类类型的数据成员在初始化阶段执行自己的默认构造函数执行初始化,那如果这个类类型没有默认构造函数,该怎么办?我在后面会提到。
接着我们谈到赋值阶段,在赋值阶段,执行构造函数体内的赋值操作。
class assign { public: assign() { i = 10; d = 10.00; } assign(int _i,double _d) { i = _i; d = _d; } public: int i; double d; }; int main() { assign a; assign b(20,20.00); std::cout << a.i << "," << a.d << std::endl; std::cout << b.i << "," << b.d << std::endl; }
测试结果为 10,10
20,20
结果与我们预想结果一致,没有什么可说的。
3.构造函数初始值列表
前面我们提到,如果类的某个数据成员是类类型,那么在初始化阶段,执行这个数据成员类的默认构造函数。如果这个类没有默认构造函数,该怎么办?
实际上,不只这种情况,如果类的成员函数时常量,也会出现问题,很简单,初始化阶段已经分配了随机值,那么在赋值阶段,我们可以给一个常量赋值吗?显然不能!!!
同理,成员函数是引用类型,也必须在初始化阶段就确定他的值。为了处理这三种情况,我们引入了初始化列表。
形式如下:
int c = 20; class A { public: A() : a(89),b(c){} const int a; int &b; }; int main() { A k; std::cout << k.a << "," << k.b << std::endl; return 0; }
测试结果为:89,20
在初始化列表中,数据成员在初始化时,一方面在内存开辟空间,一方面直接用列表中的参数初始化数据成员 。其实可以用内置类型啦对比理解类的初始化阶段,计算阶段和初始化列表。
int a;//初始化阶段,开辟空间,使用随机值初始化 a = 10;//计算阶段,执行函数体内的操作,为数据成员赋值 int a = 10;//初始化列表的方式,在初始化时直接赋值。
理解了吗?我们再考虑一个问题,如果类的某个数据成员是类类型,那么在初始化阶段,执行他的默认构造函数,赋值阶段,执行他的赋值运算符重载函数。而如果他在初始化列表中初始化,则直接执行他的拷贝构造函数。我们来做一个测试。
class A { public: A() { std::cout << "constructions" << std::endl; } A(const A&rhs) { std::cout << "copy constructions" << std::endl; } A &operator =(const A&rhs) { std::cout << "assignment" << std::endl; return *this; } }; A _a; class B { public: B(){ a = _a; } public: A a; }; class C { public: C() : a(_a){} public: A a; }; int main() { std::cout << "main start" << std::endl; B b; C c; std::cout << "main end" << std::endl; return 0; }
测试结果为constructors
main start
constructions
assignment
copy constructions
main end
分析一下,第三行的constructions表示B执行构造函数的初始化阶段,第四行赋值运算符重载表示赋值阶段,而C由于使用初始化列表,只执行一次拷贝构造。
总结如下:
1.类数据成员为常量
2.类数据成员为引用
3.类数据成员为没有默认构造函数的类类型。
在这三种情况下,构造函数必须使用初始化列表!!!即使无这三种情况中任意一种出现,也建议使用初始化列表来实现构造函数。因为初始化列表的方式将初始化和赋值结合在一步,省去了很多功夫,想象一下,如果某个数据成员为非常庞大的一个类,那么只执行一次他的拷贝构造函数要比执行一次他的默认构造函数加上赋值运算符重载函数要节省很多时间。