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

从 0 到 1 实现 C++ string 类:深入理解动态字符串的底层机制--《Hello C++ Wrold!》(11)--(C/C++)

文章目录

  • 前言
  • string类的模拟实现(可能和库里面的有偏差哈)
  • 写时拷贝

前言

在 C++ 编程中,标准库提供的string类是处理文本数据的核心工具,但它的内部实现细节却如同一个 “黑箱”。理解string类的底层原理,不仅是面试中的高频考点,更是掌握 C++ 内存管理与泛型编程思想的关键一步。本章节将从零开始模拟实现一个简化版的string类,通过手动实现动态扩容、迭代器、拷贝构造等核心功能,带你揭开string类的神秘面纱。

我们的实现将聚焦于以下核心内容:
动态内存管理:通过reserve和resize实现按需扩容,避免频繁内存分配

迭代器设计:模拟实现begin()和end()接口,支持范围 for 循环等 STL 算法

拷贝控制:深入探讨浅拷贝与深拷贝的区别,通过现代 C++ 手法实现高效赋值

核心操作实现:包括插入、删除、查找等关键接口,理解其中的边界处理与性能优化

在实现过程中,我们将特别关注以下易错点:
内存泄漏与野指针问题(如析构函数的正确实现)

扩容时的数据迁移与\0结尾符的处理

插入 / 删除操作中的大量数据移动开销

此外,我们还将延伸讨论 “写时拷贝(Copy-On-Write)” 技术,了解如何通过引用计数优化内存使用效率。通过这个完整的实现案例,你不仅能掌握string类的底层原理,更能深刻理解 C++ 中资源管理、深拷贝 / 浅拷贝等重要概念。

无论你是希望深入理解 STL 容器的开发者,还是正在备战技术面试的求职者,亲手实现一个string类都是一次不可多得的学习机会。让我们一起动手,从底层构建属于自己的字符串类,感受 C++ 内存管理的精妙之处!

string类的模拟实现(可能和库里面的有偏差哈)

namespace renshen
{class string{public:typedef char* iterator;//模拟实现迭代器typedef const char* const_iterator;//只能读的迭代器
//这俩搞的是内部类,在main函数里用要:renshen::string::iterator这样才是一个类型iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}string(const char* str = "")//构造函数
//""是C风格字符串,自带\0的{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);}string(const string& s)//拷贝构造函数{_str = new char[s._capacity + 1];memcpy(_str, s._str, s._size + 1);
//注意,这里要是memcpy,不能是strcpy;因为字符串中间可能有\0这些_size = s._size;_capacity = s._capacity;}/*string(const string& s):_str(nullptr),_size(0),_capacity(0)
这里必须要初始化,
因为delete[]一个未初始化的_str,导致未定义行为{string tmp(s._str);->如果对象是h\0a的话就遭了,因为这个时候会用上面那个构造函数swap(tmp);
}*//这种的用现代写法不好~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}const char* c_str() const{return _str;}size_t size() const{return _size;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}const char& operator[](size_t pos) const 
//const renshen::string s1;这种要用这个,而且char&前面那个const不能去掉的{assert(pos < _size);return _str[pos];}
//库里面也是这样写两种的void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];memcpy(tmp, _str, _size+1);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);//一般这么搞的//判断_capacity这一下很巧妙}_str[_size] = ch;++_size;_str[_size] = '\0';}void append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){reserve(_size+len);}memcpy(_str + _size, str, len+1);_size += len;}string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* str){append(str);return *this;}void insert(size_t pos, size_t n, char ch){assert(pos <= _size);if (_size +n > _capacity){reserve(_size + n);}size_t end = _size;while (end >= pos && end != npos)//注意这里的end!=npos避免了pos为0的话出现的异常
//异常情况的诱因:end是size_t类型的
//注意:改成int也不行,因为有符号和无符号比较,有符号会整型提升成无符号进行比较
//int转成double叫做算数转换哈{_str[end + n] = _str[end];--end;}for (size_t i = 0; i < n; i++){_str[pos + i] = ch;}_size += n;}void insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}size_t end = _size;while (end >= pos && end != npos){_str[end + len] = _str[end];--end;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;}void erase(size_t pos, size_t len = npos){assert(pos <= _size);if (len == npos || pos + len >= _size)//注意这里顺序的巧妙之处{_str[pos] = '\0';_size = pos;_str[_size] = '\0';}else{size_t end = pos + len;while (end <= _size){_str[pos++] = _str[end++];}_size -= len;}}
//由此可见:insert和erease尽量少用,代价太大了size_t find(char ch, size_t pos = 0){assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == ch){return i;}}return npos;}size_t find(const char* str , size_t pos = 0){assert(pos < _size);const char* ptr = strstr(_str + pos, str);//注意strstr的用法if (ptr){return ptr - _str;}else{return npos;}}string substr(size_t pos = 0, size_t len = npos)//pos是开始查的位置(此需查){assert(pos < _size);size_t n = len;if (len == npos || pos + len > _size){n = _size - pos;}string tmp;tmp.reserve(n);
//new出来的空间虽然出了函数还在,可是tmp是string类型的,有析构函数,出了函数就被析构释放了
//之后里面的东西会深拷贝出去哈for (size_t i = pos; i < pos + n; i++){tmp += _str[i];//注意理解这里,和s1+="a"再+="b"是一个道理//这样搞的话,\0永远还是在最后}return tmp;}void resize(size_t n, char ch = '\0'){if (n < _size){_size = n;_str[_size] = '\0';}else{reserve(n);for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\0';}bool operator<(const string& s) const
{int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);// 不要搞成_size+1这样,这样就把\0也比进去了,虽然答案还是一样,但是意义就相悖了//(字典序那样比较大小是不把\0结束标志搞进去的) return ret == 0 ? _size < s._size : ret < 0;
}bool operator==(const string& s) const
{return _size == s._size && memcmp(_str, s._str, _size) == 0;//这种先后顺序会好些
}
//只要实现两个,其他的都能用这俩个实现了bool operator<=(const string& s) const
{return *this < s || *this == s;
}void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
//不能写成std::swap(tmp,*this);不然会无穷递归
}string& operator=(string tmp)
{swap(tmp);//这里调用函数,swap里面依旧是可以直接用那个的成员变量return *this;
}//这里返回*this最主要想满足链式赋值}private:size_t _size;size_t _capacity;//像这种肯定是非负数的模拟实现一般要搞成size_tchar* _str;public:const static size_t npos;};const size_t string::npos = -1;//这个也能放在命名空间里
std::ostream& operator<<(std::ostream& out, const string& s)
//没在命名空间里搞的话,要写成renshen::string
{for (auto ch : s){out << ch;}return out;
}
std::istream& operator>>(std::istream& in, string& s)
{s.clear();char ch = in.get();// 处理前缓冲区前面的空格或者换行while (ch == ' ' || ch == '\n'){ch = in.get();}char buff[128];//这样到一定容量才放的话会比s直接+ch的频繁扩容要好int i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[i] = '\0';s += buff;i = 0;}ch = in.get();//注意这个get是cin的成员函数}if (i != 0){buff[i] = '\0';s += buff;}return in;
}
};

在这里插入图片描述

注意点:1.记得检查pos位置是否合法

        2.string里面的s1.[s1.size()]位置存的是`\0`3.范围for只要模拟实现出begin和end这两个迭代器就可以用了(这两个迭代器的名字都不能变,范围for的识别很死板的)4.自己有些遗忘strcpy和memcpy的用法了:
char * strcpy ( char * destination, const char * source );
引申:string类型不能被隐式转换成C风格的
memcpy的话是可以指定到哪截止   strcpy的话是遇到\0就终止(但他会默认拷贝上\0)
引申:回忆下strcmp和memcmp
 5.扩容时要注意_capacity是不是等于06.自己重载出来的<<和直接cout<<s1.c_str()出来的还是有不同的,比如:在string类型的字符串下中间有空格时

7.流插入和流提取不让拷贝

在这里插入图片描述

写时拷贝

用途:用来解决浅拷贝相较于深拷贝的问题(由于指向同一空间导致的)

1.会析构多次导致错误 2.一个对象修改了会影响多个对象的内容

引入的一个引用计数去解决这两个问题

解决方法:第一个问题:引用计数是1时进行析构才让析构

           第二个问题:在修改时,如果引用计数不是1,就先进行深拷贝,再修改

作用:可以避免一些用浅拷贝就可以满足的场景却用了深拷贝

http://www.lqws.cn/news/488557.html

相关文章:

  • 编程实践:sigmastar330 调用IVE图像处理加速
  • Linux密码校验机制深度剖析:从shadow文件到crypt加密
  • PicHome结合容器化与内网穿透实现跨平台影像管理
  • 使用docx4j 实现word转pdf(linux乱码处理)
  • 支持java8的kafka版本
  • Halcon ——— OCR字符提取与多类型识别技术详解
  • Python Arrow 库详解:更智能的日期时间处理
  • 北京他山科技:全球首款AI触觉感知芯片破局者
  • 七八章习题测试
  • 半导体行业中的专用标准产品ASSP是什么?
  • SRS(Simple Realtime Server) 开源直播/双录服务器
  • 商品中心—11.商品B端搜索系统的实现文档二
  • 嵌入式系统内核镜像相关(六)
  • 网络/信号/电位跟踪
  • Actuator内存泄露及利用Swagger未授权自动化测试实现
  • 【软考高级系统架构论文】论面向方面的编程技术及其应用
  • 班翎流程平台 | 逐级审批支持多种选择模式
  • OpenHarmony中默认export 添加环境变量
  • 用html实现数字生命
  • 日志技术-Logback入门程序
  • Life:Internship in OnSea Day 1
  • 【nvidia-H100-ib排障实战2】:服务器 InfiniBand 网络性能问题深度分析
  • 深入理解JavaScript设计模式之迭代器模式
  • 【python实用小脚本-111】基于PyTorch的人脸口罩检测系统技术文档
  • 构建你的API防护盾 - 抵御恶意爬虫、注入与业务欺诈
  • qwen 的词编码模型中tokenid 到 高维向量是什么实现的,tokenid的排列有什么特点
  • 【Altium Designer】---- 02创建元器件符号和封装
  • 基本图算法介绍
  • Maven 之 JUnit 测试体系构建全解析
  • 淘宝直播带货API开发:弹幕抓取与商品点击热力图生成系统