一、题目
补充该类的赋值运算符函数
#include <iostream>
using namespace std;
class CMyString
{
public:
//构造函数
CMyString(const char* pData = nullptr)
{
if (nullptr == pData)
{
m_pData = new char[1];
m_pData[0] = '\0';
}
else
{
int len = strlen(pData);
m_pData = new char[len + 1];
strcpy(m_pData, pData);
}
};
//拷贝构造函数
CMyString(const CMyString& other)
{
int len = strlen(other.m_pData);
m_pData = new char[len + 1];
strcpy(m_pData, other.m_pData);
};
//请写出该类型添加===>赋值运算符函数
~CMyString()
{
delete[] m_pData;
m_pData = nullptr;
};
private:
char* m_pData;
};
int main() {
}
二、考察点
2.1 处理自赋值情况(避免自身赋值导致的问题)
为什么需要?
当对象自我赋值时(如 str1 = str1
),如果不特殊处理,可能导致严重错误。例如在释放当前资源阶段,会先删除自身的指针成员,导致后续拷贝时访问已被释放的内存(悬空指针),引发程序崩溃。
如何实现?
通过比较当前对象地址(this
)和源对象地址,判断是否为自赋值:
if (this == &str) { // 若地址相同,说明是自赋值
return *this; // 直接返回当前对象,不执行后续操作
}
作用:跳过不必要的内存释放和拷贝操作,避免自赋值导致的内存错误。
2.2 释放当前对象已有的资源(防止内存泄漏)
为什么需要?
赋值运算符的本质是 “用新值覆盖旧值”。如果当前对象已经持有动态分配的资源(如 m_pData
指向的堆内存),不释放就直接覆盖指针,会导致旧内存无法被回收,造成内存泄漏。
如何实现?
在拷贝新数据前,先释放当前对象已有的资源:
delete[] m_pData; // 释放当前对象的旧内存
m_pData = nullptr; // 避免野指针(释放后指针置空)
作用:确保旧资源被正确回收,防止内存泄漏。
2.3 进行深拷贝(对于指针成员)
为什么需要?
如果类中包含指针成员(如 m_pData
指向堆内存),简单的 “浅拷贝”(直接复制指针地址)会导致多个对象共享同一块内存。当其中一个对象释放内存后,其他对象的指针会变成悬空指针,访问时会引发未定义行为。
如何实现?
深拷贝需要为当前对象重新分配一块独立的内存,再将源对象的数据复制到新内存中:
int length = strlen(str.m_pData); // 计算源字符串长度
m_pData = new char[length + 1]; // 分配新内存(+1 是为了存储 '\0')
strcpy(m_pData, str.m_pData); // 复制数据到新内存
作用:保证每个对象拥有独立的内存资源,避免多个对象共享内存导致的冲突。
2.4 返回引用类型(支持连续赋值)
为什么需要?
C++ 中赋值运算符支持连续赋值(如 a = b = c
),其执行逻辑是从右向左:先执行 b = c
,再将结果赋值给 a
。这要求赋值运算符的返回值能作为左值继续参与赋值。
如何实现?
将返回值类型定义为当前类的引用(CMyString&
),并返回当前对象的引用(*this
):
CMyString& operator=(const CMyString& str) {
// ... 其他逻辑 ...
return *this; // 返回当前对象的引用
}
作用:支持连续赋值语法,符合 C++ 赋值运算符的使用习惯。
三、答案
3.1 小菜写的
//请写出该类型添加===>赋值运算符函数
CMyString& operator= (const CMyString& other)
{
//1. 检查自赋值
if (&other == this)
{
return *this;
}
//2. 释放当前资源
delete[] m_pData;
m_pData = nullptr;
//3. 深拷贝
int len = strlen(other.m_pData);
m_pData = new char[len + 1];
strcpy(m_pData, other.m_pData);
return *this;
};
问题:没有满足异常安全性原则 (4.5可知)
当执行 m_pData = new char[len + 1]
时,如果内存分配失败(例如堆内存耗尽),new
会抛出 std::bad_alloc
异常。此时:
- 原对象的
m_pData
已经被delete[]
释放(变成空指针) - 新内存分配失败,
m_pData
仍然是nullptr
- 异常抛出后,函数终止,后续的
strcpy
不会执行
最终导致当前对象的 m_pData
指向无效的空指针,处于不稳定状态。如果后续代码尝试使用这个对象(例如调用 getData()
或再次赋值),可能会引发未定义行为(如访问空指针崩溃)。
3.2 中登写的
CMyString& operator=(const CMyString& other) {
if (this != &other) { // 检查自赋值
// 1. 先分配新内存(若失败,原对象不受影响)
int len = strlen(other.m_pData);
char* pNewData = new char[len + 1]; // 若此处抛异常,旧资源仍有效
strcpy(pNewData, other.m_pData);
// 2. 再释放旧资源
delete[] m_pData;
// 3. 指向新内存
m_pData = pNewData;
}
return *this;
}
- 解决了核心异常安全问题:
新内存分配(new
)在释放旧资源(delete[]
)之前,若new
失败抛出异常,旧资源未被释放,原对象仍处于有效状态,避免了 “旧资源已释放但新资源分配失败” 的问题。 - 代码冗余:
需要手动编写内存分配、数据复制的逻辑,而 “复制再交换” 可以复用拷贝构造函数的代码,减少重复劳动(符合 DRY 原则)。 - 异常场景考虑更复杂:
若strcpy
过程中出现异常(虽然strcpy
本身不抛异常,但假设是更复杂的资源复制逻辑),已分配的新内存pNewData
会泄漏(因为还没赋值给m_pData
,且后续delete[]
不会执行)。
而 “复制再交换” 中,临时对象的析构函数会自动处理这种情况。
3.3 老鸟写的
CMyString& operator= (const CMyString& other)
{
//1. 检查自赋值
if (&other != this)
{
CMyString strTemp(other);
char* pTemp = strTemp.m_pData;
strTemp.m_pData = m_pData;
m_pData = pTemp;
}
return *this;
};
这正是典型的 “复制再交换”(Copy-and-Swap)技术,完美解决了之前提到的异常安全性问题,同时满足了所有核心要求。
这种写法的优点:
- 彻底解决异常安全问题
- 先通过
CMyString strTemp(other)
创建临时对象,完成深拷贝。如果这一步失败(比如内存不足导致new
抛出异常),原对象的m_pData
完全不受影响,仍保持有效状态。 - 后续的指针交换操作(
strTemp.m_pData
与m_pData
互换)是简单的指针赋值,不会抛出任何异常,确保一旦进入交换步骤,就能安全完成资源转移。
- 先通过
- 自动处理资源释放
- 函数结束时,临时对象
strTemp
会自动调用析构函数,释放它现在持有的资源(也就是原对象的旧m_pData
),无需手动delete
,彻底避免内存泄漏。
- 函数结束时,临时对象
与传统写法的对比:
实现方式 | 异常安全性 | 资源管理 | 代码简洁度 |
---|---|---|---|
传统写法 | 存在风险(可能导致对象无效) | 需要手动释放旧资源 | 较繁琐 |
复制再交换写法 | 强保证(要么成功,要么完全不影响) | 自动释放(依赖析构函数) | 简洁优雅 |
四、扩展知识
4.1 char* str = "hello" 和char str[] ="hello"
有啥差别
在 C/C++ 中,char* str = “hello” 和 char str[] = “hello” 虽然看起来相似,但在内存分配、可修改性和使用场景上有本质区别:
1. 内存存储位置不同
char* str = "hello"
字符串常量 “hello” 存储在只读数据段(常量区),这块内存是不可修改的。
指针 str 是一个变量,存储在栈上,它指向常量区中 “hello” 的首地址。
char str[] = "hello"
会在栈上分配一块内存,并将常量区的 “hello” 复制到这块栈内存中。
str 是数组名,代表栈上这块内存的首地址(数组名本质是常量指针,不能被重新赋值)。
2. 可修改性不同
char* str = "hello"
字符串内容位于只读区,不允许修改。
例如 str[0] = 'H'
会导致未定义行为(可能崩溃)。
指针本身可以被修改(指向其他地址),例如 str = "world"
是合法的。
char str[] = "hello"
字符串内容存储在栈上,允许修改。
例如 str[0] = 'H'
是合法的,修改后数组内容变为 "Hello"
。
数组名 str
是常量指针,不能被重新赋值,例如 str = "world"
会编译报错。
3. 类型本质不同
char* str
是一个指针变量,占 4 字节(32 位系统)或 8 字节(64 位系统)。
char str[]
是一个数组,占用的内存大小等于字符串长度 + 1(包含终止符 '\0'
)。
例如 char str[] = "hello"
占用 6 字节(h e l l o \0)
。
4.2 拷贝构造中的str.m_pData怎么访问到别的对象的m_pData呢?它不是一个私有成员变量吗?
在 C++ 中,同一个类的不同对象之间,即使是私有成员也可以互相访问。这是 C++ 类访问控制的一个重要特性:访问权限(public/protected/private)是针对类而不是对象的。
C++ 的访问控制规则是 “类级别的封装”,而不是 “对象级别的封装”。这意味着:
- 类的成员函数可以访问所有该类对象的私有成员(无论是不是当前对象 this)
- 其他类(或外部函数)则不能访问该类对象的私有成员
- 这种设计的合理性在于:同一个类的对象之间需要互相操作(比如拷贝、比较等),允许访问同类对象的私有成员可以简化实现。
4.3 delete 不是删除一个指针吗?为啥这里delete char* 的时候 是delete []
在 C++ 中,delete
和 delete[]
的使用与内存分配方式严格对应,目的是正确释放内存并避免泄漏:
delete
:用于释放单个对象的内存(通过 new
分配)
delete[]
:用于释放数组对象的内存(通过 new[]
分配)
在你的代码中,m_pData
是通过 new[]
分配的字符数组:
// 例如在构造函数中
m_pData = new char[length + 1]; // 分配数组,用 new[]
//因此释放时必须使用 delete[]
delete[] m_pData; // 释放数组,必须用 delete[]
核心规则是 “分配方式决定释放方式”,与指针的具体类型无关:只要内存是通过 new[] 分配的数组(无论数组元素类型是什么),释放时就必须使用 delete[];如果是通过 new 分配的单个对象,就必须使用 delete。
字符数组(char*)
char* arr = new char[10]; // new[] 分配字符数组
delete[] arr; // 必须用 delete[],正确
整数数组(int*)
int* nums = new int[5]; // new[] 分配整数数组
delete[] nums; // 必须用 delete[],正确
对象数组(MyClass*)
class MyClass { ... };
MyClass* objs = new MyClass[3]; // new[] 分配对象数组
delete[] objs; // 必须用 delete[](会调用每个对象的析构函数)
单个对象(无论类型)
int* num = new int; // new 分配单个整数
delete num; // 必须用 delete,正确
MyClass* obj = new MyClass; // new 分配单个对象
delete obj; // 必须用 delete,正确
4.4 为啥可以&other == this,但是不能 other == *this?
CMyString& operator= (const CMyString& other)
{
if (&other == this)
{
}
};
为什么 &other == this
总是合法的?
this
是当前对象的指针(类型为ClassName*
),存储的是当前对象的内存地址。&other
是取参数other
的地址(类型也是ClassName*
),得到的是other
对象的内存地址。- 两者都是指针类型,比较的是两个对象的内存地址:如果地址相同,说明
other
就是当前对象(自引用)。 - 这种比较不依赖任何运算符重载,是 C++ 原生支持的指针比较,因此在任何类的成员函数中都可以直接使用。
为什么 other == *this
不一定能直接使用?
*this
是当前对象的引用(类型为ClassName&
),other
也是ClassName&
类型(通常函数参数会用const ClassName&
)。- 两者都是对象,比较的是两个对象的内容是否相等,而非地址是否相同。
- 这种比较依赖
==
运算符的重载:如果类中没有定义operator==
,编译器会报错(因为不知道如何比较两个对象的内容)。
关键区别:
表达式 | 比较内容 | 依赖条件 | 典型用途 |
---|---|---|---|
&other == this |
两个对象的内存地址 | 无(原生支持) | 判断是否为同一个对象 |
other == *this |
两个对象的内容是否相等 | 需要重载 operator== 运算符 |
判断两个对象的值是否相同 |
4.5 什么是“异常安全性原则”?
异常安全性原则(Exception Safety Guarantees)是 C++ 中处理异常的重要设计准则,它定义了函数在发生异常时应保证的程序状态,确保即使出现异常(如内存分配失败、操作无效等),程序也不会出现资源泄漏、数据损坏或对象处于无效状态等问题。
C++ 中通常将异常安全性分为三个级别,从弱到强依次为:
基本保证(Basic Guarantee):
- 核心要求:当异常发生后,程序能保持有效状态(所有对象的不变式仍成立,资源未泄漏),但对象的具体状态可能不可预测(不一定是异常发生前的状态)。
- 示例:
向动态数组插入元素时,若内存分配失败抛出异常,数组应保持在插入前的有效状态(或其他合法状态),不会出现部分元素丢失、指针悬空等问题。
强保证(Strong Guarantee):
- 核心要求:当异常发生后,程序状态完全回退到函数调用前的状态(仿佛函数从未执行过),即 “要么操作完全成功,要么完全不影响程序状态”。
- 实现思路:通常通过 “复制再交换”(Copy-and-Swap)技术实现,先在临时对象上完成操作,确认成功后再与原对象交换资源。
- 示例:
对链表执行插入操作时,若中途抛出异常,链表应恢复到插入前的状态,不会出现节点断裂或内存泄漏。
不抛保证(No-Throw Guarantee):
- 核心要求:函数绝对不会抛出异常,在任何情况下都能成功执行并返回。
- 适用场景:通常用于底层操作(如指针交换、基础类型赋值等),这些操作本身不会失败。
- 示例:
简单的指针赋值(int* p = q;
)、基本类型的赋值(a = b;
)等,这些操作不会抛出异常。
异常安全性的核心目标:
- 禁止资源泄漏:无论是否发生异常,动态分配的内存、文件句柄等资源必须被正确释放。
- 保持对象有效性:对象的内部状态必须符合其 “不变式”(如链表的头指针和尾指针逻辑一致、字符串以
\0
结尾等)。 - 可预测的程序状态:异常发生后,程序状态应可预测(要么回退,要么保持有效),避免后续操作因状态混乱而崩溃。
实际开发中的应用:
- 对于赋值运算符、拷贝构造函数等涉及资源管理的函数,至少应保证强保证(通过 Copy-and-Swap 实现)。
- 对于简单的访问器函数(如
getter
),通常满足不抛保证。 - 设计类时,需明确其成员函数的异常安全性级别,方便使用者正确处理。
五、整体答案
#include <iostream>
using namespace std;
class CMyString
{
public:
//构造函数
CMyString(const char* pData = nullptr)
{
if (nullptr == pData)
{
m_pData = new char[1];
m_pData[0] = '\0';
}
else
{
int len = strlen(pData);
m_pData = new char[len + 1];
strcpy(m_pData, pData);
}
};
//拷贝构造函数
CMyString(const CMyString& other)
{
int len = strlen(other.m_pData);
m_pData = new char[len + 1];
strcpy(m_pData, other.m_pData);
};
//请写出该类型添加===>赋值运算符函数
/* 小菜
CMyString& operator= (const CMyString& other)
{
//1. 检查自赋值
if (&other == this)
{
return *this;
}
//2. 释放当前资源
delete[] m_pData;
m_pData = nullptr;
//3. 深拷贝
int len = strlen(other.m_pData);
m_pData = new char[len + 1];
strcpy(m_pData, other.m_pData);
return *this;
};*/
/*中登
CMyString& operator=(const CMyString& other) {
if (this != &other) { // 检查自赋值
// 1. 先分配新内存(若失败,原对象不受影响)
int len = strlen(other.m_pData);
char* pNewData = new char[len + 1]; // 若此处抛异常,旧资源仍有效
strcpy(pNewData, other.m_pData);
// 2. 再释放旧资源
delete[] m_pData;
// 3. 指向新内存
m_pData = pNewData;
}
return *this;
}*/
/*老鸟*/
CMyString& operator= (const CMyString& other)
{
//1. 检查自赋值
if (&other != this)
{
CMyString strTemp(other);
char* pTemp = strTemp.m_pData;
strTemp.m_pData = m_pData;
m_pData = pTemp;
}
return *this;
};
~CMyString()
{
delete[] m_pData;
m_pData = nullptr;
};
private:
char* m_pData;
};
int main() {
}