[C++初阶] 9. STL--string使用(二)

一. string对象 和 字符数组 的区别

  1. string对象结尾多了一个\0。

  2. 这个 \0 不属于有效字符,只是一个标识符。所以在计算字符串长度时不算 \0 ,但是申请空间时要为\0留一个位置。

二. 迭代器

下图中的迭代器两个两个为一组分别为 普通迭代器(包含const迭代器)、普通反向迭代器(包含const反向迭代器)、const迭代器、const反向迭代器。

因为前两组已经包含后两组,虽然后两组设计出来是为了更好区分普通迭代器和const迭代器,但是实际应用比较少。

  1. 使用的角度把迭代器就当做一个指针,比如begin就是指向字符串中的第一个元素的,带不带const影响的只是这个"指针"指向的元素我们是否有权限进行读写。

  2. 这几个迭代器的指向是确定的,我们可以用它来为一个迭代器类型的变量赋值(保存到变量中)在对这个变量进行移动,却不可以直接对他++或--。因为他们返回的是临时对象,具有常性不可修改。

  3. 不同的迭代器有不同的类型:iterator、const_iterator、reverse_iterator、const_reverse_iterator

  4. 这四种类型都属于string这个类,是在类中定义的,所以使用时不仅要指明命名空间域,还要指明类域。

1)begin

2)end

  1. end() 也分普通版本和const版本。

  2. 它指向的不是最后一个字符的位置,而是最后一个字符的下一个位置 !(也就是\0)

3)rbegin

  1. rbegin 指向的是字符串的最后一个有效元素 (\0的前一个位置),他是反向的开始。

  2. 我们在使用反向迭代器时,希望他向字符串开头移动,也就是往左移,但是注意:我们习惯上可能认为左移应该--,但是反向迭代器++就是左移(向字符串开头移动)

4)rend

  1. 返回一个反向迭代器,指向字符串第一个元素的前一个位置

  1. begin 和 end 组成了一个左闭右开的区间。

  2. rbegin 和 rend 组成了一个左开右闭的区间。

  3. 他们都包含了所有元素。

  4. begin 和 rbegin 向目标方向移动的方式都是++,只不过他们标记的正方向不同。

  5. 类似的end 和 rend 向目标方向移动的方式都是--,只不过目标方向相反。

5)cbegin、cend、crbegin、crend

  1. 是C++11标准引进的,以cbegin为例:

  2. cbegin 的函数原型中有一个 noexcept,noexcept是 C++11 引入的关键字,表示这个成员函数承诺不会抛出任何异常。

6)迭代器使用示例

cpp 复制代码
void Test01()  // 迭代器
{
	string s1 = "hello world";

	// 不能对函数调用的返回值直接++
	// 所以先获取迭代器,再自增
	string::reverse_iterator rit = s1.rbegin();

	while (rit != s1.rend())
	{
		cout << *rit;
		++rit; // 反向迭代器++向左遍历,不要以为往左走就用--
	}
	cout << endl;
}

成功实现反向遍历,但实际反向迭代器并不常用。比如上篇最后的第一道算法题(仅仅反转字母)的思想,用双指针就可以实现逆置,不必使用反向迭代器。

7)const迭代器 -- 权限问题

1. 普通迭代器和const迭代器的异同

① 两版本迭代器本身的指向都可以修改。

② 都可以通过指针读取元素。

③ 普通版本可以通过迭代器对指向元素做修改(可写);const版本迭代器指向的内容不可以修改。

2. 调用时的权限问题(权限可以缩小,不可以放大)

① 一个const成员函数,普通对象和const对象都能调;普通成员函数只有普通对象能调。const成员函数内部不能修改类的成员变量。

② 当普通函数和const对象同时存在时,对象调用的一定是更匹配的。

cpp 复制代码
void func(const string& s)
{
	string::const_iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it;
		++it;
	}
	cout << endl;
}
void Test02()   // const 迭代器
{
	string s1 = "hello world";
	func(s1); // 普通对象可以传给const对象,权限缩小(从rw变成r)
}

3. 区分 const_iterator 和 const iterator

① const_iterator:迭代器指向的内容不能修改。

② const iterator:迭代器自身的指向不能修改。

三. capacity组(容量相关)

1)size、length、max_size、capacity

  1. size :有效字符个数。推荐使用,每个容器都有一个size()表示有多少个数据,更通用。

  2. length:有效字符个数。和size功能相同,但是是string独有的,不具有通用性,所以用的比较少。

  3. max_size:固定值,表示字符串理论上能达到的最大长度(只是个理论值,实际是达不到的)。没什么用。

  4. capacity :字符串当前已分配的内存能够容纳的字符数量(不包括\0)。capacity >= size,capacity是能容纳的字符个数,size是实际有的字符个数,当capacity不够了,会自动发生扩容。

cpp 复制代码
void Test03()  // capacity组
{
	string s1("hello world");
	cout << s1.size() << endl;
	cout << s1.length() << endl;
	cout << s1.max_size() << endl;
	cout << s1.capacity() << endl;
}

2)clear、empty

5. clear: 清空,只清理字符,不清理空间。即令size = 0, capacity不变。

6. empty: 判断字符串是否为空。为空返回真,不为空返回假。


3)shrink_to_fit、reserve、resize、扩容机制

7. shrink_to_fit: 缩容。很少用,开销比较大。

string的扩容机制

  1. C++标准只规定了空间不足是要发生扩容,却没有具体规定要如何扩(一次扩二倍?1.5倍?并没有规定),所以不同编译器在设计时采取的具体的扩容机制是不同的。

  2. VS(msvc)中:第一次扩2倍,之后都扩1.5倍。

cpp 复制代码
void Test04()   // string扩容机制
{
	string s;
	int old_capacity = s.capacity();
	cout << old_capacity << endl;

	for (size_t i = 0; i < 200; i++)
	{
		// push_back:向字符串尾部插入
		s.push_back('y');

		if (s.capacity() != old_capacity)
		{
			cout << s.capacity() << endl;
			old_capacity = s.capacity();
		}
	}
}
  1. Linux(g++)中:全部2倍扩容。

8. reserve:请求更改容量到n。

① 扩容:一定会扩。实际在使用reserve时我们只是用它的扩容功能。申请扩到n,他可能会扩到n或更大(可能因为内存对齐或者什么原因)。

② 扩容功能使用场景:确定大约会用多大空间,直接一把开够,减少扩容开销。

③ 缩容:缩容被视为非绑定请求,缩不缩都不一定,取决于平台。所以我们不能用这个功能,不然可能在这个平台缩了能用,换一个平台代码就跑不了了。如果实在需要缩容就用shrink_to_fit。

并且即使缩容了,也最多缩到size,不会影响数据。


cpp 复制代码
void Test05() // reserve
{
	string s = "yyyyyyyyyyy";
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 扩容
	s.reserve(100);
	cout << s.size() << endl;
	cout << s.capacity() << endl;

	// 缩容
	s.reserve(10);
	cout << s.size() << endl;
	cout << s.capacity() << endl;
}

④ VS下只扩容,不缩容。并且我要开100,他实际开的可能更多。

⑤ Linux下,使用g++编译的,缩容扩容都会做。并且我要缩到10,为了不影响数据,只缩到了11(也就是size的大小)。

9. resize: 调整元素个数到 n(size大小)。

n的大小有三种情况:

① size < n < capacity :插入数据。

② n < size:删除数据。

③ n > capacity :扩容之后插入数据。

插入的数据如果指定了字符c,则插入c。如果没有指定则用'\0'。

四. 元素访问相关

1)[]下标访问:返回对字符串中pos位置的字符的引用

  1. 普通版本:可读可写。

  2. const版本:只读。

2)at:返回对字符串中pos位置的字符的引用

  1. 该函数自动检查pos是否为字符串中字符的有效位置(即pos是否小于字符串长度),如果不是,则抛出out_of_range异常。

  2. 它的功能和[]一样。

  3. \]是运算符重载,at是函数。

① []:断言报错,直接终止程序。但是断言只在debug模式下起作用。

② at:抛异常,捕获后可继续运行。

3)back、front

  1. back:访问最后一个字符。

  2. front:访问第一个元素。

  3. 这两个一般不用:s.back() 可被 s[s.size() - 1] 代替;s.front() 可被 s[0] 代替。[] 是更常用的方法。

五. 修改相关

1)push_back、append、+=、pop_back

1. push_bach: 尾插,从字符串的尾部插入一个字符。
2. append: 追加,尾插一个串(一个字符也是一个串)。

3. += : 在尾部追加,综合了1,2最常用的功能,并且可读性很高,更好用。

4. pop_back: 尾删,删除字符串最尾部的一个元素,字符串长度减一。

  • 使用演示
cpp 复制代码
void Test08()
{
	string s1 = "hello ";
	string s2 = "world";

	s1.append(s2);
	cout << s1 << endl;

	s1.append(" hello ls");
	s1.append("y");
	cout << s1 << endl;

	s1.push_back('!');
	cout << s1 << endl;

	s1 += " hell";
	s1 += 'o';
	s1 += " ";
	s1 += s2;
	cout << s1 << endl;

	for(size_t i = 11; i > 0; i--)
	{
		s1.pop_back();
	}
	cout << s1 << endl;
}

2)insert、erase

因为头插头删需要挪动数据,效率低,所以没有提供直接可使用的接口。不建议做头插或头删,但是我们如果有需求,也提供了间接的接口 -- insert、erase。

1. insert: 在指定位置插入字符或字符串。底层要先挪动数据,再插入,效率不高。

2. erase: 删除元素。很可能要挪动数据,从而覆盖删除,效率不高。

① 删除字符串中从pos位置开始的len个字符。如果使用缺省值作用类似clear,清除字符串中所有元素。

② 删除迭代器指向的一个字符。

③ 删除迭代区间( [first, last),左闭右开)内的所有字符。

cpp 复制代码
void Test09()
{
	string s1 = "lllll";
	string s2 = "sss";

	s1.insert(3, s2);
	cout << s1 << endl; // lllsssll

	s1.insert(6, "yyy");
	cout << s1 << endl; // lllsssyyyll

	// 头插
	s1.insert(0, 1, 'i');
	s1.insert(s1.begin(), 'h');
	cout << s1 << endl; // hilllsssyyyll

	// 头删
	s1.erase(0, 2);
	cout << s1 << endl; // lllsssyyyll

	s1.erase(s1.end() - 1);
	cout << s1 << endl; // lllsssyyyl

	s1.erase(s1.begin(), s1.begin() + 3);
	cout << s1 << endl; // sssyyyl

	s1.erase(); // 清空
	cout << s1 << endl;
}

3)assign、replace

1. assign: 为字符串赋一个新值,替换其当前内容。很少用。

2. replace: 用新内容替换字符串中从字符pos开始并跨越len字符的部分(或字符串中位于[i1,i2)之间的部分)。

如果不是等比例替换,而是用长字符串替换短字符串,或者用短字符串替换长字符串,就要挪动数据,效率不高。

将字符串中所有空格变成%%

  1. 解法一:使用replace原地换

效率很低,不推荐

  1. 解法二:开一个新串

六. String operations组

1)c_str

返回C风格的字符数组,最后一个元素是'\0'。

cpp 复制代码
void Test11() // c_str
{
	string s = "Test.cpp"; // windows文件系统不区分大小写
	FILE* fout = fopen(s.c_str(), "r");
	char ch = fgetc(fout);
	while (ch != EOF)
	{
		cout << ch;
		ch = fgetc(fout);
	}
}

2)substr

取pos位置开始的len个字符并返回。不传参默认全取完。

3)find、rfind

  1. find

从pos位置开始查找完全匹配的字符。

返回第一个匹配的第一个字符的位置。如果没有找到匹配项,函数返回npos。

  1. rfind:从后往前找。

例1:1. find

2. rfind

例2:把一个网址的协议、域名、资源分别解析出来。

解析需要找两个位置,pos1和pos2,通过两个位置将网址分成三个子串sub1, sub2, sub3分别对应协议、域名、资源。

pos1比较好找,find第一个冒号即可。

pos2从前、从后都不太好找,但是find支持从指定位置开始找,所以从pos1 + 3开始的第一个 / 就是pos2。

cpp 复制代码
	string s2 = "https://legacy.cplusplus.com/reference/string/string/find/";

	size_t pos1 = s2.find(":");
	if (pos1 != string::npos)
	{
		string sub1 = s2.substr(0, pos1);
		cout << sub1 << endl;
	}

	size_t pos2 = s2.find("/", pos1 + 3);
	if (pos2 != string::npos)
	{
		//string sub2 = s2.substr(pos1 + 3, pos2); // 注意第二个参数是子串长度,不是结束位置
		string sub2 = s2.substr(pos1 + 3, pos2-(pos1+3)); 
		string sub3 = s2.substr(pos2 + 1);
		cout << sub2 << endl << sub3 << endl;
	}

4)find_fist_of、find_last_of、find_first_not_of、find_last_not_of

  1. find_first_of

为了方便理解可以记为find_any_of,即从指定位置开始只要找到字符串里的任意字符就返回。返回值为匹配的第一个字符的位置。如果没有找到匹配项,则返回string::npos。

cpp 复制代码
	string str("Please, replace the \"abcd\" in this sentence by asterisks.");
	cout << str << endl;
	size_t found = str.find_first_of("abcd");
	while (found != string::npos)
	{
		str[found] = '*';
		found = str.find_first_of("abcd", found + 1); // 下一次从found+1位置开始找
	}
	// 至此已经将str中的所有abcd用*替换了
	cout << str << endl;
  1. find_last_of

从后往前找。

cpp 复制代码
	string str("Please, replace the \"abcd\" in this sentence by asterisks.");
	cout << str << endl;
	size_t found = str.find_last_of("abcd");
	str[found] = '*';
	// find_last_of这里只找一个,验证确实是从后往前找
	cout << str << endl;
  1. find_first_not_of

不是指定字符串里的字符就返回。

cpp 复制代码
	string str("Please, replace the \"abcd\" in this sentence by asterisks.");
	cout << str << endl;
	size_t found = str.find_first_not_of("abcd");
	while (found != string::npos)
	{
		str[found] = '*';
		found = str.find_first_not_of("abcd", found + 1); // 下一次从found+1位置开始找
	}
	// 至此已经将str中的所有不是abcd的字符用*替换了
	cout << str << endl;

5)compare

将字符串对象或其子串的值与参数指定的字符序列按ASCII码进行比较。不常用,因为string重载了关系运算符。

七. Non-member function overloads非成员函数重载组

非成员函数是全局的,但是不一定要重载为类的友元,除非函数内要访问类的私有成员。
那么为什么这些函数不能重载为类的成员函数,而必须是全局的呢?
类似我们前面说的流插入和流提取的重载,因为类的成员函数的第一个参数类型是固定的--当前类类型的this指针。所以如果重载为类的成员函数,第一个参数必须是类类型。这不能满足我们左操作数可能不是当前类类型的需求,因此必须重载为全局的函数,参数类型可自定义。

1)relational operators关系运算符

每个运算符都重载了三种参数,字符串与字符串、字符数组与字符串、字符串与字符数组。这样以下三种情况都支持了,但是是有一点冗余的,因为单参数构造支持隐式类型转换,后两种重载即使不给出,字符数组也会在传参时自动隐式类型转换为字符串类型。

cpp 复制代码
	const char* str = "lsy";
	string s1("lll"), s2("sss");

	// 以下三种情况都支持了
	s1 == s2; // 字符串与字符串
	str == s1; // 字符数组与字符串
	s1 == str; // 字符串与字符数组

2)operator+

字符串拼接(字符串也可与字符数组、字符拼接)

3)流提取operator>>、流插入operator<<

  1. istream& operator>> (istream& is, string& str);

  2. ostream& operator<< (ostream& os, const string& str);

4)getline

  1. 获取整行。

  2. cin和scanf都认为空格和回车是值的分隔符,所以cin一个"hello world",实际变量只存储到"hello"。

此时就需要getline:获取一行,默认以换行为分隔符。

那么希望字符串里有换行符呢?getline支持指定分隔符。

  1. 参数:一个istream对象,即cin。字符串目标存放位置。可选:分隔符。

cpp 复制代码
	// 指定分隔符
	string s3;
	getline(cin, s3, '$'); // hello world\nhi\n$ -> hello world\n hi\n
	cout << s3 << endl;

八. 转换函数

默认转十进制,可以设置参数,指定用什么进制。

九. 流的标志位

while(cin>>s1){} 和 while(getline(cin, s1)){} 可以实现循环多组输入,按Ctrl+z回车可以结束,为什么呢?
因为流都有四个标志流的状态的参数:


Ctrl+z可以将eofbit置为true,代表该流出现异常,不可用了。
标识完又是如何判断的呢?
自定义类型转内置类型要重载一个operator bool,将cin或getline返回的istream对象转换为bool值,在operator bool函数内部检查几个标志的设置情况。

十. 算法题

https://leetcode.cn/problems/add-strings/description/

https://leetcode.cn/problems/valid-palindrome/description/

https://www.nowcoder.com/practice/8c949ea5f36f422594b306a2300315da?tpId=37&&tqId=21224&rp=5&ru=/activity/oj&qru=/ta/huawei/question-ranking

1)415. 字符串相加

题目描述

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。

你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。

示例 1:

复制代码
输入:num1 = "11", num2 = "123"
输出:"134"

示例 2:

复制代码
输入:num1 = "456", num2 = "77"
输出:"533"

示例 3:

复制代码
输入:num1 = "0", num2 = "0"
输出:"0"

提示:

  • 1 <= num1.length, num2.length <= 104
  • num1 和num2 都只包含数字 0-9
  • num1 和num2 都不包含任何前导零

题解

cpp 复制代码
class Solution 
{
public:
    string addStrings(string num1, string num2) 
    {
        int end1 = num1.size()-1, end2 = num2.size()-1;
        int next = 0; // 进位
        // 提前开好足够的空间,减少扩容开销
        string str;
        str.reserve(max(num1.size(), num2.size()) + 1);

        while(end1 >= 0 || end2 >= 0) // 两个串只要有一个还没遍历完,就继续
        {
            int n1 = end1 >= 0 ? num1[end1--] - '0' : 0;
            int n2 = end2 >= 0 ? num2[end2--] - '0' : 0;
            int ret = n1 + n2 + next;
            // 更新当前位和进位值
            next = ret / 10;
            ret = ret % 10;
            str += ret + '0';
        }
        // '9' + '1'的例子,最后的进位无法在循环中尾插到str中
        if(next == 1)   str += '1'; 
        // reverse不是string的成员函数,不要写成str.reverse()
        // 正确的结果应该头插(手算一下两数相加就知道),但是头插开销大
        // 所以我们采用全部尾插,最后逆置
        reverse(str.begin(), str.end()); 
        return str;
    }
};

2)125. 验证回文串

题目描述

如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串

字母和数字都属于字母数字字符。

给你一个字符串 s,如果它是 回文串 ,返回 true;否则,返回false

示例 1:

复制代码
输入: s = "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。

示例 2:

复制代码
输入:s = "race a car"
输出:false
解释:"raceacar" 不是回文串。

示例 3:

复制代码
输入:s = " "
输出:true
解释:在移除非字母数字字符之后,s 是一个空字符串 "" 。
由于空字符串正着反着读都一样,所以是回文串。

提示:

  • 1 <= s.length <= 2 * 105
  • s 仅由可打印的 ASCII 字符组成

题解

cpp 复制代码
class Solution 
{
public:
    bool isPalindrome(string s) 
    {
        for(auto& e : s)
        {
            // 小写转大写
            if(e >= 'a' && e <= 'z')
                e = e - 32; // 小写字母的ASCII码比大写字母大
        }

        int left = 0, right = s.size()-1;
        while(left < right)
        {
            // 在保证left < right的情况下移动指针,直到找到第一个数字或字母
            while(!isalnum(s[left]) && left < right)  left++;
            while(!isalnum(s[right]) && left < right)  right--;
            // 比较两字符并移动指针
            if(s[left++] == s[right--]) continue;
            else return false;
        }
        // 走到这说明遍历完没发现有不一样的
        return true;
    }
};

3)牛客:HJ1 字符串最后一个单词的长度

题目描述:

对于给定的若干个单词组成的句子,每个单词均由大小写字母混合构成,单词间使用单个空格分隔。输出最后一个单词的长度。

输入描述:

在一行上输入若干个字符串,每个字符串代表一个单词,组成给定的句子。

除此之外,保证每个单词非空,由大小写字母混合构成,且总字符长度不超过 103103 。

输出描述:

在一行上输出一个整数,代表最后一个单词的长度。

示例1

输入:HelloNowcoder

输出:13

说明:在这个样例中,最后一个单词是 "HelloNowcoder" ,长度为 13 。

示例2

输入:A B C D

输出:1

题解

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main()
{
    string s;
    getline(cin, s); // 输入的字符串中可能有空格所以必须用getline,而不是cin
    // 没有空格返回npos(-1),-1+1=0,即取整个串,也是正确的
    int pos = s.rfind(' '); // 为找最后一个单词,所以从后往前找第一个空格
    string sub = s.substr(pos + 1); // 取空格后面的内容,即为最后一个单词

    cout << sub.size() << endl; // 输出最后一个单词的长度

    return 0;
}
相关推荐
SunkingYang2 小时前
QT中如何使用QMessageBox 实现提示、警告、错误报告和用户决策功能
c++·qt·提示·错误·告警·用法·qmessagebox
Once_day2 小时前
CC++八股文之内存
c语言·c++
量子炒饭大师2 小时前
【C++入门】Cyber骇客的同名异梦——【C++重载函数】(与C的函数差异)
c语言·开发语言·c++·函数重载
charlie1145141912 小时前
现代嵌入式C++教程:if constexpr——把编译期分支写得像写注释 —— 工程味实战指南
开发语言·c++·笔记·学习·嵌入式·现代c++
LIZhang20162 小时前
c++ 转化句柄,解决多线程安全释放问题
开发语言·c++
youqingyike3 小时前
Qt 中 QWidget 调用setLayout 后不显示
开发语言·c++·qt
_OP_CHEN3 小时前
【从零开始的Qt开发指南】(二十二)Qt 音视频开发宝典:从音频播放到视频播放器的实战全攻略
开发语言·c++·qt·音视频·前端开发·客户端开发·gui开发
oioihoii3 小时前
从C++到C#的转型完全指南
开发语言·c++·c#
学嵌入式的小杨同学3 小时前
C 语言实战:动态规划求解最长公共子串(连续),附完整实现与优化
数据结构·c++·算法·unity·游戏引擎·代理模式