程序清单4.1 talkback.c程序

// talkback.c -- 演示与用户交互
#include <stdio.h>
#include <string.h>   // 提供strlen()函数的原型
#define DENSITY 62.4  // 人体密度(单位:磅/立方英尺)
int main()
{
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()获取字符串的长度。

4.2 字符串简介

字符串(character string)是一个或多个字符的序列,如下所示:
“Zing went the strings of my heart!”
双引号不是字符串的一部分。双引号仅告知编译器它括起来的是字符串,正如单引号用于标识单个字符一样。

4.2.1 char类型数组和null字符

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

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

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

char name[40];

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

4.2.2 使用字符串

试着运行程序清单4.2,使用字符串其实很简单。
程序清单4.2 praise1.c程序

/* praise1.c -- 使用不同类型的字符串 */
#include <stdio.h>
#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);
return 0;
}

%s告诉printf()打印一个字符串。%s出现了两次,因为程序要打印两个字符串:一个储存在name数组中;一个由PRAISE来表示。运行praise1.c,其输出如下所示:

What's your name? Angela Plains
Hello, Angela.You are an extraordinary being.

你不用亲自把空字符放入字符串末尾,scanf()在读取输入时就已完成这项工作。也不用在字符串常量PRAISE末尾添加空字符。稍后我们会解释#define指令,现在先理解PRAISE后面用双引号括起来的文本是一个字符串。编译器会在末尾加上空字符。

注意(这很重要),scanf()只读取了Angela Plains中的Angela,它在遇到第1个空白(空格、制表符或换行符)时就不再读取输入。因此,scanf()在读到Angela和Plains之间的空格时就停止了。一般而言,根据%s转换说明,scanf()只会读取字符串中的一个单词,而不是一整句。C语言还有其他的输入函数(如,fgets()),用于读取一般字符串。后面章节将详细介绍这些函数。

字符串和字符

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

4.2.3 strlen()函数

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

程序清单4.3 praise2.c程序
/* praise2.c */
// 如果编译器不识别%zd,尝试换成%u或%lu。
#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);      //Serendipity Chance
printf("Hello, %s.%s\n", name, PRAISE);
//Hello, Serendipity.You are an extraordinary being.
printf("Your name of %zd letters occupies %zd memory cells.\n",
strlen(name), sizeof name);
//Your name of 11 letters occupies 40 memory cells
printf("The phrase of praise has %zd letters ",
strlen(PRAISE));
printf("and occupies %zd memory cells.\n", sizeof PRAISE);
//The phrase of praise has 31 letters and occupies 32 memory cells.
return 0;
}

如果使用ANSI C之前的编译器,必须移除这一行:

#include <string.h>

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

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

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

对于 PRAISE,sizeof运算符把字符串末尾不可见的空字符也计算在内。该程序并未明确告诉计算机要给字符串预留多少空间,所以它必须计算双引号内的字符数。

注意 sizeof 的圆括号:
对于类型,应写成sizeof(char)或sizeof(float);对于特定量,可写成sizeof name或sizeof 6.28。
尽管如此,还是建议所有情况下都使用圆括号,如sizeof(6.28)。

4.3 常量和C预处理器

如何创建符号常量?方法之一是声明一个变量,然后将该变量设置为所需的常量。可以这样写:

float taxrate;
taxrate = 0.015;

这样做提供了一个符号名,但是taxrate是一个变量,程序可能会无意间改变它的值。C语言还提供了一个更好的方案——C预处理器。之前介绍了预处理器如何使用#include包含其他文件的信息。预处理器也可用来定义常量。只需在程序顶部添加下面一行:

#define TAXRATE 0.015

编译程序时,程序中所有的TAXRATE都会被替换成0.015。这一过程被称为编译时替换(compile-time substitution)。在运行程序时,程序中所有的替换均已完成。

注意,末尾不用加分号,因为这是一种由预处理器处理的替换机制。为什么TAXRATE 要用大写?用大写表示符号常量是 C 语言一贯的传统。初学者应该养成大写常量的好习惯。

符号常量的命名规则与变量相同。可以使用大小写字母、数字和下划线字符,首字符不能为数字。

#define指令还可定义字符和字符串常量。前者使用单引号,后者使用双
引号。如下所示:
#define BEEP '\a'
#define TEE 'T'
#define ESC '\033'
#define OOPS "Now you have done it!"

4.3.1 const限定符

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

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

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

4.3.2 明示常量

C头文件limits.h和float.h分别提供了与整数类型和浮点类型大小限制相关的详细信息。每个头文件都定义了一系列供实现使用的明示常量 [3]。例如,limits.h头文件包含以下类似的代码:

#define INT_MAX +32767
#define INT_MIN -32768

这些明示常量代表int类型可表示的最大值和最小值。
如果在程序中包含limits.h头文件,就可编写下面的代码:

printf("Maximum int value on this system = %d\n", INT_MAX);

如果系统使用4字节的int,limits.h头文件会提供符合4字节int的INT_MAX和INT_MIN。

类似地,float.h头文件中也定义一些明示常量,如FLT_DIG和DBL_DIG,分别表示float类型和double类型的有效数字位数。

程序清单4.5演示了如何使用float.h和limits.h中的数据(注意,编译器要完全支持C99标准才能识别LLONG_MIN标识符)。

程序清单4.5 defines.c程序
// defines.c -- 使用limit.h和float头文件中定义的明示常量
#include <stdio.h>
#include <limits.h>  // 整型限制
#include <float.h>  // 浮点型限制
int main(void)
{
printf("Some number limits for this system:\n");
printf("Biggest int: %d\n", INT_MAX);
printf("Smallest long long: %lld\n", LLONG_MIN);
printf("One byte = %d bits on this system.\n", CHAR_BIT);
printf("Largest double: %e\n", DBL_MAX);
printf("Smallest normal float: %e\n", FLT_MIN);
printf("float precision = %d digits\n", FLT_DIG);
printf("float epsilon = %e\n", FLT_EPSILON);
return 0;
}
该程序的输出示例如下:
Some number limits for this system:
Biggest int: 2147483647
Smallest long long: -9223372036854775808
One byte = 8 bits on this system.
Largest double: 1.797693e+308
Smallest normal float: 1.175494e-38
float precision = 6 digits
float epsilon = 1.192093e-07

4.4 printf()和scanf()

printf()函数和scanf()函数能让用户可以与程序交流,它们是输入/输出函数,或简称为I/O函数。

4.4.1 printf()函数

请求printf()函数打印数据的指令要与待打印数据的类型相匹配。例如,打印整数时使用%d,打印字符时使用%c。这些符号被称为转换说明。

由于 printf()函数使用%符号来标识转换说明, 因此打印%符号就成了个问题。 如果单独使用一个%符号, 编译器会认为漏掉了一个转换字符。 解决方法很简单, 使用两个%符号就行了:

pc = 2*6;
printf("Only %d%% of Sally's gribbles were edible.\n", pc);
下面是输出结果:
Only 12% of Sally's gribbles were edible.

4.4.3 printf()的转换说明修饰符

表4.4 printf()的修饰符

注意 类型可移植性

sizeof 运算符以字节为单位返回类型或值的大小。 这应该是某种形式的整数, 但是标准只规定了该值是无符号整数。 在不同的实现中, 它可以是unsigned int、 unsigned long甚至是unsigned long long。

因此, 如果要用printf()函数显示sizeof表达式, 根据不同系统, 可能使用%u、 %lu或%llu。 这意味着要查找你当前系统的用法, 如果把程序移植到不同的系统还要进行修改。

鉴于此, C提供了可移植性更好的类型。 首先, stddef.h头文件(在包含stdio.h头文件时已包含其中) 把size_t
定义成系统使用sizeof返回的类型, 这被称为底层类型(underlying type) 。
其次, printf()使用z修饰符表示打印相应的类型。 同样, C还定义了ptrdiff_t类型和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类型值专用的转换说明。

1.使用修饰符和标记的示例
接下来, 用程序示例演示如何使用这些修饰符和标记。 先来看看字段宽度在打印整数时的效果。 考虑程序清单4.7中的程序。
程序清单4.7 width.c程序

/* width.c -- 字段宽度 */
#include <stdio.h>
#define PAGES 959
int main(void)
{
printf("*%d*\n", PAGES);
printf("*%2d*\n", PAGES);
printf("*%10d*\n", PAGES);
printf("*%-10d*\n", PAGES);
return 0;
}
输出结果如下所示:
*959*
*959* // 因为待打印的整数有 3 位数字, 所以字段宽度自动扩大以符合整数的长度。
* 959* //在两个星号之间有7个空格和3位数字, 并且数字位于字段的右侧。
*959 *  //10 个空格宽度, -标记: 位于字段左侧

接下来看看浮点型格式。

// floats.c -- 一些浮点型修饰符的组合
#include <stdio.h>
int main(void)
{
const double RENT = 3852.99; // const变量
printf("*%f*\n", RENT);      //*3852.990000*
printf("*%e*\n", RENT);      //*3.852990e+03*
printf("*%4.2f*\n", RENT);    //*3852.99*
printf("*%3.1f*\n", RENT);     //*3853.0*
printf("*%10.3f*\n", RENT);     //*  3852.990*
printf("*%10.3E*\n", RENT);     //*3.853E+03*
printf("*%+4.2f*\n", RENT);     //*+3852.99*

本例的第1个转换说明是%f。 在这种情况下, 字段宽度和小数点后面的位数均为系统默认设置, 即字段宽度是容纳带打印数字所需的位数和小数点后打印6位数字。

第2个转换说明是%e。 默认情况下, 编译器在小数点的左侧打印1个数字, 在小数点的右侧打印6个数字。 这样打印的数字太多! 解决方案是指定小数点右侧显示的位数, 程序中接下来的 4 个例子就是这样做的。 请注意,第4个和第6个例子对输出结果进行了四舍五入。 另外, 第6个例子用E代替了e。

第7个转换说明中包含了+标记, 这使得打印的值前面多了一个代数符号(+) 。 0标记使得打印的值前面以0填充以满足字段要求。 注意, 转换说明%010.2f的第1个0是标记, 句点(.) 之前、 标记之后的数字(本例为10)是指定的字段宽度。

程序清单4.9演示了其他组合。

程序清单4.9 flags.c程序
/* flags.c -- 演示一些格式标记 */
#include <stdio.h>
int main(void)
{
printf("%x %X %#x\n", 31, 31, 31);            //1f 1F 0x1f
printf("**%d**% d**% d**\n", 42, 42, -42);    //**42** 42**-42**
printf("**%5d**%5.3d**%05d**%05.3d**\n", 6, 6, 6, 6);
//** 6** 006**00006** 006**
return 0;
}

第1行输出中, 1f是十六进制数, 等于十进制数31。 第1行printf()语句中, 根据%x打印出1f, %F打印出1F, %#x打印出0x1f。

第 2 行输出演示了如何在转换说明中用空格在输出的正值前面生成前导空格, 负值前面不产生前导空格。 这样的输出结果比较美观, 因为打印出来的正值和负值在相同字段宽度下的有效数字位数相同。

第3行输出演示了如何在整型格式中使用精度(%5.3d) 生成足够的前导0以满足最小位数的要求(本例是3) 。 然而, 使用0标记会使得编译器用前导0填充满整个字段宽度。 最后, 如果0标记和精度一起出现, 0标记会被忽略。

下面来看看字符串格式的示例。

/* stringf.c -- 字符串格式 */
#include <stdio.h>
#define BLURB "Authentic imitation!"
int main(void)
{ p
rintf("[%2s]\n", BLURB);
printf("[%24s]\n", BLURB);
printf("[%24.5s]\n", BLURB);
printf("[%-24.5s]\n", BLURB);
return 0;
}
该程序的输出如下:
[Authentic imitation!]
[     Authentic imitation!]
[                          Authe]
[Authe                          ]

注意, 虽然第1个转换说明是%2s, 但是字段被扩大为可容纳字符串中的所有字符。 还需注意, 精度限制了待打印字符的个数。 .5告诉printf()只打印5个字符。 另外, -标记使得文本左对齐输出。

4.4.4 转换说明的意义

转换说明把以二进制格式储存在计算机中的值转换成一系列字符(字符串) 以便于显示。
如, 数字76在计算机内部的存储格式是二进制数01001100。 %d转换说明将其转换成字符7和
6, 并显示为76; %x转换说明把相同的值(01001100) 转换成十六进制记数法4c; %c转换说明把01001100转换成字符L。

1.转换不匹配
前面强调过, 转换说明应该与待打印值的类型相匹配。 通常都有多种选择。 例如, 如果要打印一个int类型的值, 可以使用%d、 %x或%o。 这些转换说明都可用于打印int类型的值, 其区别在于它们分别表示一个值的形式不同。 类似地, 打印double类型的值时, 可使用%f、 %e或%g。

混淆整型和浮点型, 结果更奇怪。

程序清单4.12 floatcnv.c程序
/* floatcnv.c -- 不匹配的浮点型转换 */
#include <stdio.h>
int main(void)
{
float n1 = 3.0;
double n2 = 3.0;
long n3 = 2000000000;
long n4 = 1234567890;
printf("%.1e %.1e %.1e %.1e\n", n1, n2, n3, n4);
//    3.0e+00 3.0e+00 3.1e+46 1.7e+266
printf("%ld %ld\n", n3, n4);
//    2000000000 1234567890
printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);
//   0 1074266112 0 1074266112
return 0;
}

第1行输出显示, %e转换说明没有把整数转换成浮点数。
考虑一下, 如果使用%e转换说明打印n3(long类型) 会发生什么情况。 首先, %e转换说明让printf()函数认为待打印的值是double类型(本系统中double为8字节) 。当printf()查看n3(本系统中是4字节的值) 时, 除了查看n3的4字节外, 还会查看查看n3相邻的4字节, 共8字节单元。 接着, 它将8字节单元中的位组合解释成浮点数(如, 把一部分位组合解释成指数) 。 因此, 即使n3的位数正确, 根据%e转换说明和%ld转换说明解释出来的值也不同。 最终得到的结果是无意义的值。

第1行也说明了前面提到的内容: float类型的值作为printf()参数时会被转换成double类型。 在本系统中, float是4字节, 但是为了printf()能正确地显示该值, n1被扩成8字节。

第2行输出显示, 只要使用正确的转换说明, printf()就可以打印n3和n4。

第3行输出显示, 如果printf()语句有其他不匹配的地方, 即使用对了转换说明也会生成虚假的结果。 用%ld转换说明打印浮点数会失败, 但是在这里, 用%ld打印long类型的数竟然也失败了! 问题出在C如何把信息传递给函数。 具体情况因编译器实现而异。

参数传递

参数传递机制因实现而异。 下面以我们的系统为例, 分析参数传递的原理。 函数调用如下:

printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);

该调用告诉计算机把变量n1、 n2、 、 n3和n4的值传递给程序。 这是一种常见的参数传递方式。 程序把传入的值放入被称为栈(stack) 的内存区域。计算机根据变量类型(不是根据转换说明) 把这些值放入栈中。
因此, n1被储存在栈中, 占8字节(float类型被转换成double类型) 。 同样, n2也在栈中占8字节, 而n3和n4在栈中分别占4字节。
然后, 控制转到printf()函数。 该函数根据转换说明(不是根据变量类型) 从栈中读取值。 %ld转换说明表明printf()应该读取4字节, 所以printf()读取栈中的前4字节作为第1个值。
这是n1的前半部分, 将被解释成一个long类型的整数。 根据下一个%ld转换说明, printf()再读取4字节, 这是n1的后半部分, 将被解释成第2个long类型的整数(见图4.9) 。
类似地, 根据第3个和第4个%ld, printf()读取n2的前半部分和后半部分, 并解释成两个long类型的整数。 因此, 对于n3和n4, 虽然用对了转换说明, 但printf()还是读错了字节。

2.printf()的返回值

printf()函数返回打印字符的个数。 如果有输出错误,printf()则返回一个负值(printf()的旧版本会返回不同的值) 。
printf()的返回值是其打印输出功能的附带用途, 通常很少用到, 但在检查输出错误时可能会用到(如, 在写入文件时很常用) 。 如果一张已满的CD或DVD拒绝写入时, 程序应该采取相应的行动, 例如终端蜂鸣30秒。

/* prntval.c -- printf()的返回值 */
#include <stdio.h>
int main(void)
{ int
bph2o = 212;
int rv;
rv = printf("%d F is water's boiling point.\n", bph2o);
printf("The printf() function printed %d characters.\n",
rv);
return 0;
}
该程序的输出如下:
212 F is water's boiling point.
The printf() function printed 32 characters.

注意计算针对所有字符数, 包括空格和不可见的换行符(\n) 。

3.打印较长的字符串

给字符串断行有3种方法,

/* longstrg.c ––打印较长的字符串 */
#include <stdio.h>
int main(void)
{
printf("Here's one way to print a ");
printf("long string.\n");
printf("Here's another way to print a \
long string.\n");
printf("Here's the newest way to print a "
"long string.\n"); /* ANSI C */
return 0;
}

方法3: ANSI C引入的字符串连接。 在两个用双引号括起来的字符串之间用空白隔开, C编译器会把多个字符串看作是一个字符串。

4.4.5 使用scanf()

C库包含了多个输入函数, scanf()是最通用的一个, 因为它可以读取不同格式的数据。

当然, 从键盘输入的都是文本, 因为键盘只能生成文本字符: 字母、 数字和标点符号。 如果要输入整数 2014, 就要键入字符 2、 0、 1、 4。 如果要将其储存为数值而不是字符串, 程序就必须把字符依次转换成数值, 这就是scanf()要做的。

scanf()把输入的字符串转换成整数、 浮点数、 字符或字符串, 而printf()正好与它相反, 把整数、 浮点数、 字符和字符串转换成显示在屏幕上的文本

scanf()函数使用指向变量的指针。
如果用scanf()读取基本变量类型的值, 在变量名前加上一个&;
如果用scanf()把字符串读入字符数组中, 不要使用&。

程序清单4.15 input.c程序
// input.c -- 何时使用&
#include <stdio.h>
int main(void)
{
int age; // 变量
float assets; // 变量
char pet[30]; // 字符数组, 用于储存字符串
printf("Enter your age, assets, and favorite pet.\n");
scanf("%d %f", &age, &assets); // 这里要使用&
scanf("%s", pet); // 字符数组不使用&
printf("%d $%.2f %s\n", age, assets, pet);
return 0;
}
Enter your age, assets, and favorite pet.
42
2121.45
guppy
42 $2121.45 guppy

唯一例外的是%c转换说明。 根据%c, scanf()会读取每个字符, 包括空白。

scanf()函数所用的转换说明与printf()函数几乎相同。 主要的区别是, 对于float类型和double类型, printf()都使用%f、 %e、 %E、 %g和%G转换说明。 而scanf()只把它们用于float类型, 对于double类型时要使用l修饰符。

表4.6 ANSI C中scanf()的转换说明

可以在表4.6所列的转换说明中(百分号和转换字符之间) 使用修饰符。 如果要使用多个修饰符, 必须按表4.7所列的顺序书写。


1.从scanf()角度看输入

接下来, 我们更详细地研究scanf()怎样读取输入。

假设scanf()根据一个%d转换说明读取一个整数。 scanf()函数每次读取一个字符, 跳过所有的空白字符, 直至遇到第1个非空白字符才开始读取。

如果找到一个数字或符号, 它便保存该字符, 并读取下一个字符。scanf()不断地读取和保存字符, 直至遇到非数字字符。然后,scanf()把非数字字符放回输入。

这意味着程序在下一次读取输入时, 首先读到的是上一次读取丢弃的非数字字符。 最后, scanf()计算已读取数字(可能还有符号) 相应的数值, 并将计算后的值放入指定的变量中。

如果使用字段宽度, scanf()会在字段结尾或第1个空白字符处停止读取(满足两个条件之一便停止)。

如果第1个非空白字符是A而不是数字, 会发生什么情况? scanf()将停在那里, 并把A放回输入中, 不会把值赋给指定变量。 程序在下一次读取输入时, 首先读到的字符是A。 如果程序只使用%d转换说明, scanf()就一直无法越过A读下一个字符。 另外, 如果使用带多个转换说明的scanf(), C规定
在第1个出错处停止读取输入。

用其他数值匹配的转换说明读取输入和用%d 的情况相同。
区别在于scanf()会把更多字符识别成数字的一部分。
例如, %x转换说明要求scanf()识别十六进制数a~f和A~F。 浮点转换说明要求scanf()识别小数点、 e记数法(指数记数法) 和新增的p记数法(十六进制指数记数法) 。

如果使用%s 转换说明, scanf()会读取除空白以外的所有字符。
scanf()跳过空白开始读取第 1 个非空白字符, 并保存非空白字符直到再次遇到空白。

最后要注意一点:
当scanf()把字符串放进指定数组中时, 它会在字符序列的末尾加上’\0’, 让数组中的内容成为一个C字符串。

实际上, 在C语言中scanf()并不是最常用的输入函数。 这里重点介绍它是因为它能读取不同类型的数据。 C 语言还有其他的输入函数, 如 getchar()和 fgets()。 这两个函数更适合处理一些特殊情况, 如读取单个字符或包含空格的字符串。

2.格式字符串中的普通字符

scanf()函数允许把普通字符放在格式字符串中。 除空格字符外的普通字符必须与输入字符串严格匹配。 例如, 假设在两个转换说明中添加一个逗号:

scanf("%d,%d", &n, &m);

scanf()函数将其解释成: 用户将输入一个数字、 一个逗号, 然后再输入一个数字。 也就是说, 用户必须像下面这样进行输入两个整数:

88,121

由于格式字符串中, %d后面紧跟逗号, 所以必须在输入88后再输入一个逗号。 但是, 由于scanf()会跳过整数前面的空白, 所以下面两种输入方式都可以:

88, 121和 88
,
121

格式字符串中的空白意味着跳过下一个输入项前面的所有空白。 例如,对于下面的语句:

scanf("%d ,%d", &n, &m);

以下的输入格式都没问题:

88,121
88 ,121
88 , 121

请注意, “所有空白”的概念包括没有空格的特殊情况。
除了%c, 其他转换说明都会自动跳过待输入值前面所有的空白。 因此, scanf(“%d%d”, &n, &m)与scanf(“%d %d”, &n, &m)的行为相同。
对于%c, 在格式字符串中添加一个空格字符会有所不同。 例如, 如果把%c放在格式字符串中的空格前面, scanf()便会跳过空格, 从第1个非空白字符开始读取。 也就是说, scanf(“%c”, &ch)从输入中的第1个字符开始读取, 而scanf(” %c”, &ch)则从第1个非空白字符开始读取。

3.scanf()的返回值
scanf()函数返回成功读取的项数。 如果没有读取任何项, 且需要读取一个数字而用户却输入一个非数值字符串, scanf()便返回0。 当scanf()检测到“文件结尾”时, 会返回EOF(EOF是stdio.h中定义的特殊值, 通常用#define指令把EOF定义为-1) 。

4.4.6 printf()和scanf()的*修饰符

printf()和scanf()都可以使用 * 修饰符来修改转换说明的含义。 但是, 它们的用法不太一样。 首先, 我们来看printf()的*修饰符。
如果你不想预先指定字段宽度, 希望通过程序来指定, 那么可以用 * 修饰符代替字段宽度。 但还是要用一个参数告诉函数, 字段宽度应该是多少。也就是说, 如果转换说明是 %*d, 那么参数列表中应包含*和 d对应的值。 这个技巧也可用于浮点值指定精度和字段宽度。

程序清单4.16 varwid.c程序
/* varwid.c -- 使用变宽输出字段 */
#include <stdio.h>
int main(void)
{
unsigned width, precision;
int number = 256;
double weight = 242.5;
printf("Enter a field width:\n");
scanf("%d", &width);
printf("The number is :%*d:\n", width, number);
printf("Now enter a width and a precision:\n");
scanf("%d %d", &width, &precision);
printf("Weight = %*.*f\n", width, precision, weight);
printf("Done!\n");
return 0;
}

变量width提供字段宽度, number是待打印的数字。 因为转换说明中*在d的前面, 所以在printf()的参数列表中, width在number的前面。 同样, width和precision提供打印weight的格式化信息。 下面是一个运行示例:

Enter a field width:
6
The number is : 256:
Now enter a width and a precision:
8 3
Weight = 242.500
Done!

这里, 用户首先输入6, 因此6是程序使用的字段宽度。 类似地, 接下来用户输入8和3, 说明字段宽度是8, 小数点后面显示3位数字。 一般而言, 程序应根据weight的值来决定这些变量的值。

scanf()中*的用法与此不同。 把*放在%和转换字符之间时, 会使得scanf()跳过相应的输出项。
程序清单4.17 skip2.c程序

/* skiptwo.c -- 跳过输入中的前两个整数 */
#include <stdio.h>
int main(void)
{
int n;
printf("Please enter three integers:\n");
scanf("%*d %*d %d", &n);
printf("The last integer was %d\n", n);
return 0;
}

程序清单4.17中的scanf()指示: 跳过两个整数, 把第3个整数拷贝给n。下面是一个运行示例:

Please enter three integers:
2013 2014 2015
The last integer was 2015

4.4.7 printf()的用法提示

使用足够大的固定字段宽度可以让输出整齐美观。 例如, 若使用下面的语句:

printf("%9d %9d %9d\n", val1, val2, val3);上面的输出将变成:12     234    1222而不是这样: 12 234 1222

另一方面, 如果要在文字中嵌入一个数字, 通常指定一个小于或等于该数字宽度的字段会比较方便。 这样, 输出数字的宽度正合适, 没有不必要的空白。 例如, 下面的语句:

printf("Count Beppo ran %.2f miles in 3 hours.\n", distance);
其输出如下:
Count Beppo ran 10.22 miles in 3 hours.

假设有如下输入行:

 -13.45e12# 0

如果其对应的转换说明是%d, scanf()会读取3个字符(-13) 并停在小数点处, 小数点将被留在输入中作为下一次输入的首字符。

如果其对应的转换说明是%f, scanf()会读取-13.45e12, 并停在#符号处, 而#将被留在输入中作为下一次输入的首字符; 然后, scanf()把读取的字符序列-13.45e12转换成相应的浮点值, 并储存在float类型的目标变量中。

如果其对应的转换说明是%s, scanf()会读取-13.45e12#, 并停在空格处, 空格将被留在输入中作为下一次输入的首字符; 然后, scanf()把这 10个字符的字符码储存在目标字符数组中, 并在末尾加上一个空字符。

如果其对应的转换说明是%c, scanf()只会读取并储存第1个字符, 该例中是一个空格 。

C语言学习(四)字符串和格式化输入/输出相关推荐

  1. C语言字符串0x,0X04 字符串跟格式化输入/输出

    0X04 字符串和格式化输入/输出 字符串(character string)就是一个或多个字符的序列.如:"hello world",双引号不是字符串的一部分,而是通知编译器,这 ...

  2. 第4章 字符串和格式化输入/输出

    学习笔记--<C Primer Plus> 第4章 字符串和格式化输入/输出 4.1 前导程序 4.2 字符串简介 4.2.1 char 类型数组和 null 字符 4.2.2 使用字符串 ...

  3. c语言中使用的字符常量 其起止标记,C Primer Plus学习笔记(三)- 字符串和格式化输入/输出...

    从一个简单的例子开始 #include int main() { char name[10]; printf("Input Your Name:\n"); scanf(" ...

  4. C语言基础之4:字符串和格式化输入/输出

    Tips1 函数:strlen() 关键字:const 字符串 如何创建.存储字符串 如何使用strlen()函数获取字符串的长度 用C预处理器指令#define和ANSIC的const修饰符创建符号 ...

  5. 第五 字符串和格式化输入/输出

    C语言学习栏目目录 目录 1.前导程序 2.字符串简介 3 常量和C预处理器 这篇介绍下列内容: 函数:strlen() 关键字:const 字符串 如何创建.存储字符串 如何使用strlen()函数 ...

  6. c语言格式化输入字符型,C语言——字符串和格式化输入/输出

    今天,学习了C Primer Plus的第四章.本章讲解了一些简单的C预处理器的知识,一些字符.字符串以及数组等相关概念以及输入输出函数的使用. 现将知识点总结如下: 字符串:一个或多个字符的序列(双 ...

  7. C 语言——字符串和格式化输入/输出

    一.字符串  1.char数组类型和空字符 c没有为字符串定义专门的变量类型,而是把它存储在char数组中. 将数组看成是一行中的多个存储单元,字符串中的每个字符占用一个字符,最后一个位置显示空字符\ ...

  8. c++ 数组的输入遇到特定字符停止输入_C语言 第4章-字符串和格式化输入/输出

    #include 用数组name储存字符串,name数组有40个字节,每个字节储存一个字符值. 在scanf()函数中,输入字符串name没有&前缀. C预处理器把字符常量DENSITY定义为 ...

  9. C语言sscanf()函数(从字符串读取格式化输入,提取需要的信息)

    需包含头文件:C 标准库 - <stdio.h> 文章目录 描述 声明 参数 返回值 实例 我在VS上测试(VS上要用sscanf_s) 示例1 示例2(提取时能默认以空格分割) 描述 C ...

最新文章

  1. 分布式 id 生成器
  2. 【玩转.Net MF – 03】远程文件查看器
  3. 【计算理论】计算复杂性 ( 3-SAT 是 NP 完全问题 | 团问题是 NP 完全问题 | 团问题是 NP 完全问题证明思路 )
  4. 计算1至1000间的合数c语言,输出1000以内的素数的算法(实例代码)
  5. superset可视化-deck.gl Scatterplot与MapBox
  6. hadoop家族的各个成员
  7. cobbler工作流分析
  8. 从零开始系统化学Android,值得收藏!
  9. html 闪烁字,HTML最简单的文字闪烁代码
  10. 跨部门不配合工作_作为管理者,如何解决跨部门成员互不配合的情况?
  11. 董明珠谈和雷军续赌约:他愿意续我当然可以,网友:先把上次的账结了
  12. PyTorch——PyTorch也支持通过累加操作实现大的BatchSize的训练
  13. 在 Linux 下搭建 Java 开发环境
  14. 雅虎Yahoo 前段优化 14条军规
  15. IOS开发之——硬件开发-加速计传感器(03)
  16. 【LaTeX 常用格式设置】 如何给公式、文本添加删除线
  17. SQL读取Excel数据
  18. 通俗讲解 Kademlia 原理
  19. silvaco 仿真BJT
  20. 设计UI的语言——XAML

热门文章

  1. vim:vim从入门到放弃
  2. 使用python代码举例说明离散随机变量
  3. C/C++描述 LeetCode 167. 两数之和 II - 输入有序数组
  4. IDEA build时提示错误信息: java: System Java Compiler was not found in classpath
  5. 视觉检测系统是怎么检测尺寸的?
  6. Linux实战001:VMware下安装Ubuntu18.04(服务器版)
  7. Matlab rand()函数用法
  8. 完整简单c语言程序代码,一些简单的C语言程序代码.docx
  9. 成神结局量子计算机雏惨,成神之日:消失数月之后雏再次出现,不过形象却差点让人认不出...
  10. 【论文阅读】Semi-supervised Multi-instance Learning with Density Peaks Clustering