• 【C++】深拷贝和浅拷贝 ③ ( 浅拷贝内存分析 )






    一、浅拷贝内存分析




    1、要分析的代码


    下面的代码中 , 没有定义拷贝构造函数 , 因此 C++ 编译器会自动生成一个 只进行 浅拷贝 的 默认拷贝构造函数 ;

    调用默认拷贝构造函数 , 对新对象进行赋值 , 修改新对象的值 , 析构两个对象 , 分析整个执行过程中 栈内存 / 堆内存 的运行状态 ;


    代码示例 :

    #define _CRT_SECURE_NO_WARNINGS
    
    #include "iostream"
    using namespace std;
    
    class Student
    {
    public:
    
    	// 有参构造函数
    	Student(int age, const char* name)
    	{
    		// 获取字符串长度
    		int len = strlen(name);
    
    		// 为 m_name 成员分配内存 
    		// 注意还要为字符串结尾的 '\0' 字符分配内存
    		m_name = (char*)malloc(len + 1);
    
    		// 拷贝字符串
    		// C++ 中使用该函数需要
    		// 添加 #define _CRT_SECURE_NO_WARNINGS 宏定义
    		if (m_name != NULL)
    		{
    			strcpy(m_name, name);
    		}
    			
    		// 为 m_age 成员设置初始值
    		m_age = age;
    
    		cout << "调用有参构造函数" << endl;
    	}
    
    	~Student()
    	{
    		// 销毁 name 指向的堆内存空间
    		if (m_name != NULL)
    		{
    			free(m_name);
    			m_name = NULL;
    		}
    		cout << "调用析构函数" << endl;
    	}
    
    	// 该类没有定义拷贝构造函数 , C++ 编译器会自动生成默认的拷贝构造函数
    
    	// 打印类成员变量
    	void toString()
    	{
    		cout << "m_age = " << m_age << " , m_name = " << m_name << endl;
    	}
    
    public:
    	int m_age;
    	char* m_name;
    };
    
    int main()
    {
    	// 调用有参构造函数 , 创建 Student 实例对象
    	Student s(18, "Tom");
    	// 打印 Student 实例对象成员变量值
    	s.toString();
    
    	// 声明 Student 对象 s2 , 并使用 s 为 s2 赋值
    	// 该操作会调用 默认的拷贝构造函数 
    	// C++ 编译器提供的拷贝构造函数 只能进行浅拷贝
    	Student s2 = s;
    	s2.toString();
    
    	// 修改 s2 对象
    	strcpy(s2.m_name, "Jey");
    	s.toString();
    	s2.toString();
    
    	// 执行时没有问题 , 两个对象都可以正常访问
    	// 但是由于拷贝时 执行的是浅拷贝 
    	// 浅拷贝 字符串指针时 , 直接将指针进行拷贝 , 没有拷贝具体的值
    	// s 和 s2 的 m_name 成员是同一个指针
    	// 如果析构时 , 先析构 s2 , 将指针释放了 
    	// 之后再析构 s 时 发现 继续释放 被释放的指针 , 报错了
    
    
    
    	// 控制台暂停 , 按任意键继续向后执行
    	system("pause");
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    执行结果 : 执行后打印如下内容 ,

    调用有参构造函数
    m_age = 18 , m_name = Tom
    m_age = 18 , m_name = Tom
    m_age = 18 , m_name = Jey
    m_age = 18 , m_name = Jey
    请按任意键继续. . .
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述
    按下任意键 , 继续向后执行 , 调用完第一个析构函数后 , 再次尝试调用第二个析构函数 , 报错了 ;

    在这里插入图片描述


    2、调用有参构造函数创建 Student 实例对象


    调用有参构造函数 , 创建 Student 实例对象 ;

    	// 调用有参构造函数 , 创建 Student 实例对象
    	Student s(18, "Tom");
    
    • 1
    • 2

    Student 类中有 2 个成员变量 :

    	int m_age;
    	char* m_name;
    
    • 1
    • 2

    Student 类的有参构造函数如下 : 在有参的构造函数中 , m_age 成员直接赋值 , m_name 成员 , 需要先在堆内存中分配内存空间 , 然后再为其填充数据 ;

    	// 有参构造函数
    	Student(int age, const char* name)
    	{
    		// 获取字符串长度
    		int len = strlen(name);
    
    		// 为 m_name 成员分配内存 
    		// 注意还要为字符串结尾的 '\0' 字符分配内存
    		m_name = (char*)malloc(len + 1);
    
    		// 拷贝字符串
    		// C++ 中使用该函数需要
    		// 添加 #define _CRT_SECURE_NO_WARNINGS 宏定义
    		if (m_name != NULL)
    		{
    			strcpy(m_name, name);
    		}
    			
    		// 为 m_age 成员设置初始值
    		m_age = age;
    
    		cout << "调用有参构造函数" << endl;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    Student s 变量定义在 栈内存 中 , 首先为其在栈内存中分配数据 ,

    • m_age 就是 普通的 int 类型变量 , 这里为其分配 4 字节 栈内存 , 设置值为 18 ;
    • m_name 是 char* 类型的指针 , 是一个字符串 , 初始化为 “Tom” , 指针占 4 字节大小 ,
      • “Tom” 字符串常量 在全局区中 ,
      • 为 m_name 在堆内存中分配内存 , 地址为 0x1000
      • 分配的内存大小是 “Tom” 字符个数 + 1 , 多余的 1 字节是 ‘\0’ 字符串结尾 , 也就是 4 字节 ;
      • m_name 最终的指针值是 堆内存中的地址值 , 是 0x1000 , 也就是指向堆内存中的 0x1000 地址对应的内存空间 ;

    在这里插入图片描述


    3、调用默认拷贝构造函数为新对象赋值


    调用默认拷贝构造函数为新对象赋值 , 声明 Student 对象 s2 , 并使用 s 为 s2 赋值 , 该操作会调用 默认的拷贝构造函数 , C++ 编译器提供的拷贝构造函数 只能进行浅拷贝 ;

    	// 声明 Student 对象 s2 , 并使用 s 为 s2 赋值
    	// 该操作会调用 默认的拷贝构造函数 
    	// C++ 编译器提供的拷贝构造函数 只能进行浅拷贝
    	Student s2 = s;
    
    • 1
    • 2
    • 3
    • 4

    内存分析 :

    使用 默认的 拷贝构造函数 , 将 s 拷贝赋值给 s2 , 执行的是浅拷贝 , 也就是直接将 成员变量 进行简单的拷贝赋值 ;

    • 将 s.m_age 赋值给 s2.m_age , int 类型直接复制
    • 将 s.m_name 赋值给 s2.m_name , 指针类型也是直接复制 , 但是这样复制的就是一个 堆内存的地址 , 该操作导致了 s2.m_name 和 s.m_name 两个指针指向了相同的堆内存地址 ;

    上述指针的拷贝 , 只是将指针地址拷贝了 , 没有将指针指向的数据进行拷贝 , 这就是浅拷贝 , 显然浅拷贝是有问题的 ,

    • 如果对其中一个变量的 s.m_name 指针指向的地址进行修改 , 另外一个对象的成员也会进行改变 ;
    • 如果释放了一个对象的 s.m_name 指针 , 再尝试访问另外一个对象的 s.m_name 就会报错 ;

    在这里插入图片描述


    4、修改拷贝对象成员变量指针指向的数据


    修改拷贝对象成员变量指针指向的数据 :

    	// 修改 s2 对象
    	strcpy(s2.m_name, "Jey");
    
    • 1
    • 2

    内存分析 :

    浅拷贝时 指针的拷贝 , 只是将指针地址拷贝了 , 没有将指针指向的数据进行拷贝 , 这就是浅拷贝 , 显然浅拷贝是有问题的 ,

    s2.m_name 和 s.m_name 两个指针指向了相同的堆内存地址 ;

    如果 修改 拷贝对象 s2 的 s2.m_name 指针指向的地址存储的数据 , s 原始对象的 s.m_name 指针指向的数据也会被修改 ;

    在这里插入图片描述


    5、析构报错


    程序执行完毕 , 对栈内存对象进行销毁时 , 逐个析构对象 ;

    在下图的 栈内存 中 , 根据 栈内存 后进先出原则 , 先析构 s2 拷贝对象 , 然后析构 s 原始对象 ;

    在这里插入图片描述

    将 s2 拷贝对象析构后 , s2.m_name 指针指向的堆内存会被 free 释放 ;

    但此时 s.m_name 指针还指向被释放的内存 ;

    如果 s.m_name 继续被析构释放 , 这时就会报错 ;

    在这里插入图片描述

  • 相关阅读:
    android Google官网 :支持不同的语言和文化 rtl / ltr : 本地化适配:RTL(right-to-left) 适配
    义乌再次位列第一档!2022年跨境电商综试区评估结果揭晓!
    Vue3组件计算属性的缓存
    开关电源环路稳定性分析(01)-Buck变换器
    磁盘的挂载
    校验验证码是否过期(定时刷新验证码)
    K8s-Helm
    Android打造专有hook,让不规范的代码扼杀在萌芽之中
    队列的运行算法
    算法刷题:经典TopK问题整理
  • 原文地址:https://blog.csdn.net/han1202012/article/details/132941708