欢迎光临
我们一直在努力

【C++STL:string(下)】手把手教你:自己实现一个简易的string类

前言

前面几篇博客向大家介绍了string类的一些常用接口,包括:

1.Member functions(成员函数)

2.Iterators(迭代器)

3.Capacity(容量相关接口)

4.Element access(元素访问)

5.Modifiers(修改字符串内容的操作)

6.String operations(字符串运算)

前文基础链接:

【C++STL:string(上)】:手把手教你使用官方文档高效学习string,并熟练使用string的常用接口-CSDN博客

【C++STL:string(中)】string进阶指南:增删改查、截取查找、字符定位全搞定-CSDN博客

本文会按照前面给大家介绍接口的顺序手把手带大家自己实现一个类std的简易string类!

博主实现string使用了多文件编程,具体来说:

头文件(.h):存放类的声明、函数原型、宏定义等,供其他文件包含。实现文件(.cpp):存放类成员函数和全局函数的定义。测试文件(Test.cpp):包含 main 函数,用于测试和调用实现的功能。

目录

前言

(一)string类、测试接口的实现

(二)构造函数、拷贝构造、赋值运算符重载和析构函数的实现

1.构造函数

2.拷贝构造

strncpy和strcpy的区别

3.赋值运算符重载

4.析构函数

(三)迭代器相关:begin/end的实现

(四)元素访问,修改字符串(增删查改)的实现

1.operator [ ]

2.reserve

3.push_back

4.append

5.insert

6.erase

7.find

(五)operator<< / operator>>

1.operator<<

函数声明

2.operator>>

(六)swap

小结


(一)string类、测试接口的实现

类内有一些成员函数和三个私有成员变量 char* _str、size_t _size、size_t _capacity,还有一个静态成员zie_t npos。

class string
{
public:
size_t size()
{
return _size;
}
const char* c_str() const {
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos = -1;
};

因为还没有重载<<,所以在Test.cpp中测试时,不能直接cout一个string对象,需要先实现一个简易的c_str返回 const char*,而cout<< const char*; 编译器会自动识别const char*,然后按照字符串输出。

(二)构造函数、拷贝构造、赋值运算符重载和析构函数的实现

1.构造函数

函数声明:

string(const char* str = "");

函数定义:

string::string(const char* str)
:_size(strlen(str))
{
_str = new char[_size + 1];//_size+1是因为要预留出'\\0'的大小
_capacity = _size;
strcpy(_str, str);
}

常规写法是在初始化列表把类的三个成员变量全部用strlen初始化一遍,但strlen的时间复杂度是O(n ),所以按照上述写法可以在仅使用一次strlen的同时,也把另外两个成员变量也给定义了。这里涉及到初始化列表的知识,关于初始化列表和函数体的执行顺序,可以看博主以前发过的文章,里面有详细介绍。

2.拷贝构造

函数声明:

string(const string& s);

函数定义:

string::string(const string& s)
{
_str = new char[s._size + 1];
strncpy(_str, s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}

拷贝构造要注意的点是:要为需要构造的对象开辟一个空间,再把被拷贝的对象给拷贝过去,也就是以前说过的深拷贝,使_str指向两个空间,保护空间不被多次释放。

这里使用strncpy是为了兼容一些串内有空格或' \\0 '的场景。

strncpy和strcpy的区别

函数复制终止条件目标字符串结束符安全性典型用途
strcpy(dest, src) 复制直到遇到源字符串的 '\\0'(包括 '\\0') 自动以 '\\0' 结尾 不安全,可能超出 dest 容量 已知源长度且目标足够大时的快速复制
strncpy(dest, src, n) 最多复制 n 个字符;若源长度 < n,则剩余位置填充 '\\0' 若源长度 ≥ n,则 不会 自动添加 '\\0' 较安全,但需手动处理结束符 固定长度字段复制、防止溢出但有坑

3.赋值运算符重载

函数声明

string& operator=(const string& s);

函数定义

string& string::operator=(const string& s)
{
if(this!=&s)
{
char* temp = new char[s._size + 1];
strncpy(temp, s._str, s._size + 1);
delete[] _str;
_size = s._size;
_capacity = s._capacity;
_str = temp;
}
return *this;
}

整体大逻辑和拷贝构造相似,使用深拷贝,申请一个s._size+1 大小的空间,然后再拷贝,这里要说明的是:if语句是为了防止有的人可能有自己给自己赋值的需要,如果使用原逻辑直接开辟新空间,会降低运行效率,所以 if (this!=&s) 直接 return *this。

4.析构函数

函数声明和定义

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

常规写法:释放空间->指针置空->_size/_capacity置空。

(三)迭代器相关:begin/end的实现

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;
}

上文介绍过:迭代器就是一个类似于指针的东西,只有底层结构是数组的容器,才能直接用指针实现迭代器。const_iterator是const对象的迭代器返回值。

(四)元素访问,修改字符串(增删查改)的实现

1.operator [ ]

函数声明和定义

char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}

与begin/end一样要提供const对象 和 非const对象两个接口,可以像数组一样按照下标操作字符串,大大提高便利性与可读性。

2.reserve

函数声明

void reserve(size_t n);

函数定义

void string::reserve(size_t n)
{
assert(n >_capacity);
char* temp = new char[n + 1];
/*strcpy(temp, _str);*/
strncpy(temp, _str, _size + 1);
delete[] _str;
_str = temp;
_capacity = n;
}

逻辑:如果需要开的空间大小小于当前_capacity,直接终止程序,n>_capacity 时直接申请一块需要的空间再把原空间里的数据平移过去。

3.push_back

函数声明

void push_back(char ch);

函数定义

void string::push_back(char ch)
{
if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\\0';
}

逻辑:先判断空间大小够不够再插入一个字符,如果不够需要二倍扩容,另外要注意的是如果当前字符串里面没有任何字符,是刚被构造的情况,此时_capacity=0,二倍扩容还是0,这里需要特殊处理。

4.append

函数声明

void append(const char* str);

函数定义

void string::append(const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(std::max(len + _capacity, 2 * _capacity));
}
//strcpy(_str + _size, str);
strncpy(_str + _size, str, len + 1);
_size += len;
}

逻辑:

1.计算待追加字符串的长度

size_t len = strlen(str);

获得 str 的字符个数(不包含结尾的 '\\0')。

2.检查是否需要扩容

if (len + _size > _capacity)

如果当前容量不足以容纳追加后的字符串(_size + len 个字符,注意不包含结束符),则调用 reserve 扩容。

3.计算新容量并扩容

reserve(std::max(len + _capacity, 2 * _capacity));

新容量取 len + _capacity 和 2 * _capacity 中的较大值。这样做的目的是:既满足追加所需的最小容量(_size + len),又预留一些空间(翻倍)以减少频繁扩容。

4.复制字符串内容

复制 len + 1 个字符,即包括 str 的所有字符及其结尾的 '\\0'。这样能保证追加后当前字符串依然是 '\\0' 结尾的 C 字符串。(注释掉的 strcpy 也能工作,但 strncpy 更安全,因为指定了最大长度。)

5.更新字符串长度

_size += len;  注意:_size 不包含结尾的 '\\0',所以只需增加有效字符数。

5.insert

函数声明

void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);

函数定义

void string::insert(size_t pos, char ch)
{
if (_capacity == _size)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = size() + 1;
while (end > pos)
{
_str[end] = _str[end – 1];
–end;
}
_str[pos] = ch;
++_size;
}
void string::insert(size_t pos, const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(std::max(len + _capacity, 2 * _capacity));
}
size_t end = size() + len;
while (end >= pos + len)
{
_str[end] = _str[end – len];
–end;
}
strncpy(_str + pos, str, len);//不需要插入str的'\\0'
_size += len;
}

扩容逻辑和push_back一个ch、append一个字符串类似,要注意的是观察边界问题,避免越界。

6.erase

函数声明

string& erase(size_t pos = 0, size_t len = npos);

函数定义

string& string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len == npos || len >= _size-pos)
{
_str[pos] = '\\0';
_size = pos;
}
else {
strncpy(_str + pos, _str + pos + len, _size – len – pos + 1);
_size -= len;
}
}

逻辑:先判断删除的pos是否合法,再检查pos+len是否超过 _size,如果超过,则直接在pos处赋值上一个' \\0',同时修改_size的大小,如果没有超过,则把后面不被删除的字符串拷贝到要删除的字符串上。

7.find

函数声明

size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);

函数定义

size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos;i < _size;i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr)
{
return ptr – str;
}
else
{
return npos;
}
}

1.查找字符:从pos位置遍历字符串,一旦找到ch就返回下标,没有找到就返回npos。

2.查找字符串:这里博主偷个懒,用C标准库里的strstr直接查找,strstr是专门用于处理 以 '\\0' 结尾的 C 字符串。如果主串或子串中间包含 '\\0',strstr 会将其误判为字符串结尾,导致无法匹配到 '\\0' 之后的内容。大家可以自行完善~

(五)operator<< / operator>>

1.operator<<

注意:要在类外进行函数声明,因为<<的左操作数是ostream类的对象,右操作数才是string类的对象。类内声明成员函数默认的第一个参数就是this(也就是string对象),这样写会改变期望的书写顺序。

函数声明

std::ostream& operator<<(std::ostream& out, const string& s);

函数定义

std::ostream& operator<<(std::ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}

为什么string类需要重载<< ?

cout << s:std::string 重载了 operator<<,可以直接输出字符串内容。它会输出 s 中存储的所有字符(即使包含 '\\0' 也会输出,但 '\\0' 通常不可见,且 std::string 的 operator<< 会按长度输出,不受 '\\0' 影响)。

cout << s.c_str():如果 std::string 内部包含 '\\0',c_str() 返回的指针指向第一个 '\\0' 之前的部分,后面的内容不会被输出。

2.operator>>

函数声明

std::istream& operator>>(std::istream& in,string& s);

函数定义

std::istream& operator>>(std::istream& in,string& s)
{
s.clear();
char ch;
//in>>ch; 错误写法,下文会解释
in.get(ch);
char buff[256];
int i = 0;
while (ch != ' ' && ch != '\\n')
{
buff[i] = ch;
i++;
if (i == 255)
{
buff[i] = '\\0';
s += buff;
i = 0;
}
in.get(ch);
}
if (i > 0)
{
buff[i] = '\\0';
s += buff;
}
return in;
}

>>重载和<<重载不同,标准库里的流插入和scanf无论输入任何类型的值,空格和换行都被认为是多个值之间的分隔,读取到空格会自动跳过。

默认情况下,operator>> 读取 char 时会跳过空白字符(空格、换行、制表符等)。这意味着:永远读不到 ' ' 或 '\\n',因为 >> 会自动忽略它们。所以循环条件中的 ch != ' ' && ch != '\\n' 永远不会因为遇到空格或换行为假,循环会一直读取直到文件结束或出错。

in >> ch 使用格式化输入,它默认会跳过空白字符(空格、换行、制表符等),以便读取下一个非空白字符。这是为了便于读取像整数、单词等由非空白分隔的数据。

而 in.get(ch) 是非格式化输入,它直接读取输入流中的下一个字符(无论它是不是空白),并且不进行任何跳过。所以它能捕获空格、换行符等原始字符,非常适合需要逐字符精确处理(如实现 getline 或自定义 operator>> 不跳过空白)的场景。

另外可以设置一个缓冲区,每次直接 s += ch 可能引起多次内存重新分配。用缓冲区批量追加(每 255 个字符追加一次)可以减少扩容次数。

(六)swap

三个swap函数对比:

如果使用标准库的swap代价太大,拷贝构造一次、赋值两次,需要重新开始三次空间且要把数据平移三次,如果使用通用的深拷贝方法,除了原本就有的字符串 s1和 s2 的内存,还需要创建临时字符串的内存,导致总共需要管理 3 份完整的数据拷贝。对于大字符串而言,内存分配和复制的成本会非常高。

void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}

小结

手写 string 搞定!我们主要干了这几件事:

构造、拷贝、赋值、析构:自己管内存,深拷贝保平安,避免重复释放。

迭代器和 []:直接用指针当迭代器,const 和非 const 版本都安排上。

扩容:reserve 按需扩容,push_back、append、insert 自动翻倍或算着扩。

增删改查:erase 挪动字符,find 偷懒用了 strstr(虽然遇到 '\\0' 会跪)。

输入输出:<< 直接遍历输出,>> 用缓冲区批量读单词,记得用 get() 而不是 >> 读字符,不然空格就没了。

swap:直接交换内部指针和大小,别用标准库那个笨方法(三次深拷贝亏死了)。

虽然比不上标准库那么猛(没搞移动语义、小字符串优化、UTF-8 支持),但这一套下来,你对 string 的底层已经门儿清了。下次面试问“string 怎么实现”,直接上手写~

继续肝,下一个目标:vector!

赞(0)
未经允许不得转载:171主机测评 » 【C++STL:string(下)】手把手教你:自己实现一个简易的string类
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址