当前位置:首页 » 《关注互联网》 » 正文

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

2 人参与  2024年04月26日 15:43  分类 : 《关注互联网》  评论

点击全文阅读


目录

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 字符串对象的值赋给 s2string& 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码值+1cout << 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长度// 检查是否需扩大内部缓冲区容量以容纳追加的strif (_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 日 


点击全文阅读


本文链接:http://zhangshiyu.com/post/100637.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1