1 构造函数
1)名字和类名相同。
2)不能定义返回类型,参数个数可以有任意个。
3)如果未定义构造函数,系统会自动产生一个默认构造函数。但只要程序中有构造函数的定义,系统就不会再自动产生默认构函。
2 转换构造函数
只有一个参数的构造函数称为转换构造函数。转换构造函数可以将其他类型转换成类类型。类的构造函数只有一个参数是比较危险的,因为编译器可能隐式的把参数类型转换成类类型。为了避免隐式转换,可以在构造函数前加上explicit,这样编译器就不会隐式调用了。
假如有一个Test类,他有一个转换构造函数:
Test(int num) :num_(num) { cout << "Test(int num)" <<num<< endl; } ~Test() { cout << "~Test()" << num_<<endl; }
执行代码:
int main() { Test t(10); //t = 20; return 0; }
执行结果:
这个程序中,使用了转换构造函数“构造”的功能,并未用到其“转换”功能。将代码更改如下:
int main() { Test t(10); t = 20; return 0; }
执行结果:
这里首先调用构造函数对t完成初始化。接着将20赋给t显然类型不符,本应该出错,可是程序中定义了转换构造函数,编译器会隐式的把20作为参数构造出来一个临时的Test对象(如果构造函数前加了explicit关键字,就不能隐式的调用,从而这里出错)。将其赋值给t后,该临时对象需要被析构,因此调用了一次析构函数。最后栈上的t被析构。需要注意的是将临时对象赋给t时,还调用了赋值运算符函数;如果在定义的时候就用一个同类型的对象对其进行初始化,则调用后面将要介绍的拷贝构造函数。
3 构造函数初始化列表
可以使用初始化列表来对类成员初始化,通过 : 调出初始化列表。放在初始化列表才是真正的初始化,属于初始化段。在函数体中进行初始化实际上是赋值,属于普通计算段。因此,由于const成员和引用要求定义时必须初始化,所以二者的初始化必须放在初始化列表。
初始化列表还有一个作用:
class Object { public: Object(int num):num_(num) { cout<<"Object..."<<num<<endl; } ~Object() { cout<<"~Object..."<<endl; } private: int num_; }; class Container { public: Container() { cout<<"Container..."<<endl; } ~Container() { cout<<"~Container..."<<endl; } private: Object obj_; };
上述代码在实例化Container类对象时会报错。这是因为,Container类对象的成员是Object类对象。因此,在初始化时,需先调用Object的默认构造函数,但Object中没有默认构造函数,而且由于Object中定义了一个带参数的构造函数,所以编译器也不会自动生成一个默认构造函数。要解决这个问题,可以在初始化obj_的时候,调用Object中定义的那个构造函数,而这个调用就在Container的初始化列表中实现:
Container():obj_(0) { cout<<"Container..."<<endl; } Container(num):obj_(num) { cout<<"Container..."<<endl; }
4 拷贝构造函数
拷贝构造函数的参数必须是对该类类型的引用,它在两种情况用到:1)当用一个类对象去初始化另一个同类类对象时。2)函数的参数是类对象或者返回值是类对象时。
(1)如果函数的参数是类对象,在函数结束时,还会调用析构函数将其析构掉;(2)如果参数是类对象的引用,则不会调用拷贝构造函数,也不会调用析构函数。(3)当函数的返回值是类对象时,首先会调用拷贝构造函数。但是,如果用它去初始化一个刚刚定义的类对象,则不会再次调用拷贝构造函数,这是因为相当于给返回的临时对象更了名,例如:Test t=TestFunc();如果没人接收,只是TestFunc(),那么它会立即调用析构函数。(感觉从返回类对象,对该对象接收或不接收,到最终的销毁,都是经历了一次拷贝构造和一次析构)。
当用一个对象去给另一个对象赋值时,会调用赋值运算符函数;当用一个对象去初始化另一个对象时,会调用拷贝构造函数。一定要区分赋值和初始化,才能知道需要调用哪个函数。类最好定义自己的拷贝构造函数与赋值运算符函数,否则可能会导致浅拷贝(当类包含动态成员时)。浅拷贝会引起内存被析构两次等严重问题。