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

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

调用有参构造函数
m_age = 18 , m_name = Tom
m_age = 18 , m_name = Tom
m_age = 18 , m_name = Jey
m_age = 18 , m_name = Jey
请按任意键继续. . .

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

在这里插入图片描述


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


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

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

Student 类中有 2 个成员变量 :

	int m_age;
	char* m_name;

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

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;

内存分析 :

使用 默认的 拷贝构造函数 , 将 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");

内存分析 :

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

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 继续被析构释放 , 这时就会报错 ;

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/han1202012/article/details/132941708