C++ - 构造函数与析构函数

C++ 构造函数 & 析构函数

构造函数

  • C++规定,每个类必须有构造函数,没有构造函数就不能创建对象。
  • 若没有提供任何构造函数,那么c++提供自动提供一个默认的构造函数,该默认构造函数是一个没有参数的构造函数,它仅仅负责创建对象而不做任何赋值操作。
  • 只要类中提供了任意一个构造函数,那么c++就不在自动提供默认构造函数。
  • 构造函数在类中的定义和类名相同,并且没有任何返回类型。
  • C++规定如果一个类对象是另外一个类的数据成员,那么在创建对象的时候系统将自动调用哪个类的构造函数。
  • 一个类的成员如果是另外一个类的对象的话,不能在类中使用带参数的构造函数进行初始化
  • 构造函数定义为私有,可以防止该类被直接实例化。一般用于Singleton实现

拷贝构造函数赋值运算符

  • 在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个 拷贝构造函数赋值运算符它们只会按位拷贝对象所在的内存(浅拷贝)
  • 还有一点需要注意的是,拷贝构造函数必须以引用的方式传递参数 (const reference)
  • 这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。
  • 拷贝构造函数和赋值运算符的行为比较相似,都是将一个对象的值复制给另一个对象;但是其结果却有些不同:
    • 拷贝构造函数使用传入对象的值生成一个新的对象的实例,
    • 而赋值运算符是将对象的值复制给一个已经存在的实例。
  • 这种区别从两者的名字也可以很轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。
  • 调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。
  • 调用拷贝构造函数主要有以下场景:
    • 对象作为函数的参数,以值传递的方式传给函数。 
    • 对象作为函数的返回值,以值的方式从函数返回
    • 使用一个对象给另一个对象初始化
  • 看下面一个例子,程序会crash,正确的做法就是自己写 复制构造函数
    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
    #include <string>

    class String
    {
    private:
    char* m_Buffer;
    unsigned int m_Size;
    public:
    String(const char* s)
    {
    m_Size = strlen(s);
    m_Buffer = new char[m_Size + 1];
    memcpy(m_Buffer, s, m_Size);
    m_Buffer[m_Size] = 0;
    }

    ~String()
    {
    delete[] m_Buffer;
    }
    };

    int main()
    {
    String str = "QinHan"; // 隐式转换
    // str2 & str3 他们在析构函的时候都会出现重复 delete 的问题
    // `m_Buffer` 指向 heap 上的同一个地址,会被释放两次,crash
    // 另外,下面两种写法其实都是调用了拷贝构造函数,因为都有新的对象产生
    String str2 = str;
    String str3(str);
    return 0;
    }

explicit关键字

  • 如果类A有一个构造函数 A(int n),则以下代码是正确的
    1
    A a = 3; // “单参数构造函数”被自动型别转换(这是一个隐式转换)
  • 可以通过explicit关键字,阻止“以赋值语法进行带有转型操作的初始化”。
  • 还是上述的例子,构造函数改成 explicit A(int n),则上述写法无法编译通过,需要改成:A a(3);

一种特殊的构造函数的写法

  • 如果按照一般的写法,成员变量会被赋值两次,第一次是初始值,然后再进行一次赋值
    1
    2
    3
    4
    5
    class Entity
    {
    Entity()
    :name("Unknown"), score(0) {}
    }

析构函数

  • 析构函数 的定义:析构函数也是特殊的类成员函数,它没有返回类型,没有参数,不能随意调用,也没有重载,只有在类对象的生命期结束的时候,由系统自动调用。

  • 析构函数与构造函数的不同:析构函数与构造函数最主要大不同就是在于调用期不同,构造函数可以有参数可以重载!

  • 成员的构造是按照在类中定义的顺序进行的,而不是按照构造函数说明后的冒号顺序进行构造的。

Q&A

delete 某个类的指针会调用该类的析构函数么?

  • 会的,如果不调用的话怎么析构一个类.

  • 不过指针所指向的对象必须是在堆中用new关键词开创的

  • 如果指针指向的是一个栈中的对象,会引起调用两种析构函数而导致程序错误

    1
    2
    3
    4
    class ob
    ob a;
    ob *p=&a;
    delete p; // 这样会导致调用两次析构函数.是会引起程序错误的
  • 只有

    1
    2
    3
    class ob
    ob * p= new ob;
    delete p; //这样是正确的

对于 new 创建的对象,只有调用 delete 才能析构。