问题
本节主要整理一下关于构造函数和析构函数的几个问题。
初识析构函数
● 析构函数通过在函数名前面加一个位取反符号 ~,如 ~Cat()
● 析构函数名与类名相同,一个类只能有一个析构函数,不能重载
● 析构函数不能有任何参数,也没有返回值
● 若没有写析构函数,编译器会自动生成一个默认的析构函数
● 当对象的生命周期结束时,系统会自动调用析构函数
构造函数为什么一般不定义为虚函数?
虚函数对应一个vtable(虚函数表),类中存储一个vptr指向这个vtable。如果构造函数是虚函数,就需要通过vtable调用,可是对象没有初始化就没有vptr,无法找到vtable,所以构造函数不能是虚函数。
下面的不用看了
在 14_虚函数详解 那一节中也有提到,构造函数不能是虚函数,原因是构造函数在创建对象时必须确定对象类型。具体的理解就是:
①首先,在创建一个对象时必须确定其类型。因为类型规定了对象可以进行哪些操作,所以创建对象时必须确定类型,以防止一些不恰当的操作,否则编译器就会报错。
②其次,因为虚函数是在运行时才确定对象的类型的,如果构造函数声明为虚函数,那么在构造对象时,由于这个对象还没创建成功,编译器就不知道对象的实际类型(比如基类还是派生类),就会报错。
③另外,从内存的角度来看,虚函数的调用需要虚表指针,而该指针存放在内存空间中,如果构造函数声明为虚函数,由于对象还没创建,就没有内存空间,因此就没有虚表指针来调用虚函数(构造函数)了。
析构函数为什么一般是虚函数?
为什么这里说的是一般,而不是必须?因为析构函数可以不是虚函数,也可以是虚函数。
①C++默认的析构函数不是虚函数。因为虚函数需要虚函数表和虚表指针,会占用额外的内存空间。对于没有派生类的基类而言,将析构函数定义为虚函数就会浪费内存空间。
②如果存在派生类继承了基类,而基类的析构函数不是声明为虚函数,那么在析构一个指向派生类的基类指针时,就只会调用基类的析构函数,不会调用派生类的析构函数,因此会造成内存泄漏的问题。
子类在析构时,要调用父类的析构函数吗?
不需要自己显示地调用基类的析构函数,因为编译器会自动调用,如果再显示地调用,就会调用了两次,可能会出现意外的错误。
析构函数在析构时的顺序是,先析构派生类然后析构基类,也就是说在析构基类的时候,派生类的全部信息已经销毁了。析构函数调用的顺序与构造函数的顺序刚好相反。
构造函数
-
构造时: 父类构造函数->子类构造函数
-
析构时:子类析构函数->父类析构函数
-
构造函数中的初始化列表初始化成员只和类中定义的变量的顺序相关,与初始化列表中变量排序的顺序无关。
-
类中的const成员,reference成员变量只能用初始化列表初始化,或者赋值一个默认参数。(const是不可修改;而引用是一旦和一个对象绑定到一起后就无法再绑定其他对象,所以也不能再次修改引用。所以二值只能通过初始化列表初始化)
-
构造函数:
-
当类中定义了其他构造函数之后,将不存在默认构造函数,需要自己合成,即再无参构造函数末尾加上“=default”
-
使用explicit修饰构造函数,使其无法进行隐式类型转换。
-
拷贝构造函数的写法型如:Foo(const Foo &),一般发生在使用已有的对象初始化一个正在创建的对象:包含使用等号,传参,返回值<非引用>等
-
移动构造函数的写法型如:Foo(const Foo &&):右值引用,可以避免不必要的拷贝赋值,提升速度。
-
如果我们想禁止拷贝,例如iostream.我们应该将它们(拷贝构造函数、赋值运算符)定义为删除的=delete,表示该函数被禁止使用。
Matrix(const Matrix<_T>&) = delete;
如果要阻止类的继承,则将该类的构造函数或者析构函数设置成private
-
**浅拷贝出错怎么解决?:**1. 定义自己的拷贝构造函数;2. 禁止拷贝(将拷贝构造函数与拷贝运算符设置成删除函数)。
-
**什么时候需要定义自己的拷贝构造函数?:**1.当要处理类的静态变量的时候,例如共享指针中的引用计数。2. 需要进行深拷贝的时候。
-
相关基础知识
构造函数的隐式转换
隐式转换的例子:
class Test {
public:
Test(); // 空构造函数
Test(double a); // 含一个参数的构造函数
Test(int a, int b); // 含两个参数的构造函数
Test(int a, double b = 1.2); // 带有默认初始值的构造函数
~Test();
};
// 调用
Test d1(12.3); // 直接调用含一个参数的构造函数
Test d2 = 1.2; // 也是调用含有一个参数的构造函数,但是包含一次隐式转换
Test d3(12, 13);
Test d3 = 12; // 隐式转换,调用带有默认初始值的构造函数Test(int a, double b = 1.2)
上例中,Test d2 = 1.2
; 就是进行了一次隐式转换,其转换方式是先创建一个临时对象,再对d2进行赋值,即下面的方式:
Test temp(1.2); // 创建临时变量
d2 = temp; // 进行赋值操作
Test d3 = 12
;实际上调用的是含有两个参数,但是其中一个带有默认值的构造函数,也可以进行隐式转换。
explicit关键字
explicit可以阻止构造函数的隐式转换,但是只能对含有一个参数,或者有n个参数,但是其中 n-1 个参数是带有默认值的构造函数有效,其余的构造函数,explicit无法进行约束。
下面代码是explicit的使用:
class Test {
public:
Test() {} // 空构造函数
explicit Test(double a); // 该构造函数无法进行隐式类类型转换
explicit Test(int a, int b); // 含有两个参数,explicit对其无效,然而该构造函数本身也是不能隐式转换的
explicit Test(int a, double b = 1.2); // 该构造函数无法进行隐式类类型转换
~Test() {}
};
// 调用
Test d1(12.3); // 正确
Test d2 = 1.2; // 错误,不能进行隐式类类型转换
Test d3(12, 13); // 正确
Test d3 = 12; // 错误,不能进行隐式类类型转换
使用关键字explicit之后,构造函数就不能进行隐式类类型转换,但是其本身还是可以进行double向int类型的显示类型转换的。
隐式转换就是=赋值,如string s = "abcde";vector<int> v = { 1,2,3,4,5 }
。