分享

周立功:数据的输入输出:printf、scanf、预处理器指令

 甲基丁酸 2017-07-09


分享电子工程相关技术文档:设计技巧,应用笔记,参考设计,设计方案

经周立功教授授权,特对本书内容进行连载。第一章为程序设计基础,本文为1.2.3节数据的输入输出。

小编注:在开始阅读前,先来个小测试

以下语句执行后的输出结果是()

#define MOD(x, y)  x%y

int a = 13, b = 94;

printf('%d\n', MOD(b, a+4));

(a)5  (b)7   (c)9 (d)11


先记住你的答案,然后往下看。


周立功


1.  printf()函数

printf()函数可以传递一个参数列表,它被看成由“控制字符串和其它参数”两部分组成。

这里的控制字符串是个字符串,其中可能包含转换格式符。转换格式是以一个%字符开头,且以一个转换字符结尾。比如,在转换格式符%d中,d是转换字符,将一个整型表达式的值打印成十进制整数的形式。为了在屏幕上打印字符串,可以使用下面的语句。比如:

printf('abc');

实现这个功能的另一种用法是使用下面这条语句。比如:

printf('%s', 'abc');

转换格式符%s使参数'abc'被打印成“字符串”的形式,还有一种方法也可以实现相同的目的。比如:

 printf('%c%c%c', 'a','b', 'c');

其中的单引号表示字符常量,'a'就是与小写字母a对应的字符常量,转换格式符%c将表达式打印成“字符”的形式。

当一个参数被打印时,它的打印位置被称为“字段”,这个字段的字符数量被称为字段宽度,字段宽度可以在格式符%和转换字符之间指定。比如,%8c中的8,表示输出的字符少于8个,则用空格填充,目的是为了对齐。比如:

printf('%c%3c%5c\n','a', 'b', 'c');

其打印输出形式为:

a  b   c

格式转换符%后面是小写字母x,与&iNum1相对应,将输入流中的字符解释为16进制整数,并将结果存储在&iNum1中。然后通过printf()查看地址的实际值,应该说,这不失为理解指针的一种非常简单而有效的方式。比如:

printf('&iNum指针的值是%x\n', &iNum);

printf('iNum变量的值是%x\n', iNum);

其它的转换格式符还有,%e将表达式打印成用科学计数法表示的浮点数的形式,%f将表达式打印成浮点数的形式,%g将表达式打印成e格式或f格式中更短的那种的形式。


2.  scanf()函数

当用户使用键盘将一些值输入到程序中时,会键入一串字符,这串字符就构成了输入流,由程序所接收。如果用户键入1234,用户可能将它当成一个十进制整数,但程序在接收时却将它当成一串字符。scanf()函数可以将一串十进制数字组成的字符串解释成一个整数值,并将它存储在内存中的一个适当位置。

scanf()的第一个参数是个控制字符串,其格式对应于输入流的字符的解释方式,这个函数的其它参数都是地址。比如:

scanf('%d',&a);

格式符%d与表达式&a相对应,它将输入流中的字符解释为10进制整数,并将结果存储在a的地址中。   表达式&a理解为“a的地址”,因为&是取地址操作符。类似地:

scanf('%x', &a);

格式符%x与表达式&a相对应,它将输入流中的字符解释为16进制整数,并将结果存储在a的地址中。除了%c、%s和%f(float浮点数)转换格式符之外,还有lf或LF(lf表示long float,即长浮点型)转换格式符,它将输入流中的字符解释为double类型浮点数。


在实际的应用中,用户不一定会按照程序的指令行事。当用户的输入与程序期望的输入不匹配时,将会导致程序运行失败。因此需要事先预料一些可能的输入错误,这样才能编出能检测并处理这些问题的程序。

假设编写一个处理非负数整数的循环,但用户很可能输入一个负数,则可以用关系表达式排除可能出现的错误。比如:

long n;

scanf('%1d', &n);                                                                //获取第1个值

while(n >= 0)                                                                       //检测不在范围内的值

{

         // 处理n

         scanf('%1d', &n);                                                      // 获取下一个值

}

另一个潜在的问题是,用户可能输入错误类型的值,比如,字符q。排除这种情况的一种方法是检查scanf()的返回值。回忆一下,scanf()返回成功读取项的个数,因此下面的表达式但且仅当用户输入一个整数时才为真。比如:

scanf('%1d', &n)== 1

结合上面的while循环,循环条件可以描述为“当输入是一个整数且整数为正时”,可以改进为:

long n;

while(scanf('%1d',&n) == 1) && n >= 0){

         // 处理n

}


3.  预处理器指令

如果要将程序转化为机器可执行的形式,对于C程序来说,通常包含以下三个步骤:

  • 预处理:首先程序会被送交给预处理器。预处理器执行以#开头的指令,预处理器有点类型于编辑器,它可以给程序添加内容,也可以修改程序;

  • 编译:编译器会将程序翻译成机器指令,然后这个程序还不能运行;

  • 链接:连接器就是将由编译器产生的目标代码和所需要的其它附加代码整合在一起,产生最终完全可执行的程序,其中的附加代码包括程序中的库函数。


由于预处理器与编译器集成在一起,因此人们可能不会注意它在工作。为了便于查看变量的地址和变量的值的存储关系,在这里引入预定义的宏替换。预处理器的行为是由指令来控制的,它是以#符号开头的源文件行,分别为宏定义(即#define)、文件包含(即#inlcue)与条件编译(即#if、#ifdef、#ifndef、#elif、#else与#endif),而不包含预处理命令的行称为源程序文本行。


在C语言中预定义了一些宏,这些宏的名字都是以两个下划线字符开始和结束的。其中的“_FUNCTION_”表示当前所在的函数名,它实际上是一个代码块作用域变量,而不是一个宏,它提供了外层函数的名称,用于程序调试和异常信息报告。

(1)带参数的宏

带参数的宏定义的格式如下:

#define 标识符(x1, x2, …, xn) 替换列表

其中,x1…xn是宏的参数,宏命令总是在第一个换行符处结束,如果要在下一行继续宏命令,则必须在当前行的末尾使用“\”续行符,即将下一行看作本行的继续。

假设将宏定义为下面的形式:

#define mult(x, y)  (x) *(y)

根据这个定义4 / mult(2, 2)被展开为“4 / (2)*(2)”,由于优先级的关系,它并不等同于表达式“4 / ((2)*(2))”。注意,标识符与左边的括号之间不能有空格,否则与预想的结果千差万别。比如:

#define SQ (x)  ((x)*(x))

SQ(7)中的SQ为(x)  ((x)*(x)),SQ(7)展开后为(x)  ((x)*(x)) (7)。

同时也不能用分号来结束#define定义,否则分号就会成为替换字符串的一部分。比如:

#define SQ(x) ((x)*(x));

则“x = SQ(y);”被替换为“x= ((y)*(y));;”。如果有以下定义:

if(x == 2){

         x= SQ(y);

}else{

         ++x;

}

此时,这个额外的分号分离了if和else语句,从而产生语法错误。

带参数宏的展开过程是招聘软件工程师经常考察的经典试题,比如,以下语句执行后的输出结果是(b)

#define MOD(x, y)  x%y

int a = 13, b = 94;

printf('%d\n', MOD(b, a+4));

(a)5    (b)7    (c)9       (d)11

解题思路:MOD(b, a+4)即就是MOD(94,13 + 4),展开后相当于“94%13+4”。由于操作符的优先级,它并不等同于(94)%(13 +4)。其本意是“94%17”,因此必须用括号将“x%y”中的x、y单独括起来。即:

#define MOD(x, y)  (x) % (y)


(2)#运算符

#运算符将一个带宏的参数转换为字符串常量,它仅允许出现在带参数的宏的替换列表中。假设在调试过程中使用PRINT_INT宏作为一个便捷的方法输出一个整型变量或表达式的值,#运算符可以使PRINT_INT为每个输出的值添加标签。即:

#define PRINT_INT(i) printf(#i '=%d\n', i)

即i之前的#运算符通知预处理器根据PRINT_INT的参数创建一个字符串常量,因此调用“PRINT_INT(m/n);”等价于“printf('m/n' '=%d\n', m/n);”。由于相邻的字符串会被合并,因此上面的语句等价于

printf('m/n= %d\n', m/n);

当执行printf()函数时,则同时显示表达式m/n和它的值。比如,当m = 13、n = 5时,则输出为“m/n = 2”。因此可以用宏替换程序清单 1.3中相关的语句的代码:

#definePRINT_INT(i)                  \

printf('%8s():&%-5s = 0x%-6x,%-5s = 0x%-6x\n', __FUNCTION__, #i, &(i), #i, i);


编译器在编译程序时,会将“编译器预定义的宏'__FUNCTION__'”替换为代码所在的函数名。“%8s():”中“%”后面的“8”表示,如果输出的字符少于8个,则用空格填充,目的是便于对齐,“s”表示输出字符串"main",即printf()函数输出“main():”。


“&%-5s= 0x%-6x,”表示首先输出第1个字符'&',“-”表示输出左对齐,右边填充空格,“5”为输出的宽度,接着输出第2个字符'=',然后输出第3个字符“0x”,从第4个字符开始输出变量i的地址&i,“6”后面的“x”表示输出16进制数,详见程序清单 1.7。


程序清单 1.7  变量交换范例程序(用带参数的宏替换)

1       #include

2       #define PRINT_INT(i)                \

3       printf('%8s():&%-5s = 0x%-6x,%-5s = 0x%-6x\n', __FUNCTION__, #i, &(i), #i, i);

4

5       int main(int argc, char *argv[])

6       {

7                intiNum1, iNum2, temp;

8

9                scanf('%x%x',&iNum1, &iNum2);

10              PRINT_INT(iNum1);   PRINT_INT(iNum2);

11               temp = iNum1;  iNum1 = iNum2;  iNum2 = temp;

12              PRINT_INT(iNum1);   PRINT_INT(iNum2);

13               return 0;

14     }


特别声明,本书使用的是windows系统下的GNU开发环境MinGW,而gcc是一种广泛使用的C语言编译器,使用gccfor MinGW编译运行的结果详见图1.6。


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多