当前位置: 首页 > news >正文

剑指offer第2版——面试题1:赋值运算符函数

文章目录

  • 一、题目
  • 二、考察点
    • 2.1 处理自赋值情况(避免自身赋值导致的问题)
    • 2.2 释放当前对象已有的资源(防止内存泄漏)
    • 2.3 进行深拷贝(对于指针成员)
    • 2.4 返回引用类型(支持连续赋值)
  • 三、答案
    • 3.1 小菜写的
    • 3.2 中登写的
    • 3.3 老鸟写的
  • 四、扩展知识
    • 4.1 `char* str = "hello" 和char str[] ="hello"`有啥差别
    • 4.2 拷贝构造中的str.m_pData怎么访问到别的对象的m_pData呢?它不是一个私有成员变量吗?
    • 4.3 delete 不是删除一个指针吗?为啥这里delete char* 的时候 是delete []
    • 4.4 为啥可以&other == this,但是不能 other == *this?
    • 4.5 什么是“异常安全性原则”?
  • 五、整体答案

一、题目

补充该类的赋值运算符函数

#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 异常。此时:

  1. 原对象的 m_pData 已经被 delete[] 释放(变成空指针)
  2. 新内存分配失败,m_pData 仍然是 nullptr
  3. 异常抛出后,函数终止,后续的 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)技术,完美解决了之前提到的异常安全性问题,同时满足了所有核心要求。

这种写法的优点:

  1. 彻底解决异常安全问题
    • 先通过 CMyString strTemp(other) 创建临时对象,完成深拷贝。如果这一步失败(比如内存不足导致 new 抛出异常),原对象的 m_pData 完全不受影响,仍保持有效状态。
    • 后续的指针交换操作(strTemp.m_pDatam_pData 互换)是简单的指针赋值,不会抛出任何异常,确保一旦进入交换步骤,就能安全完成资源转移。
  2. 自动处理资源释放
    • 函数结束时,临时对象 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++ 中,deletedelete[] 的使用与内存分配方式严格对应,目的是正确释放内存并避免泄漏:
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;)等,这些操作不会抛出异常。

异常安全性的核心目标:

  1. 禁止资源泄漏:无论是否发生异常,动态分配的内存、文件句柄等资源必须被正确释放。
  2. 保持对象有效性:对象的内部状态必须符合其 “不变式”(如链表的头指针和尾指针逻辑一致、字符串以 \0 结尾等)。
  3. 可预测的程序状态:异常发生后,程序状态应可预测(要么回退,要么保持有效),避免后续操作因状态混乱而崩溃。

实际开发中的应用:

  • 对于赋值运算符、拷贝构造函数等涉及资源管理的函数,至少应保证强保证(通过 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() {}
http://www.lryc.cn/news/613500.html

相关文章:

  • CPTS Remote 复现
  • react-router/react-router-dom
  • 深度学习中主要库的使用:(一)pandas,读取 excel 文件,支持主流的 .xlsx/.xls 格式
  • 房产证识别在房产行业的技术实现及应用原理
  • 超高车辆如何影响城市立交隧道安全?预警系统如何应对?
  • 网络基础概念
  • 基于Qt的Live2D模型显示以及控制
  • ora-01658 无法为表空间 users中的段创建initial区
  • RocketMQ架构解析
  • 遥感卫星领域的AI应用
  • Day03 学习git
  • LWIP网络接口管理
  • [airplay2] airplay2简略介绍
  • 二分查找算法,并分析其时间、空间复杂度
  • IIS7.5下的https无法绑定主机头,显示灰色如何处理?
  • 前后端加密传数据实现方案
  • [ java SE ] 多人聊天窗口1.0
  • 强光干扰下裂缝漏检率↓82%!陌讯轻量化模型在道路巡检的落地实践
  • redis--黑马点评--用户签到模块详解
  • JAVA,Maven继承
  • 力扣经典算法篇-46-阶乘后的零(正向步长遍历,逆向步长遍历)
  • Linux Shell为文件添加BOM并自动转换为unix格式
  • 数据分析——Pandas库
  • 什么是 TDengine IDMP?
  • 机试备考笔记 7/31
  • 学习设计模式《二十一》——装饰模式
  • 人生后半场:从广度到深度的精进之路
  • 设计模式中的行为模式
  • 多线程 future.get()的线程阻塞是什么意思?
  • tcpdump问题记录