C++中的构造函数详解
目录
- 普通变量的初始化
- 构造函数
- 一定会生成默认构造函数吗?
- 防止隐式类型转换
- 赋值与初始化的区别
- 对象的计数
- 成员初始化的顺序
- 类的引用成员
- 构造函数使用注意事项
- 参考
- 总结
普通变量的初始化
当我们在定义一个变量不给它指定一个初始值时,这对于全局变量和局部变量来说结果会不一样。全局变量在程序装入内存时 就已经分配好空间,程序运行期间其地址不变,它会被初始化为全0(变量的每一位都为0)。但是局部变量定义在函数内部,存储在栈上,当函数被调用时,栈会分配一部分空间来存储该局部变量(也就是只分配空间,而不管赋值),这时此空间的值是已经有的,它是一个随机的值,如果程序员不对它进行初始化,将会产生不好的后果(如果将栈空间内存的变量初始化工作交给编译器,则每次函数调用都会被重新赋值,则会大大增加开销)。
示例全局变量,这些对象会被赋一个默认的初始值。如图:
构造函数
对象和我们前面提到的基本类型的变量一样,定义的时候也能够被初始化。而这些初始化操作会比基本数据类型初始化更复杂一些,不仅涉及对类内基本数据类型的初始化,还包括进行动态内存分配,打开文件等操作。这时就需要为类设计一个构造函数来专门负责这些操作,对象一旦创建就立即调用它,对其进行强制初始化(这是强制执行的)。
构造函数属于成员函数比较特殊的一种(与其他成员函数不同,它不能被显式的调用,所以也没有必要为它设置返回值),它在对象的初始化时起作用(设计类时往往将初始化功能在构造函数中实现),我们希望创建出的所有变量都能被赋予有意义的能力,而不是未知的,可以在创建之后再次改变他,但在此之前若要使用它,将会发生意外,所以在制度上我们就要对其进行严防死守(使其被创建后一定处于良好状态)。
构造函数可以重载,可以编写多个构造函数,参数列表不同。生成对象时,将会根据参数列表的参数信息决定该调用哪个构造函数。如果没有对参数有任何交代,编译器就认为应该调用无参构造函数。
当在局部空间或全局区创建自定义类型的对象时,构造函数会被自动调用(设计好初始化后,每当创建对象就会自动调用构造函数会使每一个对象都处于良好的随时可用的状态)。如图:
以上演示是在类的外部对构造函数进行了定义,实际开发中,一般将类的定义部分放到头文件中(.h),而类内成员函数的实现放到实现文件中(.cc)中。这对我们在不改变类的定义的情况下修改他的成员函数提供了方便。这比在类内直接实现有一个好处,就是不需要每次改变函数体都要重新编译(前题是不改变函数声明)。
明确一点,如果在类内对成员函数进行了实现,它将会被隐式地声明为内联函数(由编译器决定是否最终把它变为内联函数)。
一般超过5行代码的函数就没有必要设为内联函数了(函数内限制在两个表达式最好)。
在我们只定义了有参的构造函数后,就不会有无参的构造函数了,需要我们自己定义一个无参的构造函数。
class MyArray { char array_; public: MyArray(char a_new_array='x'):array_(a_new_array);//给出默认参数 MyArray(); void PrintMyArray(); }; //类实现省略 void test() { MyArray a_long_array('c');//调用有参的构造方法 MyArray a_short_array();//调用无参的构造方法 }
初始化方法一般给出形参列表,在这里对类内成员赋初值(此时构造函数的函数体还未开始执行),我们这里是对其输入了一个字符。构造函数还可以调用其他类的构造函数来初始化对象中的成员变量(这是编译器自动调用的,如在隐式的数据类型转换时)。当然也可以在构造函数函数体内为成员变量赋值(这并不是初始化,执行函数体后赋值之前,成员变量已经有一个无意义的值了)。
MyArray::MyArray(char init_array) {//进入构造函数体后成员变量已经有值了! array_=init_array;//在函数体内进行赋值 cout << "MyArray的构造方法执行了!\n"; }
形参列表具有立即性(在成员变量被创建时形参列表的值就立即为其初始化),而构造函数体内需要执行赋值语句。对于一些自定义类型的成员变量进行初始化,形参列表初始化往往比赋值语句效率更高。此外对于一些特殊的成员只能采用形参列表的方式进行初始化。
一定会生成默认构造函数吗?
前面我们说过,当我们没有编写构造函数时,编译器会为我们生成一个默认的构造函数,对于初学者来说,这就够了,但现在我们要强调几点。
1.如果类中我们没有定义构造函数并且类中成员变量含有自定义类型成员变量时,编译器会生成默认构造函数,并且调用自定义类型的构造函数。
2.当父类具有默认构造函数而子类没有任何构造函数,当生成子类对象时,会生成子类的默认构造函数,在这个函数中调用父类默认构造函数。
3.当一个含有虚函数并且没有构造函数时编译器会生成默认的构造函数。
4.出现菱形继承时,两个父类分别进行虚继承,当出现子类对象时,子类会生成一个自己的默认构造函数,在构造函数内调用两个父类的构造函数。
5.在定义类时,直接为成员变量赋值,当生成对象时,会调用默认构造函数。
防止隐式类型转换
如果构造函数只有一个参数的话最好将此构造函数声明为ecplicit,他能防止编译器将explicit的构造函数进行隐式类型转换。
class MyArray { char array_; double array_length_; public: MyArray(double init); MyArray(char init); void PrintMyArray(MyArray a_array); }; void MyArray::PrintMyArray(MyArray a_array) {//理应接受一个对象,有时会接受一个基本类型 //此时会进行隐式类型转换 cout << "MyArray为:"<< a_array.array_length_ << "\n"; } void test() { MyArray first_array(3.0); first_array.PrintMyArray(5.0);//隐式的将5.0转化为了MyArray对象 }
修改如下:
class MyArray { char array_; double array_length_; public: explicit MyArray(double init); explicit MyArray(char init); void PrintMyArray(MyArray a_array); };
此时调用first_array.PrintMyArray(5.0)就会报错了。
赋值与初始化的区别
必须明确初始化和赋值的概念,当一个对象刚被创建时,对它的赋值操作为初始化,而赋值是修改一个已经存在并且有值的对象。
int a_int=1;//初始化,a_int被创建出来并被立即初始化 a_int=2;//赋值,a_int已经存在,没有新的变量被创建出来
初始化出现在构造函数中,而赋值出现在operator=操作符中。
一个对象只能被初始化一次,而赋值却可以不限制次数,在对象的声明期内都可以进行。而且进行赋值时可能会进行类型转换(此时会产生临时对象)。
一个构造函数的初始化可以借用另一个构造函数,他被称为委托构造函数。
class MyArray { char array_; double array_length_{ 1.0 }; public: MyArray(double init, double in, double it); MyArray(double init); MyArray(); double GetMyArrayLength(); bool CompareMyArrayLength(MyArray a_array); void PrintMyArray(MyArray a_array); }; MyArray::MyArray(double init, double in, double it) { cout << "构造函数1执行了!" << endl; } MyArray::MyArray(double init) :MyArray{ init,init,init } { cout << "构造函数2执行了!" << endl; } //其他函数实现略 void test() { MyArray first_array{ 1.0,1.1,1.2 }; MyArray second_array{ 2.0 }; }
输出如下:
在执行构造函数2时先利用构造函数1进行初始化操作。
对象的计数
有时我们希望知道我们定义的一个类有多少个对象,就可以这样操作:
class MyArray { static int sum_; public: MyArray(); ~MyArray(); static int GetMyArraySum() { return sum_; } }; int MyArray::sum_ = 0; MyArray::MyArray() { sum_++; } MyArray::~MyArray() { sum_--; } void test() { MyArray first_array; MyArray second_array; MyArray forth_array; cout << "MyArray有" << MyArray::GetMyArraySum() << "个对象" << endl; }
输出如下:
因为创建对象的时候构造函数一定会被调用,所以它能准确的记录当前的对象数。
成员初始化的顺序
一个类中成员的初始化顺序和它们在类中被声明的顺序是一致的。编译器会忽略构造函数中的顺序,这是因为对象的析构需要和对象的成员构造顺序相反的要求 。
所以在编程中,成员变量的初始化顺序应与类定义中的声明顺序保持一致。
class Employee {//错误示范 string email_,first_name_,last_name_; public: Employee(const char* first_name,const char* last_name):first_name_(first_name),last_name_(last_name),email_(first_name_+last_name_+"@163.com"){}
类中定义的email_是在first_和last_之前的,但程序却用其他未初始化的成员变量来为它初始化。
类的引用成员
如果类中的某个费静态数据成员是一个引用的话,所有的引用必须被明确地初始化,所以在构造函数中都要显示初始化。
class MyArray { YourArray& a_array_; public: MyArray(YourArray&); } MyArray::MyArray(YourArray& init)//会被报错 { }
这个成员引用有两个特点,一,在创建a_array_时就要为它绑定一个对象,二,一旦绑定后,就不能再绑定其他变量了。
构造函数使用注意事项
1.除非必要不要在构造函数内做与初始化对象无关的事情,减少它的功能可以使每个函数功能更加明确,增加效率。
2.类的非静态const成员和引用成员只能在构造函数的初始化列表中进行初始化,这是由const和引用自身特点决定的。
3.不能同时定义一个无参的构造函数和一个参数全部为默认值得构造函数。
4.拷贝构造函数的参数应为引用传递,如果为传值则会与有一个参数的构造函数冲突。
参考
The C++ Programming Language (美) Bjarne Stroustrup
cpp参考:https://zh.cppreference.com
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!