首页 > 编程语言 >C++ 面向对象(构造 & 析构函数)

C++ 面向对象(构造 & 析构函数)

时间:2025-01-16 20:31:58浏览次数:3  
标签:调用 对象 成员 C++ 面向对象 析构 拷贝 类名 构造函数

二、构造 & 析构函数

2.1 构造和析构

2.1.1 功能

构造函数功能

构造函数在类实例化对象时用于完成对象成员的初始化,通常包括以下操作:
     1. 为成员变量分配内存空间
     2. 初始化成员变量
     3. 执行类似打开文件、分配资源等额外操作

析构函数功能

主要作用在于对象 销毁前 系统自动调用,执行一些清理工作。

2.1.2 格式

构造函数

类名( ) { }

  1. 构造函数与类名同名。
  2. 构造函数没有返回值,也不能写 void
  3. 构造函数通常设置为 public 权限,以便外部能够调用。
  4. 构造函数可以有参数,因此==可以发生重载==
  5. 序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次
  6. 如果不写构造函数 , 编译器会默认提供一个==构造函数==

析构函数

~类名( ){ }

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

2.1.3 调用时机

构造函数:在类实例化对象时 自动调用,不需要也不允许手动调用。

析构函数:在 对象销毁 或者 程序结束前 调用,不需要手动调用。

注意 :
1、析构函数可以手动调用 但是一般不这么干
    MyClass obj;
    obj.~MyClass(); // 手动调用析构函数


2、析构函数在堆区 和 栈区的释放方式是不同的
    栈区中 程序结束前 系统自动调用
    堆区中 程序结束不会调用 需要用户手动调用 delete 才能调用堆区空间
 

2.1.4 构造和析构调用顺序

2.1.1.1 对于堆区对象:

​ 先new那个对象 就先构造那个对象

​ 先delete 那个对象 就先析构那个对象

​ 所以一般不考虑 堆区的构造函数

2.1.1.1 对于栈区对象:

​ 构造函数的调用顺序: 按顺序调用

​ 析构函数的调用顺序:按逆序调用 // 先进 后出 先构造 后析构(栈的顺序)

2.1.5 栈区与堆区的对象创建

栈区:

当在栈区创建对象时,构造函数会自动调用。

类名 对象名(构造函数的实参表); // 调用构造函数

堆区:

类名 *指针名;        // 这一步不会调用构造函数
指针名 = new 类名();  // 这个时候才调用构造函数

在堆区创建对象时,需要使用 new 运算符来分配内存并调用构造函数:

需要特别注意的是,使用 malloc 分配内存时,不会调用构造函数

malloc 仅分配内存空间,但不会初始化对象。

2.1.6 示例代码一(测试)

#include <iostream>
#include <string>

using namespace std;

class stu
{
private:
    /* data */
public:
    stu()
    {
        cout << "我是无参构造" << endl;
    }
    stu(string str)
    {
        cout << "我是有参构造" << "传入了:" << str << endl;
    }

    // 析构函数
    ~stu()
    {
        cout << "我是析构" << endl;
    }
};

int main(int argc, char const *argv[])
{
    stu s1;
    
    s1.~stu();
    
    cout << "hello" << endl;
    
    return 0;
}

运行结果如下:

2.1.7 示例代码二 (调用时机)

#include <iostream>
#include <string>

using namespace std;

int num = 1 ;

class stu
{
private:
    int val;
public:
    stu()
    {
        val = num++;
        cout << "我是:" << val << "构建了" << endl;
    }
    stu(int stu_val)
    {
        val = stu_val;
        cout << "我是:" << val << "构建了" << endl;
    }
    ~stu()
    {
        cout << "我是:" << val << "析构了" << endl;
    }
};


int main(int argc, char const *argv[])
{
    stu s1(1);
    stu s2(2);
    stu s3(3);
    stu s4(4);

    stu * s_p_1 = new stu;
    stu * s_p_2 = new stu;
    stu * s_p_3 = new stu;
    stu * s_p_4 = new stu;

    delete s_p_1;
    delete s_p_2;
    delete s_p_3;
    delete s_p_4;

    return 0;
}

运行结果如下:

2.2 构造函数的调用方式

对于类中 构造函数的调用方式有三种 :括号法、显示法、隐式转换法

2.2.1 示例代码(构造调用)

#include <iostream>
#include <string>

using namespace std;


class stu
{
private:
    string name;
    int id;

public:
    stu(){}

    // 通过括号法
    stu(string m_name, int m_id)
    {
        name = m_name;
        id = m_id;
    }

public:
    void show()
    {
        cout << "姓名" << name << endl;
        cout << "学号" << id << endl;
    }
};


int main(int argc, char const *argv[])
{
    // 括号调用
    stu s1("唐三" , 10);

    s1.show();

    // 括号调用  s2 = s1
    stu s2(s1);
    s2.show();

    // 匿名对象
    stu s3 = stu("萧薰儿" , 11);
    s3.show();

    return 0;
}

运行结果如下:

2.3 初始化列表

在定义构造函数的时候,可以使用冒号的方式 引出 构造函数的初始化列表

2.3.1 格式

类名(构造函数实参表):成员1(初值1),成员2(初值2)
{
    构造函数函数体;
}

2.3.2 注意事项

必须使用初始化列表的场景


场景1:当形参列表 和 成员变量 名字重名的情况下
    可以使用初始化表 或者 this指针 解决
    
场景2:当 成员变量 中 有引用类型的 成员时
    这个时候 必须使用 初始化列表
    类名(type & num):val(num);
    
场景3: 当类中有const 修饰的成员变量时 
    
场景4: 当类中有成员子对象时
    
    类中 的 成员子对象 的有参构造
    
    当类中有成员子对象时,需要在构造函数的初始化中调用成员子对象的构造函数
    并传参完成对成员子对象初始化,如果没有调用成员子对象的构造函数
    默认会调用成员子对象的无参构造函数 但是如果 成员子对象 没有无参构造 则会报错

2.3.3 示例代码 (初始化列表)

#include <iostream>
#include <string>

using namespace std;

/*
使用初始化列表的条件 
    1、成员变量与构造函数实参重名
    2、成员变量中有const 
    3、成员变量中有 &
    4、有成员子类 并且需要调用成员子类的 有参构造
*/

// 成员子类
class stu1
{
private:
    /* data */
public:
    // 无参构造
    stu1(/* args */)
    {
        cout << "无参构造" << endl;
    }

    // 有参构造
    stu1(int val)
    {
        cout << "有参构造 val = " << val << endl;
    }
};


class stu2
{
private:
    // 成员变量
    string & name;
    const int id;

    // 成员子类
    stu1 stu;
public:
    // 有参
    stu2(string name , int id)
        : name(name) , id(id) , stu(10)
    {

    }

};

int main(int argc, char const *argv[])
{
    string str;
    stu2 s1(str , 10);
    
    return 0;
}

运行结果如下:

2.4 拷贝构造函数

        C++ 会为类生成一个默认拷贝构造函数赋值操作符。这两个默认实现的行为都是逐位复制(即浅拷贝),它只是简单地复制每个成员的值,包括指针的地址,而不分配新的内存空间。

        所以我们需要 自己写一个 深拷贝 构造函数来替换掉程序中的 浅拷贝构造函数。

2.4.1 格式

类名(const 类名 & other)

2.4.2 拷贝构造函数启动规则

    类名 对象1
启动规则如下:
    类名 对象2(对象1)
    类名 对象2 = 对象1
    类名 * 对象2 = new 类名(对象1)

2.4.3 浅拷贝

        浅拷贝只是简单地将对象的值(包括指针的地址)赋值给新的对象。对于指针成员,浅拷贝只是复制指针本身,而不会为指针指向的动态内存分配新的空间。这意味着两个对象中的指针将指向同一块内存。

2.4.3.1 浅拷贝的风险

        如果两个对象都指向同一块内存,当其中一个对象修改这块内存时,另一个对象也会受到影响。此外,当对象被销毁时,如果两个对象都尝试释放同一块内存,就会导致 双重释放(double deletion)错误,程序可能崩溃。

2.4.4 深拷贝

        是为指针成员分配新的内存空间,并复制原对象中的数据到新的内存。这保证了每个对象都有自己独立的一份数据,不会共享同一块内存,因此也不会引发双重释放的问题

2.4.5 深拷贝和浅拷贝的区别

浅拷贝:

        如果类中没有显性的定义拷贝构造函数,编译器会提供一个默认的拷贝构造函数,这个默认的拷贝构造函数,只完成成员之间的简单赋值。如果类中没有指针成员,只用这个默认的拷贝构造函数,是没有问题的。

深拷贝:

        如果类中有指针成员,并且使用浅拷贝,指针成员之间也是只做了简单的赋值。相当于两个对象的指针成员指向的是同一块内存空间,调用析构函数的时候,就会出现 double free 的问题。此时, 需要在类中显性的定义拷贝构造函数,并且给新对象的指针成员分配空间,再将旧对象的指针成员指向的空间里的值拷贝一份过来。

2.4.6 示例代码(浅拷贝)

#include <iostream>
#include <string>

using namespace std;

class stu
{
private:
    int val ;
public:
    int * ptr;

public:
    stu():val(10)
    {

    }
    stu(int val) : val(val)
    {

    }
public:
    void show()
    {
        cout << val << endl;
        cout << *ptr << endl;
    }
};


int main(int argc, char const *argv[])
{
    
    stu s1(10);
    s1.ptr = new int(80);

    stu s2 = s1;
    *s1.ptr =90;
    s2.show();

    stu s3(s1);
    s3.show();
    
    return 0;
}

运行结果如下:

 

2.4.7 示例代码(深拷贝) 

#include <iostream>
#include <string>

using namespace std;

class stu
{
private:
    int val;

public:
    int *ptr;

public:
    stu() : val(10)
    {
    }
    stu(int val, int ptr_val) : val(val), ptr(new int(ptr_val))
    {
    }
    stu(const stu &other)
    {
        cout << "调用拷贝构造函数" << endl;
        if (NULL == other.ptr)
        {
            ptr = other.ptr;
        }
        else
        {
            ptr = new int(*other.ptr);
        }

        val = other.val;
    }

    ~stu()
    {
        if (NULL != ptr)
        {
            cout << "调用析构 释放空间完成" << endl;
            delete ptr;
        }
    }

public:
    void show()
    {
        cout << val << endl;
        cout << *ptr << endl;
    }

};

int main(int argc, char const *argv[])
{
    stu s1(10 , 20);

    stu s2(s1);

    *s1.ptr = 90;

    s2.show();



    return 0;
}

运行结果如下:

标签:调用,对象,成员,C++,面向对象,析构,拷贝,类名,构造函数
From: https://blog.csdn.net/wakkkaaa/article/details/145191265

相关文章

  • 【C++】类与对象(中上)(难点部分)
    目录 ......
  • 在C++中std::string 和string有啥区别呀?
    在C++中,std::string和string实际上是同一个类型,只是它们的命名空间(namespace)不同。具体来说:(我说为啥在写代码的时候都有个usingnamespacestd;语句我还以为是闹着玩.)std::string明确指定了string类型位于std命名空间中。string是std::string的简写,但要使......
  • 探讨c++内存布局背后的原因以及策略
    内存布局通常是按照成员变量的声明顺序,但由于对齐和填充的影响,编译器可能会调整成员变量的顺序。改变顺序的主要目的是为了优化内存使用和提高访问效率。以下是一些具体原因:1.提高内存访问效率对齐要求:不同数据类型有不同的对齐要求。将对齐要求相同或相近的成员变量放......
  • 面向对象分析与设计Python版 控制器与多态原则
    文章目录一、控制器原则二、多态原则一、控制器原则控制器原则名称:控制器Controller应用场景:确定谁负责接收、处理和分发系统的输入事件。解决方案:系统输入事件处理的职责分给控制器对象一个控制器对象实现业务系统的所有输入事件处理和业务逻辑分发,这一类控制器......
  • C++中线程同步与互斥的四种方式介绍及对比详解
    引言在C++中,当两个或更多的线程需要访问共享数据时,就会出现线程安全问题。这是因为,如果没有适当的同步机制,一个线程可能在另一个线程还没有完成对数据的修改就开始访问数据,这将导致数据的不一致性和程序的不可预测性。为了解决这个问题,C++提供了多种线程同步和互斥的机制。互斥......
  • C++ open()和read()函数使用详解
    对于Framework工程师来说,必要C或者C++编程能力是必须的,像对设备节点的操作是最基本的操作,那么我们便会用到open和read函数。open()函数用于打开文件,而read()函数用于从打开的文件中读取数据。open()函数open()函数是C/C++标准库中的一个POSIX标准函数,用于打开一个文件并返回......
  • 深入理解C++ 空类大小
    在C++中,规定空类(即类中没有任何数据成员、成员函数、虚函数等成员的类)的大小为1字节,这背后主要有以下几方面的原因:保证对象的唯一性和可区分性在C++的面向对象编程模型中,对象是类的实例化结果,每个对象在内存中都需要占据一定的空间,以便程序能够通过地址等方式对其进行操作和区......
  • C++17 Filesystem 实用教程
    C++17标准带来了std::filesystem库,提供了强大的工具来处理文件路径,目录以及其他与文件系统相关的操作.这篇文章适合C++初学者以及希望掌握C++17新特性的开发者,旨在帮助他们高效地完成文件系统相关任务.什么是std::filesystem?std::filesystem是C++标准库的一部......
  • java面向对象继承
    1Java中的继承概念继承是面向对象编程(OOP)中的一个核心概念。在Java中,继承指的是一个类(子类)通过扩展(extends)另一个类(父类)来获得父类的属性和方法。继承有助于实现代码重用和扩展,也为多态性提供基础。继承使得子类能够拥有父类的所有非私有成员(字段、方法),同时子类还可以......
  • 【C++】开源:ImGui图形用户界面库配置与使用
    项目介绍项目Github地址:https://github.com/ocornut/imguiDearImGui(ImGui)是一个开源的、用C++编写的图形用户界面(GUI)库。它由OCornut创建,旨在为应用程序和工具提供创建用户界面的简单高效的方式。以下是DearImGui的一些主要特性和特点:1.即时模式GUI:ImGui遵循即......