C++构造函数(从本质上理解)

C++ 程序中,变量在定义时可以初始化。如果不进行初始化,变量的初始值会是什么呢?对全局变量和局部变量来说,这个答案是不一样的。

未初始化的全部变量

全局变量在程序装入内存时就已经分配好了存储空间,程序运行期间其地址不变。对于程序员没有初始化的全局变量,程序启动时自动将其全部初始化为 0(即变量的每个比特都是 0)。

在大多数情况下,这是一种稳妥的做法。而且,将全局变量自动初始化为 0,是程序启动时的一次性工作,不会花费多少时间,所以大多数 C++ 编译器生成的程序,未初始化的全局变量的初始值都是全 0。

未初始化的局部变量

对于局部变量,如果不进行初始化,那么它的初始值是随机的。局部变量定义在函数内部,其存储空间是动态分配在栈中的。

函数被调用时,栈会分配一部分空间存放该函数中的局部变量(包括参数),这片新分配的存储空间中原来的内容是什么,局部变量的初始内容也就是什么,因此局部变量的初始值是不可预测的。

函数调用结束后,局部变量占用的存储空间就被回收,以便分配给下一次函数调用中涉及的局部变量。

为什么不将局部变量自动初始化为全 0 呢?因为一个函数的局部变量在内存中的地址,在每次函数被调用时都可能不同,因此自动初始化的工作就不是一次性的,而是每次函数被调用时都耍做,这会带来无谓的时间开销。

当然,如果程序员在定义局部变量时将其初始化了,那么这个初始化的工作也是每次函数被调用时都要做的,但这是编程者要求做的,因而不会是无谓的。

对象的初始化

对象和基本类型的变量一样,定义时也可以进行初始化。一个对象,其行为和内部结构可能比较复杂,如果不通过初始化为其某些成员变量赋予一个合理的值,使用时就会产生错误。例如,有些以指针为成员变量的类可能会要求其对象生成时,指针就已经指向一片动态分配的存储空间。

对象的初始化往往不只是对成员变量赋值这么简单,也可能还要进行一些动态内存分配、打开文件等复杂的操作,在这种情况下,就不可能用初始化基本类型变量的方法来对其初始化。

虽然可以为类设汁一个初始化函数,对象定义后就立即调用它,但这样做的话,初始化就不具有强制性,难保程序员在定义对象后不会忘记对其进行初始化。面向对象的程序设计语言倾向于对象一定要经过初始化后,使用起来才比较安全。因此,引入了构造函数(constructor)的概念,用于对对象进行自动初始化。

在C++语言中,“构造函数”就是一类特殊的成员函数,其名字和类的名字一样,并且不写返回值类型(void 也不写)。

构造函数可以被重载,即一个类可以有多个构造函数。

如果类的设计者没有写构造函数,那么编译器会自动生成一个没有参数的构造函数,虽然该无参构造函数什么都不做。

无参构造函数,不论是编译器自动生成的,还是程序员写的,都称为默认构造函数(default constructor)。如果编写了构造函数,那么编译器就不会自动生成默认构造闲数。

对象在生成时,一定会自动调用某个构造函数进行初始化,对象一旦生成,就再也不会在其上执行构造函数。

初学者常因“构造函数”这个名称而认为构造函数负责为对象分配内存空间,其实并非如此。构造函数执行时,对象的内存空间已经分配好了,构造函数的作用是初始化这片空间。

为类编写构造函数是好的习惯,能够保证对象生成时总是有合理的值。例如,一个“雇员”对象的年龄不会是负的。

来看下面的程序片段:
class Complex{
private:
    double real, imag;  //实部和虚部
public:
    void Set(double r, double i);  //设置实部和虚部
};
上面这个 Complex 类代表复数,没有编写构造函数,因此编译器会为 Complex 类自动生成一个无参的构造函数。

下面两条定义或动态生成 Complex 对象的语句,都会导致该无参构造函数被调用,以对 Complex 对象进行初始化。
Complex c;  //c用无参构造函数初始化
Complex *p = new Complex;  //对象 *p 用无参构造函数初始化
如果为 Complex 类编写了构造闲数,如下所示:
class Complex{
private:
    double real, imag;
public:
    Complex(double r, double i = 0);  //第二个参数的默认值为0
};

Complex::Complex(double r,double i){
    real = r;
    imag = i;
}
那么以下语句有的能够编译通过,有的则不行:
Complex cl;  //错,Complex 类没有无参构造函数(默认构造函数)
Complex* pc = new Complex;  //错,Complex 类没有默认构造函数
Complex c2(2);  //正确,相当于 Complex c2(2, 0)
Complex c3(2, 4), c4(3, 5);  //正确
Complex* pc2 = new Complex(3, 4);  //正确
C++ 规定,任何对象生成时都一定会调用构造闲数进行初始化。第 1 行通过变量定义的方式生成了 c1 对象,第 2 行通过动态内存分配生成了一个 Complex 对象,这两条语句均没有涉及任何关于构造函数参数的信息,因此编译器会认为这两个对象应该用默认构造函数初始化。可是 Complex 类已经有了一个构造函数,编译器就不会自动生成默认构造函数,于是 Complex 类就不存在默认构造函数,所以上述两条语句就无法完成对象的初始化,导致编译时报错。

构造函数是可以重载的,即可以写多个构造函数,它们的参数表不同。当编译到能生成对象的语句时,编译器会根据这条语句所提供的参数信息决定该调用哪个构造函数。如果没有提供参数信息,编译器就认为应该调用无参构造函数。

下面是一个有多个构造函数的 Complex 类的例子程序。
class Complex{
private:
    double real, imag;
public:
    Complex(double r);
    Complex(double r, double i);
    Complex(Complex cl, Complex c2);
};

Complex::Complex(double r)  //构造函数 1
{
    real = r;
    imag = 0;
}
Complex :: Complex(double r, double i)  //构造数 2
{
    real = r;
    imag = i;
}
Complex :: Complex(Complex cl, Complex c2)  //构造函数 3
{
    real = cl.real + c2.real;
    imag = cl.imag + c2.imag;
}
int main(){
    Complex cl(3), c2(1,2), c3(cl,c2), c4 = 7;
    return 0;
}
根据参数个数和类型要匹配的原则,c1、c2、c3、c4 分别用构造函数 1、构造函数 2、构造函数 3 和构造函数 4 进行初始化。初始化的结果是:c1.real = 3,c1.imag = 0 (不妨表示为 c1 = {3, 0}),c2 = {1, 2},c3 = {4, 2}, c4 = {7, 0}。