天天看点

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

C Primer Plus

  • 第四章:字符串和格式化输入/输出
    • 4.1 前导程序
    • 4.2 字符串简介
      • 4.2.1 char类型数组和null字符
      • 4.2.2 使用字符串
      • 4.2.3 strlen()函数
    • 4.3 常量和C预处理器
      • 4.3.1 const限定符
      • 4.3.2 明示常量
    • 4.4 printf()和scanf()
      • 4.4.1 printf()函数
      • 4.4.2 使用printf()函数
      • 4.4.3 printf()的转换说明修饰符
      • 4.4.4 转换说明的意义
      • 4.4.5 使用scanf()
      • 4.4.6 printf()和scanf()的*修饰符(因系统格式要求,下文用‘米’代替该符号)
      • 4.4.7 printf()的用法提示
    • 4.5 关键概念
    • 4.6 本章小结
    • 4.7 编程练习

第四章:字符串和格式化输入/输出

前言:本章节将详细介绍C语言的两个输入/输出函数:scanf()和printf()。同时介绍函数strlen()以及关键字const的使用,最后简要介绍一个重要工具——C预处理器指令,并学习如何定义、使用符号常量。

4.1 前导程序

首先我们先来看一个小程序4.1 talkback.c

#include <stdio.h>
#include <string.h>  //提供strlen()函数的原型
#define DENSITY 62.4  //人体密度(单位:磅/立方英尺)

int main(void)
{
    float weight,volume;
    int size,letters;
    char name[40];
    
    printf("Hi!What's your first name?\n");
    scanf("%s",name);  //name没有&前缀
    printf("%s,What's your weight in pounds?\n",name);
    scanf("%f",&weight);  //weight有&前缀
    size = sizeof(name);  //获取数组name占用的字节内存
    letters = strlen(name);  //获取数组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;
}
           

运行程序,通过printf()和scanf()函数进行人机交互——输入YYC(Enter)和 130(Enter),输出如下:

Hi!What's your first name?
YYC
YYC,What's your weight in pounds?
130
Well,YYC,your volume is 2.08 cubic feet.
Also,your first name has 3 letters,
and we have 40 bytes to store it.
           

在该程序中我们会发现一些新的特性:

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

4.2 字符串简介

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

“Zing went the strings of my herat!”

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

4.2.1 char类型数组和null字符

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

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

注意上图中数组末尾的字符\0,这是空字符,C语言用它标记字符串的结束。

此外,我们还需要知晓空字符并不是数字0,它是一个非打印字符,其ASCII码值是0(可查阅ASCII表辅助理解)。C中的字符串一定是以空字符结束,这意味着数组的容量必须至少比待存储字符串数多1.例如上述程序4.1中字符数组name有40个存储单元,其实际最多只能存储39个字符,剩下一个字节留给空字符!

4.2.2 使用字符串

老规矩,我们先来看个小程序4.2 praise1.c

#include <stdio.h>
#define PRAISE "You are an extraordinary being."

int main(void)
{
	char name[40];
	
	printf("What's your name?\n");
	scanf("%s",name);
	printf("Hello,%s.%s\n",name,PRAISE);
	
	return 0;
}
           

运行程序,输入Angela Plains(Enter),输出结果如下所示:

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

%s告诉printf()打印一个字符串。%s出现了两次,是因为程序要打印两个字符串:一个存储在name数组中:一个由PRAISE来表示。在编写程序时,读者不用亲自把空字符放入字符串末尾,scanf()在读取输入时就已经完成了这项工作。也不用在字符串常量PRAISE末尾添加空字符,编译器会自动识别并且在末尾加上空字符。

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

细心的读者会发现,在实际输出中,我们输入的是Angela Plains,但输出的却只有前面的单词Angela。这是因为scanf()在读取输入时,当其读取到正确格式的字符后,此后再遇到==空白(空格、制表符、换行符)==时它就不再读取输入(未读取正确格式字符前的空白不影响)。

根据%s转换说明,scanf()只会读取字符串中的一个单词,而不是一整句话。

注意:字符串和字符的区别:字符串常量“x”和字符‘x’是不同的,字符‘x’是基本类型(char),而“x”是派生类型(char 数组),此外字符串常量“x”实际上是由两个字符组成----字符’x’和空字符\0。

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

4.2.3 strlen()函数

上一章我们有提到sizeof运算符,它是以字节为单位给出对象的大小。strlen()函数给出字符串中的字符长度。我们来通过下面的这个程序,分析一下两者的区别。

#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?\n");
	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;
}
           

运行程序,输入Serendipity Chance(Enter),输出结果为:

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个存储单元用来存放空字符\0,strlen()并未将其计入。下图演示了这个概念:

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

对于PRAISE,用strlen()得出的也是字符串常量中的字符数(包括空格和标点符号)。而sizeof()则是将字符串末尾不可见的空字符也计算在内。

4.3 常量和C预处理器

有时,在程序中要使用常量。例如,可以这样计算圆的周长:

circumference = 3.14159 * diameter;

这里,常量3.14159代表pi(π)。在该例中,输入实际值便可使用这个常量。然而,这种情况使用符号常量(symbolic constant)会更好。之所以这样说,是因为使用符号常量不仅可以使其含义表达的更加清楚,同时当某个程序中多处使用同一个常量,有时我们需要改变它的值,如果此时程序使用了符号常量,那我们只需要改变符号常量的定义便可一次修改完成,而不需要进入到代码中逐一查找并修改。

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

float taxrate;

taxrate = 0.015;

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

#define TAXRATE 0.015

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

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

#define NAME value

实际应用时,用选定的符号常量名和合适的值来替换NAME和value。注意,末尾不用加分号,因为这是一种由预处理器处理的替换机制。为什么TAXRATE 要用大写?用大写表示符号常量是 C 语言一贯的传统,大写常量只是为了提高程序的可读性,即使全用小写来表示符号常量,程序也能照常运行。

程序4.4 pizza.c

#include <stdio.h>
#define PI 3.14159

int main(void)
{
	float area,circum,radius;
	
	printf("What is the radius of your pizza?\n");
	scanf("%f",&radius);
	area = PI * radius * radius;
	circum = 2.0*PI*radius;
	printf("Your basic pizza parameters are as follows:\n");
	printf("circumference = %1.2f,area = %1.2f\n",circum,area);
	
	return 0;
} 
           

输入6.0(Enter),printf()语句中的%1.2f表明,结果被四舍五入为两位小数输出。输出如下:

What is the radius of your pizza?
6.0
Your basic pizza parameters are as follows:
circumference = 37.70,area = 113.10
           

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的值。

4.3.2 明示常量

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

#define INT_MAX +32767

#define INT_MIN -32768

这些明示常量代表int类型可表示的最大值和最小值。如果系统使用32位的int,该头文件会为这些明示常量提供不同的值。如果在程序中包含limits.h头文件,就可编写下面的代码:printf(“Maximum int value on this system = %d\n”, INT_MAX);

下面我们通过程序4.5 defines.c进一步了解

#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-038
float precision = 6 digits
float epsilon = 1.192093e-007
           

4.4 printf()和scanf()

printf()函数和scanf()函数能让用户可以与程序交流,它们是输入/输出函数,或简称为I/O函数。虽然printf()是输出函数,scanf()是输入函数,但是它们的工作原理几乎相同。两个函数都使用格式字符串和参数列表。我们先介绍printf(),再介绍scanf()。

4.4.1 printf()函数

请求printf()函数打印数据的指令要与待打印数据的类型相匹配。例如,打印整数时使用%d,打印字符时使用%c。这些符号被称为转换说明(conversion specification),它们指定了如何把数据转换成可显示的形式。

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

4.4.2 使用printf()函数

程序4.6中使用了一些转换说明:

#include <stdio.h>
#define PI 3.141593
int main(void)
{
	int number = 7;
	float pies = 12.75;
 	int cost = 7800;
 	
	printf("The %d contestants ate %f berry pies.\n", number,pies);
	printf("The value of pi is %f.\n", PI);
	printf("Farewell! thou art too dear for my possessing,\n");
	printf("%c%d\n", '$', 2 * cost);
	
	return 0;
}

           

输出结果为:

The 7 contestants ate 12.750000 berry pies.
The value of pi is 3.141593.
Farewell! thou art too dear for my possessing,
$15600
           

这是printf()函数的格式:

printf( 格式字符串, 待打印项1, 待打印项2,…);

待打印项1、待打印项2等都是要打印的项。它们可以是变量、常量,甚至是在打印之前先要计算的表达式。

例如,考虑下面的语句:

printf(“The %d contestants ate %f berry pies.\n”, number,pies);

格式字符串是双引号括起来的内容。上面语句的格式字符串包含了两个待打印项number和poes对应的两个转换说明。格式字符串实际包含了两种形式不同的信息:实际要打印的字符和转换说明。

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

警告:格式字符串中的转换说明一定要与后面的每个项相匹配,若忘记这个基本要求会导致严重的后果。由于 printf()函数使用%符号来标识转换说明,所以当我们需要打印%时,只需要使用两个%符号就行了。

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

在%和转换字符之间插入修饰符可修饰基本的转换说明。表4.4和表4.5列出可作为修饰符的合法字符。如果要插入多个字符,其书写顺序应该与表4.4中列出的顺序相同。

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出
《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

4.7程序演示了字段宽度在打印整数时的效果

#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*
*       959*
*959       *
           

第2个转换说明是%2d,其对应的输出结果应该是 2 字段宽度。因为待打印的整数有 3 位数字,所以字段宽度自动扩大以符合整数的长度。最后一个转换说明是%-10d,其对应的输出结果是 10 个空格宽度,-标记说明打印的数字位于字段的左侧。

4.8程序演示了浮点型格式的效果

#include <stdio.h>

int main(void)
{
	const double RENT = 3852.99;//const ???RENT???? 
	
	printf("*%f*\n",RENT);
	printf("*%e*\n",RENT);
	printf("*%4.2f*\n",RENT);
	printf("*%3.1f*\n",RENT);
	printf("*%10.3f*\n",RENT);
	printf("*%10.3E*\n",RENT);
	printf("*%+4.2f*\n",RENT);
	printf("*%010.2f*\n",RENT);
	
	return 0;
}
           

该程序中使用了const关键字,限定变量为只读。输出结果为:

*3852.990000*
*3.852990e+003*
*3852.99*
*3853.0*
*  3852.990*
*3.853E+003*
*+3852.99*
*0003852.99*
           

本例的第1个转换说明是%f。在这种情况下,字段宽度和小数点后面的位数均为系统默认设置,即字段宽度是容纳带打印数字所需的位数和小数点后打印6位数字。第2个转换说明是%e。默认情况下,编译器在小数点的左侧打印1个数字,在小数点的右侧打印6个数字。这样打印的数字太多!解决方案是指定小数点右侧显示的位数,程序中接下来的 4 个例子就是这样做的。请注意,第4个和第6个例子对输出结果进行了四舍五入。第7个转换说明中包含了+标记,这使得打印的值前面多了一个代数符号(+)。0标记使得打印的值前面以0填充以满足字段****要求。注意,转换说明%010.2f的第1个0是标记,句点(.)之前、标记之后的数字(本例为10)是指定的字段宽度。

程序4.9 flags.c演示了一些格式标记

#include <stdio.h>
int main(void)
{
	printf("%x %X %#x\n", 31, 31, 31);
	printf("**%d**% d**% d**\n", 42, 42, -42);
	printf("**%5d**%5.3d**%05d**%05.3d**\n", 6, 6, 6, 6);

	return 0;
}
           

输出结果为:

1f 1F 0x1f
**42** 42**-42**
**    6**  006**00006**  006**
           

第1行输出中,1f是十六进制数,等于十进制数31。第1行printf()语句中,根据%x打印出1f,%X打印出1F,%#x打印出0x1f。第 2 行输出演示了如何在转换说明中用空格在输出的正值前面生成前导空格,负值前面不产生前导空格。第3行输出演示了如何在整型格式中使用精度(%5.3d)生成足够的前导0以满足最小位数的要求(本例是3)。然而,使用0标记会使得编译器用前导0填充满整个字段宽度。最后,如果0标记和精度一起出现,0标记会被忽略。

程序4.10 stringf.c演示了字符串格式

#include <stdio.h>
#define BLURB "Auyhentic imitation!"

int main(void)
{
	printf("[%2s]\n",BLURB);
	printf("[%24s]\n",BLURB);
	printf("[%24.5s]\n"BLURB);
	printf("[%-24.5s]\n"BLURB);
	
	return 0;
}
           

输出结果为:

[Auyhentic imitation!]
[    Auyhentic imitation!]
[                   Auyhe]
[Auyhe                   ]
           

注意,虽然第1个转换说明是%2s,但是字段被扩大为可容纳字符串中的所有字符。还需注意,精度限制了待打印字符的个数。[%24.5s]中.5告诉printf()只打印5个字符。

4.4.4 转换说明的意义

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

转换(conversion) 可能会误导读者认为原始值被替换成转换后的值。实际上,转换说明是翻译说明,%d的意思是“把给定的值翻译成十进制整数文本并打印出来”。

(1)转换不匹配

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

程序4.11 演示了一些不匹配的整型转换示例

#include <stdio.h>
#define PAGES 336
#define WORDS 65618
int main(void)
{
	short num = PAGES;
	short mnum = -PAGES;
	printf("num as short and unsigned short: %hd %hu\n", num,num);
	printf("-num as short and unsigned short: %hd %hu\n", mnum,mnum);
	printf("num as int and char: %d %c\n", num, num);
	printf("WORDS as int, short, and char: %d %hd %c\n",WORDS,WORDS,WORDS);
	
	return 0;
}
           

输出结果为:

num as short and unsigned short: 336 336
-num as short and unsigned short: -336 65200
num as int and char: 336 P
WORDS as int, short, and char: 65618 82 R
           

第1行,num变量对应的转换说明%hd和%hu输出的结果都是336。这没有任何问题。然而,第2行mnum变量对应的转换说明%u(无符号)输出的结果却为65200,并非期望的336。这是由于有符号short int类型的值在我们的参考系统中的表示方式所致——首先,short int的大小是2字节;其次,系统使用二进制补码来表示有符号整数。这种方法,数字0~32767(215 )代表它们本身,而数字32768~65535(216 )则表示负数。其中,65535表示-1,65534表示-2,以此类推。因此,-336表示为65200(即, 65536-336)。第3行演示了如果把一个大于255的值转换成字符会发生什么情况。在我们的系统中,short int是2字节,char是1字节。当printf()使用%c打印336时,它只会查看储存336的2字节中的后1字节。这种截断(见图4.8)相当于用一个整数除以256,只保留其余数。在这种情况下,余数是80,对应的ASCII值是字符P。用专业术语来说,该数字被解释成“以256为模”(modulo 256),即该数字除以256后取其余数。

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

最后,我们在该系统中打印比short int类型最大整数(32767)更大的整数(65618)。这次,计算机也进行了求模运算。在本系统中,应把数字65618储存为4字节的int类型值。用%hd转换说明打印时, printf()只使用最后2个字节。

程序4.12 演示不匹配的浮点型转换效果

#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);
	printf("%ld %ld\n", n3, n4);
	printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);
	
	return 0;
}
           

输出结果为:

3.0e+00 3.0e+00 3.1e+46 1.7e+266
2000000000 1234567890
0 1074266112 0 1074266112
           
  • 第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()还是读错了字节。

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

(2)printf()的返回值

第2章提到过,大部分C函数都有一个返回值,这是函数计算并返回给主调程序(calling program)的值。例如,C库包含一个sqrt()函数,接受一个数作为参数,并返回该数的平方根。可以把返回值赋给变量,也可以用于计算,还可以作为参数传递。总之,可以把返回值像其他值一样使用。printf()函数也有一个返回值,它返回打印字符的个数。如果有输出错误,printf()则返回一个负值(printf()的旧版本会返回不同的值)。

程序4.13 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.
           

首先,程序用rv = printf(…);的形式把printf()的返回值赋给rv。因此,该语句执行了两项任务:打印信息和给变量赋值。其次,注意计算针对所有字符数,包括空格和不可见的换行符(\n)。

(3)打印较长的字符串

有时,printf()语句太长,在屏幕上不方便阅读。如果空白(空格、制表符、换行符)仅用于分隔不同的部分,C 编译器会忽略它们。在这,我们不能在双引号括起来的字符串中间断行。如果这样写:

printf(“The printf() function printed %d

characters.\n”, rv);

C编译器会报错:字符串常量中有非法字符。在字符串中,可以使用\n来表示换行字符,但是不能通过按下Enter(或Return)键产生实际的换行符。

正确的打印有三种方法:

方法1:使用多个printf()语句。因为第1个字符串没有以\n字符结束,所以第2个字符串紧跟第1个字符串末尾输出。

方法2:用反斜杠(\)和Enter(或Return)键组合来断行。这使得光标移至下一行,而且字符串中不会包含换行符。其效果是在下一行继续输出。但是,下一行代码必须和程序清单中的代码一样从最左边开始。如果缩进该行,比如缩进5个空格,那么这5个空格就会成为字符串的一部分。

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

个字符串看作是一个字符串。

4.4.5 使用scanf()

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

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

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

程序4.15中的小程序演示了这两条规则

#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;
}
           

运行程序,输入38(Enter),92360.88(Enter), llama(Enter),输入结果为:

Enter your age, assets, and favorite pet.
38
92360.88
llama
38 $92360.88 llama
           

scanf()函数使用空白(换行符、制表符和空格)把输入分成多个字段。在依次把转换说明和字段匹配时跳过空白。注意,在输入时只要在每个输入项之间输入至少一个换行符、空格或制表符即可,可以在一行或多行输入,唯一例外的是%c转换说明。根据%c,scanf()会读取每个字符,包括空白。我们稍后详述这部分。

(1)scanf()函数的转换说明

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

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

(2)scanf()函数的转换说明修饰符

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

《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出
《C Primer Plus》 学习笔记第四章:字符串和格式化输入/输出

如你所见,使用转换说明比较复杂,而且这些表中还省略了一些特性。省略的主要特性是,从高度格式化源中读取选定数据,如穿孔卡或其他数据记录。

(3)从scanf()角度看输入

假设scanf()根据一个%d转换说明读取一个整数。scanf()函数每次读取一个字符,跳过所有的空白字符,直至遇到第1个非空白字符才开始读取。因为要读取整数,所以scanf()希望发现一个数字字符或者一个符号(+或-)。如果找到一个数字或符号,它便保存该字符,并读取下一个字符。如果下一个字符是数字,它便保存该数字并读取下一个字符。scanf()不断地读取和保存字符,直至遇到非数字字符。如果遇到一个非数字字符,它便认为读到了整数的末尾。然后,scanf()把非数字字符放回输入。这意味着程序在下一次读取输入时,首先读到的是上一次读取丢弃的非数字字符。最后,scanf()计算已读取数字(可能还有符号)相应的数值,并将计算后的值放入指定的变量中。

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

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

如果使用%s 转换说明,scanf()会读取除空白以外的所有字符。scanf()跳过空白开始读取第 1 个非空白字符,并保存非空白字符直到再次遇到空白。这意味着 scanf()根据%s 转换说明读取一个单词,即不包含空白字符的字符串。如果使用字段宽度,scanf()在字段末尾或第1个空白字符处停止读取。无法利用字段宽度让只有一个%s的scanf()读取多个单词。

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

(4)格式字符串中的普通字符

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

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

88,121 (,字符后面有空白符也不影响)

除了%c,其他转换说明都会自动跳过待输入值前面所有的空白。因此,scanf(“%d%d”, &n, &m)与scanf(“%d %d”, &n, &m)的行为相同。

(5)scanf()的返回值

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

4.4.6 printf()和scanf()的*修饰符(因系统格式要求,下文用‘米’代替该符号)

printf()和scanf()都可以使用米修饰符来修改转换说明的含义。但是,它们的用法不太一样。首先,我们来看printf()的*修饰符。

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

程序4.16 演示了相关用法

#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;
}
           

运行程序,输入6(Enter),8(Space),3(Enter),输出结果为:

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

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

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

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

程序清单4.17就是一个例子

#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;
}
           

运行程序,输入2013(Space),2014(Space),2015(Enter),输出结果为:

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

在程序需要读取文件中特定列的内容时,这项跳过功能很有用。

4.4.7 printf()的用法提示

想把数据打印成列,指定固定字段宽度很有用。因为默认的字段宽度是待打印数字的宽度,如果同一列中打印的数字位数不同,打印出来的数字可能参差不齐。这个时候指定相同的字宽和对齐方式就可以使其看起来更加整齐。例如:printf(“%9d %9d %9d\n”, val1, val2, val3);

4.5 关键概念

  1. C语言用char类型表示单个字符,用字符串表示字符序列。字符常量是一种字符串形式,即用双引号把字符括起来:“Good luck,my friend”。可以把字符串储存在字符数组(由内存中相邻的字节组成)中。字符串,无论是表示成字符常量还是储存在字符数组中,都以一个叫做(空字符\0)的隐藏字符结尾。
  2. 在程序中,最好用#define 定义数值常量,用 const 关键字声明的变量为只读变量。在程序中使用符号常量(明示常量),提高了程序的可读性和可维护性。
  3. C 语言的标准输入函数(scanf())和标准输出函数(printf())都使用一种系统。在该系统中,第1个参数 中的转换说明必须与后续参数中的值相匹配。
  4. 空白字符(制表符、空格和换行符)在 scanf()处理输入时起着至关重要的作用。除了%c 模式(读取下一个字符),scanf()在读取输入时会跳过非空白字符前的所有空白字符,然后一直读取字符,直至遇到空白字符或与正在读取字符不匹配的字符。

4.6 本章小结

  • 字符串是一系列被视为一个处理单元的字符。在C语言中,字符串是以空字符(ASCII码是0)结尾的一系列字符。可以把字符串储存在字符数组中。数组是一系列同类型的项或元素。要确保有足够多的元素来储存整个字符串(包括空字符)。
  • strlen()函数(声明在string.h头文件中)可用于获得字符串的长度(末尾的空字符不计算在内)。scanf()函数中的转换说明是%s时,可读取一个单词。
  • C预处理器为预处理器指令(以#符号开始)查找源代码程序,并在开始编译程序之前处理它们。处理器根据#include指令把另一个文件中的内容添加到该指令所在的位置。#define指令可以创建明示常量(符号常量),即代表常量的符号。limits.h和float.h头文件用#define定义了一组表示整型和浮点型不同属性的符号常量。另外,还可以使用const限定符创建定义后就不能修改的变量。
  • printf()和scanf()函数对输入和输出提供多种支持。两个函数都使用格式字符串,其中包含的转换说明表明待读取或待打印数据项的数量和类型。另外,可以使用转换说明控制输出的外观:字段宽度、小数位和字段内的布局。

4.7 编程练习

1.编写一个程序,提示用户输入名和姓,然后以“名,姓”的格式打印出来。

#include <stdio.h>
int main(void)
{
	char first_name[40];
	char last_name[40];
	
	printf("Please enter your first and last name:\n");
	scanf("%s %s",first_name,last_name);
	printf("Hello,%s,%s\n",first_name,last_name);
	
	return 0;
}
           

运行程序,输入Yongchao Yan(Enter),输出结果为:

Please enter your first and last name:
Yongchao Yan
Hello,Yongchao,Yan
           

2.编写一个程序,提示用户输入名和姓,并执行一下操作:

a.打印名和姓,包括双引号;

b.在宽度为20的字段右端打印名和姓,包括双引号;

c.在宽度为20的字段左端打印名和姓,包括双引号;

d.在比姓名宽度宽3的字段中打印名和姓。

#include <stdio.h>
#include <string.h>
int main(void)
{
	char name[40];
	unsigned int num;
	
	printf("Please enter your  name:\n");
	scanf("%s",name);
	num = strlen(name);
	printf("Your name is:\"%s\"\n",name);
	printf("Your name is:\"%20s\"\n",name);
	printf("Your name is:\"%-20s\"\n",name);
	printf("Your name is:%*s\n",num+3,name);
	
	return 0;
}
           

运行程序,输入YanYongchao(Enter),输出结果为:

Please enter your  name:
YanYongchao
Your name is:"YanYongchao"
Your name is:"         YanYongchao"
Your name is:"YanYongchao         "
Your name is:   YanYongchao
           

3.编写一个程序,读取一个浮点数,首先以小数点记数法打印,然后以指数记数法打印。用下面的格式进行输出(系统不同,指数记数法显示的位数可能不同):

a.The input is 21.3 or 2.1e+001.

b.The input is +21.290 or 2.129E+001.

#include <stdio.h>
int main(void)
{
	float num;
	
	printf("Please enter a floating point number:\n");
	scanf("%f",&num);
	printf("The input is %.1f or %.1e.\n",num,num);
	printf("The input is %+.3f or %.3E.\n",num,num);
	
	return 0;
}
           

运行程序,输入21.2901(Enter),输出结果为:

Please enter a floating point number:
21.2901
The input is 21.3 or 2.1e+001.
The input is +21.290 or 2.129E+001.
           

4.编写一个程序,提示用户输入身高(单位:英寸)和姓名,然后以下面的格式显示用户刚输入的信息:

Dabney, you are 6.208 feet tall

使用float类型,并用/作为除号。如果你愿意,可以要求用户以厘米为单位输入身高,并以米为单位显示出来。

#include <stdio.h>
int main(void)
{
	char name[40];
	float height_m,height_cm;
	
	printf("Please enter your height(cm):");
	scanf("%f",&height_cm);
	height_m = height_cm / 100;
	printf("Please enter your name:");
	scanf("%s",name);
	printf("%s,you are %.3fm tall.\n",name,height_m);
	
	return 0;
}
           

运行程序,输入171(Enter),YanYongchao(Enter),输出结果为:

Please enter your height(cm):171
Please enter your name:YanYongchao
YanYongchao,you are 1.710m tall.
           

5.编写一个程序,提示用户输入以兆位每秒(Mb/s)为单位的下载速度和以兆字节(MB)为单位的文

件大小。程序中应计算文件的下载时间。注意,这里1字节等于8位。使用float类型,并用/作为除号。该

程序要以下面的格式打印 3 个变量的值(下载速度、文件大小和下载时间),显示小数点后面两位数字:

At 18.12 megabits per second, a file of 2.20 megabytes

downloads in 0.97 seconds.

#include<stdio.h>
int main(void)
{
	float speed,file_size,dw_time;
	
	printf("Please Enter the speed of download(Mb/s):");
	scanf("%f",&speed);
	printf("Please Enter the size of file(MB):");
	scanf("%f",&file_size);
	dw_time=file_size*8/speed;
	printf("At %.2f megabits per second,a file of %.2f megabytes\n",speed,file_size);
	printf("downloads in %.2f seconds.\n",dw_time);
	
	return 0;
} 
           

运行程序,输入18.120(Enter),2.201(Enter),输出结果为:

Please Enter the speed of download(Mb/s):18.120
Please Enter the size of file(MB):2.201
At 18.12 megabits per second,a file of 2.20 megabytes
downloads in 0.97 seconds.
           

6.编写一个程序,先提示用户输入名,然后提示用户输入姓。在一行打印用户输入的名和姓,下一行分

别打印名和姓的字母数。字母数要与相应名和姓的结尾对齐,如下所示:

Melissa Honeybee

7    8

接下来,再打印相同的信息,但是字母个数与相应名和姓的开头对齐,如下所示:

Melissa Honeybee

7    8

#include <stdio.h>
#include <string.h>
int main(void)
{
	unsigned int num_fir,num_last;
	char first_name[40],last_name[40];
	
	printf("Please enter your first name:\n");
	scanf("%s",first_name);
	num_fir = strlen(first_name);
	printf("Please enter your last name:\n");
	scanf("%s",last_name);
	num_last = strlen(last_name);
	printf("%s %s\n",first_name,last_name);
	printf("%*d %*d\n",num_fir,num_fir,num_last,num_last);
	printf("%s %s\n",first_name,last_name);	
	printf("%-*d %-*d\n",num_fir,num_fir,num_last,num_last);
	
	return 0;
}
           

运行程序,输入Melissa(Enter),Honeybee(Enter),输出结果为:

Please enter your first name:
Melissa
Please enter your last name:
Honeybee
Melissa Honeybee
      7        8
Melissa Honeybee
7       8
           

7.编写一个程序,将一个double类型的变量设置为1.0/3.0,一个float类型的变量设置为1.0/3.0。分别显示两次计算的结果各3次:一次显示小数点后面6位数字;一次显示小数点后面12位数字;一次显示小数点后面16位数字。程序中要包含float.h头文件,并显示FLT_DIG和DBL_DIG的值。1.0/3.0的值与这些值一致吗?

#include <stdio.h>
#include <float.h>
int main(void)
{
	double dou_out= 1.0 / 3.0;
 	float flo_out = 1.0 / 3.0;
 	
 	printf(" float values: ");
 	printf("%.6f %.12f %.16f\n",flo_out,flo_out,flo_out);
 	printf("double values: ");
 	printf("%.6f %.12f %.16f\n",dou_out,dou_out,dou_out);
 	printf("FLT_DIG: %d\n", FLT_DIG);
 	printf("DBL_DIG: %d\n", DBL_DIG);
 	
 	return 0;
}
           

运行程序,输出结果为:

float values: 0.333333 0.333333343267 0.3333333432674408
double values: 0.333333 0.333333333333 0.3333333333333333
FLT_DIG: 6
DBL_DIG: 15
           

8.编写一个程序,提示用户输入旅行的里程和消耗的汽油量。然后计算并显示消耗每加仑汽油行驶的英

里数,显示小数点后面一位数字。接下来,使用1加仑大约3.785升,1英里大约为1.609千米,把单位是英里/加仑的值转换为升/100公里(欧洲通用的燃料消耗表示法),并显示结果,显示小数点后面 1 位数字。注意,美国采用的方案测量消耗单位燃料的行程(值越大越好),而欧洲则采用单位距离消耗的燃料测量方案(值越低越好)。使用#define 创建符号常量或使用 const 限定符创建变量来表示两个转换系数。

#include <stdio.h>
#define gallon_L 3.785
#define miles_KM 1.609

int main(void)
{
	float miles,gallo,get_value,get_rvalue;
	
	printf("Please enter the mileage of the trip(miles):");
	scanf("%f",&miles);
	printf("Please enter the amount of gas consumed for the trip(gallon):");
	scanf("%f",&gallo);
	get_value = miles / gallo;
	printf("The number of miles you travel per gallon is %.1f.\n",get_value);
	get_rvalue = (gallo*gallon_L*100)/(miles*miles_KM);
	printf("The amount of petrol you need to travel 100 kilometres is %.1f.\n",get_rvalue);
	
	return 0;
}
           

运行程序,输入10(Enter),2(Enter),输出结果为:

Please enter the mileage of the trip(miles):10
Please enter the amount of gas consumed for the trip(gallon):2
The number of miles you travel per gallon is 5.0.
The amount of petrol you need to travel 100 kilometres is 47.0.
           

继续阅读