目录
1.C++为什么要封装string
c语言中,字符串是以‘\0’结尾的字符的集合,为了方便操作,C标准库中提供了一系列str函数,但是这些库函数与字符串是分离开的,不符合OOP思想,并且底层空间由用户自己管理,容易出现各种内存问题。
2.string类
借助网站学习
string是字符序列类,可以视为一种数据结构/容器——串(C++把数据结构称为STL里面的容器(containers))。
这个网站把string归入了Other,左键查看详细内容
根据这个文档的介绍,string是一个类,是一个对类模板实例化的typedef
3.默认成员函数
构造函数
构造函数重载了多种形式
// 默认成员函数 int main() { string s1;//调用1 string s2("hello world");//调用4 string s3 = s2;//调用2 string s4(s2);//调用2 }
这是第三个构造函数的介绍
//用法 string s5(s2, 1, 6); cout << s5 << endl;
operator=
有不同的三种赋值
s1 = s2; cout << s1 << endl; s1 = "world"; cout << s1 << endl; s1 = 'x'; cout << s1 << endl;
4.遍历
Way1:下标访问
string类把运算符[]重载了两个,用const修饰的可以被const 对象调用。
也有size和length函数可以求string长度
int main() { string s1("Xiaomi Honor"); //下标遍历 size_t i = 0; for (i = 0; i < s1.size(); i++) { cout << s1[i] << "|"; } }
如果要逆置string,可以直接调用STL中的swap
size_t begin = 0, end = s1.size() - 1; while (begin < end) { swap(s1[begin], s1[end]); ++begin; --end; }
Way2:迭代器iterator(主流)
//迭代器遍历 string::iterator it = s1.begin(); while (it != s1.end()) { cout << *it << '-'; it++; }
迭代器对象it是类似于指针的东西,begin函数返回值是迭代器类型
如果要逆置,可以直接用STL库里面的算法reverse,参数类型是迭代器
reverse(s1.begin(), s1.end()); cout << s1 << endl;
如果string被const修饰,迭代器类型应该是const_iterator
//迭代器遍历 const string s2 = s1; string::const_iterator it2 = s2.begin(); while (it2 != s2.end()) { cout << *it2 << '-'; it2++; }
5.容量相关的函数
1.capacity
该函数返回容量大小
可以用这个函数来验证string的扩容机制
int main(void) { string s1("Max"); cout << "初始容量" << s1.capacity() << endl; size_t init_cp = s1.capacity();//初始的容量 size_t i = 0; for (i = 0; i < 500; i++) { s1.push_back('x'); if (s1.capacity() != init_cp) { cout << "发生了扩容,当前容量" << s1.capacity() << endl; init_cp = s1.capacity(); } } return 0; }
总结,VS编译器,初始大小给15(不含'\0'),之后以1.5倍的比例扩容
相比之下,GNU的g++以2倍速度扩容
2.reserve
这里区别一下string的两个概念,数据size表示字符串大小,像函数size和length都返回的是这个值,数据capactiy表示字符串预留空间,即容量,比如函数capacity返回这个值。
reserve就是用来改变容量的
如果使用reserve来缩小容量,如果是空字符串
int main() { string s1; cout << s1.capacity() << endl; s1.reserve(10);//这句代码表示你想把容量设置为10 cout << s1.capacity() << endl; return 0; }
如果是普通字符串
string s2("Maxnap"); cout << s2 << endl; cout << s2.capacity() << endl; s2.reserve(10); cout << s2.capacity() << endl;
说明VS编译器对reserve缩容的处理是,在初始化开辟一定大小的空间,缩容是不会改变容量大小的。
以上代码用g++编译后,可以得出结论:
g++对于空字符串不开辟空间,而缩容时,会缩小容量,但不会改变字符串大小。
string s2("Maxnap"); cout << s2 << endl; cout << s2.capacity() << endl; s2.reserve(100); cout << s2.capacity() << endl;
使用reserve扩容时不一定严格扩容,可能会多开辟空间。
3.resize
resize用来改变string的size大小
可以用resize来改变字符串大小,如果改变后的n大于原来的字符串长度,那么这个函数会扩大容量并且增加字符串长度
int main() { string s1("Maxnap and Kk"); cout << "容量" << s1.capacity() << endl; cout << "大小" << s1.size() << endl; cout << s1 << endl; s1.resize(90, 'x'); cout << "容量" << s1.capacity() << endl; cout << "大小" << s1.size() << endl; cout << s1 << endl; return 0; }
如果n介于大小和容量之间,只增加字符串长度,但是不改变容量大小
string s1("Maxnap"); cout << "容量" << s1.capacity() << endl; cout << "大小" << s1.size() << endl; cout << s1 << endl; s1.resize(10, 'x'); cout << "容量" << s1.capacity() << endl; cout << "大小" << s1.size() << endl; cout << s1 << endl;
如果n要小于字符串的长度,这个函数会毫不犹豫的把字符串删减
4.元素访问
这两个函数都可以用来访问元素,区别是如果发生越界访问,警告处理方式有差异,下标访问会断言失败终止程序,at会打印错误信息。
5. 增删查改
增:最常用的是重载的+=,还有push_back,append,insert
具体的用法可以看这个网站手册的介绍
查找和修改都可以用迭代器或者下标
erase可以用来删除下标pos后面的n个字符
int main() { string s1 = "Madnap"; cout << s1 << endl; s1.erase(3); cout << s1 << endl; return 0; }
assign类似=赋值,比较少用
replace的功能是替换,比如要求把字符串的空格替换为 ‘-’
可以使用这个函数:
int main() { string s1 = "Madnap is singer"; //把空格替换为'-' size_t pos = s1.find(' ', 0); while (pos != string::npos) { s1.replace(pos, 1, "-"); pos = s1.find(' ', pos + 1); } cout << s1 << endl; return 0; }
但是不推荐使用replace,因为底层需要挪动数据,消耗太大了,直接PASS
那不用replace怎么实现替换,新开辟一个空间,遍历该字符串,把空格替换为字符,再把新字符串拷贝给旧字符串,缺点是空间复杂度高,但是时间复杂度为O(N)
//法2 string s2 = "Slow down is possesed by Madnap"; string s3; for (auto ch : s2) { if (ch ==' ') { s3 += '-'; } else { s3 += ch; } } s2.swap(s3); cout << s2 << endl; cout << s3 << endl;
这里用了范围for和成员函数swap,该swap不同于算法里面的swap
6.和字符串相关的函数
1.c_str
c++的字符串是string类型的,是自定义类型,有些场景需要c语言的字符类型,c_str就是把提供这个接口。
比如文件操作,把test.cpp文件内容输出到终端
int main() { string s1 = "test.cpp"; FILE* pf = fopen(s1.c_str(), "r"); char ch = fgetc(pf); while (ch != EOF) { cout << ch; ch = fgetc(pf); } return 0; }
2.substr
一般和find搭配使用,还有rfind(从尾开始找),找到返回下标,没找到返回-1
int main() { //题目1,要求找出后缀 string s1 = "test.cpp";//后缀为.cpp string s2 = "head.tar.zip";//后缀为.zip size_t pos1 = s1.find('.', 0); if (pos1 != string::npos) { string suffix = s1.substr(pos1); cout << suffix << endl; } size_t pos2 = s2.rfind('.', s2.size()); if (pos2 != string::npos) { string suffix2 = s2.substr(pos2); cout << suffix2 << endl; } //题目2,要求分割出网址的协议,域名,路径 string s3 = "https://fanyi.youdao.com/index.html#/"; string agremt;//协议 size_t posAgremt = s3.find(':', 0); if (posAgremt != string::npos) { agremt = s3.substr(0, posAgremt - 0); cout << agremt << endl; } string daname;//域名 size_t posdaname = s3.find('/', posAgremt + 3); if (posdaname != string::npos) { daname = s3.substr(posAgremt + 3, posdaname - (posAgremt + 3)); cout << daname << endl; } string path; path = s3.substr(posdaname + 1); cout << path << endl; return 0; }
3.find_first_of
要求:把一个句子中的脏话相关的字母用‘x’替代
int main() { string s1 = "what happend fuck you,oh,shit,shut up"; const char* arr = "fuckst"; size_t pos = s1.find_first_of(arr, 0); while (pos != string::npos) { s1[pos] = 'x'; pos = s1.find_first_of(arr, pos + 1); } cout << s1 << endl; return 0; }
find用来查找单个字符,或者一个字符串
find_first_of则用来查找一个字符串中的任意一个字符
7. 非成员函数
当用cin输入字符串时,如果是一个有空格的句子,则有效输入是空格前面的字符。
int main() { string s1; cin >> s1; cout << s1 << endl; }
全局函数getline可以解决这个问题,这个函数用换行来结束字符输入
getline(cin, s1); cout << s1 << endl;
8.模拟是为了更好的理解
1.构造函数
string(const char* str = "") { _size = strlen(str); _capacity = _size; _str = new char[_capacity + 1]; strcpy(_str, str); }
1.这里为什么要给缺省值,并且缺省值为啥什么也没有?
字符串是用双引号“”引起来的内容。
缺省值不能是nullptr,因为空字符串表示没有内容,用nullptr初始化_str表示野指针,不表示指向内容为空。如果用cout来打印用nullptr初始化的string,程序会终止,原因是cout 识别类型char*后会打印内容,需要解引用,值为nullptre的_str被解引用,出现崩溃。
缺省值也不能是'\0',单引号表示字符,不能赋值给char*。
所以这里是“\0”或者“”,是同一个意思。
2.为什么_size先初始化?
为了复用strlen的值,这种写法只调用了一次strlen,减少消耗。
3._capacity为什么和_size值相等?
_capacity表示可以存储有效字符串的容量,不推荐带上'\0’计算
2.迭代器简单模拟
对指针typedef就可以简单的模拟迭代器,当然,真正的迭代器远不止此。
对于C++11支持的范围for,底层就是替换为迭代器,在调试窗口查看汇编代码 :
3. namespace个人版string
//一般是头文件写声明,源文件写定义,这里把所有内容写到了头文件
namespace ljy
{
class string
{
public:
//普通构造函数
string(const char* str = "")//缺省值是空字符串
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//string类的拷贝构造是深拷贝
string(const string& s)
{
string tmp = s._str;
swap(tmp);
}
//赋值重载
string& operator=(string s)
{
swap(s);
return *this;
}
//析构
~string()
{
delete[]_str;
_size = _capacity = 0;
}
//库中的c_str
const char* c_str()const
{
return _str;
}
size_t size()const
{
return _size;
}
//库中的resrve
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
//push_back
void push_back(char ch)
{
if (_size == _capacity)
{
size_t NewCapacity = (_size == 0) ? 4 : _capacity * 2;
reserve(NewCapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//append
void append(const char* s)
{
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//strcpy会拷贝\0
strcpy(_str + _size, s);
_size += len;
}
//+=
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* s)
{
append(s);
return *this;//运算符一般都有返回值
}
//在某个位置插入
void insert(size_t pos,char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t NewCapacity = (_capacity == 0) ? 4 : _capacity * 2;
reserve(NewCapacity);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[end] = ch;
++_size;
}
void insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
int end = (int)_size;
while (end >=(int)pos)
{
_str[end + len] = _str[end];
--end;
}
strncpy(_str+pos, s, len);
_size += len;
}
//删除pos开始的n个字符
void 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;
}
}
//同库中的[]一样,需要实现const和非const,非const由const修饰的对象调用,只读不写
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
const char& operator[](size_t i)const
{
assert(i < _size);
return _str[i];
}
//简单的迭代器
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
//swap
void swap(string& s)
{
if (this != &s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
}
//从pos开始找字符ch,返回下标
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 -1;
}
//从下标pos开始找字符串,返回下标
size_t find(const char* s, size_t pos = 0)
{
assert(pos <= _size);
const char* tmp = strstr(_str, s);
if (tmp == nullptr)
{
return -1;
}
else
{
return tmp - _str;
}
}
//substr的意义在于取字符串,参数本质是区间,找字符串的工作由程序员做
string substr( size_t pos = 0,size_t len = npos)const
{
assert(pos <= _size);
size_t end = pos + len;
if (len == npos || pos + len >= _size)
{
end = _size;
}
string ret;
ret.reserve(end - pos);
for (size_t i = pos; i < end; i++)
{
ret += _str[i];
}
return ret;
}
void clear()
{
_size = 0;
_str[_size] = '\0';
}
const static size_t npos = -1;
private:
char* _str;
size_t _size;
size_t _capacity;
};
ostream& operator<<(ostream& out, const string& s)
{
out << s.c_str();
return out;
}
istream& operator>>(istream& in, string& s)
{
//库中的cin遇到空白符会停下来,不再读取空白符,所以对于字符串,重载的时候如果用cin,是读取不到\0
s.clear();
//由于不知道要输入字符串的长度,所以每次读取后都可能会发生扩容,为了减少这部分消耗,也可以采用类似缓冲区的数组,先把读取到的字符存入数组
char buff[128] = {0};
char ch = in.get();
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
9.string的大小
按照我们仿写的string类,string类的对象的大小是12个字节
char* _str; size_t _size; size_t _capacity;
但是C++语言并不严格拘束类中具体是如何实现的,只需提供库中应有的接口即可,这就使不同的编译器有不同的实现方式。比如g++
由于Linux默认是64位,所以8字节即一个指针的大小,该指针指向一块堆空间,指针右是字符串,指针左是一个结构体,包含:
空间总大小(容量)
字符串有效长度(size)
引用计数
为了解释引用计数,我先来说一下关于浅拷贝的问题:
1.由于浅拷贝,可能会出现对同一块空间析构两次的情况,这时有野指针问题
2.由于同一块空间被多个变量指向,其中一个变量修改会引发其他变量指向的空间也发生改变。
为了解决浅拷贝问题,用写时拷贝+引用计数的方法来解决,简单来说,就是计数有多少个变量指向同一块空间,当一个变量销毁后,引用计数就会减一,这样可以解决浅拷贝的第一个问题。而写时拷贝,就是当你需要修改这块空间的时候,才拷贝一份,这样可以让许多不需要修改空间的场景不用拷贝空间。