当前位置: 首页 > news >正文

【C++】string的模拟实现

目录

  • 一、`string`的模拟实现
    • 1.1 解决命名冲突问题
    • 1.2 `string`的成员
    • 1.2 构造和析构函数
    • 1.3 `c_str`、重载`<<、[]`、`reverse`、`push_back`
    • 1.4 `append`、重载`+=`
    • 1.5 `pop_back()`
    • 1.6 `insert()`和`erase()`
    • 1.7 `find()`
    • 1.8 `sustr()`、`拷贝构造`、`operator=`和`clear()`
    • 1.9 `范围for`
    • 1.10 `operator>>`和`getline`
    • 1.11 关系运算符重载
  • 二、拷贝构造和赋值运算符重载的现代写法
  • 三、关于库中`string`类的补充

在这里插入图片描述

个人主页<—请点击
C++专栏<—请点击

一、string的模拟实现

前面的博客我们了解了string的使用,那我们就一起来看看如何模拟实现吧,string的模拟实现部分,为了便于代码管理,我们依旧会实现三个部分,分别是test.cpp、string.h、string.cpp在这里插入图片描述

1.1 解决命名冲突问题

我们设计的string会和库里面的string名字相同,为了避免和库里的产生冲突,所以我们要把string放在一个我们定义的命名空间中,相同的名称的命名空间编译器会认为是同一个命名空间,所以我们会在头文件和.cpp文件中定义两个相同的命名空间。

//string.h:
namespace STR
{class string{public:private:};
}
//string.cpp:
namespace STR
{}

1.2 string的成员

private:char* _str = nullptr;int _size = 0;int _capacity = 0;
public:const static size_t npos;

其中我们知道有一个npos它是库里面定义的静态成员变量,而且是公有的,因为我们可以单独使用npos,那它的初始化要在外面初始化,所以我们在string.cpp中实现初始化。

namespace STR
{const size_t string::npos = -1;
}

1.2 构造和析构函数

string(const char* str = "");
~string();

构造函数

string::string(const char* str):_size(strlen(str))
{_capacity = _size;_str = new char[_size + 1];memcpy(_str, str, _size + 1);
}

注意点
为什么不都在初始化列表初始化呢?因为那样计算调用的strlen函数比较多,由于成员变量走初始化列表是由定义时的顺序走的,所以这样写是最佳方案,只需要调用计算一次。如果你把成员定义的顺序改变了,那也可以,但如果等之后有其他程序员维护时,再给你改过来,就极有可能出错。

使用测试

std::string s1("heihei\0\0\0ok~");
STR::string s2("heihei\0\0\0ok~");

由于暂时没有重载<<操作符,所以不能打印,我们可以通过内存查看:
在这里插入图片描述
从内存中可以看出s1s2中存放的字符串相同,我们的构造函数没有问题。

析构函数

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

1.3 c_str、重载<<、[]reversepush_back

  • c_str
    我们知道c_str函数是将string对象转换为const char*类型的C语言风格字符串。
const char* string::c_str() const
{return _str;
}

这样就可以执行打印操作:
在这里插入图片描述
当然这和重载<<有着本质的区别,它只能输出'\0'之前的字符,但string类对象中是可能有'\0'的。

  • reverse
    reserve函数的主要功能是为字符串预先分配内存空间,避免在后续操作(如添加字符)时频繁进行内存重新分配,从而提高性能。
    在这里插入图片描述
void reserve(size_t n);
void string::reserve(size_t n)
{if (n > _capacity){char* str = new char[n + 1];memcpy(str, _str, _size + 1);delete[] _str;_str = str;_capacity = n;}
}
  • push_back
void push_back(char ch);
void string::push_back(char ch)
{if (_size >= _capacity){int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;reserve(newcapacity);}_str[_size] = ch;_size++;_str[_size] = '\0';
}

现在我们实现了push_backreverse就可以继续讨论c_str重载<<的区别:

std::string s1("heihei");
STR::string s2("heihei");
s1.push_back('\0');
s1.push_back('h');
s2.push_back('\0');
s2.push_back('u');
cout << s1 << endl;
cout << s2.c_str() << endl;

在这里插入图片描述
在这里插入图片描述
从内存中可以看出s1s2字符串中都存在'\0',再从s1s2打印的区别可以看出,库中的cin输出不受中间'\0'的影响,而c_str中得到的本身就是C语言风格的字符串,受到'\0'的影响。这也为我们实现<<重载函数提供了思路。

  • operator<<:
    在此之前我们可能要用到size()operator[]函数,我们先实现一下它:
    size()
int size() const;
int string::size() const
{return _size;
}

operator[]:

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

注意C++string类在重载[]运算符时返回char&(字符引用)主要目的就是允许通过[]运算符直接修改原字符串中的字符引用提供了对原始数据的直接访问,而非副本,因此任何通过引用进行的修改都会反映到原始对象上

operator<<

根据之前博客实现的函数重载,我们为了不倒反天罡,我们依旧实现成全局函数,因为设置成成员函数,它的默认第一个形参this指针

ostream& operator<<(ostream& out, const string& s);
ostream& operator<<(ostream& out, const string& s)
{for (int i = 0;i < s.size();i++){out << s[i];}return out;
}

再次运行
在这里插入图片描述

1.4 append、重载+=

append:
在这里插入图片描述
我们这里只模拟实现append追加C风格字符串。

void append(const char* str);
void string::append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){int newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;reserve(newcapacity);}memcpy(_str + _size, str, len + 1);_size += len;
}

operator+=
在这里插入图片描述

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

我们要模拟实现这两个函数,此时我们之前实现的push_backappend就派上用场了。

string& string::operator+=(char ch)
{push_back(ch);return *this;
}
string& string::operator+=(const char* str)
{append(str);return *this;
}

测试

STR::string s("hello");
s += ' ';
s += "world!";
cout << s << endl;

结果
在这里插入图片描述

1.5 pop_back()

pop_back用于删除尾部字符。前提是字符串中要有字符

void pop_back();
void string::pop_back()
{assert(_size > 0);_size--;_str[_size] = '\0';
}

测试

STR::string s("hello");
s += ' ';
s += "world!";
cout << s << endl;
s.pop_back();
cout << s << endl;

在这里插入图片描述

1.6 insert()erase()

insert可以在字符串的指定位置添加字符、字符串或者子串
erase可以移除字符串中指定位置或范围内的字符

string& insert(size_t pos, char ch);
string& insert(size_t pos, const char* str);
string& erase(size_t pos = 0, size_t len = npos);

insert

string& string::insert(size_t pos, char ch)
{assert(pos <= _size);if (_size >= _capacity){int 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++;return *this;
}
string& string::insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){int newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len;reserve(newcapacity);}size_t end = _size + len;while (end > pos + len - 1){_str[end] = _str[end - len];end--;}for (size_t i = 0;i < len;i++){_str[pos + i] = str[i];}_size += len;return *this;
}

测试

STR::string s("hello world!");
cout << s << endl;
s.insert(6, 'n');
s.insert(7, "ew ");
cout << s << endl;

运行
在这里插入图片描述
erase

string& string::erase(size_t pos, size_t len)
{assert(pos <= _size);//全部删除if (len == npos || len >= _size - pos){_size = pos;_str[_size] = '\0';}else{size_t i = pos + len;memmove(_str + pos, _str + i, _size + 1 - i);_size -= len;}return *this;
}

测试1

std::string s1("This is an example sentence.");
s1.erase(10, 8);
cout << "    库中:" << s1 << endl;
STR::string s2("This is an example sentence.");
s2.erase(10, 8);
cout << "模拟实现:" << s2 << endl;

运行1
在这里插入图片描述
测试2

std::string s1("This is an example sentence.");
s1.erase(10);
cout << "    库中:" << s1 << endl;
STR::string s2("This is an example sentence.");
s2.erase(10);
cout << "模拟实现:" << s2 << endl;

运行2
在这里插入图片描述

1.7 find()

find用于查找子串或字符的重要方法。它能在字符串中搜索指定内容,并返回首次出现的位置

size_t find(char ch, size_t pos = 0) const;
size_t find(const char* str, size_t pos = 0)  const;
size_t string::find(char ch, size_t pos) const
{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
{const char* pr = strstr(_str + pos, str);if (pr == nullptr){return npos;}else return pr - _str;
}

测试

std::string s1("This is an example sentence.");
size_t pos = s1.find('s');
size_t pos1 = s1.find("exam", pos);
cout << "    库中:";
cout << pos << " " << pos1 << endl;
STR::string s2("This is an example sentence.");
size_t pos3 = s1.find('s');
size_t pos4 = s1.find("exam", pos3);
cout << "模拟实现:";
cout << pos3 << " " << pos4 << endl;

运行
在这里插入图片描述

1.8 sustr()拷贝构造operator=clear()

substr方法用于从当前字符串中提取并返回一个子串
clear方法用于清空当前字符串的内容,使其变为空字符串

substr

string substr(size_t pos, size_t len) const;
string string::substr(size_t pos, size_t len) const
{if (len == npos || len >= _size - pos){len = _size - pos;}string ret;//提前预留空间ret.reserve(len);for (size_t i = 0;i < len;i++){ret += _str[pos + i];}return ret;
}

测试

std::string s1("This is an example sentence.");
std::string s6 = s1.substr(5);
std::string s3 = s6;
cout << "    库中:" << s3 << endl;
STR::string s2("This is an example sentence.");
STR::string s7= s2.substr(5);
STR::string s4 = s7;
cout << "模拟实现:" << s4 << endl;

此时执行以上代码时,编译器就会崩溃,原因是在执行STR::string s4 = s7;语句时涉及到拷贝构造,但我们现在没有实现拷贝构造函数,在这种情况下,编译器会是自动生成拷贝构造函数,也就是执行浅拷贝s4也确实得到了该得到的字符串,此时s4s7中的_str指向同一块空间,当return 0;的时候,同一块空间析构了两次,编译器崩溃了
在这里插入图片描述
s4._strs7._str的地址相同:
在这里插入图片描述
在这里插入图片描述
为了解决这个问题,我们可以将拷贝构造函数实现一下,顺便也将赋值重载函数实现一下

拷贝构造

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

再次运行代码
在这里插入图片描述
operator=

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

clear:

void clear();
void string::clear()
{_str[0] = '\0';_size = 0;
}

1.9 范围for

我们知道库里面的string类是支持使用范围for的,而范围for的底层就是转换成迭代器(具体是begin和end)迭代器是一种用于遍历和访问字符串中字符的对象,它的本质是对指针的抽象。通过迭代器,你可以像操作指针一样遍历字符串,库中string类提供了很多迭代器其中包括beginend,那我们也可以简单模拟实现一下这两个迭代器,这样就可以使用范围for了。
在这里插入图片描述
在这里插入图片描述

typedef char* iterator;
typedef const char* const_iterator;

begin

string::iterator string::begin()
{return _str;
}
string::const_iterator string::begin() const
{return _str;
}

end

string::iterator string::end()
{return _str + _size;
}
string::const_iterator string::end() const
{return _str + _size;
}

这样我们就把beginend迭代器简单实现出来了,我们来测试一下吧!

测试

STR::string s("This is an example sentence.");
for (auto& ch : s)
{cout << ch;
}

运行
在这里插入图片描述
虽然我们实现的迭代器可以使用范围for但迭代器是很复杂的,C++中有一个typeid运算符,它能获取表达式的真实类型信息,我们可以使用它来窥知一二。

cout << typeid(STR::string::iterator).name() << endl;
cout << typeid(std::string::iterator).name() << endl;

在这里插入图片描述
库中实现的迭代器类型是类类型!

1.10 operator>>getline

operator>>:
在实现这个函数时有一个注意点,就是内部不能用cin来读取数据,因为cin它不会读取空格或者换行,它会认为空格或者换行是分隔符,也就是说如果你用cin来实现读取,将永远不会停止,你输入一个任意字符cin读取或者忽略。

这里的解决方法就是使用getchar或者get,我们推荐用后者,因为getcharscanf它们都属于C语言的流,我们在实现C++的东西,所以使用get更合理。

istream& operator>>(istream& in, string& s)
{//库中每次读取都会清空s.clear();char ch = in.get();while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;
}

测试

std::string s1;
cin >> s1;
cout <<"    库中:" << s1 << endl;
STR::string s2;
cin >> s2;
cout <<"模拟实现:" << s2 << endl;

运行
在这里插入图片描述
getline:
在这里插入图片描述
库中实现了两个,我们可以把这两个合并成一个:

istream& getline(istream& in, string& s, char delim = '\n');

默认情况下,读到换行符'\n'不就是图片中第二种嘛

istream& getline(istream& in, string& s, char delim)
{s.clear();char ch = in.get();while (ch != delim){s += ch;ch = in.get();}return in;
}

测试

STR::string s;
STR::getline(cin, s);
cout << "输出:" << s << endl;
STR::getline(cin, s, 'l');
cout << "输出:" << s << endl;

运行
在这里插入图片描述
虽然我们把operator>>getline实现出来了,但这样写是很不好的一种写法,因为当读取的字符太多会带来很多次的扩容。
我在reserve函数中加了这样一句话:cout << "_capacity:" << _capacity << endl;,我们一起看一下读取字符很多的情况:

STR::string s;
cin >> s;
cout << s << endl;

结果
在这里插入图片描述
这种情况下有7、8次扩容,扩容频率很高,那这时的代码就不好,那我们怎么优化呢?

我们可以提前开辟一定空间的字符数组,然后等数组满了,再将它转移到字符类对象中去,这样可以减少扩容次数。

优化

istream& operator>>(istream& in, string& s)
{//库中每次读取都会清空s.clear();char buff[128];int i = 0;char ch = in.get();while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;
}istream& getline(istream& in, string& s, char delim)
{s.clear();char buff[128];int i = 0;char ch = in.get();while (ch != delim){buff[i++] = ch;if (i == 127){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;
}

再次运行
在这里插入图片描述
这样就大大减少了扩容次数,同时这样写还有一个优点,就是buff数组的大小是可变的如果还嫌扩容次数多,你可以开到256、1024,这样就很少了!

1.11 关系运算符重载

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;

它们没有必要全部实现,只要实现一部分,其他都可以调用已经实现的来实现。

operator<

bool string::operator<(const string& s) const
{size_t i1 = 0, i2 = 0;while (i1 < _size && i2 < _size){if (_str[i1] < s._str[i2]){return true;}else if (_str[i1] > s._str[i2]){return false;}i1++;i2++;}return i2 < s._size;
}

最后一行中,如果条件为真,说明s1s2比较完成后,s2依旧剩余字符串,说明s1小于s2,否则s1不小于s2

operator==

bool string::operator==(const string& s) const
{size_t i1 = 0, i2 = 0;while (i1 < _size && i2 < _size){if (_str[i1] < s._str[i2]){return true;}else if (_str[i1] > s._str[i2]){return false;}i1++;i2++;}return i1 == _size && i2 == s._size;
}

其余的

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 !(*this < s);
}
bool string::operator!=(const string& s) const
{return !(*this == s);
}

测试

STR::string s1("hello");
STR::string s2("hello~");
cout << (s1 < s2) << endl;
cout << (s1 <= s2) << endl;
cout << (s1 > s2) << endl;
cout << (s1 >= s2) << endl;
cout << (s1 == s2) << endl;
cout << (s1 != s2) << endl;

在这里插入图片描述

二、拷贝构造和赋值运算符重载的现代写法

我们上面实现的是传统的写法,较为复杂,而现代的写法简洁,C++库中有一个swap函数,它可以交换任意两者。
在这里插入图片描述
从上图可以看出它使用了函数模板进行实现。

我们可以利用swap来实现这两个函数。
swap

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

拷贝构造

//传统写法
string::string(const string& s)
{_str = new char[s._capacity + 1];memcpy(_str, s._str, s._size + 1);_size = s._size;_capacity = s._capacity;
}
//现代写法
string::string(const string& s)
{string tmp(s._str);swap(tmp);
}

operator=

//传统写法
string& string::operator=(const string& s)
{if (this != &s){char* tmp = new char[s._capacity + 1];memcpy(tmp, s._str, s._size + 1);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;
}
//现代写法
string& string::operator=(const string& s)
{if (this != &s){string tmp(s);swap(tmp);}return *this;
}
//再次优化
string& string::operator=(string s)
{if (this != &s){swap(s);}return *this;
}

现代写法更简洁,但效率是没有优化的,你可以将这种写法理解为"资本",让别人干完活,然后交换。

关于string类中的全局函数swap

在观察string类的时候,你会发现string中还有一个全局的swap函数,既然库中已经有了swap函数,string类也有成员函数swap,那为什么string中还要定义一个全局函数swap呢?仔细观察库中的swap函数你会发现,swap函数中对于string类对象通过一次拷贝构造,两次赋值操作完成交换,这样它的效率低下,对于string而言不是最优解,所以string中又定义了一个全局的swap函数。

这个全局的swap函数内部通常调用string类的成员函数swap来进行指针和数值的交换,从而完成string类对象的交换

void swap(string& x, string& y);
void swap(string& x, string& y)
{x.swap(y);
}

注意当有函数模板swap和现成可以使用的函数swap时,编译器不会再次生成swap函数,而是使用现有的swap函数

三、关于库中string类的补充

当我们执行调试以下代码:

std::string s("hello");
s += "1111111111111111111111111111111111111111";
cout << s << endl;

在这里插入图片描述
你会发现当s中的字符比较短时,它会存放在定长数组_buf中,而当s中的字符比较长时,它就转移到了_ptr中,当你反复尝试,你会发现当字符长度len<16时串存在_buf数组中,而当len>=16时就转移到了_ptr中,也就是说这个定长数组长度为16这样的设计能够有效避免刚开始扩容次数多的问题,我们可以通过一段代码,来观察奥妙。

std::string s;
int n = s.capacity();
for (int i = 0;i < 300;i++)
{s += '1';if (s.capacity() != n){cout << "s.capacity():" << n << endl;n = s.capacity();}
}

在这里插入图片描述
从运行结果上可以看出,s的空间一开始不是从0开始的,而是从15,而且第一次扩容和第二次扩容和其它的情况明显不同,第一次扩容是以2倍,而其余都是约为1.5倍,因为_buf数组的存在才导致的差异。

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

相关文章:

  • QTableWidget的函数和信号介绍
  • java基础知识回顾3(可用于Java基础速通)考前,面试前均可用!
  • pinia状态管理使用
  • 使用CRTP实现单例
  • 22、web场景-web开发简介
  • 弦序参量(SOP)
  • 详解Innodb一次更新事物的执行过程
  • 【概率论基本概念02】最大似然性
  • 【MySQL成神之路】MySQL函数总结
  • 【C语言干货】free细节
  • RocketMQ 索引文件(IndexFile)详解:结构、原理与源码剖析
  • 用 Python 实现了哪些办公自动化
  • 力扣第157场双周赛
  • 湖北理元理律师事务所债务优化方案:让还款与生活平衡的艺术
  • 基于PyTorch的残差网络图像分类实现指南
  • SGMD辛几何模态分解
  • 【MATLAB代码】主动声纳多路径目标测距与定位,测距使用互相关,频率、采样率可调、声速可调,定位使用三边法|订阅专栏后可直接查看源代码
  • 第一章 半导体基础知识
  • 华为OD机试真题——出租车计费/靠谱的车 (2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现
  • 网络安全--PHP第二天
  • 酒店网站建设方案ppt/网站404页面怎么做
  • 做网站什么主题好做/文案写作软件app
  • 乐清网站建设honmau/培训学校机构有哪些
  • 怎么在自己做网站/网站网络营销
  • 门户网站模板源码/seo运营是什么意思
  • 湖南网站建设费用/怎么样关键词优化