目录
1.构造函数
string类的构造函数我采用的是初始化列表和构造函数结合的方式来实现的。
首先思考string类中有哪些元素?字符串、_size保存有效数据个数、_capacity保存可存字符的空间大小。
接下来想如何初始化他们?字符串内容是由外界给,_size表示字符串的长度可以用strlen,_capacity表示空间大小用size赋值即可。
最后new一块空间,然后将字符串内容拷贝。
注意一些细节,如无实参的构造要给缺省参数,_size和_capacity都表示不包含\0的字符个数。
//构造
string(const char* str = "")
: _size(strlen(str))
{
_capacity = _size;
//开辟_size+1个空间为'\0'预留空间
_str = new char[_size + 1];
//拷贝过去_size+1个空间是因为'\0'也要拷贝
memcpy(_str, str, _size+1);
}
2.析构函数
析构函数的实现就是看着构造来。构造函数new了空间,析构函数就要delete空间,然后将指针置空,_size和_capacity置0。
//析构
~string()
{
delete []_str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
3.遍历
string对象的遍历有三种方法,分别是方括号[]下标遍历、迭代器遍历、范围for遍历。
[]下标遍历和数组遍历类似。重载[],在重载函数内部返回下标对应的值即可;拿下标,从0到_size遍历即可(前提是需要一个size函数来返回_size的大小)。
迭代器遍历。什么是迭代器?在C++中,迭代器(Iterator) 是一种类似指针的对象,用于遍历和访问容器(如 std::vector
、std::string
、std::list
等)中的元素。它提供了一种统一的方式来访问容器内的数据,无论容器的底层实现如何(数组、链表等)。那么实现一个迭代器我们暂且需要返回一个地方的指针即可。
范围for的底层也是迭代器,这里不做深入讲解。
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];
}
//返回类型和成员函数都需要指定类域
string::iterator string::begin()
{
return _str;
}
string::const_iterator string::begin() const
{
return _str;
}
string::iterator string::end()
{
return _str + _size;
}
string::const_iterator string::end() const
{
return _str + _size;
}
我们来测试一下
void test02()
{
//[]
string s1("hello world");
for (int i = 0; i < s1.size(); i++)
cout << s1[i];
cout << endl;
//不是使用引用返回,则接受的是一个临时变量具有常性
//传引用返回才可以修改
s1[0]++;
cout << s1[0] << endl;
//s3不能被修改
const string s3("lzk lucky");
//迭代器
string s2("wit lzk");
string::const_iterator it = s2.begin();
//(*it)++; 无法修改
//const iterator 表示it这个迭代器无法被修改
//const_iterator 表示(*it)无法被修改
//范围for
for (auto ch : s2)
{
cout << ch ;
}
cout << endl;
}
4.插入删除查找
插入主要有尾插push_back(尾插入一个字符)、append(尾插入一个字符串)、+=(插入字符或字符串),指定位置插入insert(插入字符或字符串)。删除主要有erase。查找主要有find。
插入
插入字符
首先在插入之前需要检查字符串是否满了,满了就需要调整空间大小reserve,reserve可以指定扩容大小(这里我实现二倍扩容),实现步骤为开空间、拷贝原数据、改变_str、_capacity。
再者扩容完毕就需要插入,从尾部下标为_size的位置插入字符,再改变_size,
最后要把_size位置置为\0;
插入字符串
首先需要判断剩余空间和字符串长度的大小关系,前者大就无需扩容,后者大就需要扩容,此时需要再次判断原字符串长度+插入字符串长度和两倍原字符串长度的大小关系,谁大就按谁扩容。
最后扩容完毕将新字符串拷贝到原字符串后面即可。 ·
注意:拷贝时要多拷贝一个空间(将\0也拷贝过来),拷贝完毕要改变_size的大小。
void string::reserve(size_t n)
{
//如果原指针指向的空间有数据,那么直接开辟空间一定是错的
//_str = (char*)malloc(sizeof(int) * (n + 1));
//_capacity = n;
//不推荐使用reserve来缩小空间
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
memcpy(tmp, _str, _size + 1);
}
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void string::push_back(char ch)
{
//如果满了要扩容
/*if (_size == _capacity)
{
int capacity = _capacity == 0 ? 4 : 2 * _capacity;
char* str = (char*)realloc(_str, capacity + 1);
if (str == nullptr)
{
perror("realloc");
exit(-1);
}
_str = str;
_capacity = capacity;
}*/
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
void string::append(const char* str)
{
//判断剩余空间是否足够
size_t t = strlen(str);
if (_capacity - _size < t)
{
size_t capacity = t + _size > 2*_capacity ? _size + t: 2* _capacity;
/*char* str = (char*)realloc(_str, capacity+1);
if (str == nullptr)
{
perror("realloc");
exit(-1);
}
_str = str;
_capacity = capacity;*/
reserve(capacity);
}
//memmove,strcp,memcpy的区别???????????????????
//memmove(_str, str, t);
//t + 1 是为了把'\0'也拷贝过来
memcpy(_str + _size,str, t + 1);
_size = _size + t;
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
指定位置插入字符有些地方需要注意
void string::insert(size_t pos, char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
//'\0'也要往后移动
for (size_t i = _size; i >= pos; i--)
{
_str[i + 1] = _str[i];
}
_str[pos] = ch;
_size++;
}
上面这段代码在pos==0,i==0时,也会进入循坏,此后i--变为-1,而i是无符号整型size_t,此时-1会被隐式类型转换成一个很大的正数,导致程序死循环。可以将i的指向改变来解决。
void string::insert(size_t pos, char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
//'\0'也要往后移动
for (size_t i = _size+1; i > pos; i--)
{
_str[i] = _str[i - 1];
}
_str[pos] = ch;
_size++;
}
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t t = strlen(str);
if (_capacity - _size < t)
{
size_t capacity = t + _size > 2 * _capacity ? _size + t : 2 * _capacity;
reserve(capacity);
}
size_t end = _size + t;
for (end; end >= pos + t; end--)
{
_str[end] = _str[end - t];
}
memcpy(_str + pos, str, t);
_size = _size + t;
}
删除
删除实际上不是将数据消除了,而是将后面的数据和\0提前。
我将声明和定义分离到了两个文件中,这里可以进行特殊处理,直接在声明时将npos赋值为-1(在程序中转换成了一个很大的整数),如果用户不传第二个参数,就使用缺省值,默认将pos后面的数据全部删除。所以在实现erase时可以判断一下len的大小,再进行后续操作。
自己实现时想到的是和上面插入相似的逻辑,实际上可以直接进行数据拷贝,不过插入应该也可以直接拷贝,找准地址即可。无论插入和删除最后都别忘了改变_size的大小。
//指定位置删除
static const size_t npos;
void erase(size_t pos, size_t len = npos);
//缺省值不能在声明和定义同时给
const size_t string::npos = -1;
void string::erase(size_t pos, size_t len )
{
assert(pos < _size - 1);
if (len == npos || len >= _size - pos )
{
_size = pos;
_str[pos] = '\0';
}
else
{
/*for (pos; pos + len < _size; pos++)
{
_str[pos] = _str[pos + len];
}
_str[pos] = '\0';*/
//把'\0'也移动了
memmove(_str + pos, _str + pos + len, _size + 1 - (pos + len));
_size -= len;
}
}
查找
在字符串中查找字符第一次出现的位置,并返回其下标,遍历即可。
查找一个字符串,首先借助函数strstr返回的是目标字符串在原字符串中出现的地址。最后地址相减就可以拿到下标了。查找字符或者字符串时不能改变原字符串中的内容,所以要加const。
size_t string::find(char ch, size_t pos) const
{
assert(pos < _size);
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
{
assert(pos < _size);
const char* p = strstr(_str, str);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
5.获取字符串的一部分
substr一般与find函数连用,可以用来解析一段字符串。
注意需要检查传值过来的len的大小,len最大只能是(_size-pos)。
string string::substr(size_t pos, size_t len) const
{
assert(pos < _size);
if (len > _size - pos)
{
len = _size - pos;
}
string ret;
ret.reserve(len);
for (size_t i = 0; i < len; i++)
{
ret += _str[i + pos];
}
return ret;
}
6.字符串间的比较
字符串间的比较,不是单纯地比较字符串的长度大小,而是从字符串的第一个字符开始比较字符的ascll码值。几个符号之间有很强的复用关系,所以只需实现<和=即可。
<有两种情况,一是没有遍历完,通过ascll码值比较完毕;
二是其中一个字符串遍历完,ascll码值无法比较出来,再去比较长度。
bool string::operator<(const string& s) const
{
size_t len1 = _size;
size_t len2 = s._size;
size_t i = 0, j = 0;
while (i < len1 && j < len2)
{
if (_str[i] < s._str[j])
{
return true;
}
else if (_str[i] > s._str[j])
{
return false;
}
else
{
++i;
++j;
}
}
if (i == len1 && j < len2)
{
return true;
}
else
{
return false;
}
}
==就更简单了,遍历字符串不相等就false,出循环就两者长度相等就true。
bool string::operator==(const string& s) const
{
size_t len1 = _size;
size_t len2 = s._size;
size_t i = 0, j = 0;
while (i < len1 && j < len2)
{
/*if (_str[i] < s._str[j])
{
return false;
}
else if (_str[i] > s._str[j])
{
return false;
}*/
if (_str[i] != s._str[j])
{
return false;
}
else
{
++i;
++j;
}
}
/*if (len1 == len2)
{
return true;
}*/
return len1 == len2;
}
其他的复用上面两个符号的即可。
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);
}
7.重载流插入<<和流提取>>
重载流插入<<
是为了直接打印字符串对象中的内容。 在外部使用时cout<<s1,cout是osream的一个对象,函数中改名为os;s1改名为s,这里又使用了[]的访问方式,os << s[i]实际上就是输出一个字符。重载时返回对象要为流对象的引用,是为了实现链式访问。
std::ostream& operator<<(std::ostream& os, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
os << s[i];
}
//为了链式调用,返回os
return os;
}
重载流提取>>
是为了直接初始化对象。 如果原字符串中含有内容,则需要先清理原对象中的内容;先将输入流中的内容存在缓冲区buff中可以减少string中的扩容次数,优化性能。重载时返回对象要为流对象的引用,为了链式调用。
std::istream& operator>>(std::istream& is, string& s)
{
//清理字符串中的内容
s.clear();
char buff[300];
size_t t = 0;
//获取输入的字符
char ch = is.get();
while (ch != ' ' && ch != '\n')
{
buff[t++] = ch;
ch = is.get();
if (t == 299)
{
//buff满了就要先给给s,再重新存
buff[t] = '\0';
s += buff;
t = 0;
}
}
if (t > 0)
{
buff[t] = '\0';
s += buff;
}
return is;
}
8.重载swap函数
算法库中的swap函数,本质上实现了三次深拷贝,性能消耗较大,所以string类中有个swap成员函数,只交换指针指向的空间,不会重新开。而且string类中又有个全局的swap函数,重载了算法库中的swap,里面实际上就调用了string类的成员函数。
void string::swap(string& x)
{
std::swap(_str, x._str);
std::swap(_size, x._size);
std::swap(_capacity, x._capacity);
}
为什么 std::swap(string)
要特化?
泛型 std::swap
的默认实现是 拷贝+赋值(效率低),特化后直接调用 swap
成员函数,避免不要的拷贝。 尚且编译器会在调用swap时,会选择最优的方式调用。
编程的世界没有终点,每一次调试都是成长。**愿你的代码少些 Bug,多些优雅!** ✨