STL库中实现了多种版本的构造函数,我们这里实现最常用的参数为const char*的一种。还需要提供一个无参的构造函数,这里采用缺省参数来代替。
_str代表用来存放数据的数组
_size代表数组有效字符的个数
_capacity在vs中代表有效字符的容量,在g++中包含了\0。我们采用vs的实现方式,底层开辟空间时多开一个空间存放\0。
string(const char* str = "") //采用缺省参数来代替无参构造
: _size(strlen(str))
//_str(str) 不能这样初始化,会导致权限放大
{
_capacity = _size == 0 ? 3 : _size;
_str = new char[_capacity + 1]; //多开一个空间用来存放\0
strcpy(_str, str);
}
手动开辟空间,并且拷贝数据到新空间里。
string(const string& s)
: _size(s._size)
, _capacity(s._capacity)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
}
我们可以利用构造函数来创建一个中间变量tmp,然后再用swap函数将this和tmp交换。
这里swap函数分为两种:一个是std里的swap,一个是string类里的swap。
std中的:
template<class T>
void swap(T& a, T& b)
{
T c(a);
a = b;
b = c;
}
string类中的:
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
上述两种swap在使用中有什么区别吗?
std中的参数是自定义类型,在交换时需要进行连续三次深拷贝,效率很低。
string中的参数则是内置类型,无需深拷贝。
所以我们在这里选用string类中的swap函数。
string(const string& s)
// 初始化,防止交换后tmp中的_str成为野指针,在析构时崩溃
: _str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str);
// 要使用tmp作为中间值,再让this与tmp交换,不能改变s的内容
swap(tmp);
}
与拷贝构造类似,需要手动开辟空间拷贝数据。但要考虑到同一对象可能被多次赋值,所以我们要使用delete[]将原有的资源释放。
string& operator=(const string& s)
{
// 给出条件this != &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;
}
// 现代写法
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);
swap(tmp);
}
return *this;
}
// 传值传参的现代写法
string& operator=(string s)
{
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
目前我们无法完全理解迭代器,但我们暂时可以把它理解为指针,现在用typedef把iterator定义为char*来模拟实现迭代器。
begin()返回首元素的地址
end()返回末尾\0对应的地址
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;
}
范围for本质上就是迭代器,只要实现了begin()和end()就可以使用范围for,不过begin和end的名字是固定的,也就是说不能用其他的功能相同但名字不同的函数替代。
void test4()
{
string s("hello world!");
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto ch : s)
{
cout << ch << " ";
}
cout << endl;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
reserve的参数是有效字符所占的字符,不包括\0,所以底层开空间时要开n+1个。
这里的扩容是异地扩容,要将原有的内容拷贝到新的空间。
//reserve不能缩容,如果n
void reserve(size_t n)
{
if (n > capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
resize根据n的大小可分为3种操作:
1.n<_size:删除数据
2.n>_size && n<=_capacity:插入数据
3.n>_size && n>_capacity:扩容并插入数据
void resize(size_t n, char ch = '\0')
{
// 删除数据
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
// 插入数据
else if (n > _size)
{
// 扩容
if (n > _capacity)
{
reserve(n);
}
size_t i = _size;
while (i < n)
{
_str[i] = ch;
++i;
}
_size = n;
_str[_size] = '\0';
}
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
operator需要实现两个版本分别用来适应const和非const对象。
const char& operator[] (size_t pos) const
{
// 越界访问直接报错
assert(pos < _size);
return _str[pos];
}
char& operator[] (size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void push_back(char ch)
{
// 因为在构造函数中我们已经设定好capacity一定不会为0,所以这里不用担心capacity为0的情况
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = ch;
++_size;
// 最后别忘了在末尾补上\0
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
// 将str追加到_str的尾部
strcpy(_str + _size, str);
_size += len;
}
使用函数重载分别实现尾插字符和尾插字符串即可
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
insert的实现有一些需要注意的地方,先来简单实现一下:
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size;
// 挪动数据
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
上述代码适用于大多数场景,但是当pos=0时,程序会崩溃,通过调试发现,当end=0的时候,–end没有变成-1,而是会变成很大的数,导致无限循环。原因是end的类型是无符号整数,那么如何解决呢?
我们可以用int类型的end,再将pos强制转换为int类型,这样确实可以解决问题,但是代码显得杂乱,所以还有另一种方法,在while循环中加入一个判断:当end=0时跳出循环。
while (end >= pos)
{
_str[end + 1] = _str[end];
if (end == 0)
break;
--end;
}
或者,我们将end初始化为_size+1,这样end就不会在等于0时减一了
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
插入字符串和上面也类似,需要注意的点是拷贝时要用strncpy,如果用strcpy会把\0也拷贝进去不符合要求。
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
size_t end = _size + len;
if (_size + len > _capacity)
{
reserve(_capacity + len);
}
while (end-len+1 > pos)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
实现了insert之后,push_back和append也可以通过调用insert来实现了:
void push_back(char ch)
{
insert(_size, ch);
}
void append(const char* str)
{
insert(_size, str);
}
erase的实现可分为两种情况:
1.len=npos或者pos+len=_size:此时只需要把pos位置的数据改为\0即可
2.pos+len<_size:用strcpy来移动数据
在string中npos是一个静态成员变量,静态成员变量需要在类内声明,类外定义。不过对于const修饰的静态成员变量,但是只有整型可以,其他类型就不可以了。
class string
{
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
// 可以
//static const size_t npos = -1;
/*static const int N = 10;
int a[N];*/
// 不可以
//static const double dpos = -1;
};
const size_t string::npos = -1;
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size -= pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
寻找字符:遍历一遍,找到相同字符就返回索引,找不到返回npos
寻找字符串:使用strstr来解决,找到返回p-_str,否则返回npos
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
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);
char* p = strstr(_str + pos, str);
if (p == nullptr)
{
return npos;
}
return p - _str;
}
const char* c_str()
{
return _str;
}
operator<<实现很容易,一个范围for即可完成
operator>>的实现:
关于流提取,我们知道用cin或者scanf无法拿到空格或者\n,但是我们在循环插入数据时又需要用空格和\n来作为循环结束的标志,所以我们需要用in中的get()函数来读取输入的数据,它能够获取到空格和换行符。
我们知道operator+=在容量满了之后需要扩容,如果我们输入的字符串很大,就会频繁的扩容,效率很低,所以我们用buff数组作为缓冲来解决问题:当数据正常时先都装进buff中,如果buff满了,再将buff中的数据一并转入string里。
ostream& operator<<(ostream& out, string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
char buff[128];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
// 如果buff中还有数据
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
getline()遇见空格时不会停下,只有遇见\n才会停下。
istream& getline(istream& in, string& s)
{
char ch = in.get();
while (ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
都加上const,为的是让const对象也能使用。
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 || s == *this;
}
bool operator<(const string& s) const
{
return !(*this >= s);
}
bool operator<= (const string & s) const
{
return *this < s || s == *this;
}
bool operator!=(const string& s) const
{
return !(s == *this);
}
如是我们就实现了一个比较完整的string类,整体代码如下:
namespace lgr
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
: _size(strlen(str))
{
_capacity = _size == 0 ? 3 : _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
string(const string& s)
: _str(nullptr)
, _size(0)
, _capacity(0)
{
string tmp(s._str);
swap(tmp);
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
const char* c_str()
{
return _str;
}
const char& operator[] (size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
char& operator[] (size_t pos)
{
assert(pos < _size);
return _str[pos];
}
string& operator=(string s)
{
swap(s);
return *this;
}
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 || s == *this;
}
bool operator<(const string& s) const
{
return !(*this >= s);
}
bool operator<= (const string & s) const
{
return *this < s || s == *this;
}
bool operator!=(const string& s) const
{
return !(s == *this);
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else if (n > _size)
{
if (n > _capacity)
{
reserve(n);
}
size_t i = _size;
while (i < n)
{
_str[i] = ch;
++i;
}
_size = n;
_str[_size] = '\0';
}
}
void push_back(char ch)
{
insert(_size, ch);
}
void append(const char* str)
{
insert(_size, str);
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
size_t end = _size + len;
if (_size + len > _capacity)
{
reserve(_capacity + len);
}
while (end-len+1 > pos)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size -= pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
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);
char* p = strstr(_str + pos, str);
if (p == nullptr)
{
return npos;
}
return p - _str;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
};
const size_t string::npos = -1;
ostream& operator<<(ostream& out, string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
char buff[128];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
istream& getline(istream& in, string& s)
{
char ch = in.get();
while (ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
}