在之前的文章中C++ STL-->String类详解,介绍了一下STL中string类的基本使用 这篇文章将模拟实现string类的常用函数
string模拟实现
模拟实现的目的就是为了更好的使用STL
模拟实现string类函数接口
cpp
namespace ding
{
class string
{
public:
string(const char* str = "");
string(const string& s);
string& operator=(const string& s);
~string();
//=======================================================
// iterator
typedef char* iterator;
iterator begin();
iterator end();
//=======================================================
// modify
void push_back(char c);
void append(const char* str);
string& operator+=(char c);
string& operator+=(const char* str);
void clear();
void swap(string& s);
const char* c_str()const;
//=======================================================
// capacity
size_t size()const;
size_t capacity()const;
bool empty()const;
void resize(size_t n, char c = '\0');
void reserve(size_t n);
//=======================================================
// access
char& operator[](size_t index);
const char& operator[](size_t index)const;
//=======================================================
//relational operators
bool operator<(const string& s);
bool operator<=(const string& s);
bool operator>(const string& s);
bool operator>=(const string& s);
bool operator==(const string& s);
bool operator!=(const string& s);
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const;
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const;
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c);
string& insert(size_t pos, const char* str);
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len);
private:
char* _str; //存储字符串
size_t _capacity; //记录字符串当前的容量
size_t _size; //记录字符串当前的有效长度
};
ostream& operator<<(ostream& _cout, const ding::string& s);
stream& operator>>(istream& _cin, ding::string& s);
};
为了解决和标准库中string命名发生冲突,这里使用自己的命名空间解决
默认成员函数
构造函数
无参构造函数:构造空字符串 空字符串里面是有一个\0的 并不是什么都没有
cpp
string::string()
:_str(new char [1])
,_capacity(0)
,_size(0)
{
_str[0] = '\0';
}
带参构造函数
cpp
string::string(const char* str)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];//多开辟一块空间存放\0
strcpy(_str, str);
}
省值可以将无参的和带参的构造函数写成一个函数
函数声明:string(const char* str = "");
注意 这里缺省值只能给函数声明,定义的时候不能再使用缺省值
实现:
cpp
string::string(const char* str )
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
析构函数
这里的构造函数用new从堆区申请空间,所以析构函数需要自己实现去主动释放资源,编译器默认生成的无法满足需求
cpp
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
拷贝构造函数
这里如果使用编译器默认生成的拷贝构造函数去初始化对象 在对象生命周期结束时 c++编译器自动调用析构函数会出错 如下图
原因 :这里是浅拷贝 s1和s2共用同一块地址空间,在对象s1,s2生命周期结束时,调用析构函数。对s1进行析构时,已经将资源释放掉了,而s2还不知道资源已经被释放,再次对其资源进行释放时,就会出现访问违规。 关于深浅拷贝
浅拷贝:编译器只会将对象中的值拷贝过来。拷贝的对象和源对象共用同一块地址空间,对其中一个对象的修改会影响到另一个对象
深拷贝:拷贝的对象与源对象使用不同的地址空间,二者互不干扰。
在这里,我们应该实现的是深拷贝,编译器默认生成的不能完成。需要自己实现一个拷贝构造函数 代码实现:
cpp
string::string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
赋值运算符重载函数
赋值时,同样应该采用深拷贝的方式进行赋值
首先将原来的空间释放掉,然后申请新空间进行拷贝
注意:如果自己给自己赋值,应该进行判断。否则将自己释放后,在进行赋值,会出错
cpp
string& string::operator=(const string& s)
{
if(this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
这里申请临时对象是为了防止申请空间失败,对原来的数据造成破坏
迭代器相关函数(iterator)
迭代器在string类中的实现很简单 就是给字符指针取了个高大上的名字
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()
返回字符串结束位置的地址 即\0位置的地址
cpp
string::iterator string::end()
{
return _str + _size;
}
string::const_iterator string::end()const
{
return _str + _size;
}
利用迭代器遍历字符串 :
在底层 用迭代器遍历字符串,实际上就是用指针的方式去遍历
cpp
string s1("Hello world");
auto it = s1.begin();
while (it != s1.end())
{
cout << *it << endl;
it++;
}
capacity(容量和大小函数)
size函数
返回字符串的元素个数不包含\0;
cpp
size_t string::size()const
{
return _size;
}
capacity函数
返回字符串的有效空间大小
cpp
size_t string::capacity()const
{
return _capacity;
}
reserve函数
扩容
reserve函数规则:
- 大于当前对象的capacity时,将capacity扩大到n或者大于n
- 当n小于当前对象的capacity时,什么也不做。(不会缩容)
cpp
void string::reserve(size_t n)
{
_capacity = n + _capacity;
char* tmp = new char[_capacity];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
//_size 不用处理
//_size = strlen(_str);
}
resize函数
扩容+初始化
resize函数的规则:
- 当n大于当前对象的size时 将size扩大到n 若未指定字符 默认为'\0'
- 当n小于当前对象的size时 将size缩小到n
cpp
void string::resize(size_t n, char c = '\0')
{
if (n <= _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
if (n > _capacity)
{
reserve(n);
}
//加数据
size_t i = _size;
while (i < n)
{
_str[_size] = c;
i++;
}
_size = n;
_str[_size] = '\0';
}
}
modfiy(修改字符串函数)
push_back(尾插)
在字符串结尾插入一个字符
插入之前首先要考虑一下容量是否足够 不够需要扩容
cpp
void string::push_back(char c)
{
//扩容
if (_size >= _capacity)
{
_capacity = _capacity == 0 ? 2 : _capacity;//处理空串
reserve(_capacity * 2);
}
//push_back
_str[_size] = c;
++_size;
//处理\0
_str[_size] = '\0';
}
注意:插入一个字符后需要自己处理一下字符串结束标志\0
append(尾插一个字符串)
在字符串尾部插入一个字符串 在插入之前还是首先要检查一下容量是否足够
cpp
void string::append(const char* str)
{
//扩容
size_t len = strlen(str);
if (_size + len >= _capacity)
{
_capacity = _capacity == 0 ? len : _capacity;//处理空串
reserve(_capacity + len);
}
strcpy(_str+_size, str);
_size += len;
}
注意:这里使用strcat也可以完成。但是strcat需要遍历找尾,效率太低,使用strcpy即可。strcpy会处理字符串结束标志\0,不用自己手动处理
+=赋值运算符重载
+=运算符重载实现很简单了 直接函数复用即可
cpp
string& string::operator+=(char c)
{
push_back(c);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
string& string::operator+= (const string& str)
{
append(str.c_str());
return *this;
}
这里的c_str()函数就体现出价值了。append函数的形参是const char* 类型 而str是string类型 直接传参会导致参数类型不匹配错误。使用c_str转换一下类型即可
cpp
const char* string::c_str()const
{
return _str;
}
注意:这里的c_str()实现一个const成员函数即可,cosnt和非const对象都可以调用。如果只实现非cosnt版本的话,const对象去调用c_str函数则会出错。
clear
清空字符串中的有效字符,不会改变底层空间的大小(不会影响容量的大小)
cpp
void string::clear()
{
_size = 0;
_str[0] = '\0';
}
find
默认从0(下标)位置开始查找,返回第一个匹配项的下标位置,找不到返回npos
npos :static const size_t npos = -1;
cpp
size_t string::find(char c, size_t pos) const
{
for (size_t i = 0; i < _size; i++)
{
if (_str[i] == c)
{
return i;
}
}
//没找到
return npos;
}
cpp
size_t string::find(const char* s, size_t pos) const
{
const char* ret = strstr(_str + pos, s);
if (ret)
{
return ret - _str;
}
else
{
return npos;
}
}
insert
在指定位置插入n个字符
cpp
string& string::insert(size_t pos, size_t n,char c)
{
//扩容
if (_size + n >= _capacity)
{
reserve(_capacity+n);
}
//挪动数据
int end = _size;//会把\0挪动
while (end >= (int)pos)
{
_str[end + n ] = _str[end];
--end;
}
while (n--)
{
_str[pos] = c;
pos++;
}
_size += n;
return *this;
}
在指定位置插入字符串
cpp
string& string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
//扩容
if (_size + len >= _capacity)
{
reserve(_capacity + len);
}
//挪动数据
int end = _size;
while (end >= (int)pos)
{
_str[end + len] = _str[end];
--end;
}
strncpy(_str + pos, str, len);//不会处理\0
return *this;
}
pos可以通过find函数查找指定 比如
cpp
string s2("Hello");
size_t pos2 = s2.find('e');
s2.insert(pos2, 5, 'x');//在e的前面插入5个x
cout << s2.c_str() << endl;
erase
删除指定位置指定长度的字符(不指定长度全部删除)
- 当长度大于字符串长度时,直接在pos位置赋值\0 即可
- 其他情况挪动数据删除即可。
cpp
string& string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (pos+len >= _size || len == npos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
访问字符串相关函数(access)
[]运算符重载
[]运算符重载函数目的是为了能让string对象像字符数组一样,使用[]+下标的方式进行访问 通过[]+下标,可以访问对应下标位置的元素。模拟实现时把对应位置的引用返回即可 这样可读可写
cpp
char& string::operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& string::operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
front && back函数
获取字符串有效元素第一个元素和最后一个元素(\0之前的)
cpp
char& string::front()
{
return _str[0];
}
const char& string::front() const
{
return _str[0];
}
char& string::back()
{
return _str[_size - 1];
}
const char& string::back() const
{
return _str[_size - 1];
}
关系运算符重载函数(relational operators)
关系运算符有 == 、!=、 <、 <=、 >、 >= 六个
实现其中一个==和> 或者< 函数复用一下,其他的就全部能实现了
cpp
bool string::operator<(const string& s)
{
return strcmp(_str, s.c_str()) < 0;
}
bool string::operator==(const string& s)
{
return strcmp(_str, s.c_str()) == 0;
}
bool string::operator<=(const string& s)
{
return *this < s || *this == s;
}
bool string::operator>(const string& s)
{
return !(*this <= s);
}
bool string::operator>=(const string& s)
{
return *this > s || *this == s;
}
bool string::operator!=(const string& s)
{
return !(*this == s);
}
>>和<<的重载
<<重载
<< 的重载是为了让string对象 像内置类型一样,直接使用<<进行输出
重载时就是遍历一遍string对象,string对象存的是char类型,在使用<<输出即可
cpp
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
注意: cout的类型是ostream类类型;返回ostream的引用是为了连续输出
>>重载
<<的重载是为了让string对象可以像内置类型一样,直接使用cin进行输入。
输入时应该注意,先清空对象中的数据在进行输入。
cpp
//>>运算符的重载
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
while (ch != ' '&&ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
注意 这里不能使用>>直接输入char类型 要用cin对象的成员函数get()
因为>> 会将空格或者\n当成默认的分隔符 ,ch不会向缓冲区中拿空格或者\n。 导致循环一直无法终止,显然不符合要求。C++ cin对象有一个成员函数get(),会忽略空格和\n。都会从缓冲区中读取。