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

C++初阶-string类的模拟实现与改进

目录

1.序言

2.std::operator>>(istream& in, string& s)的改进

2.1原始代码

2.2问题分析

2.3最终改进

3.getline(istream& is,string& str,char delim='\n')的模拟实现

4.string(const string& s)的改进(拷贝构造)

4.1原始版本

4.2现代版本

5.string::swap(string& s)的模拟实现

6.string::operator=(const string& s)的改进

6.1原始版本

6.2现代版本

7.*引用计数的写时拷贝(扩展)

(1) 什么是写时拷贝?

(2) 适用场景

(1) 引用计数(Reference Counting)

示例:简单 COW 字符串

(2) 智能指针 + COW

8.总结



1.序言

我们在用之前的代码的时候总是会出现这个或那个限制或问题,所以需要优化代码,提升效率,这一讲将围绕>>运算符、swap、拷贝构造、=运算符重载等部分进行改进或模拟实现,并且会额外介绍一下其余的知识比如:写时拷贝。这讲内容还是比较重要的,所以建议还是看一下。

2.std::operator>>(istream& in, string& s)的改进

2.1原始代码

istream& operator>>(istream& in, string& s)
{char ch;in >> ch;while (ch != ' ' && ch != '\n'){s += ch;in >> ch;}return in;
}

2.2问题分析

用以下代码进行测试

void test1()
{string s;cin >> s;cout << s << endl;string s1;cin >> s1;cout << s1 << endl;
}

如果输入:

那么是不会结束输入的,因为我们那样写,不管怎么样都拿不到空格和换行,因为本身cin和scanf是拿不到空格和换行的!,所以我们不能这样写,要用另外一个函数叫get(),这个函数的解释我们可以通过deepseek来了解:

我们现在只要了解第一个即可,所以我们可以把这个形式改为:

istream& operator>>(istream& in, string& s)
{char ch = in.get();while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;
}

用刚刚的代码进行测试,则结果为:

第一行为输入的,后面两行是输出的。但是如果我们用+=的方式是会出问题的,只是在这里演示不出来,如果在其他编译器上这样输入,可能会导致输入Hello world结果打印

Hello 

Hello world world

了,所以我们要在开始加:s.clear();

所以变成:

istream& operator>>(istream& in, string& s)
{s.clear();char ch = in.get();while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;
}

2.3最终改进

如果我们一个一个字符加,要不断扩容,有效率的损失,我们可以参考之前的string的优化,加一个buff来进行存储,但是这个buff,我们把buff这个数组的空间开辟成1024个字符即:char buff[1024];因为若提前s.reserve(1024)则可能会有空间的浪费,未知扩容我们最好还是别做;所以最终代码可以改为:

istream& operator>>(istream& in, string& s)
{s.clear();//好处// 输入短串,不会浪费空间// 输入长串,避免不断扩容const size_t N = 1024;char buff[N];int i = 0;char ch = in.get();while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 1023){buff[1023] = '\0';s += buff;i = 0;//这样就不用扩容了}}//可能还有数据没有插入if (i > 0){buff[i] = '\0';s += buff;}return in;
}

3.getline(istream& is,string& str,char delim='\n')的模拟实现

这个实现之前是讲不了的,因为这个函数也是需要用到我刚刚讲过的知识的,这个函数与>>的区别仅仅是遇到delim时就停止输入了,所以我们只要稍微改变一点代码即可:

//.cpp
istream& getline(istream& in, string& s, char delim)
{s.clear();//好处// 输入短串,不会浪费空间// 输入长串,避免不断扩容const size_t N = 1024;char buff[N];int i = 0;char ch = in.get();while (ch != delim){buff[i++] = ch;if (i == N - 1){buff[i] = '\0';s += buff;i = 0;//这样就不用扩容了}//一定要记得(我自己也忘记写了)ch = in.get();}//可能还有数据没有插入if (i > 0){buff[i] = '\0';s += buff;}return in;
}

这两个函数(>>和getline)我们都可以不置为友元函数。

用以下函数进行测试:

//.cpp
void test1()
{string s;td::getline(cin, s);cout << s << endl;string s1;td::getline(cin, s1, '!');cout << s1 << endl;
}

则运行结果为:

4.string(const string& s)的改进(拷贝构造)

4.1原始版本

//.cpp
string::string(const string& s)
{//我们需要多申请一块空间,因为我们还要存储\0_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}

这个拷贝构造是用深拷贝的方式,借用了strcpy函数,俗称传统写法,而现代写法更好一些(里面用了swap函数等下我会实现string类专门的swap函数):

4.2现代版本

//.cpp
//现代版本
string::string(const string& s)
{string tmp(s._str);swap(_str, tmp._str);swap(_size, tmp. _size);swap(_capacity, tmp._capacity);
}

这种方法没有提高效率,只是创建了一个临时对象,由于通过tmp来进行交换,所以tmp的改变不会影响s,但是如果我们直接string tmp(s)则会造成死循环!

如果我们用之后实现的swap那么就可以直接这样写:

//.cpp
//现代版本(改进)
string::string(const string& s)
{string tmp(s._str);swap(tmp);
}

我们要使用的时候可以直接string s;string s1(s);即可。

5.string::swap(string& s)的模拟实现

这个东西的实现如果直接创建三个临时变量(_str,_capcity,_size的类型),这样写法太麻烦了,而且我们实现的时候可以用库里面的swap,不过一定要指定类域,否则会直接从该类域找函数了,这样会导致参数不匹配而报错!

//.h
void swap(string& s);
//.cpp
void string::swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}

6.string::operator=(const string& s)的改进

我们也可以根据拷贝构造的改进写出赋值运算符重载函数的改进:

6.1原始版本

//.cpp
//原始版本
string& string::operator=(const string& s)
{//开辟新空间,释放旧空间delete[]  _str;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;return *this;
}

6.2现代版本

//.cpp
//现代版本
string& string::operator=(const string& s)
{//开辟新空间,释放旧空间if (this != &s){string tmp(s._str);swap(tmp);}return *this;
}

当然还有第二种方式:

//.cpp
//现代版本(2)
string& string::operator=(string s)
{swap(s);return *this;
}

因为s是形参,不是实参的别名,故可以直接交换,建议用第二种写法。但是一定不要把第二种写法的形参改为引用了!

7.*引用计数的写时拷贝(扩展)

这里涉及到的内容非常多,到deepseek的搜索结果后可以跳过到总结部分。

我们先看引用计数,这个的意思是有几个对象指向一块相同的空间就用引用计数,也就是说假设s1、s2都是s3的别名,那么这个就是s3的引用计数就是2,这样拷贝对象的代价就低很多了,因为不用开辟空间拷贝数据了,而且析构时也不是直接delete[]了,而是看是否有另外一个对象指向这块空间了,若s1、s2指向同一块空间,若s2先析构,则引用计数--,如果引用计数不是0,则代表还有对象指向这个空间,则这个时候不能释放该空间。这种方式保证了不会一块空间被释放多次的问题了!

但是这样还会存在一个对象修改还会影响另外一个的问题,如果s1修改了,那s2也修改了,我们不希望这样的情况发生,这个就需要写时拷贝,假设我要把s1修改,然后要看引用计数,如果引用计数为1,则能直接修;否则s1改变,我就需要拷贝,把引用计数--后再进行深拷贝。写时拷贝就是谁写谁先深拷贝再修改,本质就是延迟拷贝。但是这样很麻烦了。

因此这种方式本质上是一种赌博,只要对象不修改就赚大了(拷贝后不进行修改就赚了),因为若编译器未在拷贝构造时优化,比如我们string func(){string ret="abcdef";return ret;}再用string s = func();那么就会有三次拷贝,这个之前说过(讲类的构造的时候,之前写过博客);如果编译器没有优化,那么这种引用计数的写时拷贝就赚麻了,因为它不一定会修改的,就算要修改要深拷贝,代价也差不多。

在string部分,这个技术已经被淘汰了,但是其他的地方可能还会用,比如Liunx、智能指针里面会用到!

如果想要详细了解这个部分的话我们可以看一下Deepseek的搜索结果:

写时拷贝(Copy-On-Write, COW)是一种延迟拷贝的优化技术,在对象被多个引用共享时,只有在真正需要修改数据时才进行实际的拷贝。这种技术广泛应用于字符串、智能指针、文件系统等场景,以减少不必要的内存复制。

(1) 什么是写时拷贝?

  • 默认共享:多个对象引用同一块内存(避免立即拷贝)。

  • 修改时分离:当某个对象尝试修改数据时,才进行真正的拷贝(深拷贝)。

  • 节省资源:减少不必要的内存复制,提高性能。

(2) 适用场景

  • 不可变数据(如字符串、只读文件)。

  • 多引用共享(如 std::shared_ptr 的引用计数)。

  • 大对象优化(避免频繁深拷贝)。

实现方式

(1) 引用计数(Reference Counting)

  • 每个对象维护一个引用计数器,记录有多少个引用指向它。

  • 引用增加:计数器 +1

  • 引用减少:计数器 -1,如果 count == 0,释放内存。

  • 修改时检查:如果 count > 1,先拷贝再修改。

示例:简单 COW 字符串
#include <iostream>
#include <cstring>class CowString {
private:char* data;         // 实际字符串数据int* ref_count;     // 引用计数器public:// 构造函数CowString(const char* str = "") {data = new char[strlen(str) + 1];strcpy(data, str);ref_count = new int(1);  // 初始引用计数=1}// 拷贝构造函数(共享数据,不拷贝)CowString(const CowString& other) : data(other.data), ref_count(other.ref_count) {(*ref_count)++;  // 引用计数+1}// 写时拷贝:修改数据时才真正拷贝char& operator[](size_t index) {if (*ref_count > 1) {  // 如果被共享,先拷贝char* new_data = new char[strlen(data) + 1];strcpy(new_data, data);(*ref_count)--;     // 旧数据引用-1data = new_data;    // 指向新数据ref_count = new int(1);  // 新数据引用=1}return data[index];    // 返回可修改的引用}// 析构函数~CowString() {if (--(*ref_count) == 0) {  // 引用计数归零时释放delete[] data;delete ref_count;}}// 打印字符串和引用计数void print() const {std::cout << "String: " << data << " (Refs: " << *ref_count << ")" << std::endl;}
};int main() {CowString s1 = "Hello";  // s1 引用计数=1CowString s2 = s1;       // s2 和 s1 共享数据,引用计数=2s1.print();  // String: Hello (Refs: 2)s2.print();  // String: Hello (Refs: 2)s2[0] = 'J';  // 写时拷贝!s2 现在独立,s1 仍指向原数据s1.print();  // String: Hello (Refs: 1)s2.print();  // String: Jello (Refs: 1)return 0;
}

输出:

String: Hello (Refs: 2)
String: Hello (Refs: 2)
String: Hello (Refs: 1)
String: Jello (Refs: 1)

(2) 智能指针 + COW

std::shared_ptr 本身已经实现了引用计数,可以结合 COW 优化:

#include <memory>
#include <iostream>class BigObject {
public:BigObject() { std::cout << "BigObject created\n"; }~BigObject() { std::cout << "BigObject destroyed\n"; }void modify() { std::cout << "BigObject modified\n"; }
};int main() {std::shared_ptr<BigObject> ptr1 = std::make_shared<BigObject>();std::shared_ptr<BigObject> ptr2 = ptr1;  // 共享,引用计数=2if (!ptr1.unique()) {  // 检查是否唯一引用ptr1 = std::make_shared<BigObject>(*ptr1);  // 写时拷贝}ptr1->modify();  // 现在 ptr1 是独立的return 0;
}

也可以进入以下链接去看内容:

https://coolshell.cn/articles/12199.html

8.总结

该讲主要是把一些补充的知识添加了一下,真正讲解的不是很多,到此,所有关于string类的内容已经讲解完全了,下一讲内容是vector,需要到下一周去了,之后可能会把一些string类的练习题的博客发出来,这个是不确定时间的!

喜欢的可以一键三连哦!下讲再见!

相关文章:

  • ORB特征点检测算法
  • Java SpringMVC 异常处理:保障应用健壮性的关键策略
  • uni-app微信小程序登录流程详解
  • 【SSM-SpringMVC(三)】Spring接入Web环境!介绍SpringMVC的拦截器和异常处理机制
  • 《Asp.net Mvc 网站开发》复习试题
  • 典籍知识问答重新生成和消息修改Bug修改
  • Linux `man` 指令终极指南
  • 【Python】UV:单脚本依赖管理
  • GitDiagram - GitHub 仓库可视化工具
  • WordPress 网站上的 jpg、png 和 WebP 图片插件
  • C++23 中的 views::stride:让范围操作更灵活
  • 5.5.1 WPF中的动画2-基于路径的动画
  • 用python清除PDF文件中的水印(Adobe Acrobat 无法删除)
  • python可视化:2025Q1北方游客量与客运流动分析3
  • 设计模式之中介者模式
  • 基于STM32、HAL库的CH342F USB转UART收发器 驱动程序设计
  • 人工智能时代:解锁职业新身份,从“认证师”到“工程师”的进阶之路
  • 电商物流管理优化:从网络重构到成本管控的全链路解析
  • 【Linux笔记】——进程信号的保存
  • JVM对象头中的锁信息机制详解
  • 西王食品连亏三年:主业齐“崩”,研发人员多为专科生
  • 新疆交通运输厅厅长西尔艾力·外力履新吐鲁番市市长候选人
  • 5天完成1000多万元交易额,“一张手机膜”畅销海内外的启示
  • 人民财评:网售“婴儿高跟鞋”?不能让畸形审美侵蚀孩子身心
  • 央行谈MLF:逐步退出政策利率属性回归流动性投放工具
  • 47本笔记、2341场讲座,一位普通上海老人的阅读史