一学习string的原因
1.从个人理解角度上:
在刚开始学习之前,我只知道学习完string在以后的刷题中能提高做题效率,在对字符串的处理string库中也许有对应的接口去实现需求,不用自己去写函数的实现。
但在学string中改变了之前的看法:不仅是要会用接口,而且在理解了接口的底层原理后能更好的去理解,使用它。
总结:使用——明理——扩展
2从C语言上:
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
二标准库中的string
使用string必须用它对应的头文件:#include<string>
在学习接口及其文档的说明,可以到cpulspuls官网中去学习:
cplusplus.com - The C++ Resources Network
1(拷贝)构造和析构
string() 构造string类的空字符串(其中有’\0‘)
string(char* s) 构造的同时进行string的拷贝(支持单参数隐式类型转换)
string(const string& str) string拷贝构造
string(const string& str , size_t pos , size_t len = npos) 进行str子串的获取,pos是子串的起始位置,len是子串的结束位置(没有说明就默认是str的结束位置)
~string() string的析构
2运算符重载
使string的访问向数组访问数据一样数组名+[]
char& operator[] (size_t pos)
const char& operator[] (size_t pos) const
string类的赋值,对象可以是string,字符串,字符
string& operator = (const string& str)
string& operator = (const char* s)
string& operator = (char c)
string类字符串的尾插,对象可以是string,字符串,字符
string& operator += (const string& str)
string& operator += (const char*s)
string& operator += (char c)
实现string类与char*,char类型的字符(串)进行拼接(反过来也成立)
string operator+ (const string& lhs , const char* rhs)
string operator+ (const char* lhs , const string& rhs)
string operator+ (const string& lhs , char rhs)
string operator+ (char rhs , const string& lhs)
流插入,流提取的重载
istream& operator >>(istream is , string& str)
ostream& operator <<(ostream os , string& str)
3迭代器
迭代器行为像指针一样,但不=指针
iterator begin() 指向string的开始
const_iterator begin() const
iterator end() 指向string的末尾
const_iterator end() const
反向迭代器
iterator rbegin() 指向string的末尾
irerator rend() 指向string的开始
有了迭代器,我们就有了两种遍历string的方式:迭代器与范围for(本质是迭代器)
#include<string>
int main()
{string s1("hello warld");string::iterator it = s1.begin();while (it != s1.end()){cout << *it << ' ';it++;}cout << endl;for (auto ch : s1){cout << ch << ' ';}cout << endl;return 0;
}
2查询string的各种数据
size_t size() const 返回string内的字符个数(不包含’\0‘) =size_t length() const
size_t capacity() const 返回string的空间大小(包含’\0‘)
void reserve(size_t n=0) 开n个空间大小
(知道要插入的多少数据提前开好空间就不用进行扩容,提高了效率)
(但空间不够时,string应该要进行对应的空间扩容。那么是要扩容多少呢?2倍扩?
我们用+=的方式在string末尾每次插入一个字符,循环100次来观察capacity的变化:
#include<string>
int main()
{string s;size_t sz = s.capacity();cout << "capacity changed: " << sz << '\n';cout << "making s grow:\n";for (int i = 0; i < 100; ++i){s += ('c');if (sz != s.capacity()){sz = s.capacity();cout << "capacity changed: " << sz << '\n';}}return 0;
}
在VS底层中实现的扩容:先开出15内存空间,等到空间不够时,第一次扩容是2倍扩,接下来的扩容都是大概按1.5来扩容。
而在g++下的扩容全是按照2倍扩容来的:
但两者的扩容方式不同会不会有什么影响呢?
没有!! 2倍扩容也好1.5倍扩容也好,说到底它们的目标都是要进行扩容。实现起来的细节是不做考虑的。
void resize(size_t n,char c) 开n个空间大小并插入n-size()个字符c
resize一般不缩容,在以下的不同情况使用resize会有不同的效果:
5增删查改
viod push_back (char c) 在string的末尾插入一个字符
void pop_back (char c) 在string的末尾删除一个字符
string& insert(size_t pos , char c) 在pos下插入字符(字符串,string)
string& erase(size_t pos , size_t len = npos) 在pos位置下删除len个字符(不加len默认删除pos后面的全部字符) (左闭右开)
void swap(string& str) 不仅仅是数据的交换,还有size,capacity的交换
6其它操作
void swap(string& x , string& y) 与算法库的swap分离开(如果调用算法库的swap,三次拷贝+一次析构代价太大)
const char* c_str() const 类型转换成char*方便打印(string类的要用到运算符重载)
string substr(size_t pos = 0 , size_t len = npos) 从pos位置到len个位置的字符串截取
size_t find(char c , size_t pos=0) 返回字符c所在的下标
string& replace(size_t pos , size_t len = npos , char c) 在pos到len的位置插入c字符
两者的特性结合可用来解决:
字符串空格的替换
三string的模拟实现
在string类中共有三个成员:_str(字符串),_size(字符个数),_capacity(空间大小)
其中,_size个数不包含‘\0’,但在_capacity中要多开出一个空间存储‘\0’!!
3.1成员函数
3.1.1构造函数
在实现构造时,如果string构造不传参,默认为空串。
但不意味着给缺省值时就是”\0“,因为写成”“是有’\0‘的存在的。
//string(const char* str = "\0")
string(const char* str = ""):_size(strlen(str))
{_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);//把字符串拷贝给_str成员对象}
3.1.2拷贝构造
拷贝构造里不单单只是数据拷贝!要在拷贝之前先new一块跟str一样长的空间,在进行数据的拷贝。也就是所谓的深拷贝!
一般的思路是:直接开出一个与拷贝对象一样长的空间,再把对应的数据赋值过去就完事了
string(const string& str)
{_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;
}
但这种属于是’自己买菜自己做‘。是比较传统的写法。
有这样的写法:构造一个与拷贝对象一样的临时对象,在进行交换(深拷贝),比较方便简洁:
string(const string& str)
{string tmp(str.c_str());swap(tmp);
}
有了拷贝构造,与它对应的还有一个赋值运算符的重载:也是类似的道理:
string& operator=(const string& str)
{char* tmp = new char[str._capacity + 1];strcpy(tmp, str._str);delete[] _str;//释放_str = tmp;_size = str._size;_capacity = str._capacity;return *this;
}//现代写法
string& operator=(const string& str)
{string tmp(str.c_str());swap(tmp);return *this;
}string& operator=(string ss)
{ swap(ss);return *this;
}
3.1.3析构函数
~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
3.2迭代器
实现的迭代器因为要支持可读可写与只读两个版本,实现时要实现俩个版本出来:
对应的begin(),end()实现成string类中的成员函数
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;
}
3.3扩容
传参进来的n如果比_capacity要小则需要进行空间的扩容到n+1(’\0‘要开给它):
void reserve(size_t n)
{//扩容if (_capacity < n){char* tmp = new char[n + 1];strcpy(tmp , _str);_str = tmp;_capacity = n;}
}
它的扩容不是我们在C语言学的函数realloc扩容那样有两种情况:原地扩与异地扩。它的扩容就是直接看出一块新的空间,在把_str的数据拷贝的!
3.4插入字符(串)
3.4.1append
将原来的_capacity与拼接形成新的string的_size个数进行比较,空间不够才进行扩容:
void append(const char* s)
{size_t len = strlen(s);//扩容if (_capacity < _size + len){reserve(_size + len);}/*for (size_t i = _size; i < len; i++){_str[i] = *s;s++;}*/strcpy(_str +_size, s);_size += len;
}
3.4.2insert
在pos位置上插入字符也好,字符串也好,都需要进行是否扩容与数据挪动,字符串的挪动必须是从后往前进行字符串的覆盖:
void insert(size_t pos, char ch)
{assert(pos <= _size);if (_capacity == _size){//扩容reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end=_size;while (end>=pos){_str[end+1] = _str[end];end--;}_str[pos] = ch;_size += 1;
}
但在这有个细节:如果pos的位置是0即进行头插,这将会进行程序出现问题:因为但end=0循环进入,end--使得end=-1。但end的类型是size_t,不会为负数!修改方式有两种:
1修改类型;2让end=_size+1进行循环时end永远都>pos:
void insert(size_t pos, char ch){assert(pos <= _size);if (_capacity == _size){//扩容reserve(_capacity == 0 ? 4 : _capacity * 2);}1size_t end = _size;//当pos=0时,size_t end=0,end--!=-1int end = _size;while (end >= (int)pos){_str[end + 1] = _str[end];end--;2size_t end = _size + 1;while (end>pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size += 1;}
插入的是字符串也是同理的:
void insert(size_t pos, const char* str){size_t len = strlen(str);assert(pos <= _size);if (_capacity <= _size + len){//扩容reserve(_size + len);}/*int end = _size;while (end >= (int)pos){_str[end + len] = _str[end];end--;}*/size_t end = _size + len;while (end > pos+len-1){_str[end] = _str[end-len];end--;}strncpy(_str+pos, str, len);_size += len;}
3.4.3push_back
复用insert进行尾插或者直接实现:
void push_back(char ch)
{if (_capacity == _size){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';//insert(_size, ch);
}
3.4.4赋值运算符=重载
string& operator+=(char ch)
{push_back(ch);return *this;
}
string& operator+=(const char* str)
{append(str);return *this;
}
3.5resize
实现resize是要了解的是:使用resize一般不缩容。要插入n个字符是有两种情况:n<=_size是仅需保留前n个字符串后加’\0‘即可;n>_capacity就要进行扩容与插入n-_size个字符:
void resize(size_t n, char ch = npos){if (n <= _size){_str[n] = '\0';_size = n;}else{//扩容reserve(n);//n>size后面有要求加字符for (size_t i = _size; i < n; i++){_str[i] = ch;}_str[n] = '\0';_size = n;}}
对应resize与reserve:基本上都是有扩容的功能的,但resize能在进行扩容的基础上将数据进行初始化,比较适合在对数据的管理上使用它。
3.6find
进行string的遍历,如果找到该字符就返回该字符的下标:如果是找字符串,找到了就返回该字符串的起始位置:
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;
}
size_t find(const char* s, size_t pos = 0) const
{assert(pos < _size);const char* p=strstr(_str + pos, s);if(p){return p - _str;//指针-指针=s的下标}else{return npos;}
}
3.7运算符重载<<,>>
流提取或者是流插入重载成全局函数好方便调用:
ostream& operator<< (ostream& os, const string& str)
{for (int i = 0; i < str.size(); i++){os << str[i];}return os;
}istream& operator>>(istream& in, string& s)
{s.clear();//防止string中有数据影响流插入char ch;//in >> ch;ch = in.get();//s.reserve(128);while (ch != '\n' && ch != ' '){s += ch;ch = in.get();}return in;
}
在重载流插入时,如果按照上面的实现方式的话,在每次插入时都要进行扩容,非常影响程序运行的效率。
如果要进行reserve开空间,但至于要看多少不能确定。所以就有人进行对这段代码的优化:
istream& operator>>(istream& in, string& s)
{s.clear();char ch;//in >> ch;ch = in.get();char buff[128];size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;// [0,126]if (i == 127){buff[127] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;
}
要不要扩容取决于会不会超出buff的数组总个数,非常的妙!!!
完整源代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<assert.h>
#include<iostream>
using namespace std;
namespace bit
{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;}//在末尾加n个ch字符void resize(size_t n, char ch = npos){if (n <= _size){_str[n] = '\0';_size = n;}else{//扩容reserve(n);//n>size后面有要求加字符for (size_t i = _size; i < n; i++){_str[i] = ch;}_str[n] = '\0';_size = n;}}//先开空间void reserve(size_t n){//扩容if (_capacity < n){char* tmp = new char[n + 1];strcpy(tmp, _str);_str = tmp;_capacity = n;}}//末尾插人字符void push_back(char ch){if (_capacity == _size){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';//insert(_size, ch);}//末尾插入字符串void append(const char* s){size_t len = strlen(s);//扩容if (_capacity < _size + len){reserve(_size + len);}/*for (size_t i = _size; i < len; i++){_str[i] = *s;s++;}*/strcpy(_str +_size, s);_size += len;}string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* str){append(str);return *this;}//在pos位置插入字符void insert(size_t pos, char ch){assert(pos <= _size);if (_capacity == _size){//扩容reserve(_capacity == 0 ? 4 : _capacity * 2);}//size_t end = _size;//当pos=0时,size_t end=0,end--!=-1/*int end = _size;while (end >= (int)pos){_str[end + 1] = _str[end];end--;}*/size_t end = _size + 1;//size-1Xwhile (end>pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size += 1;}void insert(size_t pos, const char* str){size_t len = strlen(str);assert(pos <= _size);if (_capacity <= _size + len){//扩容reserve(_size + len);}/*int end = _size;while (end >= (int)pos){_str[end + len] = _str[end];end--;}*/size_t end = _size + len;while (end >= pos+len){_str[end] = _str[end-len];end--;}strncpy(_str+pos, str, len);_size += len;}//删除pos位置的字符void erase(size_t pos, int len = npos){assert(pos <= _size);if (len == npos || len >= _size - pos)//len+pos会溢出{_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}}//string(const char* str = "\0")string(const char* str = ""):_size(strlen(str)){_capacity = _size;_str = new char[_size + 1];strcpy(_str, str);//把字符串拷贝给_str成员对象}//传统写法//s2(s1)//string(const string& str)//{// _str = new char[str._capacity + 1];// strcpy(_str, str._str);// _size = str._size;// _capacity = str._capacity;//}//s2=s1//string& operator=(const string& str)//{// char* tmp = new char[str._capacity + 1];// strcpy(tmp, str._str);// delete[] _str;//置空// _str = tmp;// _size = str._size;// _capacity = str._capacity;// return *this;//}//现代写法string(const string& str){string tmp(str.c_str());swap(tmp);}string& operator=(string str){swap(str);return *this;}~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}const char* c_str() const{return _str;}size_t size() const{return _size;}size_t capacity() const{return _capacity;}char& operator[](size_t pos){assert(pos <= _size);return _str[pos];}const char& operator[](size_t pos) const{assert(pos <= _size);return _str[pos];}void clear(){_size = 0;_str[_size] = '\0';}//string的成员函数void swap(string& str){//共有三种swap使用:算法,string.swap,swapstd::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}bool empty()const{if (_size == 0){return true;}return false;}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;}size_t find(const char* s, size_t pos = 0) const{assert(pos < _size);const char* p=strstr(_str + pos, s);if(p){return p - _str;//指针-指针=s的下标}else{return npos;}}string substr(size_t pos = 0, size_t len = npos) const{string s;if (len >= _size-pos){for (size_t i = pos; i < _size; i++){s += _str[i];}}else{for (size_t i = pos; i < pos + len; i++){s += _str[i];}}return s;}private:char* _str;size_t _size;size_t _capacity;public:static const int npos;//类中共有的对象};const int string::npos = -1;bool operator==(const string& s1,const string& s2){return strcmp(s1.c_str(), s2.c_str()) == 0;}bool operator<(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) < 0;}bool operator>(const string& s1, const string& s2){return !(strcmp(s1.c_str(), s2.c_str()) == 0 || strcmp(s1.c_str(), s2.c_str()) < 0);}//重载成全局函数ostream& operator<< (ostream& os, const string& str){for (int i = 0; i < str.size(); i++){os << str[i];}return os;}istream& operator>> (istream& is, string& str)//getline的实现方式 {str.clear();char ch = is.get();char buffer[128];size_t i = 0;while (ch != ' ' && ch != '\n'){//str += ch;buffer[i++] = ch;if (i == 127){buffer[127] = '\0';//开128个空间str += buffer;//重新计算i = 0;}ch = is.get();}if (i > 0){//有多少看多少buffer[i] = '\0';str += buffer;}return is;}/*istream& operator>> (istream& is, string& str){str.clear();char ch = is.get();while(ch != ' ' && ch != '\n'){str += ch;ch = is.get();}return is;}*///定义一个全局的swap就不会去调用算法的swapvoid swap(string& s1,string& s2){s1.swap(s2);}