目录
后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教! ——By 作者:新晓·故知
GIF:
1. 为什么学习string类?
1.1 C语言中的字符串
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数, 但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
1.2 两个面试题
1.2.1字符串转整形数字
1.2.2字符串相加
在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数类
2. 标准库中的string类
2.1 string类(了解)
1. 字符串是表示字符序列的类2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。总结:1. string是表示字符串的字符串类2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;4. 不能操作多字节或者变长字符的序列。在使用string类时,必须包含#include头文件以及using namespace std;string是模板,只是被typedef了![]()
![]()
![]()
string的使用举例:
int main() { string path = "C:\\"; path += "Visual Studio2019文件夹\\"; path += "C++"; cout << path << endl; return 0; }
// string constructor #include <iostream> #include <string> int main () { std::string s0 ("Initial string"); // constructors used in the same order as described above: std::string s1; std::string s2 (s0); std::string s3 (s0, 8, 3); std::string s4 ("A character sequence"); std::string s5 ("Another character sequence", 12); std::string s6a (10, 'x'); std::string s6b (10, 42); // 42 is the ASCII code for '*' std::string s7 (s0.begin(), s0.begin()+7); std::cout << "s1: " << s1 << "\ns2: " << s2 << "\ns3: " << s3; std::cout << "\ns4: " << s4 << "\ns5: " << s5 << "\ns6a: " << s6a; std::cout << "\ns6b: " << s6b << "\ns7: " << s7 << '\n'; return 0; }
2.2 string类的常用接口说明(最常用的接口)
1. string类对象的常见构造
![]()
void Teststring() { string s1; // 构造空的string类对象s1 string s2("hello bit"); // 用C格式字符串构造string类对象s2 string s3(s2); // 拷贝构造s3 }
![]()
遍历string的每一个字符:
![]()
//遍历string的每一个字符 void TestString1() { string s1("hello"); cout << s1.size() << endl; //计算字符串的size,不被包含\0 //第一种遍历方式:下标+[] [是运算符重载 //C++的内置类型数组可以用[]直接进行运算,但自定义类型需要通过函数重载实现 //通过重载让自定义类型的使用运算,像内置类型一样简便 for (size_t i = 0; i < s1.size(); i++) { cout << s1[i] << " "; //s1[i]相当于s1.operator[](i); } cout << endl; //C语言思想: /*const char* s2 = "world"; s2[i]; s2[i]->*(s2+i) */ //编译器根据识别的类型,(自定义类型或内置类型),调用函数重载或者对应函数 //第二种遍历方式:迭代器:像指针一样的东西 string::iterator it=s1.begin(); //指定类域,有可能直接定义,有可能typedef //begin 指向的是第一个数据 while (it != s1.end()) //end指向的是最后字符串只读结尾的下一个位置,即\0 { cout << *it << " "; ++it; } cout << endl; //第三种遍历方式:范围for 原理:编译器将其替换成迭代器 //通过汇编代码可以看出 for (auto ch : s1) { cout << ch << " "; } cout << endl; } int main() { TestString1(); return 0; }
2. string类对象的容量操作
// size/clear/resize void Teststring1() { // 注意:string类对象支持直接用cin和cout进行输入和输出 string s("hello, bit!!!"); cout << s.size() << endl; cout << s.length() << endl; cout << s.capacity() << endl; cout << s << endl; // 将s中的字符串清空,注意清空时只是将size清0,不改变底层空间的大小 s.clear(); cout << s.size() << endl; cout << s.capacity() << endl; // 将s中有效字符个数增加到10个,多出位置用'a'进行填充 // “aaaaaaaaaa” s.resize(10, 'a'); cout << s.size() << endl; cout << s.capacity() << endl; // 将s中有效字符个数增加到15个,多出位置用缺省值'\0'进行填充 // "aaaaaaaaaa\0\0\0\0\0" // 注意此时s中有效字符个数已经增加到15个 s.resize(15); cout << s.size() << endl; cout << s.capacity() << endl; cout << s << endl; // 将s中有效字符个数缩小到5个 s.resize(5); cout << s.size() << endl; cout << s.capacity() << endl; cout << s << endl; } //==================================================================================== void Teststring2() { string s; // 测试reserve是否会改变string中有效元素个数 s.reserve(100); cout << s.size() << endl; cout << s.capacity() << endl; // 测试reserve参数小于string的底层空间大小时,是否会将空间缩小 s.reserve(50); cout << s.size() << endl; cout << s.capacity() << endl; } // 利用reserve提高插入数据的效率,避免增容带来的开销 //==================================================================================== void TestPushBack() { string s; size_t sz = s.capacity(); cout << "making s grow:\n"; for (int i = 0; i < 100; ++i) { s.push_back('c'); if (sz != s.capacity()) { sz = s.capacity(); cout << "capacity changed: " << sz << '\n'; } } } void TestPushBackReserve() { string s; s.reserve(100); size_t sz = s.capacity(); cout << "making s grow:\n"; for (int i = 0; i < 100; ++i) { s.push_back('c'); if (sz != s.capacity()) { sz = s.capacity(); cout << "capacity changed: " << sz << '\n'; } } }
注意:1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。2. clear()只是将string中有效字符清空,不改变底层空间大小。3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
3. string类对象的访问及遍历操作
iterator:void Teststring() { string s1("hello Bit"); const string s2("Hello Bit"); cout << s1 << " " << s2 << endl; cout << s1[0] << " " << s2[0] << endl; s1[0] = 'H'; cout << s1 << endl; // s2[0] = 'h'; 代码编译失败,因为const类型对象不能修改 } void Teststring() { string s("hello Bit"); // 3种遍历方式: // 需要注意的以下三种方式除了遍历string对象,还可以遍历是修改string中的字符, // 另外以下三种方式对于string而言,第一种使用最多 // 1. for+operator[] for (size_t i = 0; i < s.size(); ++i) cout << s[i] << endl; // 2.迭代器 string::iterator it = s.begin(); while (it != s.end()) { cout << *it << endl; ++it; } string::reverse_iterator rit = s.rbegin(); while (rit != s.rend()) cout << *rit << endl; // 3.范围for for (auto ch : s) cout << ch << endl; }
//迭代器:4种 void TestString2() { string s1("hello"); //1.正向迭代器 string::iterator it = s1.begin(); //指定类域,有可能直接定义,有可能typedef //begin 指向的是第一个数据 while (it != s1.end()) //end指向的是最后字符串只读结尾的下一个位置,即\0 { cout << *it << " "; ++it; } cout << endl; //2.反向迭代器 string::reverse_iterator rit = s1.rbegin(); while (rit != s1.rend()) { cout << *rit << " "; ++rit; } cout << endl; } int main() { TestString2(); return 0; }
//迭代器:4种 void Func(const string& s) { //正向只读迭代器 string::const_iterator it = s.begin(); while (it != s.end()) { cout << *it << " "; ++it; } cout << endl; //反向只读迭代器 string::const_reverse_iterator rit = s.rbegin(); while (rit != s.rend()) { cout << *rit << " "; ++rit; } cout << endl; } void TestString3() { string s1("hello"); //正向迭代器、反向迭代器可以读写,遇到只能读权限的情况就需要只读迭代器 Func(s1); cout << endl; } int main() { TestString3(); return 0; }
lenth和size:
capacity:
4. string类对象的修改操作
注意:1. 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般 情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。扩空间:(尾插版)![]()
void TestPushBack() { string s; cout << s.capacity() << endl; size_t sz = s.capacity(); cout << "making s grow:\n"; for (int i = 0; i < 200; ++i) { s.push_back('c'); if (sz != s.capacity()) { sz = s.capacity(); cout << "capacity changed: " << sz << '\n'; } } } int main() { TestPushBack(); return 0; }
reserve:
resize:
insert:头插
insert插入字符:
void TestString1() { string s1("hello"); s1.insert(0, 1, 'x'); //在第一个位置插入数据 cout << s1 << endl; s1.insert(3, 1, 'y'); //在第3个位置插入数据 cout << s1 << endl; s1.insert(2, 1, 'x'); s1.insert(s1.begin() + 2, 'q'); //在第2个位置通过迭代器插入数据 cout << s1 << endl; } int main() { TestString1(); return 0; }
insert插入字符串:
erase:
![]()
//erase void TestString3() { string s1("hello,world"); s1.erase(s1.begin()); //通过使用迭代器头删 cout << s1 << endl; s1.erase(s1.begin()+3); //通过使用迭代器删除指定位置 cout << s1 << endl; s1.erase(3,2); //第3个位置后删除n个 cout << s1 << endl; } int main() { TestString3(); return 0; }
swap:
//swap void TestString3() { string s1("hello,world"); string s2("string"); //C++98中 s1.swap(s2); //string中的swap,交换指针,效率高 //swap(s1, s2); //STL中的swap,是全局的,可交换任意类型,深拷贝交换,代价大,效率低 cout << s1 << endl; cout << s2 << endl; } int main() { TestString3(); return 0; }
find:
//find void TestString4() { // 获取file的后缀 string file("string.cpp"); size_t pos = file.find('.'); //正着找 if (pos != string::npos) { string suffix(file.substr(pos, file.size() - pos)); //string suffix(file.substr(pos)); 单个形参,有多少取多少 cout << file << "后缀:" << suffix << endl; } else { cout << "没有后缀" << endl; } } int main() { TestString4(); return 0; }
反着找:
利用string对网址操作:
void TestString5() { // 取出url中的域名 string url("http://www.cplusplus.com/reference/string/string/find/"); cout << url << endl; string protocol; //协议 size_t pos1 = url.find("://"); if (pos1!= string::npos) { protocol = url.substr(0, pos1); cout << "protocol:"<<protocol<< endl; } else { cout << "未找到协议,非法url" << endl; } string domain; //取出域名 size_t pos2 = url.find('/',pos1+3); if (pos2 != string::npos) { domain = url.substr(pos1+3, pos2-(pos1+3)); cout << "domain:" << domain << endl; } string uri; //资源 uri = url.substr(pos2 + 1); cout << "uri:" << uri << endl; // 删除url的协议前缀 pos1 = url.find("://"); url.erase(0, pos1 + 3); cout << url << endl; } int main() { TestString5(); return 0; }
5. string类非成员函数
上面的几个接口了解一下,下面的OJ题目中会有一些体现它们的使用。string类中还有一些其他的操作,这里不一一列举,在需要用到时不明白了查文档即可。
6. 题目练习:
6.1仅仅反转字母:
class Solution { public: //判断是否为字符 bool isLetter(char ch) { if(ch>='a'&&ch<='z') return true; else if(ch>='A'&&ch<='Z') return true; else return false; } string reverseOnlyLetters(string s) { int left=0,right=s.size()-1; while(left<right) { while(left<right&&!isLetter(s[left])) ++left; while(left<right&&!isLetter(s[right])) --right; swap(s[left],s[right]); ++left; --right; } return s; } };
6.2找字符串中第一个只出现一次的字符
class Solution { public: int firstUniqChar(string s) { int count[26]={0}; //统计次数 for(auto ch : s) { count[ch-'a']++; } for(size_t i=0;i<s.size();++i) { if(count[s[i]-'a']==1) { return i; } } return -1; } };
6.3 字符串里面最后一个单词的长度
![]()
#include <iostream> #include <string> using namespace std; int main() { string str; //cin>>str; 缓存区的空格会干扰读取 getline(cin, str); size_t pos = str.rfind(' '); if (pos != string::npos) { cout << str.size() - pos - 1 << endl; } else { cout << str.size() << endl; } return 0; }
6.4 验证一个字符串是否是回文:
class Solution { public: bool isLetterOrNumber(char ch) { return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); } bool isPalindrome(string s) { // 先小写字母转换成大写,再进行判断 for (auto& ch : s) { if (ch >= 'a' && ch <= 'z') ch -= 32; } int begin = 0, end = s.size() - 1; while (begin < end) { while (begin < end && !isLetterOrNumber(s[begin])) ++begin; while (begin < end && !isLetterOrNumber(s[end])) --end; if (s[begin] != s[end]) { return false; } else { ++begin; --end; } } return true; } };
6.5 字符串相加
class Solution { public: string addStrings(string num1, string num2) { int end1 = num1.size() - 1; int end2 = num2.size() - 1; int carry = 0; string retStr; while(end1 >= 0 || end2 >= 0) { int val1 = end1 >= 0 ? num1[end1] - '0' : 0; int val2 = end2 >= 0 ? num2[end2] - '0' : 0; int ret = val1 + val2 + carry; if (ret > 9) { ret -= 10; carry = 1; } else { carry = 0; } //retStr.insert(retStr.begin(),'0'+ret); //效率低 retStr += ('0' + ret); --end1; --end2; } if (carry == 1) retStr += '1'; reverse(retStr.begin(), retStr.end()); return retStr; } };