字符与字符串
字符与字符串
字符是计算机中表示文本的基本单位。在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 码表分为以下几个部分:
- 控制字符(0-31):这些字符用于控制设备,例如换行符
\n
(ASCII 10)、回车符\r
(ASCII 13)等。 - 可打印字符(32-126):这些字符包括空格、数字、大写字母、小写字母和标点符号。
- 扩展字符(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:857:958::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;}
运行程序,输出
M7798b
在使用
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;}
运行程序,得出结果
helloffffffff
需要注意的是,在 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 行字符串并输出。
例如,按以下格式输入数据:
3AlanMale18
直接应该输出
AlanMale18
很容易写出下面这样的代码:
#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;}
感觉没什么毛病,运行程序看看,结果显示:
AlanMale
发现最后的 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;}
运行结果:
Hello
C++
练习:数字和
题目描述
输入一个很大的数字,求各位上的数字和。
输入描述
一个很大的整数 (不超过 200 位)。
输出描述
为一个整数,表示这个整数的各个位上的数字之和。
输入输出样例
输入:
12345678912345678912345678
输出:
135
题目解析
因为输入的是一个很大的整数,我们就不能用 int
或 long 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 个字符是否相等,如果不相等,那肯定不是回文字符串,如果相等,再按同样的规则继续对比下去…
如下图所示,下标 i
和 j
从两相向推进,只要 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)中的一种,简单来说就是将明文中所有的字母在字母表中按照一定的字数进行「平移」,平移后对应的字母组成密文。
关于信息加密,这里有几个概念:
明文
,指我们需要加密的内容,也就是加密前的内容密文
,也就是加密后的内容密钥
,也就是「平移」多少位,例如:如果平移 3 位,密钥就是 3
例如将字母ABC进行加密,密钥是 3,根据规则,字母 A 将被替换成它向后移 3 位的字母 D,字母 B 与 C 按同样的规则进行替换,因此 ABC(明文)加密后就变成了 DEF(密文)。如下图所示:
题目描述
给定一个字符串,根据恺撒密码,输出加密后的字符串。
输入输出样例
输入
I Love Programing3
输出
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
。
这个转换是通过取模运算来实现的,先把字符转换为数字,再移位,对移位后得到的数字进行取模运算,取模运算后再转换为对应的字母。
小结
字符是构成文本的基本单元,而字符串则是由字符组成的序列,这两者都是非常重要的数据类型,在处理文本和字符串数据时非常有用。在算法竞赛中也很常见,如字符统计与转换、字符串匹配、字符串压缩等等。