浅析Cpp中的构造函数和析构函数

构造函数

分类

构造函数分为以下四类:

  • ⽆参数构造函数:如果没有明确写出⽆参数构造函数,编译器会⾃动⽣成默认的⽆参数构造函数。
  • ⼀般构造函数:创建对象时根据传⼊参数不同调⽤不同的构造函数。
  • 拷⻉构造函数:拷⻉构造函数的函数参数为对象本身的引⽤,⽤于根据⼀个已存在的对象复制 出⼀个新的该类的对象。
  • 类型转换构造函数:根据⼀个指定类型的对象创建⼀个本类的对象,也可以算是⼀般构造函数的⼀种。

这里注意,还有一个与拷贝构造相关的运算符重载:

  • 赋值运算符的重载:类似拷⻉构造函数,将=右边的本类对象的值复制给=左边的对象,它不属于构造函数,=左右两边的对象必需已经被创建。如果没有显示的写赋值运算符的重载,系统也会⽣成默认的赋值运算符,做⼀些基本的拷⻉⼯作。

初始化表(参考)

1
2
3
4
5
6
7
class Test
{
int a;
double b;
string c;
Test(int a1, double b1, string c1): a(a1), b(b1), c(c1){}
};

性能消耗

构造函数的执行分两个阶段:

  1. 初始化阶段:类中成员变量的初始化
  2. 计算阶段:在构造函数的函数体内执行

如果不使用初始化列表,类会在初始化阶段先调用默认构造参数对成员变量进行初始化,然后在函数体内调用拷贝构造函数利用传入参数对成员变量进行赋值操作。

如果使用初始化列表,类只会调用一次拷贝构造函数对成员变量进行初始化赋值操作,省去了调用默认构造函数的性能消耗。

必须使用初始化列表的情况

常量成员:因为常量成员在定义时就必须进行初始化
引用类型:引用相当于指针常量,不能修改指向,即不能赋值
没有默认构造函数的类成员:在初始化阶段无法调用默认构造函数进行初始化

(注意)类成员的初始化顺序取决于在类中定义的顺序,和初始化列表的顺序无关

和虚函数的关系

虚函数指针

虚函数指针的初始化是在构造函数中完成的。

构造函数为什么⼀般不定义为虚函数

  • 虚函数调⽤只需要知道“部分的”信息,即只需要知道函数接⼝,⽽不需要知道对象的具体 类型。但是,我们要创建⼀个对象的话,是需要知道对象的完整信息的。特别是,需要知 道要创建对象的确切类型,因此,构造函数不应该被定义成虚函数;
  • ⽽且从⽬前编译器实现虚函数进⾏多态的⽅式来看,虚函数的调⽤是通过实例化之后对象 的虚函数表指针来找到虚函数的地址进⾏调⽤的,如果说构造函数是虚的,那么虚函数表 指针则是不存在的,⽆法找到对应的虚函数表来调⽤虚函数,那么这个调⽤实际上也是违 反了先实例化后调⽤的准则。

拷贝构造函数的调用

拷贝构造函数会在以下几个地方被调用:

  1. 类的一个对象去初始化类的另一个对象时。
  2. 当函数的形参是类的对象,调用函数进行形参和实参的结合时。
  3. 当函数的返回值是对象,函数执行完成返回调用者时。

注意,如果出现以下方式,拷贝构造只会被调用一次,因为编译器对其做出了优化,省去了一次拷贝构造。

1
2
3
4
5
A getA(){
A a;
return a;
}
A a = getA();

析构函数

析构函数没有参数,也没有返回值,⽽且不能重载,在⼀个类中只能有⼀个析构函数。 当撤销对象时,编译器也会⾃动调⽤析构函数。 每⼀个类必须有⼀个析构函数,⽤户可以⾃定义析构函数,也可以是编译器⾃动⽣成默认的析构函数。⼀般析构函数定义为类的公有成员。

析构函数一般写为虚函数

是为了降低内存泄漏的可能性。举例来说就是,⼀个基类的指针指向⼀个派⽣类的对象,在使⽤完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那么编译器根据指针类型就会认为当前对象的类型是基类,调⽤基类的析构函数 (该对象的析构函数的函数地 址早就被绑定为基类的析构函数),仅执⾏基类的析构,派⽣类的⾃身内容将⽆法被析构,造成内存泄漏。 如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执⾏派⽣类的析构函 数,再执⾏基类的析构函数,成功释放内存。

二者共同存在的问题

在构造函数或析构函数中调⽤虚函数会怎样

派生类的自身部分还没有被初始化,对于这种还没有初始化的东西,C++选择当它们还不存在作为⼀种安全的⽅法。 也就是说构造派⽣类的基类部分是,编译器会认为这就是⼀个基类类型的对象,然后调⽤基类类型中的虚函数实现,并没有按照我们想要的⽅式进⾏。析构函数中也同理。

调用顺序

构造函数顺序

  1. 基类构造函数。如果有多个基类,则构造函数的调⽤顺序是某类在类派⽣表中出现的顺序,⽽不是它们在成员初始化表中的顺序。
  2. 成员类对象构造函数。如果有多个成员类对象则构造函数的调⽤顺序是对象在类中被声明的顺序,⽽不是它们出现在成员初始化表中的顺序。
  3. 派⽣类构造函数。

析构函数顺序

  1. 调⽤派⽣类的析构函数;
  2. 调⽤成员类对象的析构函数;
  3. 调⽤基类的析构函数。

delete/default 关键字

delete

用于限制一些默认函数的生成。

例如:需要禁止拷贝构造函数的使用。以前通过把拷贝构造函数声明为private访问权限,这样一旦使用编译器就会报错。

default

如果对构造函数进行了重载,则编译器不会隐式的生成一个默认的构造函数,此时如果调用了默认构造函数会在编译时报错,为了避免这种情况,一般会选择重写默认构造函数,且函数体为空。关键字 =default 优化了这种行为,用该关键字标记重写的默认拷贝构造函数,编译器会隐式生成一个版本,在代码更加简洁的同时,编译器隐式生成的版本的执行效率更高。

1
2
3
4
5
6
7
8
9
class A
{
public:
// 重载构造函数,此时不会隐式生成默认构造函数
A(int num) {}
// 关键字 =default 标记编译器隐式生成该类的默认构造函数,
// 代码更简洁,且隐式生成的版本执行效率更高
A() = default;
};

可见性重写

C++中构造函数和析构函数可被private修饰;并且因为构造函数为私有,所以该类无法拥有派生类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A{
private:
A(){}
~A(){}
public:
static A* get(){
A* a1 = new A();
return a1;
}
void Destroy(){
delete this;
}
};

int main(){
A* a = A::get();
return 0;
}

利用这一机制,可实现限制该类的实例在内存中存在的数量,也可实现不让该实例出现在栈上。