到目前为止,我们所研究的基本数据类型都用于存储数字(整数和浮点数)或布尔值(真/假)。但如果我们要存储字母或标点符号呢?
#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字符表:
代码 | 符号 | 代码 | 符号 | 代码 | 符号 | 代码 | 符号 |
---|---|---|---|---|---|---|---|
0 | NUL(空) | 32 | (空格) | 64 | @ | 96 | ` |
1 | SOH(报头开始) | 33 | ! | 65 | A | 97 | a |
2 | STX(报文开始) | 34 | " | 66 | B | 98 | b |
3 | ETX(报文结束) | 35 | # | 67 | C | 99 | c |
4 | EOT(传输结束) | 36 | $ | 68 | D | 100 | d |
5 | ENQ(询问) | 37 | % | 69 | E | 101 | e |
6 | ACK(确认) | 38 | & | 70 | F | 102 | f |
7 | BEL(响铃) | 39 | ' | 71 | G | 103 | g |
8 | BS(退格) | 40 | ( | 72 | H | 104 | h |
9 | HT(水平制表符) | 41 | ) | 73 | I | 105 | i |
10 | LF(换行) | 42 | * | 74 | J | 106 | j |
11 | VT(垂直制表符) | 43 | + | 75 | K | 107 | k |
12 | FF(换页) | 44 | , | 76 | L | 108 | l |
13 | CR(回车) | 45 | - | 77 | M | 109 | m |
14 | SO(移出) | 46 | . | 78 | N | 110 | n |
15 | SI(移入) | 47 | / | 79 | O | 111 | o |
16 | DLE(数据链路转义) | 48 | 0 | 80 | P | 112 | p |
17 | DC1(数据控制1) | 49 | 1 | 81 | Q | 113 | q |
18 | DC2(数据控制2) | 50 | 2 | 82 | R | 114 | r |
19 | DC3(数据控制3) | 51 | 3 | 83 | S | 115 | s |
20 | DC4(数据控制4) | 52 | 4 | 84 | T | 116 | t |
21 | NAK(否定确认) | 53 | 5 | 85 | U | 117 | u |
22 | SYN(同步空闲) | 54 | 6 | 86 | V | 118 | v |
23 | ETB(传输块结束) | 55 | 7 | 87 | W | 119 | w |
24 | CAN(取消) | 56 | 8 | 88 | X | 120 | x |
25 | EM(介质结束) | 57 | 9 | 89 | Y | 121 | y |
26 | SUB(替换) | 58 | : | 90 | Z | 122 | z |
27 | ESC(转义) | 59 | ; | 91 | [ | 123 | { |
28 | FS(文件分隔符) | 60 | < | 92 | \ | 124 | | |
29 | GS(组分隔符) | 61 | = | 93 | ] | 125 | } |
30 | RS(记录分隔符) | 62 | > | 94 | ^ | 126 | ~ |
31 | US(单元分隔符) | 63 | ? | 95 | _ | 127 | DEL(删除) |
代码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字符。使用其他字符集的字符可能会导致你的字符显示不正确。