目录
一、成员函数
1.1 构造函数
1.1.1 无参构造
1.1.2 传参构造
1.1.3 优化
1.2 析构函数
1.3 拷贝构造函数
1.4 赋值运算符重载
二、容量成员
2.1 size 函数
2.2 capacity 函数
2.3 reserve 函数
2.3 resize 函数
2.4 clear 函数
三、元素访问成员
3.1 [] 的重载
四、修饰符成员
4.1 push_back 函数
4.2 append 函数
4.3 += 的重载
4.4 insert 函数
4.5 erase 函数
4.6 swap 函数
五、迭代器成员
5.1 begin 函数
5.2 end 函数
六、字符串操作成员
6.1 find 函数
6.2 substr 函数
七、非成员函数重载
7.1 << 的重载
7.2 << 的重载
由于历史遗留问题, string 早于 STL 出现,所以 string 并不能属于 STL 库,但是由于其特性和 STL 中的其他容器类似,所以我把它当作 STL 的成员之一。
首先先创建一个 .h 头文件 和 .cpp 源文件:
在 .h 文件中使用命名空间
一、成员函数
1.1 构造函数
1.1.1 无参构造
my_string()
{
_str = nullptr;
_size = 0;
_capacity = 0;
}
1.1.2 传参构造
传参构造还可以像无参构造这样直接赋值吗?
my_string(const char* str)
{
_str = str;
_size = strlen(str);
_capacity = _size + 1;
}
很显然是不行的,如下,如果我们一开始就传入字符串,字符串是 const char* 类型的,明显不可以用 char* 类型的 _str 接收,那我们应该怎么办?写一个 const char* 类型的类显然是不可取的。
下面来看我们的解决方法:
my_string(const char* str)
{
_str = new char[strlen(str)];
_str = strcpy(_str, str);
_size = strlen(str);
_capacity = strlen(str);
}
1.1.3 优化
我们可以通过给缺省值完成对无参构造和传参构造的优化:
my_string(const char* str = "")
{
_str = new char[strlen(str) + 1];
strcpy(_str, str);
_size = strlen(str);
_capacity = _size;
}
1.2 析构函数
~my_string()
{
_str = nullptr;
_size = 0;
_capacity = 0;
}
1.3 拷贝构造函数
my_string(const my_string& s)
{
//注意这里的s._size + 1
_str = new char[s._size + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
1.4 赋值运算符重载
赋值运算符重载需要我们使用深拷贝的方式进行赋值,这就需要我们重新开空间,而且我们要事先完成对原类中 _str 的清空。
my_string operator=(const my_string& s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
二、容量成员
2.1 size 函数
size_t size() const //加const可以扩大适用范围
{
return _size;
}
2.2 capacity 函数
size_t capacity() const
{
return _capacity;
}
2.3 reserve 函数
reserve 的主要目的是为了优化性能。如果你知道将来会要在字符串后附加大量字符,通过 reserve 函数提前分配足够的内存可以避免多次的内存重新分配,从而提高性能。
也就是说 reserve 函数只是让 _str 指向了更大的内存空间,并不改变字符串本身的值。
void reserve(size_t n)
{
if (n > _capacity)
{
//为'/0'预留空间
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
2.3 resize 函数
void resize(size_t n, char ch = '\0')
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
}
2.4 clear 函数
void clear()
{
_size = 0;
_str[_size] = '\0';
}
三、元素访问成员
3.1 [] 的重载
我们首先要清楚的是,我们的 _str 定义的数组是 new 出来的,它的数据存放在堆中,析构函数调用前一直存在,所以我们可以直接返回它的引用。
而且我们可以使用 assert 有效地检查是否越界!
char& operator[](size_t pos)
{
assert(pos > _size);
return _str[pos];
}
但是如果我们创建了一个 const 修饰的 string 对象,我们上面的函数就不能用了,所以我们还要再写一个使用 const 修饰的函数
const char& operator[](int& pos) const
{
assert(pos > _size);
return _str[pos];
}
其中,第一个 const 防止我们修改返回的字符,而第二个 const 保证了函数不会修改其所属的对象。
四、修饰符成员
4.1 push_back 函数
push_back 函数要考虑到扩容问题,由于每个编译器的扩容机制不同,这里默认二倍扩容,且要考虑到 capacity 为 0 时的情况!
void push_back(char ch)
{
if (_size == _capacity)//二倍扩容
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
_size += 1;
_str[_size] = '\0';
}
4.2 append 函数
void append(const char* s)
{
int len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, s);
_size += len;
}
4.3 += 的重载
my_string& operator+=(char ch)
{
push_back(ch);
return *this;
}
my_string& operator+=(const char* s)
{
append(s);
return *this;
}
4.4 insert 函数
insert 函数涉及到数据挪动的问题,如果我们在 pos 处插入 ch 字符,那么从最后一个元素开始直到 pos 位置的元素都要依次挪到后一位后,再将 ch 插入。
//插入普通字符
void insert(size_t pos, char ch)
{
assert(pos <= _size)
if (_size <= _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
for (size_t i = _size; i > pos; i--)
{
_str[i + 1] = _str[i];
}
_str[pos] = ch;
_size += 1;
}
//插入字符串
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size <= _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
for (size_t i = _size; i > pos; i--)
{
_str[i + len] = _str[i];
}
strncpy(_str + pos, str, len);
_size += len;
}
4.5 erase 函数
void erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
if (pos + npos >= _size || len == npos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
4.6 swap 函数
void swap(my_string& s)
{
// 交换 _str,_size,_capacity
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
五、迭代器成员
如果要实现遍历元素,我们就必不可少的要提供迭代器。
为了向官版靠拢,我们也 typedef 一下
5.1 begin 函数
typedef char* iterator;
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
iterator begin()
{
return _str;
}
5.2 end 函数
const_iterator end() const
{
return _str + _size;
}
iterator end()
{
return _str + _size;
}
六、字符串操作成员
6.1 find 函数
//查找字符
size_t find(char ch, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
//查找字符串
size_t find(const char* sub, size_t pos = 0) const
{
assert(pos < _size);
const char* p = strstr(_str + pos, sub);
if (p)
{
return p - _str;
}
else
{
return npos;
}
6.2 substr 函数
my_string substr(size_t pos = 0, size_t len = npos) const
{
my_string sub;
if (len >= _size - pos)
{
for (size_t i = pos; i < _size; i++)
{
sub += _str[i];
}
}
else
{
for (size_t i = pos; i < pos + len; i++)
{
sub += _str[i];
}
}
return sub;
}
七、非成员函数重载
7.1 << 的重载
重载 ostream 的 << 运算符一般不能在类内部完成,原因是 << 运算符的左侧是一个 ostream 对象,而不是你正在定义的类的对象。在类内部定义的成员函数,其隐含的 this 指针总是指向类的一个对象,这就是为什么我们通常需要在类外部,但在同一个命名空间内,定义这样的函数。
不过,可以在类内部声明这个函数为友元函数,然后在类外部提供定义。
friend std::ostream& operator<<(std::ostream& out, const my_string& s);
函数定义:
std::ostream& operator<<(std::ostream& out, const my_string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
7.2 << 的重载
我们可以直接使用 += ,将输入的字符新增到原字符串中:
std::istream& operator>>(std::istream& in, my_string& s)
{
s.clear();
char ch;
in >> ch;
while (ch != '\n' && ch != ' ')
{
s += ch;
ch = in.get();
}
return in;
}
但是我们的 in 在这里是 cin 的别名,而 cin 不支持对 [空格] 和 [换行] 的读取,它们被认为是分隔的标志,所以默认不读取,这样的话,我们的 while 循环条件就不成立了,会陷入死循环的境地!
然后我们去到 istream 的类中,观察到其还有一个 get 函数成员: get 函数有多种形式,但其基本用法是从输入流中读取一个字符,包括空白字符(如空格、制表符和换行符)。
注意:仅读取单个字符时包含 '\0' 、 ' ' 与 '\n' !
std::istream& operator>>(std::istream& in, my_string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != '\n' && ch != ' ')
{
s += ch;
ch = in.get();
}
return in;
}
但是有前辈发明了更好的写法,他们模拟了一个缓冲区,当缓冲区满了才会把数据刷新
std::istream& operator>>(std::istream& in, my_string& s)
{
s.clear();
char ch;
ch = in.get();
char buff[128];
size_t i = 0;
while (ch != '\n' && ch != ' ')
{
buff[i++] = ch;
if (i == 127)
{
buff[i] = '\0';
s += buff;
i = 0;
}
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}