C++拷贝构造函数、构造函数和析构函数

  1. encapsulation
    1. 减少类之间的耦合
    2. 类内部的结构可以自由的进行修改
    3. 对成员进行控制
    4. 对代码的理解性更好
  2. information hidding:不需要知道如何初始化,只需要使用提供的接口
  3. Cfront 第一个C++的编译器,转为C
  • 基于对象:没有继承
  • 面向对象:封装、继承、多态

原因

开发效率、软件外部质量、软件内部质量都得到明显提升

封装 Encapsulation

  • 成员变量
  • 成员函数
  • 头文件、源文件:C++是一个个编译单元进行编译,所以需要提前知道其他编译单元的相关信息(存储在头文件中),只需要知道声明,不需要知道具体定义,减少编译复杂度

将方法放在头文件中时,会将该方法当作inline函数。
主调函数运行时,如果有其他函数,会先运行其他函数,在返回主调函数。如果其他函数很短,则会在调用上消耗太多时间,所以需要变为内联函数–直接用函数体代替函数调用,代码展开,提高性能。但是,如果函数体很大时,会把代码变得很长。所以,一般的**set****函数 ****get**函数、代码十行之内、没有**for**循环、没有**switch**语句声明成内联函数


为了优化编译,使用其他编译单元时,先不引入进来,而是在链接过程中,保证有定义即可。所以需要先声明,再使用,告诉本编译单元,该函数是合法的,所以需要头文件

  1. 本地单元进行编译时,头文件和源文件是一致的,肯定是合法的,减少了编译时的依赖关系,只需要和头文件建立依赖关系就可以了。
  2. 减少编译时引入的内容
  3. 把定义和声明放入一块,是为了支持inline直接替换函数调用。如果头文件中没有具体声明,则无法使用inline。所以,inline要求头文件中必须要有声明
  4. 所以**inline**过多,会使编译单元过于庞大,不适合写在头文件中,而是写在源文件中

构造

构造函数

  • 如果提供了有参构造函数,则编译系统不再提供默认构造函数
  • 当类中未提供构造函数时,由编译系统提供
  • 如果没有指定c++默认初始化,则各种变量都会有不确定的值
  • 成员变量如果是成员对象,则总是会初始化的,需要为成员对象设置构造函数

image.png

  • 全局变量和静态变量,未初始化,默认为0
  • 局部变量、成员变量,未初始化,默认为不确定的值
  • 编译系统提供的默认构造函数不会对成员变量进行处理,主要功能是完成对象的初始化,创建标识符,开辟内存空间,最后再根据传入的参数或者默认值进行对数据的处理。
  • 构造函数可定义为private,避免在其他代码中创建该对象,所以只能通过类内部的方法进行创建,而类内部的方法是我自己写的,因此可以接管对象的创建,例如保证单例,或者保证只有十个对象创建
数组构造

image.png

成员初始化表

在冒号和花括号之间的代码部分称为构造函数的初始值列表,它的作用是给创建的对象的某些成员赋初值。这种是在构建对象的时候的初始化,是在对象创建成功之前完成的,和在函数体内赋值是不一样的,函数体内赋值是你的对象成员都已经创建好后对成员进行的赋值。
image.png

  1. static const类的常量
  2. 引用类型必须初始化,不能重新赋值
  3. 构造函数内赋值,实际上是先初始化为默认值,再赋值,相当于两次赋值
  4. 如果使用初始化表,则是在初始化的同时进行赋值,效率更高
  5. 声明处进行初始化最为方便
  6. 在构造函数内进行赋值,成员变量已经初始化了,这次是二次赋值,效率上更低
举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Time {
public:
int hour;
Time(int t) {
this->hour = t;
}
public:

};

class Date {
public:
Date(int year = 1990, int month = 1, int day = 1)
: _year(year), _month(month), _day(day), t(10) {}

void print() {
cout << _year << "-" << _month << "-" << _day << endl;
}

private:
int _year = 1990;
int _month;
int _day;
Time t;
};
  1. 对于const和引用类型,必须要进行初始化,所以他们必须在初始化列表中进行初始化
  2. 当类类型成员有缺省(默认)的构造函数时,在创建对象的时候系统会默认调用,因为不用传参。当你的构造函数不是缺省的,如果不在初始化列表中进行调用构造函数,系统就无法知道怎么调用t的构造函数,那么就无法创建t了。

如上代码中,需要在参数列表中调用t的构造函数才不会出错

作用

image.png
数据太多,可以在声明的同时进行初始化。

注意
  • 在上面的初始列表中,每个成员只能出现一次,因为一个变量多次初始化是无意义的。
  • 初始化列表的顺序并不限定初始化的执行顺序。成员的初始化顺序是与类中定义的顺序保持一致。最好让构造函数初始值的顺序与成员声明的顺序保持一致。

析构函数

程序员负责资源的申请和释放
类的析构函数,它是类的一个成员函数,名字由波浪号加类名构成,是执行与构造函数相反的操作:释放对象使用的资源,并销毁非static成员。
同样的,我们来看看析构函数的几个特点:

  1. 函数名是在类名前加上~,无参数且无返回值。
  2. 一个类只能有且有一个析构函数,如果没有显式的定义,系统会生成一个缺省的析构函数(合成析构函数)。
  3. 析构函数不能重载。每有一次构造函数的调用就会有一次析构函数的调用。

image.png

  1. 对象离开作用域
  2. 使用delete方法调用
  3. 作用:把对象在运行中获得的额外资源进行释放

声明为private

  1. 禁止用户对此类型的变量进行定义,即禁止在栈内存空间内创建此类型的对象。要创建对象,只能用 new 在堆上进行。
  2. 禁止用户在程序中使用 delete 删除此类型对象。对象的删除只能在类内实现,也就是说只**有类的实现者才有可能实现对对象的 **delete**,用户不能随便删除对象。如果用户想删除对象的话,只能按照类的实现者提供的方法进行 **
  3. 如果一个类不打算作为基类,通常采用的方案就是将其析构函数声明为private,限制栈对象,却不限制继承

image.png
系统无法调用析构函数,因为是自动消亡的,内存分配在栈中,离开作用域就会自动消亡
image.png
通过将对象的析构函数定义为**private**,强制在堆上分配内存,场景:栈的内存有限,对象的内存很大。
image.png
这种方法也能够将p指针重新定义为空指针,更好

栈对象的生命

  1. 会移动栈顶指针以“挪出”适当大小的空间
  2. 在这个空间上直接调用对应的构造函数以形成一个栈对象
  3. 当函数返回时,会调用其析构函数释放这个对象
  4. 调整栈顶指针收回那块栈内存。

GC 垃圾回收

  1. 存在效率障碍,发生时间不确定
  2. 存在不能使用GC的场合
  3. 只能回收内存,不能回收文件操作的句柄等 finalize
  4. 不能由程序员自己控制

RAII Resource Acquisition Is Initialization

资源获取就是初始化

  1. 什么时候获取什么时候释放都是确定的
  2. 对象获得的资源都是要在析构函数中释放的
  3. 栈上的内存资源自动释放,堆上的内存资源需要通过析构函数释放

拷贝构造函数

使用场景

  1. 一个对象以值传递的方式传入函数
  2. 一个对象以值传递的方式从函数中返回
  3. 一个对象需要通过另外一个对象进行初始化
  • 创建对象时,用一个同类的对象对其初始化
  • 自动调用:Test(Test &c_t)是自定义的拷贝构造函数,**拷贝构造函数的名称必须与类名称一致**,函数的形式参数是**本类型的一个引用变量,且必须是引用**

image.png

  1. 使用引用:如果不写引用,则传参本身就会引发拷贝构造函数,导致递归
  2. 使用const:防止拷贝时值被修改
  3. 默认拷贝构造函数:
    1. 逐个成员初始化
    2. 对于对象成员,该函数是递归的

需要深拷贝时,要自己提供拷贝构造函数

浅拷贝 深拷贝

  • 如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。
  • 自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。
  • 浅拷贝:在某些状况下,类内成员变量需要动态开辟堆内存,如果实行浅拷贝,就是把对象里的值完全复制给另一个对象

深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
using namespace std;
class CA
{
 public:
  CA(int b,char* cstr)
  {
   a=b;
   str=new char[b];
   strcpy(str,cstr);
  }
  CA(const CA& C)
  {
   a=C.a;
   str=new char[a]; //深拷贝
   if(str!=0)
    strcpy(str,C.str);
  }
  void Show()
  {
   cout<<str<<endl;
  }
  ~CA()
  {
   delete str;
  }
 private:
  int a;
  char *str;
};

int main()
{
 CA A(10,"Hello!");
 CA B=A;
 B.Show();
 return 0;
}
浅拷贝危害

如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。

自定义

程序员如果不会去做一件事,则编译器会接管,但如果程序员接管了,则编译器什么都不做
image.png

易错

显式地定义了析构函数的情况下,应该也把拷贝构造函数和赋值操作显式定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Date
{
public:
Date(int year=1990,int month=1,int day=1)
: _year(year),_month(month), _day(day)
{
p = new int;
}
~Date()
{
delete p;
}

private:
int _year=1990;
int _month;
int _day;
int *p;
};

成员中有动态开辟的指针成员,在析构函数中对它进行了delete,如果不显式的定义拷贝构造函数,当你这样:Date d2(d1)来创建d2,我们都知道默认的拷贝构造函数是浅拷贝,那么这么做的结果就会是d2的成员p和d1的p是指向同一块空间的,那么调用析构函数的时候回导致用一块空间被释放两次,程序会崩溃的哦!

移动构造函数

A&&右值引用
左值:赋值操作符左边的值。是可以赋值的,通常是一个变量
右值:赋值操作符右边的值。是一个值,通常是一个常数、表达式、函数调用

  • 不能把右值绑定在非const的引用上
  • 临时变量在再次赋值时可能已经被销毁了
  • 右值只能绑定在常量引用const int &z = 5
  • 右值通常不能修改
  • 右值引用可以绑定在右值引用上

举例

使用swap
image.png
缺点:总是需要不断拷贝

  • 移动构造:把移动完的指针置为**Null**,防止二次释放
  • 右值绑定在右值引用上时,则右值可以修改了,因为获得了其对应的内存
  • 右值引用是为了提高效率
  • 没有定义拷贝构造、拷贝赋值、析构函数,则会提供移动构造函数:移动构造是为了降低拷贝的消耗,一旦定义了拷贝构造,则编译器不再提供默认
  • 定义了析构函数,是对申请资源进行释放,额外的资源需要如何拷贝、如何移动,编译器不知道,所以不会提供移动构造

类型的匹配顺序

优先级:不需要进行数据转换的优先

  1. 如果既有移动构造又有拷贝构造:
    1. 普通变量:调用拷贝
    2. 右值(临时变量):调用移动
  2. 临时变量的值不能绑定到左值上
  3. 临时变量+const,可以绑定到
  4. 拷贝

五三原则

在c++ 中,当我们定义一个类时,我们显式或隐式地定义了此类型的对象在拷贝、赋值和销毁时做什么?
一个类通过定义三种特殊成员成员函数来控制这些操作:拷贝构造函数、拷贝赋值函数、析构函数。
什么是三法则
C++三法则:如果需要析构函数,则一定需要拷贝构造函数和拷贝赋值操作符。
如何理解这句话,通常,若一个类需要析构函数,则代表其合成的析构函数不足以释放类所拥有的资源,其中最典型的就是指针成员。
所以,我们需要自己写析构函数来释放给指针所分配的内存来防止内存泄露。
那么为什么说“一定需要拷贝构造函数和赋值操作符”呢?
原因还是这样:类中出现了指针类型的成员。有指针类型的成员,我们必须防止浅拷贝问题,所以,一定需要拷贝构造函数和赋值操作符,这两个函数是防止浅拷贝问题所必须的。
什么是五法则
在较新的 C++11 标准中,为了支持移动语义,又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数,所以又称为“C++五法则”;
也就是说,“三法则”是针对较旧的 C++89 标准说的,“五法则”是针对较新的 C++11 标准说的;为了统一称呼,后来人们干把它叫做“C++ 三/五法则”;
因此,如果自定义了拷贝构造函数/拷贝赋值/析构函数,则不会提供默认的移动构造函数/移动赋值函数