目录
STL
STL六大组件
标准库中的string类
string类
string类常用接口
构造函数
下标遍历[]
迭代器
范围for
push_back()
append()
insert()
operator+=
pop_back()
erase()
reserve
resize
clear
c_str()
substr()
find()
rfind()
find_first_of
getline
string库中的函数
to_string
从字符串装换为其他类型
深浅拷贝
拷贝构造的另一种写法
赋值运算符重载的另一种写法
在讲解string之前先来简单了解一下STL
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
为什么说他是标准库中的类呢,因为string比STL出现的还要早;就单单一个string类就有很多的函数接口,那这么多东西都要记住还是很困难的,但是不用担心,我们要记住的是其中常用的一些函数接口,不常用的记不住就去查文档https://cplusplus.com/reference/,个人认为这个文档还是非常好用的。
- string是表示动态增长的char类型字符数组的字符串类。
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名,typedef basic_string
string; - 不能操作多字节或者变长字符的序列。
- 在使用string类时,必须包含#include头文件以及using namespace std;
#include
using namespace std; string str = "abcdefg"; 在这个basic_string类下还有如图这样的,string使用的是typedef basic_string
string; 其他的就不是用char来实例化模板类,比如wstring使用wchar_t,就是宽字符,这样设计自然有它的道理在有些编码的地方需要使用它,主要使用的还是string。
构造函数
string (); string (const string& str); string (const char* s); string s1; // 空字符串 string s2 = "abcdefg"; // 构造+拷贝构造->编译器优化为直接构造,这最后当然也包括\0 cout << s1 << endl; // 这里重载了流插入和流提取 string s3(s2); // 拷贝构造,这里肯定要用深拷贝实现 string (const string& str, size_t pos, size_t len = npos); // 从sting对象的pos位置截取len长度的字符串 string s4(s2, 1, 2); // 从s2中下标为1的位置开始复制2个字符,上面是这个函数的声明 // 可以看到len有一个缺省参数是npos,len的长度为要几个字符,如果大于剩余的字符就拷贝到结尾 // 如果不传入参数就是npos // npos是一个静态成员变量,static const size_t npos = -1 // 这是一个无符号的值,所以是一个很大的值,意思就是取到结尾,npos后面用的还是很多的 s1 = s2; // 赋值重载
下标遍历[]
// string也支持下标访问 string s1("hello world"); cout << s1[0] << endl; char& operator[](size_t pos); // 也有const版本,const对象会去匹配 // 引用返回可以减少拷贝,也可以修改返回值 size_t size() const; // 返回字符的个数 for (int i = 0; i < s1.size(); i++) // 遍历每一个字符 {}
迭代器
// 另一种遍历方式,迭代器,现在先理解为一个指针就可以了 string s1("hello"); string::iterator it = s1.begin(); // 在string类下的迭代器,所以其他容器里也有迭代器 // it是一个名字。通常都是it,简单理解就是指针 // iterator begin(); 返回对象的初始位置的指针 // iterator end(); 返回对象的最后一个位置的下一个,所以begin和end的区间是左闭右开 while (it != s1.end()) // 意思就是迭代器一直向后走,直到遇到最后一个位置的下一个 { // *it 解引用拿到这个对象 it++; } // 还有一种就是反向迭代器 string s2("hello"); string::reverse_iterator rit = s2.rbegin(); // rbegin()返回最后一个位置 while (rit != s2.rend()) // rend()返回第一个位置的前一个 { //... rit++; } // 常量迭代器 string s3("hello"); string::const_iterator cit = s3.cbegin(); while (cit != s3.cend()) { //... cit++; } // 反向常量迭代器 string s4("hello"); string::const_reverse_iterator crit = s4.crbegin(); while (crit != s4.crend()) { //... crit++; }
相比于迭代器,string使用下标遍历更好;但是迭代器还是很重要,毕竟有的容器只能用迭代器,它是所有容器通用的访问方式。如果这个声明太长了,也可以使用auto it。如果使用const迭代器的话还是会有const对象和普通对象的权限问题,注意一下就可以。范围for
string s("hello"); // 范围for -- 自动迭代,自动判断结束 // 依次取s中每个字符,赋值给c // 它只可以正向遍历 for (auto c : s) {} // 范围for的底层其实就是迭代器,其实去查一下这两种写法的汇编就知道,基本是一样的 // 还有一个点就是范围for这里是赋值给c的,要是想修改就不行了 // c只是一份拷贝,它的值修改不会影响原来的值 // 所以这里也可以使用引用& for (auto& c: s) {}
push_back()
// 在字符串后添加一个字符 void push_back (char c); string s("hello"); s.push_back(' ');
append()
// 在字符串后追加一个字符串 string& append (const string& str); string& append (const char* s); string& append (const char* s, size_t n); // 追加指定的s字符串的前n个字符 string& append (size_t n, char c); // 追加n个字符c string s("hello"); s.append("_world")
insert()
string& insert (size_t pos, const string& str); string& insert (size_t pos, const char* s); string s("hello"); s.insert(0, "x"); // 在下标为0的位置前插入"x"
operator+=
string& operator+= (const string& str); string& operator+= (const char* s); string& operator+= (char c); string s("hello"); s += '_'; // +=一个字符 s += s; // +=一个对象 s += "world"; // +=一个字符串
pop_back()
void pop_back(); string s("hello"); s.pop_back(); // 删除字符串的最后一个字符
erase()
string& erase (size_t pos = 0, size_t len = npos); // 不指定删除个数就删到结尾 string s("hello") s.erase(0, 5) // 从下标为0的位置,删除5个字符
reserve
void reserve (size_t n = 0); string s; s.reserve(1000); // 设置string对象的空间大小为1000字节,最后也不一定是1000字节,会对齐
resize
void resize (size_t n); void resize (size_t n, char c); string s; s.resize(1000, 'x'); // 设置string对象的长度,可以开空间大小为1000字节并且初始化为'x'
clear
void clear(); string s; s.resize(100, 's'); s.clear(); // 清空string对象
c_str()
const char* c_str() const; // 返回这个字符串的const char*形式 string s("hello"); const char* ch = s.c_str();
substr()
string substr (size_t pos = 0, size_t len = npos) const; string s("hello world"); string s1 = s.substr(6, string::npos); // 从下标为6的位置开始截取字符串到结尾
find()
size_t find (const string& str, size_t pos = 0) const; size_t find (const char* s, size_t pos = 0) const; size_t find (const char* s, size_t pos, size_t n) const; size_t find (char c, size_t pos = 0) const; string s("hello_world"); size_t pos = s.find('_', 0); // 从下标为0的位置开始找字符'_',找到了返回这个字符的下标,找不到返回npos的值
rfind()
size_t rfind (const string& str, size_t pos = npos) const; size_t rfind (const char* s, size_t pos = npos) const; size_t rfind (const char* s, size_t pos, size_t n) const; size_t rfind (char c, size_t pos = npos) const; string s("hello_world"); size_t pos = s.rfind('_'); // 从最后一个位置开始找,找不到返回npos
find_first_of
size_t find_first_of (const string& str, size_t pos = 0) const; size_t find_first_of (const char* s, size_t pos = 0) const; size_t find_first_of (const char* s, size_t pos, size_t n) const; size_t find_first_of (char c, size_t pos = 0) const; string str ("hello world"); size_t found = str.find_first_of("aeiou"); // 修改了文档的实例,匹配"aeiou"中的任意一个字符就可以 while (found!=std::string::npos) { str[found]='*'; // 把这些字符替换成'*' found=str.find_first_of("aeiou",found+1); }
getline
istream& getline (istream& is, string& str); // 相比较与cin,它可以输入带有空格符的字符串 string s; getline(cin, s);
to_string
string to_string (int val); // 可以把int类型装换为string类型,不止是int、float、long等 int n = 123456; string s = to_string(n);
从字符串装换为其他类型
// 只举例了两种,还有很多种可以去看文档 string is = "1234"; string ds = "1234.00" int n = stoi(is); double d = stod(ds);
在我们包含的string库中是不会出现这样的问题,别人都已经解决了,这个问题也不是很难。之前说的浅拷贝的问题是拷贝构造函数不写编译器会默认生成一个函数,按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,这样两个对象指向同一块空间,作用域结束后调用析构函数,这样同一块空间析构两次肯定会出问题,既然指向同一块空间出问题,那就再开一块空间分配个这个对象。
赋值操作也会出问题,我们不写编译器会默认生成一个赋值运算符重载,这也是浅拷贝,会出现两个问题,第一个就是最后会析构两次,还有一个问题就是被赋值的对象原来所指向的空间丢失,这样会导致内存泄漏。
class string // 这里简单模拟实现一下string { public: string(const& string s) :_str(new char[s._capacity+1]) // 初始化列表,新开一块空间并且多开一个存放\0 ,_size(s._size) ,_capacity(s._capacity) { strcpy(_str, s._str); // 再把s1的值拷贝过来 } string& operator=(const satring& s) // 直接释放s1原来的空间,再申请一块和s3同样大小的空间就可以了 { // 还要注意自己给自己赋值的情况,因为语法是支持这样做的 // 如果不处理,开始就会直接释放自己的空间,这样也就没办法赋值了 if (this != &s) { char* tmp = new char[s._capacity+1]; // +1位置存放\0, 先去开空间,保证空间开辟不出错,再去赋值和释放 strcpy(tmp, s._str); delete[] _str; _str = tmp; _size = s._size; _capacity = s._capacity; } return *this; } private: char* _str; size_t _size; size_t _capacity }; int main() { string s1("hello"); string s2(s1); // 用s1拷贝构造s2 string s3("123456"); s1 = s3; // 赋值操作也是有问题的,编译器生成的默认赋值,把s1指向s3指向的空间,但是原来s1指向的空间丢失,会造成内存泄露的问题 return 0; }
拷贝构造的另一种写法
// string s2(s1) string(const string& s) :_str(nullptr) ,_size(0) ,_capacity(0) { string tmp(s._str); // 这里调用了构造函数,开辟一块新的空间给tmp // 再把所有的成员变量交换,这样就把s1赋值给了s2,拷贝构造结束后,tmp会自动析构 // 但是tmp交换完之后就是原来s2的值,s2原来没有开辟过,指向的空间是一块随机的 // delete随机空间是会出错的,但是delete nullptr不会,所以在初始化列表中初始化一下 swap(_str, tmp._str); swap(_size, tmp._size); swap(_capacity, tmp._capacity); }
赋值运算符重载的另一种写法
// s1 = s2 string& operator=(const string& s) { if (this != &s) { string tmp(s); swap(_str, tmp._str); swap(_size, tmp._size); swap(_capacity, tmp._capacity); } return *this; } // 还有一种 string& operator=(string s) // 这里的s是一个临时变量,是拷贝构造出来的,出了作用域也会自动释放 { swap(_str, s._str); swap(_size, s._size); swap(_capacity, s._capacity); return *this; }