一.构造函数
类通过一个或几个成员函数来控制其对象的初始化过程,这些函数成为构造函数。构造函数的任务是初始化类的数据成员,只要类的对象被创建,就会执行构造函数。构造函数与类同名且没有返回值。
①默认构造函数
默认构造函数是没有参数的构造函数,当类没有定义任何构造函数时,编译器会帮我们合成一个默认构造函数,如:
class A
{
private:
int x ;
double *p;
B y;
};
当我们写下:
A a;
编译正常通过,因为有合成的默认构造函数,但是合成的默认构造函数不能对内置类型以及指针、数组进行初始化,所以上面 x,p是未定义的,而y只有在有默认构造函数时才能被A的合成默认构造函数初始化,否则不能通过编译
当类有定义任意构造函数时,不再会有默认的构造函数,除非我们在定义一个
class A
{
public:
A(int n): x(n) { }
private:
int x ;
double *p;
B y;
};
A a;//不能通过编译
需改成:
class A
{
public:
A() { }
A(int n): x(n) { }
private:
int x ;
double *p;
B y;
};
拷贝构造函数只有一个参数同时是本类型的一个引用变量,如:
A(const A &b) { }
在我们未定义拷贝构造函数时,编译器也会帮我们产生一个默认的拷贝构造函数,进行位拷贝,这在有指针的情况下是不适用的,如:
A a1;
A a2(a1);
这时a2.p == a1.p; 这显然不是我们所希望的,所以需要我们自己定义从而为p分配内存
当我们不希望拷贝现象存在,可将拷贝构造函数定义为私有的
③构造函数的初始化表
构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体{}之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。
以下是构造函数初始化表的使用规则和注意点:
1)如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
2)类的const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化。
3)成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。这是因为类的声明是唯一的,而类的构造函数可以有多个,因此会有多个不同次序的初始化表。如果成员对象按照初始化表的次序进行构造,这将导致析构函数无法得到唯一的逆序。
2. 赋值函数(=)
赋值函数为运算符重载,写法如下
A &operator=(const A &b);//赋值函数
赋值函数与拷贝构造函数一样,在未定义时编译器也会默认生成,也是位拷贝的方式;
拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用:
A a1;
A a2(a1);//拷贝构造函数
A a3 = a1;//拷贝构造函数
a3 = a2;//赋值函数
3.析构函数
析构函数主要任务是销毁对象的成员变量,成员变量按照初始化的逆序销毁,函数形式如下:
~A ( ) { }
编译器也会帮我们合成默认析构函数,同样对有指针成员的类不适用
4.构造函数和析构函数可以是虚函数吗
构造函数不能是虚函数,因为虚函数需要 vtable ,而vtable只有在只有在构造函数调用后才生成,这就产生矛盾了。
析构函数可以是虚函数,当类有派生类时,析构函数最好是虚函数,这样才能正确释放资源。