目录
1. string 构造函数
2. 析构函数
3. string 的基础函数
4. 迭代器
5. 拷贝构造函数
6. 赋值运算符重载
7. 简便写法(调用swap函数实现)
8. reserve 与 resize?
9. string 之插入数据
10. 流提取、流插入操作符重载
11. find 和 substr?
12. 比较函数重载
接下来我们就来模拟实现一下 string 类的基本操作。
1. string 构造函数
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
① 缺省值为“”,“\0”这样的写法实际上是两个字符,为\0 \0,因为常量字符串会默认加一个\0。
②推荐使用函数体内初始化,在函数体内初始化只用调用一次 strlen 函数,如果使用初始化列表,其中_size、和_capacity 的计算都应经过 strlen 计算,因为初始化列表的顺序是通过成员变量定义的顺序来决定的。
③即使字符串为空,那也至少开辟一个空间,留给\0的空间。
2. 析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
析构函数比较简单,就不多解释了。
3. string 的基础函数
以下是一些经常使用的string中的基础函数,特别注意,const对象只能调用const成员函数。
所以,如果我们有 const 成员变量想获取某个位置的值时,我们要提供一个const型的 []操作符函数重载。
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
4. 迭代器
在 string 中,迭代器不过就是一个类似指针的存在,所以我们直接typedef 一下char型指针即可。
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
5. 拷贝构造函数
即根据所传参数来构建一个相同的对象。
//拷贝构造函数
string(const string& s)
:_str(new char [s._capacity+1])
,_size(s._size)
,_capacity(s._capacity)
{
strcpy(_str, s._str);
}
6. 赋值运算符重载
赋值运算符重载的实现,有以下几个需要注意的点:
- 先检查左右是否为相同的值,避免自己给自己赋值情况。
- 先开辟空间,然后将数据拷贝过去,防止开辟空间失败又丢失了数据。
- 再释放原先的空间,进行参数的替换。
string& operator=(const string& s)
{
if (this!= &s)
{
char* temp= new char[s._capacity + 1];
strcpy(temp,s._str);
delete[] _str;
_str = temp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
7. 简便写法(调用swap函数实现)
拷贝构造函数和赋值运算符重载这两个功能的实现,我们可以采用更为简便的方式来实现。
思路:根据函数参数生成一个临时对象,将该临时对象的数据与this指向的数据进行交换。
//拷贝构造函数
string(const string& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
//构造出一个函数
string temp(s._str);
swap(temp);
}
//赋值运算符重载
string& operator=(const string& s)
{
if (this != &s)
{
string temp(s._str);
swap(temp);
}
return *this;
}
这其中所调用的?swap 函数为全局swap函数,但是全局的 swap 函数会生成一个临时变量,这样便会产生很大的代价。对于string类来说,其交换数据无非是将其中的成员变量_str的指向改变一下而已,完全不需要这么大的代价,所以我们来自己实现一个string类中的?swap 函数。
//编译器会在当前命名空间中寻找,然后再去全局域中找
void swap(string& temp)
{
::swap(_str, temp._str);
::swap(_size, temp._size);
::swap(_capacity, temp._capacity);
}
加上了域作用限定符则表示调用了全局的 swap 函数,而这里的调用,仅仅是交换一些内置类型,并没有将我们编写的string类拷贝生成一份,这便大大提高了运行效率。
赋值运算符重载的简化写法还有优化的地方。
我们知道,函数传参,传的是实参的临时拷贝,那既然函数传参已经生成了临时拷贝,我们便可以利用其自动生成的临时拷贝对象。
//注意,这里是传值传参
string& operator=(string s)
{
swap(s);
return *this;
}
注意了,这种简洁方式采用的传值传参,然后调用我们 string 类中的 swap 函数。
8. reserve 与 resize?
reserve 的实现:
分清楚resize和reserve的区别:
reserve是设置了capacity的值,比如reserve(20),表示该容器最大容量为20,但此时容器内还没有任何对象,也不能通过下标访问。
resize既分配了空间,也创建了对象,可以通过下标访问。当resize的大小
reserve只修改capacity大小,不修改size大小,resize既修改capacity大小,也修改size大小。
void reserve(size_t n)
{
//大于_capacity 才扩容
if (n > _capacity)
{
char* temp = new char[n + 1];
strcpy(temp, _str);
delete[] _str;
_str = temp;
_capacity = n;
}
}
注意:
需要n个空间,则要开辟n+1开空间,留一个给\0
注意插入数据后要在末尾放入\0
resize 的实现:
void resize(size_t n, char ch ='\0')
{
//1.比当前_size大 2.小于等于_size
if (n > _size)
{
//插入数据
reserve(n);
for (size_t i = _size;i<n;i++)
{
_str[i] = ch;
}
_str[n] = '\0';
}
else
{
//删除数据
_str[n] = '\0';
_size = n;
}
}
9. string 之插入数据
① push_back
push_back的实现其实与实现顺序表的尾插相似。
- 先检查大小,然后将数据进行尾插。
- 注意 _capacity==0 的情况出现,要额外检查capacity==0的情况。
- 将 _size 进行++,然后在_size处放入'\0'即可。
void push_back(char ch)
{
//满了就扩容
if (_size == _capacity)
{
//判断一下,因为我们开始给的缺省值为\0,其中capacity为0
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
//插完数据后要再插入一个\0
++_size;
_str[_size] = '\0';
}
② append 的实现
关于 append 的实现:
- 计算传入的字符串大小,根据字符串的大小检查是否开辟空间以及开辟多大空间(防止盲目开辟空间).
- 如果是 _size+len > _capacity ,不理解 > 还是 >= 可以代值进行计算一下。
- 使用 strcpy 拷贝数据,不过是从_str+_size处开始拷贝,将 str 的数据拷贝到 string 类中。
- 将_size += len,表示数据插入完成。
void append(const char* str)
{
//计算需要多大的空间
size_t len = strlen(str);
//满了就扩容
if (_size + len > _capacity)
{
//至少要开辟这么大的空间
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
③ +=运算符重载(字符)
实现了 push_back 和 append 后,我们来实现+=运算符重载,关于+=运算符重载,它比push_back和 append 的优势在于:1. 支持连续+=;2.增强代码可读性。
//字符型调用 push_back
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//字符串类型调用 append
string& operator+=(char* str)
{
append(str);
return *this;
}
④ insert
- 检查pos位置是否大于 _size,大于则直接报错。
- 检查_size 是否与_capacity相等,相等则扩容。
- 将数据向后挪动,然后插入数据。
void insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size ==_capacity)
{
reserve(_capacity==0?4:_capacity*2);
}
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
}
10. 流提取、流插入操作符重载
接下来我们就来实现流插入和流提取操作符的重载,实现这两个操作符重载,可以方便我们打印的插入数据。
①流提取操作符
- 注意要重载为全局函数,这样才能不用使用.(成员访问操作符)既可以打印数据了。
- 不必声明此函数为string类的友元函数,因为此操作符并未访问私有成员变量
- 因为提取操作符不涉及更改数据,所以使用const型函数形式参数,使用[ ]访问数据时,[ ]操作符重载函数必须为 const 型,因为 const 型对象只能调用 const 型成员函数。
- 因为支持连续提取,所以要做 ostream& 作为返回值。
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
② 流插入操作符
- 每次插入数据都要清空当前对象中_str存放的数据。
- 因为cin 会将' ' (空格)或 '\n' 视为分隔符,不会读入其中,所以我们无法通过检测' ' (空格)或 '\n' 来停止数据。这时我们便使用istream中的一个成员函数get()来帮我们获取分隔符。
- 使用一个字符 ch来存储数据,并将其不断插入到类中。
- 返回 in,因为流插入操作符要支持连续插入。
void clear()
{
_str[0] = '\0';
_size = 0;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
当输入的字符串很长的时候,不断+=,频繁的扩容,会使效率变得底下,所以我们可以使用一个类似内存池的机制来优化以上这种场景。
开辟一个大小合适的数组用来临时存储数据,临时数组存放满了之后再将其插入到 string 对象中。
istream& operator >> (istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
const size_t N = 32;
char buff[N];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
//将数据插入到对象中
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
buff[i] = '\0';
s += buff;
return in;
}
11. find 和 substr?
在上一篇博客中我们使用了find和substr实现了网页链接切割,接下来我们就来模拟实现一下这两个功能。
find函数的实现:
- 如果是字符,遍历_str即可。
- 字符串可以使用 strstr 函数,其作用是返回字串在字符串中第一次出现的位置。
- 然后用 strstr 返回的值减去_str,即可得该子串得起始位置。
//使用find 和 substr 进行域名字符串的切割
size_t find(char ch, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (ch == _str[i])
return i;
}
return npos;
}
//子串查找
size_t find(const char* sub, size_t pos = 0) const
{
assert(sub);
assert(pos < _size);
//查找
const char *ptr=strstr(_str + pos, sub);
if (ptr == nullptr)
return npos;
return ptr - _str;
}
substr 函数的实现:
- 检查是否切割到字符串结尾,如果是直接将_pos位置后的数据全部放入临时变量中
- 如果不是切割到字符串结尾,则一个一个放入到临时变量中。
string substr(size_t pos, size_t len = npos) const
{
assert(pos < _size);
size_t realLen = len;
//如果切割的部分大于字符串的大小
if (len == npos || pos + len > _size)
{
realLen = _size - pos;
}
//创建一个临时变量,返回切割后的数据。
string sub;
for (size_t i = 0; i < realLen; ++i)
{
sub += _str[pos + i];
}
return sub;
}
12. 比较函数重载
这就是实现一些大于、等于、小于此类的操作符重载了。
bool operator>(const string& s) const
{
return strcmp(_str, s._str) > 0;
}
bool operator==(const string& s) const
{
return strcmp(_str, s. _str)==0;
}
bool operator>=(const string& s) const
{
return *this > s || *this == s;
}
bool operator<=(const string& s) const
{
return !(*this > s);
}
bool operator<(const string& s) const
{
return !(*this >= s);
}
bool operator!=(const string& s) const
{
return !(*this == s);
}
|