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

news2024/9/24 17:11:25

1 构造函数概述

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

1.1 为什么会存在构造函数

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

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

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

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

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

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

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

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

1.2 构造函数的分类

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

  • 参数数量和类型分类:

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

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

  • 特殊用途分类:

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

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

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

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

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

2 默认构造函数

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

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

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

#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;
}
  • 显式定义默认构造函数:显式地定义一个默认构造函数,即不带任何参数的构造函数。

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

拷贝构造函数语法格式

class ClassName
{
public:
    // 拷贝构造函数
    ClassName(const ClassName &obj);
}

其中:

  • ClassName 是类的名称。

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

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

#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 在构造函数体内部进行初始化

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

#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关键字),那么默认的析构函数可能无法正确释放这些内存,导致内存泄漏。因此,对于包含动态分配内存的类,通常需要用户自定义析构函数来正确释放这些资源。

#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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2160901.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《线性代数》学渣笔记

文章目录 1 行列式1.1 克拉默法则1.2 基本性质1.3 余子式 M i j M_{ij} Mij​1.4 代数余子式 A i j ( − 1 ) i j ⋅ M i j A_{ij} (-1)^{ij} \cdot M_{ij} Aij​(−1)ij⋅Mij​1.5 具体型行列式计算&#xff08;化为基本型&#xff09;1.5.1 主对角线行列式&#xff1a;主…

MMD模型及动作一键完美导入UE5-IVP5U插件方案(二)

1、下载并启用IVP5U插件 1、下载IVP5U插件, IVP5U,点击Latest下载对应引擎版本,将插件放到Plugins目录,同时将.uplugin文件的EnableByDefault改为false 2、然后通过Edit->Plugins启用插件 2、导入pmx模型 1、直接在Content的某个目录拖入pmx模型,选择默认参数 2、…

13年408计算机考研-计算机网络

第一题&#xff1a; 解析&#xff1a;OSI体系结构 OSI参考模型&#xff0c;由下至上依次是&#xff1a;物理层-数据链路层-网络层-运输层-会话层-表示层-应用层。 A.对话管理显然属于会话层&#xff0c; B.数据格式转换&#xff0c;是表示层要解决的问题&#xff0c;很显然答案…

使用Python和OpenCV生成灰阶图像

代码如下&#xff1a; import cv2 import numpy as npimg np.zeros((256, 256), np.uint8)for i in range(0,16):for j in range(0,16):img[i*16:(i1)*16][j*16:(j1)*16]i*16jcv2.imwrite(result.jpg, img) 效果如下&#xff1a;

新能源汽车充电桩怎么选?

新能源汽车是我国七大战略性新兴产业之一&#xff0c;已成为汽车产业转型升级的重要推动力。毫无疑问。充电桩作为我国新能源汽车产业链下游的重要环节&#xff0c;在国家政策的大力支持和市场需求的带动下&#xff0c;有着非常广阔的前景。安科瑞叶西平187-06160015 新能源汽…

科研服务新高度:表观组学的一站式实验服务

生物信息实验室致力于分子育种技术的研发和在生物医学研究领域的应用&#xff0c;实验室以分子遗传学实验技术和高通量生物信息分析技术为核心&#xff0c;建立了基因组、表观组、互作组的全面科研服务体系。50余位教授、研究员智库专家&#xff0c;您身边的分子实验专家!

哈里斯表态:承诺支持加密货币投资!

KlipC报道&#xff1a;近日&#xff0c;在曼哈顿举办的一次筹款活动中&#xff0c;美国副总统哈里斯首次公开表态&#xff0c;如果当选&#xff0c;她将支持增加对人工智能和加密货币行业的投资。 哈里斯表示&#xff0c;“我将把劳工、小企业创始人、创新者和大公司团结在一起…

【网络安全】公钥密码体制

1. 公钥密码体制概述 1.1 基本概念 公钥密码体制&#xff0c;又称为非对称密码体制&#xff0c;是一种基于数学函数的加密方式&#xff0c;它使用一对公钥和私钥来进行加密和解密。公钥用于加密&#xff0c;私钥用于解密。这种体制提供了一种安全的通信方式&#xff0c;因此在…

安装程序不用鼠标,Windows也玩程序包管理存储库

网管小贾 / sysadm.cc “嘿&#xff0c;嘿&#xff0c;看见没&#xff0c;今年某某著名大学建筑专业才招了4名新生&#xff01;” 大刘用手点指手机&#xff0c;带着一脸的吃惊相。 我冲他笑了笑&#xff0c;说道&#xff1a;“那是他们的教学水平不行。” “要是换了我&…

【JS】正则表达

正则表达式 reg /匹配规则/ reg.test(str) 1.边界符&#xff1a;^ 以...开头&#xff0c;$ 以...结尾 2.量词&#xff1a;* 出现0次或多次&#xff0c; 出现1次或多次, ? 出现0次或1次,{n}出现n次&#xff0c;{n,m}出现n到m次 3.字符类&#xff1a;[]中的字符任一出现&…

无人机的避障的航迹规划详解!!!

一、无人机避障技术 视觉避障系统&#xff1a;通过安装在无人机上的摄像头捕捉周围环境的图像&#xff0c;利用计算机视觉技术对图像进行处理和分析&#xff0c;提取出障碍物的信息。这种方法直观、信息丰富&#xff0c;但在光线不足或变化多的情况下可能影响识别效果&#xf…

生成测试图片的步骤

生成测试图片的步骤&#xff1a; 1、通义万象画图&#xff1a;https://tongyi.aliyun.com/wanxiang/creation 2、改图宝修改尺寸&#xff1a;https://www.gaitubao.com/

set的使用

序列式容器和关联式容器 序列式容器&#xff1a; 前⾯我们已经接触过STL中的部分容器如&#xff1a;string、vector、list、deque、array、forward_list等&#xff0c;这些容器统称为序列式容器&#xff0c;因为逻辑结构为线性序列的数据结构&#xff0c;两个位置存储的值之间…

【Python报错已解决】AttributeError: ‘Tensor‘ object has no attribute ‘kernel_size‘

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 专栏介绍 在软件开发和日常使用中&#xff0c;BUG是不可避免的。本专栏致力于为广大开发者和技术爱好者提供一个关于BUG解决的经…

使用compile_commands激活vscode索引-跳转-代码提示功能

最近发现&#xff0c;使用vscode打开一个大的c工程很容易无法正常调转和代码提示。所以经常会手动修改.vscode/c_cpp_properties.json文件的"includePath"属性。然而&#xff0c;当pkg越来越多 工程体量越来越大之后&#xff0c;我不得不探索如何自动的完成这一过程&…

Matplotlib画图相关代码

绘制不同类型的线条 import matplotlib.pyplot as plt import numpy as npx np.array([1, 2, 3]) y np.array([2, 4, 6])# 不同线型的示例 plt.plot(y, marker*, linestyle-) # 实线 plt.plot(y 1, markero, linestyle--) # 虚线 plt.plot(y 2, markerx, linestyle-.)…

【IDEA配置Maven环境】

在IDEA欢迎界面 选择 IDEA中 Customize > ALLSettings > Build,Execution,Deployment > Build Tools > Maven

VirtualBox+Vagrant快速搭建Centos7系统【最新详细教程】

VirtualBoxVagrant快速搭建Centos7系统 &#x1f4d6;1.安装VirtualBox✅下载VirtualBox✅安装 &#x1f4d6;2.安装Vagrant✅下载Vagrant✅安装 &#x1f4d6;3.搭建Centos7系✅初始化Vagrantfile文件生成✅启动Vagrantfile文件✅解决 vagrant up下载太慢的问题✅配置网络ip地…

从零开始的软件开发详解:数字药店系统源码与医保购药APP

很多小伙伴们疑问&#xff0c;医保购药APP是如何开发的&#xff0c;今天我将从零数字药店系统源码开始为大家提供一条清晰的实现方案。 一、技术架构设计 在开发医保购药APP之前&#xff0c;首先需要明确技术架构。一般来说&#xff0c;APP的技术架构可以分为前端和后端。 1…

网络分段:您需要了解的一切

什么是网络分段&#xff1f;为什么它很重要&#xff1f; 在当今互联互通的世界中&#xff0c;网络分段已成为组织网络安全战略中不可或缺的一部分。随着网络威胁不断演变和变得更加复杂&#xff0c;保护网络免受潜在入侵并尽量减少攻击面变得至关重要。根据最近的研究&#xf…