😎【博客主页:你最爱的小傻瓜】😎
🤔【**本文内容:**C++ string类😍】🤔
字符串若璀璨织锦,assign 是重新勾勒的经纬,insert 是悄然添彩的绣线。在重置与嵌入间穿梭,领略代码里藏着的,关于重塑与点缀的智慧。下面就开启精彩剖析,带各位实现 string 的奇妙工坊,认真瞧,别掉队呀!
上文链接:string类的使用
1.前言:
我们接下来要实现的string类主要是 其构造 拷贝构造 赋值运算符重载 析构函数。
2.模拟实现string类的准备:
为了避免自己实现的string类 与 库中的string类 重名,则我们需要用一个命名空间域。
在我们之前所了解过的数据结构里,动态顺序表中的结构体的变量,其实与string类里面的成员变量类似。
其文链接:顺序表
cpp
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 类对象,即空字符串:
cpp
string()
:_size(0)
,_capacity(0)
,_str(new char[1])
{
_str[0] = '\0';
}
空字符串:大小为0,容量为0,申请的空间为1字节。最后我们在将申请的空间赋值为'\0' 就完成了空字符串的构造了。
2..字符串构造:
cpp
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.综合:
对于构造 空字符和字符串 我们是不是可以综合下在一起。
cpp
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 )。
注意:我们初始化列表的顺序不是在构造函数里面的初始化顺序,而是在成员变量里面声明的顺寻。
二 . 拷贝构造函数:
拷贝构造函数是用对象来创建一个新的对象。
cpp
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 的成员变量申请空间,再将已有的对象初始化。
三.析构函数:
析构函数是将申请的空间,不用时释放出去,防止内存泄漏。
cpp
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
先是delete销毁_str,然后将_str赋为空指针防止被访问,再将容量改为0。
四.迭代器:
这里的迭代器底层就是指针。用来访问。
cpp
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能应对常量,以及防止修改成员。
五. 容量操作:
cpp
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 风格字符串需保证结束符)。
六.修改操作:
cpp
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 + len
reserve(_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 + len
reserve(_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 + len
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;
}
}
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成员函数:
cpp
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" true
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);
}
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
的原有内容,避免旧数据干扰新输入(例如连续读取时,防止前一次的内容残留)
- 跳过前导空白符(空格 / 换行)
- 关键工具:
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.成员变量:
cpp
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类:
cpp
#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 + len
reserve(_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 + len
reserve(_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 + len
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;
}
}
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;
};
};
❤️总结
相信坚持下来的你一定有了满满的收获。那么也请老铁们多多支持一下,为爱博,点点举报,偶布,是点点关注,收藏,点赞。❤️
