---
title: "字符与字符串"
description: "从环境准备到 STL，系统学习 C++ 编程的基础知识。"
---

> Documentation Index
> Fetch the complete documentation index at: https://cppguide.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# 字符与字符串

![](https://img.dailyup.xyz/originals/8208463145777-b4117fc7-9085-49d0-932d-2d5feefecd05.png)

字符是计算机中表示文本的基本单位。在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 编码表来自维基百科。

![](https://img.dailyup.xyz/optimized/8208463144887-ca509d5d-39d3-4722-b817-1b125ee3e5c2.png)

通过这张 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** 及其对应的字符。我们只需要在输出时做一个强制类型转换就可以了。

```cpp
#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 码值相互转换。

```cpp
#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;
}
```

运行程序，输出

```cpp
M
77
98
b
```

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


下面通过几个练习来熟悉一下字符与 ASCII 码的一些常见操作。
### 3. 字符类型判断

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

```cpp
#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 所对应的整数来进行比较，强烈推荐这样来使用。

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

```cpp
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 来实现字符的大小写转换。

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

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

```cpp
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. 字符数组声明与初始化

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

```cpp
// 方式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`，如果像下面这样实始化字符数组：

```cpp
char str[5] = "Hello"; 
```

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

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

### 2. 字符数组操作

**获取字符串长度**

字符数组的长度可以通过 `strlen` 函数获取，该函数返回字符串的长度（不包括 `'\0'`）。

示例代码：

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

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

**遍历字符数组**

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

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

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

```cpp
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` 转换为大写字母 |


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

改写后的代码如下：

```cpp
#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. 字符串定义与初始化

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

```cpp
#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` 去读入字符串，例如：

```cpp
#include <iostream>
using namespace std;

int main() {
  
    string s;
    cin >> s;
    
    cout << s << endl;
        
    return 0;
}
```

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

```
Hello C++
```

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

```
Hello
```

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

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

```cpp
#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
```

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

```cpp
#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` 后面的换行符了，后面的读入也就正常了。

```cpp
#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. 字符串基本操作

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

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

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

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

```cpp
#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`。

**题目解析**

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

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

**参考代码**

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

```cpp
#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
```

**题目分析**

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

**实现代码 - 方法一**

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

```cpp
#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"` 得到的结果相好是逆序，我们可以巧妙地利用这个特点，不用倒着来一遍，正着来也是一样的。

```cpp
#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` 就一直对比下去。

```cpp
#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()` 来实现字符串逆序操作。判断回文数就更简单了。

```cpp
#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（密文）。如下图所示：

![](https://img.dailyup.xyz/optimized/8208463142876-c7a92efe-da95-4ba2-a6d0-66c20148fc21.png)

**题目描述**

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


**输入数据**

```
I Love Programing
3
```

**输出数据**

```
L Oryh Surjudplqj
```

**参考代码**

```cpp
#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`。

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

## 六、小结

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

Source: https://cppguide.xyz/basic/string/index.mdx
