C++ 字符类型完全指南:char、ASCII和转义序列

到目前为止,我们所研究的基本数据类型都用于存储数字(整数和浮点数)或布尔值(真/假)。但如果我们要存储字母或标点符号呢?

#include <iostream>

int main()
{
    std::cout << "Would you like a burrito? (y/n)";

    // 我们希望用户输入一个'y'或'n'字符
    // 我们该怎么做呢?

    return 0;
}

char 数据类型基础

char数据类型被设计用来存储单个字符。字符可以是单个字母、数字、符号或空白字符。

char数据类型是一种整型,这意味着底层值以整数形式存储。类似于布尔值0被解释为假,非0被解释为真,char变量存储的整数被解释为ASCII字符。

ASCII代表美国信息交换标准代码,它定义了一种特定的方式来将英文字符(以及一些其他符号)表示为0到127之间的数字(称为ASCII码或码点)。例如,ASCII码97被解释为字符"a"。

字符字面量总是放在单引号之间(例如"g"、“1”、" “)。

下面是一个完整的ASCII字符表:

代码符号代码符号代码符号代码符号
0NUL(空)32(空格)64@96`
1SOH(报头开始)33!65A97a
2STX(报文开始)34"66B98b
3ETX(报文结束)35#67C99c
4EOT(传输结束)36$68D100d
5ENQ(询问)37%69E101e
6ACK(确认)38&70F102f
7BEL(响铃)39'71G103g
8BS(退格)40(72H104h
9HT(水平制表符)41)73I105i
10LF(换行)42*74J106j
11VT(垂直制表符)43+75K107k
12FF(换页)44,76L108l
13CR(回车)45-77M109m
14SO(移出)46.78N110n
15SI(移入)47/79O111o
16DLE(数据链路转义)48080P112p
17DC1(数据控制1)49181Q113q
18DC2(数据控制2)50282R114r
19DC3(数据控制3)51383S115s
20DC4(数据控制4)52484T116t
21NAK(否定确认)53585U117u
22SYN(同步空闲)54686V118v
23ETB(传输块结束)55787W119w
24CAN(取消)56888X120x
25EM(介质结束)57989Y121y
26SUB(替换)58:90Z122z
27ESC(转义)59;91[123{
28FS(文件分隔符)60<92\124|
29GS(组分隔符)61=93]125}
30RS(记录分隔符)62>94^126~
31US(单元分隔符)63?95_127DEL(删除)

代码0-31和127被称为不可打印字符。这些代码被设计用来控制外围设备,如打印机(例如,通过指示打印机如何移动打印头)。如今,大多数这样的代码已经过时。如果你尝试打印这些字符,结果取决于你的操作系统(你可能会得到一些类似表情符号的字符)。

代码32-126被称为可打印字符,它们代表大多数计算机用来显示基本英文文本的字母、数字字符和标点符号。

如果你尝试打印一个值超出ASCII范围的字符,结果也取决于你的操作系统。

初始化字符

你可以使用字符字面量来初始化char变量:

char ch2{ 'a' }; // 使用"a"的码点进行初始化(以整数97存储)(推荐)

你也可以使用整数来初始化字符,但最好尽量避免。

char ch1{ 97 }; // 使用整数97进行初始化("a")(不推荐)

警告

注意不要混淆字符数字和整数数字。以下两种初始化方式并不相同:

char ch{5}; // 使用整数5进行初始化(以整数5存储)
char ch{'5'}; // 使用"5"的码点进行初始化(以整数53存储)

字符数字用于表示文本中的数字,而不是用于进行数学运算的数字。

字符的输入和输出

打印字符

当使用std::cout打印char时,std::cout会将char变量作为ASCII字符输出:

#include <iostream>

int main()
{
    char ch1{ 'a' }; // (推荐)
    std::cout << ch1; // cout打印字符"a"

    char ch2{ 98 }; // "b"的码点(不推荐)
    std::cout << ch2; // cout打印一个字符("b")

    return 0;
}

这将产生以下结果:

ab

我们也可以直接输出字符字面量:

std::cout << 'c';

这将产生以下结果:

c

输入字符

以下程序要求用户输入一个字符,然后打印出该字符:

#include <iostream>

int main()
{
    std::cout << "Input a keyboard character: ";

    char ch{};
    std::cin >> ch;
    std::cout << "You entered: " << ch << '\n';

    return 0;
}

一次运行的输出如下:

Input a keyboard character: q
You entered: q

注意,std::cin允许你输入多个字符。然而,变量ch只能存储1个字符。因此,只有第一个输入字符被提取到变量ch中。用户输入的其余部分保留在std::cin使用的输入缓冲区中,并可以通过后续调用std::cin来提取。

你可以在以下示例中看到这种行为:

#include <iostream>

int main()
{
    std::cout << "Input a keyboard character: "; // 假设用户输入"abcd"(不含引号)

    char ch{};
    std::cin >> ch; // ch = 'a', "bcd"留在队列中
    std::cout << "You entered: " << ch << '\n';

    // 注意:以下cin不会提示用户输入,而是获取队列中的输入!
    std::cin >> ch; // ch = 'b', "cd"留在队列中
    std::cout << "You entered: " << ch << '\n';

    return 0;
}

输入结果如下:

Input a keyboard character: abcd
You entered: a
You entered: b

如果你想一次读取多个字符(例如,读取一个名字、单词或句子),你应该使用字符串而不是字符。字符串是一系列连续字符的集合(因此,字符串可以存储多个符号)。我们在后续课程(5.7 – std::string简介)中讨论这个问题。

提取空白字符

由于提取输入会忽略前导空白,这可能导致在尝试将空白字符提取到字符变量时出现意外结果:

#include <iostream>

int main()
{
    std::cout << "Input a keyboard character: "; // 假设用户输入"a b"(不含引号)

    char ch{};
    std::cin >> ch; // 提取a,留下" b\n"在流中
    std::cout << "You entered: " << ch << '\n';

    std::cin >> ch; // 跳过前导空白(空格),提取b,留下"\n"在流中
    std::cout << "You entered: " << ch << '\n';

    return 0;
}

输入结果如下:

Input a keyboard character: a b
You entered: a
You entered: b

在上述示例中,我们可能期望提取空格,但由于前导空白被跳过,我们提取了字符b。

解决这个问题的一个简单方法是使用std::cin.get()函数来执行提取,因为该函数不会忽略前导空白:

#include <iostream>

int main()
{
    std::cout << "Input a keyboard character: "; // 假设用户输入"a b"(不含引号)

    char ch{};
    std::cin.get(ch); // 提取a,留下" b\n"在流中
    std::cout << "You entered: " << ch << '\n';

    std::cin.get(ch); // 提取空格,留下"b\n"在流中
    std::cout << "You entered: " << ch << '\n';

    return 0;
}

输入结果如下:

Input a keyboard character: a b
You entered: a
You entered:  

字符类型的特性

字符大小、范围和默认符号

C++定义char始终为1字节大小。默认情况下,char可以是有符号的或无符号的(尽管通常是有符号的)。如果你使用char来存储ASCII字符,你不需要指定符号(因为有符号和无符号的char都可以存储0到127之间的值)。

如果你使用char来存储小整数(除非你明确地为了节省空间进行优化,否则你不应该这样做),你应该始终指定它是有符号的还是无符号的。有符号的char可以存储-128到127之间的数字。无符号的char可以存储0到255之间的数字。

转义序列详解

基本转义序列

在C++中,有些字符序列具有特殊含义。这些字符被称为转义序列。转义序列以”"(反斜杠)字符开头,然后是一个字母或数字。

你已经看到了最常用的转义序列:’\n’,它可以用来打印换行符:

#include <iostream>

int main()
{
    int x { 5 };
    std::cout << "The value of x is: " << x << '\n'; // 独立的\n放在单引号中
    std::cout << "First line\nSecond line\n";        // \n可以嵌入在双引号中
    return 0;
}

这将输出:

The value of x is: 5
First line
Second line

另一个常用的转义序列是’\t’,它嵌入一个水平制表符:

#include <iostream>

int main()
{
    std::cout << "First part\tSecond part";
    return 0;
}

这将输出:

First part	Second part

其他三个值得注意的转义序列是:

  • ' 打印单引号
  • " 打印双引号
  • \ 打印反斜杠

以下是所有转义序列的表格:

名称符号含义
警报\a发出警报,如蜂鸣声
退格\b将光标向后移动一个空格
换页\f将光标移动到下一页
换行\n将光标移动到下一行
回车\r将光标移动到行首
水平制表符\t打印水平制表符
垂直制表符\v打印垂直制表符
单引号'打印单引号
双引号"打印双引号
反斜杠\打印反斜杠
问号?打印问号
无相关性。你可以使用未转义的问号。
八进制数(number)转换为由八进制表示的字符
十六进制数\x(number)转换为由十六进制数表示的字符
以下是更多示例:
#include <iostream>

int main()
{
    std::cout << "\"This is quoted text\"\n";
    std::cout << "This string contains a single backslash \\\n";
    std::cout << "6F in hex is char '\x6F'\n";
    return 0;
}

打印结果如下:

"This is quoted text"
This string contains a single backslash \
6F in hex is char 'o'

警告

转义序列以反斜杠(\)开头,而不是正斜杠(/)。如果你不小心使用了正斜杠,它仍然可能编译,但不会产生预期的结果。

转义序列的使用注意事项

换行符(\n)与std::endl

我们在课程1.5 – iostream简介:cout、cin和endl中讨论了这个主题。

字符字面量使用指南

单引号和双引号的区别

单引号之间的文本被视为字符字面量,它代表一个字符。例如,“a"代表字符a,"+“代表加号字符,“5"代表字符5(而不是数字5),而”\n"代表换行符。

双引号之间的文本(例如"Hello, world!")被视为C风格字符串字面量,它可以包含多个字符。我们在课程5.2 – 字面量中讨论字符串。

多字符字面量的问题

为了向后兼容,许多C++编译器支持多字符字面量,它们是包含多个字符的字符字面量(例如"56”)。如果支持,这些字面量的值由实现定义(意味着它取决于编译器)。因为它们不是C++标准的一部分,而且它们的值没有严格定义,所以应该避免使用多字符字面量。

最佳实践

避免使用多字符字面量(例如"56”)。

多字符字面量的支持常常给新手程序员带来问题,当他们忘记转义序列是使用正斜杠还是反斜杠时:

#include <iostream>

int add(int x, int y)
{
    return x + y;
}

int main()
{
    std::cout << add(1, 2) << '/n'; // 我们这里使用了正斜杠而不是反斜杠

    return 0;
}

程序员期望这个程序打印出3和一个换行符。但相反,在作者的机器上,它输出了以下内容:

312142

问题是程序员错误地使用了"/n"(一个多字符字面量,由一个正斜杠和一个"n"字符组成),而不是"\n"(换行的转义序列)。程序首先正确地打印出3(add(1, 2)的结果)。然后,它打印出多字符字面量"/n"的值,在作者的机器上,这个值是12142。

警告

确保你的换行符使用转义序列"\n",而不是多字符字面量"/n"。

关键见解

注意,如果我们对输出使用了双引号"/n",程序将打印出3/n,这仍然是错误的,但要少

另一个示例

让我们从以下代码开始:

#include <iostream>

int main()
{
    int x { 5 };
    std::cout << "The value of x is " << x << '\n';

    return 0;
}

这个程序的输出结果正如你所期望的那样:

The value of x is 5

但这个输出结果还不够令人兴奋,于是我们决定在换行符之前添加一个感叹号:

#include <iostream>

int main()
{
    int x { 5 };
    std::cout << "The value of x is " << x << '!\n'; // 添加了感叹号

    return 0;
}

我们期望这个程序的输出结果如下:

The value of x is 5!

然而,由于"!\n"是一个多字符字面量,在作者的机器上,这个程序实际输出的结果是:

The value of x is 58458

这不仅是一个错误,而且很难调试,因为你可能会误以为变量x的值是错误的。

在输出字符字面量时使用双引号(而不是单引号)可以使这类问题更容易被发现,或者完全避免这类问题。

扩展字符类型

Unicode字符支持

就像ASCII将整数0到127映射到美式英语字符一样,其他字符编码标准也存在,用于将不同大小的整数映射到其他语言的字符。除了ASCII之外,最著名的映射是Unicode标准,它将超过144000个整数映射到许多不同语言的字符。由于Unicode包含如此多的码点,一个Unicode码点需要32位来表示一个字符(称为UTF-32)。然而,Unicode字符也可以使用多个16位或8位字符进行编码(分别称为UTF-16和UTF-8)。

C++11引入了char16_t和char32_t,以明确支持16位和32位Unicode字符。这些字符类型分别与std::uint_least16_t和std::uint_least32_t的大小相同(但它们是不同的类型)。C++20引入了char8_t,以支持8位Unicode(UTF-8)。它是一个与unsigned char具有相同表示形式的不同类型。

除非你打算使你的程序与Unicode兼容,否则你不需要使用char8_t、char16_t或char32_t。在几乎所有情况下(除非与Windows API接口),都应该避免使用wchar_t,因为它的大小是由实现定义的。

Unicode和本地化通常超出了这些教程的范围,因此我们不会进一步讨论它们。在此期间,当你处理字符(和字符串)时,你应该只使用ASCII字符。使用其他字符集的字符可能会导致你的字符显示不正确。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"