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

一、string
的模拟实现
前面的博客我们了解了string
的使用,那我们就一起来看看如何模拟实现吧,string
的模拟实现部分,为了便于代码管理,我们依旧会实现三个部分,分别是test.cpp、string.h、string.cpp
:
1.1 解决命名冲突问题
我们设计的string
会和库里面的string
名字相同,为了避免和库里的产生冲突,所以我们要把string
放在一个我们定义的命名空间中 ,相同的名称的命名空间编译器会认为是同一个命名空间,所以我们会在头文件和.cpp
文件中定义两个相同的命名空间。
cpp
//string.h:
namespace STR
{
class string
{
public:
private:
};
}
//string.cpp:
namespace STR
{
}
1.2 string
的成员
cpp
private:
char* _str = nullptr;
int _size = 0;
int _capacity = 0;
public:
const static size_t npos;
其中我们知道有一个npos
它是库里面定义的静态成员变量 ,而且是公有的 ,因为我们可以单独使用npos
,那它的初始化要在外面初始化,所以我们在string.cpp
中实现初始化。
cpp
namespace STR
{
const size_t string::npos = -1;
}
1.2 构造和析构函数
cpp
string(const char* str = "");
~string();
构造函数:
cpp
string::string(const char* str)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_size + 1];
memcpy(_str, str, _size + 1);
}
注意点 :
为什么不都在初始化列表初始化呢 ?因为那样计算调用的strlen
函数比较多,由于成员变量走初始化列表是由定义时的顺序走的,所以这样写是最佳方案,只需要调用计算一次。如果你把成员定义的顺序改变了,那也可以,但如果等之后有其他程序员维护时,再给你改过来,就极有可能出错。
使用测试:
cpp
std::string s1("heihei\0\0\0ok~");
STR::string s2("heihei\0\0\0ok~");
由于暂时没有重载<<
操作符,所以不能打印,我们可以通过内存查看:
从内存中可以看出s1
和s2
中存放的字符串相同,我们的构造函数没有问题。
析构函数:
cpp
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
1.3 c_str
、重载<<、[]
、reverse
、push_back
c_str
我们知道c_str
函数是将string
对象转换为const char*
类型的C语言
风格字符串。
cpp
const char* string::c_str() const
{
return _str;
}
这样就可以执行打印操作:
当然这和重载<<
有着本质的区别 ,它只能输出'\0'
之前的字符,但string类对象
中是可能有'\0'
的。
reverse
reserve
函数的主要功能是为字符串预先分配内存空间 ,避免在后续操作(如添加字符)
时频繁进行内存重新分配,从而提高性能。
cpp
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
cpp
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
和重载 <<
的区别:
cpp
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()
cpp
int size() const;
int string::size() const
{
return _size;
}
operator[]
:
cpp
char& operator[](size_t i);
const char& operator[](size_t i) const;
cpp
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指针
。
cpp
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风格
字符串。
cpp
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+=
:
cpp
string& operator+=(char ch);
string& operator+=(const char* str);
我们要模拟实现这两个函数,此时我们之前实现的push_back
和append
就派上用场了。
cpp
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
测试:
cpp
STR::string s("hello");
s += ' ';
s += "world!";
cout << s << endl;
结果 :
1.5 pop_back()
pop_back
用于删除尾部字符。前提是字符串中要有字符。
cpp
void pop_back();
void string::pop_back()
{
assert(_size > 0);
_size--;
_str[_size] = '\0';
}
测试:
cpp
STR::string s("hello");
s += ' ';
s += "world!";
cout << s << endl;
s.pop_back();
cout << s << endl;

1.6 insert()
和erase()
insert
可以在字符串的指定位置添加字符、字符串或者子串 。
erase
可以移除字符串中指定位置或范围内的字符。
cpp
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
:
cpp
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;
}
测试:
cpp
STR::string s("hello world!");
cout << s << endl;
s.insert(6, 'n');
s.insert(7, "ew ");
cout << s << endl;
运行 :
erase
:
cpp
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:
cpp
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:
cpp
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
是用于查找子串或字符的重要方法 。它能在字符串中搜索指定内容,并返回首次出现的位置。
cpp
size_t find(char ch, size_t pos = 0) const;
size_t find(const char* str, size_t pos = 0) const;
cpp
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;
}
测试:
cpp
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
:
cpp
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;
}
测试:
cpp
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
的地址相同:
为了解决这个问题,我们可以将拷贝构造函数
实现一下,顺便也将赋值重载函数
实现一下。
拷贝构造
:
cpp
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=
:
cpp
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
:
cpp
void clear();
void string::clear()
{
_str[0] = '\0';
_size = 0;
}
1.9 范围for
我们知道库里面的string类
是支持使用范围for
的,而范围for
的底层就是转换成迭代器 (具体是begin和end)
,迭代器是一种用于遍历和访问字符串中字符的对象,它的本质是对指针的抽象。通过迭代器,你可以像操作指针一样遍历字符串 ,库中string类
提供了很多迭代器 其中包括begin
和end
,那我们也可以简单模拟实现一下这两个迭代器,这样就可以使用范围for
了。
cpp
typedef char* iterator;
typedef const char* const_iterator;
begin
:
cpp
string::iterator string::begin()
{
return _str;
}
string::const_iterator string::begin() const
{
return _str;
}
end
:
cpp
string::iterator string::end()
{
return _str + _size;
}
string::const_iterator string::end() const
{
return _str + _size;
}
这样我们就把begin
和end
迭代器简单实现出来了,我们来测试一下吧!
测试:
cpp
STR::string s("This is an example sentence.");
for (auto& ch : s)
{
cout << ch;
}
运行 :
虽然我们实现的迭代器可以使用范围for
但迭代器是很复杂 的,C++
中有一个typeid
运算符,它能获取表达式的真实类型信息,我们可以使用它来窥知一二。
cpp
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
更合理。
cpp
istream& operator>>(istream& in, string& s)
{
//库中每次读取都会清空
s.clear();
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
测试:
cpp
std::string s1;
cin >> s1;
cout <<" 库中:" << s1 << endl;
STR::string s2;
cin >> s2;
cout <<"模拟实现:" << s2 << endl;
运行 :
getline
:
库中实现了两个,我们可以把这两个合并成一个:
cpp
istream& getline(istream& in, string& s, char delim = '\n');
默认情况下,读到换行符'\n'
不就是图片中第二种嘛。
cpp
istream& getline(istream& in, string& s, char delim)
{
s.clear();
char ch = in.get();
while (ch != delim)
{
s += ch;
ch = in.get();
}
return in;
}
测试:
cpp
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;
,我们一起看一下读取字符很多的情况:
cpp
STR::string s;
cin >> s;
cout << s << endl;
结果 :
这种情况下有7、8
次扩容,扩容频率很高,那这时的代码就不好,那我们怎么优化呢?
我们可以提前开辟一定空间的字符数组,然后等数组满了,再将它转移到字符类对象中去,这样可以减少扩容次数。
优化:
cpp
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 关系运算符重载
cpp
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<
:
cpp
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==
:
cpp
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;
}
其余的:
cpp
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);
}
测试:
cpp
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
:
cpp
void swap(string& s);
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
拷贝构造:
cpp
//传统写法
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=
:
cpp
//传统写法
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
类对象的交换。
cpp
void swap(string& x, string& y);
void swap(string& x, string& y)
{
x.swap(y);
}
注意 :当有函数模板swap
和现成可以使用的函数swap
时,编译器不会再次生成swap
函数,而是使用现有的swap
函数。
三、关于库中string
类的补充
当我们执行调试以下代码:
cpp
std::string s("hello");
s += "1111111111111111111111111111111111111111";
cout << s << endl;

你会发现当s
中的字符比较短时,它会存放在定长数组_buf
中,而当s
中的字符比较长时,它就转移到了_ptr
中,当你反复尝试,你会发现当字符长度len<16
时串存在_buf
数组中,而当len>=16
时就转移到了_ptr
中,也就是说这个定长数组长度为16
,这样的设计能够有效避免刚开始扩容次数多的问题,我们可以通过一段代码,来观察奥妙。
cpp
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账号,我们一同成长!
(~ ̄▽ ̄)~