【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~");
由于暂时没有重载<<
操作符,所以不能打印,我们可以通过内存查看:
从内存中可以看出s1
和s2
中存放的字符串相同,我们的构造函数没有问题。
析构函数:
string::~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
1.3 c_str
、重载<<、[]
、reverse
、push_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_back
和reverse
就可以继续讨论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;
从内存中可以看出s1
和s2
字符串中都存在'\0'
,再从s1
和s2
打印的区别可以看出,库中的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_back
和append
就派上用场了。
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
也确实得到了该得到的字符串,此时s4
和s7
中的_str
指向同一块空间,当return 0;
的时候,同一块空间析构了两次,编译器崩溃了。
s4._str
和s7._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类
提供了很多迭代器其中包括begin
和end
,那我们也可以简单模拟实现一下这两个迭代器,这样就可以使用范围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;
}
这样我们就把begin
和end
迭代器简单实现出来了,我们来测试一下吧!
测试:
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
,我们推荐用后者,因为getchar
和scanf
它们都属于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;
}
最后一行中,如果条件为真,说明s1
和s2
比较完成后,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账号,我们一同成长!
(~ ̄▽ ̄)~