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

C++_string类_调用及模拟实现

【本节目标】

  • 1. 为什么要学习string类
  • 2. 标准库中的string类
  • 3. string类的模拟实现
  • 4. 扩展阅读

1. 为什么学习string类?

C语言中的字符串:

C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。

2. 标准库中的string类

2.1 string类(了解)

  1. string是表示字符串的字符串类。
  2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string。
  4. 不能操作多字节或者变长字符的序列。

注:在使用string类时,必须包含#include头文件以及using namespace std;

2.2 string类的常用接口说明(只讲解最常用的接口)

1. string类对象的常见构造


2.1.1 初始字符串类


2.1.2 拷贝字符串类部分数据

注:

  • 当后面元素个数不及拷贝个数多的时候,那么就会有多少元素拷贝多少元素,直到拷贝最后一个元素即可。
  • 如果我们在拷贝个数上面没有传入具体实参,那么缺省参数则为:size_t -1,也就是无符号整型最大数字。

2.1.3 在创建字符串类的过程中包含n个字符


2.1.4 字符串类赋值重载

2.2 string类对象的容量操作

2.2.1 容量操作_练习

输出结果:

问题:

从以上操作我们可以看到字符串类中的size()成员函数计算个数到'\0'停止,那这里就会产生问题了,我们有指向首元素地址的成员函数(begin)也有指向尾元素地址的成员函数(end())以及还有计算存储有效元素个数的成员函数(size),为什么还需要'\0'呢?

答:

  • std::string类不需要'\0'来标记字符串的结束,它依靠size()成员函数来确定字符串的长度。
  • 兼容C语言,C 风格字符串依赖'\0'来标记字符串的结束,在处理字符串时,需要遍历字符数组,直至遇到'\0'

2.2.2 反转字符串类

输出结果:


2.2.3 找到字符串类中最近的唯一字符

输出结果:

总结:在本道题目中使用计数排序是较为不错的办法,区别于传统的暴力查找办法,突破n^2限制。

如果对计数排序不了解的,有兴趣可以看看我的往期文章:C语言_数据结构_排序


2.2.4 观察字符串类扩容情况

输出结果:

我们可以看到编译器帮我们完成了自动扩容的步骤,但我们还可以用reserve()成员函数预留该空间进行插入操作。


2.2.5 resize()的使用:

输出结果:


2.3 string类对象的访问及遍历操作

遍历字符串类:

我们可以借助operator[]运算符重载进行对字符串类的遍历。

2.3.1 实现字符串类读、写操作


2.3.2 使用迭代器对字符串类进行遍历


2.3.3 反向迭代器_逆序字符串类


2.3.4 通过auto关键字进行遍历字符串类

当然,我们可以尝试以下能否对字符串类进行修改:

输出结果:

其原因在于auto类似于实参得临时拷贝,拷贝参数的改变并不能影响到实参的结果,所以这里我们可以使用引用进行改变:

当我们仅仅想读取数据的时候,为了数据的安全,可以在形参上变成:const 类&,这样就不需要担心别人会对数据做出改变。

目前为止,我们学习的迭代器共有四种:


2.4 string类对象的修改操作

2.4.1 练习

输出结果:

实际上,用append()成员函数还是过于冗余,下面我们可以用更简单的办法实现_运算符重载:

输出结果:


2.4.2 替换字符串类中指定的某个元素

输出结果:


2.4.3 Swap()成员函数的使用

输出结果:


2.4.4 find()成员函数的使用

对域名做出分段保存处理:

输出结果:

总结:通过这次练习实现了自动解析域名进行分段保存,对于find()以及substr()的使用会更加深刻。

2.4.5 find_first_of()

其主要功能是在对象里查找首个与指定字符序列中任意字符相匹配的字符,并返回该字符的位置。

输出结果:

我们从以上代码可以看出是n*m的时间复杂度,那么如何做到n的时间复杂度呢?

输出结果:


2.4.6 find_first_not_of()

帮助我们在字符串或者容器中找出第一个不匹配指定字符序列或元素集合的元素,在数据处理和筛选等场景中十分有用。

输出结果:


2.5 string类非成员函数

2.6 仅仅字母反转

输出结果:


2.7 获取最后一个单词的长度以及单词

输出结果:


2.8 扩展内容_getline()以及cin.ignore()的使用:

下面,我们延申出getline()以及cin.ignore()的使用:

2.9 判断字符串是不是回文结构

输出结果:

2.10 字符串相加

输出结果:


扩展内容:


3. 模拟实现string类

3.1 模拟实现string类构造函数(支持重载)

3.1.1 string(const char* str)

注:

  • 初始化列表中并不是按照列表中顺序进行执行,而是按照类中的成员变量顺序进行执行。

  • 初始化列表中初始和在成员变量中加上缺省参数的意义是一样的。

  • 之所以在容量上比size多出一个,是因为size是显示存储字符个数,而为了兼容C语言,需要留一个额外的空间给'\0'

3.1.2 拓展内容 _ string(const char* str) _ 结构完善

当我们对该成员函数使用缺省参数的时候,以下的用法是错误的:

解答:我们试图将char类型的值(ASCII码值为0)赋值给const char* 类型指针,类型不兼容。

正确写法:

当我们使用双引号的时候,编译器会自动帮我们在末尾隐式添加’\0’,所以我们也不需要额外传任何字符元素。

总结:

  • 使用const char* str = ""作为默认参数,统一处理无参构造和字符串初始化,避免了构造函数重载的冗余。

  • 在初始化列表中检查str == nullptr,并通过_size(str ? strlen(str) : 0)避免strlen 对空指针的解引用,符合防御性编程原则。

  • 计算_capacity = _size + 1 为 '\0'预留空间,且通过new char[_capacity]分配足够内存,避免缓冲区溢出。

  • 通过strcpymemcpy完整拷贝字符串内容(包括 ‘\0’),符合 C 风格字符串的约定。


3.1.3 string()

注:首先,给大家一个错误写法:

输出结果:

在平常的写法中,这样的代码并没有什么太大问题,但是在模拟string类中存在问题。我们在输出_str的时候,由于_strnullptr,导致无法正确找到'\0'所以导致产生问题。

正确写法:

这样的写法对于无参传入避免了后续输出_str的时候产生问题。


3.2 模拟实现string类析构函数


3.3 模拟实现string类的检查容量_成员函数


3.4 模拟实现返回字符串初始地址以及末尾地址


3.5 模拟实现operator[]成员函数


3.6 模拟实现size()成员函数


3.7 遍历字符串类中的元素

3.7.1 手动实现迭代器遍历


3.7.2 for循环遍历


3.8 模拟实现push_back()_CheckCapacity()


3.9 通过push_back实现对字符串类中的字符串不断增加

冗余写法:

输出结果:

简洁写法:

问题:

这里,我们不经产生一个问题,我们自己模拟实现的类通过调用成员函数,但是外面标准库中也有string类以及一模一样的成员函数,会不会调用到标准库中的string类成员函数

解答:

即便在代码中使用了using namespace std; 展开std命名空间,​只要明确使用XingC::string类型创建对象,所有成员函数的调用(如s[i]s.push_back())都会优先解析到你的自定义类 XingC::string 的成员函数,而不会调用到标准库的 std::string


3.10 模拟实现reserve(预分配内存)


3.11 模拟实现push_back()_reverse()


3.12模拟实现append(追加字符串)

3.13 模拟实现operator+=


3.14 模拟实现Insert()

3.14.1 Insert(size_t pos, char c)


3.14.2 Insert(size_t pos, const char* arr)


3.15 模拟实现erase


3.15 模拟实现比较运算符重载(< or <= or == or >= or >)


3.16 非成员函数模拟实现

3.16.1 模拟实现operator<<

声明友元函数:

定义友元函数:

可以发现我们这里的定义友元函数加上了作用域限定符,这也是我们之前定义友元函数时没有出现过的用法。让我们不经怀疑这个作用域限定符的出现是否跟命名空间内定义类存在关联。

  • 当命名空间的核心作用是将代码划分成不同的逻辑单元,避免不同模块或者库中的标识符发生命名冲突。当你在命名空间中定义类和声明友元函数时,这些标识符都处于该命名空间的作用域内。而全局作用域中的标识符则独立于所有命名空间。

  • 编译器在查找标识符时,会遵循特定的名称查找规则。当它处理命名空间中类声明的友元函数时,会优先在该命名空间内查找对应的函数定义。如果在命名空间内找不到匹配的定义,编译器不会自动将其与全局作用域中同名的函数关联起来,因为这可能会导致意外的结果和潜在的命名冲突。

  • 从代码设计的角度来看,将不同的功能模块放在不同的命名空间中,是为了让代码结构更加清晰、易于维护。如果允许命名空间中类声明的友元函数和全局作用域中同名的函数自动关联,会破坏这种设计意图,使代码的逻辑变得混乱,难以理解和调试。

综上所述,命名空间中的类声明的友元函数不能和全局作用域中同名的友元函数关联,这是为了保证命名空间的隔离性、遵循编译器的名称查找规则以及维护代码的清晰性和可维护性。


3.16.2 模拟实现operator>>

前置成员函数clear():

示例:

get()函数的作用:

cin.get()的核心特点就是仅读取一个字符,无论该字符是什么(包括空格、制表符、换行符),而不会自动跳过空白字符,并且未读取的字符会保留在输入缓冲区中。这使得它在需要精确控制输入流时非常有用。

  • 第一次需要输入:因为缓冲区初始为空,cin.get() 必须等待用户输入内容。
  • 后续无需输入:因为第一次输入的内容(如 abc\n)在缓冲区中排队,后续的 cin.get() 直接从缓冲区取字符,直到缓冲区被清空。

总结一下关键点:

  1. cin.get()只做一件事:从缓冲区 “拿走” 一个字符,不管是不是空格 / 换行符;
  2. 缓冲区是 “先进先出” 的队列:输入的内容会一直存在,直到被读取或清空;
  3. 遇到输入异常时(比如输入类型不匹配),记得用cin.clear()重置状态,配合cin.ignore()清空缓冲区~

输出结果:

更新写法:


3.17 模拟实现resize

输出结果:


3.18 模拟实现find


3.19 模拟实现substr


3.20 模拟实现拷贝构造

先说错误写法:

该错误原因在于:_str = new char[_size + 1]。我们要知道_size是我们目标对象的,而我们使用自己的_size重新开辟以后赋值给自己,这里产生的问题是:如果传递过来的源对象中_str开辟空间比目标对象中的还要大,那么也就会造成没有开辟足够的空间,造成越界行为。

正确写法:

提前确定好存储元素个数以及容量等,随后开辟_size + 1(不能漏掉存储’\0’所需要的空间)赋值给自己,最后进行strcpy把值拷贝回来即可。

现代写法简单了解:

浅拷贝存在的问题:

  1. 会造成析构两次
  2. 一个修改会影响另一个

解决方案:

引用计数:为解决浅拷贝引发的问题,可在类中增添一个计数变量。在进行对象赋值操作时,将该计数变量的值加 1;而在对象析构时,把计数变量的值减 1。当计数变量的值最终变为 0 时,再执行析构操作,释放 _str 所占用的内存。

3.21 模拟实现operator=(const string& s)

现代写法简单了解一下:


3.22 拓展内容

问题:以下s1和s2是否一样大呢?

答:是一样的,因为sizeof并不是针对存储有效个数而是实际开辟的空间也就是capacity。


引言:本章内容中主要讲解了string类的调用及模拟实现,在学会用的过程中对内部构造进一步加深自己的理解。

本章完~

http://www.jsqmd.com/news/887974/

相关文章:

  • tools.simonwillison.net图像处理工具集:从裁剪到优化的完整指南
  • 芯片逆向工程中的‘脏活累活’:如何用Cadence Virtuoso高效整理与验证提取后的电路?
  • 高密度光纤定位观测规划及相关技术【附代码】
  • 从Anthropic事件看AI安全:代码泄露、模型治理与工程实践
  • Python基础语法:访问器@property和修改器@xxx.setter
  • 抖音内容批量获取终极方案:Douyin Downloader 专业指南
  • MuJoCo物理仿真终极指南:深度解析接触动力学与7个实战调优技巧
  • 3个关键功能解析:USBToolBox如何简化macOS与Windows的USB端口映射难题
  • 告别无效投递:智能时间标签让你的简历精准触达活跃岗位
  • FCEUX终极指南:从怀旧游戏到专业调试的完整NES模拟器教程
  • MinIO + Docker 快速搭建 S3 兼容对象存储
  • 保姆级教程:手把手带你走通UDS Bootloader刷写全流程(附报文解析)
  • CPU环境也能跑!ChatGLM-6B-INT4嵌入式设备部署指南
  • 如何用AOT-GAN实现高分辨率图像修复:从原理到实践
  • Unity与Android Studio联合开发实战:AAR集成与双向调用避坑指南
  • 含分布式风力发电的微电网系统优化控制【附代码】
  • 身份证OCR识别接口接入实战:Python/Java/PHP/C#四语言代码示例与踩坑指南
  • 用Google Trends数据做时间序列可视化分析实战
  • Cloud Run 实战指南:容器即服务的零运维部署与生产优化
  • WinDiskWriter:macOS平台上的Windows启动盘制作技术解析
  • BeepBox高级功能探索:和弦、琶音和音效处理技巧 - 终极在线音乐创作指南
  • 2026年比较好的企业app软件开发/app软件开发榜单优选公司 - 行业平台推荐
  • 数据漂移与模型漂移实战检测:Python轻量级监控流水线
  • 如何利用Playwright CLI实现高效自动化测试:迁移后的终极实践指南 [特殊字符]
  • 数据竞赛实战方法论:从Kaggle竞赛到工业级解决方案的转型路径
  • tldr.jsx部署教程:快速搭建属于你的命令行文档浏览平台
  • 2026年高品质合金厂家哪家好?高品质Inconel718高温合金厂商推荐 - 品牌2025
  • Unity安卓APK安装失败排查指南:架构、签名与清单文件深度解析
  • 保姆级教程:在ROS2 Humble上搞定GY-95T IMU串口驱动与数据解析(附完整Python代码)
  • Unity WebView实战:3D渲染、JSBridge通信与跨端状态同步