在本篇博客中,作者将会带领你模拟实现简单的STL中的string类的。至于string的常规使用,这里不做讲解。
string类的c++参考文档
string - C++ Reference (cplusplus.com)
一.string的基本结构
string类的大致结构可以分为三个变量来表示,一个是字符指针,一个是size,还有一个是capacity。这三个变量分别表示:字符指针指向第一个字符串的第一个位置,size存放字符串的长度,capacity表示动态内存开辟空间的大小。如下图所示。
所以string类的成员变量可以像下面一样设计。
namespace string_blog
{
class string
{
public:
private:
char* _str;
size_t size;
size_t capacity;
};
}
二.Member functions的实现
Member functions可以分为三类:构造函数,析构函数,赋值=重载
1.构造函数
构造函数又可以分为三种:无参构造函数,有参构造函数,拷贝构造
无参构造函数和带参构造函数实现
首先说明,其实string类就是一个字符串数组,而在C语言中,字符串数组是用'\0'来结尾的,所以string类中的字符串也是用'\0'来结尾的。
同时,由于C++支持缺省参数,所以无参构造函数和带参构造函数可以同时实现。
//无参与带参构造函数
//string s1;
//string s2("hello world");
string(const char* tmp = "")//缺省值给"",代表一个空字符串,而空字符串中只有一个\0
:_str(new char[strlen(tmp)+1])//多开一个空间是为了存\0
,_size(strlen(tmp))
,_capacity(strlen(tmp)+1)
{
strcpy(_str, tmp);
}
拷贝构造函数实现
虽然我们不显式定义拷贝构造函数,编译器也会帮我们默认生成一个默认的拷贝构造函数,但是这个拷贝构造函数是一个浅拷贝,浅拷贝在析构时会出现问题,原因可以看下面这篇博客,所以这里我们需要字节手动实现一个拷贝构造函数,来实现深拷贝。
【C++】类与对象 (中篇)(6个默认成员函数)-CSDN博客
//拷贝构造函数
//string s2(s1);
string(const string& tmp)
:_str(new char[tmp.size()+1])//size函数是获取字符串长度,在后面实现
,_size(tmp._size)
,_capacity(tmp._capacity)
{
strcpy(_str, tmp._str);
}
2.析构函数的实现
//析构函数
~string()
{
delete[] _str;//释放_str指向的空间
_str = nullptr;//把_str指针置成nullptr
_size = _capacity = 0;
}
3.赋值=重载
赋值=重载同样需要注意深浅拷贝的问题。
//赋值=重载
//string s1("hello world");
//s1=s2;
string& operator=(const string& tmp)
{
if (this != &tmp)//防止自己给自己赋值
{
delete[] _str;//先释放原来的字符串
char* ptr = new char[tmp.size() + 1];//开一块新空间给_str
strcpy(ptr, tmp._str);//将tmp的内容拷贝给ptr
_str = ptr;
_size = tmp._size;
_capacity = tmp._capacity;
}
return *this;
}
三.Iterators的实现
迭代器可以分为四种:正向的,反向的,const的,非const的,在这里先不讨论反向迭代器
对于string类来说,迭代器的底层其实就是一个char类型的指针,所以string的迭代器可以这样实现。
//对char* 重命名为iterator迭代器
typedef char* iterator;
typedef const char* const_iterator;
//begin迭代器
iterator begin()
{
return _str;//返回第一个字符的指针
}
//end迭代器
iterator end()
{
return _str + size();//返回\0位置的指针
}
//const begin迭代器
const_iterator begin() const
{
return _str;
}
//const end迭代器
const_iterator end() const
{
return _str + size();
}
四.capacity的实现
capacity有以上这么多个函数,我们都来把常用的实现一下。
1.size、length和capacity的函数实现
其实size和length本质上是同一个功能,都是获取字符串的长度,只不过函数名不同而已,所以我们在这里只实现size。同时,capacity是去获取字符串的容量。
//返回string的字符个数
size_t size() const
{
return _size;
}
//返回string的容量
size_t capacity() const
{
return _capacity;
}
2. resize和reserve的实现
resize是将字符串大小调整到n个字符的长度, reserve是调整string的容量大小。
reserve的实现
在这里,我们先实现reserve函数,这有助于我们在实现resize时,去复用reserve。
//调整string的容量
void reserve(const size_t n = 0)
{
if (n > size())//如果要调整到的容量大于原来的容量,才进行调整
{
char* tmp = new char[n + 1];//预留一个位置存放\0
strcpy(tmp, _str);//将原来的字符串拷贝到新空间中
delete[] _str;//释放原来的空间
_str = tmp;//把新空间_str
_capacity = n;
}
}
resize的实现
resize的实现要分下面这三种情况:
//调整string的size
void resize(const size_t n, const char& ch = '\0')
{
if (n > _size)
{
if (n > _capacity)//如果size大于容量,则需要扩容
{
reserve(n);//扩容到n个容量
}
size_t left = _size;
size_t right = n;
while (left < right)//将ch字符内容赋值到没有内容的位置中
{
_str[left++] = ch;
}
}
//将最后一个位置赋值\0
_str[n] = '\0';
}
3.clear和empty的实现
clear清空string中所有的内容,empty判断string是否为空。
//clear实现
void clear()
{
_str[0] = '\0';//将第一个字符的位置给\0
_size = 0;//并将长度给0
}
//empty实现
bool empty() const
{
if (size() == 0)
return true;
else
return false;
}
五. Element access的实现
operator[]与at的作用时一样的,都是获取第n个位置字符的引用,所有这里只实现operator[]
//重载[]
char& operator[](const size_t pos)
{
assert(pos < size());//如果要返回的位置不存在,则断言
return *(_str + pos);
}
//重载const[]
const char& operator[](const size_t pos) const
{
assert(pos < size());//如果要返回的位置不存在,则断言
return *(_str + pos);
}
注意,重载[]有两个,一个是非const的,可读可写,一个是const的,只能读。
六.Modifiers的实现
在实现这些函数时,我们可以优先实现insert和erase,这样在后面尾插尾删时可以复用insert和erase。
1.insert和erase的实现
//在pos位置插入一个字符串
void insert(const size_t pos, const char* s)
{
assert(pos <= size());//断言 插入的位置要<=size
if ((size() + strlen(s)) > capacity())//判断追加字符串是否需要扩容
{
reserve(size() + strlen(s));
}
//将pos位置后的字符串后挪
memmove(_str + pos + strlen(s), _str + pos, end() - (_str + pos));
//将s字符串插入到pos的位置
memmove(_str + pos, s, strlen(s));
//将最后一个位置赋值\0
*(_str + capacity()) = '\0';
}
//在pos位置插入n个字符
void insert(const size_t pos, size_t n, const char& ch)
{
assert(pos <= size());
if ((size() + n) > capacity())
{
reserve(size() + n);
}
memmove(_str + pos + n, _str + pos, end() - (_str + pos));
memset(_str + pos, ch, n);
*(_str + capacity()) = '\0';
}
//在pos位置删除n个字符
void erase(const size_t pos, size_t len)
{
assert(pos < size());
if (len >= (end() - (_str + pos)))//如果要删除的长度长于pos后面的字符串长度,则直接删到尾
{
*(_str + pos) = '\0';
}
else
{
strcpy(_str + pos, _str + pos + len);
}
}
2.push_back、pop_back和append的实现
这几个函数的实现,可以直接复用insert和erase。
//尾插一个字符
void push_back(const char& ch)
{
insert(size(), 1, ch);
}
//尾删一个字符
void pop_back()
{
assert(size());
erase(size() - 1, 1);
}
//尾插一个字符串
void append(const char* s)
{
insert(size(), s);
}
3.operator+=的实现
operator+=其实就是尾插的意思,这里我直接复用push_back和append就行。
//重载operator+=
string& operator+=(const char* s)
{
append(s);
return *this;
}
string& operator+=(const char& ch)
{
push_back(ch);
return *this;
}
七.String operations的实现
在这里我们只挑最常用的几个来实现。
1.c_str的实现
c_str是返回一个C语言形式的字符串,即一个char类型的指针。
//转换为C形字符串
char* c_str() const
{
return _str;
}
2.find的实现
find函数是一个字符串查找函数,在模拟实现里面,我们可以调用C语言的strstr来实现。
//字符串查找 从第pos个位置开始查找
int find(const char* s, const size_t pos =0) const
{
char* tmp = strstr(_str + pos, s);
if (tmp == nullptr)
{
return -1;
}
else
{
return tmp - _str;
}
}
八.Non-member function overloads的实现
最后是一些字符串比较和重载输入输出的实现。
1.各种比较的实现
bool operator==(const char* s) const
{
int ret = strcmp(_str, s);
if (ret == 0)
return true;
else
return false;
}
bool operator!=(const char* s) const
{
return !(*this == s);
}
bool operator>(const char* s) const
{
int ret = strcmp(_str, s);
if (ret > 0)
return true;
else
return false;
}
bool operator>=(const char* s) const
{
return (*this > s) && (*this == s);
}
bool operator<(const char* s) const
{
return !(*this >= s);
}
bool operator<=(const char* s) const
{
return (*this < s) && (*this == s);
}
2.重载输入和输出
//重载cout<<
ostream& operator<<(ostream& out, string& s)
{
out << s.c_str() << endl;
return out;
}
//重载cin>>
istream& operator>>(istream& in, string& s)
{
//输入之前,需要清空字符串
s.clear();
while (1)//进去循环,一个一个字符的插入
{
char ch;
//cin >> ch;
ch = in.get();//读取一个字符
if (ch == ' ' || ch == '\n')//判断这个字符是否结束
{
break;
}
else
{
s += ch;
}
}
s += '\0';
return in;
}
3.getline的实现
getline也是实现字符串的输入,但是getline和operator>>的区别是,getline可以输入空格,
operator>>不能输入空格。
void getline()
{
//输入之前,需要清空字符串
clear();
while (1)
{
char ch;
//cin >> ch;
ch = cin.get();
if (ch == '\n')
{
break;
}
else
{
(*this) += ch;
}
}
(*this) += '\0';
}
写到这里,我们模拟实现的string类基本上就已经完成了,当然,STL库中的string类不止这么点接口,但是有很多是很相似的,有兴趣的朋友可以继续完善。
九.所有源代码
string.h文件
namespace string_blog
{
class string
{
public:
//对char* 重命名为iterator迭代器
typedef char* iterator;
typedef const char* const_iterator;
public:
//无参与带参构造函数
//string s1;
//string s2("hello world");
string(const char* tmp = "")//缺省值给"",代表一个空字符串,而空字符串中只有一个\0
:_str(new char[strlen(tmp)+1])//多开一个空间是为了存\0
,_size(strlen(tmp))
,_capacity(strlen(tmp))
{
strcpy(_str, tmp);
}
//拷贝构造函数
//string s2(s1);
string(const string& tmp)
:_str(new char[tmp.size()+1])//size()函数在后面实现
,_size(tmp._size)
,_capacity(tmp._capacity)
{
strcpy(_str, tmp._str);
}
//析构函数
~string()
{
delete[] _str;//释放_str指向的空间
_str = nullptr;//把_str指针置成nullptr
_size = _capacity = 0;
}
//赋值=重载
//string s1("hello world");
//s1=s2;
string& operator=(const string& tmp)
{
if (this != &tmp)//防止自己给自己赋值
{
delete[] _str;//先释放原来的字符串
char* ptr = new char[tmp.size() + 1];//开一块新空间给_str
strcpy(ptr, tmp._str);//将tmp的内容拷贝给ptr
_str = ptr;
_size = tmp._size;
_capacity = tmp._capacity;
}
return *this;
}
//begin迭代器
iterator begin()
{
return _str;//返回第一个字符的指针
}
//end迭代器
iterator end()
{
return _str + size();//返回\0位置的指针
}
//const begin迭代器
const_iterator begin() const
{
return _str;
}
//const end迭代器
const_iterator end() const
{
return _str + size();
}
//返回string的字符个数
size_t size() const
{
return _size;
}
//返回string的容量
size_t capacity() const
{
return _capacity;
}
//调整string的容量
void reserve(const size_t n = 0)
{
if (n > size())//如果要调整到的容量大于原来的容量,才进行调整
{
char* tmp = new char[n + 1];//预留一个位置存放\0
strcpy(tmp, _str);//将原来的字符串拷贝到新空间中
delete[] _str;//释放原来的空间
_str = tmp;//把新空间_str
_capacity = n;
}
}
//调整string的size
void resize(const size_t n, const char& ch = '\0')
{
if (n > _size)
{
if (n > _capacity)//如果size大于容量,则需要扩容
{
reserve(n);//扩容到n个容量
}
size_t left = _size;
size_t right = n;
while (left < right)//将ch字符内容赋值到没有内容的位置中
{
_str[left++] = ch;
}
}
//将最后一个位置赋值\0
*_str = '\0';
}
//clear实现
void clear()
{
_str[0] = '\0';//将第一个字符的位置给\0
_size = 0;//并将长度给0
}
//empty实现
bool empty() const
{
if (size() == 0)
return true;
else
return false;
}
//重载[]
char& operator[](const size_t pos)
{
assert(pos < size());//如果要返回的位置不存在,则断言
return *(_str + pos);
}
//重载const[]
const char& operator[](const size_t pos) const
{
assert(pos < size());//如果要返回的位置不存在,则断言
return *(_str + pos);
}
//尾插一个字符
void push_back(const char& ch)
{
insert(size(), 1, ch);
}
//尾删一个字符
void pop_back()
{
assert(size());
erase(size() - 1, 1);
}
//尾插一个字符串
void append(const char* s)
{
insert(size(), s);
}
//重载operator+=
string& operator+=(const char* s)
{
append(s);
return *this;
}
string& operator+=(const char& ch)
{
push_back(ch);
return *this;
}
//在pos位置插入一个字符串
void insert(const size_t pos, const char* s)
{
assert(pos <= size());//断言 插入的位置要<=size
if ((size() + strlen(s)) > capacity())//判断追加字符串是否需要扩容
{
reserve(size() + strlen(s));
}
//将pos位置后的字符串后挪
memmove(_str + pos + strlen(s), _str + pos, end() - (_str + pos));
//将s字符串插入到pos的位置
memmove(_str + pos, s, strlen(s));
//将最后一个位置赋值\0
*(_str + capacity()) = '\0';
_size = size() + strlen(s);//调整新_size值
}
//在pos位置插入n个字符
void insert(const size_t pos, size_t n, const char& ch)
{
assert(pos <= size());
if ((size() + n) > capacity())
{
reserve(size() + n);
}
memmove(_str + pos + n, _str + pos, end() - (_str + pos));
memset(_str + pos, ch, n);
*(_str + capacity()) = '\0';
_size = _size + n;
}
//在pos位置删除n个字符
void erase(const size_t pos, size_t len)
{
assert(pos < size());
if (len >= (end() - (_str + pos)))//如果要删除的长度长于pos后面的字符串长度,则直接删到尾
{
*(_str + pos) = '\0';
}
else
{
strcpy(_str + pos, _str + pos + len);
}
_size = _size - len;
}
//字符串查找 从第pos个位置开始查找
int find(const char* s, const size_t pos =0) const
{
char* tmp = strstr(_str + pos, s);
if (tmp == nullptr)
{
return -1;
}
else
{
return tmp - _str;
}
}
//转换为C形字符串
char* c_str() const
{
return _str;
}
bool operator==(const char* s) const
{
int ret = strcmp(_str, s);
if (ret == 0)
return true;
else
return false;
}
bool operator!=(const char* s) const
{
return !(*this == s);
}
bool operator>(const char* s) const
{
int ret = strcmp(_str, s);
if (ret > 0)
return true;
else
return false;
}
bool operator>=(const char* s) const
{
return (*this > s) && (*this == s);
}
bool operator<(const char* s) const
{
return !(*this >= s);
}
bool operator<=(const char* s) const
{
return (*this < s) && (*this == s);
}
void getline()
{
//输入之前,需要清空字符串
clear();
while (1)
{
char ch;
//cin >> ch;
ch = cin.get();
if (ch == '\n')
{
break;
}
else
{
(*this) += ch;
}
}
(*this) += '\0';
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
//重载cout<<
ostream& operator<<(ostream& out, string& s)
{
out << s.c_str() << endl;
return out;
}
//重载cin>>
istream& operator>>(istream& in, string& s)
{
//输入之前,需要清空字符串
s.clear();
while (1)
{
char ch;
//cin >> ch;
ch = in.get();
if (ch == ' ' || ch == '\n')
{
break;
}
else
{
s += ch;
}
}
s += '\0';
return in;
}
//测试构造函数 和 赋值=重载
void Test1()
{
string s1;
string s2("hello world");
string s3(s2);
s1 = s3;
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
cout << s3.c_str() << endl;
}
//测试迭代器
void Test2()
{
string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
//测试
void Test3()
{
string s1("hello world");
s1.clear();
s1.resize(10, 'b');
cout << s1.c_str() << endl;
cout << s1.empty() << endl;
s1.clear();
cout << s1.c_str() << endl;
cout << s1.empty() << endl;
}
//测试operator[]
void Test4()
{
string s1("hello world");
for (int i = 0; i < s1.size(); i++)
{
s1[i] -= 1;
}
cout << s1.c_str() << endl;
}
//测试insert
void Test5()
{
string s1("hello world");
s1.getline();
cout << s1 << endl;
}
}
string.c文件
#include"string.h"
using namespace string_blog;
int main()
{
string_blog::Test1();
string_blog::Test2();
string_blog::Test3();
string_blog::Test4();
string_blog::Test5();
return 0;
}
十.拷贝构造和赋值=重载的现代写法
在做完上面的工作后,string类的模拟实现也基本完成了,而现在在补充一下拷贝构造和赋值=重载的现代写法。
什么是现代写法?
在上面的写法中,是比较常规的写法,但是,在现在,我们可以这样写。
1.拷贝构造现代写法
void Swap(string& s)//交换函数,交换两个string类对象
{
swap(_str, s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);
}
//拷贝构造现代写法
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);//用s的字符串去构造tmp
Swap(tmp);//用tmp和this进行交换
}
2.赋值=重载的现代写法
//赋值=重载的现代写法
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);//用s去构造一个临时对象
Swap(tmp);//交换两个string对象
}
return *this;
}