换个姿势学C语言 第5章 获取完整的牌价数据 5.3节 字符和字符串
0. 说明
《换个姿势学C语言》由何旭辉 著,清华大学出版社2022年出版。感谢何老师!

这是一本非常不错的书!
- 第一、二、三章总结参考换个姿势学C语言 第1-3章
- 第四章总结参考换个姿势学C语言 第4章
- 第五章5.1节总结参考换个姿势学C语言 第5章 5.1节
- 第五章5.2节总结参考换个姿势学C语言 第5章 5.2节
- 第五章5.3节总结参考换个姿势学C语言 第5章 5.3节
5. 获取完整的牌价数据
5.3 字符和字符串
在5.1.2节中,我们已经可以使用一个双精度浮点型数组存储现汇买入价、现钞买入价、现汇卖出价、现钞卖出价和中行折算价。

对于文字,很多语言提供了“字符串”这种数据类型。
字符串可用于存储多个连续的字符。
你正在阅读的教程文本、人名、要显示的货币名称等都是字符串。
字符串中的字符可以是汉字、字母、数字、符号或者其他国家的文字符号。
C语言并没有提供“字符串”这种数据类型。
deepseek的回答:
在C语言中,没有直接提供“字符串”数据类型,而是通过字符数组和字符指针来处理字符串。这种设计有以下几个原因:
历史原因 C语言诞生于20世纪70年代,设计初衷是用于系统编程,强调简洁和高效。当时的内存和处理能力有限,因此C语言选择了一种轻量级的方式处理字符串,即使用字符数组和指针。
灵活性 C语言的设计理念是提供底层控制,允许程序员直接操作内存。通过字符数组和指针,程序员可以灵活地管理字符串的内存分配、复制、拼接等操作,而不依赖于高级语言中的“黑箱”实现。
与硬件的紧密联系 C语言的设计目标是贴近硬件,字符数组和指针可以很好地映射到内存的连续地址空间。这种设计使得C语言在处理字符串时非常高效,适合系统编程和嵌入式开发。
简单性 C语言的核心语法非常简洁,没有引入复杂的字符串类型。通过字符数组和指针,C语言可以用最小的语法开销实现字符串操作。
以
\0结尾的约定 C语言中的字符串以\0(空字符)作为结束标志,这种设计简单且高效。虽然需要程序员手动管理字符串的长度和内存,但也避免了额外的存储开销。标准库支持 虽然C语言本身没有字符串类型,但标准库(如
<string.h>)提供了丰富的字符串操作函数(如strcpy、strlen、strcat等),弥补了语言本身的不足。总结 C语言不提供字符串类型是为了保持语言的简洁性、灵活性和高效性。通过字符数组和指针,程序员可以完全控制字符串的内存和操作,虽然这增加了编程的复杂性,但也使得C语言在系统编程和底层开发中表现出色。
我们先来了解C语言中是如何处理字符和字符串的。
5.3.1 计算机中的字符
- 严格意义上说,无论是中文使用的汉字还是英文中的字母都是一种图形,而计算机只能处理数值,准确地说是只能处理二进制数值。所有信息都必须转换成二进制的数值后才能被计算机处理。
例如字符“F”,要在屏幕上显示它就必须将这个字母的外观用二进制数值进行描述。方法之一就是把字符的外形划分成若干个栅格,然后分别用1和0来描述每个栅格的内容。
如下图所示就是字符F的点阵描述:

可以看到,描述字母F的外观需要32个二进制位,也就是4个字节。但是文字是会重复使用的,如果每存储一个英文字符都要使用4个字节,在计算机技术发展的初期是不可接受的。
因此,在计算机系统中只保留一份用于显示的字符点阵信息(被称为字库或字模),然后给每个字符一个唯一的编号,在传输、存储字符时只使用字符的编号。换句话说,在存储大写字母F时不是存储它的点阵信息,而是存储一个数字编号来代表它(只须1个字节即可),这个编号被称为“字符编码”,到了要显示F的时候再根据这个编码从字库中调出它的点阵信息用于显示。
用数字代表文字不是计算机领域的发明,电报也是将文字转换成数字发出,接收方再根据双方一致的“密码本”将其翻译成文字。
- 计算机中的字符编码必须采用公开。通用的编码标准。这样才能在不同类型的计算机之间交换信息。
5.3.2 字符编码
现今的计算机系统中存在多种字符编码标准,例如ASCII码、GB2312、GBK和Unicode码等。
- ASCII码是目前使用最普遍的字符编码集,是美国信息交换标准代码(American Standard Code for Information Interchange)。
- 基本的ASCII字符集只包含128个字符,含控制字符、符号、数字和字母。例如英语字母A对应的编码值是65,字母B对应的编码值是66.后来扩展的ASCII字符集包含了256个字符。 详细可参考 https://ascii-code.net/

- 在ASCII编码中,最小的编码值是0,最大的编码值是255。因此表达一个ASCII字符编码需要使用1个字节(因为一个字节能表示的最小值是0,最大值是255)。
- 汉字编码:最多支持256个字符的ASCII编码自然不包含汉字。中国国家标准总局在1980年发布了国家标准GB 2312用于统一汉字编码,在GB 2312标准中共收录了6763个汉字。
- GB 2312字符集只能满足基本的汉字处理需要,但对于人名、古汉语等方面出现的罕用字和繁体字,GB 2312不能处理,因此1995年又发布了《汉字内码扩展规范》(简称GBK),GBK共收录了21886个汉字和图形符号。GBK出现以后,解决了很多人名中的汉字无法输入计算机的问题。例如,镕这个字在GB2312中是不包含的。
- Unicode字符集:不同国家和地区采用不同的字符集来显示文字,在计算机技术发展之初没有什么问题,但是在交流日益频繁的今天,特别是互联网流行以后,制定一种统一的字符集标准越来越有必要。随着计算机存储容量、运算速度的提高,设计一种可以包含全人类所有字符的字符集成为可能,于是就有了Unicode。Unicode编码标准由一个叫做统一码联盟的机构制定,这个机构有来自多个国家政府和各大软件商的代表参与,工作目标是最终以Unicode取代现存的其他字符编码标准。最新的Unicode字符集收录了超过13万个字符。
- 在本课程中,我们只使用ASCII字符集和GB 2312字符集,不涉及对Unicode编码的处理。
5.3.3 编码是如何被显示成字符的
现在,我们已经知道字符在内存里存储的是编码,编码本身是不易被阅读的,那么谁负责把字符编码转换成人眼可以阅读的图形符号并显示在屏幕上呢?简单地说,是显卡负责将文本或图形数据转换成视频信号,并显示在屏幕上。
显卡的三个重要组成部分: 显存,显卡BIOS,GPU。
- 显存是显卡上的独立存储器,它用来存储即将显示的数据。
- 显卡BIOS(基本输入输出系统),存储在显卡上的ROM芯片中。在显卡BIOS中存储了每一个ASCII字符的图形数据,你可以将图形数据理解为这些字符的“笔画”,它也被称为“字库”。在文本模式下送入显存的是字符的ASCII码值,BIOS根据ASCII码值在BIOS中查询到每个字符的图形信息,生成视频信号并输出给显示器,这样字符就显示到屏幕了。实际的情况要复杂一些。因为现代计算机系统保留了多份字库用以显示不同的字体,字体也不仅存储在显卡BIOS中。
- GPU-Graphics Procssing Unit是显卡的核心,全称是“图形处理器”,它负责将要显示的数据转换为视频信号。
- 图形显示的原理与文本显示类似,都是把要显示的图形数据送到显存中去即可。在后面的章节会通过显示.bmp图片的例子进行详细阐述其原理。
5.3.4 C语言中的字符
现在,我们知道了所有文字都要按照某个标准进行编码后才可以被计算机存储和传输。在C语言程序中,我们使用“字符型变量”或“字符型常量”来存储单个字符的编码。
5.3.4.1 字符型变量
在C语言中,char数据类型用于存储单字节字符的编码,例如ASCII编码。char数据类型占用1个字节,它用于存储0~255的字符编码,声明字符型变量的方法与声明整型变量一样。
| |
char数据类型占用1个字符,可以存储的值是0~255,所以它只能存储ASCII字符的编码。如果希望变量ch存储字符A的编码,代码可以写成这样:
| |
5.3.2节点已经介绍过: 英语字母A对应的编码值是65,字母B对应的编码值是66.后来扩展的ASCII字符集包含了256个字符。 详细可参考 https://ascii-code.net/
5.3.4.2 字符型常量
- ASCII字符一共有256个,普通人不会记得所有字母的ASCII码值。所以如果要让字符变量
ch存储字符A的码值,一般是这样写的:
| |
编译器在编译这行代码时会自动将'A'替换成65。 这样写代码显示比要我们记住每个字符的编码更容易和直观。两端包含单引号的A被称为【字符常量】,字符常量的本质是一个整型数,即这个字符的编码值。
- 在C语言中,用一对单引号包含一个字符被称为【字符常量】,使用字符常量可以使程序更容易编写和阅读。
- 一定要记住,在使用ASCII字符集的环境里,
'A'与整数65是相等的。
看以下示例代码:
| |
运行代码,显示输出如下:

5.3.5 从键盘输入字符并显示
现在我们知道了字符变量和字符常量可以存储一个ASCII字符的编码,也知道了如何将一个字符的编码值(或字符常量)赋值给它。那么字符是如何输入到计算机中的呢?
5.3.5.1 字符是如何输入计算机的
在程序设计中最常见的需求是从键盘获得用户的输入 。用户的输入可能是一个字符、一串字符或者数字。
- 键盘是由一组开关组成的,在键盘里有一个微处理器负责扫描这些开关的状态。
键盘的微处理器实时扫描每个按键的状态,当微处理器检测到一个按键动作时会产生一个“扫描码”,并且通知操作系统,操作系统立即停下目前的工作来处理这个按键动作。这个动作不需要程序员干预。
扫描码的本质:每个按键在键盘硬件层面有唯一的 “扫描码”(比如回车键扫描码是 0x1C),这是硬件厂商约定的,和操作系统、编程语言无关。键盘微处理器实时扫描(比如每秒上千次),检测到按键按下 / 松开时,就生成对应的扫描码。
中断机制(无需程序员干预的核心):键盘是计算机的 “高优先级外设”,按下按键时会触发 CPU 的硬件中断(键盘中断号通常是 IRQ1)。CPU 会立刻暂停当前执行的程序,转而去执行操作系统预设的 “中断服务程序(ISR)”—— 这个程序是操作系统内核写好的,负责:
- 读取扫描码并转换为系统能识别的 “虚拟键码”(比如 Windows 的 VK_RETURN);
- 处理按键的附加状态(如 Shift/Ctrl/Alt);
- 把按键事件分发到当前激活的窗口 / 应用。
程序员的角色:程序员不需要关心 “扫描码→中断→键码转换” 这些底层步骤,只需要在应用层接收操作系统封装好的 “按键事件”(比如 C# 的事件、C++ 的消息),处理业务逻辑即可(比如按回车提交表单)。
graph TD
A[按下按键] --> B[键盘微处理器扫描]
B --> C[生成扫描码(硬件级唯一标识)]
C --> D[通过接口(USB/PS2)发送给主板]
D --> E[主板转发给CPU的中断控制器]
E --> F[CPU触发键盘中断(优先级高)]
F --> G[操作系统中断服务程序ISR响应]
G --> H[将扫描码转换为系统级键码(如ASCII/虚拟键码)]
H --> I[操作系统处理(如输入字符/执行快捷键)]
I --> J[应用程序接收键码(可选,程序员仅需处理这一步)]
我们只需要知道,在用户按下字母按键后,该键对应的ASCII码值就会被送到键盘缓冲区中,等待被程序读取。
- 当需要把用户输入的内容当作字符处理时,就使用获取字符输入的函数,例如
getchar、getch等。 - 当需要把用户输入的内容转换成其他类型的数值时,最简单的方法是使用
scanf函数,它提供了类型转换功能。scanf函数也可以用于输入多个字符(即字符串)。 - 需要说明的是,C语言的标准输入采用了“行缓冲”机制,只有在用户按下回车键并在键盘缓冲区中产生换行符后才会认为输入已完成,程序中的函数才可以读取键盘缓冲区中的内容。
5.3.5.2 使用getchar函数获取用户输入的单个字符
请看以下示例:
| |
运行程序时,输入字母a并按下回车键:

从以上程序可以看出getchar函数调用时无须参数,它将用户输入的第1个字符的ASCII码值(97)作为返回值。我们用字符型变量ch存储它,并在第7行代码中分别以字符(%c)和整数(%d)格式显示出来。
此时,需要注意的是getchar()读取到a后,键盘缓冲区的a会被移出键盘缓冲区,但如下的内容(如回换行符)仍然保留在键盘缓冲区。
| |
像这样运行代码后,可以看到第一次输入a回车后,ch1是直接读取到回车键数据的。

通过以下代码:
| |
运行输出:

可以看到,当用户输入abcd并回车后,程序直接运行输出了结果,程序并没有等待用户再次输入,而是直接从键盘缓冲区中取出了第1次输入的字符b和c。
5.3.5.3 清空键盘缓冲区
可以使用rewind函数来清空键盘缓冲区,这样可以让getchar函数每次都读取用户重新输入的数据。
| |
输出如下:

可以看到,使用rewind(stdin)清空标准输入(standard input)后,后面的getchar不会从前面的输入中数据数据,而是重新从用户输入读取数据。
清空键盘缓冲区有时特别重要,例如,程序在询问用户“确定要删除全部数据吗? (Y/N)”,而恰好键盘缓冲区中残留着一个“Y”,惨剧就会发生!!
- 在需要精准获取用户交互式确认(如 Y/N)前,必须清空键盘缓冲区,避免残留字符被误读。
但通过实验发现rewind(stdin);也不是很靠谱!!
看以下代码:
| |

推荐使用clearInputBuffer函数来清空缓冲区。
5.3.6 C语言中的字符串
char型变量只能存储单个ASCII值,但是在程序设计中更多的时候是使用“字符串”,例如一个人的名字,一种货币的名称都是字符串。
5.3.6.1 字符串常量
- 如果要描述一个固定不变的字符串,可以使用【字符串常量】。
printf("Hello,World\n");中的"Hello,World\n"就是字符串常量。
字符常量与字符串常量的区别:
- 使用一对单引号包含的单个字符,称为【字符常量】,如,
char ch = 'A'; - 使用一对双引号包含的一个字符或多个字符,称为【字符串常量】,如,
printf("Hello");
编译器在处理被双引号包含的字符串常量时,会根据字符串的内容自动为其分配一块内存空间,将双引号内的字符逐一存入这块内存空间。
- 编译器会自动字符末尾追加一个ASCII码值为0的字符标识字符串结束,这个字符被称为【字符串终止符】,当处理这个字符串的程序遇到字符串终止符时就知道这个字符串到此结束了,不再理会之后的内容。

直接使用char* str = "Hello"; 编译器会提示异常:"const char *" 类型的值不能用于初始化 "char *" 类型的实体和“初始化”: 无法从“const char [6]”转换为“char *”。这并是因为我们对字符串常量理解有误,而是编译器在试图阻止你犯错误。
- 编译器在处理被双引号包含的字符串常量时,会根据字符串的内容自动为其分配一块内存空间,这块内存空间是在“常量区”的,修改常量区的数据是不常见且危险的,因此新的编译器不允许你这么干。
要解决这个问题,只需要在声明指针时前面加一个const限定符。
优化后的代码:
| |
运行代码:

如果尝试使用*str = 'B';来改变字符串常量的内容,编译器会立即阻止程序。

- 被双引号包含的若干个字符称为【字符串常量】。
- 程序编译时会为字符串常量分配一块内存空间,并将这些字符连续存入其中,且在末尾追加一个字符串终止符
'\0'。 - 在表达式中,字符串常量的值是存储字符串的内存空间的首地址,可以将其赋值给指针变量。
- 不允许通过上述指针变量修改这块内存空间的内容,以确保字符串常量的安全。
5.3.6.2 C语言中存储可变化字符串的方法
既然字符串常量的内容是不可以变化的,那么该如何存储可能会变化的字符串呢?例如我们从服务器获取到的货币名称该如何存储?
在有些编程语言里存在“字符串”这种数据类型,可以直接声明字符串变量用于存储字符串。但是C语言没有设计“字符串”这种数据类型,只能使用“字符数组”来存储字符串。
- 数组是在内存中的一块连续空间,空间的大小取决于数组的定义。
char型变量可以用于存储单个字符。- 可以使用字符型数据来存储字符串。
声明一个100个字符的字符型数组str的方法如下:
| |
然后就可以将多个字符按次序存入其中,但不要试图使用这样的方法:
| |
虽然它看上去很容易理解,在很多语言里面也都可以这么做,但在前面我们讲过“在表达式中,字符串常量的值存储的是字符串的内存空间的首地址”,你不能将一个地址赋值给数组str,因此上面的写法是错误的!!!
在C语言里面要向字符数组写入字符串“Hello”的最朴素的方法是一个数组元素写一个字符。
| |
运行程序:

需要注意的是,虽然程序在运行时只使用了100个数组元素中的6个,但数组str仍然占用100个字节的内存空间,而且未赋值的数组元素的值是不确定的。使用固定大小的字符数组和字符串终止符的方式存储字符串会带来浪费,但这是一种最简便的方法。
- 如果要存储大量文本,也一样可以使用
malloc函数来动态分配内存。5.1节详细介绍了mailloc的使用。
示例:
| |
上面的程序使用逐个给数组元素赋值的方法演示了字符串的存储方式,但我们定义字符数组的目的是为了存储可变的字符串。最情况就是让用户输入字符串的内容,此时可以使用scanf函数。
5.3.7 使用scanf函数输入字符串
- 相对于
getchar函数从键盘缓冲区取得单个字符,scanf函数可以从键盘缓冲区中获取多个字符,并将其存入字符数组。
以下程序是从键盘输入字符串的代码:
| |
运行程序:

当我输入abcdeABCDE12345时,scanf马上读取到我的输入,printf函数就打印出了我的输入。
第13行代码使用scanf函数获取键盘输入内容,第1个参数%s表示要将用户的输入作为字符串处理,第2个参数str表示要将获取的字符串送入数组str中,此时scanf函数相当于一个搬运工。
当我在输入字符串中间有空格时,scanf也不会读取空格后面的字符:

如我输入abcde ABCDE 12345,最终输出的是abcde,说明e后面空格开始的字符都没读取到。
- 在
scanf函数运行时,如果键盘缓冲区中没有包含换行符的内容,会停下来等待用户输入;在用户输入若干个字符并按下回车键后,开始进行处理; scanf函数将键盘缓冲区中的内容复制到数组中,但也不是全部复制—–当scanf函数遇到空格或换行符时将不再复制,没有复制完的字符仍然留在键盘缓冲区中;scanf函数还做了一件事:自动在用户输入的最后一个字符后加上字符串终止符\n并一起送入到数组,这一点很重要!!!- 默认情况下,从键盘缓冲区取字符串时遇到空格就“停止搬运”是C语言标准中对
scanf函数的规定。
以下是C语言中使用字符数组存储字符串变量的方法:
- 预先定义一个固定大小的字符数组;
- 在字符数组中逐个存储组成字符串的字符;
- 约定字符串中以
\0表示字符串结束。
我们可以通过逐个给字符数组中的元素赋值来改变字符串的内容,也可以通过scanf函数从键盘输入字符串到字符数组中。后面还会学习其他方法。
