一学习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);
}
1
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--;
2
size_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-1X
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)
{
_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,swap
std::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就不会去调用算法的swap
void swap(string& s1,string& s2)
{
s1.swap(s2);
}