【C++】经典string类问题

目录

1. 浅拷贝

2. 深拷贝 

3. string类传统写法

4. string类现代版写法 

5. 自定义类实现swap成员函数

6. 标准库swap函数的调用 

7. 引用计数和写时拷贝


1. 浅拷贝

若string类没有显示定义拷贝构造函数与赋值运算符重载,编译器会自动生成默认的,编译器生成的默认版本只会简单的复制指针地址,当用s1构造s2时,s1的_str指针存了"hello"的地址,拷贝给s2后,两者都指向同一块内存,这种拷贝方式叫做浅拷贝。当s1和s2先后析构时,这块内存会被delete两次,一旦其中一个对象释放了这块内存,_str所指的空间被释放掉,另一个对象的_str指针就会变成野指针,再次释放就会导致程序崩溃。

2. 深拷贝 

要避免浅拷贝问题,就需要自己实现深拷贝,让s2有独立的内存复制s1的内容,每个对象都有自己独立的内存空间,这样析构时各删各的就不会出问题。

//拷贝构造函数(实现深拷贝)
string(const string& s)
{_str = new char[s._capacity + 1]; //新开辟内存strcpy(_str, s._str); //复制内容_size = s._size;_capacity = s._capacity;
}

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显示给出来。

3. string类传统写法

class string
{
public://默认构造函数string(const char* str=""){if (str == nullptr) //strlen(nullptr)会触发未定义行为可能导致程序崩溃{str = "";     //将nullptr转为空字符串}_size = strlen(str);_str = new char[_size + 1];_capacity = _size;strcpy(_str, str);}//拷贝构造函数(深拷贝)string(const string& s):_str(new char[s._capacity + 1]), _size(s._size), _capacity(s._capacity){		strcpy(_str, s._str);  //拷贝字符串内容		}//赋值运算符重载(深拷贝)string& operator=(const string& s){if (this != &s)//防止自己给自己赋值{delete[] _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;return *this;}}//析构函数~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;	
};

4. string类现代版写法 

class string
{
public://默认构造函数string(const char* str=""){if (str == nullptr){str = "";     //将nullptr转为空字符串}_size = strlen(str);_str = new char[_size + 1];_capacity = _size;strcpy(_str, str);}//swap成员函数void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//拷贝构造函数优化 s2 = s1string(const string& s) //没有在成员初始化列表进行显式初始化时,使用了默认成员初始化器,防止默认初始化成随机值。{string tmp(s._str); //用s的字符串数据创建局部对象tmp   这里也可以调用拷贝构造string tmp(s);swap(tmp);          //交换当前对象s2和tmp     tmp出了作用域调用析构函数销毁}赋值运算符重载优化  s2 = s1//string& operator=(const string& s)//{		//	string tmp(s._str);//	swap(tmp);		//	return *this;//}//赋值运算符重载再优化  s2 = s1string& operator=(string tmp)  //传值传参触发拷贝构造!使用s1构造局部对象tmp{swap(tmp);                 //交换当前对象s2和tmp return *this;}//析构函数~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;
};

5. 自定义类实现swap成员函数

当自定义string类实现了swap成员函数,执行 std::swap(s1,s2) 时会通过模版机制自动转发到swap成员函数即s1.swap(s2),完成高效交换(交换内部指针,长度等,避免深拷贝),实现高效交换。如果自定义类中不实现成员swap,std::swap会走模版逻辑:

  • C++98:T c(a); a=b; b=c; 走“1次拷贝构造+2次拷贝赋值”的逻辑,3次深拷贝开销。
  • C++11:T c(std::move(a)); a=std::move(b); b=std::move(c);移动构造+两次移动赋值,依赖类的移动语义。

std::swap是一个模版函数,它的标准实现大致如下:

namespace std 
{// 默认模板:通过三次赋值实现交换(对于无swap成员函数的类类型交换:走深拷贝 对于内置类型的交换:无性能损耗)template <class T>void swap(T& a, T& b) {T temp = std::move(a);a = std::move(b);b = std::move(temp);}// 特化模板:若类型T存在swap成员函数,则调用该成员函数template <class T>void swap(T& a, T& b, std::enable_if_t<std::is_class_v<T> &&     {a.swap(b);     //……检测T是否有swap成员函数,如果有就调用它。}
}

这个模版的特别之处在于:它会自动检测类型T是否存在swap成员函数,若类型T未定义swap成员函数,std::swap会使用默认模版逻辑。

注意:std::swap模版的核心机制是严格匹配成员函数名swap,若成员函数名叫其它名字(如my_swap)无法转发调用成员函数,只能走模版逻辑。

总结:所以,只要类有swap成员函数,调用std::swap(s1,s2)最终会转调s1.swap(s2)。

    //swap成员函数void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}

示例:

int main()
{string s1("hello world");string s2("xxxxxxxxxxxxxxxxxxx");std::swap(s1, s2);//通过模版机制自动转发调用成员函数s1.swap(s2)cout << s1 << endl;cout << s2 << endl;s1.swap(s2); //调用成员函数cout << s1 << endl;cout << s2 << endl;return 0;
}

运行结果:

std::string类自身实现了swap成员函数,用于高效交换两个字符串的内部数据,直接交换内部的指针、大小、容量,相当于只交换“容器的壳”,数据本体不动,效率接近O(1),几乎是“零拷贝”操作,比默认的拷贝逻辑快得多。

6. 标准库swap函数的调用 

当调用 std::swap(basic_string)  这个全局函数时,它内部实际上会转发调用 basic_string 类的成员函数swap,也就是 std::basic_string::swap  。 所以,当swap(s1,s2)时,会匹配到特化版本,实际执行的是string对象成员的swap逻辑,等价于s1.swap(s2),例:

#include <string>
#include <iostream>
using namespace std;int main() 
{string s1 = "Hello";string s2 = "World";//调用全局 swap(匹配特化版本)swap(s1, s2); //内部转发调用s1.swap(s2);cout << "s1: " << s1 << ", s2: " << s2 << endl; // 输出 s1: World, s2: Helloreturn 0;
}

简单来说,怎么方便怎么写,想写什么写什么,它们最终执行的是同一个逻辑。

成员函数风格:s1.swap(s2)   

全局函数风格:swap(s1,s2) std::(s1,s2)

7. 引用计数和写时拷贝

引用计数

用来记录有多少个对象正在引用该资源。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源, 如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

写时拷贝 

当对象被复制时,采用浅拷贝的方式,不立即拷贝实际数据(“写时”才拷贝),此时多个对象共享同一资源。普通浅拷贝若多个对象共享数据,修改时会影响所有对象,所以当某个对象需要修改数据时,会触发数据的深拷贝,确保修改不会影响其他共享该数据的对象。

比如:先构造s1,再调用拷贝构造构造s2,我们走浅拷贝,将引用计数+1变成2,销毁s2的时候,不需要释放资源,只需要将引用计数-1变成1,对象的资源留给最后一个使用者释放。但是如果要修改s2,就需要引用计数-1,再执行深拷贝,修改s2的数据,对s2的修改不影响s1。

引用计数和写时拷贝通过“延迟拷贝”和“共享资源”减少开销,适用于读多写少的场景。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/87060.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/87060.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

kotlin中object:的用法

在Kotlin中&#xff0c;object: 用于声明匿名对象&#xff08;Anonymous Object&#xff09;&#xff0c;这是实现接口或继承类的轻量级方式&#xff0c;无需显式定义具名类。以下是核心用法和场景&#xff1a; 1. 基本语法 val obj object : SomeInterface { // 实现接口ov…

js代码04

题目 非常好。我们刚刚看到了回调函数在处理多个异步操作时会变得多么混乱&#xff08;回调地狱&#xff09;。为了解决这个问题&#xff0c;现代 JavaScript 提供了一个更强大、更优雅的工具&#xff1a;Promise。 Promise&#xff0c;正如其名&#xff0c;是一个“承诺”。…

Jenkins初探-通过Docker部署Jenkins并安装插件

简介 本文介绍了使用Docker安装Jenkins并进行初始配置的完整流程。主要内容包括&#xff1a; (1)通过docker pull命令获取Jenkins镜像&#xff1b;(2)使用docker run命令启动容器并映射端口&#xff1b;(3)访问Jenkins界面获取初始管理员密码&#xff1b;(4)安装推荐插件并创…

嵌入式开发:GPIO、UART、SPI、I2C 驱动开发详解与实战案例

&#x1f4cd; 本文为嵌入式学习系列第二篇&#xff0c;基于 GitHub 开源项目&#xff1a;0voice/EmbeddedSoftwareLearn &#x1f4ac; 作者&#xff1a;0voice &#x1f440; 适合对象&#xff1a;嵌入式初学者、STM32学习者、想搞明白外设驱动开发的C语言学习者 一、驱动是什…

常用 Linux 命令和 shell 脚本语言整理

目录 一、Linux 命令大全 1、文件和目录操作 &#xff08;1&#xff09;ls 列出目录内容 &#xff08;2&#xff09;pwd 查看当前目录 &#xff08;3&#xff09;cd 切换目录 &#xff08;4&#xff09;mkdir 创建目录 &#xff08;5&#xff09;cp 复制文件或目录 &…

YOLOv12_ultralytics-8.3.145_2025_5_27部分代码阅读笔记-autobackend.py

autobackend.py ultralytics\nn\autobackend.py 目录 autobackend.py 1.所需的库和模块 2.def check_class_names(names: Union[List, Dict]) -> Dict[int, str]: 3.def default_class_names(data: Optional[Union[str, Path]] None) -> Dict[int, str]: 4.cla…

【MySQL基础】MySQL索引全面解析:从原理到实践

MySQL学习&#xff1a; https://blog.csdn.net/2301_80220607/category_12971838.html?spm1001.2014.3001.5482 前言&#xff1a; 在前面我们基本上已经把MySQL的基础知识都进行了学习&#xff0c;但是我们之前处理的数据都是十分少的&#xff0c;但是如果当我们的数据量很大…

第三十五章 I2S——音频传输接口

第三十五章 I2S——音频传输接口 目录 第三十五章 I2S——音频传输接口 1 I2S概述 1.1 简介 1.2 功能特点 1.3 工作原理 1.4 利用DMA通信的I2S 1.4.1 I2S配合DMA通信工作原理 1.4.2 配置要点 2 应用场景 2.1 消费类音频设备 2.2 专业音频设备 2.3 通信设备 2.4 汽车电子 2.5 嵌…

产品-Figma(英文版),图像的布尔类型图例说明

文章目录 Union SelectionSubtract SelectionIntersect SelectionExclude SelectionFlatten Selection Union Selection 把多个形状合并成一个新的完整形状&#xff0c;保留所有外部轮廓&#xff0c;内部不被切割。由于红色的长方形在外面的一层&#xff0c;所以切割后&#x…

Windows CMD命令分类大全

⚙️ ‌一、系统与磁盘管理‌ ‌系统信息‌ systeminfo&#xff1a;查看详细硬件及系统配置&#xff08;版本/内存/补丁&#xff09;211 winver&#xff1a;快速检查Windows版本11 msinfo32&#xff1a;图形化系统信息面板811‌磁盘工具‌ chkdsk /f&#xff1a;修复磁盘错误&…

【Dify系列】【Dify1.4.2 升级到Dify1.5.0】

1. 升级前准备工作 1.1 数据备份&#xff1a; 进入原安装包 docker 目录&#xff0c;备份“volumes”文件夹&#xff0c;此文件夹包含了 Dify 数据库数据&#xff1a; rootjoe:/usr/local/dify/docker/volumes# pwd /usr/local/dify/docker/volumesrootjoe:/usr/local/dify/…

DeepSeek网页版随机点名器

用DeepSeek帮我们生成了一个基于html5的随机点名器&#xff0c;效果非常棒&#xff0c;如果需要加入名字&#xff0c;请在代码中按照对应的格式添加即可。 提示词prompt 帮我生成一个随机点名的HTML5页面 生成真实一点的名字数据 点击随机按钮开始随机选择 要有闪动的效果 &…

前后端分离实战2----后端

戳我抵达前端 项目描述&#xff1a;用Vscode创建Spring Bootmybatis项目&#xff0c;用maven进行管理。创建一个User表&#xff0c;对其内容进行表的基本操作&#xff08;增删改查&#xff09;&#xff0c;显示在前端。 项目地址&#xff1a;戳我一键下载项目 运行效果如下&…

深入 ARM-Linux 的系统调用世界

1、引言 本篇文章以 ARM 架构为例&#xff0c;进行讲解。需要读者有一定的 ARM 架构基础 在操作系统的世界中&#xff0c;系统调用&#xff08;System Call&#xff09;是用户空间与内核空间沟通的桥梁。用户态程序如 ls、cp 或你的 C 程序&#xff0c;无权直接操作硬件、访问文…

LabVIEW键盘鼠标监测控制

通过Input Device Control VIs&#xff0c;实现对键盘和鼠标活动的监测。通过AcquireInput Data VI 在循环中持续获取输入数据&#xff0c;InitializeKeyboard与InitializeMouse VIs 先获取设备ID 引用&#xff0c;用于循环内监测操作&#xff1b;运行时可输出按键信息&#xf…

Linux 系统管理:自动化运维与容器化部署

在现代 IT 基础设施中&#xff0c;自动化运维和容器化部署是提高系统管理效率和可维护性的关键。Linux 系统因其稳定性和灵活性而被广泛应用于服务器和数据中心。本文将深入探讨 Linux 系统管理中的自动化运维和容器化部署技术&#xff0c;帮助系统管理员实现高效运维和快速部署…

直播 APP 开发需要多少成本

直播行业的火爆催生了大量直播 APP 开发需求&#xff0c;而开发成本是开发者最关注的问题之一。其成本构成复杂&#xff0c;受功能需求、开发方式、技术难度等多种因素影响。​ 基础功能开发是成本的重要组成部分。用户注册登录、直播间创建与管理、视频播放、聊天互动等功能开…

Reactor操作符的共享与复用

在 Reactor 中&#xff0c;transform 和 transformDeferred 是两个用于代码复用和操作符链封装的高级操作符。它们允许你将一组操作符封装成一个函数&#xff0c;并在适当的时候应用到响应式流中。以下是它们的详细总结&#xff1a; 1. transform 操作符 作用&#xff1a;tran…

C#中的Converter详解

Converter是C#中一个非常有用的概念&#xff0c;主要用于类型转换。它通常以委托或接口的形式出现&#xff0c;允许开发者定义如何将一种类型转换为另一种类型。下面我将详细介绍Converter的概念、使用场景&#xff0c;并以布尔型转换为例展示具体应用。 Converter的基本概念 …

LabVIEW荧光微管图像模拟

利用LabVIEW平台&#xff0c;集成 PI 压电平台、Nikon 荧光显微镜及Andor sCMOS 相机等硬件&#xff0c;构建荧光微管滑行实验图像序列模拟系统。通过程序化模拟微管运动轨迹、荧光标记分布及显微成像过程&#xff0c;为生物医学领域微管跟踪算法测试、运动特性分析提供标准化仿…