Learning C Primer Plus | Chapter4

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

前导程序

// filename talkback.c
#include
#include

#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); // 数组不需要加取地址符,因为数组名本身就是地址。
    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 in.\n", size);
    return 0;
}

程序交互如下 :
Hi! What's your first name?
winn
winn, what's your weight in pounds?
139
Well, winn, your volume is 2.23 cubic feet.
Also, your first name has 4 letters,
and we have 40 bytes to store it in.

该程序有如下新特性:

  • 使用一个数组(array)来存放字符串,char name[40]该数组的 40 表明有40个字节的连续存储空间。
  • 使用 %s 转换说明符(conversion specification)来处理字符串的输入和输出。
  • 使用了C预处理器定义了代表值 62.4 的符号常量(后面会详细说明)。
  • 使用了头文件 string.h 中的 strlen() 函数来获取字符串长度。

字符串简介

字符串 (character string)就是一个或多个字符的序列。
"Zing went the strings of my heart ! "
双引号不是字符串的一部分。双引号用于通知编译器其中包含了一个字符串,正如单引号标识着一个字符一样。

char 数组类型和空字符

C将字符串存储在 char 数组中。字符串中字符存放在相邻的存储单元,每个字符占一个单元。

Learning C Primer Plus | Chapter4_第1张图片
字符串在数组中的存储

数组的最后一个位置显示空字符 \0。它代表空字符(null character),用来标记字符串的结束。
由于空字符的存在,数组的单元数会在存储的字符数基础上加一。
字符与字符串的区别在于是否有空字符, 'x' 属于基本类型(char), "x"则属于派生类型(char数组)。

字符串的打印和输入需要使用类型说明符 %s

除了使用 %c 说明符,一般 scanf() 读取输入会在遇到的第一个空白字符空格(blank)、制表符(tab)、或者换行符(newline)处停止读取。所以一般只会读取一个单词而不是整个语句。

strlen() 函数和 sizeof 运算符的区别

使用 strlen() 函数需要包含 #include(或 #include
strlen() 是一个函数,sizeof 是运算符(圆括号对于类型是必须的,但对于具体量是可选的)。
strlen() 计算字符串的字符长度(不含空字符),sizeof 计算占用的内存单元大小(会包含空字符)。

常量和 C 预处理器

用符号常量代替数字常量(增加可读性)还可以达到一改全改方便使用。
#define PI 3.14159

注意:预定义语句,这里不会有一般语句的分号和赋值符号,变量名使用大写利于区分一般变量(也有使用c_k_ 前缀来表示常量的做法)。

使用 #define 语句实际上实在编译之前对源文件进行的替换操作(类似于 #include),称为编译时代入法(compiletime substitution),这样定义的常量称为明显常量(manifest constant)。

const 修饰符

C90新增了创建符号常量的第二种方法,使用 const 关键字把一个变量声明转换成常量声明(变为具有只读属性的变量)。
const int MONTHS = 12;
还有第三种创建符号常量的方法,枚举(enum)会在后面讲到。

系统定义的明显变量

C 头文件 limits.hfloat.h 分别提供有关整数类型和浮点类型大小限制的详细信息
例如 limits.h 文件包含了与下面类似的行:

#define INT_MAX        +32767 // int类型的最大可能值
#define INT_MIN        -32768  // int类型的最小可能值

那么可以使用如下代码:

#include
printf("Maximum int value on this system = %d\n", INT_MAX); // 具体输出结果由计算机决定

书中P66 ~ 67 表4.1和4.2 展示了 limits.hfloat.h 这两个头文件中的一些符号常量。

研究和利用 printf() 和 scanf()

printf() 和 scanf() 函数使您能够与程序通信。它们被称为输入/输出函数(I/O函数),是最通用的 I/O 函数。
它们不是 C 的定义的一部分,最初的时候 C 把输入和输出的实现留给了编译器的编写者,这样可以更好地使 I/O 与特定的机器相匹配。
为了兼容性起见,不同的实现中都带有各自的 printf() 和 scanf() ,但是它们之间偶尔有一些差异。
大多数使用的都是 C90 和 C99 描述的这些函数的标准版本。

printf() 函数

使用 printf() 函数打印变量的指令取决于变量的类型,例如打印整数时使用 %d 符号,打印字符时使用 %c 符号,这些符号被称为转换说明(conversion specification)。
下面表4.3,列出了 ANSI C 标准为 printf() 提供的各种转换说明:

转换说明 输出
%c 一个字符
%d 和 %i 有符号的十进制整数
%u 无符号的十进制整数
%f 浮点数、十进制计数法
%e 和 %E 浮点数 e- (E-)计数法
%a 和 %A 浮点数、十六进制数字和p-(P-)计数法 (C99)
%g 和 %G 根据数值不同自动选择 %f 或%e(%E)。%e 格式在指数个数小于-4或大于等于精度时使用
%o 无符号的八进制整数
%x 和 %X 使用十六进制数字 0f(0F)的无符号十六进制整数
%p 指针
%s 字符串
%% 打印一个百分号
#include

#define PI 3.141593

int main(void)
{
    int number = 3;
    float expresso = 13.5;
    int cost = 3100;
    
    printf("The %d CEOs drank %f cups of expresso. \n", number, 
    expresso);
    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 3 CEOs drank 13.500000 cups of expresso.
The value of pi is 3.141593.
Farewell! thou art too dear for my possessing,
$6200

printf() 的使用格式
printf(Control-string, item1,item2, ...);

  • item1,item2 等等都是要打印的项目。可以是变量,也可以是常量,甚至可以是在打印之前进行计算的表达式。
  • 控制字符串(control-string)是一个描述项目如何打印的字符串。为每个要打印的项目包含一个转换说明符。
Learning C Primer Plus | Chapter4_第2张图片

打印项目必须和转换说明两者数目保持一致。

printf() 的转换说明修饰符

可以在 % 和定义转换字符之间通过插入修饰符对基本的转换说明加以修改

表 4.4 printf()修饰符

修饰符 含义
标志 五种标志(-、+、空格、# 和 0)可以使用零个或者多个标志。示例:"%-10d"
digit(s) 字段宽度的最小值,如果无法容纳要打印的数或字符串,系统会自动使用更宽的字段。示例:"%4d"
.digit(s) 精度。对于%e、%E 和 %f 有转换,表示小数点的右边打印的数字位数。 对于 %g 和 %G 转换,是有效数字的最大位数。对于 %s 转换,是将要打印的字符的最大数目。对于整数转换,是将要打印的数字的最小位数;如果必要,要使用前导零来达到这个位数。只需使用.表示其后跟随一个零,所以%.f%.0f相同。 示例: "%5.2"打印一个浮点数,它的字段宽度为 5 个字符,小数点后有两个数字。
h 和整数转换说明符一起使用,表示一个 short int 或者 unsigned short int 类型的值。 示例:"%hu""%hd"
hh 和整数说明符一起使用,表示 signed char 或 unsigned char 类型的值。示例:"%hhu""%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 类型的值(C99)。示例:"%lld""%8llu"
L 和浮点数转换说明一起使用,表示 long double 类型的值。示例:"%Ld""%10.4Le"
t 和整数转换说明一起使用,表示 ptrdiff_t 类型的值。ptrdiff_t 是两个指针差值的类型(C99) ...........................示例:"%ld""%8lu"
z 和整数转换说明一起使用,表示 size_t 类型的值。size_t 是 sizeof 返回类型(C99)示例:"%zd""%12zd"

表 4.5 printf() 的标志

修饰符 意义
-(负号) 打印的项目是左对齐的,会在字段的左侧开始打印。 示例:"%-20s"
+(加号) 打印出来是带正负符号的。示例:"%+6.2f"
(空格) 如果显示的值为正则带一个空格,若为负,则带减号表示。 示例:"% 6.2f"
#(井号) 转换说明的可选形式,若为 %o 格式,则以 0 开始;若为 %x 或者 %X 格式,则以 0x0X 开始;对于所有的浮点形式,# 保证了即使后面没有任何数字,也打印一个小数点字符。对于 %g 和 %G 格式,# 防止结果后面的 0 被删除。示例:"%#o""%#8.0f"
0 (零)....... 对于数值格式,用前导 0 代替空格填充字段宽度,对于整数格式如果出现 -(负)标记或指定精度,则忽略此标记。

浮点型参数的转换
有用于打印浮点类型 doublelong double 的转换说明符,但没有用于 float 的说明符。原因是 K&C Cfloat 值在被用于表达式中或被用作参数之前,会被自动转换为 double 类型。一般情况下,ASCII C 不会自动把 float 转换为 double。不过,为了兼容大量会自动转换的程序,printf() 和其他任何不使用显式原型的 C 函数的所有 float 参数仍然会自动被转换成 double 。因此都无需专门的转换说明符来显示 float 类型。

整数格式的示例程序

#include

#define PAGES 931
int main(void)
{
    printf("*%d*\n", PAGES); 
    printf("*%2d*\n", PAGES);                // 宽度不足自动扩展
    printf("*%10d*\n", PAGES);
    printf("*%-10d*\n", PAGES);             // 左对齐
    return 0;
}

程序的输出结果如下:
*931*
*931*
*       931*
*931       *

浮点格式的示例程序

#include

int main(void)
{
    const double RENT = 3852.99;
    
    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+003*
*3852.99*
*3853.0*
*  3852.990*
*3.853e+003*
*+3852.99*
*0003852.99*

一般情况下小数点右边会有 6 位有效数字,除非对小数点的位数进行限制。第四个和第六个进行的四舍五入。最后 +标志使得记过带有它的代数符号。0 标志产生前导零并填充整个字段。

字符串的示例程序

#include

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

格式说明符中的 .5 说明只打印 5 个字符。

格式标志的示例程序

#include

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

空格当值为正时会产生,当为负值时会有负号。 小数点后面是最小数字位数,当位数不够时会产生前导零来填充。

转换说明的意义

转换说明实际上就是翻译,比如存储在计算机中的二进制格式的数值 01001100%d转换符就会将其转换为字符 7 和 6 并显示成 76。%x转换则把它转换为十六进制表示法 4c。%c 转换符会把该值变为字符表示 L 。

类型说明符不匹配的情况
#include

#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 的补码(two‘s complement)的方法来表示有符号整数,0 ~ 32767 代表它们本身,32768 ~ 65535 表示负数。65535 表示 -1,65534 表示 -2 以此类推 65536-336 = 65200(在有符号数中表示 -336) 即为结果。
  • 第三行输出,将 num 变量当作字符串输出,然而 336 超过了 char 类型存放的大小(一个字节),这个时候就会发生截断,相当于用 256 除以一个整数并取余。余数为 80 也就是字符 P 的 ASCII 码值。
    Learning C Primer Plus | Chapter4_第3张图片
    把 336 转换成字符
  • 最后打印一个打印的数值同上也进行的截断,只使用最后两个字节,相当于被 65536 除后的余数,在这里余数是 82,对应字符是 R。

当混淆了整数和浮点类型的时候的结果

#include

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+000 3.0e+000 9.9e-315 6.1e-315
2000000000 1234567890
0 0 2000000000 1234567890

第一行打印出前两个相符类型的值,而当使用 %e 打印 long 类型时,函数期望一个 double 类型值(8个字节)但是 n3 的类型只有 4 个字节,所以它也查看邻近 4 个字节的值,并把这 8 个字节的内存存放的值解释成 浮点数。结果是无意义的。
第二行打印证明了类型符正确时可以打印出 n3 和 n4 。
第三行打印输出前面两个是浮点类型的,与类型符不符所以打出了错误的结果零。

原因可能是浮点类型占 8 个字节,然后赋值的数比较小,所以截取了高字节是全零的,于是解释出来的也是零。

参数传递
原书中第三行打印出来的结果是:

0    1074266112    0    1074266112

关于这一段书中的解释是:


Learning C Primer Plus | Chapter4_第4张图片

Learning C Primer Plus | Chapter4_第5张图片
参数传递

所以这里不同的原因可能是函数里面使用了不只是简单的顺序读取,而是通过读取首地址的方法去确定读取内存区域的开始 (具体还未考证)。

pintf() 的返回值

printf() 的返回值是它打印的字符的数目。如果有输出错误,那么 printf() 会返回一个负数。这是个附加功能,通常很少被用到,一般用于检测输出错误。

#include

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.

返回的字符包括空格和不可见的换行字符。
不支持在引号括起来的字符串中间断行。

分割一个字符串的三个选择
#include

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 newest way to print a"
    "long string.\n"); // ANSI C
    return 0;
}

程序的输出如下:
Here's one way to print a long string.
Here's another way to print a long string.
Here's newest way to print along string.
  1. 第一种方法不是用 \n 结束,可以使用多个 printf() 函数输出一个长的字符串。
  2. 第二种方法使用了 \ (反斜线)和回车键分割一个字符串,缺点是第二行缩进也会变成字符串的一部分。
  3. 第三种方法是 ANSI C 的新方法。如果两个双引号引起来的字符串之间仅用空格字符分隔,那么 C 会把该组合当一个字符串处理。

使用 scanf()

scanf() 的工作就是把输入的字符串转换成各种形式:整数、浮点数、字符和C 的字符串。它是 printf() 的逆操作。
printf() 一样,scanf() 使用控制字符串和参数列表。控制字符串指出输入将被转换成的格式。
主要区别在于参数列表。printf() 函数使用变量名、常量和表达式;而 scanf() 函数则使用指向函数的指针所以有以下两点需要注意:

  • 当使用 scanf() 来读取某种基本类型的值时,需要在变量名前面加上 &(取地址符)。
  • 当使用 scanf() 把一个字符串读进一个字符数组时,不需要使用 &(字符串名本身就是地址)。

scanf() 函数使用空格(换行、制表符合空格)来决定这样把输入分为几个字段。它依次把转换说明与字段相匹配,并跳过它们之间的空格。可以分一行或者 5 行输入,只要在每个输入项目之间至少键入一个换行符、空格或制表符。例外的情况就是 %c 它不会跳过空白字符。

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

表 4.6 列出了 C99 标准中描述的主要转换说明符

转换说明符 意义
%c 把输入解释成字符
%d,%i 把输入解释成有符号十进制整数
%u 把输入解释成一个无符号十进制整数
%e,%f,%g,%a 把输入解释成一个浮点数(%a 是 C99 标准)
%E,%F,%G,%A 把输入解释成一个浮点数(%A 是 C99 标准)
%o 把输入解释成有符号八进制数
%x,%X 把输入解释成有符号十六进制数
%p 把输入解释成一个指针(一个地址)
%s 把输入解释成一个字符串;输入的内容以第一个非空字符开始到下一个空字符结束

也可以在表 4.6 所示的转换说明符中使用修饰符。修饰符出现在百分号和转换字符之间。如果一个说明符内使用多个修饰符,那么它们出现的顺序应该与在表 4.7 中出现的顺序相同。

修饰符 意义
* (星号) 滞后符号(具体后面说明)示例:"%*d"
digit(s)(数字) 最大自动宽度;在达到最大宽度或遇到第一个空白字符时停止对输入的读取 示例:"%10s"
hh 把整数读作 signed char 或 unsigned char 示例:"%hhd""%hhu"
ll 把整数读作 long long 或 unsigned long long(C99)示例:"%lld""%llu"
h,l 或 L "%hd""%hi" 指示该值会存储在一个 short int 中。"%ho""%hx""%hu" 指示该值会存储在一个 unsigned short int 中。"%ld""%li" 指示该值会存储在一个 long 中。"%lo""%lx""%lu"指示该值会存储在一个 unsigned long 中。"%le""%lf""%lg" 指示该值以 double 类型存储。将 L(而非 l)与 e、f 和 g 一起使用指示该值以 long double 存储。如果没有这些修饰符,d、i、o 和 x 指示 int 类型,而 e、f 和 g 指示 float 类型。
从 scanf()的角度看输入

这一部分太多了 水一水贴个图 但是内容是很重要的。


Learning C Primer Plus | Chapter4_第6张图片
格式字符串中的常规字符

scanf() 函数允许您把普通字符放在格式字符串中。除了空格字符之外的普通字符一定要与输入的字符串准确匹配。例如:

scanf("%d,%d", &a, &b);

不是原本的 :12 88
而应该输入:12,88 
或者 88 ,  121 
或者 
88,
121

scanf() 函数会将其解释成,您将键入一个数字,键入一个逗号,然后再键入一个数字(当然也会跳过空白字符)。

对于 %c 来说,如果在格式字符串中 %c之前有一个空格,那么 scanf() 就会跳到第一个非空字符串处。也就是说,命令 scanf("%c", &ch) 读取在输入中遇到的第一个字符,命令 scanf(" %c", &ch) 读取在输入中遇到的第一个非空白字符。

scanf() 的返回值
  • scanf() 函数返回成功读取的项目的个数
  • 如果没有读取任何项目则返回零。
  • 检测到 “文件结尾(end of file)” 时,它返回 EOF(EOF 是文件 stdio.h 中定义的特殊值。一般被定义为 -1)

printf()scanf() 的 * (星号)修饰符

对于 printf() 来说,* 用于不想事先指定字段宽度,而是希望由程序来指定该值。您必须使用一个参数来告诉函数字段宽度。例如:printf("%*d", width, number); 其中需要输入 width 来指定宽度,而 number 才是要打印的数字。
也可以是 printf("%*.*f", width, precision, number); 这样的语句来指定浮点数的宽度和精度。

对于 scanf()来说,* 是当把它放在 % 和说明符字母之间时,用于跳过相应的输入项目。示例:

`scanf("%*d %*d %d", &val)`
有如下输入:
2004 2005 2006
程序会跳过第一第二个输入,把第三个输入读入变量 `val` 里面。

如果程序需要读取一个文件中某个特定的列,那么该功能将非常有用。

printf() 的用法提示

`printf("%9d %9d %9d", val1, val2, val3);`

使用一个较大的宽度来使输出整齐排列

`printf("**%.2f**", distance);`
输出结果可能是: ** 10.22 **
如果改成
`printf("**%10.2f**", distance);`
输出结果可能是: **       10.22 **

一个关键的概念:
假设有这样一个值 -13.45e12# 0

  • 使用 %d 模式,会读取三个字符(-13)并在小数点处停止,将小数点作为下一个输入字符。
  • 使用 %f 模式,它将会读取字符 -13.45e12 并在 # 号处停止,将它作为下一个输入字符。
  • 使用 %s 模式,他会读取字符 -13.45e12# 并在空格处停止,将空格作为下一个输入字符。
  • 使用 %c 模式,它会存储读取的第一个字符,在这里是一个空格。

你可能感兴趣的:(Learning C Primer Plus | Chapter4)