首页 > 编程笔记 > C++笔记 阅读:20

C++构造函数和析构函数(非常详细)

在现实世界中,每个事物都有其生命周期,从诞生到消亡。程序是对现实世界的反映,其中的对象代表了现实世界的各种事物,自然也有生命周期,会被创建和销毁。

C++ 程序中,一个对象从创建到销毁,往往需要处理很多复杂的事情。例如,在创建对象时,需要进行大量初始化工作,如设置某些属性的初始值;而在销毁对象时,需要执行一些清理操作,最重要的是释放掉申请的资源和关闭之前打开的文件。

为了完成对象的创建和销毁,C++ 中的类提供了两个特殊的函数,分别是构造函数(Constructor)和析构函数(Destructor),它们会在对象创建和销毁时会被自动调用。

C++构造函数

构造函数在对象创建时自动调用,用于完成对象的初始化工作。构造函数的使用确保了对象在被使用之前已经具备了合理的初始状态。例如,它可以为对象的属性设置初始值,或者分配必要的资源。

正如人的性别在出生时就已经确定,对象的关键属性也应在构造时明确。C++ 要求每个类至少有一个构造函数。如果程序员没有为类显式声明构造函数,编译器将自动提供一个默认构造函数。这个默认构造函数不接受任何参数,并且不执行任何初始化操作。

然而,在许多情况下,我们需要在对象创建时执行特定的初始化逻辑。这时,我们可以自定义构造函数来满足这些需求。例如:
class 类名
{
public:
    // 构造函数
    类名(参数列表)
    {
        // 对类进行构造,完成初始化工作
    }
};
因为构造函数具有特殊性,所以它的定义也比较特殊。

首先,在大多数情况下,构造函数的访问级别应该是公有(public)的,因为构造函数需要被外界调用以创建对象。只有在少数的特殊用途下,才会使用其他访问级别。

其次,构造函数没有返回值类型,因为它只是完成对象的创建,并不需要返回数据,自然也就无所谓返回值类型的问题。

再次,构造函数的名称必须与类同名,即用类名作为构造函数的名字。

最后,像普通函数一样,构造函数也可以拥有参数列表,通过这些参数传递的数据来完成对象的初始化工作,从而可以用不同的参数创建具有差别的对象。根据参数列表的不同,一个类可以拥有多个构造函数,以适应不同的构造方式。

观察下面的 Teacher 类,为它添加一个带有 string 类型参数的构造函数,以便在创建对象时通过构造函数对成员变量进行合理的初始化,从而创建有差别的对象:
class Teacher
{
public:
    // 构造函数
    // 参数表示 Teacher 类对象的名字
    Teacher(string strName)    // 带参数的构造函数
    {
        // 使用参数对成员变量赋值,进行初始化
        m_strName = strName;
    }
    void GiveLesson();    // 备课

protected:
    string m_strName = "Chen";    // 类声明中默认的初始值
private:
};
现在,可以在定义对象时将参数写在对象名之后的括号中,这种定义对象的形式会调用带参数的构造函数 Teacher(string strName),从而用参数给这个对象的名字属性赋值:
// 使用参数创建一个名为WangGang的老师对象
Teacher MrWang("WangGang");
在上面的代码中,我们使用字符串 "WangGang" 作为构造函数的参数,它会调用 Teacher 类中需要 string 类型为参数的 Teacher(string strName) 构造函数来完成对象的创建。在构造函数中,这个参数值被赋给了类的 m_strName 成员变量,以代替它在类定义中给出的固定初始值 "Chen"。

当对象创建完成后,参数值 "WangGang" 就会成为 MrWang 这个对象的名字属性 m_strName 的值。这样,我们通过参数创建了一个具有特定“名字”的 Teacher 对象。

在构造函数中,除可以使用“=”运算符给对象的成员变量赋值以完成初始化外,还可以使用“:”符号在构造函数后引出初始化属性列表,直接利用构造函数的参数或者其他的合理初始值对成员变量进行初始化。其语法格式如下:
class 类名
{
public:
    // 使用初始化属性列表的构造函数
    类名(参数列表)
        : 成员变量1(初始值1), 成员变量2(初始值2) // 初始化属性列表
    {
    }
    // 类的其他声明和定义
};
在构造函数执行之前,系统会完成成员变量的创建,并使用括号内的初始值对其进行赋值。这些初始值可以是构造函数的参数,也可以是成员变量的某个合理初始值。如果一个类有多个成员变量需要通过这种方式进行初始化,多个变量之间可以使用逗号间隔。

例如,可以利用初始化属性列表将 Teacher 类的构造函数改写为:
class Teacher
{
public:
    // 使用初始化属性列表的构造函数
    Teacher(string strName)
        // 初始化属性列表,使用构造函数的参数 strName 创建并初始化 m_strName
        : m_strName(strName)
    {
        // 构造函数中无须再对 m_strName 赋值
    }

protected:
    string m_strName;
};
使用初始化属性列表改写后的构造函数,利用参数 strName 直接创建 Teacher 类的成员变量 m_strName 并对其进行初始化,从而省去了使用“=”对 m_strName 进行赋值时的额外工作,这样可以在一定程度上提高对象构造的效率。

另外,有些成员变量必须在创建的同时就给予初始值,例如使用 const 关键字修饰的成员变量。在这种情况下,使用初始化属性列表来完成成员变量的初始化就成为必要的了。因此,在可能的情况下,最好使用构造函数的初始化属性列表来完成类成员了。因此,在可能的情况下,最好使用构造函数的初始化属性列表来完成类成员变量的初始化。

需要注意的是,如果类已经有了显式定义的构造函数,编译器就不会再为其生成默认的构造函数。例如,在 Teacher 类已经拥有显式声明的构造函数之后,如果仍然尝试采用如下形式定义对象,将会导致编译错误:
// 试图调用默认构造函数创建一个没有名字的老师
Teacher MrUnknown; // 编译错误
这时编译器会提示错误,因为这个类没有默认的构造函数。在这种情况下,创建对象的语句会因为找不到合适的构造函数而导致编译错误。因此,在实现类时,一般都会显式地编写构造函数,并根据需要添加带参数的构造函数来处理一些特殊的构造任务。

在 C++ 中,根据初始条件的不同,我们往往需要使用多种方式创建对象,所以一个类通常会有多个不同参数形式的构造函数,以负责以不同的方式创建对象。在这些构造函数中,往往有一些大家都需要完成的工作。一个构造函数完成的工作很可能是另一个构造函数所需完成工作的一部分。

例如,Teacher 类有两个构造函数:一个是不带参数的默认构造函数,它会给 Teacher 类的 m_nAge 成员变量赋值默认值 28;另一个是带参数的构造函数,它首先需要判断参数是否在合理的范围内,然后将合理的参数赋值给 m_nAge。这两个构造函数都需要完成的工作就是给 m_nAge赋值。

第一个构造函数的工作可以通过指定参数 28,利用第二个构造函数来完成,这样,第二个构造函数的工作就成为第一个构造函数所要完成工作的一部分。为了避免重复代码,只需在某个特定构造函数中实现这些共同功能,而在需要这些共同功能的构造函数中,直接调用这个特定构造函数即可。这种方式被称为委托调用构造函数(delegating constructors)。示例代码如下:
class Teacher
{
public:
    // 带参数的构造函数
    Teacher(int x)
    {
        // 判断参数是否合理,决定赋值与否
        if (0 < x && x <= 100)
            m_nAge = x;
        else
            cout << "错误的年龄参数" << endl;
    }
    // 构造函数 Teacher() 委托调用构造函数 Teacher(int x)
    // 用默认的年龄参数 28 委托调用构造函数 Teacher(int x)
    // 直接实现了参数合法性验证并赋值的功能
    Teacher() : Teacher(28)
    {
        // 完成特有的创建工作
    }

    // ...
private:
    int m_nAge;    // 年龄
};
在这里,我们在构造函数之后加上冒号“:”,然后跟上另一个构造函数的调用形式,从而实现了构造函数委托调用另一个构造函数。在一个构造函数中调用另一个构造函数,把部分工作交给另一个构造函数来完成,这就是委托。不同的构造函数各自负责处理自己的特定情况,而把最基本的、共用的构造工作委托给某个基础构造函数来完成,实现分工协作。

C++析构函数

如果某个对象是通过定义变量的形式创建的,在使用完毕离开其作用域之后,这个对象会自动销毁。而对于使用 new 关键字创建的对象,则需要在使用完毕后,通过 delete 关键字主动销毁。但无论使用哪种方式,对象在使用完毕后都需要销毁,也就是完成一些必要的清理工作,例如释放申请的内存、关闭打开的文件等。

与对象的创建需要专门的构造函数来完成一样,对象的销毁同样需要专门的析构函数来完成。同为类中负责对象创建与销毁的特殊函数,两者有很多相似之处。

首先,它们都会被自动调用,只不过一个是在创建对象时,而另一个是在销毁对象时。

其次,两者的函数名都是由类名构成的,只不过析构函数名在类名前加了个“~”符号,以便与构造函数名进行区分。

再次,两者都没有返回值,并且都是公有的(public)访问级别。

最后,如果没有必要,两者在类中都是可以省略的。如果类中没有显式地声明构造函数和析构函数,编译器会自动为生成默认的函数。

两者唯一的不同之处在于,构造函数可以接受多种形式的参数,而析构函数却不接受任何参数。

下面为 Teacher 类加上析构函数以完成一些清理工作,替代默认的析构函数:
class Teacher
{
public:    // 公有的访问级别
    // ...
    // 析构函数
    // 在类名前加上“~”构成析构函数名
    ~Teacher()    // 不接受任何参数
    {
        // 进行清理工作
        cout << "春蚕到死丝方尽,蜡炬成灰泪始干" << endl;
    }
    // ...
};
在这里,因为 Teacher 类不需要额外的清理工作,所以我们没有定义任何操作,只是输出一段信息表示 Teacher 类对象的结束。一般来说,会将那些需要在对象被销毁之前完成的事情放在析构函数中来处理。

例如,对象创建时申请的内存资源,在对象销毁后就不能再继续占用,因此需要在析构函数中进行合理地释放,归还给操作系统。就像一个有信誉的人在离开人世之前,要把欠别人的钱还清一样,干干净净地离开。

相关文章