面向对象学习笔记3

发布时间 2023-10-29 01:01:37作者: ZTer

构造与析构函数

  • 生命从出生到死亡,拥有它的一生,对象也是如此。
    构造与析构函数,则见证了一个对象的创生与消亡。

对象的初始化与构造函数

初始化

  • Background Information:
    在 C++ 中, 为了提高效率,申请内存之后并不会对内存所占的单元进行清空操作,所以初始化这件事就显得尤为重要。

例如下面这段代码:

class Point{
  public:
    void Init(const int &x, const int &y);
    void Print();
  private:
    int x, y;
};

void Point::init(const int &x, const int &y)
{
  this -> x = x, this -> y = y;
}

void Point::Print()
{
  cout << x << ' ' << y << endl;
}

int main()
{
  Point A;
  A.Init(1, 2);
  A.Print();
  return 0;
}

这是一个坐标点 Point 的类,使用 void Init(const int &x, const int &y) 来初始化它。

但是这样存在一个问题,每次创建一个对象都要调用一次 Init 函数,不仅麻烦,程序员一不小心甚至可能会忘记调用它,这样会导致程序中存在一些隐式的 bug。

聪明的人们针对这个问题,创建了一种新的函数——构造函数。

构造函数

  • 构造函数具有如下特点
    1. 构造函数没有返回类型,并且它的名字和类的名称是一模一样的
    2. 构造函数在创建对象的时候自动调用,并且程序员无法干预这一过程。
    3. 每个类都有构造函数,如果程序员未给出构造函数,那么编译器会自动生成一个空的构造函数。
    4. 构造函数可以带参数,也可以不带参数,但使用方式有区别,下面细说。

下面这段代码使用构造函数替代了 Init 函数,创建对象的时候会自动根据给出的参数初始化对象。

class Point{
  public:
    Point(const int &x, const int &y);
    void Print();
    void Move(const int &dx, const int &dy);
  private:
    int x, y;
};

Point::Point(const int &x, const int &y)
{
  this -> x = x, this -> y = y;
}

void Point::Print()
{
  cout << x << ' ' << y << endl;
}

int main()
{
  Point A(1, 2);
  //带参数构造函数用法:class_name object_name(parameters);
  A.Print();
  return 0;
}

如果我们忘记了给出参数,编译器便会报错 “未找到对 Point 的默认构造函数”。

什么是 “默认构造函数” ?其实并不是前面说的当程序员未给出构造函数的时候编译器自动补充的那个空的构造函数(自动构造函数),而是泛指 “不含参数的构造函数” 。

假设构造函数是带参数的,那么它如果想要在对象创建的时候就进行调用,那么我们就必须在创建对象的同时给出参数,否则它就无法运行。

如果构造函数是一个默认构造函数,那么直接 Point A; 就是合法的,此时程序会自动调用空的构造函数对 A 进行初始化。

对象的销毁与析构函数

对象的销毁

在说析构函数之前,我们应该先搞清楚什么时候需要销毁对象,也就是一个变量的作用域是什么。

  • 一个变量的作用域是把他包围起来的离它最近的一对大括号之间的部分。

也就是说,当遇到 '}' 的时候,从这个 '}' 到上面的第一个 '{' 之间定义的所有对象都要被销毁。

析构函数

  • 析构函数具有以下特点:
    1. 析构函数在对象销毁的时候自动调用。
    2. 析构函数没有参数。
    3. 析构函数的名字是在构造函数之前加上字符 '~' 。
    4. 未调用构造函数的对象无法调用析构函数。

下面这段代码的运行结果可以帮助我们直观的看到构造函数和析构函数的调用时间和顺序。

#include <iostream>

using namespace std;

class Point
{
  public: 
    Point()
    {
      x = y = 0;
      cout << "Object constructed" << endl;
    }
    ~Point()
    {
      cout << "Object distoryed" << endl;
    }
  private:
    int x, y;
};

int main()
{
  cout << "Program Started" << endl;
  {
    cout << "Going to define object" << endl;
    Point A;
    cout << "Going to distory object" << endl;
  }
  cout << "Program Ended" << endl;
  return 0;
}

运行结果如下:

可以看出来,在定义 A 的时候,调用了构造函数;碰到 '}' 的时候,调用了析构函数。

未执行构造函数的对象无法被析构

下面我们来详说开头的第四条,但在这之前,要先了解一下 C++ 是如何分配内存的。

  • 当代码运行遇到 '{' 的时候,为这一对大括号内所有的变量分配内存。

注意,我说的是分配内存,而不是定义,分配内存的时候是不会调用构造函数的。根据上面的例子可以得知,代码运行到定义对象的一行时,才会调用构造函数。

这样就出现了一些问题,比如这样:

void f(int i)
{
  if(i < 10)
    goto LOOP1;
  X x1;
  LOOP1:
}

这段代码中存在一个 goto 语句,可能导致定义对象 x1 的一行代码被跳过。

但是上面已经说过,在 f 函数执行的时候,系统就已经给 x1 分配内存了,那么 f 函数结束的时候,就需要把 x1 销毁并执行析构函数。

但是这是做不到的。

你可能觉得不对劲,那么我们来打个比方。

假设我们把分配的内存比作阎王的生死簿,对象比做人,构造函数比作出生,析构函数比作死亡。

我们分配内存的时候,就相当于在阎王的生死簿上写下了对象的名字。现在 f 函数即将结束执行,对象大限已至,阎王要来索对象的命了。但是定义对象的一行代码被跳过了,这就导致构造函数没有被调用——也就是对象根本就没出生,而阎王却需要让这个没出生的人去死。

为了给阎王减少一些工作量,这样的代码是不被允许编译的。

但是特殊的,对象的声明被跳过是无伤大雅的,譬如这样:

void f(int i)
{
  if(i < 10)
    goto LOOP1;
  extern X x1;
  LOOP1:
  X x1;
}

这样的代码是可以通过编译的,因为定义 x1 的这一行一定会执行,也就是一定会调用构造函数,从而销毁对象的时候,析构函数也可以顺利调用。