【C++初阶】第八站:string类的模拟实现

Dream_Chaser~ 2024-06-28 08:35:02 阅读 51

目录

string类的模拟实现

经典的string类问题

浅拷贝

深拷贝

写时拷贝(了解)

构造函数

string的全缺省的构造函数:

string的拷贝构造函数

传统写法

现代写法

string的赋值重载函数

传统写法

现代写法

string的无参构造函数:

遍历函数

operator[ ]

迭代器

迭代器的底层实现begin和end:

范围for的使用

修改字符串相关函数

reserve 

push_back

append

operator+=

insert(字符的版本)

insert(字符串常量)

erase

resize

find(查找单个字符)

find(查找子串)

substr

比较运算符的重载:

流插入cout

流提取cin

优化写法

clear


前言:

🎯个人博客:Dream_Chaser

🎈博客专栏:C++

📚本篇内容:string类通用函数的模拟实现

string类的模拟实现

经典的string类问题

上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以下string类的实现是否有问题?

浅拷贝

说明:上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构 造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块 空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。

浅拷贝

浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共 享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为 还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。

深拷贝

如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。

写时拷贝(了解)

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。

引用计数:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。

构造函数

string的全缺省的构造函数

我们对string成员函数进行模拟实现的时候,首先要定义一个自定义的命名空间,避免与库里面的冲突:

让我们来看看错误的案例:

①注意常量字符串的用法,权限不能放大:

②尝试给_str声明加上const,这样就会导致初始化的时候出现随机值:

③需要注意的是"string.h"要定义在std的下方:

那么带参的构造可以怎么写呢?初始化列表的顺序应该对应着成员变量声明的顺序

string带参的构造是否可以再优化一下呢?

注意new的时候的+1是为了给'\0'预留的空间:

代码实现:

此构造函数接受一个可选参数(默认为空字符串),用于初始化一个字符串对象,根据传入的C风格字符串(const char* 类型)计算其长度,并据此分配内存空间,最后,将传入的字符串内容复制到新分配的内存区域,以便在C++字符串类中进行管理。

//构造函数,用于初始化一个字符串类对象

string(const char* str = "")

:_size(strlen(str))// 初始化_size成员变量,存储传入C风格字符串(char数组)str的长度

, _capacity(_size)// 初始化_capacity成员变量,初始容量与传入字符串长度相同

{

_str = new char[_capacity + 1];// 动态分配内存,为字符串对象分配一个新的字符数组

strcpy(_str, str); // 使用strcpy将传入的字符串str复制到新分配的字符数组中

}

string的拷贝构造函数

传统写法

//传统写法 -- 拷贝构造

//s2(s1)

string(const string& s)

{

_str = new char[s._capacity + 1];//仅仅开空间

strcpy(_str, s._str);//

_size = s._size;

_capacity = s._capacity;

}

现代写法

// 定义交换函数,将当前字符串对象与输入参数s的内部数据(字符数组、长度及容量)进行交换

void swap(string& s) {

// 使用STL中的std::swap交换三个关键成员变量

std::swap(_str, s._str);

std::swap(_size, s._size);

std::swap(_size, s._capacity);

}

// 构造函数,复制输入参数s的字符串内容

// 使用临时字符串对象tmp存储s的内容,然后通过调用swap函数交换数据

string(const string& s) :

_str(nullptr),

_size(0),

_capacity(0)

{

string tmp(s._str); // 创建临时字符串tmp,拷贝s的内容

swap(tmp); // 交换当前对象与tmp的数据,实现深拷贝

}

string的赋值重载函数

传统写法

//传统写法--赋值重载

// 使得可以使用 "s2 = s3" 的形式,将 s3 字符串对象的值赋给 s2

string& operator=(const string& s)

{

// 检查是否为自我赋值,即检查是否同一个对象

if (this != &s)

{

// 创建一个临时字符数组 tmp,大小为 s 的容量加一(包含结束符 '\0')

char* tmp = new char[s._capacity + 1];

// 使用 strcpy 函数将 s 的内部字符串复制到临时数组 tmp 中

strcpy(tmp, s._str);

// 删除当前对象已有的内部字符串数组

delete[] _str;

// 将临时数组 tmp 的地址赋给当前对象的内部字符串指针 _str

_str = tmp;

// 将源字符串对象 s 的大小赋给当前对象的大小属性 _size

_size = s._size;

// 同样将源字符串对象 s 的容量赋给当前对象的容量属性 _capacity

_capacity = s._capacity;

}

// 返回当前对象的引用,以支持连续赋值如 "s1 = s2 = s3"

return *this;

}

现代写法

// 重载赋值运算符,实现字符串对象之间的深拷贝赋值操作

string& operator=(const string& s)

{

// 检查是否自我赋值(即源对象与目标对象相同),避免不必要的资源释放与重新分配

if (this != &s)

{

// 创建临时字符串对象tmp,并使用s的内容初始化

string tmp(s);

// 调用自定义的swap函数,将当前对象的数据与临时对象tmp的数据进行交换

// 这样可以确保原对象的资源被正确释放,并且新内容被赋值给当前对象

swap(tmp);

// 注:这里原本可能是 "this->swap(tmp);",但因为 "swap" 是成员函数,

// 在成员函数内部可以直接调用,无需 "this->"。

}

// 返回当前对象的引用,以便支持连续赋值操作

return *this;

}

现代写法优化

//现代写法优化

string& operator=(string tmp)

{

swap(tmp);

return *this;

}

string的无参构造函数:

经过尝试之后,可以发现以下问题:

无参的构造应该这样写:

代码实现: 建议只写一个全缺省的构造函数即可,因为默认构造函数只能存在一个。

String()

:_str(new char[1]{'\0'})//数组的初始化方式

,_size(0)

,_capacity(0)

{}

遍历函数

c_str函数

简要说明

const char* c_str()const

{

return _str;

}

operator[ ]

这里要演示的话就得遍历这个字符串,需要重载一下operator[ ]:

void test_string1()

{

string s1("hello world");

cout << s1.c_str() << endl;

string s2;

cout << s2.c_str() << endl;

for (size_t i = 0; i < s1.size(); i++)

{

cout << s1[i] << " ";

}

cout << endl;

}

需要区分只能读和可读可写的版本:

//可读可写

char& operator[](size_t pos)

{

assert(pos < _size);

return _str[pos];

}

//只能读

const char& operator[](size_t pos)const

{

assert(pos < _size);

return _str[pos];

}

代码执行: 

既然说到遍历数组的话,那就少不了迭代器。

迭代器

迭代器声明: 

//声明

typedef char* iterator;//可读可写

typedef const char* const_iterator;//只可读

迭代器的底层实现begin和end:

begin:函数的作用就是返回字符串中第一个字符的地址

//实现:

iterator begin()

{

return _str;

}

const_iterator begin()const

{

return _str;

}

end函数的作用就是返回字符串中最后一个字符的后一个字符的地址(即’\0’的地址)

//实现:

iterator end()

{

return _str + _size;//指向'\0'

}

const_iterator end()const

{

return _str + _size;//指向'\0'

}

迭代器外层调用:

范围for的使用

同时为了方便理解,把范围for的实现列举出来:

当咱们将begin改为Begin时,发现会出错,还有一个点:范围for的本质是迭代器

迭代器与范围for的应用: 

怎么能够让改变后的字符串,不再变回原来的字符串呢?使用引用即可:

实例代码:

void test_string1()

{

string s1("hello world");

cout << s1.c_str() << endl;

string s2;

cout << s2.c_str() << endl;

/*for (size_t i = 0; i < s1.size(); i++)

{

cout << s1[i] << " ";

}

cout << endl;*/

string::iterator it = s1.begin();

while (it != s1.end())

{

(*it)++;

cout << *it << " ";

++it;

}

cout << endl;

for (auto& ch : s1)//使用引用

{

ch++;//让字符串里每个字符的ascll码值+1

cout << ch << " ";

}

cout << endl;

cout << s1.c_str() << endl;

}

修改字符串相关函数

reserve 

        其主要作用是预先为容器分配足够的未使用容量,以应对未来可能的元素添加,而不立即改变容器的大小(元素数量)。使用 reserve() 可以有策略地控制容器的内存分配,提高连续添加元素时的效率

void reserve(size_t n)

{

// 检查请求的容量n是否大于当前内部缓冲区的实际容量(_capacity)

if (n > _capacity)

{

// 如果是,则需要重新分配更大的内存空间。新分配的内存大小为n+1,+1用于存储结尾的空字符'\0

char* tmp = new char[n + 1];//+1存“\0'

strcpy(tmp,_str);//将当前字符串内容(包括结尾的'\0')复制到新分配的tmp缓冲区中

delete[] _str;// 释放原有的内部缓冲区(_str),以避免内存泄漏

_str = tmp;// 更新内部缓冲区指针,指向新分配的tmp缓冲区

_capacity = n;// 更新内部记录的缓冲区容量为请求的值n

}

}

push_back

  push_back() 是序列容器(如 std::vectorstd::liststd::deque 等)的成员函数,用于在容器末尾添加新元素。它自动处理内存管理,确保新元素顺利加入,且保持原有元素顺序不变。对于动态扩容容器(如 std::vector),在必要时会自动增大容量。 

尝试:可以写成这样吗?不行,当_capacity = 0 时,空间还没分配到,此时如果访问就属于越界访问

代码实现:

void push_back(char ch)

{

// 检查当前内部字符串长度(_size)是否已达到内部缓冲区容量(_capacity)

if (_size == _capacity)

{

// 若已满,调用reserve函数预分配新的内存,扩大容量。

// 首次扩容时容量设为4,后续扩容按当前容量翻倍。

reserve(_capacity == 0 ? 4 : 2 * _capacity);

}

// 将待添加字符ch 存储在 内部字符串缓冲区的当前长度索引处(_size)

_str[_size] = ch;

++_size;// 更新内部字符串长度(_size),增1以包含新添加的字符

_str[_size] = '\0';// 在新添加字符之后添加空字符('\0'),确保字符串正确终止

}

append

对于std::string:append()用于在字符串末尾追加其他字符串、字符数组或单个字符,可以指定追加的起始位置和长度。

void append(const char* str)

{

int len = strlen(str);// 获取输入字符串str长度

// 检查是否需扩大内部缓冲区容量以容纳追加的str

if (_size + len > _capacity)

{

reserve(_size + len);

}

//这个地方要是忘记 +_size,从头到尾覆盖新字符串

strcpy(_str+_size,str);// 将str追加到内部字符串缓冲区末尾

_size += len;// 更新内部字符串长度

}

push_back 和 append 的区别在于一个追加字符,另一个追加字符串

示例:

operator+=

+=运算符的重载:分别复用了 push_back  和  append, 实现字符串与字符,字符串与字符串之间能够直接使用+=运算符进行尾插。

字符串追加字符:

string& operator+=(char ch)

{

push_back(ch);

return *this;

}

字符串追加字符串:

string& operator+=(const char* str)

{

append(str);

return *this;

}

示例运行:

void test_string2 ()

{

string s1("hello world");

s1 += '#';

s1 += "css";

cout << s1.c_str() << endl;

string s2;

s2 += '#';

s2 += "csgo";

cout << s2.c_str() << endl;

}

insert(字符的版本)

尝试写一下insert:

测试一下代码的可行性:

 可以发现头插会出错:

头插:

那就换成有符号,此时pos为0,end为-1,此时end<pos,但是依然进入了循环,明显的不对吧,这里是发生了类型转换,end变成无符号整型了。

怎么解决呢:以下两种写法都是正确的:

此时函数的功能:

在动态字符数组 _str 的指定位置 pos 插入一个字符 ch。首先,它验证插入位置的有效性(必须小于等于当前数组大小_size)。

若当前数组容量已满,函数会自动扩容,初始容量为4或者当前容量的两倍。

然后,函数通过循环将 pos位置之后的所有元素向右移动一位,为新字符腾出空间。

最后,函数在指定位置pos插入字符ch,并将数组大小_size加一,表示数组元素数量增加。 

//上图的版本二

void insert(size_t pos, char ch)

{

assert(pos <= _size);

if (_size == _capacity)

{

reserve(_capacity == 0 ? 4 : 2 * _capacity);

}

size_t end = _size + 1;

while (end > pos)

{

_str[end] = _str[end - 1];

--end;

}

_str[pos] = ch;

_size++;

}

insert(字符串常量)

函数的作用:

        用于在动态字符数组(或类似字符串结构)的指定位置pos插入一个C风格字符串str。首先,它通过断言确保插入位置的有效性。

        接着计算待插入字符串的长度,并检查现有容量是否足够容纳插入后的完整字符串,不足则调用reserve进行扩容。

        然后,函数将插入位置之后的所有字符向右移动适当距离,为新字符串腾出空间。最后,使用strncpy函数将待插入字符串复制到目标位置,并更新整个字符串的大小,反映出新增字符的影响。

首先要注意:

所以需要强转:

// pos:插入位置,从0开始计数

// str:要插入的C风格字符串

void insert(size_t pos, const char* str)

{

// 断言检查,确保插入位置pos不大于当前字符串的大小

assert(pos <= _size);

// 获取要插入字符串str的长度,即需要移动的字符数量

size_t len = strlen(str);

// 检查插入后字符串总长度是否超过当前容量,如果超过则进行扩容

if (_size + len > _capacity)

{

reserve(_size + len); // 调用reserve函数来增加内部缓冲区的容量

}

// 数据挪动部分:

// 将插入位置pos之后的所有字符向右移动len个位置

int end = static_cast<int>(_size); // 使用int类型方便进行减操作,注意这里假设_size不会超出int表示范围

while (end >= static_cast<int>(pos))

{

_str[end + len] = _str[end]; // 将原字符串中下标为end的字符移动到end + len的位置

--end; // 移动指针至下一个待移动的字符

}

// 在指定位置pos处插入字符串str

strncpy(_str + pos, str, len); // 使用 strncpy 函数将str复制到目标字符串相应位置

// 更新字符串的大小

_size += len; // 插入操作完成后,字符串的新长度应增加len

}

erase

        作用是从动态字符数组(或类字符串结构)的特定位置pos开始删除指定长度len的子串。

        首先,它通过断言确保删除起始位置的有效性(小于当前字符串长度_size)。根据传入的参数,函数会判断是否需要删除从pos到结尾的所有字符,

        如果是,则将pos位置之后的字符置为结束符\0,并将字符串大小更新为pos;否则,将从pos+len开始到结尾的字符依次向前移动len个位置以覆盖待删除区域,并相应减少字符串大小_size

        简而言之,该函数实现了对字符串的指定范围擦除操作。

// 函数:删除指定位置起始的子串,若未指定长度,默认删除到字符串末尾

void erase(size_t pos, size_t len = npos)

{

// 检查删除起始位置是否合法(必须在当前字符串内)

assert(pos < _size);

// 如果未指定长度或指定长度使删除范围超出字符串末尾,则删除从pos到字符串末尾的部分

if (len == npos || pos+len >= _size)

{

// 截断字符串,将pos位置置空字符,字符串长度变为pos

_str[pos] = '\0';

_size = pos;

}

// 否则,按指定长度删除子串

else

{

// 计算删除范围结束后的新起始位置

size_t begin = pos + len;

// 将删除范围后的字符逐个向前移动len位,覆盖待删除部分

while (start <= _size)

{

_str[begin - len] = _str[begin];

++begin;

}

// 更新字符串长度,减少len

_size -= len;

}

}

resize

作用: 改变动态字符数组(或类字符串结构)的大小,并可选地用指定字符ch填充新添加的空间。

        若新指定长度n小于等于当前长度,函数将截断字符串,使其长度变为n

        若新长度大于当前长度且可能超过当前容量时,函数先调用reserve进行扩容,然后使用给定字符ch填充新增的字符位置,直到字符串长度达到指定的n,并在字符串末尾添加结束符\0,以确保字符串的完整性和正确性。

        总之,此函数灵活地调整了字符串的长度,并保持其内容的有效性。

void resize(size_t n, char ch = '\0')

{

// 当新长度n小于等于当前字符串长度时,执行删除操作

if (n <= _size)

{

// 将第n个字符设置为空字符,相当于截断字符串,并更新字符串的实际长度为n

_str[n] = '\0';

_size = n;

}

// 当新长度n大于当前字符串长度但小于等于当前容量时,或新长度n大于当前容量时

else

{

// 先调用reserve函数确保容量足够容纳新长度的字符串

reserve(n);

// 循环填充字符,直至字符串长度达到n

while (_size < n)

{

_str[_size] = ch; // 使用指定字符ch填充

++_size;

}

// 最后,在新字符串末尾添加结束符'\0'

_str[_size] = '\0';

}

}

find(查找单个字符)

// 定义一个成员方法,用于在当前字符串对象中查找指定字符 ch 的首次出现位置

//ch -- 要查找的字符

// 查找的起始位置,默认从字符串开头(索引0)开始

size_t find(char ch, size_t pos = 0)

{

// 使用一个循环遍历从 pos 位置开始到字符串结尾的所有字符

for (size_t i = pos; i < _size; i++)

{

// 检查当前遍历到的字符是否与要查找的字符 ch 相同

if (_str[i] == ch)

{

// 如果相同,则返回该字符在字符串中的索引位置

return i;

}

}

// 若遍历完整个指定区间后仍未找到字符 ch,则返回 npos

// npos 是一个特殊的值,通常表示“未找到”或“无效位置”

return npos;

}

find(查找子串)

size_t find(const char* sub, size_t pos = 0)

{

// 使用C语言库函数strstr查找子串sub在当前字符串(从pos开始的部分)中首次出现的位置

const char* p = strstr(_str + pos, sub);

// 如果找到了子串sub,则返回子串首字符在当前字符串中的相对索引(即下标)

if (p)

{

return p - _str;

}

// 若找不到子串,则返回特殊值npos,表示子串不在当前字符串中

else

{

return npos;

}

}

substr

// 定义一个方法,用于从原始字符串中提取子串

string substr(size_t pos, size_t len /* 默认为最大长度 */) {

// 创建一个新字符串 s 来保存子串

string s;

// 计算子串的结束位置(默认取到原始字符串结尾)

size_t end = pos + len;

if (len == npos || pos + len >= _size) {

// 若指定长度超过原始字符串剩余长度,则取剩余全部字符

len = _size - pos;

end = _size;

}

// 为新字符串预分配足够内存以存放子串

s.reserve(len);

// 循环遍历原始字符串,从 pos 位置开始,复制到新字符串 s 中,直到结束位置 end

for (size_t i = pos; i < end; i++) {

s += _str[i];

}

// 返回包含子串的新字符串 s

return s;

}

比较运算符的重载:

bool operator<(const string& s)const

{

return strcmp(_str,s._str);

}

bool operator==(const string& s)const

{

return strcmp(_str, s._str)==0;

}

//<= 复用<

bool operator<=(const string& s)const

{

return *this < s || *this == s;

}

//>复用 !(<=)

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

}

流插入cout

这里的范围for可以访问吗?不可以:这里要用const迭代器,因为是一个被const修饰的对象,const迭代器指针本身是可以++(修改)的,指针指向的内容不可以被修改:

所以要这样做:

代码:

class string

{

public:

typedef char* iterator;

typedef const char* const_iterator;//const迭代器

}

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;

}

流提取cin

需要注意的地方:

那么应该这样去写:

代码: 

istream& operator>>(istream& in, string& s)

{

s.clear();//清理原来的字符数据,不然就变成尾插了

char ch;

//in >> ch;//拿不到 空格 或者 换行

ch = in.get();//一个字符一个字符的拿

while (ch != ' ' && ch != '\n')

{

s += ch;

//in >> ch;

ch = in.get();

}

return in;

}

优化写法

// 重载输入流提取运算符 >>,使得可以从输入流(如cin)中读取一串连续的非空格和非换行符字符到string对象s中

istream& operator>>(istream& in, string& s)

{

// 清空目标string对象s,准备接收新的输入

s.clear();

// 定义一个固定大小的缓冲区buff,用于暂存从输入流中读取的字符

char buff[129];

size_t i = 0; // 初始化缓冲区索引为0

// 从输入流in中获取第一个字符

char ch;

ch = in.get();

// 当读取到的字符不是空格也不是换行符时,继续循环

while (ch != ' ' && ch != '\n')

{

// 将字符放入缓冲区

buff[i++] = ch;

// 如果缓冲区已满(达到128个字符)

if (i == 128)

{

// 在缓冲区末尾添加结束符'\0',将其视为一个C风格字符串添加到目标string对象s中

buff[i] = '\0';

s += buff; // 更新s的内容

// 重置缓冲区索引为0,以便继续填充下一个子字符串

i = 0;

}

// 继续从输入流中获取下一个字符

ch = in.get();

}

// 若缓冲区中仍有剩余字符(循环结束后)

if (i != 0)

{

// 添加结束符'\0',将剩余的子字符串添加到目标string对象s中

buff[i] = '\0';

s += buff;

}

// 返回输入流in的引用,以支持链式输入操作

return in;

}

clear

不清理之前的数据就变成尾插了:

void clear()

{

_str[0] = '\0';

_size = 0;

}

本篇结束。

🔧本文修改次数:0

🧭更新时间:2024年4 月 24 日 



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。