字符与字符串

字符是计算机中表示文本的基本单位。在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 。

1. 输出 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:?

2. 字符与整数相互转换

在 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 码的一些常见操作。

3. 字符类型判断

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

#include <iostream>
using namespace std;

int main() {
  char c;
  cin >> c;

  if (c >= 'a' && c <= 'z') {
    cout << "lower" << endl;
  } else if (c >= 'A' && c <= 'Z') {
    cout << "upper" << endl;
  } else if (c >= '0' && c <= '9') {
    cout << "digit" << endl;
  }else {
    cout << "others" << endl;
  }

  return 0;
}

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

在判断比较时,初学者容易像下面这样写:

if (c >= 97 && c <= 122) {
  cout << "lower" << endl;
} else if (c >= 65 && c <= 90) {
  cout << "upper" << endl;
} else if (c >= 48 && c <= 57) {
  cout << "digit" << endl;
} else {
  cout << "others" << endl;
}

尽管程序没问题,但不建议这样写,这需要我们能记住字母所对应的 ASCII 码值是多少(至少范围的开始与结束),一旦记错,程序的判断逻辑就错了,而且这种错误不容易发现。

4. 字母大小写转换

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 来实现判断逻辑。

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

但不建议这么做,因为这个 32 很可能会记错,比如记成 33,那结果就完全错了。我们知道,小写字母对应的大写字母之间的码值是固定的,因此可以通过字符运算计算出来的,实际上, 'a' - 'A' 刚好就是 32,但不用我们去记。

二、字符数组

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

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

字符串本质上就是以空字符 \0 为结尾的字符数组,\0 表示字符串的的结束;而字符数组就是一个存储字符的普通数组,并非一定要以 \0 为结尾。

1. 字符数组声明与初始化

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

// 方式1:逐个字符初始化
char str1[5] = {'H', 'e', 'l', 'l', 'o'};

// 方式2:用字符串字面量来初始化,结尾自动添加 '\0'
char str2[6] = "Hello"; 

// 方式3:不指定大小
char str3[] = "Hello";

cout << str1 << endl;
cout << str2 << endl;
cout << str3 << endl;

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

对于字符串 str2,如果像下面这样实始化字符数组:

char str[5] = "Hello";

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

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

2. 字符数组操作

获取字符串长度

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

示例代码:

#include <bits/stdc++.h>
using namespace std;

int main() {
  char str[] = "hello";
  
  cout << strlen(str) << endl; // 输出:5
  return 0;
}

遍历字符数组

char msg[] = "Hello";
for(int i = 0; i < strlen(msg); i++) {
    cout << msg[i] << " ";
}

需要注意的是,这里我们不需要输出字符数组末尾的 \0,因此 i < strlen() 这里没有取等号。

也可以通过判定是否到末尾来遍历字符数组,像下面这样。

char msg[] = "Hello";
for(int i = 0; msg[i] != '\0'; i++) {
    cout << msg[i] << " ";
}

三、字符判断与转换函数

字符操作函数主要针对单个字符,通常用在判断字符类型或转换字符大小写。

1. 判断字符类型的函数

函数名作用
isalpha(c)判断字符 c 是否是字母
isdigit(c)判断字符 c 是否是数字
isalnum(c)判断字符 c 是否是字母或数字
islower(c)判断字符 c 是否是小写字母
isupper(c)判断字符 c 是否是大写字母
isspace(c)判断字符 c 是否是空白字符

2. 字符大小写转换函数

函数名作用
tolower(c)将大写字母 c 转换为小写字母
toupper(c)将小写字母 c 转换为大写字母

有了字符的判断与转换函数,前面的字母大小写转换示例我们就可以用这些函数改写一下。

改写后的代码如下:

#include <bits/stdc++.h>
using namespace std;

int main() {
  char c;
  cin >> c;

  if (islower(c)) {
    c = toupper(c);
  } else {
    c = tolower(c);
  }

  cout << c << endl;
  return 0;
}

使用现有的函数后,程序更简洁一些,而且可读性也更强了一些。

从学习的角度来说,既要学会使用函数,也需要知道函数背后的实现原理(如何利用 ASCII 字符的规律),两种方式都需要掌握。

四、字符串

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

1. 字符串定义与初始化

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

#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 语言接受的形式(实际上转换成为字符数组)。

2. 单行字符串读入

同样的,我们也可以用 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;
}

3. 多行字符串读入

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

例如,输入正整数 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;
}

4. 字符串基本操作

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

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 + +

5. 字符串常用方法

下面列举了 string 类的一些常用方法。

方法名作用
s.size() / s.length()获取字符串 s 的长度
s.empty()判断字符串 s 是否为空
s.substr(pos, len)截取子串
s.find(str)在字符串 s 中查找子串 str 的位置
s.insert(pos, str)在字符串 s 中指定位置插入字符串 str
s.erase(pos, len)删除子串
s.replace(pos, len, str)替换子串

五、任务练习

1. 数字和

题目描述

输入一个很大的整数(不超过 200 位),求各位上的数字和。

如,输入数字 12345,输出 15

题目解析

因为输入的是一个很大的整数,我们就不能用 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;
}

2. 判断回文字符串

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

输入数据

madam

输出数据

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>,就没这个问题了。

3. 恺撒密码加密信息

在信息传输过程中,为了保证信息的安全,往往需要对信息进行一定的加密处理。恺撒密码(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

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

六、小结

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