C++构造函数(从本质上理解)
在 C++ 程序中,变量在定义时可以初始化。如果不进行初始化,变量的初始值会是什么呢?对全局变量和局部变量来说,这个答案是不一样的。
在大多数情况下,这是一种稳妥的做法。而且,将全局变量自动初始化为 0,是程序启动时的一次性工作,不会花费多少时间,所以大多数 C++ 编译器生成的程序,未初始化的全局变量的初始值都是全 0。
函数被调用时,栈会分配一部分空间存放该函数中的局部变量(包括参数),这片新分配的存储空间中原来的内容是什么,局部变量的初始内容也就是什么,因此局部变量的初始值是不可预测的。
函数调用结束后,局部变量占用的存储空间就被回收,以便分配给下一次函数调用中涉及的局部变量。
为什么不将局部变量自动初始化为全 0 呢?因为一个函数的局部变量在内存中的地址,在每次函数被调用时都可能不同,因此自动初始化的工作就不是一次性的,而是每次函数被调用时都耍做,这会带来无谓的时间开销。
当然,如果程序员在定义局部变量时将其初始化了,那么这个初始化的工作也是每次函数被调用时都要做的,但这是编程者要求做的,因而不会是无谓的。
对象的初始化往往不只是对成员变量赋值这么简单,也可能还要进行一些动态内存分配、打开文件等复杂的操作,在这种情况下,就不可能用初始化基本类型变量的方法来对其初始化。
虽然可以为类设汁一个初始化函数,对象定义后就立即调用它,但这样做的话,初始化就不具有强制性,难保程序员在定义对象后不会忘记对其进行初始化。面向对象的程序设计语言倾向于对象一定要经过初始化后,使用起来才比较安全。因此,引入了构造函数(constructor)的概念,用于对对象进行自动初始化。
在C++语言中,“构造函数”就是一类特殊的成员函数,其名字和类的名字一样,并且不写返回值类型(void 也不写)。
构造函数可以被重载,即一个类可以有多个构造函数。
如果类的设计者没有写构造函数,那么编译器会自动生成一个没有参数的构造函数,虽然该无参构造函数什么都不做。
无参构造函数,不论是编译器自动生成的,还是程序员写的,都称为默认构造函数(default constructor)。如果编写了构造函数,那么编译器就不会自动生成默认构造闲数。
对象在生成时,一定会自动调用某个构造函数进行初始化,对象一旦生成,就再也不会在其上执行构造函数。
初学者常因“构造函数”这个名称而认为构造函数负责为对象分配内存空间,其实并非如此。构造函数执行时,对象的内存空间已经分配好了,构造函数的作用是初始化这片空间。
为类编写构造函数是好的习惯,能够保证对象生成时总是有合理的值。例如,一个“雇员”对象的年龄不会是负的。
来看下面的程序片段:
下面两条定义或动态生成 Complex 对象的语句,都会导致该无参构造函数被调用,以对 Complex 对象进行初始化。
构造函数是可以重载的,即可以写多个构造函数,它们的参数表不同。当编译到能生成对象的语句时,编译器会根据这条语句所提供的参数信息决定该调用哪个构造函数。如果没有提供参数信息,编译器就认为应该调用无参构造函数。
下面是一个有多个构造函数的 Complex 类的例子程序。
未初始化的全部变量
全局变量在程序装入内存时就已经分配好了存储空间,程序运行期间其地址不变。对于程序员没有初始化的全局变量,程序启动时自动将其全部初始化为 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}。