程序清单4.1与用户进行简单的交互。为了使程序的形式灵活多样,代码中使用了新的注释风格。
程序清单4.1 talkback.c程序
#define _CRT_SECURE_NO_WARNINGS
#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 pound?\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 storeit.\n", size);
system("pause");
return 0;
}
VS运行结果如下:
该程序包含以下新特性。
-
用数组存储字符串。在该程序中,用户输入的名被存储在数组中,该数组占用内存中40个连续的字节,每个字节存储一个字符值。
-
使用%s转换说明来处理字符串的输入和输出。注意,在scanf()中,name没有&前缀,而weight有。
-
用C预处理器把字符常量DENSITY定义位62.4。
-
用C函数strlen()获取字符串的长度。
4.2 字符串简介
字符串是一个或多个字符的序列。如:“Zing went the strings of my heart!”
双引号不是字符串的一部分。双引号仅告知编译器它括起来的是字符串,正如单引号用于标识单个字符。
4.2.1 char类型数组和null字符
C语言没有专门存储字符串的变量类型,字符串都被存储在char类型的数组中。数组有连续的存储单元组成,字符串中的字符被存储在相邻的存储单元中,每个单元存储一个字符。
上图中数组末尾的字符\0。这是空字符,C语言用它标记字符串的结束。
空字符不是数字0,它是非打印字符,其ASCII码值是0。C中的字符串一定以空字符结束,这意味着数组的容量必须至少比待存储字符串中的字符数多1。
程序清单4.1中有40个存储单元的字符串,只能存储39个字符,剩下一个字节留给空字符。
什么是数组?可以把数组看作是一行连续的多个存储单元。更正式的说法是,数组是同类型数据元素的有序序列。
如,char name[40];
name后面的方括号表明这是一个数组,方括号中的40表明该数组中的元素数量。char表明每个元素的类型。
4.2.2 使用字符串
程序清单4.2 praisel.c程序
#define _CRT_SECURE_NO_WARNINGS
#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);
system("pause");
return 0;
}
VS运行结果如下:
%s告诉printf()打印一个字符串。%s出现了2次,因为程序要打印两个字符串:
一个存储在name中
一个有PRAISE来表示。
不用亲自将空字符放入字符串末尾,scanf()在读取输入的时候就已经完成了这项工作。也不用在字符常量PRAISE末尾添加空字符,编译器会在末尾加上空字符。
scanf()只读取了Angela Plains中的Angela,它遇到第一个空白(空格、制表符或换行符)时就不再读取输入,因此,scanf()在读到Angela和Plains之间的空格时就停止了。
根据%s转换说明,scanf()只会读取字符串中的第一个单词,而不是一整句。C语言还有其他输入函数(如,fgets()),用于读取一般字符串。
字符串和字符
字符串常量“x”和字符常量'x'不同
'x'是基本类型(char),“x”是派生类型(char 数组)
“x”实际上由两个字符组成:‘x’和空字符\0
4.2.3 strlen()函数
strlen()函数给出字符串中的字符长度。
程序清单4.3 praise2.c清单
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h> // 提供strlen()函数的原型
#define PRAISE "You are an extraordinary being"
int main(void)
{
char name[40];
printf("What your name? ");
scanf("%s", name);
printf("Hello, %s. %s\n", name, PRAISE);
printf("Your name of %u letters occupies %u memory cells.\n", strlen(name), sizeof(name));
printf("The phrase of praise has %u letters ", strlen(PRAISE));
printf("and occupies %u memory cells.\n", sizeof(PRAISE));
system("pause");
return 0;
}
C把库函数中相关的函数归为一类,并为每类函数提供一个头文件。例如,printf()和scanf()都隶属于输入和输出函数,使用stdio.h头文件。string.h头文件包含了strlen()函数和其他一些字符串相关的函数(如拷贝字符串和字符串查找函数)。
上述例子使用了2中方法处理很长的printf()语句。
将printf()语句分为两行(可以在参数之间断为2行,但是不要在双引号中的字符串中间断开);
使用2个printf()语句打印一行内容,只在第2条printf()语句中使用换行符(\n)。
VS运行结果如下:
sizeof运算符报告,name数组由40个存储单元,但是只有前11个单元用户存储Serendipity,所以strlen()得出的结果是11。name数组的第12个单元存储空字符,strlen()并未将其计入。
对于PRAISE,用strlen()得出的也是字符串的字符数(包括空格和标点符号)。sizeof运算符给出的数更大,因为它把字符串末尾不可见的空字符也计算在内。该程序并未明确告诉计算机要给字符串预留多少空间,所以他必须要计算双引号内的字符数。
sizeof是否使用圆括号:圆括号的使用时机取决于运算对象是类型还是特定量?
运算对象是类型时,圆括号必不可少,应写成sizeof(char)、sizeof(float)
运算对象是特定量时,可有可无,可写成sizeof name、sizeof 6.28。尽管如此,还是建议所有情况下都使用圆括号。
4.3 常量和C预处理器
有时在程序中要使用常量。例如:计算圆的周长:
circumference = 3.14159 * diameter;
这里的常量3.14159代表pi(Π)。在该例中,输入实际值便可使用这个常量,然而,这种情况使用符号常量会更好。使用下面的语句,计算机稍后会用实际值完成替换:
circumference = pi * diameter;
为什么使用符号常量会更好?常量名比数字表达的信息更多,请比较以下两条语句:
owed = 0.015 * housevalue;
owed = taxrate * housevalue;
如果阅读一个很长的程序,第2条语句所表达的含义更清晰。
假设程序中的多处使用同一个常量,有时需要改变他的值。税率通常是浮动的,如果程序使用符号常量,则只需要更改符号常量的定义,不用在程序中查找使用常量的地方,然后逐一替换。
第2章中介绍了预处理器如何使用#include包含其他文件的信息。预处理器也可以定义常量。
#define TAXRATE 0.015
编译程序时,程序中所有的TAXRATE都会被替换成0.015。这一过程被称为编译时替换。在运行程序时,程序中所有的替换均已完成。
请注意格式,首先是#define,接着是符号常量名(TAXRATE),然后是符号常量值(0.015)(注意,其中并没有=符号)。所以,其通用格式如下:
#define NAME value
实际应用时,用选定的符号常量名和合适的值来替换NAME和value。注意,末尾不用加分号。
为什么TAXRATE需要大写?用大写表示符号常量是C语言的传统,这样,在程序中看到全大写的名称就立刻明白这是一个符号常量,而非变量。大写常量只是为了提高程序的可读性,即使全用小写来表示符号常量,程序也能照常运行。
符号常量的命名规则与变量相同,可以使用大小写字母、数字和下划线,首字符不能是数字。
程序清单4.4 pizzc.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_s("%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);
system("pause");
return 0;
}
VS运行结果如下:
printf()语句中的%1.2f表明,结果被四舍五入位2位小数输出。
4.3.1 const限定符
const关键字,用于限定一个变量为只读。
const int MONTHS = 12; // MONTHS 在程序中不可更改,值为12
这使得MONTHS成为一个只读值。可以在计算中使用MONTHS,可以打印MONTHS,但是不能更改MONTHS的值。
4.3.2 明示常量
系统已经定义好的一些字符常量,需要时查阅使用。(limit.h/float.h)
程序清单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);
system("pause");
return 0;
}
VS运行结果如下:
4.4 printf()和scanf()
printf()函数和scanf()能让用户可以与程序交流,他们是输入/输出函数,或简称I/O函数。
printf()是输出函数,scanf()函数是输出函数,但是它们的工作原理几乎相同。两个函数都使用格式字符串和参数列表。
4.4.1 printf()函数
请求printf()函数打印数据的指令要与待打印数据的类型相匹配。例如,打印整数时使用%d,打印字符时使用使用%c。这些符号被称为转换说明,它们指定了如何把数据转换成可显示的形式。
转换说明 | 输出 |
---|---|
%a | 浮点数、十六进制数和p计数法 |
%A | 浮点数、十六进制数和p计数法 |
%c | 单个字符 |
%d | 有符号十进制数 |
%e | 浮点数,e计数法 |
%E | 浮点数,e计数法 |
%f | 浮点数,十进制计数法 |
%g | 根据值的不同,自动选择%f或%e。%e格式用于指数小于-4或者大于等于精度时 |
%G | 根据值的不同,自动选择%f或%e。%e格式用于指数小于-4或者大于等于精度时 |
%i | 有符号十进制数(与%d相同) |
%o | 无符号八进制数 |
%p | 指针 |
%s | 字符串 |
%u | 无符号十进制数 |
%x | 无符号十六进制整数,使用十六进制数 0f |
%X | 无符号十六进制整数,使用十六进制数 0F |
%% | 打印一个百分号 |
4.4.2 使用printf()
程序清单4.6 printout.c程序
/* printout.c -- 使用转换说明 */
#include <stdio.h>
#define PI 3.14159
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 pf pi is %f.\n", PI);
printf("Farewell! thou art too dear for my possessing,\n");
printf("%c%d\n", '$', 2 * cost);
system("pause");
return 0;
}
VS运行结果如下:
printf格式:printf(格式字符串, 待打印项1, 待打印项2,......);
待打印项1和2都是要打印的项。它们可以是变量、常量,甚至是在打印之前先要计算的表达式。
如果只打印短语或与子,就不需要使用任何转换说明。如果只打印数据,也不用加入说明文字。上例程序中最后两个printf()语句都没问题。
printf("Farewell! thou art too dear for my possessing,\n");
printf("%c%d\n", '$', 2 * cost);
注意第2条语句,待打印列表的第一项是一个字符常量,不是变量;第2项是一个乘法表达式,这说明printf()使用的是值,无论是值、常量还是表达式。
由于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()的转换说明修饰符
在%和转换字符之间插入修饰符可修饰基本的转换说明。
修饰符 | 含义 |
---|---|
标记 | 下表描述了5中标记(-、+、空格、#和0),可以不使用标记或使用多个标记。示例:"%-10d" |
数字 | 最小字段宽度 如果该字段不能容纳打印的数字或者字符串,系统会使用更宽的字段 示例:"%4d" |
.数字 | 精度 对于%e、%E、%f转换,表示小数点右边数字的位数 对于%g和%G转换,表示有效数字最大位数 对于%s转换,表示待打印字符的最大数量 对于整型转换,表示待打印数字的最小位数 如有必要,使用前导0来达到这个位数 只使用.表示其后跟一个0,所以%.f和%.0f相同 示例:"5.2f"打印一个浮点数,字段宽度位5字符,其中小数点后有2位数字 |
h | 和整型转换说明一起使用,表示short int或unsigned short int类型的值 示例:"%hu"、"%hx"、"%6.4hd" |
hh | 和整型转换说明一起使用,表示short char或unsigned char类型的值 示例:"%hhu"、"%hhx"、"%6.4hhd" |
j | 和整型转换说明一起使用,表示intmax_t或uintmax_t类型的值。这些定义在stdint.h中 示例:"%jd"、"%8jx" |
l | 和整型转换说明一起使用,表示long int或unsigned long int类型的值 示例:"%ld"、"%8lu" |
ll | 和整型转换说明一起使用,表示long long int或unsigned long long int类型的值 示例:"%lld"、"%8llu" |
L | 和浮点转换说明一起使用,表示long double的值 示例:"%Ld"、"10.4Le" |
t | 和整型转换说明一起使用,表示ptrdiff_t型的值。ptrdiff_t是两个指针差值的类型 示例:"%td"、"%l2tiu" |
z | 和整型转换说明一起使用,表示size_t型的值。size_t是sizeof返回的类型 示例:"%zd"、"%l2zd" |
printf()中的标记
标记 | 含义 |
---|---|
- | 待打印左对齐。即,从字段的左侧开始打印该项 示例:"%-20s" |
+ | 有符号值若为正,则在前面显示加号;若为负,则在前面显示减号 +标记覆盖一个空格 示例:"%+6.2f" |
空格 | 有符号值若为正,则在前面显示前导空格;若为负,则在前面显示减号 +标记覆盖一个空格 示例:"%6.2f" |
# | 把结果转换位另一种形式。如果是%o格式,则以0开始;如果是%x或%X格式,则以0x或0X开始;对于所有的浮点格式,#保证了即使后面没有任何数字,也打印一个小数点字符。对于%g和%G格式,#防止结果后面的0被删除。 示例:"%#o"、"%#8.0f"、"%+=10.3e" |
0 | 对于数值格式,用前导0代替空格填充字段宽度。对于整数格式,如果出现-标记或指定精度,则忽略该标记 |
1、使用修饰符和标记的示例
先看看字符宽度在打印整数的效果。
程序清单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);
system("pause");
return 0;
}
VS运行结果如下:
程序清单4.7通过4种不同的转换说明把相同的值打印了4次。程序中使用星号(*)标出每个字段的开始和结束。
第1个转换说明%d不带任何修饰符,其对应的输出结果与带整数字段宽度的转换说明的输出结果相同。在默认情况下,没有任何修饰符的转换说明,就是这样的打印结果。
第2个转换说明是%2d,其对应的输出结果应该是2字段宽度。因为待打印的整数有3位数字,所以字段宽度自动扩大以符合整数的长度。
第3个转换说明是%10d,其对应的输出结构有10个空格宽度,实际上在两个星号之间有7个空格和3位数字,并且数字位于字段的右侧。
最后一个转换说明是%-10d,其对应的输出结果同样是10个空格宽度,-标记说明打印的数字位于字段的左侧。
接下来看看浮点型格式。
程序清单4.8 floats.c程序
// floats.c -- 一些浮点型修饰的组合
#include <stdio.h>
int main(void)
{
const double RENT = 3852.99; // const 变量
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);
system("pause");
return 0;
}
VS运行结果如下:
该程序使用了const关键字,限定变量为只读。
第1个转换说明是%f。在这种情况下,字段宽度和小数点后面的位数均为系统默认设置,即字段宽度是容纳待打印数字所需的位数和小数点后打印6位数字。
第2个转换说明是%e。默认情况下,编译器在小数点的左侧打印1个数字,在小数点的右侧打印6个数字。这样打印的数字太多了,解决方案是指定小数点右侧显示的位数。
第7个转换说明中包含了+标记,这使得打印的值前面多了一个代数符号(+)。0标记使得打印的值前面以0填充以满足字段要求。注意,转换说明%010.2f的第一个0是标记,句点(.)之前、标记之后的数字(本例为10)是指定的字段宽度。
程序清单4.9 flags.c程序
/* 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);
system("pause");
return 0;
}
VS运行结果如下:
第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程序
/* stringf.c -- 字符串格式 */
#include <stdio.h>
#define BLURB "Authentic imitation!"
int main(void)
{
printf("[%2s]\n", BLURB);
printf("[%24s]\n", BLURB);
printf("[%24.5s]\n", BLURB);
printf("[%-24.5s]\n", BLURB);
system("pause");
return 0;
}
VS运行结果如下:
注意,虽然第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.11 intconv.c程序
/* intconv.c -- 一些不匹配的整型转换 */
#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);
system("pause");
return 0;
}
VS运行结果如下:
第1行输出,num变量对应的转换说明%hd和%hu输出的结果都是336。
第2行输出,mnum变量对应的转换说明%u输出的结果缺位65200,并非期望的336。这是由于有符号short int类型的值在我们的参开系统中的表示方式所致。首先,short int的大小是2字节,其次,系统使用二进制补码来表示有符号的整数。这种方法,数字0~32767代表它们本身,而数字32768~65535则表示负数。其中,65535表示-1,65534表示-2,一次类推。因此-336表示位65200(即65536 - 336)。所以被解释成有符号int时,65200代表-336,而被解释成无符号int是,65200则代表65200。
第3行演示了如果把一个大于255的值转成字符会发生什么情况。short int是2字节,char是1字节。当printf()使用%c打印336时,它只会查看存储336的2字节中的后1字节。这种截断相当于一个整数除以256,只保留其余数。因此,余数是80,对应的asic码是字符P。
最后,在系统中打印比short int类型最大整数(32767)更大的整数(65618)。这次,计算机也进行了求模运算。65618存储位4字节的int类型值。用%hd转换说明打印时,printf()只打印最后2个字节。这相当于65618除以65536的余数。这里余数是82。鉴于负数的存储方法,如果余数在32767~65535范围内会被打印成负数。
程序清单4.12 floatcnv.c程序
/* floatenv.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);
printf("%ld, %ld\n", n3, n4);
printf("%ld %ld %ld %ld\n", n1, n2, n3, n4);
system("pause");
return 0;
}
VS运行结果如下:
第1行输出显示,float类型的值作为printf()参数时会被转成double类型,float是4字节,但是为了printf()能正确地显示该值,n1被扩成8字节。
第2行输出显示,只要使用正确的转换说明,printf()就可以打印n3、n4。
第3行输出显示,如果Printf()语句有其他不匹配的地方,即使用对了转换说明也会生成虚假的结果。用%ld转换说明打印浮点数会失败,用%ld打印Long类型的数竟然也失败!问题在与C如何把信息传递给函数。
2、 printf()的返回值
大部分C语言函数都有一个返回值,这是函数计算并返回给主调程序的值。例如,C库包含一个sqrt()函数,接受一个数作为入参,并返回该值的平方根。可以把返回值赋给变量,也可以用于计算,还可以作为参数传递。
printf()也有一个返回值,它返回打印字符的个数,如果输出有错误,printf()则返回一个负值。
程序清单4.13 printval.c程序
/* printval.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);
system("pause");
return 0;
}
VS运行结果如下:
程序用rv = printf(...);形式把printf()的返回值赋给rv。因此,该语句执行了两项任务:打印信息和给变量赋值。
其次,注意针对所有的字符数,包括空格和不可见的换行符(\n)。
3、打印较长的字符串
有时,printf()语句太长,在屏幕上不方便阅读。如果空白(空格、制表符、换行符)仅用于分隔不同的部分,C编译器会忽略它们。
因此,一条语句可以写成多行,只需在不同部分之间输入空白即可。如:
printf("The Printf() function printed %d characters.\n",
rv);
该语句在逗号和rv之间断行。为了让读者知道该行未完。示例缩进了rv。C编译器会忽略多余的空白。
但是,不能在双引号括起来的字符串中断行。如:
printf("The printf() function printed %d
characters.\n", rv);
C编译器会报错:字符串常量有非法字符。在字符串中,可以使用\n来表示换行符,但是不能通过按下Enter键产生实际的换行符。
给字符串断行有3种方法。
程序清单4.14 longstrg.c程序
/* 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");
system("pause");
return 0;
}
VS运行结果如下:
方法1:使用多个printf()语句。因为第 1个字符串没有以\n字符结束,所以第2个字符串紧跟着第1个字符串末尾输出。
方法2:用反斜杠(\)和Enter键组合来断行。这使得光标移至下一行,而且字符串中不会包含换行符。其效果是在下一行继续输出。但是,下一行的代码必须和程序清单中的代码一样从最左边开始。如果缩进该行,比如缩进5个空格,那么这5个空格就会成为字符串的一部分。
方法3:ANSI C引入的字符串连接。在两个用双引号括起来的字符串之间用空白隔开,C编译器会把多个字符串看作是一个字符串。
4.4.5 使用scanf()
C库包含了多个输入函数,scanf()是最通用的一个,因为它可以读取不同格式的数据。
从键盘输入的都是文本,因为键盘只能生成文本字符:字母、数字和标点符号。如果要输入整数2014,就要键入字符2、0、1、4。如果要将其存储为数值而不是字符串,程序就必须把字符依次转换成数值。scanf()把输入的字符串转换成整数、浮点数、字符或字符串,而printf()正好与它相反,把整数、浮点数、字符和字符串转换成显示在屏幕上的文本。
scanf()和printf()类似,也使用格式字符串和参数列表。scanf()中的格式字符串表明字符输入流的目标数据类型。两个函数主要区别在参数列表中。printf()函数使用变量、常量和表达式,而scanf()函数使用指向变量的指针。
如果使用scanf()读取基本变量类型的值,在变量名前加上一个&;
如果使用scanf()把字符串读取字符数组中,不需要使用&
程序清单4.15 input.c程序
// input.c -- 何时使用&
#define _CRT_SECURE_NO_WARNINGS
#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);
system("pause");
return 0;
}
VS运行结果如下:
scanf()函数使用空白(换行符、制表符和空格)把输入分成多个字段。在依次把转换说明和字段匹配时跳过空白。注意,上面示例的输入项分成了两行。只要在每个输入项之间输入至少一个换行符、空格或制表符即可,可以在一行或多行输入。
唯一例外的是%c转换说明。根据%c,scanf()会读取每个字符,包括空白。
scanf()函数所用的转换说明与printf()函数几乎相同。主要区别是,对于float类型和double类型,printf()都是用%f、%e、%E、%g和%G转换说明。而scanf()只把它们用于float类型,对于double类型时要使用l修饰符。
scanf()的转换说明:
转换说明 | 含义 |
---|---|
%c | 把输入解释成字符 |
%d | 把输入解释成有符号十进制整数 |
%e、%f、%g、%a | 把输入解释成浮点数 |
%E、%F、%G、%A | 把输入解释成浮点数 |
%i | 把输入解释成有符号十进制整数 |
%o | 把输入解释成有符号八制整数 |
%p | 把输入解释成指针 |
%s | 把输入解释成字符串。从第1个非空白字符开始,到下一个空白字符之前的所有字符都是输入 |
%u | 把输入解释成无符号十进制整数 |
%x、%X | 把输入解释成有符号十六进制整数 |
在上表所列的转换说明中(百分号和转换字符之间)使用修饰符。如果要使用多个修饰符,必须按下表所列的顺序书写。
转换说明 | 含义 |
---|---|
* | 抑制赋值 示例:"%*d |
数字 | 最大字段宽度。输入达到最大字段宽度处,或第1次遇到空白字符时停止。 示例:"%10s" |
hh | 把整数作为signed char或unsigned char类型读取 示例:"hhd"、"hhu" |
ll | 把整数作为long long或unsigned long long类型读取 示例:"lld"、"llu" |
h、l或I | "%hd"和"hi"表明把对应的值存储位short int类型 "%ho"、"%hx"和"%hu"表明把对应的值存储位unsigned short int类型 "%ld"和"%li"表明把对应的值存储为long类型 “%lo”、"%lx"和"%lu"表明把对应的值存储为unsigned long类型 "%le"、"%lf"和"%lg"表明把对应的值存储为double类型 在e、f和g前面使用L而不是l,表明把对应的值被存储为int类型,f和g表明把对应的值存储为float类型 |
j | 在整型类型转换说明后面时,表明使用intmax_t或uintmax_t类型 示例:“%zd”、"%zo" |
z | 在整型类型转换说明后面是,表明使用sizeof的返回类型 |
t | 在整型转换说明后面时,表明使用两个指针差值的类型 示例:"%td"、"%tx" |
1、从scanf()角度看输入
假设scanf()根据一个%d转换说明读取一个整数,scanf()函数每次读取一个字符,跳过所有的空白字符,知道遇到第1个非空白字符才开始读取。因为要读取整数,所以scanf()希望发现一个数字字符或者一个符号(+或-)。如果找到一个数字或符号,它便保存该字符,并读取下一个字符。如果下一个字符是数字,它便保存该数字并读取下一个字符。scanf()不断地读取和保存字符,直到遇到非数字字符。如果遇到一个非数字字符,它便认为读到了整数的末尾。然后scanf()把非数字字符放回输入。这意味着程序在下一次读取输入时,首先读到的是上一次读取丢弃的非数字字符。最后,scanf()计算已读取数字相应的数值,并将计算后的值放入到指定的变量中。
如果使用字段宽度,scanf()会在字段结尾或第1个空白字符处停止读取。
如果第1个非空白字符是A而不是数字,会发生什么?scanf()会停在那里,并把A放回输入中,不会把值赋值给指定变量。程序在下一次读取时,首先读到的字符是A。如果程序只使用%d转换说明,scnaf()就一直无法越过A读下一个字符。另外,如果使用带多个转换说明的scanf(),C规定在第1个出错处停止读取输入。
用其他数值匹配的转换说明读取输入和用%d的情况相同。区别在于scanf()会把更多的字符识别成整数的一部分。例如,%x转换说明要求scanf()识别十六进制数a~f和A~F。浮点转换说明要求scanf()识别小数点、e计数法和p计数法。
如果使用%s转换说明,scanf()会读取除空白以外的所有字符。scanf()跳过空白开始读取第一个非空白字符,并保存非空白字符直到再次遇到空白。这意味着scanf()根据%s转换说明读取一个单词,即不包含空白字符的字符串。如果使用字符宽度,scanf()在字段末尾或第1个空白字符处停止读取。无法利用字段宽度让只有一个%s的scanf()读取多个单词。最后要注意一点:当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)从输入的第一个字符开始读取,而scanf(” %c“, &ch)则从第1个非空白字符开始读取。
3、scanf()的返回值
scanf()函数返回成功读取的项数。如果没有读取任何项,且需要读取一个数字而用户输入了一个非数值字符串,scanf()返回0。当scanf()检测到”文件结尾“时,会返回EOF。
4.4.6 printf()和scanf()的*修饰符
printf()和scanf()都可以使用*修饰符来修改转换说明的含义。但是,它们的用法不太一样。
printf()的*修饰符:
如果不想预先指定字段宽度,希望通过程序指定,那么可以用*修饰符来代替字符宽度。但还是要用一个参数告诉函数,字段宽度应该是多少。也就是说,如果转换说明是%d,那么参数列表中应包含*和d对应的值。这个技巧也可用于浮点值指定精度和字符宽度。
程序清单4.16 varwidth.c程序
/* varwidth.c -- 使用变宽输出字符 */
#include <stdio.h>
int main(void)
{
unsigned width, precision;
int number = 256;
double weight = 242.5;
printf("Enter a field width:\n");
scanf_s("%d", &width);
printf("The number is :%*d;\n", width, number);
printf("Now enter a width and a precision:\n");
scanf_s("%d %d", &width, &precision);
printf("Weight = %*.*f\n", width, precision, weight);
printf("Done!");
system("pause");
return 0;
}
VS运行结果如下:
变量width提供字段宽度,number是待带引的追踪,因为转换说明中*在d的前面,所以在printf()的参数列表中,width在number的前面。同样,width和precision提供打印weight的格式花信息。
这里,用户首先输入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 enetr three integers:\n");
scanf_s("%*d %*d %d", &n);
printf("The last integer was %d\n", n);
system("pause");
return 0;
}
VS运行结果如下:
在程序中需要读取文件中特定列的内容时,这项跳过功能很有用。
4.4.7 printf()的用法提示
想把数列打印成列,指定固定字段宽度很有用。因为默认的字段宽度是待打印数字的宽度,如果同一列中打印的数字位数不同,那么下面的语句:
printf("%d %d %d\n", bal1, val2, val3);
打印出来的数字可能层次不齐。例如,假设执行3次printf()语句,用户输入不同的变量,其输出可能是这样:
12 234 1222
4 5 23
22334 2322 10001
使用足够大的固定字段宽度可以让输出整齐美观。例如:
printf("%9d, %9d %9d\n", val1, val2, val3);
上面的输出将变为:
12 234 1222
4 5 23
22334 2322 10001
在两个转换说明中间插入一个空白字符,可以确保及时一个数字溢出了自己的字段,下一个数字也不会紧跟该数字一起输出。这是因为格式字符串中的普通字符会被打印出来。
另一方面,如果要在文字中嵌入一个数字,通常指定一个小于或等于该数字宽度的字段会比较方便。这样,输出数字的宽度正合适,没有不必要的空白。例如:
printf("Count Beppo ran %.2f miles in 3 hours.\n", distance);
其输出如下:
Count Beppo ran 10.22 miles in 3 hours.
如果把转换说明改为"%10.2f",则输出如下:
Count Bepo ran 10.22 miles in 3 hours.
4.5 复习题
1、再次运行程序清单4.1 但是在要求输入名时,请输入名和姓(根据英文书写习惯,名和姓中间有一个空格),看看会发生什么情况?为什么?
姓名只保存了名,姓并未输出出来,scanf()在输入时遇到第一个空白字符时,就停止了输入,因此name字符数组中就只保存了名的字符串信息。
2、假设下列示例都是完整程序中的一部分,它们打印的结果是什么?
-
printf("He sold the painting for $%2.2f.\n", 2.345e2);
-
He sold the painting for $234.50.
-
-
printf("%c%c%c%c\n", 'H', 105, '\41');
-
H
-
-
#define Q "His Hamlet was funny weithout being vulgar."
printf("%s\nhas %d characters.\n", Q, strlen(Q));
-
His Hamlet was funny weithout being vulgar.
-
has 32 characters
-
-
printf("Is %2.2e the same as %2.2f?\n", 1201.0, 1201.0);
-
Is
-
3、在第2题的c中,要输出包含双引号的字符串Q,应如何修改
#define Q "\"His Hamlet was funny weithout being vulgar.\""
printf("%s\nhas %d characters.\n", Q, strlen(Q));
4、找出下面程序的错误。
define B booboo
define X 10
main(int)
{
int age;
char name;
printf("Please enter your first name.");
scanf("%f", name);
printf("All right, %c, what's your age?\n", name);
scanf("%f", age);
xp = age + X;
printf("That's a %s! You must be at least %d.\n", B, xp);
return 0;
}
修改后的代码:
#include <stdio.h>
#define B "booboo"
#define X 10
int main(void)
{
int age;
char name[40];
printf("Please enter your first name.");
scanf("%s", name);
printf("All right, %s, what's your age?\n", name);
scanf("%d", &age);
int xp = age + X;
printf("That's a %s! You must be at least %d.\n", B, xp);
return 0;
}
5、假设一个程序的开头是这样:
#define BOOK "War and Peace"
int main(void)
{
float cost = 12.99;
float percent = 80.0;
请构造一个使用BOOK、cost和percent和printf()语句,打印以下内容:
This copy of "War and Peace" sells for $12.99.
That is 80% of list.
#define BOOK "War and Peace"
int main(void)
{
float cost = 12.99;
float percent = 80.0;
printf("This copy %s sells for $%f.\n", BOOK, cost);
printf("That is %d%% of list.\n", percent).
return 0;
}
6、 打印下列各项内容要分别使用什么转换说明?
-
一个字段宽度与位数相同的十进制整数:%d
-
一个形如8A、字段宽度为4的十六进制:%4X
-
一个形如232.346、字段宽度为10的浮点数:%10.3f
-
一个形如2.33e+002、字段宽度为12的浮点数:%12.2e
-
一个字段宽度为30、左对齐的字符串:"%-30s"
7、打印下面各项内容要分别使用什么转换说明?
-
字段宽度为15的unsigned long类型的整数:%15lu
-
一个形如0x8a、字段宽度为4的十六进制整数:%#4x
-
一个形如2.33E+02、字段宽度为12,左对齐的浮点数:%-12.2E
-
一个形如+232.346、字段宽度为10的浮点数:#+10.3f;
-
一个字段宽度位8的字符串的前8个字符:%-30s
4.6 编程练习
1、编写一个程序,提示用户输入名和姓,然后以“名.姓”的格式打印出来。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char lastName[40] = {0};
char firstName[40] = {0};
int tmp;
printf("Please enter your last name:");
scanf("%s", lastName);
printf("Please enter your first name:");
scanf("%s", firstName);
printf("Hello, %s.%s\n", lastName, firstName);
system("pause");
return 0;
}
VS运行结果如下:
2、编写一个程序,提示用户输入名和姓,并执行下列操作:
-
打印名和姓,包括双引号;
-
在宽度为20的字段右端打印名和姓,包括双引号;
-
在宽度为20的字符左端打印名和姓,包括双引号;
-
在比姓名宽度宽3的字段中打印名和姓
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main(void) { char name[40] = { 0 }; printf("Please enter your last name and first name:"); scanf("%s", name); printf("\"%s\"\n", name); printf("\"%20s\"\n", name); printf("\"%-20s\"\n", name); system("pause"); return 0; }
3、编写一个程序,读取一个浮点数,首先以小数点计数法打印,然后以指数计数法打印。应下面的格式进行输出
输入21.3或2.1e+001
输入+21.290或2.129E+001
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
float num;
printf("Please enter a flost num:");
scanf("%f", &num);
printf("%.1f或%.1e\n", num, num);
system("pause");
return 0;
}
VS运行结果如下:
4、编写一个程序,提示用户输入身高(单位:英寸)和姓名,然后以下面的格式显示用户刚输入的信息:
Dabney, you are 6.208 feet tall
使用float类型,并用/作为除号。如果你愿意,可以要求用户以厘米位单位输入身高,并以米位单位显示出来。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char name[40];
float height;
printf("Please enter your name:");
scanf("%s", name);
printf("Please enter your height(unit:inch):");
scanf("%s", &height);
printf("%s, you are %.3f feet tall\n", name, height / 12.0);
system("pause");
return 0;
}
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 fileSpeed;
float fileSize;
printf("Please enter the file download speed:");
scanf_s("%f", &fileSpeed);
printf("Please enter the file size:");
scanf_s("%f", &fileSize);
printf("At %.2f megabits per second, a file of %.2f megabytes downloads in %.2f seconds.\n", fileSpeed, fileSize, (fileSize * 8) / fileSpeed);
system("pause");
return 0;
}
VS运行结果如下:
6、编写一个程序,先提示用户输入名,然后提示用户输入姓。在一行打印输出的名和姓,下一行分别打印名和姓的字母数。字母数要与相应名和姓的结尾对齐,如下所示:
Melissa Honeybee
7 8
接下来,再打印相同的信息,但是字母个数与相应名和姓的开头对齐,如下所示:
Melissa Honeybee
7 8
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
char lastName[40] = { 0 };
char firstName[40] = { 0 };
printf("Please enter your last name:");
scanf("%s", lastName);
printf("Please enter your first name:");
scanf("%s", firstName);
printf("%s %s\n", lastName, firstName);
printf("%*d %*d\n", strlen(lastName), strlen(lastName), strlen(firstName), strlen(firstName));
printf("\n", lastName, firstName);
printf("%s %s\n", lastName, firstName);
printf("%-*d %-*d\n", strlen(lastName), strlen(lastName), strlen(firstName), strlen(firstName));
system("pause");
return 0;
}
VS运行结果如下:
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)
{
int num;
printf("请输入要显示的位数:");
scanf_s("%d", &num);
double dValue = 1.0 / 3.0;
float fValue = 1.0 / 3.0;
printf("double Value:%.*f\n", num, dValue);
printf("float Value:%.*f\n", num, fValue);
system("pause");
return 0;
}
VS运行结果如下:
8、编写一个程序,提示用户输入旅行的里程和消耗的汽油量。然后计算并显示消耗每加仑汽油行驶的英里数,显示小数点后面一位数字。接下来,使用1加仑大约3.785升,1英里大约1.609千米,把单位是英里/加仑的值转换为升/100公里,并显示结果,显示小数点后面1位数字。