跳转到内容

字符与字符串

字符与字符串

字符是计算机中表示文本的基本单位。在C++中,字符用 char 类型表示,通常占用1个字节(8位)的内存空间。字符可以用单引号 '' 括起来表示,例如 'a''1''@' 等。

字符串是由多个字符组成的序列。在C++中,字符串可以用字符数组或 string 类来表示。字符串通常用双引号 "" 括起来表示,例如 “hello”、“how are you” 等。

字符与字符串处理,在编程中一直是一个比较重要的主题,相较整数类型而言,也更为复杂一些。下面分别来看看。

字符与ASCII 编码

在计算机,字符通常是被编码成整数,最终以 0 和 1 来存储的,每个字符都有不同整数与之对应。

编码的方式有很多种,在 C++ ,使用的是 ASCII 编码,ASCII(American Standard Code for Information Interchange,美国信息交换标准代码),这是一种基于拉丁字母的编码系统,用于数字、大小写字母以及一些特殊符号的编码。

下面这张 ASCII 编码表来自维基百科。

通过这张 ASCII 码表可以看到,ASCII 码表定义了128个字符,对应于整数 0 ~ 127,包括英文字母、数字、标点符号和控制字符,每个字符对应一个 ASCII 码值,例如字符 ‘A’ 的 ASCII 码值是 65 。

ASCII 码表分为以下几个部分:

  1. 控制字符(0-31):这些字符用于控制设备,例如换行符 \n(ASCII 10)、回车符 \r(ASCII 13)等。
  2. 可打印字符(32-126):这些字符包括空格、数字、大写字母、小写字母和标点符号。
  3. 扩展字符(127):这个字符是删除字符(DEL)。

这张表不需要记忆,但下面几个特点最好知道

  • 字符 0 ~ 9 之间是连续的,A ~ Z 之间是连续的;,a ~ z 之间也是连续的。
  • 小写字母所对应的 ASCII 码值比大写字母更大一些。
  • 常见几个字符所对应的整数,如:字符 0 对应的码值是 48;字符 A 对应的码值是 65;字符 a 对应的码值是 97 。

输出 ASCII 码表

我们可以通过一个循环来输出数字 0 ~ 127 及其对应的字符。我们只需要在输出时做一个强制类型转换就可以了。

#include <iostream>
using namespace std;
// 字符与 ASCII 码
int main() {
for (int i = 0; i < 128; i ++) {
cout << i << ":" << char(i) << endl;
}
return 0;
}

输出结果类似下面这样(这里只选取了其中一部分显示),大家可以对照上面的 ASCII 码表看看是否正确呢。

56:8
57:9
58::
59:;
60:<
61:=
62:>
63:?

字符与整数相互转换

在 C++ 中,字符和ASCII码之间可以相互转换。字符在内存中是以整数形式存储的,因此可以直接将字符赋值给整数变量,或者将整数赋值给字符变量。字符也可参与运算,运算时会转换为对应的码值来计算。

下面的代码演示了字符与对应的 ASCII 码值相互转换。

#include <iostream>
using namespace std;
int main() {
char c = 'M';
int n = 98;
cout << c << endl;
cout << int(c) << endl;
cout << n << endl;
cout << char(n) << endl;
return 0;
}

运行程序,输出

M
77
98
b

在使用 cout 输出时,C++ 编译器会根据数据类型来决定最终输出什么,如果数字 i 在 0 ~ 127 之间,char(i) 则会被解读为数字对应的字符并输出,否则将会直接将数字输出。

下面通过几个练习来熟悉一下字符与 ASCII 码的一些常见操作。

示例:字符判断

由 ASCII 码表可知,大写字母之间是连续,小写字母之间也是连续,我们可以利用这个特性来判断一个字符是否是大写或小写字母。

#include <iostream>
using namespace std;
int main() {
char c;
cin >> c;
if (c >= 'a' && c <= 'z') {
cout << "是小写字母" << endl;
} else if (c >= 'A' && c <= 'Z') {
cout << "是大写字母" << endl;
} else {
cout << "是其它字符" << endl;
}
return 0;
}

在判断比较时,不建议去直接使用数字比较,例如下面这样写:

if (c >= 97 && c <= 122) {
cout << "是小写字母" << endl;
} else if (c >= 65 && c <= 90) {
cout << "是大写字母" << endl;
} else {
cout << "是其它字符" << endl;
}

尽管程序没问题,但程序不易读,而且我们也不一定能准确的记住每个字母所对应的 ASCII 整数是多少,一旦记错,程序的判断逻辑就错了,而且很不容易发现这种错误。

在这里我们直接比较的是字符,程序运行时,实际上会转换为 ASCII 所对应的整数来进行比较,强烈推荐这样来使用。

示例:字母大小写转换

ASCII 码表中,大写字母和小写字母的ASCII码值相差32。因此,可以通过加减32来实现字符的大小写转换。

如果从键盘读入一个字符,这个字符可能是大写字母也可能是小写字母。如果大写字母,输出其对应的小写字母;如果是小写字母,输出其对应的大写字母。

#include <bits/stdc++.h>
using namespace std;
int main() {
char c;
cin >> c;
if (c >= 'a' && c <= 'z') {
c = c - ('a' - 'A');
} else {
c = c + ('a' - 'A');
}
cout << c << endl;
return 0;
}

需要注意的,我们在进行大小写字母转换的时候,可以通过加 32 或减 32 来实现。

if (c >= 'a' && c <= 'z') {
c = c - 32;
} else {
c = c + 32;
}

但不建议这么做,因为这个 32 很可能会记错,比如记成 33,那结果就完全错了。而我们知道,小写字母对应的大写字母之间的码值是固定的,是可以直接通过字符运算计算出来的。

字符数组

char 只代表单个的字符,而在实际应用中,更多是多个字符组成的文字,比如 hello, c++ ,由多个字符组成的这种类型我们称之为字符串(string)。字符串通常是用 "" 双引号括起来的。

C++ 标准库提供了 string 类,用于更方便地处理字符串。而在 C 语言中并没有 string 类型,在 C 语言中都是用字符数组来对字符串进行处理的。

字符串本质上就是以空字符 \0 为结尾的字符数组,\0 表示字符串的的结束。

字符数组的声明与初始化

字符数组可以通过以下方式声明和初始化:

char str1[] = "Hello"; // 结尾自动添加 '\0'
char str2[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 手动添加 '\0'

需要特别注意的是,当我们使用字符串来初始化字符时,字符数组末尾会自动添加空字符 \0,因此字符数组的长度实际上要比看到的字符串长度多 1 。

如果像下面这样实始化字符数组:

char str[5] = "Hello";

运行会报错,原因是没有没有足够的数组空间来存放字符。字符串 “Hello” 共 5 个字符,长度为 5,但对应的字符数组长度实际应该为 6,因为末尾还有一个空字符 \0

我们可以像普通数组一样对字符数组进行操作,例如,我们可以通过下标访问和修改字符数组中的字符。

字符数组的长度

字符数组的长度可以通过 strlen 函数获取,该函数返回字符串的长度(不包括 '\0')。

示例代码:

#include <bits/stdc++.h>
using namespace std;
int main() {
char str[] = "hello";
cout << strlen(str) << endl; // 输出:5
return 0;
}

字符串 String

字符串是由多个字符组成的序列。在C++中,字符串可以用 string 类来表示。string 类封装了字符数组,并提供了丰富的成员函数来操作字符串。

字符串定义与初始化

下面的程序演示了几种字符串定义与初始化的方式。

#include <iostream>
#include <string>
using namespace std;
int main() {
string s1; // 定义一个字符串变量 s1,默认为空字符串
string s2 = "hello world"; // 定义并初始化
string s3("hello world"); // 定义并初始化
string s4(s3); // 用 s3 的值来初始化 s4
stirng s5(s)
string s5(8, 'f'); // s3 的内容是 ffffffff
cout << s2 << endl;
printf("%s\n", s3.c_str()); // 注意 printf 输出的写法
return 0;
}

运行程序,得出结果

hello
ffffffff

需要注意的是,在 C 语言中并没有 string 类型,如果想要使用 printf() 进行输出,需要使用字符串中内置的函数 c_str()string 转换为 C 语言接受的形式(实际上转换成为字符数组)。

单行字符串读入

同样的,我们也可以用 cin 去读入字符串,例如:

#include <iostream>
using namespace std;
int main() {
string s;
cin >> s;
cout << s << endl;
return 0;
}

和之前读入整数类型完全一致的用法,非常简单!不过如果输入的是一串带空格的字符串,输出就会有点问题了,例如输入:

Hello C++

运行程序,发现只输出了 Hello ,后面的 C++ 并没有读入进去。

Hello

在使用 cin 读入字符或字符串时,一旦遇到空白字符(如 tab、空格、换行等),就会自动停止读取,所有的空白字符都会被跳过,不会成为字符串的一部分。

如果要读入一串带空白字符的字符串,需要用到getline() ,从名称就可以看出,这个指令是以「」为单位来读入数据的。

#include <iostream>
using namespace std;
int main() {
string s;
getline(cin, s); // 以行为单位对字符串进行读入
cout << s << endl;
return 0;
}

多行字符串读入

在实际应用中,我们经常会遇到读入多行字符串的情况。

例如,输入正整数 n,要求依次读入接下来的 n 行字符串并输出。

例如,按以下格式输入数据:

3
Alan
Male
18

直接应该输出

Alan
Male
18

很容易写出下面这样的代码:

#include <iostream>
using namespace std;
int main() {
int n;
string arr_s[10]; # 定义一个字符串数组
cin >> n;
string s;
for (int i = 0; i < n; i++) {
getline(cin, s);
arr_s[i] = s;
}
for (int i = 0; i < n; i++) {
cout << arr_s[i] << endl;
}
return 0;
}

感觉没什么毛病,运行程序看看,结果显示:

Alan
Male

发现最后的 18 没读入进来,但输出结果前面有一个空行,这是为什么呢?

这是因为 getline() 在每次读入数据时会读到换行符才停止,而 cin 读完数据就停下来了。对于上面的代码,第 7 行的 cin 读入,只会读入数字 3,读取的就停在 3 和后面的换行符之间了。

后面 getline() 接着读,会接着读,首先就读到了换行符,因此会产生一个空行,再读入后面的 Alan

解决的办法是,在 cin 后加上一句 cin.ignore(),这样就忽略掉 cin 后面的换行符了,后面的读入也就正常了。

#include <iostream>
using namespace std;
int main() {
int n;
string arr_s[10];
cin >> n;
cin.ignore();
string s;
for (int i = 0; i < n; i++) {
getline(cin, s);
arr_s[i] = s;
}
for (int i = 0; i < n; i++) {
cout << arr_s[i] << endl;
}
return 0;
}

字符串操作

字符串本质上就是一个字符数组,因此我们可以像常见的数组那样去访问和操作字符串。

string s = "Hello C++";
cout << s[0] << endl; // 输出字符 H

在 C++ 中,我们可以通过 s.length()s.size() 来获取字符串的长度,两者是等价的,s.size() 是后面为了兼容 STL 容器加入的,推荐使用 s.size() 即可。

因此我们可以像遍历数组那样用循环去遍历字符串中的每个字符,下面这个例子就是将字符中的所有字符依次打印出来,每个字符一行。

#include <iostream>
using namespace std;
int main() {
string s = "Hello C++";
for (int i = 0; i < s.size(); i++) {
cout << s[i] << endl;
}
return 0;
}

运行结果:

H
e
l
l
o
C
+
+

练习:数字和

题目描述

输入一个很大的数字,求各位上的数字和。

输入描述

一个很大的整数 (不超过 200 位)。

输出描述

为一个整数,表示这个整数的各个位上的数字之和。

输入输出样例

输入:

12345678912345678912345678

输出:

135

题目解析

因为输入的是一个很大的整数,我们就不能用 intlong long 变量来存储了。比较好的方法是用字符数组或直接用字符串来存储。

无论用字符数组还是字符串,我们都需要解决一个问题,因为读入的数字每一位都是一个字符,我们要求数字和,需要将字符数字变为真正的数字。

参考代码

这里用字符串来存储输入的大数字。

#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
cin >> s;
int t = 0;
for (int i = 0; i < s.size(); i++) {
t += s[i] - '0';
}
cout << t << endl;
return 0;
}

练习:判断回文字符串

题目描述

输入一个字符串,判断该字符串是不是一个回文字符串。如果是回文数,则输出 “Y”,否则输出 “N”。

输入描述

输入为一个字符串

输出描述

输出 “Y” 或 “N”

输入输出样例

输入1:

hello c++

输出1:

N

输入2:

madam

输出2:

Y

题目分析

如果一个字符串正着读和倒着读是一样,这个字符串就是一个回文字符串,否则就不是回文字符串。我们可以找到多种方法来实现回文字符串的判断。

参考方法一

利用字符串拼接,将一个字符串倒序,再将倒序后的字符串与原始字符串对比,如果相等,则是回文字符串,否则不是回文字符串。

#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
getline(cin, s);
string p = ""; // 用于拼接得到倒序后的字符串
for (int i = s.size() - 1; i >= 0; i--) {
p += s[i];
}
if (p == s) cout << "Y" << endl;
else cout << "N" << endl;
return 0;
}

实际上,由于拼接是有顺序的,"a" + "b""b" + "a" 得到的结果相好是逆序,我们可以巧妙地利用这个特点,不用倒着来一遍,正着来也是一样的。

#include <bits/stdc++.h>
using namespace std;
int main() {
string s;
getline(cin, s);
string p = "";
for (int i = 0; i < s.size(); i++) {
p = s[i] + p; // 注意这一段代码
}
if (p == s) cout << "Y" << endl;
else cout << "N" << endl;
return 0;
}

参考方法二

方法二稍微巧妙一点,我们可以同时对比字符串的第一个字符和最后一个字符是否相等,如果不相等,那肯定不是回文字符串,如果相等,再对比第 2 个字符与倒数第 2 个字符是否相等,如果不相等,那肯定不是回文字符串,如果相等,再按同样的规则继续对比下去…

如下图所示,下标 ij 从两相向推进,只要 i < j 就一直对比下去。

#include <iostream>
using namespace std;
int main() {
string s;
getline(cin, s);
bool is_palindrome = true;
int s_len = s.size(); // 获取字符串长度,避免反复调用
for (int i = 0, j = s_len - 1 ; i < j; i++, j--) {
if (s[i] != s[j]) {
is_palindrome = false;
break;
}
}
if (is_palindrome) cout << "Y" << endl;
else cout << "N" << endl;
return 0;
}

参考方法三

在我们学习 STL 后,就可以直接使用 reverse() 来实现字符串逆序操作。判断回文数就更简单了。

#include <iostream>
#include <algorithm>
using namespace std;
int main() {
string s;
getline(cin, s);
string p = s; // 将字符串复制一份,后续比较
reverse(s.begin(), s.end()); // 将 s 字符串反转
if (p == s) cout << "Y" << endl;
else cout << "N" << endl;
return 0;
}

要使用 reverse(),需要加入头文件 #include <algorithm>。当然你也可以直接使用万能头文件 #include <bits/stdc++.h>

练习:恺撒密码加密信息

什么是恺撒密码

在信息传输过程中,为了保证信息的安全,往往需要对信息进行一定的加密处理。恺撒密码(Caesar Cipher) 是一种非常简单的加解密方法,因古罗马时代的恺撒大帝曾使用过这种密码而得名。

恺撒密码是属于移位密码(Shift Cipher)中的一种,简单来说就是将明文中所有的字母在字母表中按照一定的字数进行「平移」,平移后对应的字母组成密文

关于信息加密,这里有几个概念:

  1. 明文,指我们需要加密的内容,也就是加密前的内容
  2. 密文,也就是加密后的内容
  3. 密钥,也就是「平移」多少位,例如:如果平移 3 位,密钥就是 3

例如将字母ABC进行加密,密钥是 3,根据规则,字母 A 将被替换成它向后移 3 位的字母 D,字母 B 与 C 按同样的规则进行替换,因此 ABC(明文)加密后就变成了 DEF(密文)。如下图所示:

题目描述

给定一个字符串,根据恺撒密码,输出加密后的字符串。

输入输出样例

输入

I Love Programing
3

输出

L Oryh Surjudplqj

参考代码

#include <iostream>
using namespace std;
int main(){
string s;
getline(cin, s);
int n;
cin >> n;
for (auto &c : s) {
if (c <= 'z' && c >= 'a') {
c = (c - 'a' + n) % 26 + 'a';
} else if (c <= 'Z' && c >= 'A') {
c = (c - 'A' + n) % 26 + 'A';
}
}
cout << s << endl;
return 0;
}

代码解析

上面的代码中唯一需要注意的是如何处理字母循环移位的,例如,如果字母 z 需要向后移位,应该又再回字母 a 从头开始计算,z 向后移 3 位,得到的是字母 c

这个转换是通过取模运算来实现的,先把字符转换为数字,再移位,对移位后得到的数字进行取模运算,取模运算后再转换为对应的字母。

小结

字符是构成文本的基本单元,而字符串则是由字符组成的序列,这两者都是非常重要的数据类型,在处理文本和字符串数据时非常有用。在算法竞赛中也很常见,如字符统计与转换、字符串匹配、字符串压缩等等。