12 字符串

C++ 提供了两种字符串的形式:

  1. C 风格字符串
  2. C++ 中的 string
  3. C++17 带来的 string_view

12.1 C 风格字符串

C 风格的字符串起源于 C 语言,并在 C++ 中继续得到支持。

特点: 它实际上是一个 字符数组 ,并且 \0 作为字符串内容结束的标志

12.1.1 C 风格字符串的定义与赋值

  1. 与普通数组相同,我们在定义字符数组时,可以指定数组长度。

char str[5] = {'C', 'H', 'I', 'N', 'A'};

TODO: 内存图

  1. 与普通数组相同,如果想要的数组长度,与我们赋初值的字符数相同,我们可以省略数组长度。

char str[] = {'C', 'H', 'I', 'N', 'A'};

  1. 我们为字符数组赋值的方式可以进一步简化

char str[] = {"CHINA"};char str[] = "CHINA";

这种方式右边的值,其实是 C 风格字符串字面量,不仅仅包含我们可以看到的 5 个字符,还包括一个结束字符 \0 。所以,不指定数组长度的话,数组长度为 6 。

  1. 与普通数组相同,如果赋值的个数少于数组长度,剩余的数组项将被赋值为 0 ,对应的字符为 \0

char str[10] = {'C', 'H', 'I', 'N', 'A'};

TODO: 内存图

12.1.2 C 风格字符串的输出

  1. 与普通数组相同,可以使用 for 循环遍历数组的方式进行输出
  2. 直接使用 cout 输出字符串
char str[] = "CHINA";
cout << str << endl;
  1. 使用这种方式时,计算机判断字符串结束的唯一标准是 \0
  2. 如果字符数组有意义的内容中,存在一个 \0 ,那么输出会提前结束。
  3. 如果字符数组中没有 \0 ,计算机将会继续访问下一个内存地址,并把其中的数据当作字符,逐个输出,知道遇到 \0 。只会导致输出的结果变得不可预测。
  4. char str[5] = {'C', 'H', 'I', 'N', 'A'}; 这种为字符数组逐个赋值,并且数组长度与赋值个数相同的情况下,字符数组末尾不存在 \0 ,使用 cout 输出时,末尾可能出现随机字符。

12.1.3 C 风格字符串的输入

  1. 与普通数组相同,可以使用 for 循环遍历数组的方式进行输入
  2. 使用 cin 输入字符串时,不能输入空白符号,如果需要输入空格,需要使用 cin.getline()
#include<iostream>
using namespace std;

int main() {
    char str[50];
    cout << "请输入字符串:" << endl;
    cin >> str;
    cout <<  "您输入的字符串是:" << endl;
    cout <<  str << endl;
    return 0;
}

请输入字符串:
This is a string.
您输入的字符串是:
This

cin 在遇到空白符时,会认为用户输入已经结束,所以只使用 cin 我们只能输入一个单词。如果我们想要输入一句话,我们想要使用 cin.getline(字符数组名,允许用户输入的最大字符个数) ,允许用户输入的最大字符个数,含结束标志 \0 ,一般是字符数组长度。

#include<iostream>
using namespace std;

int main() {
    char str[50];
    cout << "请输入字符串:" << endl;
    cin.getline(str, 50);
    cout <<  "您输入的字符串是:" << endl;
    cout <<  str << endl;
    return 0;
}

请输入字符串:
This is a string.
您输入的字符串是:
This is a string.

  1. 直接使用 cin 后,会对之后的 cin 产生影响
#include<iostream>
using namespace std;

int main() {
    double price;
    cout << "请输入单价:" << endl;
    cin >> price;
    char name[5];
    cout << "请输入商品名:" << endl;
    cin.getline(name,50);
    cout <<  "您输入的商品是:" << name << ",单价为:" << price << endl;
    return 0;
}

请输入单价:
12.5
请输入商品名:
您输入的商品是:,单价为:12.5

当你使用 cin >> 输入一个值时, cin 不仅会捕捉到这个值,还会捕捉到你按下回车键时出现的换行符('\n')。因此,当我们输入 12.5 然后按回车键时, cin 捕捉到字符串 "2\n" 作为输入。

然后它将数值 2 提取到变量 price 中,留下换行符供以后使用。然后,当 cin.getline() 去提取文本到 name 时,它看到 "\n" 已经在输入流缓冲区中等待了,并认为我们之前一定是输入了一个空字符串!这肯定不是我们想要的结果。

因此,在我们再次使用 cin 前,我们需要使用清空输入流缓冲区。

12.1.4 清空输入流缓冲区

在清空缓冲区前,我们一般先要执行 cin.clear()cin.clear() 是用来更改 cin 的状态标示符的, cin 在接收到错误的输入的时候,会设置状态位 good 。如果 good 位不为 1 ,则 cin 不接受输入,直接跳过。如果下次输入前状态位没有改变那么即使清除了缓冲区数据流也无法输入。所以清除缓冲区之前必须要 cin.clear()

清空缓冲区相关的方法有四个。

  1. fflush(stdin);
    C语言中的方法。作用是清空输入缓冲区的所有内容。在 C++ 中是否可用,需要编译器支持。
  2. cin.sync();
    C++:清除输入缓冲区的所有内容。编译器决定是否支持。
  3. cin.ignore( int a,char c)
    C++:所有编译器支持。

    不断从输入流cin中提取字符然后忽略提取到的字符,直到遇到字符c或者提取到的字符个数达到a为止。

    换句话说,这个函数清空输入流cin中字符c(包括字符c)之前的字符,或者清空了a个字符都没有遇到字符c也结束。 这个方法可以只提供需要清除字符的个数,但需要注意,此时即使缓冲区已经被清空了,它也不会停下来,它会一直等待新输入的字符,把它们清除,以完成它的目标数量。

    一般为了只让第二个参数起作用,我们把第一个参数设为一个很大的数(比如INT_MAX)。而第二个参数常设为“\n”,也就是换行符,这样我们可以清空输入流cin中换行符(包括换行符)之前的所有字符。

    cin.ignore()函数的默认参数为,1和“EOF”,即默认形式为cin.ignore(1, EOF),把EOF前的1个字符清掉,没有遇到EOF就清掉一个字符然后结束。
  4. 循环从缓冲区取值。
#include<iostream>
using namespace std;

int main() {
    double price;
    cout << "请输入单价:" << endl;
    cin >> price;
    char name[5];
    cout << "请输入商品名:" << endl;
    cin.clear();
    cin.ignore(INT_MAX,'\n');
    cin.getline(name,50);
    cout <<  "您输入的商品是:" << name << ",单价为:" << price << endl;
    return 0;
}

请输入单价:
12.5
请输入商品名:
菠萝
您输入的商品是:菠萝,单价为:12.5

12.2 C++ 中的 string

上面我们介绍了“ C 风格的字符串”, 它实际上是字符数组,但它的结束位置却与数组长度无关!!! 所以,我们在使用它时,很容易出错。

此外,C 提供的许多用于处理数字的直观运算符(例如赋值和比较)根本不适用于 C 风格的字符串。有时这些看起来有效但实际上会产生不正确的结果——例如,使用 == 比较两个 C 风格的字符串实际上会进行指针比较,而不是字符串比较。使用 = 将一个 C 风格的字符串分配给另一个字符串一开始看起来是可行的,但实际上是在进行指针复制(浅拷贝),这通常不是您想要的。这些事情会导致程序崩溃,而且很难发现和调试!

最重要的是,使用 C 风格的字符串需要记住很多关于什么是安全/不安全的挑剔规则,记住一堆具有有趣名称的函数,如 strcat() 和 strcmp() 而不是使用直观的运算符,并进行大量手动内存管理。

幸运的是,C++ 和标准库提供了一种更好的方法来处理字符串:std::string 和 std::wstring 类。通过使用构造函数、析构函数和运算符重载等 C++ 概念,std::string 允许您以直观和安全的方式创建和操作字符串!不再有内存管理,不再有奇怪的函数名称,并且发生灾难的可能性大大降低。

如果是 C 风格字符串有什么优势的话,它的空间占用和处理速度要比 std::string 优秀。

std::string 用于标准的 ascii 和 utf-8 字符串。std::wstring 用于宽字符/unicode (utf-16) 字符串。我们的课程不涉及 std::wstring 。

12.2.1 std::string 的定义与赋值

最基本的定义与赋值方法:

string str = "abc";

std::string 属于 “对象” 。这是一个我们尚未接触过的内容,但我们可以感受一下 std::string 赋值的灵活程度。

string str1;                      //生成空字符串
string str2("123456789");         //生成"1234456789"的复制品
string str3("123456789", 1, 3);   //结果为"234"
string str4("123456789", 5);      //结果为"12345"
string str5(5, '1');              //结果为"11111"
string str6(str2, 2);             //结果为"3456789"

请注意 str4str6 ,我们都使用了相同的结构: string str(字符串, int n); ,但我们得到结果的逻辑却大不相同。
如果字符串是 C 风格字符串,即字符数组,那么最终效果是获取字符数组前 n 个字符。 字符串字面量为 C 风格字符串。
如果是 std::string ,那么其实这个方法就是 string str(字符串, int n, int length); 方法,省略取多少个字符的版本,效果是从第 n 个字符开始取,但不限制取字符个数,即取到字符串结束位置。

12.2.2 std::string 的输出

直接使用 cout 输出即可。 std::string 并不存在字符串结束标志 \0 ,而是由计算机记住它究竟有多长,所以完全不用担心会输出乱码。

12.2.3 std::string 的输入

我们依然可以使用 cin >> 字符串名 输入一个单词,与字符数组相同,如果有空格,就需要使用 getline() 用于获取一整行文字,但用法与 C 风格字符串完全不同。

#include<iostream>
using namespace std;

int main() {
    string str;
    cout << "请输入字符串:" << endl;
    getline(cin,str);
    cout <<  "您输入的字符串是:" << endl;
    cout <<  str << endl;
    return 0;
}

请输入字符串:
This is a string.
您输入的字符串是:
This is a string.

12.2.4 std::string 忽略缓冲区中的空白符号

std::string 也会受到缓冲区残留内容的影响,但比起 C 风格字符串,它的解决方式更加简单。

我们可以用 std::ws 指定 std::cin 去忽略有效字符前的所有空白字符(空格、制表符、换行符)。

#include<iostream>
using namespace std;

int main() {
    double price;
    cout << "请输入单价:" << endl;
    cin >> price;
    string name;
    cout << "请输入商品名:" << endl;
    getline(cin >> ws, name);
    cout <<  "您输入的商品是:" << name << ",单价为:" << price << endl;
    return 0;
}

请输入单价:
12.5
请输入商品名:
菠萝
您输入的商品是:菠萝,单价为:12.5

12.2.5 连接字符串

我们可以非常方便地使用 ++= 运算符对 string 进行拼接。如果我们需要更复杂的功能,我们可以使用 append() 方法,括号中的参数与初始化时的用法相同。

#include<iostream>
using namespace std;

int main() {
    string s1("123"), s2("abc"), s3;
    cout <<  s1 + s2 << endl;
    s3 += s1;
    cout << s3 << endl;
    s3.append(s2);
    cout << s3 << endl;
    s3.append(s2, 1, 2);
    cout << s3 << endl;
    return 0;
}

123abc
123
123abc
123abcbc

12.2.6 复制字符串

只需要使用 = 运算符,即可对将其右边字符串的内容,复制到左边。

注意:复制的是字符串的内容,不是使两个字符串地址相同。

#include<iostream>
using namespace std;

int main() {
    string s1("123"), s2("abc"), s3;
    s3 = s1;
    cout << s3 << endl;
    cout << &s1 << endl;
    cout << &s3 << endl;
    return 0;
}

123
0x6304fff6b0
0x6304fff670

12.2.7 比较两个字符串的大小

  1. 比较操作符: >, >=, <, <=, ==, !=
  2. compare() 方法

其核心都是从左到右,将字符串中的字符两两比较,比较的方式是两个字符的 ACSII 码。compare() 方法的返回值为:前面减去后面的 ASCII 码,>0 返回 1 , <0 返回 -1 ,相同返回 0 。

#include<iostream>
using namespace std;

int main() {
    string s1("123"), s2("abc"), s3;
    s3 = s1;
    cout << (s1 > s2) << endl;
    cout << (s1.compare(s2)) << endl;
    return 0;
}

0
1

12.2.8 求字符串长度

  1. size()和length():返回string对象的字符个数,他们执行效果相同。
  2. max_size():返回string对象最多包含的字符数,超出会抛出length_error异常(了解即可)
  3. capacity():重新分配内存之前,string对象能包含的最大字符数(了解即可)
#include<iostream>
using namespace std;

int main() {
    string s("1234567");
    cout << "size=" << s.size() << endl;
    cout << "length=" << s.length() << endl;
    cout << "max_size=" << s.max_size() << endl;
    cout << "capacity=" << s.capacity() << endl;
    return 0;
}

size=7
length=7
max_size=4611686018427387903
capacity=15

12.2.9 全部字母转小写/大写

#include<iostream>
#include <algorithm>

using namespace std;

int main() {
    string s("AbCd");
    // 全部字母转大写
    transform(s.begin(), s.end(), s.begin(), ::toupper);
    cout << s << endl;
    // 全部字母转小写
    transform(s.begin(), s.end(), s.begin(), ::tolower);
    cout << s << endl;
    return 0;
}

ABCD
abcd

12.3 std::string_view 只读字符串

前面提到过, std::string 的使用起来非常方便,但缺点是占用空间较大,运行效率较慢。

为了解决这些问题, C++17 标准,带来了新的字符串类型 std::string_view 。这种字符串的用法与 std::string 相似,但是只能对它进行读取操作。