《C Primer Plus》第四章—字符串和格式化输入/输出(scanf()/printf()详解,字符常量定义方式与limits.h float.h,转换修饰符,*修饰符)

本文详细介绍了C语言中处理字符串的方法,包括字符数组、空字符、strlen()函数以及预处理器#define和const限定符。此外,还讲解了printf()和scanf()函数的使用,包括转换说明、字段宽度、精度和修饰符,以及处理输入输出的注意事项。通过示例代码,展示了如何有效地处理字符串和格式化输入输出。
摘要由CSDN通过智能技术生成

加粗是用来标记我的知识盲区,无特殊含义。

总结的我其中不熟悉的点

  • scanf输入字符串的停止条件
  • sizeof和strlen返回类型的转换说明是zd
  • 实际类型是unsigned和unsigned long
  • sizeof运算对象不是类型时可以不带括号
  • 常量的两种定义方式
  • 明示常量两个头文件limits.h float.h
  • printf转换说明
  • printf转换说明修饰符
  • 转换不匹配导致不可测输出的原因
  • printf返回值
  • 打印长字符串的另外两种方法
  • scanf的转换说明
  • scanf赋值不同类型时发生的情况
  • printf与scanf的*修饰符

字符串和格式化输入/输出

本章内容

本章介绍以下内容:

  • 函数——strlen();
  • 关键字——const;
  • 字符串;
  • 如何创建、存储字符串;
  • 如何使用strlen()函数获取字符串的长度;
  • 用C预处理器指令#define和ANSI C的const修饰符创建符号常量。

前导程序

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

Hi! What’s your first name?Christine

Christine, what’s your weight in pounds?154

Well, Christine, your volume is 2.47 cubic feet.

Also, your first name has 9 letters,

and we have 40 bytes to store it.

  • 该程序包含以下新特性。

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

字符串简介

  • 字符串(character string)是一个或多个字符的序列
  • 双引号不是字符串的一部分。双引号仅告知编译器它括起来的是字符串,正如单引号用于标识单个字符一样。

char类型数组和null字符

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

在这里插入图片描述

  • 注意图中数组末尾位置的字符\0。这是空字符(null character),C语言用它标记字符串的结束。

  • 空字符不是数字0,它是非打印字符,其ASCII码值是(或等价于)0。C中的字符串一定以空字符结束,这意味着数组的容量必须至少比待存储字符串中的字符数多1。

数组是同类型数据元素的有序序列。

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

使用字符串

/* 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来表示。

  • 你不用亲自把空字符放入字符串末尾,scanf()在读取输入时就已完成这项工作。也不用在字符串常量PRAISE末尾添加空字符。

  • 注意(这很重要),scanf()只读取了Angela Plains中的Angela,它在遇到第1个空白(空格、制表符或换行符)时就不再读取输入。

字符串常量"x"和字符常量’x’不同。

区别之一在于’x’是基本类型(char),而"x"是派生类型(char数组);

区别之二是"x"实际上由两个字符组成:'x’和空字符\0

strlen()函数

  • strlen()函数给出字符串中的字符长度。

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

/* 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);
    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;
}

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()并未将其计入。

  • 对于PRAISE,用strlen()得出的也是字符串中的字符数(包括空格和标点符号)。然而,sizeof运算符给出的数更大,因为它把字符串末尾不可见的空字符也计算在内。

  • 第3章提到过,C99和C11标准专门为sizeof运算符的返回类型添加了%zd转换说明,这对于strlen()同样适用。

  • 对于早期的C,还要知道sizeof和strlen()返回的实际类型(通常是unsigned或unsigned long)。

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

常量和C预处理器

#define

circumference = 3.14159 * diameter;

  • 这里,常量3.14159代表著名的常量pi(π)。在该例中,输入实际值便可使用这个常量。然而,这种情况使用符号常量(symbolic constant)会更好。

  • 首先,常量名比数字表达的信息更多。请比较以下两条语句:

    owed = 0.015 * housevalue;

    owed = taxrate * housevalue;

  • 如果阅读一个很长的程序,第2条语句所表达的含义更清楚。

  • 另外,假设程序中的多处使用一个常量,有时需要改变它的值。毕竟,税率通常是浮动的。如果程序使用符号常量,则只需更改符号常量的定义,不用在程序中查找使用常量的地方,然后逐一修改。

  • 如何创建符号常量?

  • 方法之一是声明一个变量,然后将该变量设置为所需的常量。可以这样写:

    float taxrate;

    taxrate = 0.015;

  • **这样做提供了一个符号名,但是taxrate是一个变量,程序可能会无意间改变它的值。**C语言还提供了一个更好的方案——C预处理器。

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

    #define TAXRATE 0.015

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

  • 通用格式:#define NAME value

  • 用大写表示符号常量是C语言一贯的传统。

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

#define BEEP '\a’

#define TEE 'T’

#define ESC '\033’

#define OOPS “Now you have done it!”

记住,符号常量名后面的内容被用来替换符号常量。不要犯这样的常见错误:

/* 错误的格式 */

#define TOES = 20

如果这样做,替换TOES的是= 20,而不是20。

const限定符

  • C90标准新增了const关键字,用于限定一个变量为只读

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

  • 也就是说,可以在计算中使用MONTHS,可以打印MONTHS,但是不能更改MONTHS的值。

  • const用起来比#define更灵活

明示常量

  • 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);

在这里插入图片描述

在这里插入图片描述

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

  • 表中所列都与float类型相关。把明示常量名中的FLT分别替换成DBL和LDBL,即可分别表示double和long double类型对应的明示常量

在这里插入图片描述

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

printf()和scanf()

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

printf()函数

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

在这里插入图片描述

使用printf()

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

  • 待打印项1、待打印项2等都是要打印的项。它们可以是变量、常量,甚至是在打印之前先要计算的表达式。第3章提到过,格式字符串应包含每个待打印项对应的转换说明。

  • 格式字符串包含两种形式不同的信息:实际要打印的字符;转换说明

在这里插入图片描述

  • 如果只打印短语或句子,就不需要使用任何转换说明。如果只打印数据,也不用加入说明文字。

    printf(“Farewell! thou art too dear for my possessing,\n”);

    printf("%c%d\n", ‘$’, 2 * cost);

  • printf()使用的是值,无论是变量、常量还是表达式的值。

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

pc = 2*6;

printf(“Only %d%% of Sally’s gribbles were edible.\n”, pc);

printf()的转换说明修饰符

  • 在%和转换字符之间插入修饰符可修饰基本的转换说明。

  • 如果要插入多个字符,其书写顺序应该与列出的顺序相同。不是所有的组合都可行。

  • 在这里插入图片描述

  • 在这里插入图片描述

  • 在这里插入图片描述

/* 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*

* 959*

*959 *

  • 第1个转换说明%d不带任何修饰符,其对应的输出结果与带整数字段宽度的转换说明的输出结果相同。在默认情况下,没有任何修饰符的转换说明,就是这样的打印结果。
  • 第2个转换说明是%2d,其对应的输出结果应该是2字段宽度。因为待打印的整数有3位数字,所以字段宽度自动扩大以符合整数的长度。
  • 第3个转换说明是%10d,其对应的输出结果有10个空格宽度,实际上在两个星号之间有7个空格和3位数字,并且数字位于字段的右侧。
  • 最后一个转换说明是%-10d,其对应的输出结果同样是10个空格宽度,-标记说明打印的数字位于字段的左侧。
// 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);
    return 0;
}

*3852.990000*

*3.852990e+03*

*3852.99*

*3853.0*

* 3852.990*

* 3.853E+03*

*+3852.99*

*0003852.99*

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

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

[Authentic imitation!]

[ Authentic imitation!]

[ Authe]

[Authe ]

转换说明的意义

  • 转换(conversion)可能会误导读者认为原始值被替换成转换后的值。
  • 实际上,转换说明是翻译说明,%d的意思是“把给定的值翻译成十进制整数文本并打印出来”。
转换不匹配
/* 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);
    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

  • 第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字节。这种截断(见图4.8)相当于用一个整数除以256,只保留其余数。在这种情况下,余数是80,对应的ASCII值是字符P。用专业术语来说,该数字被解释成“以256为模”(modulo 256),即该数字除以256后取其余数。

  • 最后,我们在该系统中打印比short int类型最大整数(32767)更大的整数(65618)。这次,计算机也进行了求模运算。在本系统中,应把数字65618存储为4字节的int类型值。用%hd转换说明打印时,printf()只使用最后2个字节。这相当于65618除以65536的余数。这里,余数是82。鉴于负数的存储方法,如果余数在32767~65536范围内会被打印成负数。对于整数大小不同的系统,相应的处理行为类似,但是产生的值可能不同。

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

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

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

  • 该调用告诉计算机把变量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()还是读错了字节。

在这里插入图片描述

printf()的返回值
  • printf()函数也有一个返回值,它返回打印字符的个数。如果有输出错误,printf()则返回一个负值(printf()的旧版本会返回不同的值)。

  • printf()的返回值是其打印输出功能的附带用途,通常很少用到,但在检查输出错误时可能会用到(如,在写入文件时很常用)。

打印较长的字符串
/* 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;
}
  • 方法1:使用多个printf()语句。因为第1个字符串没有以\n字符结束,所以第2个字符串紧跟第1个字符串末尾输出。
  • 方法2:用反斜杠(\)和Enter(或Return)键组合来断行。这使得光标移至下一行,而且字符串中不会包含换行符。其效果是在下一行继续输出。但是,下一行代码必须和程序清单中的代码一样从最左边开始。如果缩进该行,比如缩进5个空格,那么这5个空格就会成为字符串的一部分。
  • 方法3:ANSI C引入的字符串连接。在两个用双引号括起来的字符串之间用空白隔开,C编译器会把多个字符串看作是一个字符串。

使用scanf()

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

  • scanf()中的格式字符串表明字符输入流的目标数据类型。两个函数主要的区别在参数列表中。printf()函数使用变量、常量和表达式,而scanf()函数使用指向变量的指针。

  • 这里,读者不必了解如何使用指针,只需记住以下两条简单的规则:

    • 如果用scanf()读取基本变量类型的值,在变量名前加上一个&;
    • 如果用scanf()把字符串读入字符数组中,不要使用&。
// 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;
}
  • scanf()函数使用空白(换行符、制表符和空格)把输入分成多个字段。

  • 注意,上面示例的输入项(粗体部分是用户的输入)分成了两行。只要在每个输入项之间输入至少一个换行符、空格或制表符即可,可以在一行或多行输入

  • 对于float类型和double类型,printf()都使用%f、%e、%E、%g和%G转换说明。而scanf()只把它们用于float类型,对于double类型要使用l修饰符。

  • 在这里插入图片描述

  • 在这里插入图片描述

从scanf()角度看输入
  • 如果scanf("%d %d",&a,&b),输入2 3,则会忽略2 3之间的所有空白字符(空格,回车,换行),但是紧接着3之后的任何字符都会保留在输入缓冲,会被紧接着的下一个getchar()读取。

  • scanf()不断地读取和保存字符,直至遇到非数字字符。如果遇到一个非数字字符,它便认为读到了整数的末尾。然后,scanf()把非数字字符放回输入。这意味着程序在下一次读取输入时,首先读到的是上一次读取丢弃的非数字字符。

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

  • 如果使用%s转换说明,scanf()会读取除空白以外的所有字符。scanf()跳过空白开始读取第1个非空白字符,并保存非空白字符直到再次遇到空白。这意味着scanf()根据%s转换说明读取一个单词,即不包含空白字符的字符串。

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

  • 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

    请注意,“所有空白”的概念包括没有空格的特殊情况。

    个人测试补充:scanf(“%d,%d”,&num1,&num2);与 scanf(“%d ,%d”,&num1,&num2);

    区别就是逗号前有无空格,无空格若输入1,2或1, 2没问题,因为会忽略整数前的空白,但是1 ,2就会导致第二个参数获取,逗号前面有空格就解决了这个问题。

  • 除了%c,其他转换说明都会自动跳过待输入值前面所有的空白。

  • scanf("%d%d", &n, &m)与scanf("%d %d", &n, &m)的行为相同。

  • 对于%c,在格式字符串中添加一个空格字符会有所不同。例如,如果在格式字符串中把空格放到%c的前面,scanf()便会跳过空格,从第1个非空白字符开始读取。

  • 也就是说,scanf("%c", &ch)从输入中的第1个字符开始读取,而scanf(" %c", &ch)则从第1个非空白字符开始读取。

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

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

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

    /* 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;
    }
    

Enter a field width:6

The number is : 256:

Now enter a width and a precision:8 3

Weight = 242.500

Done!

  • scanf()中*的用法与此不同。把*放在%和转换字符之间时,会使得scanf()跳过相应的输入项。
/* 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

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

关键概念

  • 字符串,无论是表示成字符常量还是存储在字符数组中,都以一个叫作空字符的隐藏字符结尾。

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

  • 假设有如下输入行:

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

本章小结

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

复习题

  1. 再次运行程序清单4.1,但是在要求输入名时,请输入名和姓(根据英文书写习惯,名和姓中间有一个空格),看看会发生什么情况?为什么?

  2. 假设下列示例都是完整程序中的一部分,它们打印的结果分别是什么?
    a.printf(“He sold the painting for $%2.2f.\n”, 2.345e2);
    b.printf("%c%c%c\n", ‘H’, 105, ‘\41’);
    c.#define Q “His Hamlet was funny without being vulgar.”
    printf("%s\nhas %d characters.\n", Q, strlen(Q));
    d.printf(“Is %2.2e the same as %2.2f?\n”, 1201.0, 1201.0);

  3. 在第2题的c中,要输出包含双引号的字符串Q,应如何修改?

  4. 找出下面程序中的错误。

    define B booboo
    define X 10 main(int)
    {
        int age;
        char name;
        printf("Please enter your first name.");
        scanf("%s", 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);
        rerun 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.

  6. 打印下列各项内容要分别使用什么转换说明?
    a.一个字段宽度与位数相同的十进制整数
    b.一个形如8A、字段宽度为4的十六进制整数
    c.一个形如232.346、字段宽度为10的浮点数
    d.一个形如2.33e+002、字段宽度为12的浮点数
    e.一个字段宽度为30、左对齐的字符串

  7. 打印下面各项内容要分别使用什么转换说明?
    a.字段宽度为15的unsigned long类型的整数
    b.一个形如0x8a、字段宽度为4的十六进制整数
    c.一个形如2.33E+02、字段宽度为12、左对齐的浮点数
    d.一个形如+232.346、字段宽度为10的浮点数
    e.一个字段宽度为8的字符串的前8个字符

  8. 打印下面各项内容要分别使用什么转换说明?
    a.一个字段宽度为6、最少有4位数字的十进制整数
    b.一个在参数列表中给定字段宽度的八进制整数
    c.一个字段宽度为2的字符
    d.一个形如+3.13、字段宽度等于数字中字符数的浮点数
    e.一个字段宽度为7、左对齐字符串中的前5个字符

  9. 分别写出读取下列各输入行的scanf()语句,并声明语句中用到变量和数组。
    a.101
    b.22.32 8.34E−09
    c.linguini
    d.catch 22
    e.catch 22 (但是跳过catch)

  10. 什么是空白?

  11. 下面的语句有什么问题?如何修正?
    printf(“The double type is %z bytes…\n”, sizeof(double));

  12. 假设要在程序中用圆括号代替花括号,以下方法是否可行?

#define ( {

#define ) }


  1. 程序不能正常运行。第1个scanf()语句只读取用户输入的名,而用户输入的姓仍留在输入缓冲区中(缓冲区是用于存储输入的临时存储区)。下一条scanf()语句在输入缓冲区查找重量时,从上次读入结束的地方开始读取。这样就把留在缓冲区的姓作为体重来读取,导致scanf()读取失败。另一方面,如果在要求输入姓名时输入Lasha 144,那么程序会把144作为用户的体重(虽然用户是在程序提示输入体重之前输入了144)。

  2. a.He sold the painting for $234.50.
    b.Hi!(注意,第1个字符是字符常量;第2个字符由十进制整数转换而来;第3个字符是八进制字符常量的ASCII表示)
    c.His Hamlet was funny without being vulgar.
    has 42 characters.
    d.Is 1.20e+003 the same as 1201.00?

  3. 在这条语句中使用":printf(""%s"\nhas %d characters.\n", Q, strlen(Q));

  4. 下面是修改后的程序:

    #include <stdio.h> /* 别忘了要包含合适的头文件 */
    #define B "booboo" /* 添加#、双引号 */
    #define X 10       /* 添加# */
    int main(void)     /* 不是main(int) */
    {
        int age;
        int xp;                                    /* 声明所有的变量 */
        char name[40];                             /* 把name声明为数组 */
        printf("Please enter your first name.\n"); /* 添加\n,提高可读性 */
        scanf("%s", name);
        printf("All right, %s, what's your age?\n", name); /* %s用于打印字符串*/
        scanf("%d", &age);                                 /* 把%f改成%d,把age改成&age */
        xp = age + X;
        printf("That's a %s! You must be at least %d.\n", B, xp);
        return 0; /* 不是rerun */
    }
    
  5. 记住,要打印%必须用%%:

    printf("This copy of \"%s\" sells for $%0.2f.\n", BOOK, cost);printf("That is %0.0f%% of list.\n", percent);
    
  6. a.%d
    b.%4X
    c.%10.3f
    d.%12.2e
    e.%-30s

  7. a.%15lu
    b.%#4x
    c.%-12.2E
    d.%+10.3f
    e.%8.8s

  8. a.%6.4d
    b.%o
    c.%2c
    d.%+0.2f
    e.%-7.5s

  9. a.int dalmations;
    scanf("%d", &dalmations);
    b.float kgs, share;
    scanf("%f%f", &kgs, &share);
    (注意:对于本题的输入,可以使用转换字符e、f和g。另外,除了%c之外,在%和转换字符之间加空格不会影响最终的结果)
    c.char pasta[20];
    scanf("%s", pasta);
    d.char action[20];
    int value;
    scanf("%s %d", action, &value);
    e.int value;
    scanf("%*s %d", &value);

  10. 空白包括空格、制表符和换行符。C语言使用空白分隔记号。scanf()使用空白分隔连续的输入项。

  11. %z中的z是修饰符,不是转换字符,所以要在修饰符后面加上一个它修饰的转换字符。可以使用%zd打印十进制数,或用不同的说明符打印不同进制的数,例如,%zx打印十六进制的数。

  12. 可以分别把(和)替换成{和}。但是预处理器无法区分哪些圆括号应替换成花括号,哪些圆括号不能替换成花括号。因此,

    #define ( {
    #define ) }
    int main(void)(printf("Hello, O Great One!\n");)
    

    将变成:

    int main{void}{
        printf{"Hello, O Great One!\n"};}
    

编程练习

  1. 编写一个程序,提示用户输入名和姓,然后以“名,姓”的格式打印出来。
  2. 编写一个程序,提示用户输入名字,并执行以下操作:
    a.打印名字,包括双引号;
    b.在宽度为20的字段右端打印名字,包括双引号;
    c.在宽度为20的字段左端打印名字,包括双引号;
    d.在比姓名宽度宽3的字段中打印名字。
  3. 编写一个程序,读取一个浮点数,首先以小数点记数法打印,然后以指数记数法打印。用下面的格式进行输出(系统不同,指数记数法显示的位数可能不同):
    a.The input is 21.3 or 2.1e+001.
    b.The input is +21.290 or 2.129E+001.
  4. 编写一个程序,提示用户输入身高(单位:英寸)和姓名,然后以下面的格式显示用户刚输入的信息:
    Dabney, you are 6.208 feet tall
    使用float类型,并用/作为除号。如果你愿意,可以要求用户以厘米为单位输入身高,并以米为单位显示出来。
  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.
  6. 编写一个程序,先提示用户输入名,然后提示用户输入姓。在一行打印用户输入的名和姓,下一行分别打印名和姓的字母数。字母数要与相应名和姓的结尾对齐,如下所示:
    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的值与这些值一致吗?
  8. 编写一个程序,提示用户输入旅行的里程和消耗的汽油量。然后计算并显示消耗每加仑汽油行驶的英里数,显示小数点后面一位数字。接下来,使用1加仑大约3.785升,1英里大约为1.609千米,把单位是英里/加仑的值转换为升/100公里(欧洲通用的燃料消耗表示法),并显示结果,显示小数点后面1位数字。注意,美国采用的方案测量消耗单位燃料的行程(值越大越好),而欧洲则采用单位距离消耗的燃料测量方案(值越低越好)。使用#define创建符号常量或使用const限定符创建变量来表示两个转换系数。

  1. #include <stdio.h>
    int main(void)
    {
        char lastName[30], firstName[30];
        printf("请输入您的名字:");
        scanf("%s", lastName);
        printf("请输入您的姓氏:");
        scanf("%s", firstName);
        printf("欢迎%s,%s\n", lastName, firstName);
        return 0;
    }
    
  2. #include <stdio.h>
    #include <string.h>
    int main(void)
    {
        char name[40] = "jianshengnan";
        printf("Please input your name:");
        scanf("%s", name);
        printf("\"%s\"\n", name);
        printf("\"%18s\"\n", name);
        printf("\"%-18s\"\n", name);
        unsigned int length = strlen(name);
        printf("%*s\n", length + 3, name);
        return 0;
    }
    
  3. #include <stdio.h>
    int main(void)
    {
        float num;
        // 读取21.29
        scanf("%f", &num);
        printf("a. The input is %.1f or %.1e\n", num, num);
        printf("b. The input is %.3f or %.3E\n", num, num);
        return 0;
    }
    
  4. #include <stdio.h>
    int main(void)
    {
        float tall;
        char name[30];
        printf("Please input your name:");
        scanf("%s", name);
        printf("Welcome to here,%s!\n", name);
        printf("Please input your tall:(cm)");
        scanf("%f", &tall);
        printf("%s, you are %.3fm tall.", name, tall / 100);
        return 0;
    }
    
  5. #include <stdio.h>
    int main(void)
    {
        float speed, fileSize;
        printf("Please input download speed:(Mb/s)");
        scanf("%f", &speed);
        printf("Please input file size:(MB)");
        scanf("%f", &fileSize);
        printf("At %.2f megabits per second, a file of %.2f megabytes\ndownloads in %.2f seconds.\n", speed, fileSize, fileSize * 8 / speed);
        return 0;
    }
    
  6. #include <stdio.h>
    #include <string.h>
    int main(void)
    {
        char firstName[30], lastName[30];
        unsigned int lengthFirstName, lengthLastName;
        printf("Please input your first name:");
        scanf("%s", firstName);
        lengthFirstName = strlen(firstName);
        printf("Please input your last name:");
        scanf("%s", lastName);
        lengthLastName = strlen(lastName);
        printf("%s %s\n", firstName, lastName);
        printf("%*u %*u\n", lengthFirstName, lengthFirstName, lengthLastName, lengthLastName);
        printf("%s %s\n", firstName, lastName);
        printf("%-*u %-*u\n", lengthFirstName, lengthFirstName, lengthLastName, lengthLastName);
        return 0;
    }
    
  7. #include <stdio.h>
    #include <float.h>
    int main(void)
    {
        double db_num = 1.0 / 3.0;
        float fl_num = 1.0 / 3.0;
        printf("%.6f %.6f\n",db_num,fl_num);
        printf("%.12f %.12f\n",db_num,fl_num);
        printf("%.16f %.16f\n",db_num,fl_num);
        printf("%f %f\n",FLT_DIG ,DBL_DIG);
        return 0;
    }
    /* 0.333333 0.333333
       0.333333333333 0.333333343267
       0.3333333333333333 0.3333333432674408
       0.000000 0.333333 
    */
    
  8. #include <stdio.h>
    #define JIALUN_TO_L 3.785
    int main(void)
    {
        // 我在这里使用了两种定义符号常量的方式,目的是练习,在实际中请统一为一种编码风格。
        const float yingli_to_km = 1.609f;
        float yingli, jialun;
        printf("请输入旅行的英里数与消耗的汽油加仑数:");
        scanf("%f %f", &yingli, &jialun);
        printf("旅行历程:%.1f英里,消耗汽油量:%.1f加仑,%.1f英里/加仑\n", yingli, jialun, yingli / jialun);
        printf("旅行历程:%.1f公里,消耗汽油量:%.1f升,%.1f升/公里",
               yingli * yingli_to_km, jialun * JIALUN_TO_L, jialun * JIALUN_TO_L / yingli * yingli_to_km * 100);
        return 0;
    }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jian圣楠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值