C++ 类的构造与析构
类的构造
1 | User(int id, string name, string phone); |
可以采用如下格式来进行类实例的创建:
1 | User user = User(1, "Jone", "123456"); |
如果User类有默认构造函数,则下面方式可以直接构造一个类:
1 | User user; //此处不是变量的声明,而是类实例的创建 |
对于只有一个参数的构造函数,可以采用赋值的方式创建一个类实例:
1 | A a = 10; //构造函数为 A(int id); |
这种形式容易带来问题,可以使用explicit
来修饰只有一个参数的构造函数,就能禁用这种类的构造写法。
构造函数分类
- 明确定义的可以传参的普通构造函数;
- 默认构造函数,如果没有明确定义构造函数,那么编译器就会生成一个默认的构造函数;
- 拷贝构造函数,如果没有明确定义拷贝构造函数,编译器会默认生成一个拷贝构造函数,默认的拷贝构造函数是浅拷贝,如果需要实现深拷贝,则需要明确定义一个;
- 赋值运算符,建议和构造函数一起定义,而不要只定义其中一个【参考】;
1 | class A { |
上述代码的输出结构是:
1 | A(a1) |
从输出中可以看出赋值操作符调用了拷贝构造函数;
类的初始化顺序
根据ISO/IEC 14882:1998(E) 中12.6.2一节的介绍。
Initialization shall proceed in the following order:
- First, and only for the constructor of the most derived class as described below, virtual base classes shall be initialized in the order they appear on a depth-first left-to-right traversal of the directed acyclic graph of base classes, where “left-to-right” is the order of appearance of the base class names in the derived class base-specifier-list.
- Then, direct base classes shall be initialized in declaration order as they appear in the base-specifier-list (regardless of the order of the mem-initializers).
- Then, nonstatic data members shall be initialized in the order they were declared in the class definition (again regardless of the order of the mem-initializers).
- Finally, the body of the constructor is executed.
上面的引用文字我没有完全看懂。但可以大体解释如下:
- 首先,对基类进行初始化;
- 然后,初始化非静态数据成员。对于在初始化成员列表中的数据成员,编译器会使用列表中的值对数据成员进行初始化;没有出现在初始化成员列表中的数据成员,编译器会按照默认方式进行初始化(如果是类,就是调用类的默认构造函数);
- 最后,构造函数体会被执行。
初始化成员列表(member initializer list)
执行构造函数时,先执行初始化列表的内容,若初始化里面没有数据,则编译器按照系统默认的方式对成员变量赋值,随后再进行构造函数中花括号内部的指令。
- 对于引用型成员变量和const常量型成员变量,则必须通过初始化列表初始化该成员变量。
- 如果成员类或者基类没有默认构造函数,则必须通过初始化表初始化该成员变量。
- 成员变量初始化顺序有声明顺序决定,而与初始化列表的顺序无关。
- 对于参数列表中所列的成员变量,可以不包含所有的成员变量。
出于效率方面的考虑:对于有默认构造函数的成员类,编译器会在构建父类之前调用成员类的默认构造函数,如果在构造函数中又对该成员类进行赋值,则该成员类的构造函数调用了两次。
1 | class A { |
上述程序的执行结果是:
1 | A(a1) |
可以看出A的默认构造函数在B构造函数之前被执行了,这不符合预期,我们期望只有A(a1),而A()是多余的。如果使用初始化列表的方式,则不存在这个问题,将B的构造函数改成如下形式:
1 | B::B(A& a) : _a(a) { |
修改之后的程序运行结果如下,这样就符合预期了:
1 | A(a1) |
构造和析构顺序
C++一个原则是:先构造后析构,在类的继承关系中,父类先构造,子类后构造;而析构时,则是子类先析构,父类后析构。如果在一个含注重,栈中构造了先后构造了A、B两个类(注意不是在堆中),则函数退出时,B先执行析构,然后才是A的析构。
1 | class GrandFather { |
1 | construct grandfather |
可以看出来,构造函数的顺序是先父类后子类,而析构则是先子类后父类。
析构函数导致的资源泄漏
如果main函数改为下面代码:
1 | int main() |
1 | construct grandfather |
Son
的析构函数没有调用,同样如果改成GrandFather* f = new Son();
,Son
和Father
的析构函数都不会被调用。如果析构函数中需要进行资源释放,那么就会导致资源泄漏问题。要解决此问题,需要通过virtual
来修饰析构函数。可以只修饰GrandFather的析构,也可以全部都加上virtual。
父类被多次构造
1 | class GrandFather { |
运行程序的输出结果:
1 | construct grandfather |
可以看到GrandFather
被构建了两次,这明显不符合预期。要解决这个问题需要在继承时添加virtual
关键字,如下:
1 | class Father1: virtual public GrandFather {}; |
数组与构造函数
1 | Stock stocks[4]; |
这个数据会构造4个Stock类,即,调用了4次Stock的默认构造函数。如果数组的元素类没有默认构造函数可以使用下面形式:
1 | Shop shops[4] = { |