C++ 构造析构函数详细分析

2017-05-31 language c/cpp

在 C++ 中,当类的内存申请完之后,会通过构造函数进行初始化,而构造函数在不同的使用场景下又分成了不同的类型,例如默认构造函数、简单构造函数、复制构造函数、转换构造函数等等。

同时,在涉及到赋值操作时,最好将赋值操作运算符也重载掉,尤其是会动态申请内存的类;另外,随着 C++11 标准的发布,有引入了移动构造函数以及移动赋值运算符。

这就导致构造函数与赋值重载不断重叠,很容易引起混淆,所以,这里就详细介绍其使用方式。

构造函数

C++ 中类内存空间创建完之后,会通过构造函数初始化,可以分成 4 类,如下以 class Rectangle 为例:

  • 默认构造函数,Rectangle() 没有任何参数,生成的对象是默认的参数。
  • 简单构造函数,Rectangle(int heigh, int width) 可以在创建类对象时对类变量进行设置。
  • 复制(拷贝)构造函数,Rectangle(Rectangle &s) 参数时本类对象的引用。
  • 转换构造函数,Rectangle(int w) 形参是其它类型的变量,而且只有一个。

除了构造函数之外,还包括了赋值操作符。

析构函数是类的一种特殊的成员函数,其名称与类的名称是完全相同,只是在前面加了个波浪号 ~ 前缀,但不会返回任何值,也不能带有任何参数。该函数会在删除对象时执行,通常用来释放类申请的资源。

三法则

Rule of Three 三法则,是一个实际使用时的指导方法,简单来说,就是,如果用户显示定义了析构函数、复制构造函数、赋值运算符中的一个,那么也需要定义其它两个。

这一三法则其主要目的是为了避免一些常见的陷阱,如果没有定义这三个函数,编译器会自动创建,而当用户自己定义了其中某一个时,而其它仍然使用编译器默认,那么就可能会引入一些异常。

另外,如果有使用到 RAII ,那么可以不定义析构函数,也被称为二法则;在 C++11 新增了移动构造函数以及移动赋值运算符,也被称为五法则 (之前的三法则严格来说应该是复制赋值运算符)。

也就是说,在需要管理类的资源分配时使用,也就是需要确定如何分配、赋值时是否需要深拷贝、如何释放资源等。

实例

如下是一个简单示例。

#include <vector>
#include <iostream>

class Rectangle {
private:
    double width, height;

public:
    // Default Constructor
    Rectangle() : width(0), height(0) {
        std::cout << "Default Constructor" << std::endl;
    }

    // Simple Constructor
    Rectangle(double w, double h) {
        std::cout << "Simple Constructor" << std::endl;
        width = w;
        height = h;
    }

    // Convert Constructor
    explicit Rectangle(int v) {
        std::cout << "Convert Constructor" << std::endl;
        width = v;
        height = v;
    }

    // Copy Constructor
    Rectangle(const Rectangle &r) {
        std::cout << "Copy Constructor" << std::endl;
        width = r.width;
        height = r.height;
    }

    // Copy-Assignment Operator
    Rectangle &operator=(const Rectangle &r) {
        std::cout << "Copy Assignment Operator" << std::endl;
        if (this == &r) /* self assignment */
            return *this;
        width = r.width;
        height = r.height;
        return *this;
    }

    // Move Constructor
    Rectangle(Rectangle &&r) noexcept {
        std::cout << "Move Constructor" << std::endl;
        width = r.width;
        height = r.height;
    }

    // Move-Assignment Operator
    Rectangle &operator=(Rectangle &&r) noexcept {
        std::cout << "Move Assignment Operator" << std::endl;
        if (this == &r) // self assignment
            return *this;
        width = r.width;
        height = r.height;
        return *this;
    }

    ~Rectangle() {
        std::cout << "Destructor" << std::endl;
    }

    void String() const {
        std::cout << "Width " << width << " Height " << height << std::endl;
    }
};

Rectangle CreateRectangle() {
    return {5, 6};
}

int main() {
    Rectangle r1;           // Default Constructor
    Rectangle r2(3, 4);     // Simple Constructor
    Rectangle r3(3);        // Convert Constructor
    Rectangle r4 = r2;      // Copy Constructor
    Rectangle r5;           // Default Constructor
    r5 = r2;                // Copy-Assignment Operator

    Rectangle&& r6 = CreateRectangle(); // Move Constructor
    Rectangle r7;           // Default Constructor
    r7 = Rectangle(3, 4);   // Simple Constructor + Move Assignment Operator
    Rectangle r8 = 3;       // Convert Constructor + Move Constructor

    r1.String();
    r2.String();
    r3.String();
    r4.String();
    r5.String();
    r6.String();
    r7.String();
    r8.String();
}

简单、转换构造函数

先介绍两个相对来说比较简单的构造函数,也就是简单构造函数和转换构造函数。

简单构造函数

通过入参构造一个类对象。

转换构造函数

用于将其它类型的变量隐式转换为本类对象,一般用于将基本的类型转换成类对象使用,例如上述的 Rectangle(int v) 函数,会将基本 int 类型的变量转换为 Rectangle 类对象。

例如,类重载了 + 运算符,使得两个类对象可以相加,那么如果使用是一个类对象和一个 int 类型对象,那么在相加之前会先将 int 类型转换为类对象,这里用到的就是转换构造函数。

默认构造函数

一般来说,就是没有任何参数传入,例如如下示例。

Rectangle r;

调用该构造函数时不需要传入任何参数,可以是却是没有入参,所有的成员变量初始化为默认值;也可以是有参数列表,但是都指定的默认值,允许调用时不传入任何参数。

Rectangle(void): width(0), height(0) {
	std::cout << "Default Constructor" << std::endl;
}

Rectangle(int w = 0, int h = 0): width(w), height(h) {
	std::cout << "Default Constructor" << std::endl;
}

如果没有定义默认构造函数,编译器会自动生成默认构造函数,注意,并不是所有场景都会生成的,只有需要时才会。而关键时,什么时候才需要默认构造函数。

复制构造函数

在介绍之前,可以先尝试回答几个问题:什么场景会调用复制构造函数?如果没有定义会如何处理?

以如下的 class Rectangle 为例,赋值构造函数声明为 Rectangle(const Rectangle &r) ,注意,拷贝构造函数 必须以引用的方式传参,否在在传值时会再次调用一个拷贝构造函数生成对象,以此往复直至栈溢出。

如果实现时没有定义,编译器会自动生成一个拷贝构造函数和拷贝赋值运算符,当然,如果不需要,可以通过 delete 关键字显示指定不自动生成,这样会导致对象在函数传参时不能通过值传递,而且不能执行赋值运算。

使用场景

拷贝构造函数和赋值运算符比较类似,都是将一个对象的值复制给另一个对象,其区别是:A) 拷贝构造函数使用传入对象的值生成一个新的对象的实例;B) 赋值运算符是将对象的值复制给一个已经存在的实例。关键是是否要生成新的对象。

调用拷贝构造函数主要有以下场景:

  • 对象作为函数的参数,以值传递的方式传给函数。
  • 对象作为函数的返回值,以值的方式从函数返回。
  • 使用一个对象给另一个对象初始化。

可以参考如下的示例,也就是其中的如下几个语句。

Rectangle r3 = r1; // Copy Constructor
Rectangle r4(r1);  // Copy Constructor
FuncFoo(r1);       // Copy Constructor

其中比较容易混淆的是 p3 = p1 ,如果 p3 已经创建完成,那么会调用赋值运算符,如果还未创建,就会调用复制构造函数。

深拷贝 VS. 浅拷贝

默认生成的拷贝构造函数和赋值运算符,只进行简单的值复制,对于像 int string 这类的基本类型是不影响的,两个对象包含了各自的成员。

但是,如果使用的是指针,如果只是值复制,那么会导致两个对象操作的是一个相同的对象。

示例

在调用函数时,编译器会进行部分的优化。

#include <iostream>

class Rectangle {
private:
    int width, height;
public:
    Rectangle() { }
    Rectangle(int w, int h): width(w), height(h) {
            std::cout << "Constructor" << std::endl;
    }
    ~Rectangle() {
            std::cout << "Destructor" << std::endl;
    }
    Rectangle(const Rectangle &r) {
            std::cout << "Copy Constructor" << std::endl;
    }
    Rectangle& operator=(const Rectangle &r) {
            std::cout << "Assignment Operator" << std::endl;
            return *this;
    }
    //Rectangle(const Rectangle &p) = delete;
    //Rectangle& operator=(const Rectangle &p) = delete;
};

void FuncFoo(Rectangle r)
{
}

Rectangle FuncBar(void)
{
    Rectangle r(20, 40);
    return r;
}

int main()
{
    Rectangle r1(10, 20), r2;

    Rectangle r3 = r1; // Copy Constructor
    Rectangle r4(r1);  // Copy Constructor
    r2 = r1;           // Assignment Operator
    FuncFoo(r1);       // Copy Constructor
    r2 = FuncBar();    // Constructor + Assignment Operator
    Rectangle r5 = FuncBar(); // Constructor
}