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

今日分享:C++ string 类模拟实现

😎【博客主页:你最爱的小傻瓜】😎

🤔【本文内容:C++  string类😍】🤔

---------------------------------------------------------------------------------------------------------------------------------

字符串若璀璨织锦,assign 是重新勾勒的经纬,insert 是悄然添彩的绣线。在重置与嵌入间穿梭,领略代码里藏着的,关于重塑与点缀的智慧。下面就开启精彩剖析,带各位实现 string 的奇妙工坊,认真瞧,别掉队呀!

---------------------------------------------------------------------------------------------------------------------------------

上文链接:string类的使用

1.前言:

我们接下来要实现的string类主要是 其构造  拷贝构造 赋值运算符重载 析构函数。

2.模拟实现string类的准备:

为了避免自己实现的string类 与 库中的string类 重名,则我们需要用一个命名空间域

在我们之前所了解过的数据结构里,动态顺序表中的结构体的变量,其实与string类里面的成员变量类似。

其文链接:顺序表

namespace xin
{class my_string{public://成员函数private://给出缺省值char* _str = nullptr;//对应指向字符串数组的指针size_t _size = 0;    //实际存储的字符串的大小size_t _capacity = 0;//开辟的总空间大小。};
};

有了这个命名空间,我们需要通过xin::前缀来访问我们命名空间里面的内容。在调用模拟的内容时,要写成xin::string::调用的内容。并且也可以与库中的string类进行对比

3.模拟实现string类里的成员函数:

一 . 构造函数:

        在我们之前学过的string的使用里面的构造函数 ,我们只要是实现里面的重点。

1.构造空的 string 类对象,即空字符串:
 

string():_size(0),_capacity(0),_str(new char[1])
{_str[0] = '\0';
}

空字符串:大小为0,容量为0,申请的空间为1字节。最后我们在将申请的空间赋值为'\0' 就完成了空字符串的构造了。

2..字符串构造:

string(const char* str):_size(strlen(str)),_capacity(_size), _str(new char[_capacity + 1])
{//strcpy(_str, str);memcpy(_str, str, _size+1);
}

在我们的这里解析一下为什么不用 strcpy将字符串复制到类里面的成员变量,而是用memcpy 去将字符串复制到了里面的成员变量。

1.strcpy:

功能:将 source 指向的字符串(包括字符串结束符 '\0' )复制到 destination 指向的字符数组中,覆盖 destination 原有内容 ,最终返回 destination 的指针,常用于字符串的复制操作。

原型:char * strcpy ( char * destination, const char * source );

特点:

  1. 不检查目标空间大小:如果 destination 指向的缓冲区空间不足以容纳 source 字符串(包含 '\0' ),会发生缓冲区溢出,可能破坏程序其他数据、导致程序崩溃,甚至引发安全问题(如缓冲区溢出攻击 ),使用时需确保目标空间足够。
  2. 会复制字符串结束符:把 source 字符串从第一个字符开始,直到遇到的 '\0' 都复制到 destination ,保证复制后 destination 也是以 '\0' 结尾的合法 C 字符串。
  3. 源字符串需合法source 必须是有效的以 '\0' 结尾的 C 字符串,否则 strcpy 无法正确判断复制结束位置,可能导致复制过多内容,引发未定义行为。

2.memcpy:

功能:从 source 指向的内存地址开始,复制 num 个字节的数据到 destination 指向的内存地址,用于内存块的复制操作,常用来复制数组、结构体等数据 。

原型:void * memcpy ( void * destination, const void * source, size_t num );

特点:

  1. 按字节复制:不管数据类型,严格复制 num 个字节,可用于复制任意类型数据(数组、结构体等 ),但要注意类型匹配和内存布局(如结构体有填充字节时也会复制 )。
  2. 不检查内存重叠:若 source 和 destination 指向的内存区域有重叠(部分地址重合 ),可能导致复制结果异常(覆盖未复制的源数据 ),需手动确保无重叠或谨慎处理;不过 memmove 函数可处理重叠情况,内部实现做了区分。
  3. 需手动保证空间足够:不会检查 destination 指向的内存是否能容纳 num 个字节,若空间不足,会发生缓冲区溢出,破坏其他数据、引发程序崩溃等问题。

我们用memcpy 不需要担心‘ \0’的问题,它是按字节复制过去的,当我们传的字符串有‘\0’,不会因其停止。

3.综合:

对于构造   空字符和字符串  我们是不是可以综合下在一起。

string(const char* str = "")
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];//strcpy(_str, str);memcpy(_str, str,_size+1)
}

简单来说:就是我们赋空字符串,我们可以用缺省参数,加一个空字符串,这样就不需要写两个。

那么为什么选择" " 呢?而不是'\0'nullptr"\0"

其实:string 构造函数接收 const char* 时,要求指向有效 C 风格字符串(至少能解引用到 '\0' 结束符 ),nullptr 是空指针,传递它会导致未定义行为;'\0' 是单个字符(ASCII 码为 0 ),不是合法的 const char* 指针值;"\0" 虽为合法 C 风格字符串,但长度为 1(仅含结束符 ),和常规空字符串语义(长度 0 )有差异。

string(const char* str = "") 是合理写法,"" 代表空的 C 风格字符串(指向的字符数组仅含 '\0' 结束符,strlen 计算长度为 0 )。

注意:我们初始化列表的顺序不是在构造函数里面的初始化顺序,而是在成员变量里面声明的顺寻。

二 . 拷贝构造函数:

拷贝构造函数是用对象来创建一个新的对象。

string(const string& s)
{_str = new char[s._capacity + 1];//strcpy(_str, s._str);memcpy(_str,s.str,s.size);_size = s._size;_capacity = s._capacity;
}

先是为新建对象中的_str 的成员变量申请空间,再将已有的对象初始化。

三.析构函数:

析构函数是将申请的空间,不用时释放出去,防止内存泄漏。

~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}

先是delete销毁_str,然后将_str赋为空指针防止被访问,再将容量改为0。

四.迭代器:

这里的迭代器底层就是指针。用来访问。

typedef char* iterator;
typedef const char* const_iterator;iterator begin()
{return _str;
}iterator end()
{return _str + _size;
}const_iterator begin() const
{return _str;
}const_iterator end() const
{return _str + _size;
}

在这里有两种的begin与end成员函数,这是因为我们需要应对常量与非常量的数据,加const能应对常量,以及防止修改成员。

五. 容量操作:


size_t size() const
{return _size;
}
void reserve(size_t n)
{if (n > _capacity){cout << "reserve()->" << n << endl;char* tmp = new char[n + 1];//strcpy(tmp, _str);memcpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}
}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';}
}

1.为什么加const呢?那是因为不会让其改变成员函数。

2.reserve,保留,对于一个字符串大于容量,就重新申请一个适合的内存空间,然后将之前的_str复制给tmp,再将_str释放,后tmp复制给_str.

3.resize .简单来说就是重置大小,当n小于原先的就截取,将_size = n,再访问时_size 就是n而不是之前_size,如果是n大于_size,那么就申请空间再用循环把新增位置(_size 到 n-1)填充为 ch(如果没传 ch,默认填 \0 或其他默认值 )最后更新 _size = n,并在 _str[_size] 补 \0(C 风格字符串需保证结束符)。

六.修改操作:

void push_back(char ch)
{if (_size == _capacity){// 2倍扩容reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;++_size;_str[_size] = '\0';
}void append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){// 至少扩容到_size + lenreserve(_size + len);}//strcpy(_str + _size, str);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){// 至少扩容到_size + lenreserve(_size + n);}// 挪动数据/*int end = _size;while (end >= (int)pos){_str[end + n] = _str[end];--end;}*/// 添加注释最好size_t end = _size;while (end >= pos && end != npos){_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){// 至少扩容到_size + lenreserve(_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;}}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);if (ptr){return ptr - _str;}else{return npos;}}string substr(size_t pos = 0, size_t len = npos){assert(pos < _size);size_t n = len;if (len == npos || pos + len > _size){n = _size - pos;}string tmp;tmp.reserve(n);for (size_t i = pos; i < pos + n; i++){tmp += _str[i];}return tmp;}void clear(){_str[0] = '\0';_size = 0;}

1.尾插(单个字符):对于尾插来说就是要找到最后面的左标,而size便是,这是其核心逻辑。接着因为是插入,所以要检查空间,然后处理加完后的"\0".与size的改变。

2.追加:对于追加来说就是再原先的字符串后面再加字符,既然是追加我们就要计算追加的与原先的数,然后查看空间,再用memcpy拷贝进去,因为不用下标,是因为效率(对于追加很多的字符的字符串)。

3.重载+= :+=其实就是再后面加字符串,跟数字里面的+=的效果类似。其让我们用起来很舒服。

有两种请况:1.是单字符追加,我们就用尾插,2.是很多字符的字符串追加,我们用append来实现。

4.插入:简单来说,它是在你想要的位置插入。对于插入的数据我们有两种:1.多个一样的字符,2.字符串。他们的插入法也很简单,都是通过判断插入的数据和原先的数据之和查看空间够不够,接着挪移往后,为要插入的数据腾出位置。其中有个(nops)其实就是-1,对于size_t无正负的值,-1就是表达的最大数。接着用下标将数据插入进去。

5.删除:这里的删除是指定位置,指定长度的。有两种情况:1.从pos到最后面,2.pos到len。

对于1,我们就是npos了,然后就是直接将size = pos 不然他访问到并加"\0".对于2,我们就是将后面的数据覆盖前面了,他是将end = pos+len 的位置开始往前覆盖,接着size减去len了。

6.查找:对于查找我们是将找到的下标位置或地址给返回,它有两种情况:1.查找一个字符,2.查找一个字符串。他们都有找不到就返回npos。

对于1:我们便是从pos位置开始查找通过循环遍历。对于2:我们便是通过strstr函数来查找到要的字符串的开始地址,然后通过指针-指针的方式来返回地址。

7.提取:对于提取,我们可以分两种情况:1.提取pos后面的所有字符,2.提取pos到pos +len的字符。当遇到第一种情况时,我们直接用n = _size - pos 来找到长度,好申请空间进行数据的传递,再返回string类对象。当我们遇到第二种情况:那便是用n(长度)+pos来直接进行数据传递,再返回string类对象。

8.清除:那便是将开始的位置改为"\0",再将_size = 0;进行不法访问。

七.余下的重载以及c_str成员函数:

void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
string& operator=(string tmp)
{swap(tmp);return *this;
}const char* c_str() const
{return _str;
}const char& operator[](size_t pos) const
{assert(pos < _size);return _str[pos];
}
bool operator<(const string& s) const
{int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);// "hello" "hello"   false// "helloxx" "hello" false// "hello" "helloxx" truereturn 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;
}bool operator>(const string& s) const
{return !(*this <= s);
}bool operator>=(const string& s) const
{return !(*this < s);
}bool operator!=(const string& s) const
{return !(*this == s);
}ostream& operator<<(ostream& out, const string& s){/*for (size_t i = 0; i < s.size(); i++){out << s[i];}*/for (auto ch : s){out << ch;}return out;}istream& operator>>(istream& in, string& s){s.clear();char ch = in.get();// 处理前缓冲区前面的空格或者换行while (ch == ' ' || ch == '\n'){ch = in.get();}//in >> ch;char buff[128];int i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[i] = '\0';s += buff;i = 0;}//in >> ch;ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;}
};

重载 = :简单来说就是复制,我们可以用std里面的swap来实现数据的交换,将其写入再我们的swap函数,然后我们再调用,接着返回string类型的引用。

重载 []:它的目的是为了能让string类对象能像访问数组一样去访问,我们底层就直接用_str[pos]来实现。

重载< <=  ==  >  >=  !=:他的目的是为了能让我们像数学一样进行比大小,无需用字符函数。

这里的底层我们可以用memcmp来实现,再实现了==  与>之后便可以用这些重载的进行实现接下来的了。

重载 << >>(让自定义类型支持自然的输入输出语法) :输出流 输入流重载:对于为什么要放类外,那是由于它第一个参数不能是this指针且能灵活处理访问权限和代码设计 

<<

  • 遍历字符串 s 里的每个字符,依次赋值给 chauto 自动推导 ch 是 char 类型 )。
  • 每次循环通过 out << ch,把字符 ch 输出到流 out(比如 cout 的话,就是打印到控制台 )。

>>

  • 第一个参数istream& in:输入流引用(如cin),用于读取数据,返回该引用以支持链式输入(如cin >> s1 >> s2)。
  • 第二个参数string& s:目标string对象引用,用于存储读取的结果(传引用避免拷贝开销)。
  • 1. 初始化:清空目标字符串
  • 读取前先清空s的原有内容,避免旧数据干扰新输入(例如连续读取时,防止前一次的内容残留)
  • 2. 跳过前导空白符(空格 / 换行)
  • 关键工具:istream::get()函数 —— 与标准>>不同,get()会读取所有字符(包括空格、换行、制表符等),而非自动跳过空白符,因此需要手动处理前导空白。
  • 逻辑目的:模拟标准string提取运算符的行为 ——忽略输入开头的空白符(例如输入" Hello",会跳过开头的两个空格,从'H'开始读取)
  • 核心设计:局部缓冲区优化——
    若直接用s += ch逐字符拼接,string会因频繁扩容(每次追加可能触发内存重新分配)导致效率低下;而用固定大小的buff攒够字符(最多 127 个)再批量追加到s,能大幅减少string的内存分配次数,提升效率。
  • 逻辑必要性:若读取的有效字符数不足 127(例如读取"Hello"仅 5 个字符),缓冲区不会触发 “满了就追加” 的逻辑,因此循环结束后需手动将缓冲区中剩余的字符追加到s,避免数据丢失
  • 支持链式输入:例如cin >> s1 >> s2,本质是(cin >> s1) >> s2—— 第一次调用返回cin,第二次继续用cin读取s2

c_str是提取c语言的字符串形式的成员函数,直接返回成员变量里的_str的字符指针就行。

4.成员变量:

namespace xin
{class my_string{public://成员函数private://给出缺省值char* _str = nullptr;//对应指向字符串数组的指针size_t _size = 0;    //实际存储的字符串的大小size_t _capacity = 0;//开辟的总空间大小。public://const static size_t npos = -1; // 虽然可以这样用,但是不建议const static size_t npos;};const size_t string::npos = -1;
};

让代码更清晰、兼容更广泛的编译环境,避免因静态成员的声明 / 定义规则踩坑。如果用现代 C++(C++11+),直接用 inline const static 类内初始化更简洁;若需兼容旧标准,就严格分开声明和定义.

完整代码:

string类:

#pragma once
#include<iostream>
#include<assert.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
using namespace std;namespace xin
{class string{public:typedef char* iterator;typedef const char* const_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 = ""){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];//strcpy(_str, str);memcpy(_str, str, _size + 1);}string(const string& s){_str = new char[s._capacity + 1];//strcpy(_str, s._str);memcpy(_str, s._str, s._size + 1);_size = s._size;_capacity = s._capacity;}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}string& operator=(string tmp){swap(tmp);return *this;}~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{assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){cout << "reserve()->" << n << endl;char* tmp = new char[n + 1];//strcpy(tmp, _str);memcpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}}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';}}void push_back(char ch){if (_size == _capacity){// 2倍扩容reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;++_size;_str[_size] = '\0';}void append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){// 至少扩容到_size + lenreserve(_size + len);}//strcpy(_str + _size, str);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){// 至少扩容到_size + lenreserve(_size + n);}// 挪动数据/*int end = _size;while (end >= (int)pos){_str[end + n] = _str[end];--end;}*/// 添加注释最好size_t end = _size;while (end >= pos && end != npos){_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){// 至少扩容到_size + lenreserve(_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;}}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);if (ptr){return ptr - _str;}else{return npos;}}string substr(size_t pos = 0, size_t len = npos){assert(pos < _size);size_t n = len;if (len == npos || pos + len > _size){n = _size - pos;}string tmp;tmp.reserve(n);for (size_t i = pos; i < pos + n; i++){tmp += _str[i];}return tmp;}void clear(){_str[0] = '\0';_size = 0;}bool operator<(const string& s) const{int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);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;}bool operator>(const string& s) const{return !(*this <= s);}bool operator>=(const string& s) const{return !(*this < s);}bool operator!=(const string& s) const{return !(*this == s);}private:size_t _size;size_t _capacity;char* _str;public://const static size_t npos = -1; // 虽然可以这样用,但是不建议const static size_t npos;//const static double x;};const size_t string::npos = -1;//const double string::x = 1.1;ostream& operator<<(ostream& out, const string& s){/*for (size_t i = 0; i < s.size(); i++){out << s[i];}*/for (auto ch : s){out << ch;}return out;}istream& operator>>(istream& in, string& s){s.clear();char ch = in.get();// 处理前缓冲区前面的空格或者换行while (ch == ' ' || ch == '\n'){ch = in.get();}//in >> ch;char buff[128];int i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[i] = '\0';s += buff;i = 0;}//in >> ch;ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;};
};

❤️总结

相信坚持下来的你一定有了满满的收获。那么也请老铁们多多支持一下,为爱博,点点举报,偶布,是点点关注,收藏,点赞。❤️

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

相关文章:

  • 深度学习之第四课卷积神经网络CNN(一)
  • 不卡顿、不掉线!稳定可靠的体育赛事直播系统源码解析
  • 【Chrome】更新后白屏无法显示问题
  • 【力扣】面试经典150题总结04-区间/栈
  • python 自学笔记13 numpy数组规整
  • 智能驾驶机器学习知识总结
  • 越过千万生死线,鸿蒙直面商业化考验
  • ME_INFORECORD_MAINTAIN_MULTI,创建采购单信息记录,报错ME 816 系统错误(方法PROCESS_CONDITION中错误)
  • Feign 调用为服务报 `HardCodedTarget(type=xxxClient, name=xxxfile, url=http://file)`异常
  • 关于C#中运算符的简单说明
  • 为什么的中小企业很难承受“大型系统”的成本
  • 【RAGFlow代码详解-10】文本处理和查询处理
  • 深度学习(五):正则化:约束模型的复杂度
  • 什么样的 IP 能穿越周期,持续被用户买单?​
  • 深入解析交换机端口安全:Sticky MAC的工作原理与应用实践
  • 自动化测试概念与 Web 自动化实战(基于 Selenium)
  • 第一篇:MySQL安装部署全攻略
  • 计算机毕业设计 java 养老院管理系统 基于 Java 的养老院管理平台 Java 开发的养老服务系统
  • Linux云计算运维简明教程02 应用运维
  • 视频合成素材视频-多合一功能-青柠剪吧
  • 智能手机使用(2015-2019)
  • 基于MATLAB的运动模糊图像修复方法研究(LW+源码+讲解+部署)
  • vue2+elementui 表格单元格增加背景色,根据每列数据的大小 颜色依次变浅显示
  • 科研笔记:SCI论文中的功能性图表
  • 【技术教程】如何将文档编辑器集成到用 .Net 编写的网络应用程序中
  • VScode,设置自动保存
  • 支持向量机学习
  • Ubuntu22.04 安装和使用标注工具labelImg
  • GZ-CTF平台pwn题目部署
  • GitHub 热榜项目 - 日榜(2025-08-26)