今日分享: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 );
特点:
- 不检查目标空间大小:如果
destination
指向的缓冲区空间不足以容纳source
字符串(包含'\0'
),会发生缓冲区溢出,可能破坏程序其他数据、导致程序崩溃,甚至引发安全问题(如缓冲区溢出攻击 ),使用时需确保目标空间足够。- 会复制字符串结束符:把
source
字符串从第一个字符开始,直到遇到的'\0'
都复制到destination
,保证复制后destination
也是以'\0'
结尾的合法 C 字符串。- 源字符串需合法:
source
必须是有效的以'\0'
结尾的 C 字符串,否则strcpy
无法正确判断复制结束位置,可能导致复制过多内容,引发未定义行为。2.memcpy:
功能:从
source
指向的内存地址开始,复制num
个字节的数据到destination
指向的内存地址,用于内存块的复制操作,常用来复制数组、结构体等数据 。原型:void * memcpy ( void * destination, const void * source, size_t num );
特点:
- 按字节复制:不管数据类型,严格复制
num
个字节,可用于复制任意类型数据(数组、结构体等 ),但要注意类型匹配和内存布局(如结构体有填充字节时也会复制 )。- 不检查内存重叠:若
source
和destination
指向的内存区域有重叠(部分地址重合 ),可能导致复制结果异常(覆盖未复制的源数据 ),需手动确保无重叠或谨慎处理;不过memmove
函数可处理重叠情况,内部实现做了区分。- 需手动保证空间足够:不会检查
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
里的每个字符,依次赋值给ch
(auto
自动推导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;};
};
❤️总结
相信坚持下来的你一定有了满满的收获。那么也请老铁们多多支持一下,为爱博,点点举报,偶布,是点点关注,收藏,点赞。❤️