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

【C++】string的使用与模拟实现

请添加图片描述

string的使用与模拟实现

前言:string 是 C++ 中最常用的类之一,但你真的了解它的底层实现吗?本文将带你快速掌握 string 的核心用法,并深入模拟实现其关键功能,理解深浅拷贝等核心概念。
📖专栏:【C++成长之旅】


目录

  • string的使用与模拟实现
    • 一、标准库中的string类
      • 1.1 auto和范围for
      • 1.2 string类的常用接口说明
    • 二、string类的模拟实现
      • 2.1 string.h
      • 2.2浅拷贝与深拷贝
      • 2.3 string.cpp
    • 三、简单总结


一、标准库中的string类

首先,对于string类的学习我们也可以参考:
【string类文档介绍】
然后,在此之前,我们在这里学习2个C++11的小语法,方便我们后面的学习:

1.1 auto和范围for

auto关键字

在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

int main()
{int a = 10;auto b = a;auto c = 'a';return 0;
}

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&

int main()
{int x = 10;auto y = &x;auto* z = &x;auto& m = x;return 0;
}

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

int main()
{//编译报错:error C3538: 在声明符列表中,“auto”必须始终推导为同一类型auto cc = 3, dd = 4.0;return 0;
}

auto不能作为函数的参数,可以做返回值,但是建议谨慎使用

// 不能做参数
void func2(auto a)
{
}
// 可以做返回值,但是建议谨慎使用
auto func3()
{return 3;
}

auto不能直接用来声明数组

int main()
{//编译报错:error C3318: “auto []”: 数组不能具有其中包含“auto”的元素类型auto array[] = { 4, 5, 6 };return 0;
}
int main()
{// 编译报错:rror C3531: “e”: 类型包含“auto”的符号必须具有初始值设定项,不然无法推导auto e;return 0;
}

范围for

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束。
范围for可以作用到数组和容器对象上进行遍历
范围for的底层很简单,容器遍历实际就是替换为迭代器

对于迭代器简单解释一下:

我们可以把迭代器在行为上看做是指针(因为它模拟指针的用法),但实际上它的实现不一定是指针,而是一个为了统一遍历各种数据结构而设计的抽象工具。

其实这种“统一接口”的特性,正是C++ STL(标准模板库)的基石。(慢慢体会)

#include<iostream>
#include <string>
using namespace std;
int main()
{int array[] = { 1, 2, 3, 4, 5 };// C++98的遍历for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i){array[i] *= 2;}for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i){cout << array[i] << endl;}// C++11的遍历for (auto& e : array)e *= 2;for (auto e : array)cout << e << " " << endl;string str("hello world");//用"hello world"构造了一个stringfor (auto ch : str){cout << ch << " ";}cout << endl;return 0;
}

有了上面两个的基础,我们在C++中遍历容器就比较方便。

1.2 string类的常用接口说明

对于string类我只讲解最常用的接口,因为各种原因导致string类有些冗余。

  1. string类对象的常见构造
函数名称功能说明
string() (重点)构造空的 string 类对象,即空字符串
string(const char* s) (重点)用 C-string 来构造 string 类对象
string(size_t n, char c)string 类对象中包含 n 个字符 c
string(const string& s) (重点)拷贝构造函数
void test()
{string s1;              // 构造空的 string 类对象 s1string s2("hello world"); // 用 C 格式字符串构造 string 类对象 s2string s3(s2);          // 拷贝构造 s3
}

在这里插入图片描述

  1. string类对象的容量操作
函数名称功能说明
size (重点)返回字符串有效字符长度
length返回字符串有效字符长度
capacity返回空间总大小
empty (重点)检测字符串是否为空串,是返回 true,否则返回 false
clear (重点)清空有效字符
reserve (重点)为字符串预留空间**
resize (重点)将有效字符的个数改成 n 个,多出的空间用字符填充

说明:
size()与length()
方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
clear():
只是将string中有效字符清空,不改变底层空间大小。
resize(size_t n) 与 resize(size_t n, char c)
都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char
c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
reserve(size_t res_arg=0):
为string预留空间,不改变有效元素个数,但是reserve的参数小于string的底层空间总大小时,reserver会不会缩容是不确定的。
我们来看官方说法:

在这里插入图片描述
不确定的,也就是说reserve的参数小于string的底层空间总大小时,reserver会不会缩容是不确定的。但是:
在这里插入图片描述
此函数不会影响字符串长度,也无法修改其内容。其实可以看出来reserve()函数还是很温柔的,它只是建议保留n个空间。

  1. string类对象的访问及遍历操作
函数名称功能说明
operator[] (重点)返回 pos 位置的字符,const string 类对象调用
begin + endbegin 获取第一个字符的迭代器,end 获取最后一个字符下一个位置的迭代器 (左闭右开)
rbegin + rendrbegin 获取最后一个字符的迭代器,rend 获取第一个字符前一个位置的迭代器
范围for (C++11)C++11 支持更简洁的范围 for 的新遍历方式
#include<string>
#include<iostream>
using namespace std;
int main()
{string str = "hello world";// 1. operator[] 访问for (size_t i = 0; i < str.size(); ++i){cout << str[i] << " ";}cout << endl;// 2. 迭代器遍历for (auto it = str.begin(); it != str.end(); ++it){cout << *it << " ";}cout << endl;// 3. 反向迭代器遍历for (auto rit = str.rbegin(); rit != str.rend(); ++rit){cout << *rit << " ";}cout << endl;// 4. 范围for遍历 (C++11)for (char ch : str){cout << ch << " ";}cout << endl;return 0;
}

在这里插入图片描述

  1. string类对象的修改操作
函数名称功能说明
push_back在字符串后尾插字符 c
append在字符串后追加一个字符串
operator+= (重点)在字符串后追加字符串 str
c_str (重点)返回 C 格式字符串 (const char* 类型)
find + npos (重点)从字符串 pos 位置开始往后找字符 c,返回该字符在字符串中的位置
rfind从字符串 pos 位置开始往前找字符 c,返回该字符在字符串中的位置
substr在 str 中从 pos 位置开始,截取 n 个字符,然后将其返回
std::string str = "hello";// 1. push_back - 尾插字符
str.push_back('!');  // str becomes "hello!"// 2. append - 追加字符串
str.append(" world");  // str becomes "hello! world"// 3. operator+= - 追加字符串(更常用)
str += " C++";  // str becomes "hello! world C++"// 4. c_str - 返回C格式字符串
const char* cstr = str.c_str();  // 可用于C语言接口// 5. find - 查找字符/字符串
size_t pos = str.find('o');  // 返回第一个'o'的位置
if (pos != std::string::npos) 
{  // npos表示未找到std::cout << "Found at: " << pos << std::endl;
}// 6. rfind - 反向查找
size_t rpos = str.rfind('o');  // 从后往前找第一个'o'// 7. substr - 截取子串
std::string sub = str.substr(0, 5);  // 截取前5个字符:"hello"

对于npos解释:

在这里插入图片描述
它的字面意思是 “not a position” 或 “no position”,用于表示无效的或未找到的位置。
由于 size_t 是无符号类型,-1 会变成该类型能表示的最大值,即0xFFFFFFFF,我们的string的大小不可能是npos,就用npos表示无效的或未找到的位置
简单来说,npos 就是一个官方定义的、用来判断字符串查找操作是否成功的“失败标志”。

注意(习惯):

  1. 在string尾部追加字符时,s.push_back© / s.append(1, c) / s += 'c’三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
  2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
  1. string类非成员函数
函数名称功能说明
operator+字符串拼接,但尽量少用,因为传值返回导致深拷贝,效率低
operator>> (重点)输入运算符重载(用于从流中提取字符串)
operator<< (重点)输出运算符重载(用于将字符串插入到流中)
getline (重点)获取一行字符串(可指定分隔符)
relational operators (重点)大小比较(包括 ==, !=, <, <=, >, >= 等)
#include <iostream>
#include <string>
using namespace std;int main() {string s1 = "Hello";string s2 = "World";// 1. operator+ (不推荐频繁使用)string s3 = s1 + " " + s2; // 产生临时对象,深拷贝效率低cout << s3 << endl; // Output: Hello World// 2. operator>> 输入string input;cout << "Please enter a string: ";cin >> input; // 遇到空格停止cout << "You entered: " << input << endl;// 3. operator<< 输出cout << s1 << " " << s2 << endl; // Output: Hello World// 4. getline 获取一行(推荐)cin.ignore(); // 清除输入缓冲区cout << "Please enter a line: ";getline(cin, input); // 读取整行,包括空格cout << "You entered: " << input << endl;// 5. relational operators 比较if (s1 == s2) {cout << "Strings are equal" << endl;} else if (s1 < s2) {cout << s1 << " is less than " << s2 << endl;} else {cout << s1 << " is greater than " << s2 << endl;}return 0;
}

对于string类的接口还有很多,上面的几个接口大家了解一下,后续慢慢练习就熟悉了。string类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查前面说过的文档即可。

二、string类的模拟实现

2.1 string.h

上面已经对string类进行了简单的介绍,我们只要能够正常使用即可。但是在面试中,面试官总喜欢让我们自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
string的底层我们可以看作是:

在这里插入图片描述
当然,也可以更为简化,但是,我们为了方便书写代码,就可以像我这种,下面我们来完成string类的实现:

可以根据下面头文件中的声明来试着实现一下:
string.h:

class string
{friend std::ostream& operator<<(std::ostream& _out, const sxn::string& s);friend std::istream& operator>>(std::istream& _in, sxn::string& s);
public://迭代器重命名typedef char* iterator;
public:string(const char* str = "");string(const string& s);//string& operator=(const string& s);string& operator=(string s);~string();// iteratoriterator begin();iterator end();// modifyvoid swap(string& s);void push_back(char c);string& operator+=(char c);void append(const char* str);string& operator+=(const char* str);void clear();void swap(string& s);const char* c_str()const;// capacitysize_t size()const;size_t capacity()const;bool empty()const;void resize(size_t n, char c = '\0');void reserve(size_t n);// accesschar& operator[](size_t index);const char& operator[](size_t index)const;//relational operatorsbool operator<(const string& s);bool operator==(const string& s);bool operator<=(const string& s);bool operator>(const string& s);bool operator>=(const string& s);bool operator!=(const string& s);// 返回c在string中第一次出现的位置size_t find(char c, size_t pos = 0) const;// 返回子串s在string中第一次出现的位置size_t find(const char* s, size_t pos = 0) const;// 在pos位置上插入字符c/字符串str,并返回该字符的位置string& insert(size_t pos, char c);string& insert(size_t pos, const char* str);// 删除pos位置上的元素,并返回该元素的下一个位置string& erase(size_t pos, size_t len);private:char* _str = NULL;size_t _capacity = 0;size_t _size = 0;
};

2.2浅拷贝与深拷贝

这里就有个问题:拷贝构造和赋值重载需要我们自己实现吗?

在这里插入图片描述
回答是,要,必须要。

这里就涉及到了深浅拷贝的问题,前面的文章也说过了,但是都没有实操的机会,这里真好赶上了,就展开再说说,就以拷贝构造为例,假如是编译器自己生成的,就是浅拷贝/值拷贝,会导致:

在这里插入图片描述
这是我们想要的结果吗,不是,它还会导致在调用析构函数的时候,会调用两次,出现问题。
所以这里我们就要实现深拷贝,深拷贝之后要达到的效果:
在这里插入图片描述
其实也很好理解,因为编译器不知道你这里到底指向资源还是干什么,它只知道在这块空间上放着_str、_size、_capacity这三个,所以它只能完成浅拷贝。

有了深浅拷贝的理解,这些代码就很好完成了。

2.3 string.cpp

#include"string.h"const size_t string::npos = -1;//构造函数
string::string(const char* str):_str(new char[strlen(str) + 1])
{_size = strlen(str);strcpy(_str, str);_capacity = _size;// std::cout << "string(const char* str)" << std::endl;
}
//拷贝构造函数
/*string::string(const string& s)
{_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}*///现代写法
string::string(const string& s)
{string tmp(s._str);swap(tmp);
}//析构函数
string::~string()
{delete[] _str;_str = nullptr;_size = _capacity;//std::cout << "~string()" << std::endl;
}
// iterator
string::iterator string::begin()
{return _str;
}
string::iterator string::end()
{return _str + _size;
}
//赋值
/*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;
}*/
//现代写法
/*string& string::operator=(const string& s)
{string tmp(s._str);swap(tmp);return *this;
}*/
string& string::operator=(string s)
{swap(s);return *this;
}// modify
//尾插
void string::push_back(char c)
{reserve(_size + 1);_str[_size++] = c;
}
string& string::operator+=(char c)
{push_back(c);return *this;
}
//追加
void string::append(const char* str)
{int len = strlen(str);reserve(_size + len);strcpy(_str + _size, str);_size = _size + len;
}
string& string::operator+=(const char* str)
{append(str);return *this;
}
void string::clear()
{_size = 0;_str[0] = '\0';
}
void string::swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
const char* string::c_str()const
{return _str;
}
/////////////////////////////////////////////////////////////
// capacity
size_t string::size()const
{return _size;
}
size_t string::capacity()const
{return _capacity;
}
bool string::empty()const
{return _size == 0;
}
void string::resize(size_t n, char c)
{if (n <= _size){_str[n] = '\0';_size = n;}else{reserve(n);while (_size < n){_str[_size++] = c;}_str[_size] = '\0';}
}
void string::reserve(size_t n)
{static size_t count = 0;if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;std::cout << "reserve->" << ++count << std::endl;}
}
/////////////////////////////////////////////////////////////
// access
char& string::operator[](size_t index)
{assert(index < _size);return _str[index];
}
const char& string::operator[](size_t index)const
{assert(index < _size);return _str[index];
}
/////////////////////////////////////////////////////////////
//relational operators
bool string::operator<(const string& s)
{int i = 0;int j = 0;while (i < _size && j < s._size){if (_str[i++] >= s._str[j++]){return false;}}if (i < _size && j == s._size)return false;return true;
}
bool string::operator==(const string& s)
{int i = 0;int j = 0;while (i < _size && j < s._size){if (_str[i++] != s._str[j++]){return false;}}if (i != _size || j != s._size)return false;return true;
}
bool string::operator<=(const string& s)
{return *this == s || *this < s;
}
bool string::operator>(const string& s)
{return !(*this <= s);
}
bool string::operator>=(const string& s)
{return *this == s || *this > s;
}bool string::operator!=(const string& s)
{return !(*this == s);
}// 返回c在string中第一次出现的位置
size_t string::find(char c, size_t pos) const
{if (pos >= _size)return npos;while (pos < _size){if (_str[pos] == c)return pos;}return npos;
}
// 返回子串s在string中第一次出现的位置
size_t string::find(const char* s, size_t pos) const
{char* str = strstr(_str + pos, s);if (str == nullptr){return npos;}return str - _str;
}
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& string::insert(size_t pos, char c)
{assert(pos <= _size);reserve(_size + 1);size_t i = _size + 1;while (i > pos){_str[i] = _str[i - 1];--i;}_str[pos] = c;++_size;return *this;
}
string& string::insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (len == 0)return *this;reserve(_size + len);size_t i = _size + len;while (i >= pos + len){_str[i] = _str[i - len];--i;}strncpy(_str + pos, str, len);_size += len;return *this;
}// 删除pos位置上的元素,并返回该元素的下一个位置
string& string::erase(size_t pos, size_t len)
{assert(pos < _size);if (pos + len >= _size){_str[pos] = '\0';_size = pos;}else{int i = 0;while (len <= _size){_str[pos + i] = _str[pos + len];i++;len++;}_size = pos + i - 1;}return *this;
}//输入输出
std::ostream& operator<<(std::ostream& _out, const sxn::string& s)
{int n = 0;while (n < s._size){_out << s._str[n++];}return _out;
}/*  std::istream& operator>>(std::istream& _in, sxn::string& s){s.clear();char ch = (char)_in.get();while (ch != '\n'){s.reserve(++s._size);s._str[s._size-1] = ch;s._str[s._size] = '\0';//不写这个reserve函数中的strcpy()拷贝就会出错ch = _in.get();}s._str[s._size] = '\0';return _in;}*/
std::istream& operator>>(std::istream& _in, sxn::string& s)
{s.clear();//用一个数组做缓冲,减少开辟内存的时间const int N = 5;char buffer[N];//要使用get函数才行,因为cin以及输入时将空格忽视int ch = _in.get();int i = 0;while (ch != '\n'){//先写入bufferif (i < N){buffer[i++] = ch;}else{s.reserve(s._size + N);strncpy(s._str + s._size, buffer, N);s._size += N;s._str[s._size] = '\0';i = 0;}ch = _in.get();}if (i > 0){s.reserve(s._size + i + 1);strncpy(s._str + s._size, buffer, i + 1);s._size += i + 1;s._str[s._size] = '\0';}return _in;
}

在这里再提一个点:

在这里插入图片描述
可以看看区别,巧妙:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
函数之后,s自动析构,只能说,完美。

对于string类的模拟实现不仅仅是一个编程练习,更是理解C++面向对象、资源管理和运算符重载等核心概念的绝佳范例,要时间都可以练习练习。


三、简单总结

记住,真正掌握一个类,不仅要会用,更要知其所以然。正是我们迈向C++高手之路的重要一步!


如果本文对您有启发:
点赞 - 让更多人看到这篇硬核技术解析 !
收藏 - 实战代码随时复现
关注 - 获取数据结构系列深度更新
您的每一个[三连]都是我们持续创作的动力!

请添加图片描述

http://www.dtcms.com/a/394108.html

相关文章:

  • 新手向 算法 希尔排序-yang
  • 如何用RAG增强的动态能力与大模型结合打造企业AI产品?
  • 黑马头条_SpringCloud项目阶段五:openFeign服务接入以及接入腾讯云内容安全服务实现文章提交违规信息自动审核
  • Spring、SpringBoot框架核心流程详解
  • 195. Java 异常 - finally 块:Java 中的“兜底侠”
  • C语言底层学习(2.指针与数组的关系与应用)(超详细)
  • 008 Rust注释
  • ubuntu防火墙开放端口
  • ​MySQL 8.0.29 RPM 安装教程(CentOS 7 / RHEL 7 详细步骤)​附安装包
  • AIPPT:AI一键生成高质量PPT
  • [已更新]2025华为杯E题数学建模研赛E题研究生数学建模思路代码文章成品:高速列车轴承智能故障诊断问题
  • 从零到一:Vue3 + Spring Boot + MySQL 全栈项目部署到阿里云服务器完整教程
  • 微服务基础2-网关路由
  • ubuntu创建新用户
  • 黑豹X2(Panther-x2)armbian 驱动NPU/VPU的驱动下载安装
  • 50.Mysql主从复制与读写分离
  • 软件设计师,经典计算题
  • Python的bz2库讲解
  • 抖音2025创作者大会:优质内容播放时长增220%,推出四大计划
  • C++面向对象编程之继承:深入理解与应用实践
  • [Windows] OFD转PDF 1.2.0
  • TDengine 聚合函数 VAR_POP 用户手册
  • 跨域及其解决方法
  • LeetCode:37.二叉树的最大深度
  • 【C++深学日志】C++“类”的完全指南--从基础到实践(一)
  • BUS-消息总线
  • 23种设计模式之【单例模式模式】-核心原理与 Java实践
  • 精度至上,杜绝失真,机器视觉检测中为何常用BMP格式?
  • 关于wireshark流量分析软件brim(Zui)安装方法
  • springboot3.4.1集成pulsar