【C++】 string类常用接口的实现
目录
1. 成员变量
2. 构造函数
3. 析构函数
4. 拷贝构造函数
5. 赋值运算符重载
6. size()
7. capacity()
8. c_str()
9. operator[]
10. begin() end()
11. reserve(size_t n = 0)
12. push_back( char c)
13. append
14. operator+=
15. insert
16. erase
17.clear
18.resize
19. find
20. substr
21.类的比较
上一篇博客介绍了string的常用接口,为了加深理解本篇博客一起看看string类常用接口的模拟实现。我用顺序表的结构来实现string类。
1. 成员变量
calss mystring
{
public:
private:
char* _str;
size_t size;
size_t capacity;
};
字符在string类的位置和string类的容量都为非负数,故两个变量定义为size_t类型。
2. 构造函数
string(const char* s ="")
{
int len = strlen(s);
_str = new char[len + 1];//字符串末尾的‘\0’不记录在size和capacity中,故需要加1
strcpy(_str, s);
_size = len;
_capacity = len;
}
这里是一个缺省值构造函数,它可兼顾空string类和用字符串初始化的string类,一举两得。当然看到这个实现方式可能会有疑问,为什么不用初始化列表来实现构造函数?其实用初始化列表也可实现,如下:
string(const char* s)
:_str(new char[strlen(s) + 1])//每次都要计算字符串s的长度效率低
,_size(strlen(s))
,_capacity(strlen(s))
{
strcpy(_str, s);
}
可以看到这样实现每次又要计算字符串s的长度,效率必然会降低。
3. 析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
完成空间的释放和资源的清理。
4. 拷贝构造函数
写好拷贝构造函数,我们必须要了解浅拷贝和深拷贝;
浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。
深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。
显然我们这里需要的是两个独立的类,故需要使用深拷贝。
拷贝构造函数大致上可分为两种写法:
1)传统写法
string(const string& s)
{
int len = strlen(s._str);
_str = new char[len + 1];
_size = s._size;
_capacity = s._capacity;
strcpy(_str, s._str);
}
该方法先记录string类的大小,然后开辟空间,最后进行string类内容的复制。
2)现代写法
string(const string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tem(s._str);
swap(tem);
}
该方法利用构造函数先构造出一个临时的string类对象,再将临时的string类对象与源string类交换;这里将源是string类初始化是为了交换后的临时对象是一个没有资源的类对象,当然这一步也可删去,在出作用域时会调用析构函数。注意:交换函数需要我们自己写,因为stl提供的交换函数模板里需要拷贝构造函数,会进入一个循环过程。
swap函数实现
void swap( string& tem)
{
::swap(_str, tem._str);
::swap(_size, tem._size);
::swap(_capacity, tem._capacity);
}
::代表调用全局域的swap函数; 告诉编译器要先在全局范围寻找swap函数,否则编译器编译时会认为调用的是成员函数。
5. 赋值运算符重载
赋值运算符重载也有两种写法;
1)传统写法:
string& operator=(const string& s)
{
if (_capacity >= s._capacity)
{
strcpy(_str, s._str);
_size = s._size;
}
else
{
delete[] _str;
int len = strlen(s._str);
—str = new char[len + 1];
_size = len;
_capacity = len;
strcpy(_str, s._str);
}
return *this;
}
赋值运算符两边的类的容量并不一定相同,所要检查容量,当‘=’左边的操作数的容量大于右边的操作数时,可直接将右操作数的内容拷贝给左操作数,这是左操作数的_size也已经改变,所以要及时更新。当左边的操作数的容量小于右操作数时,要想将右操作数赋值给左操作数,但是new操作符又不能直接扩容,故先将左操作数释放,之后在开辟与右操作数容量相同的空间,之后将右操作数的内容复制给左操作数,更新_size和_capacity的值。
2)现代写法
string& operator=(const string& s)
{
string tem(s);
swap(tem);
return *this;
}
借助拷贝构造函数,构造一个临时的类,然后将this指向的类的内容和临时的类的内容全部交换(交换函数见拷贝构造函数),即完成赋值。与传统写法相比,现代写法跟加便捷。
6. size()
size_t size()
{
return _size;
}
返回类的大小。
7. capacity()
size_t capacity()
{
return _capacity;
}
返回类的容量
大小和容量都是成员函数直接返回就行。
8. c_str()
const char* c_str()const
{
return _str;
}
也是返回成员变量,但要注意这里返回的类型。
9. operator[]
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
要返回pos位置的字符,因为_str是一个指向字符串开头的指针,故它可用像数组一样用下标访问,需要注意的是:[]操作的对象必须是在类的大小之内,不能越界,故要先判断。另外函数返回的是引用,故可以通过[]操作符更改pos位置的字符。当然也有禁止使用[]操作符更改字符串内容的时候,这是返回值就要用const修饰。
10. begin() end()
上篇博客讲过迭代器的底层可能使指针;这里我用指针实现 。
begin是指向字符串的开头,而end是指向字符串结尾的下一个位置,故代码为:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size ;
}
若不容通过迭代器更改字符串,则需要用const修饰构成重载;
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
11. reserve(size_t n = 0)
当n大于当前类的容量时,将类的容量增加至n。当n小于当前类的容量时,不作任何变化。
假设n现在大于当前类的容量,那我们要思考一些问题呢?
1)肯定要进行扩容,
2)类的内容除了容量外其他没有任何改变
方法一:我们可以借助实现赋值运算符重载的思想,利用类的内容构造一个临时的类,之后将_str指向的空间释放,再重新开辟n+1大小的空间,之后再将临时类和重新开辟的类进行交换,更新_capacity即完成函数的功能
方法二:其实和方法一基本类似,新开辟一个n+1大小的空间,将类里的字符串复制到新开辟的地址中去,之后释放_str原来指向的空间,将开辟的新的空间的地址给_str,之后更新_capacity.
代码:
void reserve(size_t n = 0)
{
if (n > _capacity)
{
//方法一
//string tem(*this);
//delete[] _str;
//_str = new char[n + 1];
//swap(tem);
//_capacity = n;
//方法二
char* tem = new char[n + 1];
strcpy(tem, _str);
delete[] _str;
_str = tem;
_capacity = n;
}
}
12. push_back( char c)
在字符串的尾插入一个字符,必然要考虑是否要进行扩容。在不与要扩容时直接将字符c赋值为给_size位置就行,然后因为加上了一个字符所以_size也要加一,而字符串的结尾时'\0'所以在当前的_size的位置要更改为'\0'。
扩容的方法有种:
1)不借助函数
先确定需要扩容多少,我这里设置为若类里面有内容,那么一次扩充原来的二倍,若为空类则第一次扩容4个空间。为了保证扩容后类的内容不变,我们先用这个类拷贝构造一个临时的类。当然了在扩容之前要先将类释放以免发生内存泄漏,之后在进行扩容,然后再将临时的类与扩容的类相互交换,在将字符c加在最后面(方法和不需要扩容一样),之后更新容量就完成了。
代码:
void push_back(char c)
{
if (_size == _capacity)
{
size_t capacity = _capacity == 0 ? 4 : _capacity * 2;
string tem(*this);
delete[] _str;
_str = new char[capacity];
swap(tem);
_str[_size] = c;
++_size;
_str[_size] = '\0';
_capacity = capacity;
}
else
{
_str[_size] = c;
++_size;
_str[_size] = '\0';
}
}
第二种是调用reserve()函数,
这个和第一种的原理大致相同只是将扩容交给reserve函数。
void push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = c;
++_size;
_str[_size] = '\0';
}
13. append
这里实现append函数的两个功能:1)在类的字符串后端加上字符串,2)在一个类的后段接上一个类的内容。
1)不管是1)还是2)只要是需要在字符串后面追加那必要考虑扩容的问题,与上一个函数不同,这里我们要追加的是字符串或者类,所以我们要知道字符串的长度,那我们该怎样判断时候需要扩容呢?其实很简单我们是在原来的字符串的基础上追加一个长度为len的字符串,那只要现在字符串的长度加上追加的字符串长度大于类的容量那么我们就要扩容,显而易见我们就扩容到追加字符串之后的长度,这里可能有同学会问:字符串之后不是还有‘\0’为什么扩容不加上它的空间,原因是在与我们扩容调用reserve函数传给它的参数是扩容的实际大小,在reserve内部是现实将‘\0’的空间也加上了。这里实现追加我使用的是strcpy函数第一个参数是原来字符串‘\0’字符串的地址,这样就可以非常简单的将字符串s追加在_str后面,而且这样实现也不用考虑最后一个是‘\0’,因为strcpy函数会将字符串的‘\0’进行拷贝。代码:
string& append(const char* s)
{
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, s);
_size += len;
return *this;
}
2)其实这个实现非常简单调用1)就行,只是在传参是要传的是字符串。
string& append(const string& s)
{
append(s._str);
return *this;
}
14. operator+=
这里实现三个重载:1) +=字符;2)+=字符串;3)+=类
1)实现+=字符串只需调用push_back就行;
代码:
string& operator+=(char c)
{
push_back(c);
return *this;
}
2)调用append
代码:
string& operator+=(const char* s)
{
append(s);
return *this;
}
3)同样的调用append
代码:
string& operator+=(const string& s)
{
append(s._str);
return *this;
}
15. insert
实现两个功能:1)插入字符,2)插入字符串
插入和在字符串尾部追加一样,也可以说在字符串尾部追加是插入的一种情况。那在插入之前肯定要进行扩容扩容方法和上一个函数相同借助reserve函数。另外一点insert函数是在pos的位置进行插入那么pos一定要在字符串内,也就是要判断pos是否小于_size,大于的话就是错误的。还有一点,既然是pos位置插入那必然要腾出位置,不能覆盖,那就涉及到要挪动数据,插入一个字符要将pos位置之后的字符都要往后挪动一个位置,插入一个长度为len的字符串,那pos位置之后的字符就要往后挪动len个位置。
1) 代码:
string& insert(size_t pos, char ch)
{
assert(pos < _size);//位置检查
if (_size == _capacity)//检查是否需要扩容
{
reserve(_capacity+1);
}
size_t end = _size + 1;
while (end > pos)//挪动数据
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;//插入数据
++_size;
return *this;
}
2)代码
string& insert(size_t pos, const char* str)
{
assert(pos < _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;
while (end >= pos)//挪动数据
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);//插入字符串
_size += len;
return *this;
}
这里出入数据我使用了strncpy函数,为什么要使用这个函数,1)用复制函数,只要确定插入位置的地址即可,比较简单,2,不能将‘\0’也拷贝进去那就要限定长度。
16. erase
从pos位置开始向后消除len个字符,当len为-1是,pos之后的字符全部消除;
代码:
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);//判断pos位置是否合法
if (pos + len >= _size || len == npos)//判断是否是pos位置之后的字符需要全部消除
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);//消除len个字符
_size -= len;//更新字符串的长度
}
}
因为是消除pos位置之后的len个字符,也就是在pos +len之后位置的字符要往前挪动,为就是往前覆盖,所以我这里用了strcpy函数,将pos +len之后的字符复制到pos位置之后。
17.clear
清理类的内容,也就是将字符串消除
代码:
void clear()
{
_str[0] = '\0';
_size = 0;
}
18.resize
1)resize(size_t n); 2)resize(size_t n, char ch = '\0')
函数的功能在于当n大于类的字符串长度时,将字符串的长度增加到n,容量不够时进行扩容,扩容后的容量为n,并且大于原来长度的位置用‘\0’或者字符c填充。 类的字符串长度大于n时将字符串的长度缩小到n(也就是删除数据)。
代码:
void resize(size_t n)
{
if (_size > n)//删除数据
{
_str[n] = '\0';
_size = n;
}
else
{
//扩容加插入数据
reserve(n);
size_t i = _size;
for (i = _size; i < n; ++i)
{
_str[i] = '\0';
}
}
}
void resize(size_t n, char ch = '\0')
{
if (_size > n)//删除数据
{
_str[n] = '\0';
_size = n;
}
else
{
//扩容加插入数据
reserve(n);
size_t i = _size;
for (i = _size; i < n; ++i)
{
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
}
19. find
1) find(const char ch, size_t pos = 0); 2)find(const char* str, size_t pos = 0)
从pos的位置向后查找,查找在字符串中与参数匹配的字符或者字符串,并返回位置,当 未找到时返回npos(静态成员变量值为-1 也就是size_t的最大值)。
代码:
size_t find(const char ch, size_t pos = 0)
{
assert(pos < _size);//检查pos的合法性
for (size_t i = pos; i < _size; ++i) //查找与字符匹配的位置
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);//检查pos的合法性
assert(str); //防止字符串为空
const char* p = strstr(_str + pos, str);//在pos之后开始找
if (p == nullptr)
{
return npos;
}
else
{
//指针减指针
return p - _str;
}
}
20. substr
string substr(size_t pos = 0, size_t count = npos)
从pos位置开始向后数count个字符,用这些字符创建一个新类并返回。
若pos往后的字符个数小于count 或者 count == npos,则用pos之后的所有字符创建类。
string substr(size_t pos = 0, size_t count = npos)
{
assert(pos < _size);//检查位置的合法性
int len = count; 向新类中添加多少字符
if (pos + len > _size|| len == npos) //检查是否超出字符串的实际大小,更新len 的大小
{
len = _size - pos;
}
string sub;
for (size_t i = pos; i < pos + len; ++i)向字符串中添加字符
{
sub += _str[i];
}
return sub;
}
21.类的比较
这部分比较简单就直接附上代码了,说明一点 通常情况下只需要实现 > 和 == 或者 < 和 ==其他比较皆可用逻辑实现。
代码:
bool operator>(const string& str)const
{
return strcmp(_str, str._str) > 0;
}
bool operator==(const string& str)const
{
return strcmp(_str, str._str) == 0;
}
bool operator<(const string& str)const
{
return strcmp(_str, str._str) < 0;
}
bool operator>=(const string& str)const
{
return (*this > str) || (*this == str);
}
bool operator<=(const string& str)const
{
return (*this < str) || (*this == str);
}
bool operator!=(const string& str)const
{
return !(*this == str);
}
22. << 和 >>重载
1) <<
ostream& operator<<(ostream& out, const string& str)
{
out << str.c_str();
return out;
}
2) >>
istream& operator>>(istream& in, string& str)
{
str.clear();
const size_t N = 32;
char bu[N];
int i = 0;
char ch;
ch = in.get();//可以输入' ';
while (ch != ' ' && ch != '\0')
{
bu[i++] = ch;
if ( N - 1 == i)
{
bu[i] = '\0';
str += bu;
i = 0;
}
ch = in.get();
}
bu[i] = '\0';
str += bu;
return in;
}