文章目录
- 一、浅拷贝内存分析
- 1、要分析的代码
- 2、调用有参构造函数创建 Student 实例对象
- 3、调用默认拷贝构造函数为新对象赋值
- 4、修改拷贝对象成员变量指针指向的数据
- 5、析构报错
一、浅拷贝内存分析
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 继续被析构释放 , 这时就会报错 ;