目录
9、增删查改(注意很多函数都重载了好几个函数,下述只举例常见的,具体可查阅文档)
11、replace和find联合使用,将字符串中的所有空格替换成%
12、c_str():将string类型转化为const char*
13、substr:从字符串pos位置开始,将往后len个字符组成新字符串返回。
14、find_first_of 和 find_last_of
15、find_first_not_of 和 find_last_not_of
1、为什么要学习string?
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP(面向对象编程)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
2、介绍
1. 字符串是表示字符序列的类。2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
3、特点
1. string是表示字符串的字符串类2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;4. 不能操作多字节或者变长字符的序列。5、在使用string类时,必须包含#include头文件以及using namespace std;
4、string类常用构造函数
1、string() :构造空的string类对象,即空字符串(重点)2、string(const char* s) :用C-string来构造string类对象(重点)3、string(const string&s) 拷贝构造函数(重点)4、string(size_t n, char c) :string类对象中包含n个字符c//空字符串 string test1; //用C-string构造string类对象 string test2("hello"); //拷贝构造 string test3 = test1; string test4(test2); //使string对象包含n个字符 string test5(5, 'a');
注意:string类是重载了流插入和留提取运算符,所以其对象可以直接使用cout和cin;
这里只举例常见的构造,具体可前往官网查看文档。
5、string类的析构函数
因为string类支持cin,所以底层实现是右new操作符动态开辟空间的,所以需要析构函数~string()释放,但析构函数是自动调用的,所以平时可以不管。
6、string类对象的遍历和访问
(1)、利用流插入运算符重载
(2)、string类运算符重载了[ ],所以可以用[ ]进行读写(注意[ ]重载提供了两个版本,有个版本用const修饰了this指针,返回值也会const引用,所以只可读不能写,具体查看文档)
int main() { string s1("hello world"); //读 for (int i = 0; i < s1.size(); i++) { cout << s1[i] << " "; } cout << endl; //写:字符串逆序 int begin = 0; int end = s1.size() - 1; while (begin < end) { swap(s1[begin], s1[end]); begin++; end--; } cout << s1; return 0; }
(3)、利用迭代器iterator(用法像指针):
int main() { string s1("hello"); string::iterator it = s1.begin(); while (it != s1.end()) { cout << *it << " "; ++it; } cout << endl; return 0; }
这里begin()指向第一个位置,end()指向数据内容的后一个位置;
因为[ ]只适合部分容器,如树形、哈希结构、链式结构等等只能用迭代器,迭代器才是容器访问的主流。
知道了迭代器,那么容器逆序可以用一个函数模版:
reverse(s1.begin(), s1.end());
reverse函数需要左闭右开的区间,而迭代器的begin和end刚好满足。
需要注意迭代器的两个写法:
(4)、范围for(适用于多种容器,相比上述几种方式是最便捷的,但底层会替换成迭代器)
string s1("hello"); for (auto e : s1) { cout << e << " "; } cout << endl;
7、string类对象的容量操作
(1)、size(重点) :返回字符串有效字符长度。(2)、length :返回字符串有效字符长度。(3)、capacity :返回空间总大小。(4)、empty (重点):检测字符串释放为空串,是返回true,否则返回false。(5)、clear (重点) :清空有效字符。(6)、reserve (重点) :为字符串预留空间,但不会缩容,也不会影响字符串原数据,缩容代价大。(7)、resize (重点) :将有效字符的个数改成n个,多出的空间用字符c填充(即影响容量也影响数据)。解释:(1)、size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。(2)、 clear()只是将string中有效字符清空,不改变底层空间大小。(3)、resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用‘\0’(ascll码为0,此时的'\0'是有效字符,会被size()统计,不是标识字符)来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。(4)、reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。注意:(1)、通过下图几个操作我们会发现string对象当空间不足时,会自动扩容,不同编译器扩容规则不同,vs2022大概是1.5倍:如上图,第一次预留5个空间,但结果是15,第二次预留16个空间,但容量却是31,这表明,当string对象的空间不够时,它会以一个规则来扩容。这也表明了如果我们确定自己需要多少空间,就可以用reserve提前开好空间,就少去扩容的步骤,提高效率。
8、at和[ ]
两者用法相同,但有一些区别:
(1)、用[ ],越界会直接报错。
(2)、用at,越界会抛异常。
9、增删查改(注意很多函数都重载了好几个函数,下述只举例常见的,具体可查阅文档)
(1)、增:
int main() { string str1; //(1)、 //push_back插入一个字符 str1.push_back('1'); str1.clear(); //(2)、 //append插入字符串 str1.append("23"); //append插入一个string对象 string str2("56789"); str1.append(str2); str1.clear(); //append插入字符串的一部分,注意是左闭右开区间 str1.append(str2.begin() + 2, str2.end() - 1); cout << str1 << endl; str1.append("abcd", 0, 2); cout << str1 << endl; //(3)、 //+=运算符重载 str1 += "1234"; str1 += "s2"; //(4)、 //实现在某个位置插入字符串。 string str1("hello"); str1.insert(5, "xxxx"); //在头部插入5个A str1.insert(0, 5, 'A'); //用迭代器头插一个字符 str1.insert(str1.begin(), 'B'); return 0; }
(2)、删
int main() { string str1 = "hello"; //删除字符串str1的0位置处往后三个字符 str1.erase(0, 3); cout << str1; //尾删:pop_back string str2("hello"); str2.pop_back(); cout << str2 << endl; return 0; }
(3)、查:迭代器、[ ]
(4)、改:迭代器、[ ]
10、assign:赋值
string str2; //赋值一串字符串 str2.assign("hello"); cout << str2 << endl; //赋值字符串的一段区间,左闭右开 str2.assign("abcd", 0, 3); cout << str2 << endl;
不怎么使用,因为赋值直接构造更方便。
string str3("hello"); str3 = "a"; cout << str3;
11、replace和find联合使用,将字符串中的所有空格替换成%
replace:替换某个字符或字符串。
find:查找某个字符,并返回下标。若没有找到,返回string类的静态成员npos(值为-1)。
//将字符串中的所有空格替换成%
string str3("hello world I love you"); size_t pos = 0; while ((pos = str3.find(' ', pos + 1)) != string::npos) { str3.replace(pos, 1, "%"); } cout << str3 << endl;
但平时一定要少用replace,因为每次都要重复挪动很多数据,效率及其低。
下面是不使用replace,而使用+=运算符重载进行实现。
string str4("hello world I love you"); //借助临时字符串 string str5; for (auto ch : str4) { if (ch == ' ') { str5 += '%'; } else { str5 += ch; } } cout << str5 << endl; //如果想要在原字符串上进行修改,直接用成员函数swap,注意和算法函数swap不同 str4.swap(str5); cout << str4 << endl;
12、c_str():将string类型转化为const char*
例题1:打印文件内容
string fname = "test.cpp"; //因为fopen函数的第一个参数是const char*类型,而string类型不能直接给其赋值,所以用c_str返回c式类型 FILE* f = fopen(fname.c_str(), "r"); char ch = getc(f); while (ch != EOF) { cout << ch; ch = getc(f); }
13、substr:从字符串pos位置开始,将往后len个字符组成新字符串返回。
首先我们需要了解:
len参数的缺省值是npos(size_t中是整形最大值,int中是-1,与补码有关)
(1)、找出文件名的后缀
string str6 = "test.cpp"; size_t pos = str6.find('.'); string str7 = str6.substr(pos + 1); cout << str7 << endl; //文件名有多个点的情况,用rfind,反向寻找 string str8 = "test.tar.txt"; size_t pos1 = str8.rfind('.'); string str9 = str8.substr(pos1 + 1); cout << str9 << endl;
注意这里第二种情况,文件名中有多个点,这时就用了另一个成员函数rfind(),这个函数是从字符串尾部向头部进行查找,但返回的下标还是字符在字符串中的下标。
(2)、将网址的三部分分离(协议、域名、文件路径)
string str1 = "https://legacy.cplusplus.com/info/"; string sub1, sub2, sub3; size_t pos1 = str1.find(':'); sub1 = str1.substr(0, pos1); cout << sub1 << endl; size_t pos2 = str1.find('/', pos1 + 3); sub2 = str1.substr(pos1 + 3, pos2 - (pos1 + 3)); cout << sub2 << endl; sub3 = str1.substr(pos2 + 1); cout << sub3 << endl;
14、find_first_of 和 find_last_of
find_first_of:是从头向尾返回字符串A中在字符串B中出现的所有字符的下标
find_last_of:是从尾向头返回字符串A中在字符串B中出现的所有字符的下标
string str1 = "abcdeabcdeabcde"; size_t pos = str1.find_first_of("abc"); while (pos != string::npos) { str1.replace(pos, 1, "*"); pos = str1.find_first_of("abc", pos + 1); } cout << str1 << endl;
15、find_first_not_of 和 find_last_not_of
这两个函数的功能就是与上述两个函数相反:
返回字符串A中指定字符串B中出现的所有字符的下标。
string str1 = "abcdeabcdeabcde"; size_t pos = str1.find_first_not_of("abc"); while (pos != string::npos) { str1.replace(pos, 1, "*"); pos = str1.find_first_not_of("abc", pos + 1); } cout << str1 << endl;
16、getline:读取一段字符串,遇到换行结束
(1)、头文件:<string>
(2)、该函数主要用于读取一段带有空格符的字符串,平时用的cin留提取运算符是以空格符和换行符作为分隔,所以读取不了空格符。而该函数只以换行作为分隔。
int main() { string str; getline(cin, str); cout << str << endl; return 0; }
注意有两个参数,第一个是流对象,第二个字符串对象。
17、解决浅拷贝的问题
浅拷贝有两大问题:
(1)、会析构两次;
(2)、多个对象指向同一块空间,会互相影响。
解决:
解决(1):引用计数,记录这块空间有多少个对象在引用,当引用计数等于01时才析构。
解决(2):写实拷贝,即刚开始进行浅拷贝,哪个对象要对这份空间进行修改,就叫它重新拷贝一份。这样做的意义在于,后续可能没有对象会进行修改这时就赚了效率。
18、注意编码问题
19、string类模拟实现
typedef char* iterator; typedef const char* const_iterator; class string { private: size_t _size; size_t _capacity; char* _str; const static int npos = -1; public: string(const char* str = "") { _size = strlen(str); _capacity = _size; _str = new char[_size + 1]; strcpy(_str, str); } //传统写法 /*string(const string& str) { _str = new char[str._capacity + 1]; strcpy(_str, str._str); _size = str._size; _capacity = str._capacity; } string& operator=(const string& str) { if (this != &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); swap(tmp); } string& operator=(string str) { swap(str); return *this; } ~string() { delete[] _str; _size = 0; _capacity = 0; } //c_str() const char* c_str()const { return _str; } //size() size_t size()const { return _size; } //[ ] const char& operator[](size_t pos)const { assert(pos < _size); return _str[pos]; } char& operator[](size_t pos) { assert(pos <= _size); return _str[pos]; } //迭代器 iterator begin() { return _str; } iterator end() { return _str + _size; } const_iterator begin()const { return _str; } const_iterator end()const { return _str + _size; } void reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } void push_back(const char s) { if (_size == _capacity) { size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity; reserve(newCapacity); } _str[_size] = s; _size++; _str[_size] = '\0'; } void append(const char* str) { size_t len = strlen(str); if (len+_size > _capacity) { reserve(len + _size); } strcpy(_str + _size, str); _size += len; } string& operator+=(const char c) { push_back(c); return *this; } string& operator+=(const char* str) { append(str); return *this; } void insert(size_t pos, const char c) { assert(pos >= 0); if (_size == _capacity) { size_t newCapacity = _capacity == 0 ? 4 : 2 * _capacity; reserve(newCapacity); } size_t end = _size + 1; while (end > pos) { _str[end] = _str[end - 1]; end--; } _str[pos] = c; _size++; } void insert(size_t pos, const char* str) { assert(pos <=_size); size_t len = strlen(str); if (len + _size > _capacity) { reserve(len + _size); } int end = _size; while (end >= (int)pos) { _str[end + len] = _str[end]; end--; } strncpy(_str + pos, str, len); _size += len; } void erase(size_t pos, size_t len = npos) { assert(pos < _size); if (len == npos || len + pos >= _size) { _str[pos] = '\0'; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } } void swap(string& str) { std::swap(_str, str._str); std::swap(_size, str._size); std::swap(_capacity, str._capacity); } size_t find(const char* str,size_t pos = 0) { const char* tmp = strstr(_str + pos, str); if (tmp == nullptr) { return npos; } else { return tmp - _str; } } string substr(size_t pos, size_t len = npos) { assert(pos <= _size); size_t end = pos + len; if (len == npos || len + pos > _size) { end = _size; } string tmp; tmp.reserve(end - pos); for (size_t i = pos; i < end; i++) { tmp += _str[i]; } return tmp; } void clear() { _size = 0; _str[0] = '\0'; } }; ostream& operator<<(ostream& out, const string& str) { for (auto ch : str) { out << ch; } return out; } istream& operator>>(istream& in, string& str) { str.clear(); char ch; ch = in.get(); while (ch != ' ' && ch != '\n') { str += ch; ch = in.get(); } return in; }