C++基础知识(一)——构造函数
目录
构造函数定义
构造函数到底是什么,它在C++对象创建过程到底起什么样的作用呢
《C++ Primer》中文版(第五版)中是这么定义的:
每一个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。
看完书中的定义其实我们还是什么似懂非懂,我们都知道C++中一个对象的创建分两步:
- 为对象分配空间;
- 对对象进行初始化工作;
而我们的构造函数其实就是做的第二步的工作,所以构造函数的实质其实就是一个对对象进行初始化的工作。
构造函数的形式
那么到底如何进行初始化呢,或者说如何进行构造函数的编写和使用呢
构造函数分为默认构造函数,拷贝构造函数以及其他形式的构造函数。至于调用哪一个构造函数则根据函数重载机制进行选择。
class Test
{
public:
Test();
Test(const Test ©_test);
Test(const std::string &arg_s, int arg_x, int arg_y);
private:
std::string s;
int x = 0;
int y;
};
对于上面的Test类,
1. Test()
就是一个简单的默认构造函数,当然这是一种最简单的形式;
2. Test(const Test ©_test)
是拷贝构造函数;
3. Test(const std::string &arg_s, int arg_x, int arg_y)
是其他形式的构造函数;
4. Test() : Test("", 0, 0)
1是委托构造函数。即委托它所属类中其他构造函数执行自己的初始化过程。委托函数执行完后会返回该构造函数的函数体继续执行。
而对于Test(const std::string &arg_s = "", int new_x = 0, int new_y = 0);
这样含有默认参数的构造函数,当所有的参数都有默认值时,则其实质上相当于也定义了默认构造函数。
构造函数初始化的方式
1.在函数体内进行初始化
Test(const std::string &arg_s, int new_x, int new_y)
{
s = arg_s;
x = new_x;
y = new_y;
}
这种方式效率最低,不建议大家使用。至于为什么,后面会讲到。
2.使用初始化列表进行初始化
Test() : x(5),y(5)
{
}
在构造函数后面加:成员变量名(值),成员变量名(值)这种形式就是参数化列表。
使用参数化列表初始化效率高于函数体内赋值。建议使用这种方法进行初始化。
为什么说参数化列表的效率比函数体内赋值要高呢?
- 构造函数在执行函数体之前,会使用初始化列表对数据成员进行初始化;
- 对没有初始化列表的数据成员,若有类内参数值,则会采用类内参数进行初始化;
- 其他的则采用默认初始化。
也就是说,如果你在初始化列表中初始化了,就会采用你的初始化值,否则就是编译器执行默认初始化操作。像本例子中,根据声明顺序s,x,y(类定义中数据成员排列的顺序),先初始化s,采用默认初始化;在初始化x,y,采用自己的初始化值5进行初始化。
初始化的顺序只与声明顺序有关,与初始化列表中的顺序没有关系。也就是说,你最好不要打乱顺序,且不要用一个成员来初始化另一个成员。例如:Test(int value) : y(value), x(y) { }
,由于初始化顺序只与声明顺序有关,因此先初始化s,在初始化x,然后是y,造成x在y之前初始化,此时由于y没有初始化,所有x的值是未知的。
那么默认初始化究竟是执行一个什么样的过程呢?
- 对于类成员,则会调用类的默认构造函数进行初始化(当然,如果没有默认构造函数的话,编译器就会报错);
- 对于内置类型(int,char,double等),若在函数体之外,则被初始化为0;在函数体内,则值为未定义(即不被初始化)。
编译器合成的默认构造函数
1.定义
没有定义任何一个构造函数时,编译器就会自动合成默认构造函数。
下列情况下,编译器不能合成默认的构造函数:
- 当类中有其他的构造函数;(C++认为如果类在某种情况下需要控制对象初始化,即调用自定义的构造函数,它就会认为你会在所有的情况下都调用自定义的构造函数,即控制对象初始化)
- 当类中的成员没有默认构造函数。
2.合成默认构造函数的形式
Test() = default;
2这种形式表示使用编译器合成的默认构造函数。可以在类内声明时使用,也可以在类外定义时使用。- 没有构造函数时,编译器会自动加一个默认构造函数。不建议使用,这样无法控制合成的默认构造函数是不是自己想要的情况。
3.合成默认构造函数的使用场景
使用自定义默认构造函数,而不用编译器合成的的场景
- 某些情况下,假如我们的成员中有指针,我们可能需要初始化指针所指空间中的所有内容,而不仅仅是拷贝指针的值。使用编译器合成的默认构造函数会造成浅拷贝。(详情请查看浅拷贝与深拷贝的区别,string类的实现);
- 含有内置类型或者复合类型,由于它们在函数内默认初始化为未定义的值,所以最好使用自定义的默认构造函数进行初始化;
继承中的构造函数
当该类继承自父类时,调用构造函数时,如果没有显示在参数列表中列出父类初始化(形式为父类构造函数调用),则会先调用父类的默认构造函数进行父类对象的初始化,然后才会进行子类成员的初始化。
含有虚函数的构造函数
当类中含有虚函数时,在调用构造函数初始化时,会首先初始化对象的虚表指针,在初始化其他的成员,然后进入函数体内执行某些操作。
总结
1.构造函数的调用过程
- 当创建一个对象时,会首先在内存中为对象开辟空间,然后调用构造函数;
- 构造函数调用时,会首先进行初始化;
- 有参数列表,则根据参数列表进行初始化;
- 没有参数列表的成员,若类中其有初始值,则根据类中初始值进行初始化;
- 若都没有,则采用默认初始化。
2.构造函数返回值
构造函数是回调函数,当C++创建对象时,会首先使用内存分配函数对对象进行空间的分配(类似于malloc,::operator new),当分配空间完成后(类似于做GUI的时候双击按钮),会回调构造函数(类似于点完按钮,然后会执行某些操作),构造函数完成后,又会返回回来,最后返回内存分配函数分配的空间地址(malloc,::operator new最后都会返回开辟内存的地址)。
构造函数是没有返回值的,而创建对象会返回创建的对象的地址。(这是分配空间完成的操作,当构造函数回调完成之后返回对象的地址)。