一、string的结构
class string
{
public:
//迭代器
typedef char* iterator;
typedef const char* const_iterator;
public:
string(const char* str = "");//构造
string(const string& s);//拷贝构造
string& operator=(const string &s);//赋值重载
~string();//析构
// iterator
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
/
// modify
void push_back(char c);//尾插一个字符
string& operator+=(char c);//+=字符
void append(char c);//拼接一个字符
void append(const char* str);//拼接一个字符串std
void append(const string& s);//拼接一个string
string& operator+=(const char* str);//+=字符串
void clear();//清理
void swap(string& s);//交换
const char* c_str()const;//返回C形式字符串
/
// capacity
size_t size()const;//string 大小
size_t capacity()const;//string 容量
bool empty()const;//判空
void resize(size_t n, char c = '\0');//设置大小
void reserve(size_t n);//设置容量
/
// access
char& operator[](size_t index);//像数组访问[]
const char& operator[](size_t index)const;
/
//字符串比较
bool operator<(const string& s);
bool operator<=(const string& s);
bool operator>(const string& s);
bool operator>=(const string& s);
bool operator==(const string& s);
bool operator!=(const string& s);
// 返回c在string中第一次出现的位置
size_t find (char c, size_t pos = 0) const;
// 返回子串s在string中第一次出现的位置
size_t find (const char* s, size_t pos = 0) const;
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c);
string& insert(size_t pos, const char* str);
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len);
//返回子串 - 返回从pos位置向后的len个字符
string substr(size_t pos = 0, size_t len = npos);
private:
char* _str;
size_t _capacity;
size_t _size;
}
private:
char* _str;
size_t _size;
size_t _capacity;
public:
static size_t npos;//npos 类外定义为-1
};
size_t npos = -1;
ostream& operator<<(ostream& out, const string& s);//重载cout
istream& operator>>(istream& in, string& s);//重载cin
string的接口
1. 构造函数
注意点:
- 默认构造给空字符串 “”,_str不能给nullptr,因为 _str为char*类型,cout输出时,实际上是解引用输出字符串,直到\0, _str为空会发生空指针的解引用
- capacity为有效数据容量,开空间应多开一个空间给’\0’
写法1
string(const char* s = "")
:_str(new char[strlen(s) + 1])
,_size(strlen(s))
,_capacity(strlen(s))
{
strcpy(_str, s);//拷贝
}
缺点:strlen()是O(N)的接口,调用多次,效率太低!
写法2(优化): 只调用一次strlen
string(const char* s = "") // 也可赋值为"\0"
{
_size = strlen(s);
_capacity = _size;
_str = new char[_capacity+1]; //空间大小是有效容量 + 1('\0')
strcpy(_str, s);//拷贝
}
错误写法
string(const char* s = "")
:_size(strlen(s))
,_capacity(_size)
_str(new char[_capacity+1])
{
strcpy(_str, s);//拷贝
}
//错误原因:
//虽然strlen只用一次,但是初始化列表的初始化顺序是根据成员函数的声明来的,所以会先走_str的初始化,此时_capacity是随机值,会出错! 如果要使用这种方法,要保证成员声明的顺序并且当增加新的成员的时候,还要维护声明的顺序!
2. 析构函数
//直接释放资源即可
~string()
{
_size = _capacity = 0;
delete[] _str;
_str = nullptr;
}
3. swap函数
void swap(string& tmp) //当前对象和tmp交换的函数
{
//调用库中的swap来交换基本类型
::swap(_str, tmp._str);
::swap(_capacity, tmp._capacity);
::swap(_size, tmp._size);
}
4. 拷贝构造
注意问题:深浅拷贝问题
_str是一个指针,指向动态申请的数组,如果不显示写一个深拷贝,那么默认生成的拷贝构造就会采取值拷贝的形式进行,
这样拷贝的对象中的_str指针和原对象的 _str指针指向同一块空间,存在问题:
- 拷贝对象的修改会影响原对象
- 析构的时候,会析构两次,导致崩溃
所以:拷贝的时候需要另外开辟一段新空间,把原对象的空间的内容拷贝到新空间
1) 传统写法
string(const string& s)
:_size(s._size)
,_capacity(s._capacity)
,_str(new char[s._capacity+1]) //新开空间
{
strcpy(_str, s._str);//把数组中的内容进行拷贝
}
2) 现代写法
//现代写法就是让一个临时对象帮this去做开空间和拷贝 最后再交换
//需要先把this的成员初始化,都是随机值,_str就是野指针,野指针delete会崩溃!
string(const string& s)
:_size(0)
,_capacity(0)
,_str(nullptr)
{
string tmp(s._str); //利用s的_str构造一个临时对象tmp
swap(tmp);//this指向的对象和tmp交换所有成员变量
}
5. 赋值重载(operator=)
1) 传统写法
string& operator=(const string& s)
{
//自己给自己赋值 直接返回 如果不处理,自己给自己释放了就变成了随机值
if (this == &s) {
return *this;
}
//开空间成功才会删除原来的 .这样的好处是如果new失败 原对象不会被破坏
char* tmp = new char[s._capacity + 1];
//失败的话抛异常就直接跳到catch了
strcpy(tmp, s._str);
delete[] _str;
_str = tmp; //然后再把tmp赋值给_str
_size = s._size;
_capacity = s._capacity;
return *this;
}
2) 现代写法
//形参不传递引用,直接用实参拷贝构造一个形参
//然后形参与this交换
//形参最后会销毁
string& operator=(string s)
{
swap(s); //s作为形参,调用结束也会销毁
return *this;
}
6. 返回C形式字符串(c_str)
const char* c_str() const
{
return _str;
}
7. 返回长度(size)
size_t size() const
{
return _size;
}
8. 判空(empty)
bool empty() const
{
return _size == 0;
}
9. 容量(capacity)
size_t capcacity() const
{
return _capacity;
}
10. operator[]
//operator[] -> 返回pos位置的元素的引用
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//operator[] const --- const对象调用 返回const引用
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
11. 迭代器
string的底层是数组,迭代器就是原生指针
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size; //end()返回最后一个位置的下一个位置的迭代器
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
12. 设置容量(reserve)
//扩容 -> 大于当前容量就扩容
// 注意要进行深拷贝 -> 开新空间
void reserve(size_t n)
{
//如果n大于当前的capacity 才会进行预留空间
if (n > _capacity)
{
char* tmp = new char[n + 1];
//把原空间的内容拷贝进去
strcpy(tmp, _str);
//释放原空间
delete[] _str;
_str = tmp;
_capacity = n;
}
}
13. 设置大小(resize)
//改变大小
void resize(size_t n,char c = '\0')
{
//如果n大于_size 把size到n位置的元素 置为c 并改变size
if (n > _size)
{
reserve(n);//扩容一下,因为可能n大于_capacity
for (size_t i = _size; i < n; ++i) //n==size时不会进入循环
{
_str[i] = c;
}
_str[n] = '\0'; //不要忘了末尾加'\0'
_size = n;
}
//如果n<size,直接保留前n个字符,后面的删除即可
else
{
_size = n;
_str[n] = '\0';
}
}
14. 尾插字符(push_back)
void push_back(char c)
{
//如果满了
if (_size == _capacity)
{
//扩容复用reserve
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = c;
++_size;
//最后一个位置保证是'\0'
_str[_size] = '\0';
}
15. 拼接字符串(append)
//拼接一个字符-> 复用push_back
void append(char c)
{
push_back(c);
}
//拼接n个字符
void append(size_t n, char c)
{
reserve(n + _size);// 减少扩容(如果n+_size 大于此时的容量)就会扩容
for (size_t i = 0; i < n; ++i)
{
push_back(c);
}
}
//拼接C字符串
void append(const char* s)
{
size_t len = strlen(s);
//如果 追加之后的字符串大于此时的容量 就要扩容
if (len + _size > _capacity)
{
//扩容 复用reserve
reserve(len + _size);
}
strcpy(_str + _size, s);//拷贝到从_size开始往后的位置
//如果用strcat(_str,s)追加,那么每次都要找'\0' 效率很低
_size += len;//更新size
}
//拼接string
void append(const string& s)
{
//直接复用重载即可
append(s._str);
}
16. operator+=
// += 一个字符
string& operator+=(char c)
{
//复用push_back即可
push_back(c);
return *this;
}
//+=字符串
string& operator+=(const char* s)
{
//复用append
append(s);
return *this;
}
17. 插入字符/字符串(insert)
//pos位置插入字符
string& insert(size_t pos, char c)
{
assert(pos <= _size); // =:可以尾插
//如果满了 扩容
if (_size == _capacity)
{
//扩容复用reserve
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}//另一种写法 end表示后面的位置 而不是前面的位置
//向后挪动
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;//插入字符c
++_size;//更新size
return *this;
}
/
//pos位置插入字符串
string& insert(size_t pos, const char* s)
{
assert(pos <= _size);//检查pos合法
size_t len = strlen(s);
if (len + _size > _capacity)
{
reserve(_capacity + len);//扩容
}
//向后挪动
size_t end = _size + len;
while (end>=pos+len) //最后一次是_str[pos+len] = _str[pos]
{
_str[end] = _str[end - len];//要插入的字符串的长度为len pos位置之后的向后挪动len个位置
--end;
}
//把数据拷贝进去 -- 拷贝n个
strncpy(_str + pos, s,len);
_size += len;//更新_size
return *this;
}
18. 删除字符(erase)
//从pos位置删除n个字符
//默认是从0位置,删除所有字符,因为npos为整数最大值
void erase(size_t pos = 0,size_t n = npos)
{
assert(pos < _size);
//如果 n >= _size-pos 即要删除的个数 大于等于 pos之后的字符的个数 直接全删了就可以
if (n == npos || n >= _size - pos)
{
_str[pos] = '\0';
_size = pos; //更新长度
}
else
{
//hello worldxxx 否则就直接向前覆盖
strcpy(_str + pos, _str + pos + n);//覆盖pos后面的n个位置
_size -= n; //更新长度
}
}
19. 查找字符/子串(find)
//从pos位置查找字符 - 返回第一次出现字符c的位置
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (c == _str[i])
{
return i;
}
}
return npos;//找不到返回npos
}
//从pos位置查找字符串 - 返回第一次出现字符串的位置
size_t find(const char* s, size_t pos = 0) const
{
//复用C语言中的 strstr
assert(s);
assert(pos < _size);
char* start = strstr(_str+pos,s);//从_str + pos位置开始查找字符串s
//如果找到了 -> 返回npos
if (start == nullptr)
{
return npos;
}
else
{
return start - _str; //start是目标子串的首地址,指针相减得到start的坐标
}
}
20. 返回子串(substr)
// 返回从pos位置向后的len个字符
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
string sub; //临时对象
size_t reallen = len; //正常情况下真实长度就是len
if (len == npos || len > _size - pos) //如果len很大,或者len超过从pos到结尾的字符个数
{
reallen = _size - pos; //就吧pos位置后面的全删除即可
}
for (size_t i = 0; i < reallen; ++i)
{
sub += _str[pos + i];
}
return sub;
}
21 字符串比较
//compare
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 || *this > s;
}
bool operator!=(const string& s) const
{
return !(*this == s);
}
22. 重载cout
误区:认为cout和cin的重载必须声明为友元!
其实不是的,因为可以通过接口来实现容器内成员的更改
ostream& operator<<(ostream& out, const string& s)
{
//遍历string对象,利用重载的[]依次访问即可
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i]; //用接口访问数据
}
return out;
}
23. 重载cin
1) 初始版本
istream& operator>>(istream& in, string& s)
{
//首先清理原字符串
if (s.size() != 0)
{
s.clear();
}
//一个字符一个字符的读入 //get()是istream中的一个成员函数,用istream对象cin调用
//get就是单纯的读字符,不会考虑不同元素之间的间隔也是空格的问题
char ch = in.get(); // 用cin>>ch 不可以,因为cin自动忽略空格和换行 不会读到缓冲区 因为读的时候他认为一个一个输入之间的空格就是间隔!,所以空格根本不会读入 所以下面循环不会跳出!
while (ch != ' ' && ch != '\n') // 如果不是空格也不是换行,就继续输入
{
//尾插到结尾
/*s.push_back(ch);*/ //cin也完全不用直接访问成员,通过接口可以完成
s += ch;
ch = in.get();//继续读
}
return in;
}
2) 优化
优化问题:如果输入的字符串很长,初始版本中读入字符就会不断+= 可能会频繁扩容,效率很低
优化的思想: 利用一个临时数组,先把读到的数据放到临时数组中
如果临时数组满了,就接到字符串的后面
istream& operator>>(istream& in, string& s)
{
//清理
if (s.size() != 0)
{
s.clear();
}
const size_t N = 32;//设定临时数组的大小
char tmp[N];
char c = in.get();
size_t i = 0;
while (c!=' ' && c!= '\n')
{
tmp[i++] = c; //从缓冲区读入的数据放到临时数组
if (i == N - 1) //如果是最后一个数据,那么放入\0 然后 接到string的后面
{
tmp[i] = '\0';
s += tmp;
//然后i回到最开始
i = 0;
}
c = in.get();
}
//如果某一次没有读到 N-1 ,在中途就遇到 '\n' 或者' '而跳出循环,所以再加一次
tmp[i] = '\0';
s += tmp;
return in;
}
,这样有两个好处
- 如果频繁输入并且输入的字符串较小的话,减少扩容 ,每有输入N个才进行一次扩容
- 如果直接reserve来解决频繁扩容问题的话,若输入的字符串比较少,可能会浪费空间,用临时数组扩容的话,扩容后的容量就是输入字符串的长度(因为调用的是 +=)
并且一般不会输入这么多!