【C++】string的模拟实现

J.呵呵 2024-09-13 17:35:01 阅读 84

目录

前言总体结构默认成员函数构造函数析构函数拷贝构造赋值重载

string类对象的访问遍历操作_size和operator[]迭代器

string类对象的修改操作reservepush_back、append和operator+=insert和erasefindswapsubstr大小比较

输入输出流插入重载流提取重载

全部代码string.hstring.cpptest.cpp

请添加图片描述

前言

前面我们已经学习了解了string重要接口的使用:【C++】string的使用。

对于C++,这是偏向底层的语言,所以我们来模拟实现一下。

虽然string本质上是一个模版类,但是我们在使用时更多的只是使用basic_string< char>,因此我们模拟实现就简单实现一个string就好了。

总体结构

由于string在标准库中已经定义好了,因此我们在模拟实现时<code>使用命名空间进行隔离,防止命名冲突。

namespace bit//使用命名空间与标准库中的string进行隔离

{ -- -->

class string

{

private:

char* _str; //指向字符串的指针

size_t _size; //大小

size_t _capacity; //容量

const static size_t npos ;//静态成员变量在类里面声明,在类外初始化

//类里面的静态成员变量就相当于全局变量

};

//const size_t string::npos = -1;错误

//error LNK1169: 找到一个或多个多重定义的符号

//因为此时在string.cpp和test.cpp中都包含了,也就是它被定义了两次,一链接就会报错。

}

//string.cpp

const size_t string::npos = -1;

//此时应该在string.cpp中定义

但是对于const修饰的静态成员变量,对于整型可以直接设置初始值

private:

const static size_t npos = -1;

但是只有整型家族可以使用,其余的像double的则不行。

我们在模拟实现时,同样是进行声明和定义分离。因此我们用三个文件:string.hstring.cpptest.cpp

在实现各种函数前,同样我们先把能打印出来的函数给写了,即c_str,也比较简单,返回指针即可:

const char* string::c_str() const

{

return _str;

}

默认成员函数

构造函数

对于构造函数,分为无参和带参的我们建议将两者合为一起,写成一个全缺省的。

//string.h文件

string(const char* str = " ");

//string.cpp文件

string::string(const char* str)

:_size(strlen(str))

{

_str = new char[_size + 1]; //多开一个给'\0'存放

_capacity = _size;

strcpy(_str, str);

}

缺省值要放在函数声明的地方。

我们用初始化列表用strlen给大小进行初始化,后面直接用大小初始化剩余的值。

析构函数

string::~string()

{

delete[] _str;

_str = nullptr;

_size = _capacity = 0;

}

注意:要使用delete[]。

拷贝构造

string类涉及到空间的申请和销毁,应当使用深拷贝来拷贝构造,否则原对象s1和拷贝后的对象s2都指向同一块空间,在析构时会崩溃。

//s2(s1),s1就是s,s2就是*this

string::string(const string& s)

{

_str = new char[s._capacity + 1];

strcpy(_str, s._str);

_size = s._size;

_capacity = s._capacity;

}

赋值重载

对于赋值重载也同样有空间的问题,也要进行深拷贝

在这里插入图片描述

<code>string& string::operator=(const string& s)

{ -- -->

if (this != &s)//避免自己给自己赋值,减少消耗

{

char* p = new char[s._capacity + 1];//开一块新空间,多开一个给'\0'

strcpy(p, s._str);//拷贝数据

delete[] _str;//释放旧空间

_str = p;//修改指针指向

_size = s._size;

_capacity = s._capacity;

}

return *this;//有返回值可以进行像a=b=c一样的连续赋值

}

string类对象的访问遍历操作

_size和operator[]

访问遍历数据最典型的就是通过下标+[]使用for循环进行访问。

size_t string::size() const

{

return _size;

}

char& string::operator[](size_t pos)

{

assert(pos < _size);

return _str[pos];

}

来测试一下:

void TestString1()

{

bit::string s1("hello world");

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

{

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

}

cout << endl;

}

在这里插入图片描述

迭代器

迭代器是STL的六大组件之一,它适用于所有容器,因此它的类型是不能确定的,但是前面我们说过,它是一个像指针一样的东西,因此我们可以在这里<code>获取原生指针的方式作为迭代器。

//string.h

typedef char* iterator;//迭代器是一种像指针一样的东西,所以我们可以使用最简单的指针来尝试实现

typedef const char* const_iterator;

iterator begin();

iterator end();

//string.cpp

string::iterator string::begin()

//迭代器需要指定类域进行使用,所以iterator前面还加上了string::

{ -- -->

return _str;

}

string::iterator string::end()

{

return _str + _size;

}

我们知道,范围for的底层其实就是迭代器,当我们实现了迭代器,范围for也可以同时使用,可以测试实现一下:

bit::string s1("hello world");

string::iterator it1 = s1.begin();//迭代器遍历

while (it1 != s1.end())

{

cout << *it1 << " ";

it1++;

}

cout << endl;

for (auto e : s1)//范围for遍历

{

cout << e << " ";

}

cout << endl;

在这里插入图片描述

注意

我们使用typedef char* iterator;是<code>代码封装的一种体现

我把迭代器的真实类型进行typedef,因为迭代器真实的类型并不一定是指针,

string::iterator it1 = s1.begin();//迭代器遍历

while (it1 != s1.end())

{ -- -->

cout << *it1 << " ";

it1++;

}

这样子写无论iterator是什么类型,都可以直接这样使用,这是提供了一种简单通用访问容器的方式,屏蔽了底层的实现细节。

同样还存在const迭代器,它指向的内容不可修改。

//string.h

typedef const char* const_iterator;

const_iterator begin() const;

const_iterator end() const;

//string.cpp

string::const_iterator string::begin()const

{

return _str;

}

string::const_iterator string::end()const

{

return _str + _size;

}

有const迭代器,则也存在const operator[]:

const char& string::operator[](size_t pos) const

{

assert(pos < _size);

return _str[pos];

}

string类对象的修改操作

reserve

我们想要插入字符或者字符串,往往需要修改空间大小,因此先来实现reserve扩容

void string::reserve(size_t n)

{

if (n > _capacity)

{

char* tmp = new char[n + 1];//多开一个留个\0,虽然它不算在_size和_capacity中

strcpy(tmp, _str);

delete[] _str;

_str = tmp;

_capacity = n;

}

}

注意:只有在当传入的容量n大于_capacity时才需要扩容,其余都不变。

push_back、append和operator+=

push_back也就是尾插操作,我们在数据结构用的也比较多,也比较简单,但是在插入前还是要判断空间是否足够再进行插入。

void string::push_back(char ch)//尾插字符

{

if (_size == _capacity)//判断空间是否足够

{

size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;

reserve(newcapacity);

}

_str[_size] = ch;

_str[_size + 1] = '\0';

_size++;

}

注意:要在末尾加上’\0’,因为加进去的字符占用的是’\0’的位置。


append是追加的意思,其实也就是尾插

void string::append(const char* str)//尾插字符串

{

size_t len = strlen(str);//需要多少扩多少

if (_size + len > _capacity)

{

reserve(_size + len);

}

//strcpy(_str, str);//它要找到前面字符串的'\0',然后在后面追加

strcpy(_str + _size, str);//我知道'\0'的位置,直接拷贝连接过来即可

_size = _size + len;

}


其实push_back和append使用的并不是很多,因为使用operator+=尾插字符串/字符要方便快捷很多。

//都直接覆用即可

string& string::operator+=(char ch)

{

push_back(ch);

return *this;

}

string& string::operator+=(const char* str)

{

append(str);

return *this;

}

可以来测试实现一下:

bit::string s1("hello world");

s1.push_back('!');

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

s1.append(" aaaaaaa");

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

s1 += " bbbbbb";

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

在这里插入图片描述

这样三个尾插都可以实现。

insert和erase

<code>//string.h文件

void insert(size_t pos, char ch);

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

void erase(size_t pos, size_t len = npos);//在pos位置删除长度为len的字符串,不写长度默认全删


insert是插入字符/字符串,挪动数据后插入即可。

void string::insert(size_t pos, char ch)

{ -- -->

assert(pos <= _size);

if (_size == _capacity)

{

size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;

reserve(newcapacity);

}

size_t end = _size + 1;

//要注意pos等于0时的越界问题

while (end > pos)

{

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

--end;

}

_str[pos] = ch;

_size++;

}


对于插入字符串,挪动数据结束循环的条件要搞清楚。

void string::insert(size_t pos, const char* str)

{

assert(pos <= _size);

size_t len = strlen(str);//需要多少扩多少

if (_size + len > _capacity)

{

reserve(_size + len);

}

size_t end = _size + len;

while (end > pos + len - 1)

{

_str[end] = _str[end - len];

--end;

}

memcpy(_str + pos, str, len);

_size += len;

}

在这里插入图片描述


<code>erase在任意位置删除长度为len的字符串。

长度为len又有两种情况:

len大于后面字符的个数时,有多少删多少不大于时,就需要把后面pos+len处的字符串挪动到pos处

void string::erase(size_t pos, size_t len)

{ -- -->

assert(pos <= _size);

if (len >= _size - pos)//len大于后面字符的个数时,有多少删多少

{

_str[pos] = '\0';

_size = pos;

}

else

{

strcpy(_str + pos, _str + pos + len);

_size -= len;

}

}

删除并不是真删除,只要合理的调整 '\0' 位置和 _size 值,使其访问不到后续元素即可。

find

find是找到字符/小字符串在原字符串中的位置。

查找字符:

传入字符,直接遍历字符串,查找在其中的位置,如果有则返回其位置,没有就返回npos。

它是可以指定位置进行查找,若不指定,则从缺省值pos = 0处开始。

//string.h

size_t find(char ch, size_t pos = 0);

//string.cpp

size_t string::find(char ch, size_t pos)

{

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

{

if (_str[i] == ch)

return i;

}

return npos;//找不到就返回整数的最大值

}


查找字符串:

有一个算法叫KMP算法,匹配字符串查找算法,大家可以去了解了解。我们这里直接使用strstr足矣。

//string.h

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

//string.cpp

size_t string::find(const char* str, size_t pos)

{

const char* p = strstr(_str + pos, str);//返回对应位置的指针

if (p == nullptr)

return npos;

else

return p - _str;

}

指针-指针即可找到下标位置。

swap

swap,对于交换,其实可以使用库里面的swap即可,但是这样会进行深拷贝调用拷贝构造的次数比较多,代价比较大,但是可以将这三个变量分别调用库中的函数交换,可以减少拷贝的代价。

//s1.swap(s3);

void string::swap(string& s)

{

std::swap(_str, s._str);//直接交换指针指向

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

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

}

substr

substr是从pos位置开始,截取n个字符并返回。

string string::substr(size_t pos, size_t len)

{

if (len > _size - pos)//len大于后面剩余的字符,有多少取多少

{

string sub(_str + pos);

return sub;

}

else

{

string sub;

sub.reserve(len);

for (size_t i = 0; i < len; i++)

{

sub += _str[pos + i];

}

return sub;

}

}

大小比较

string的大小比较取决于ASCII码值,直接用strcmp即可。

我们只需要实现小于等于的判断,其余直接复用即可。

bool string::operator<(const string& s) const

{

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

}

bool string::operator>(const string& s) const

{

return !(*this < s);

}

bool string::operator<=(const string& s) const

{

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

}

bool string::operator>=(const string& s) const

{

return !(*this < s);

}

bool string::operator==(const string& s) const

{

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

}

bool string::operator!=(const string& s) const

{

return !(*this == s);

}

输入输出

流插入重载

使string类可以直接通过cout来输出。

直接遍历输出每个字符即可:

ostream& operator<<(ostream& os, const string& s)

{

for (auto e : s)

{

os << e;

}

return os;

}

流提取重载

void string::clear()

{

_str[0] = '\0';

_size = 0;

}

istream& operator>>(istream& is, string& str)

{

//char ch;//把一个一个字符提取出来

//is >> ch;//cin拿不到空格,会自动忽略掉空格或换行

//还要清空以前的字符串。

str.clear();

char ch = is.get();

while (ch != ' ' && ch != '\n')//等于空格或换行就结束,空格或者换行被认为是多个值之间的分割

{

str += ch;//提取出来的字符连接到str上,

ch = is.get();

}

return is;

}

第一次ch = is.get(),用于从输入流 is 中读取第一个字符,并将其存储在变量 ch 中。第二次ch = is.get(),用于在每次循环迭代结束时从输入流中读取下一个字符。这个新读取的字符将用于下一次循环迭代的条件检查。

全部代码

string.h

#pragma once

#define _CRT_SECURE_NO_WARNINGS

#include<iostream>

#include<assert.h>

using namespace std;

namespace bit

{

class string

{

public:

typedef char* iterator;//迭代器是一种像指针一样的东西,所以我们可以使用最简单的指针来尝试实现

typedef const char* const_iterator;

iterator begin();

iterator end();

const_iterator begin() const;

const_iterator end() const;

string(const char* str = " ");

string(const string& s);

string& operator=(const string& s);

~string();

const char* c_str() const;

size_t size() const;

char& operator[](size_t pos);

const char& operator[](size_t pos) const;

void reserve(size_t n);//扩容

void push_back(char ch);

void append(const char* str);

string& operator+=(char ch);

string& operator+=(const char* str);

void insert(size_t pos, char ch);

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

void erase(size_t pos, size_t len = npos);//在pos位置删除长度为len的字符串,不写长度默认全删

size_t find(char ch, size_t pos = 0);

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

void swap(string& s);

string substr(size_t pos = 0, size_t len = npos);

bool operator<(const string& s) const;

bool operator>(const string& s) const;

bool operator<=(const string& s) const;

bool operator>=(const string& s) const;

bool operator==(const string& s) const;

bool operator!=(const string& s) const;

void clear();

private:

char* _str;

size_t _size;

size_t _capacity;

const static size_t npos;//静态成员变量在类里面声明,在类外初始化

//类里面的静态成员变量就相当于全局变量

};

//const size_t string::npos = -1; 错误

istream& operator>>(istream& is, string& str);

ostream& operator<<(ostream& os, const string& s);

}

string.cpp

#include"string.h"

namespace bit

{

const size_t string::npos = -1;

string::string(const char* str)

:_size(strlen(str))

{

_str = new char[_size + 1]; //多开一个给'\0'存放

_capacity = _size;

strcpy(_str, str);

}

//s2(s1),s1就是s,s2就是*this

string::string(const string& s)

{

_str = new char[s._capacity + 1];

strcpy(_str, s._str);

_size = s._size;

_capacity = s._capacity;

}

string& string::operator=(const string& s)

{

if (this != &s)//避免自己给自己赋值,减少消耗

{

char* p = new char[s._capacity + 1];//开一块新空间,多开一个给'\0'

strcpy(p, s._str);//拷贝数据

delete[] _str;//释放旧空间

_str = p;//修改指针指向

_size = s._size;

_capacity = s._capacity;

}

return *this;//有返回值可以进行像a=b=c一样的连续赋值

}

string::~string()

{

delete[] _str;

_str = nullptr;

_size = _capacity = 0;

}

const char* string::c_str() const

{

return _str;

}

size_t string::size() const

{

return _size;

}

char& string::operator[](size_t pos)

{

assert(pos < _size);

return _str[pos];

}

string::iterator string::begin()//迭代器需要指定类域进行使用,所以iterator前面还加上了string::

{

return _str;

}

string::iterator string::end()

{

return _str + _size;

}

string::const_iterator string::begin()const

{

return _str;

}

string::const_iterator string::end()const

{

return _str + _size;

}

const char& string::operator[](size_t pos) const

{

assert(pos < _size);

return _str[pos];

}

void string::reserve(size_t n)

{

if (n > _capacity)

{

char* tmp = new char[n + 1];//多开一个留个\0,虽然它不算在_size和_capacity中

strcpy(tmp, _str);

delete[] _str;

_str = tmp;

_capacity = n;

}

}

void string::push_back(char ch)//尾插字符

{

if (_size == _capacity)

{

size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;

reserve(newcapacity);

}

_str[_size] = ch;

_str[_size + 1] = '\0';

_size++;

}

void string::append(const char* str)//尾插字符串

{

size_t len = strlen(str);//需要多少扩多少

if (_size + len > _capacity)

{

reserve(_size + len);

}

//strcpy(_str, str);//它要找到前面字符串的'\0',然后在后面追加

strcpy(_str + _size, str);//我知道'\0'的位置,直接拷贝连接过来即可

_size = _size + len;

}

string& string::operator+=(char ch)

{

push_back(ch);

return *this;

}

string& string::operator+=(const char* str)

{

append(str);

return *this;

}

void string::insert(size_t pos, char ch)

{

assert(pos <= _size);

if (_size == _capacity)

{

size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;

reserve(newcapacity);

}

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)

{

assert(pos <= _size);

size_t len = strlen(str);//需要多少扩多少

if (_size + len > _capacity)

{

reserve(_size + len);

}

size_t end = _size + len;

while (end > pos + len - 1)

{

_str[end] = _str[end - len];

--end;

}

memcpy(_str + pos, str, len);

_size += len;

}

void string::erase(size_t pos, size_t len)

{

assert(pos <= _size);

if (len >= _size - pos)//len大于后面字符的个数时,有多少删多少

{

_str[pos] = '\0';

_size = pos;

}

else

{

strcpy(_str + pos, _str + pos + len);

_size -= len;

}

}

size_t string::find(char ch, size_t pos)

{

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)

{

const char* p = strstr(_str + pos, str);//返回对应位置的指针

if (p == nullptr)

return npos;

else

return p - _str;

}

//s1.swap(s3);

void string::swap(string& s)

{

std::swap(_str, s._str);//直接交换指针指向

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

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

}

string string::substr(size_t pos, size_t len)

{

if (len > _size - pos)//len大于后面剩余的字符,有多少取多少

{

string sub(_str + pos);

return sub;

}

else

{

string sub;

sub.reserve(len);

for (size_t i = 0; i < len; i++)

{

sub += _str[pos + i];

}

return sub;

}

}

bool string::operator<(const string& s) const

{

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

}

bool string::operator>(const string& s) const

{

return !(*this < s);

}

bool string::operator<=(const string& s) const

{

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

}

bool string::operator>=(const string& s) const

{

return !(*this < s);

}

bool string::operator==(const string& s) const

{

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

}

bool string::operator!=(const string& s) const

{

return !(*this == s);

}

void string::clear()

{

_str[0] = '\0';

_size = 0;

}

istream& operator>>(istream& is, string& str)

{

//char ch;//把一个一个字符提取出来

//is >> ch;//cin拿不到空格,会自动忽略掉空格或换行

//还要清空以前的字符串。

str.clear();

char ch = is.get();

while (ch != ' ' && ch != '\n')//等于空格或换行就结束,空格或者换行被认为是多个值之间的分割

{

str += ch;//提取出来的字符连接到str上,

ch = is.get();

}

return is;

}

ostream& operator<<(ostream& os, const string& s)

{

for (auto e : s)

{

os << e;

}

return os;

}

}

test.cpp

#include"string.h"

namespace bit

{

void TestString1()

{

bit::string s1("hello world");

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

{

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

}

cout << endl;

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

while (it1 != s1.end())

{

cout << *it1 << " ";

it1++;

}

cout << endl;

for (auto e : s1)

{

cout << e << " ";

}

cout << endl;

const bit::string s2("aaaaaaaaa");

string::const_iterator it2 = s2.begin();

while (it2 != s2.end())

{

//*it2 = 'y';不能修改

cout << *it2 << " ";

it2++;

}

cout << endl;

for (int i = 0; i < s2.size(); i++)

{

//s2[i]++; 不能修改

cout << s2[i] << " ";

}

cout << endl;

}

void TestString2()

{

bit::string s1("hello world");

/*s1.push_back('!');

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

s1.append(" aaaaaaa");

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

s1 += " bbbbbb";

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

s1.insert(0, 'a');

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

s1.insert(0, "bbbbbb");

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

s1.erase(5, 3);

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

s1.erase(3, 20);

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

}

void TestString3()

{

bit::string s1("hello world");

cout << s1.find("hell") << endl;

cout << s1.find("abc") << endl;

}

void TestString4()

{

bit::string s1("hello world");

bit::string s2(s1);

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

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

}

void TestString5()

{

bit::string s1("hello world");

bit::string s2("aaaaaaaaaaa");

s1.swap(s2);

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

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

}

void TestString6()

{

bit::string s("https://legacy.cplusplus.com/reference/string/string/substr/");

cout << "链接为:" << s.c_str() << endl;

size_t pos1 = s.find(':');//找到冒号,并返回它所在的位置

bit::string s1 = s.substr(0, pos1 - 0);//从下标0开始,想要截取pos1-0个长度的字符串

//左闭右开区间下标相减就是长度

cout << "协议为:" << s1.c_str() << endl;

size_t pos2 = s.find('/', pos1 + 3);//从pos1+3的位置即字符c这个位置开始往后找'/'

bit::string s2 = s.substr(pos1 + 3, pos2 - (pos1 + 3));

cout << "域名为:" << s2.c_str() << endl;

bit::string s3 = s.substr(pos2 + 1);//从pos2+1开始直到最后

//只有一个参数,截取长度为缺省值npos,为最大整数,因此直接取到最后

cout << "路径为:" << s3.c_str() << endl;

}

void TestString7()

{

bit::string s1("hello");

bit::string s2("abcd");

cout << (s1 < s2) << endl;

}

void TestString8()

{

bit::string s1("hello");

cout << s1 << endl;

cin >> s1;

cout << s1 << endl;

}

}

int main()

{

//bit::TestString1();

//bit::TestString2();

//bit::TestString3();

//bit::TestString4();

//bit::TestString5();

//bit::TestString6();

bit::TestString8();

return 0;

}


感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。

请添加图片描述



声明

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