C 基础(4) - 字符串和格式化输入输出

重点介绍输入和输出。与程序交互和使用字符串可以编写个性化的程序,将详细介绍 C语言 的两个输入/输出函数: printf()和 scanf()。学会使用这两个函数,不仅能与用户交互,还可根据个人喜好和任 务要求格式化输出。最后,简要介绍一个重要的工具C 预处理器指令,并学习如何定义、使用符号常量

1. 前导程序

复制代码
// talkback.c -- 演示与用户交互
#include <stdio.h>
#include <string.h>

#define DENSITY 62.4


int main()
// 提供strlen()函数的原型
// 人体密度(单位:磅/立方英尺)
{
    float weight, volume;
    int size, letters;
    char name [40]: // name 是一个可容纳40个字符的数组
    printf("Hi! What's your first name?\n");
    scanf("%s", name);
    printf("%s, what's your weight in pounds?\n", name);
    scanf("%f", &weight);
    size = sizeof name;
    letters = strlen (name);
    volume = weight / DENSITY;
    printf("Well, %s, your volume is %2.2f cubic feet.\n",
    name, volume) ;
    printf("Also, your first name has %d letters, \n",
    letters);
    printf("and we have %d bytes to store it.\n", size);
    
    return 0;
}

该程序包含以下新特性。

  • 用数组(array)储存字符串(character string)。在该程序中,用户输入的名被储存在数组中,该数 组占用内存中40个连续的字节,每个字节储存一个字符值。
  • 使用%s 转换说明来处理字符串的输入和输出。注意,在 scanf () 中, name 没有&前缀,而weight 有(稍后解释,&weight 和 name都是地址)。
  • 用C预处理器把字符常量 DENSITY 定义为62.4。
  • 用C函数 strlen()获取字符串的长度。

对于 BASIC 的输入/输出而言,C 的输入/输出看上去有些复杂。不过,复杂换来的是程序的高效和方便 控制输入/输出。而且,一旦熟悉用法后,会发现它很简单。

2. 字符串简介

字符串(character string)是一个或多个字符的序列,如下所示:

"Zing went the strings of my heart!"

双引号不是字符串的一部分。双引号仅告知编译器它括起来的是字符串,正如单引号用于标识单个字 符一样。

C语言没有专门用于储存字符串的变量类型,字符串都被储存在 char 类型的数组中。数组由连续的存 储单元组成,字符串中的字符被储存在相邻的存储单元中,每个单元储存一个字符

注意图 4.1 中数组末尾位置的字符0。这是空字符(mull character),C语言用它标记字符串的结束。 空字符不是数字0,它是非打印字符,其 ASCII 码值是(或等价于)。C中的字符串一定以空字符结束, 这意味着数组的容量必须至少比待存储字符串中的字符数多1。因此,程序清单 4.1 中有40个存储单元的 字符串,只能储存39个字符,剩下一个字节留给空字符。

那么,什么是数组?可以把数组看作是一行连续的多个存储单元。用更正式的说法是,数组是同类型数据元素的有序序列。程序清单4.1 通过以下声明创建了一个包含40个存储单元(或元素)的数组,每个 单元储存一个char 类型的值:

char name [40];

name 后面的方括号表明这是一个数组,方括号中的40 表明该数组中的元素数量。char 表明每个元 素的类型(见图4.2)。

字符串看上去比较复杂!必须先创建一个数组,把字符串中的字符逐个放入数组,还要记得在末尾加 上一个\0。还好,计算机可以自己处理这些细节。

3. 字符串和字符

字符串常量"x"和字符常量'x'不同。区别之一在于'x'是基本类型(char),而"x"是派生类型(char 数组);区别之二是"x"实际上由两个字符组成:'x'和空字符0(见图4.3)

4. strlen()函数

上一章提到了 sizeof 运算符,它以字节为单位给出对象的大小。strlen()函数给出字符串中的字 符长度。因为1字节储存一个字符,读者可能认为把两种方法应用于字符串得到的结果相同,但事实并非 如此。请根据程序清单4.3,在程序清单 4.2 中添加几行代码,看看为什么会这样。

复制代码
#include <stdio.h>
#include <string.h> /* 提供 strlen()函数的原型 */

#define PRAISE "You are an extraordinary being."

int main (void)
{
    char name [40];
    printf("What's your name? ");
    scanf("%s", name);
    printf("Hello, %s. %s\n", name, PRAISE);
    printf("Your name of %zd letters occupies %zd memory cells.\n",
    strlen (name), sizeof name);
    printf("The phrase of praise has %zd letters ",
    strlen (PRAISE));
    printf("and occupies %zd memory cells.\n", sizeof PRAISE);
    return 0;
}

一般而言,C 把函数库中相关的函数归为一类,并为每类函数提供一个头文件。例如, printf()和 scanf () 都隶属标准输入和输出函数,使用stdio.h 头文件。string.h 头文件中包含了 strlen()函 数和其他一些与字符串相关的函数(如拷贝字符串的函数和字符串查找函数)。

注意,程序清单 4.3 使用了两种方法处理很长的 printf()语句。第1 种方法是将 printf() 语句分 为两行(可以在参数之间断为两行,但是不要在双引号中的字符串中间断开);第2种方法是使用两个 printf() 语句打印一行内容,只在第2条 printf()语句中使用换行符(\n)。运行该程序,其交互输出 如下:

What's your name? Serendipity Chance

Hello, Serendipity. You are an extraordinary being.

Your name of 11 letters occupies 40 memory cells.

The phrase of praise has 31 letters and occupies 32 memory cells.

sizeof 运算符报告,name 数组有40个存储单元。但是,只有前11个单元用来储存 Serendipity, 所以 strlen()得出的结果是11。name 数组的第12个单元储存空字符, strlen () 并未将其计入。图4.4 演示了这个概念。

对于 PRAISE,用 strlen()得出的也是字符串中的字符数(包括空格和标点符号)。然而, sizeof 运算符给出的数更大,因为它把字符串末尾不可见的空字符也计算在内。该程序并未明确告诉计算机要给 字符串预留多少空间,所以它必须计算双引号内的字符数。

第3章提到过,C99 和 Cll 标准专门为 sizeof 运算符的返回类型添加了%zd 转换说明,这对于 strlen()同样适用。对于早期的C,还要知道sizeof 和 strlen() 返回的实际类型(通常是unsigned 或unsigned long)。

另外,还要注意一点:上一章的 sizeof 使用了圆括号,但本例没有。圆括号的使用时机否取决于运 算对象是类型还是特定量?运算对象是类型时,圆括号必不可少,但是对于特定量,可有可无。也就是说, 对于类型,应写成 sizeof (char)或sizeof(float);对于特定量,可写成 sizeof name 或 sizeof 6.28。尽管如此,还是建议所有情况下都使用圆括号,如 sizeof (6.28)。

5. 常量和C预处理器

前面介绍了预处理器如何使用#include 包含其他文件的信息。 预处理器也可用来定义常量。只需在程序顶部添加下面一行:

复制代码
#define TAXRATE 0.015

编译程序时,程序中所有的TAXRATE 都会被替换成 0.015。这一过程被称为编译时替换(compile-time substitution)。在运行程序时,程序中所有的替换均已完成(见图 4.5)。通常,这样定义的常量也称为明示 常量(manifest constant)

请注意格式,首先是#define,接着是符号常量名(TAXRATE),然后是符号常量的值(0.015)(注 意,其中并没有=符号)。所以,其通用格式如下:

复制代码
#define NAME value

实际应用时,用选定的符号常量名和合适的值来替换 NAME 和 value。注意,末尾不用加分号,因为 这是一种由预处理器处理的替换机制。为什么 TAXRATE 要用大写?用大写表示符号常量是 C语言一贯的 传统。这样,在程序中看到全大写的名称就立刻明白这是一个符号常量,而非变量。大写常量只是为了提 高程序的可读性,即使全用小写来表示符号常量,程序也能照常运行。尽管如此,初学者还是应该养成大 写常量的好习惯。

#define 指令还可定义字符和字符串常量。前者使用单引号,后者使用双引号。如下所示:

复制代码
#define BEEP '\a'
#define TEE 'T'
#define ESC '\033'
define OOPS "Now you have done it"

5.1 const 限定符

C90 标准新增了 const 关键字,用于限定一个变量为只读。其声明如下:

const int MONTHS = 12; // MONTHS 在程序中不可更改,值为12

这使得 MONTHS 成为一个只读值。也就是说,可以在计算中使用 MONTHS,可以打印 MONTHS,但是不 能更改 MONTHS的值。const 用起来比#define 更灵活,第12章将讨论与 const 相关的内容。

6. printf() 和 scanf()

printf()函数和scanf()函数能让用户可以与程序交流,它们是输入/输出函数,或简称为I/O 函数。 它们不仅是C语言中的I/O 函数,而且是最多才多艺的函数。过去,这些函数和C库的一些其他函数一样, 并不是C语言定义的一部分。最初,C把输入/输出的实现留给了编译器的作者,这样可以针对特殊的机器 更好地匹配输入/输出。后来,考虑到兼容性的问题,各编译器都提供不同版本的printf()和scanf ()。 尽管如此,各版本之间偶尔有一些差异。C90 和 C99 标准规定了这些函数的标准版本,本书亦遵循这一 标准。 虽然 printf()是输出函数,scanf ()是输入函数,但是它们的工作原理几乎相同。两个函数都使用 格式字符串和参数列表。我们先介绍 printf(),再介绍 scanf()。

6.1 printf()函数

请求 printf()函数打印数据的指令要与待打印数据的类型相匹配。例如,打印整数时使用%d,打印 字符时使用%c。这些符号被称为转换说明(conversion specification),它们指定了如何把数据转换成可显示 的形式。我们先列出ANSIC 标准为printf()提供的转换说明,然后再示范如何使用一些较常见的转换说 明。表4.3 列出了一些转换说明和各自对应的输出类型。

这是printf()函数的格式:

复制代码
printf( 格式字符串,待打印项1,待打印项2,...);

注意 类型可移植性

sizeof 运算符以字节为单位返回类型或值的大小。这应该是某种形式的整数,但是标准只规定 了该值是无符号整数。在不同的实现中,它可以是unsigned int unsigned long 甚至是unsigned long long。因此,如果要用 printf()函数显示 sizeof 表达式,根据不同系统,可能使用%u、lu 或%11u。这意味着要查找你当前系统的用法,如果把程序移植到不同的系统还要进行修改。鉴于此, C提供了可移植性更好的类型。首先,stddef.h 头文件(在包含 stdio.h 头文件时已包含其中)把 size_t 定义成系统使用 sizeof 返回的类型,这被称为底层类型( underlying type)。其次, printf() 使用z修饰符表示打印相应的类型。同样,C还定义了 ptrdiff t类型和修饰符来表示系统使用 的两个地址差值的底层有符号整数类型。

注意 float 参数的转换

对于浮点类型,有用于 double 和 long double 类型的转换说明,却没有 float 类型的。这 是因为在K&R C 中,表达式或参数中的 float 类型值会被自动转换成 double 类型。一般而言,ANSI C不会把float 自动转换成 double。然而,为保护大量假设 float 类型的参数被自动转换成 double 的现有程序, printf() 函数中所有 float 类型的参数(对未使用显式原型的所有C函数都有效)仍 自动转换成 double 类型。因此,无论是K&R C 还是 ANSI C,都没有显示 float 类型值专用的转 换说明。

6.2 scanf()

刚学完输出,接下来我们转至输入------学习 scanf ()函数。C库包含了多个输入函数,scanf()是最 通用的一个,因为它可以读取不同格式的数据。当然,从键盘输入的都是文本,因为键盘只能生成文本字 符:字母、数字和标点符号。如果要输入整数 2014,就要键入字符 2、0、1、4。如果要将其储存为数值 而不是字符串,程序就必须把字符依次转换成数值,这就是scanf) 要做的。scanf ()把输入的字符串转 换成整数、浮点数、字符或字符串,而 printf () 正好与它相反,把整数、浮点数、字符和字符串转换成 显示在屏幕上的文本。

scanf() 和 printf()类似,也使用格式字符串和参数列表。scanf()中的格式字符串表明字符 输入流的目标数据类型。两个函数主要的区别在参数列表中。printf()函数使用变量、常量和表达式, 而scanf()函数使用指向变量的指针。这里,读者不必了解如何使用指针,只需记住以下两条简单的 规则:

  • 如果用 scanf () 读取基本变量类型的值,在变量名前加上一个&;
  • 如果用 scanf ()把字符串读入字符数组中,不要使用&。
  1. 从 scanf()角度看输入 接下来,我们更详细地研究 scanf ()怎样读取输入。假设 scanf() 根据一个%d 转换说明读取一个整数。 scanf() 函数每次读取一个字符,跳过所有的空白字符,直至遇到第1个非空白字符才开始读取。因为要读 取整数,所以 scanf()希望发现一个数字字符或者一个符号(+或-)。如果找到一个数字或符号,它便保存 该字符,并读取下一个字符。如果下一个字符是数字,它便保存该数字并读取下一个字符。scanf()不断地 读取和保存字符,直至遇到非数字字符。如果遇到一个非数字字符,它便认为读到了整数的末尾。然后, scanf () 把非数字字符放回输入。这意味着程序在下一次读取输入时,首先读到的是上一次读取丢弃的非数字字符。 最后,scanf()计算已读取数字(可能还有符号)相应的数值,并将计算后的值放入指定的变量中。 如果使用字段宽度, scanf ()会在字段结尾或第1个空白字符处停止读取(满足两个条件之一便停止)。 如果第1个非空白字符是A而不是数字,会发生什么情况? scanf()将停在那里,并把A放回输入中, 不会把值赋给指定变量。程序在下一次读取输入时,首先读到的字符是A。如果程序只使用%d转换说明, scanf ()就一直无法越过A读下一个字符。另外,如果使用带多个转换说明的 scanf(), C 规定在第1个 出错处停止读取输入。 用其他数值匹配的转换说明读取输入和用%d 的情况相同。区别在于 scanf ()会把更多字符识别成数 字的一部分。例如,%x 转换说明要求 scanf ()识别十六进制数af 和A~E。浮点转换说明要求 scanf() 识别小数点、e记数法(指数记数法)和新增的p记数法(十六进制指数记数法)。 如果使用%s 转换说明, scanf ()会读取除空白以外的所有字符。scanf()跳过空白开始读取第1个 非空白字符,并保存非空白字符直到再次遇到空白。这意味着 scanf () 根据s 转换说明读取一个单词, 即不包含空白字符的字符串。如果使用字段宽度, scanf () 在字段末尾或第1个空白字符处停止读取。无 法利用字段宽度让只有一个s 的scanf () 读取多个单词。最后要注意一点:当scanf () 把字符串放进指 定数组中时,它会在字符序列的末尾加上'0',让数组中的内容成为一个C字符串。

7. 关键概念

C语言用 char 类型表示单个字符,用字符串表示字符序列。字符常量是一种字符串形式,即用双引号 把字符括起来:"Good luck, my friend"。可以把字符串储存在字符数组(由内存中相邻的字节组成)中。 字符串,无论是表示成字符常量还是储存在字符数组中,都以一个叫做空字符的隐藏字符结尾。

在程序中,最好用#define 定义数值常量,用 const 关键字声明的变量为只读变量。在程序中使用 符号常量(明示常量),提高了程序的可读性和可维护性。

C语言的标准输入函数(scanf())和标准输出函数(printf())都使用一种系统。在该系统中, 第1个参数中的转换说明必须与后续参数中的值相匹配。例如,int 转换说明%d与一个浮点值匹配会产生 奇怪的结果。必须格外小心,确保转换说明的数量和类型与函数的其余参数相匹配。对于 scanf(),一定 要记得在变量名前加上地址运算符(&)。

空白字符(制表符、空格和换行符)在 scanf() 处理输入时起着至关重要的作用。除了%c 模式(读 取下一个字符), scanf()在读取输入时会跳过非空白字符前的所有空白字符,然后一直读取字符,直至遇 到空白字符或与正在读取字符不匹配的字符。考虑一下,如果scanf()根据不同的转换说明读取相同的输 入行,会发生什么情况。假设有如下输入行:

-13.45e12# 0

如果其对应的转换说明是%d, scanf ()会读取3个字符(-13)并停在小数点处,小数点将被留在输 入中作为下一次输入的首字符。如果其对应的转换说明是%f, scanf ()会读取-13.45e12,并停在#符号 处,而#将被留在输入中作为下一次输入的首字符;然后,scanf()把读取的字符序列-13.45e12 转换成 相应的浮点值,并储存在 float 类型的目标变量中。如果其对应的转换说明是%s, scanf()会读取 -13.45e12#,并停在空格处,空格将被留在输入中作为下一次输入的首字符;然后, scanf ()把这10 个字符的字符码储存在目标字符数组中,并在末尾加上一个空字符。如果其对应的转换说明是%c, scanf () 只会读取并储存第1个字符,该例中是一个空格。

相关推荐
散峰而望10 小时前
【算法竞赛】C++函数详解:从定义、调用到高级用法
c语言·开发语言·数据结构·c++·算法·github
冷凝雨10 小时前
复数乘法(C & Simulink)
c语言·开发语言·信号处理·simulink·dsp
CoderCodingNo10 小时前
【GESP】C++五级真题(贪心思想考点) luogu-B4071 [GESP202412 五级] 武器强化
开发语言·c++·算法
0和1的舞者10 小时前
Spring AOP详解(一)
java·开发语言·前端·spring·aop·面向切面
MoonBit月兔10 小时前
年终 Meetup:走进腾讯|AI 原生编程与 Code Agent 实战交流会
大数据·开发语言·人工智能·腾讯云·moonbit
智航GIS10 小时前
8.2 面向对象
开发语言·python
小小星球之旅11 小时前
CompletableFuture学习
java·开发语言·学习
智者知已应修善业11 小时前
【求等差数列个数/无序获取最大最小次大次小】2024-3-8
c语言·c++·经验分享·笔记·算法
kylezhao201912 小时前
C# 语言基础(变量、数据类型、流程控制、面向对象编程)
开发语言·计算机视觉·c#·visionpro