大话C++:第12篇 构造函数与析构函数

1 构造函数概述

C++构造函数是一种特殊的成员函数,用于初始化类的对象。当创建对象时,构造函数会自动执行,且只执行一次。它主要用于设置对象的初始状态或执行一些必要的初始化操作。

1.1 为什么会存在构造函数

构造函数在C++中存在的原因主要是为了初始化对象的状态。当创建一个类的实例(即对象)时,构造函数会被自动调用,确保对象的数据成员在对象生命周期的开始时就被赋予合适的初始值。

除此之外,构造函数存在的几个关键原因:

  • 对象初始化:构造函数提供了一种机制,确保对象在创建时即被正确初始化。在C++中,未初始化的对象可能会导致未定义的行为或程序错误。通过构造函数,开发者可以定义对象创建时应该执行的初始化逻辑。

  • 封装性:构造函数是封装对象状态的一种方式。它允许开发者将对象的初始化细节隐藏在类的内部实现中,外部代码只需要通过构造函数提供必要的参数即可创建并初始化对象。

  • 代码重用:通过构造函数重载,可以为类定义多个构造函数,每个构造函数接受不同数量和类型的参数。这允许开发者根据需要使用不同的构造函数来创建对象,从而重用类的代码。

  • 控制资源分配:在某些情况下,构造函数还用于分配对象所需的资源(如动态内存)。通过构造函数,可以在对象创建时立即分配这些资源,确保对象在使用前已经准备好。

  • 确保一致性:构造函数确保每个对象在创建时都遵循相同的初始化过程。这有助于维护代码的一致性和可维护性,因为所有对象都将通过相同的途径进行初始化。

  • 简化代码:通过使用构造函数,开发者可以避免在对象创建后手动初始化每个数据成员的繁琐工作。构造函数提供了一种简洁、一致的方式来初始化对象的状态。

1.2 构造函数的分类

C++构造函数的分类可以根据不同的标准进行划分。以下是两种常见的分类方式:

  • 参数数量和类型分类:

    • 无参数构造函数:也称为默认构造函数,当没有显式提供参数时,用于创建和初始化对象。如果没有为类定义任何构造函数,编译器会自动提供一个无参构造函数。

    • 带参数构造函数:接受一个或多个参数,用于根据提供的参数值初始化对象。

  • 特殊用途分类:

    • 默认构造函数:当没有提供显式初始值时,用来创建对象的构造函数。如果一个类没有定义任何构造函数,编译器会提供一个默认的无参构造函数。

    • 拷贝构造函数:用于根据另一个同类型对象来初始化一个新对象。当一个对象以值传递的方式传入函数,或者一个对象从函数以值返回时,会调用拷贝构造函数。如果没有显式定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数。

    • 转换构造函数:允许使用不同类型的值来初始化对象。这种构造函数通常接受一个不同类型的参数,并将其转换为类的适当形式。

    • 移动构造函数(C++11及后续版本):用于根据另一个同类型对象(通常是临时对象或即将销毁的对象)的资源来初始化一个新对象。这可以提高性能,因为资源可以直接从一个对象转移到另一个对象,而不是进行复制。

    • 委托构造函数C++11及后续版本):它允许一个构造函数调用同一类的另一个构造函数来执行初始化。

2 默认构造函数

默认构造函数(Default Constructor)是C++中的一种特殊类型的构造函数,它不带任何参数。默认构造函数的主要用途是在没有提供任何初始化参数的情况下创建和初始化对象。

在C++中,实现默认构造函数的方式:

  • 非显式定义默认构造函数:如果没有为类定义任何构造函数,编译器会自动为类提供一个默认的无参构造函数。这个默认构造函数不做任何操作,即它是一个空构造函数。
cpp 复制代码
#include <iostream>

class Student 
{
public:
    // 非显示定义默认构造函数

    // 显示学生信息
    void DisplayInfo() const 
    {
        std::cout << "该学生姓名:" << _name 
            	  << ",年龄:" << _age 
            	  << ",学号:" << _num 
                  << std::endl;
    }

    // 获取学生姓名
    std::string GetName() const
    {
        return _name;
    }

    // 获取年龄
    int GetAge() const
    {
        return _age;    
    }

    // 获取学号
    int GetNum() const
    {
        return _num;
    }

    // 设置姓名
    void SetName(const std::string& name)
    {
        _name = name;
    } 

    // 设置年龄
    void SetAge(const int age)
    {
        _age = age;
    }

    // 设置学号
    void SetNum(const int num)
    {
        // 相当于this.num = num;
        _num = num;
    }    
    
private:
    // 私有成员变量
    std::string _name;	// 姓名
    int _age;			// 年龄
    int _num;			// 学号    
};

int main() 
{
    // 编译器会自动调用默认的无参构造函数
    Student student;
    student.DisplayInfo();
    
    return 0;
}
  • 显式定义默认构造函数:显式地定义一个默认构造函数,即不带任何参数的构造函数。
cpp 复制代码
#include <iostream>

class Student 
{
public:
    // 显示定义默认构造函数
    Student()
    {
        std::cout << "显示定义默认构造函数" << std::endl;
        _name = "Jack";
        _age = 21;
        _num = 20240001;
    }

    // 显示学生信息
    void DisplayInfo() const 
    {
        std::cout << "该学生姓名:" << _name 
            	  << ",年龄:" << _age 
            	  << ",学号:" << _num 
                  << std::endl;
    }

    // 获取学生姓名
    std::string GetName() const
    {
        return _name;
    }

    // 获取年龄
    int GetAge() const
    {
        return _age;    
    }

    // 获取学号
    int GetNum() const
    {
        return _num;
    }

    // 设置姓名
    void SetName(const std::string& name)
    {
        _name = name;
    } 

    // 设置年龄
    void SetAge(const int age)
    {
        // 相当于this.age = age;
        _age = age;
    }

    // 设置学号
    void SetNum(const int num)
    {
        // 相当于this.num = num;
        _num = num;
    }    
    
private:
    // 私有成员变量
    std::string _name;	// 姓名
    int _age;			// 年龄
    int _num;			// 学号    
};

int main() 
{
    // 编译器会自动调用默认的无参构造函数
    Student student;
    student.DisplayInfo();
    
    return 0;
}

3 拷贝构造函数

C++拷贝构造函数是一种特殊的构造函数,它用于根据一个已存在的对象来创建并初始化一个新对象。当一个对象以值传递的方式被传入函数,或者一个对象从函数以值返回时,或者一个对象需要被初始化为另一个同类型对象的副本时,拷贝构造函数都会被调用。

拷贝构造函数语法格式

cpp 复制代码
class ClassName
{
public:
    // 拷贝构造函数
    ClassName(const ClassName &obj);
}

其中:

  • ClassName 是类的名称。

  • &obj 是对同类型对象的常量引用,它指向被拷贝的对象。这里的 const 关键字表示我们不会通过这个引用修改被拷贝的对象。

cpp 复制代码
#include <iostream>
#include <string>

class Person 
{
public:
    // 构造函数
    Person(const std::string& name, int age) 
    {
        std::cout << "调用构造函数" << std::endl;
        
        _name = name;
        _age = age;
    }

    // 拷贝构造函数
    Person(const Person& other)
    {
        std::cout << "调用拷贝构造函数" << std::endl;
        
        // 深拷贝name成员
        _name = other._name;
        // 拷贝age成员
        _age = other._age;
    }

    // 析构函数
    ~Person() 
    {
    }

    // 获取name
    std::string GetName() const 
    {
        return _name;
    }

    // 获取age
    int GetAge() const 
    {
        return _age;
    }

    
private:
    std::string _name;
    int _age;    
};

int main() 
{
    Person person1("Jack", 25);
    // 调用拷贝构造函数
    Person person2(person1);

    std::cout << "person1个人信息:" << std::endl;
    std::cout << "姓名: " << person1.GetName() << std::endl;
    std::cout << "年龄: " << person1.GetAge() << std::endl;

    std::cout << "person2个人信息:" << std::endl;
    std::cout << "姓名: " << person2.GetName() << std::endl;
    std::cout << "年龄: " << person2.GetAge() << std::endl;

    return 0;
}

拷贝构造函数调用时机:

  • 当一个对象以值传递的方式被传入函数时。

  • 当一个对象从函数以值返回时。

  • 当使用一个已存在的对象来初始化一个新对象时。

注意,没有为类显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。默认的拷贝构造函数会执行成员到成员的浅拷贝(shallow copy),对于类中的每个成员,都会执行其相应类型的拷贝操作。如果类中含有指针成员,默认的拷贝构造函数只会复制指针的值,而不会复制指针所指向的内容,这可能会导致资源共享和所有权问题。

4 构造函数初始化

在C++中,构造函数的初始化可以通过两种方式:

  • 成员初始化列表

  • 在构造函数体内部进行初始化

4.1 成员初始化列表

在C++中,构造函数的成员初始化列表是一种在构造函数体执行之前初始化对象成员的方式。初始化列表在构造函数的参数列表之后、函数体之前,使用冒号(:)分隔。初始化列表特别适用于初始化常量成员、引用成员以及没有默认构造函数的类类型成员。

cpp 复制代码
#include <iostream>
#include <string>

class MyClass 
{
public:
    // 构造函数
    MyClass(int var1, int var2, int& var3, const std::string& var4)
        : _member1(var1),       // 初始化_member1
          _member2(var2),       // 初始化_member2(常量成员)
          _member3(var3),   	// 初始化_member3(引用成员)
          _member4(var4)   		// 初始化_member4(类类型成员)
    {
        // 构造函数体,如果需要的话
        std::cout << "显示调用构造函数" << std::endl;
    }

    void Display() const 
    {
        std::cout << "_member1: " << _member1 
            		<< ", _member2: " << _member1
           			<< ", _member3: " << _member3 
            		<< ", _member4: " << _member4 << std::endl;
    }
 
    
private:
    int _member1;
    const int _member2;       	// 常量成员
    int& _member3;          	// 引用成员
    std::string _member4;  		// 类类型成员
};

int main() 
{
    int x = 10;
    MyClass obj(5, 20, x, "Hello, World!");
    obj.Display();
    
    return 0;
}

4.2 在构造函数体内部进行初始化

在构造函数体内部进行初始化是指在构造函数的函数体内部,使用赋值操作来初始化类的成员变量。这种方式适合非常量和非引用成员。

cpp 复制代码
#include <iostream>
#include <string>

class MyClass 
{
public:
    // 构造函数
    MyClass(int var1/*, int var2, int& var3*/, const std::string& var4)
    {
        // 构造函数体,如果需要的话
        std::cout << "显示调用构造函数" << std::endl;
        
        _member1 = var1;       	// 初始化_member1
        // _member2 = var2;       	// 初始化_member2(常量成员)
        // _member3 = var3;   		// 初始化_member3(引用成员)
        _member4 = var4;   		// 初始化_member4(类类型成员)        
    }

    void Display() const 
    {
        std::cout << "_member1: " << _member1 
            		// << ", _member2: " << _member1
           			// << ", _member3: " << _member3 
            		<< ", _member4: " << _member4 << std::endl;
    }
 
    
private:
    int _member1;
    // const int _member2;       	// 常量成员
    // int& _member3;          		// 引用成员
    std::string _member4;  			// 类类型成员
};

int main() 
{
    MyClass obj(5, "Hello, World!");
    obj.Display();
    
    return 0;
}

4.3 两种初始化方式的区别

成员初始化列表和在构造函数体内部进行初始化的主要区别:

  • 执行时机

    • 成员初始化列表在构造函数体执行之前初始化成员变量。这意味着当构造函数体开始执行时,所有的成员变量都已经被初始化。

    • 在构造函数体内部进行初始化则发生在构造函数的函数体内部,即在所有成员变量已经默认构造之后。

  • 效率

    • 成员初始化列表通常更高效,特别是对于那些有非平凡构造函数的成员变量。使用成员初始化列表可以避免先调用成员变量的默认构造函数,然后再在构造函数体内部进行赋值或拷贝操作。

    • 在构造函数体内部进行初始化可能会导致额外的函数调用和拷贝操作,特别是对于用户自定义类型的成员变量。

  • 适用场景

    • 成员初始化列表特别适用于初始化常量成员、引用成员以及没有默认构造函数的类类型成员。

    • 在构造函数体内部进行初始化则适用于所有类型的成员变量,但可能不是最高效的方式。

  • 初始化顺序

    • 成员初始化列表中的初始化顺序与成员变量在类定义中的声明顺序有关,而与初始化列表中的顺序无关。

    • 在构造函数体内部进行初始化时,成员变量的初始化顺序则遵循它们在代码中的出现顺序。

总之,成员初始化列表通常更受推荐,因为它提供了更高的效率和更灵活的初始化方式,特别是在处理复杂的数据类型和需要避免多重初始化的场景下。然而,在构造函数体内部进行初始化仍然是一个有效的选择,特别是在处理简单类型或当成员初始化列表不适用时。

5 析构函数

析构函数(destructor)是一种特殊的成员函数,当对象的生命周期结束时,例如对象所在的函数已调用完毕,或者对象脱离了其作用域,系统会自动执行析构函数。析构函数的主要作用是进行"清理善后"的工作,例如释放对象在生命周期内可能分配的资源。

在C++中,析构函数的名字与类名相同,但在函数名前面会加一个位取反符(~)。析构函数不能带任何参数,也没有返回值(包括void类型)。每个类只能有一个析构函数,且不能重载。

如果用户没有为类编写析构函数,编译器会自动生成一个默认的析构函数。这个默认的析构函数不做任何操作,但如果类中有动态分配的内存(如使用new关键字),那么默认的析构函数可能无法正确释放这些内存,导致内存泄漏。因此,对于包含动态分配内存的类,通常需要用户自定义析构函数来正确释放这些资源。

cpp 复制代码
#include <iostream>
#include <string>

class Person 
{
public:
    // 构造函数
    Person(const std::string& name, int age) 
    {
        // 为成员变量分配动态内存
        _name = new std::string(name);
        _age = age;
        
        std::cout << "调用构造函数 姓名: " << *_name 
            	  	<< " and age: " << _age << std::endl;
    }

    // 析构函数
    ~Person() 
    {
        // 释放动态分配的内存
        delete _name;
        std::cout << "调用析构函数" << std::endl;
    }

    // 获取姓名和年龄
    void Display() const 
    {
        std::cout << "姓名: " << *_name << ", 年龄: " << _age << std::endl;
    }
   
    
private:
    std::string* _name;
    int _age;    
};

int main() 
{
    // 创建Person对象
    Person person("Alice", 30);
    
    // 显示Person信息
    person.Display();

    // 当person对象离开作用域时,析构函数将被自动调用
    return 0;
}
相关推荐
幻听嵩的留香44 分钟前
面向对象总结
面向对象
染指11102 小时前
50.第二阶段x86游戏实战2-lua获取本地寻路,跨地图寻路和获取当前地图id
c++·windows·lua·游戏安全·反游戏外挂·游戏逆向·luastudio
委员2 小时前
基于NodeMCU的物联网空调控制系统设计
单片机·mcu·物联网·智能家居
Code out the future2 小时前
【C++——临时对象,const T&】
开发语言·c++
sam-zy2 小时前
MFC用List Control 和Picture控件实现界面切换效果
c++·mfc
aaasssdddd963 小时前
C++的封装(十四):《设计模式》这本书
数据结构·c++·设计模式
发呆小天才O.oᯅ3 小时前
YOLOv8目标检测——详细记录使用OpenCV的DNN模块进行推理部署C++实现
c++·图像处理·人工智能·opencv·yolo·目标检测·dnn
qincjun3 小时前
文件I/O操作:C++
开发语言·c++
逝灮3 小时前
【蓝桥杯——物联网设计与开发】拓展模块3 - 温度传感器模块
驱动开发·stm32·单片机·嵌入式硬件·物联网·蓝桥杯·温度传感器
星语心愿.3 小时前
D4——贪心练习
c++·算法·贪心算法