【C++杂货铺】构造函数和析构函数

简介: 【C++杂货铺】构造函数和析构函数

7bd84868140e4dad95cea952b3b0ca29.gif

一、类的六个默认成员函数

📖默认成员函数

用户没有显式实现,编译器会自动生成的成员函数,称为默认成员函数。

9385d391062c4bf0b1c9e868bdadc2c5.png



构造函数:完成对象的初始化工作。

析构函数:完成对象空间的清理工作。

拷贝构造:使用同类对象初始化创建对象。

赋值重载:把一个对象赋值给另外一个对象(该对象已存在)。

取地址重载:获取对象的地址,这两个很少自己实现。

注意:构造和析构函数,不是创建对象和销毁对象。对象的创建和销毁都是编译器做的工作。

二、构造函数

📖为什么要有构造函数?

为了避免每次创建对象后,都要去调用专门的成员函数设置对象的信息,这样很麻烦,并且容易遗忘,那就想着能否在创建对象的同时,就将信息设置进去。因此,就有了构造函数。以日期类为例:

class Date
{
public:
  void Init(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;//第一步:创建对象
  d1.Init(2022, 7, 5);//调用初始化函数
  d1.Print();
  Date d2;
  d2.Init(2022, 7, 6);
  d2.Print();
  return 0;
}

如上面的代码,每次创建一个日期类对象后,都要手动的去调用Init函数,完成对象的初始化,整个过程繁琐,而且容易遗忘,为此,提出了构造函数的概念。

📖定义

构造函数是一个特殊的成员函数,名字与类名相同,创建对象的时候由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象的整个生命周期中只调用一次。

📖构造函数的特性

函数名与类名相同。

无返回值。(无需void)

对象实例化时编译器自动调用对应的构造函数。

构造函数可以重载。

🎊示例:

 class Date
 {
  public:
      // 1.无参构造函数
      Date()
     {}
      // 2.带参构造函数
      Date(int year, int month, int day)
     {
          _year = year;
          _month = month;
          _day = day;
     }
  private:
      int _year;
      int _month;
      int _day;
 };
  void TestDate()
 {
      Date d1; // 调用无参构造函数
      Date d2(2015, 1, 1); // 调用带参的构造函数
      Date d3();
 }

注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明。即Date d3();是声明了一个d3函数,该函数无参,返回一个日期类对象,并不是创建了一个日期类对象d3。

构造函数在语法上可以是私有的,但是在创建对象的时候就调不动了。在单例模式中,会把构造函数搞成私有,具体的我们以后再说。

📖编译器生成的构造函数

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义,编译器将不在生成。

🎊示例:

class Date
{
 public:
  /*
  // 如果用户显式定义了构造函数,编译器将不再生成
  Date(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  */
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
 private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  return 0;
}

将Date类中的构造函数注释掉后,代码可以编译通过,因为编译器生成了一个无参的默认构造函数。将Date类中的构造函数放开后,代码编译失败,因为一旦显式定义了任何构造函数,编译器将不再生成默认构造函数。而此时Date中的构造函数需要三个参数,Date d1;会去调用无参的构造函数,但是当前类中没有无参的构造函数,所以编译会报错。

📖编译器生成的构造函数干了什么?

C++中把类型分为内置类型和自定义类型。内置类型就是语言提供的数据类型,如:int、char……自定义类型就是我们使用class、struct、union等自己定义的类型。(所有类型的指针都属于内置类型)。

编译器生成的默认构造函数,对内置类型不做处理,对自定义类型,会去调用它的默认构造函数。

🎊示例:

//先定义一个时间类
class Time
{
public:
  Time()
  {
    cout << "Time()" << endl;
    _hour = 0;
    _minute = 0;
    _second = 0;
  }
private:
  int _hour;
  int _minute;
  int _second;
};
//再定义一个日期类
class Date
{
private:
  // 基本类型(内置类型)
  int _year;
  int _month;
  int _day;
  // 自定义类型
  Time _t;
};
int main()
{
  Date d;
  return 0;
}

58ca4b22bb7d44aeb95c849c7ea77d31.png

上面代码,先定义了一个时间类Time,它的成员变量都是内置类型,给这个类写了一个无参的构造函数,接下来,定义了一个日期类Date,他有四个成员变量,其中_year、_month、 _day都是内置类型,_t是自定义类型,并且,我们没有写日期类的构造函数,这意味着,在创建对象的时候,会去使用编译器生成的无参默认构造函数。

📖内置类型给默认值

C++11中针对内置类型成员不初始化的缺陷,打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

🎊示例:

class Time
{
public:
  Time()
  {
    cout << "Time()" << endl;
    _hour = 0;
    _minute = 0;
    _second = 0;
  }
private:
  int _hour;
  int _minute;
  int _second;
};
class Date
{
private:
  // 基本类型(内置类型)
  int _year = 1970;//给默认值
  int _month = 1;
  int _day = 1;
  // 自定义类型
  Time _t;
};
int main()
{
  Date d;
  return 0;
}

e73148e3fbb846028275f791fbfd4612.png

📖默认构造函数

我们没写编译器自动生成的构造函数、无参构造函数、全缺省构造函数,这三种都叫做默认构造函数,它们都有一个共同的特点:可以不用传参。默认构造函数只能有一个,后面俩,在语法上可以构成函数重载,但是在无参调用的时候,会发生歧义,出现调用不明确。

de3522236f664b2c8a30a6c1da722e62.png

注意:要把默认构造函数和默认成员函数区分清楚,默认成员函数是我们不写编译器会自动生成的,默认构造函数是不需要传参的构造函数。编译器生成的构造函数,既是默认构造函数,同时也是默认成员函数。

📖总结

一般情况下,都需要我们自己写构造函数。如果满足以下情况,即:内置类型的成员变量都有默认值,且初始化符合我们的要求,自定义类型都定义了默认构造,此时可以考虑不写构造函数,使用编译器自动生成的默认构造函数。自定义类型如果没有对应的构造函数,那就意味着初始化自定义类型需要传参,此时必须自己写构造函数,并且还会用到初始化列表。

三、析构函数

📖定义

与构造函数的功能相反,析构函数不是完成对对象本身的销毁,局部对象的销毁工作是由编译器完成的。而对象在销毁的时候,会自动调用析构函数,完成对象中资源的清理工作。

📖特性

  • 析构函数名是在类名前加上~。
  • 无参数无返回值类型。
  • 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。
  • 对象生命周期结束时,C++编译器自动调用析构函数。

小Tips:析构函数不能重载。

🎊示例:

typedef int DataType;
class Stack
{
public:
  Stack(size_t capacity = 3)
  {
    _array = (DataType*)malloc(sizeof(DataType) * capacity);
    if (NULL == _array)
    {
      perror("malloc申请空间失败!!!");
      return;
    }
    _capacity = capacity;
    _size = 0;
  }
  void Push(DataType data)
  {
    // CheckCapacity();
    _array[_size] = data;
    _size++;
  }
  // 其他方法...
  ~Stack()//析构函数
  {
    if (_array)
    {
      free(_array);
      _array = NULL;
      _capacity = 0;
      _size = 0;
    }
  }
private:
  DataType* _array;
  int _capacity;
  int _size;
};
void TestStack()
{
  Stack s;
  s.Push(1);
  s.Push(2);
}


b331d59d5f40464fbadb74c76b7531c4.png

Stack中的成员变量_array、_capacity、_size都是内置类型,所以在对象s生命周期结束要销毁的时候,不需要资源清理,最后系统直接将其内存回收即可,而_array指向的空间是在堆区上申请的,这块空间不会随着对象生命周期的结束而自动释放(归还给操作系统),所以_array被回收后,就找不到动态申请的那块空间,会造成内存泄漏,因此在对象销毁前,要通过析构函数去释放成员变量_array指向的空间,这就是析构函数的作用。

📖编译器生成的析构函数干了什么?

我们不写,编译器会自动生成一个析构函数。该析构函数对内置类型不做处理,对自定义类型会去调用它的析构函数。

🎊示例:

class Time
{
public:
  ~Time()
  {
    cout << "~Time()" << endl;
  }
private:
  int _hour;
  int _minute;
  int _second;
};
class Date
{
private:
  // 基本类型(内置类型)
  int _year = 1970;
  int _month = 1;
  int _day = 1;
  // 自定义类型
  Time _t;
};
int main()
{
  Date d;
  return 0;
}

24c18a9c0452410f8460030feb38aea3.png

main方法中创建了Date对象d,而d中包含4个成员变量,其中_year、_month、_day、三个是内置类型成员,对象销毁时不需要资源清理,最后系统直接将其内存回收即可,而_t时Time类对象,在d销毁时,要将器内部包含的Time类的_t对象销毁,所以要去调用Time类的析构函数。但是main函数中不能直接调用Time类的析构函数,实际销毁的是Date类对象d,所以编译器会调用Date类的析构函数,而Date类没有显示提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数。

📖总结

一般情况下,有动态申请资源,就需要显式的写析构函数来释放资源,没有动态申请的资源,可以不写析构函数,需要释放资源的成员都是自定义类型,也不需要写析构函数。

🎁结语:

 今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!


目录
相关文章
|
2月前
|
编译器 C++
C++ 类构造函数初始化列表
构造函数初始化列表以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。
72 30
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
18 1
|
1月前
|
C++
C++构造函数初始化类对象
C++构造函数初始化类对象
16 0
|
1月前
|
C++
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
C++入门4——类与对象3-2(构造函数的类型转换和友元详解)
20 0
|
3月前
|
编译器 C++
C++的基类和派生类构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。 在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。 这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:
|
4月前
|
编译器 C++
【C++】详解构造函数
【C++】详解构造函数
|
5月前
|
存储 编译器 C++
【C++】类和对象④(再谈构造函数:初始化列表,隐式类型转换,缺省值
C++中的隐式类型转换在变量赋值和函数调用中常见,如`double`转`int`。取引用时,须用`const`以防修改临时变量,如`const int& b = a;`。类可以有隐式单参构造,使`A aa2 = 1;`合法,但`explicit`关键字可阻止这种转换。C++11起,成员变量可设默认值,如`int _b1 = 1;`。博客探讨构造函数、初始化列表及编译器优化,关注更多C++特性。
|
5月前
|
存储 编译器 C语言
【C++】类和对象②(类的默认成员函数:构造函数 | 析构函数)
C++类的六大默认成员函数包括构造函数、析构函数、拷贝构造、赋值运算符、取地址重载及const取址。构造函数用于对象初始化,无返回值,名称与类名相同,可重载。若未定义,编译器提供默认无参构造。析构函数负责对象销毁,名字前加`~`,无参数无返回,自动调用以释放资源。一个类只有一个析构函数。两者确保对象生命周期中正确初始化和清理。
|
5月前
|
编译器 C语言 C++
【C++】:构造函数和析构函数
【C++】:构造函数和析构函数
47 0
|
23天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
21 4