第十四章 若干深入问题和C独有的特性 作业: 函数指针 函数作参数 函数副作用 运算 语句 位段 存储类别 编译预处理

Slides:



Advertisements
Similar presentations
C++语言程序设计教程 第5章 构造数据类型 第6章 C++程序的结构.
Advertisements

C语言程序设计 主讲教师 :张群燕 电话:
Loops.
第一章 C语言概述 计算机公共教学部.
编译原理上机实习
C语言程序设计 第八章 函数.
C语言程序设计 第十二章 位运算.
第一章 程序设计入门.
第5章 函数与模块化设计 学习目的与要求: 掌握函数的定义及调用方法 理解并掌握参数的传递方法 理解函数的嵌套与递归调用
C语言程序设计 第五章 选择结构程序设计.
高级语言程序设计 主讲人:陈玉华.
第5章 函数与预处理 《 C语言程序设计》 (Visual C++ 6.0环境) 本章导读
由C程序结构所知,一个完整的C语言程序是由一个且只能有一个main()函数(又称主函数)和若干个其他函数组合而成的。而前面各章仅学习main()函数的编程,本章将介绍其他函数的编程,包括其他函数的定义、调用、参数传递及变量的作用域等。
主讲教师:吴琼 微信群:C语言2016 QQ群: 密码scu2016 昵称:“真名+学号”
循环结构又称为重复结构:用来处理需要重复处理的问题,它是程序中一种很重要的结构。
第二章 C# 基础知识.
第3章 顺序结构程序设计 本章要点: 格式化输出函数──printf() 格式输入函数——scanf() 字符输出函数——putchar()
C++程序设计 第二讲 清华大学软件学院.
Chap 10 函数与程序结构 10.1 函数的组织 10.2 递归函数 10.3 宏定义 10.4 编译预处理.
C程序设计.
第五章 选择结构程序设计 一、关系运算符和表达式 1、关系运算符 在程序中经常需要比较两个量的大小关系, 以决定程序下一步
第七章 函数 目录 有参的加法函数的开发 函数定义的一般形式 函数参数和函数的值 函数的调用
计算概论 第十八讲 C语言高级编程 结构与习题课 北京大学信息学院.
授课老师:龚涛 信息科学与技术学院 2018年3月 教材: 《Visual C++程序员成长攻略》 《C++ Builder程序员成长攻略》
Function.
Object-Oriented Programming in C++ 第一章 C++的初步知识
第三章 C++中的C 面向对象程序设计(C++).
2 C++ 的基本語法和使用環境 親自撰寫和執行程式是學好程式語言的不二法門。本章藉由兩個簡單的程式,介紹C++ 程式的基本結構和開發環境,讓初學者能逐漸建立使用C++ 的信心。
程序设计专题一 结构化程序设计与递归函数 主讲教师: 刘新国.
第八章 函数.
第5章 堆疊(Stacks) 5-1 堆疊的基礎 5-2 堆疊的表示法 5-3 堆疊的應用 - 運算式的計算與轉換
第7章 编译预处理 本章要求: 本章重点: 本章难点: 掌握用#define定义无参数宏和带有参数宏定义和调用方法;
Introduction to the C Programming Language
C语言 程序设计基础与试验 刘新国、2012年秋.
期中考试成绩分布 《程序设计》-2017年秋.
C++语言程序设计 第二章 C++简单程序设计.
2.1 C语言的数据类型 2.2 常量与变量 2.3 变量赋初值 2.4 各类数值型数据间的混合运算 2.5 C语言的运算符和表达式
第5讲 结构化程序设计(Part II) 周水庚 2018年10月11日.
第七章 函数及变量存贮类型 7.1 函数基础与C程序结构 7.2 函数的定义和声明 7.3 函数的调用 7.4 函数的嵌套与递归
授课老师:龚涛 信息科学与技术学院 2016年3月 教材:《Visual C++程序员成长攻略》 《C++ Builder程序员成长攻略》
第九章 预处理命令.
第1章 概述 本章要点: C语言程序结构和特点 C语言程序的基本符号与关键字 C语言程序的编辑及运行 学习方法建议:
C语言概述 第一章.
第1讲 C语言基础 要求: (1) C程序的组成 (2) C语言的标识符是如何定义的。 (3) C语言有哪些基本数据类型?各种基本数
C语言大学实用教程 第5章 函数与程序结构 西南财经大学经济信息工程学院 刘家芬
程式結構&語法.
第 二 章 数据类型、运算符与表达式.
Chap 5 函数 5.1 计算圆柱体积 5.2 使用函数编写程序 5.3 变量与函数.
Chap 5 函数 5.1 计算圆柱体积 5.2 数字金字塔 5.3 复数运算.
程式的時間與空間 Time and Space in Programming
7.1 C程序的结构 7.2 作用域和作用域规则 7.3 存储属性和生存期 7.4 变量的初始化
C程序设计.
C++语言程序设计教程 第2章 数据类型与表达式 第2章 数据类型与表达式 制作人:杨进才 沈显君.
<编程达人入门课程> 本节内容 为什么要使用变量? 视频提供:昆山爱达人信息技术有限公司 官网地址: 联系QQ:
第2章 数据类型、运算符与表达式 本章要点: 基本数据类型 常量和变量 算术运算符和算术表达式 关系运算符和关系表达式
第7章 程序的结构 四、生存期与存储属性 五、extern关键字与外部连接属性 六、static关键字与内部连接属性.
第二章 类型、对象、运算符和表达式.
Introduction to the C Programming Language
#include <iostream.h>
第五章 逻辑运算和判断选取控制 §5.1 关系运算符和关系表达式
第1章 数据结构基础概论 本章主要介绍以下内容 数据结构研究的主要内容 数据结构中涉及的基本概念 算法的概念、描述方法以及评价标准.
单片机应用技术 (C语言版) 第4章 C51程序设计入门
PPT注意事项: 当前PPT课件文件必须和提供的源代码文件夹“代码”在同一目录中即不要移动文件夹“代码”的默认位置。
第二章 数据类型、运算符和表达式 §2.1 数据与数据类型 §2.2 常量、变量和标准函数 §2.3 基本运算符及其表达式 目 录 上一章
第18讲 从C到C++ 计算机与通信工程学院.
第2章 Java语言基础.
基本資料型態 變數與常數 運算子 基本的資料處理 授課:ANT 日期:2014/03/03.
Chap 10 函数与程序结构 10.1 圆形体积计算器 10.2 汉诺塔问题 10.3 长度单位转换 10.4 大程序构成.
第三章 流程控制 程序的运行流程 选择结构语句 循环结构语句 主讲:李祥 时间:2015年10月.
第二章 Java基础语法 北京传智播客教育
Presentation transcript:

第十四章 若干深入问题和C独有的特性 作业: 14.6 14.7 函数指针 函数作参数 函数副作用 运算 语句 位段 存储类别 编译预处理 本章小结 作业: 14.6 14.7

§14.1 不定方向的数组排序——函数指针

【例14-1】编函数,对给定整数数组排序,递增或递减按给定参数决定。 bool ascending(int a, int b){ return a>b; } /* 具体说明函数 */ bool descending(int a, int b){ return a<b; } void swap(int *a , int *b ) { int temp; temp = *a; *a = *b; *b = temp; } void sort ( int a[ ], int n, char *flag) { bool (*ad) (int,int); /* 函数指针 */ int pass,c; if (flag=="ascending")/* 根据排序方向给函数指针赋值具体函数 */ ad = ascending ; else ad = &descending ; /* 函数名前加“&”和不加“&”意义相同 */ for ( pass=0; pass<n; pass++) /* 冒泡排序 */ for ( c=0; c<n-1; c++) if ( (*ad)(a[c],a[c+1]) ) /* 比较,此处使用函数指针 */ swap(&(a[c]),&(a[c+1]) ); /* 交换 */ 当调用sort 时,sort 根据 flag 的值是否“ascending”,决定按递增或递减排序,把函数名 ascending 或 descending 赋值给函数指针变量 ad 。通过调用 ad 所指向的函数,判断是否需要交换数组两个相邻成分。当需要交换时,调用函数 swap。经过多次扫描,最终达到排序目的。 函数 sort 使用了指向函数的指针调用函数 ascending 或 descending 。 在本例题中, 函数sort有三个参数,a是被排序的数组,n是数组长度,flag是递增或递减标记; 函数swap交换两个变量的值; 函数ascending判断是否a>b ; 函数descending判断是否a<b 。

在数组与指针一节中曾指出数组名表示数组首地址,若将数组名赋值给一个类型兼容的指针变量,那么这个指针变量也指向这个数组。同样函数名也具有上述相同的特性,即 函数名表示函数控制块的首地址,函数控制块中包括函数入口地址等信息。 如果用一个指针变量来标识函数控制块的首地址,则称这个指针变量为指向函数的指针变量,简称指向函数的指针、函数指针。函数指针声明形式是: 类型符 (*标识符)(形式参数表); 其中 标识符是被声明的“指向函数的指针变量”名; 类型符给出函数指针变量所指向函数的类型信息; 形式参数表给出函数指针变量所指向的函数的形式参数信息。 例如 int (*f) ( float d , char c ); 声明指向“返回int类型值的函数”的函数指针变量 f ,f 所指向的函数有两个形式参数: 第一个参数是float类型, 第二个参数是char类型。

函数指针声明中用括号把星号“. ”和“标识符”括起来是必须的,比如上述的“( 函数指针声明中用括号把星号“*”和“标识符”括起来是必须的,比如上述的“(*f)”,原因是由优先级造成的。引进函数指针概念不是凭空臆造的,它的作用在于 使用函数指针调用函数 实现其它程序设计语言中函数参数的功能 可以把函数名赋值给一个函数指针变量,然后通过函数指针变量调用函数。形式是: 函数指针变量 = 函数名; 要求函数指针的特性与函数名的特性一致,这种一致性体现在 它们的返回类型相同; 它们的参数个数相同; 对应位置上,每个形式参数的类型相同。

使用函数指针要注意: 给函数指针赋值时,右端只是一个函数名,不许带参数表。 ad = ascending 是正确的,而 ad = ascending(int,int) 是错误的。 不能对函数指针变量进行任何运算。“ad+n”、“ad++”、“ad--” 等是错误的。 利用函数指针调用函数时,把 “*” 和函数名用括号括起来,成 (*函数名)( … ) 形式,是必须的。因为“()”的优先级高于“*”。在例13-3中,调用函数指针 ad 所指函数的形式是 (*ad)(a[c],a[c+1]) 不能写成 *ad (a[c],a[c+1])

函数指针声明中用括号把星号“*”和“标识符”括起来是必须的,比如上述的 (*f) 原因是由优先级造成的。 引进函数指针概念不是凭空臆造的,它的作用在于 使用函数指针调用函数 实现其它程序设计语言中函数参数的功能 本节先介绍利用函数指针调用函数,下一节介绍函数参数。 可以把函数名赋值给一个函数指针变量,然后通过函数指针变量调用函数。形式是: 函数指针变量 = 函数名; 这个赋值要求“函数指针的特性与函数名的特性一致”,这种一致性体现在 它们的返回类型相同; 它们的参数个数相同; 对应位置上,每个形式参数的类型相同。

§14.2 计算定积分—函数作参数 ò ò dx e x six + f x dx ( ) x six x §14.2 计算定积分—函数作参数 一个函数可以调用其它函数,这是大家熟知的事实。有时遇到这种情况,在一个函数P内,要调用另一个函数,但到底调用哪一个函数要到执行函数P才能确定。 【例14.2】编程序,用梯形公式计算并打印定积分 解:最好能有一个计算定积分的函数 integrate 能够计算任意函数 f 在区间 [a,b] 上的的定积分,然后分别以函数 等为参数,调用函数 integrate。 dx e x six ò + - 2 3 1 f x dx a b ( ) ò 3 x 2 six x

我们希望 integrate 能计算任意函数 f ,在任意区间 [a,b] 上的定积分。而具体计算那个函数的积分由调用 integrate 时确定。显然 f 作为integrate 的一个参数比较合适。调用 inegrate 的函数调用可以写成: integrate(g,a,b) 其中 g 为被积分函数。这就要求函数 integrate 带有函数参数,integrate 的函数定义说明符可以写成: float integrate( float (*f)(float) ,float a, float b ) 函数作参数时,在形式参数表中应列出作参数函数的函数原型。目的是为了说明该形式参数函数的特性。

ò 下边继续开发函数 integrate 。设 ; 其中 h=(b-a)/n 。计算函数 f 在区间 [a,b] 上定积分的梯形公式是: 求定积分的函数如下: float integrate ( float (*f)(float) /* f 为被积分函数 */ ,float a,float b /* a , b 分别为积分区间下、上界 */ ,int n ) { /* n 为积分区间分割个数 */ float h,s; int i; h=(b-a)/n ; s=( (*f)(a) + (*f)(b) ) / 2.0 ; for ( i=1; i<=n-1; i++ ) s=s+(*f)(a+i*h) ; // 调用函数“*f”,是被积分函数,由使用者决定 return s*h ; } Y f a h b n 1 2 = + ( ), , ) L S f x dx h Y a b n = » + ò - ( ) (( / 1 2 3 L

e + sin 其中,实在参数 0、1 ; -1.0、1.0 ; 0.0、2.0 另外分别编出: 分别表示积分区间 0、1 ; -1.0、1.0 ; 0.0、2.0 分别表示积分区间 [0、1] ,[-1.0、1.0] ,[0.0、2.0] 100为把积分区间等分成100份; cube、sin2、r 分别为被积函数的函数标识符。 被积函数 cube,sin2,r 都是一元函数,其参数是 float 型的,结果类型也是 float 的。 被积函数 cube,sin2,r 的函数定义说明符与 integrate 形式参数表中的函数参数说明 float (*f)(float) 是一致的。 x e + 3 2 sin 另外分别编出: 的函数说明: float cube(float x) { return x*x*x ; }; float sin2(float x) { return sin(x)*sin(x) ; float r(float x) { return sqrt(x*x*x+sqrt(exp(x))) ; 主程序如下: mian (){ printf("%f", integrate( cube , 0.0 , 1.0 , 100 ) ); printf("%f", integrate( sin2 , -1.0 , 1.0 , 100 ) ); printf("%f", integrate( r , 0.0 , 2.0 , 100 ) ); }

函数作参数就是“指向函数的指针”作参数,它的形式是: 形式参数表中函数形式参数以函数指针的函数原型形式说明。其中,函数形式参数的函数名使用指针并用括号括上,函数形式参数的形式参数表使用函数原型形式的形式参数表,形式是 类型名 (*函数形式参数的形式参数名)(函数原型形式的形式参数表) 例如:float integrate( float (*f)(float) , ... ) 函数调用中对应函数指针形式参数的实在参数直接使用函数名或函数指针 在函数内部,调用函数指针形式参数表示的函数,使用形式 (*形式参数函数名)(实在参数表) 例如: s=s+(*f)(a+i*h) 使用函数参数应该注意,函数调用中的实在参数函数与函数定义中的函数指针形式参数必须一致。这种一致性体现在: 实在参数函数和形式参数函数的返回类型一致; 实在参数函数和形式参数函数的参数个数相同; 实在参数函数和形式参数函数的相对应的每个参数类型一致。

【例14.3】用指向函数的指针作函数参数,实现例14.1同样的问题: 函数 bubsort 有三个形式参数, s 是欲排序的整数数组, Size 是 s 数组的尺寸, P 是一个bool型函数,调用 bubsor t时以 ascending 函数或者以 descending函数作实在参数对应它,具体指明是按递增排序还是按递减排序。 【例14.3】用指向函数的指针作函数参数,实现例14.1同样的问题: 编一个排序函数,该函数对给定整数数组既可以按递增排序也可以按递或递减排序。 解:程序片段如下: void bubsort( int s[], int size, bool(*p)(int,int) ) { int u , v ; void swap( int * , int * ); for ( u=0; u<size; u++ ) for ( v=0; v<size-1; v++ ) if ( (*p)(s[v],s[v+1]) ) swap(&s[v],&s[v+1]); } void swap( int *r1; int *r2 ) // 与例13-3同,略 bool ascending(int a; int b) { return a>b ; } bool descending(int a; int b) { return a<b ; }

函数 sum( ) 的第一个参数为函数指针,该指针指向的是带有一个double型参数并返回double类型数据的函数。而在头文件 math 函数 sum( ) 的第一个参数为函数指针,该指针指向的是带有一个double型参数并返回double类型数据的函数。而在头文件 math.h 中定义的函数 sin( ) 与 cos( ) 正是这样的函数。 主程序中,分别以 sin( ) 与 cos( ) 作实在参数对应 sum 函数的函数指针参数 func。达到函数 sum 分别求 sin 和 cos 之和。 【例14.4】编程序,以0.1为间隔,计算区间 [0,1] 内所有正弦函数和所有余弦函数之和。 #include <stdio.h> #include <math.h> double sum( double(*func)(double) /* 参数func是函数指针 */ , double d1, double d2) { double dt=0.0 ,d ; for( d=d1; d<d2; d+=0.1) dt += (*func)(d); /* 用函数指针调用函数 */ return dt; } void main(){ double s; s=sum(sin,0.1,1.0); /* 求sin函数之和 */ printf("The sum of sin for 0.1 to 1.0 is %e \n",s); s=sum(cos,0.5,3.0); /* 求cos函数之和 */ printf("The sum of cos for 0.5 to 3.0 is %e \n",s); 程序运行结果为: The sum of sin for 0.1 to 1.0 is 5.01388 The sum of cos for 0.5 to 3.0 is -2.44645

14.3 函数副作用 所谓函数副作用是指,当调用函数时,被调用函数除了返回函数值之外,还对主调用函数产生附加的影响。例如,调用函数时在被调用函数内部 修改全局量的值; 修改主调用函数中声明的变量的值(一般通过指针参数实现)。 函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。 第三章介绍表达式值的计算时曾经举过一个例子,由于双目运算的两个运算分量的计算次序不同,而带来运算结果不同,就是由函数副作用引起的。 对函数副作用的看法与对GOTO语句的看法一样,在程序设计语言界一直有分歧,有人主张保留,有人主张取消。我们认为,可以保留函数副作用,但是应该限制程序员尽量不要使用函数副作用。由于函数副作用的影响: 会使双目运算的结果依赖于两个运算分量的计算次序; 还可能使某些在数学上明显成立的事实,在程序中就不一定成立。

* 例如,在数学上乘法符合交换律, a*f(x) 与 f(x)*a 显然相等。但是在程序中,若函数f改变全局量a的值,则上述交换律就不成立。 设有函数: float f(float u) { a = a*2; return 2*u } 假定,计算时开始 a=3 , x=5 ; 3 a : 6 5 x : 5 u : * 3 10 10 6 第一运算分量 第二运算分量 而在同样条件下计算: f(x) * a 当双目运算符的运算对象从左向右计算时,计算: a * f(x) 第一步,求运算分量 a 的值,为 3 ; 第二步,求运算分量 f(x) 的值,调用函数 f,u 取 x 值为 5 ,进入 f 执行 a=a*2 a 得 6,再执行 return 2*u 得函数值为10,返回; 第三步,计算表达式值为 3*10 得 30 。 第一步,求运算分量 f(x) 的值,调用函数 f,u 取 x 值为 5,进入f执行 a=a * 2 a 得 6 ,再执行 return 2 * u 得函数值为 10 ,返回; 第二步,求运算分量a的值,为6 ; 第三步,计算表达式值为10*6得60 。 计算结果显然不一样,使乘法交换率不成立。这就是因为副作用的影响造成的,因为在函数 f 内改变了全局量 a 的值。

希望读者在编程序时,尽量不要使用带副作用的函数 若函数有指针参数,在函数分程序内修改指针参数所指变量的值,也产生函数副作用,也可以引起同样的问题。例如有函数声明: float f(int *a , float u) { *a = *a * 2; return 2*u } 该函数存在副作用,调用该函数将使用 f( &z , e ) 的形式。其中, z 是一个变量, e 是一个表达式。 该函数在计算表达式 z * f(&z,e) 时,将产生与第三章讲述过的例子相同的问题,表达式的值依赖于运算分量的计算次序; 即使计算次序固定,也同样会产生与上述相同的问题,使 z * f(&z,e) 不等于 f(&z,e) * z 希望读者在编程序时,尽量不要使用带副作用的函数

§14.4 运算 C语言的运算符非常丰富,第三章表3.1列出了所有C运算符,包括运算符的记号、运算、类别、优先级、结合关系等。常用的运算符及其意义我们已经在前面相关章节介绍过,本节介绍那些C独有的特色。

14.4.1 赋值运算 “=”可以和一些双目运算符结合,形成新的附加运算意义的赋值运算符,称为复合赋值运算符。 例如,设 x=3 则 14.4.1 赋值运算 “=”可以和一些双目运算符结合,形成新的附加运算意义的赋值运算符,称为复合赋值运算符。 复合赋值运算符包括: += -= *= /= %= <<= >>= &= ^= |= 使用这些赋值运算符的格式是: 操作数1 赋值运算符 操作数2 复合赋值运算 a op= b 与简单赋值运算 a = a op b 等价。 例如,设 x=3 则 x += x*5 ; 相当于 x = x+(x*5) ; 结果 x 中为整数 18 。

14.4.2 顺序表达式 表达式的结果类型是最右端表达式的类型, 表达式的结果值是最右端表达式的值。 例如 : 14.4.2 顺序表达式 用逗号运算符 , 分隔开的若干个表达式称为逗号表达式,又称为顺序表达式。逗号表达式按行文顺序从左向右计算各个子表达式的值。 表达式的结果类型是最右端表达式的类型, 表达式的结果值是最右端表达式的值。 例如 : j = ( x=0.5 , y=10 ,15+x ,y=(int)x+y*2 ) 将顺序的: 先计算 “x=0.5” 给 x 赋值 0.5 ,得 float 类型的 0.5; 再计算 “y=10” 给 y 赋值 10 ,得 int 类型的 10 ; 再计算 “15+x” ,得 float 类型的 15.5 ; 再计算 “y=(int)x+y*2” 给 y 赋值 20 ,得 int 类型的 20 ; 最终括号内表达式的结果值是 20 ,结果类型是整数类型,j 被赋值 20 。

14.4.3 条件表达式 C条件表达式格式是: 操作数1 ? 操作数2 : 操作数3 14.4.3 条件表达式 C条件表达式格式是: 操作数1 ? 操作数2 : 操作数3 该表达式含有两个运算符 “ ?” 和 “ :” 、三个操作数。条件表达式是三元表达式,运算符 “ ?” 和 “ :” 合并使用称为三元运算符。 条件表达式的计算步骤是: 1. 计算“操作数1 ”; 2. 若“操作数1”的值为true,则计算“操作数2” ,表达式的值为“操作数2”的值; 3. 否则“操作数1”的值为false ,计算“操作数3” ,表达式的值为“操作数3”的值。 条件表达式是右结合的,优先级别高于赋值运算符,低于二元操作符。

表达式语句 x = a ? b : c ; 相当于如下条件语句: if ( a != 0 ) x = b; else x = c; 由于右结合的特性,表达式 u=a>b?x:c>d?y:z 相当于 u=a>b?x:(c>d?y:z) 用条件语句表示如下: if (a>b) u=x; else if (c>d) u=y; else u=z;

14.4.4 位运算 C可以直接针对二进制位进行操作,这使得用它描述系统程序十分方便。位运算的所有操作数必须为整数类型,下表列出 C 的位运算符,下边分别介绍它们定义的运算。 记号 运算符 类别 结合关系 优先级 ~ 按位取反 一元 从右到左 15 << >> 左移、右移 二元 从左到右 11 & 按位与 8 ^ 按位异或 7 | 按位或 6

按位取反: 按位取反运算的格式是: ~ 操作数 该运算“~”对操作数结果值的二进制表示的每一位取反码。 【例14-5】如果 X 是一个 int 类型的整数,十六进制表示为 0XF0F0 它的二进制表示为 111100001111000 ~X 结果的二进制表示为 0000111100001111 十六进制的表示为 OX0F0F

位移运算: 位移运算的格式是: 操作数1 位移运算符 操作数2 C有两个位移运算符 “<<” 和 “>>” 。其中 操作数1 位移运算符 操作数2 C有两个位移运算符 “<<” 和 “>>” 。其中 << 为左移; >> 为右移; 操作数1 是要进行位移整数; 操作数2 指定移动的位数。 位移运算的操作是: 按运算符的要求把 “操作数1” 移动 “操作数2” 指定的位数。在进行移位运算过程中,移到边界之外的多余位放弃扔掉;另一侧产生的缺位以“0”补足。

(X<<5)>>2: 【例14.6】设变量 x 值为 0x1B4F ,计算表达式“x << 5 >>2”的值。 解:首先 x 左移 5 位,所得结果为 0x69E0,如上图所示。然后再对所得结果向右移 2 位,结果为 0x1A78 ,如下图所示。 X: 1 X<<5: X << 5 舍掉 补0 X<<5的结果: 1 (X<<5)>>2: (x<<5)>>2 舍掉 补0

按位逻辑运算: 按位逻辑运算是双目运算,它的格式是: 操作数1 运算符 操作数2 具体操作是: 首先将两个操作数都转换成二进制数; 操作数1 运算符 操作数2 具体操作是: 首先将两个操作数都转换成二进制数; 然后根据运算符的要求以二进制位为单位,按位对其进行 位与 、位异或 、 位或 运算。 位逻辑运算符以及它定义的操作 X y x & y (按位与) x ^ y (按位异或) x | y (按位或) 1

【例14.7】设整数x值为0x1B4F,y值为0x1A78,它们按位逻辑运算结果如图. 1 0x1B4F   0X1A78 x&y 0X1A48 x^y 0X0137 x|y 0X1B7F x与y的按位运算

§14.5 语句 本节深入介绍三个语句break 、continue 和 for 。 §14.5 语句 本节深入介绍三个语句break 、continue 和 for 。 break 和 continue 语句是受限制的 goto 语句,用来改变循环或分支语句的控制流程。在达到相同目的的情况下,使用 break和 continue 语句比起 goto 语句具有更好的风格和结构。但与全部用标准控制流程写出的程序相比,break 和 continue 语句的结构要差。 for循环语句读者已经很熟悉,本节延伸它。

14.5.1 break 执行break 语句使包含它的最内层while、do、for、switch语句终止执行,立即转移到所终止语句之外的程序点。在没有循环或switch语句的场合使用break是错误的。 【例14-8】迭代中使用break int x = 0; while (x < 10){ printf("Looping "); x++; if (x == 5) break; else 其他代码 } 后续代码 int x=0 ; 后续代码 x<10 F printf("Looping "); x ++ ; x==5 其他代码 T 在该程序片段中,循环将在x==5时停止,去执行后续代码,尽管循环控制当 x<10 时都执行循环体。用流程图来表示该程序片段如上图。

注意break在switch语句中的作用。如下两段代码执行的结果是不一样的,请认真体会。 switch (x) { case 1: printf(“ 1 ”); case 2: printf(“ 2 ”); case 3: printf(“ 3 ”); default: printf(“no_meaning”); } case 1: printf(“ 1 ”); break; case 2: printf(“ 2 ”); break; case 3: printf(“ 3 ”); break; switch 语句中使用 break 之一 switch 语句中使用 break 之二 1) 在左端程序中,当x==2时,打印结果为: 2 3 no_meaning 因为 switch 语句将控制转移到 case 2 处打印 2 之后,接着执行 printf(“ 3 ”) 和 printf(“ no_meaning ”)。 如果需要在每次调用 printf 之后都终止 switch 体,则要象右端一样 使用 break 语句。在右端程序片段中,当 x==2 时,执行结果只打印 出一个数字2。

14.5.2 continue continue语句终止它所在的最内层 while 、do 、for

printf("Looping %d\n",x); 【例14-9】continue语句示例 for ( x = 0 ; x < 5 ; x++){ if( x == 2) continue; else printf("Looping %d\n",x); } 该程序不可能打印”Looping 2 ” ,程序执行流程如右图. 程序执行结果: Looping 0 Looping 1 Looping 3 Looping 4 int x=0 ; x<5 F printf("Looping %d\n",x); x ++ ; x==2 T continue对循环的影响

14.5.3 for的延伸 for 语句包括 for 关键字和括在括号中的用分号分开的 3 个表达式,然后是语句。通常: 第一个表达式初始化循环控制变量; 第二个表达式测试循环是否终止; 第三个表达式更新循环控制变量; 如果使用逗号表达式,就可以书写带有多个控制变量的for语句 【例14-10】编函数,判断两个字符串str1和str2是否相等,相等则返回真,否则返回假。 bool str_equal ( const char * str1 , const char *str2 ){ char *t1, *t2; for ( t1 = str1 , t2 = str2 ; *t1 && *t2 ; t1++ ,t2++ ) if ( *t1 != *t2 ) return false; return *t1 == *t2; }

14.5.4 goto和标号 goto 语句是强制改变程序正常执行顺序的手段。但是我们事先声明: 频繁使用goto语句不是好的程序设计习惯,不符合结构化程序设计原则。 除有特殊需要外,尽量不要使用goto语句和标号。

goto 语句的意义是中断正常的程序执行顺序,转到本函数内标号标出的语句处,并从其开始继续向下执行。 9.1.1 带标号的语句 带标号的语句形式是: 标号 : 语句 9.1.2 goto 语句 goto 语句的形式是: goto 标号 其中: ● 标号就是带标号语句中用的标号,是一个标识符; ● goto 是保留字,表示转向。 goto 语句的意义是中断正常的程序执行顺序,转到本函数内标号标出的语句处,并从其开始继续向下执行。 goto 语句与带标号语句配合使用,达到改变程序正常执行顺序的目的。

【例14.11】前述第5章例5-8中迭代法求解方程根的问题,不改造流程图, 可以用如下右端的程序片段表示 x0=0.9; /* 1 */ r1:x1=f(x0); /* 2 */ if ( abs(x1-x0)<1e-5 )/* 3 */ goto r2 ; /* 4 */ x0=x1; /* 5 */ goto r1; /* 6 */ r2: ; /* 7 */ 开始 X0=初值 X1 = f(X0) X0≈X1 结束 x0=x1 T F 当第3行的条件为true时,执行第4行的goto语句,则转到标号r2标出的第7行去执行,从而结束迭代过程。 当程序执行到第6行的goto语句后,则无条件强制控制转到标号r1标出的第2行去执行,继续进行迭代。

虽然C允许使用 goto 语句转向本函数内任何语句,但是下述转向是极其不好的程序设计习惯。它使程序逻辑混乱,同时也给编译优化带来麻烦。 l从if语句外转入if语句的“then”或“else”子句之中; l在if语句的“then”或“else”子句之间转向; l从switch语句之外转入switch语句之内; l从循环语句之外转入循环语句之内; l从复合语句之外转入复合语句之内。

14.6 位段 为了适应系统程序设计的需要,通过使用位段,C 允许在结构体中把整数类型成员存储在比通常使用的空间更小的空间内。比如在微型计算机内一般把 int 类型数据存储成 2 个字节(16bit),使用位段可以把它存储在比 2 个字节少的电bit 内。例: struct s { unsigned a:4; unsigned b:5,c:7; } u ; 结构体变量 u 有三个成员 a 、b 、c 分别占用 4 bit 、5 bit 、7 bit 一共 2 个字节。U 的成员 a、b、c 称为位段。 位段通过在结构体成员名后加上冒号 “:” 和一个整数类型常量表达式来声明,整数类型常量表达式指明相应位段的宽度。

位段一般依赖于具体计算机系统,比如 计算机系统一个机器字的宽度、 计算机系统存储数据是采用 “高位存储法” 还是 “低位存储法” 等等。使用位段要注意: 位段仅允许声明为各种整数类型; 位段长度不允许超越特定计算机的自然字长; 位段占用的空间不能跨越特定计算机的地址分配边界(该边界与特定计算机的自然字长有关),出现跨越,将移到下一个机器字。 位段通常用于与具体计算机相关的程序中,因此破坏程序的可移植性。

14.7 存储类别 C语言中每个变量和函数都具有 2 个属性: 类型 存储类别 存储类别指的是数据的存储的方式。存储方式分为两大类: 静态 14.7 存储类别 C语言中每个变量和函数都具有 2 个属性: 类型 存储类别 存储类别指的是数据的存储的方式。存储方式分为两大类: 静态 动态 具体包括五种: 自动的(auto) 静态的(static) 寄存器的(register) 外部的(extern) 类型定义符(typedef)。

静态存储方式: 在程序运行期间分配固定的存储空间(在静态区)。包括: 外部存储类别 静态存储类别 动态存储方式: 在程序运行期间根据函数调用(函数被激活)和复合语句的开始执行(复合语句被激活)的需要进行动态存储分配(在栈区)。包括: 自动存储类别 寄存器存储类别 类型定义符则是用来定义类型名的。 C通过在类型符前缀以“存储类别关键字”来声明变量和函数的存储类别。

这里所说的“动态存储方式”和“静态存储方式”要和第13章动态数据结构中所讲的“动态变量”和“静态变量”区别开。 按第13章的概念,本节所有存储类别的变量全部都是静态变量,它们由系统在栈区或静态区分配存储空间;而第13章的动态变量没有显示的名字,是通过执行由程序员安排的申请空间函数(例malloc)在堆区分配存储空间,并由指针变量标识。 本节的概念是指程序中显示声明的变量的存储分配方式,表明它们是在栈区还是在静态区分配存储空间,及其分配方式。 例如: auto float x,y; 声明两个浮点类型变量 x、y,其存储类别是自动的。

14.7.1 数据在内存中的存储 库程序代码区:用于存放库函数的代码。 14.7.1 数据在内存中的存储 数据的存储类别规定了数据的存储区域,同时也说明了数据的生存期。计算机中,用于存放程序和数据的物理单元有 寄存器 内存 内存速度慢但空间大,可存放程序和数据。内存中又分为 系统区 用户程序区 数据区(包括:堆区、栈区、静态存储区) 数据区:用来存储用户程序数据,包括 堆区、栈区和静态存储区。 堆区:用于存储动态变量; 经过 malloc 申请来的动态变量经常存储在堆区。 栈区:具有先进后出特性。 用于存储程序中显式声明的自动存储方式的变量。 静态存储区:用于存储程序中显式声明的静态存储方式的变量。 系统区:用于存放系统软件,如操作系统、语言编译器。 只要计算机运行,这一部分空间就必须保留给系统软件使用。 用户程序代码区:用于存放用户程序的程序代码。 库程序代码区:用于存放库函数的代码。 内存 系统程序和数据 系 统 区 用户程序代码区 静态存储区 库程序代码区 堆区 ↓ ↑ 栈区 用 户 寄存器速度快但空间小,常常只存放经常参与运算的少数数据。 寄存器 寄存器1 寄存器2 寄存器n ... ...

{ int i, j, k ; bool test; ... } auto int i, j, k; auto bool test; 14.7.2 自动存储类别 具有自动存储类别的变量简称自动变量。C用auto表示自动存储类别,它是C中使用最广泛的一种存储类别。C规定,在函数内凡未加存储类别说明的变量均视为自动变量,也就是说在函数内自动变量可省去存储类别说明符 auto 。如下两个程序片段等价 。 { int i, j, k ; bool test; ... } auto int i, j, k; auto bool test;

在该程序片段中, 函数 example_auto 的两个参数 x 和 y 的作用域是在 L1 和 L9 之间, 生存期是程序执行进入函数 example_auto 开始,直到该函数执行完毕返回; 自动变量的作用域仅限于定义它的相应个体(函数、复合语句)内。 如果是在函数内定义的,则只在函数内有效; 若是在复合语句中定义,则只在相应语句中有效。 自动变量具有动态生存期, 当定义自动变量的相应个体开始执行时,自动变量生存期开始; 当定义自动变量的相应个体执行结束时,自动变量也离开它的生存期,不敷存在。 下例说明动态变量作用域,以及生存期。 【例14.12】动态变量作用域,以及生存期 int example_auto (int x, int y){ /* L1 */ auto int w, h; /* L2 */ …… …… /* L3 */ { /* L4 */ auto char c; /* L5 */ …… /* L6 */ } /* L7 */ …… /* L8 */ } /* L9 */ 自动变量w和h的作用域在L2和L9之间, 生存期是程序进入复合语句L2-L9执行开始,直到该复合语句执行完毕退出; 自动变量c的作用域则局限于L5和L7之间。如果在L8有引用变量c的语句,则错误。生存期是程序进入复合语句L4-L7执行开始,直到该复合语句执行完毕退出。

14.7.3 寄存器存储类别 寄存器变量分配在CPU的寄存器中,使用时不访问内存,直接在寄存器中进行,提高了程序运行效率。寄存器变量的存储类别明符是register 。 寄存器的个数与cpu相关,十分有限,所以寄存器变量的个数必然也有限。现代编译系统一般自动分配cpu寄存器,所以程序员说明的寄存器变量不起作用。 【例14.13】求1000以内可以被3整除所有整数的积,并打印 void main() { register int i,s=1; for (i=1; i<=1000; i++){ if ( i%3 == 0 ) s = s * i ; } printf("s=%d",s); 本程序循环1000次,i和s都将频繁使用,因此可定义为寄存器变量。

14.7.4 变量的静态存储类别 静态存储类别使用关键字static声明,静态存储类别的变量简称静态变量,静态存储类别的函数称为静态函数。 14.7.4 变量的静态存储类别 静态存储类别使用关键字static声明,静态存储类别的变量简称静态变量,静态存储类别的函数称为静态函数。 C规定,静态变量必须使用 static 声明。静态变量分为 静态全局变量 静态局部变量。 静态全局变量和静态局部变量的生存期都是贯穿于整个程序的运行期间;它们的不同点在于作用域: 静态全局变量的作用域是包含它的声明的整个源程序文件, 而静态局部变量的作用域是声明它的复合语句或函数。

静态局部变量: 在局部变量的声明前再加上static说明符就构成静态局部变量。 例如: { static char x , y; static int str[3]={0,1,2}; …… …… …… } 复合语句内的局部变量 x 、y 、str 被声明成 static 存储类别的,是静态局部变量。 静态局部变量采用静态存储方式,被分配在静态存储区。它的生存期为整个程序,但是它的作用域与自动变量相同,即只能在定义该变量的复合语句或函数中使用。离开复合语句和函数后,静态局部变量仍然存在却不能使用。 虽然静态局部变量在离开声明它的函数或复合语句后不能使用,但是如果再次调用声明它的函数或再一次进入声明它的复合语句时,又可以继续使用它,而且这时还保存了前次被使用后留下的值。 虽然用全局变量也可以达到上述目的,但全局变量有时会造成意外的副作用,因此仍以采用静态局部变量为宜。

【例14.14】静态局部变量使用举例 void main (){ // 主程序 for(int i=0;i<4;i++) not_test(); /* 函数调用 */ } void not_test(){ static bool test=false; test=!test; if(test) printf(“true ”); else printf(“false ”) } bool test=false; printf(“true\n”); printf(“false \n”) 图1 图2 而把函数not_test说明成图2的形式,由于 test 为静态变量,能在每次调用后保留其值并在下一次调用时继续使用,同样的 main 程序,输出结果为: true false true false 。 图1定义函数not_test,其中变量 test 说明为自动变量并赋予初始值 false。当 main 中多次调用 not_test 时,test 均赋初值为 false,故每次输出值均为true。

静态全局变量 如果全局变量之前冠以 static 就构成了静态全局变量,此种变量同全局变量的存储方式一样都是静态存储方式。不同点是,当源程序由多个文件组成时, 非静态全局变量的作用域是整个源程序,可以被程序中的所有文件所共享; 而静态全局变量只在声明它的源程序文件内有效,不是整个源程序。 【例14.15】静态全局变量使用举例 程序 example 由两个文件构成,每个文件中都定义了 char 类型变量 chr 。文件 ch14_07_01.c 中以 “char chr” 声明变量chr ,文件 ch14_07_02 中以 “static char chr” 声明变量 chr 。两个源程序文件分别编译,当连接程序为变量分配存储空间时,两个变量互不干扰,各分配各的存储空间,形成一个可执行文件。 程序运行输出结果为: chr_in_14_07_01=a chr_in_14_07_02=b 但是如果将文件 ch14_07_02.c 中的 chr 声明改为 char chr 那么两个文件虽然都能各自通过编译但是在连接时会出现错误,同一变量被声明了两次。 程序example static char chr; void fn(){ chr =‘b’; printf(“chr_in_14_07_02=%c\n” , chr); } char chr; void fn(); void main(){ chr = ‘a’; printf(“chr_in_14_07_01=%c\n”, chr); fn(); /* 文件ch14_07_02.c */ /* 文件ch14_07_01.c */

14.7.5 变量的外部存储类别 C用extern表示外部存储类别,包括外部变量和外部函数。 14.7.5 变量的外部存储类别 C用extern表示外部存储类别,包括外部变量和外部函数。 在C中,所有未加存储类别说明的全局变量均视为外部变量。外部变量意味着,变量在一个源程序文件中被声明,在其它所有源程序文件中都可以使用它。C使用外部变量采用如下程序结构: 在一个源程序文件中声明该变量,不附加 extern 存储类别说明符,例如 int x ; 在其他所有使用x的源程序文件中以extern存储类别说明符声明同一个变量,例如 extern int x; 如此结构,各个源程序文件分别编译,每个文件中变量 x 都有定义。当连接时,连接程序把各个文件中的 x 分配在同一个存储区,占用相同存储空间。

上述程序,在 ch14_07_02.c 中把 chr 的声明成外部的,两个文件中的chr是同一个变量。程序正确,程序运行后将打印 程序example char chr; void fn(); void main(){ chr = ‘a’; printf(“chr_in_14_07_01=%c\n”, chr); fn(); } /* 文件ch14_07_02.c */ /* 文件ch14_07_01.c */ extren char chr; void fn(){ chr =‘b’; printf(“chr_in_14_07_02=%c\n” , chr); 上述程序,在 ch14_07_02.c 中把 chr 的声明成外部的,两个文件中的chr是同一个变量。程序正确,程序运行后将打印 chr_in_14_07_01=a chr_in_14_07_02=b 但是,如果把ch14_07_02.c中把chr声明中的extern 去掉,两个文件虽都能各自通过编译,但是在连接时会出现“同一变量被声明了两次”的错误。原因在于 chr 在ch14_07_01.c和ch14_07_02.c中都被声明成全局变量。

14.7.6 函数的存储类别 内部函数 C函数只能被定义成 static extern 两种存储类别。 14.7.6 函数的存储类别 C函数只能被定义成 static extern 两种存储类别。 被定义成static存储类别的函数称静态函数,也称内部函数; 被定义成extern存储类别的函数称外部函数。 函数的省缺存储类别是外部存储类别。 内部函数 若在一个源文件中定义的函数只能被本文件中的函数调用,而不能被同一源程序其它文件中的函数调用,这种函数称为内部函数。定义内部函数的一般形式是: static 类型说明符 函数名 ( 形参表 ) ... ... 此处静态 static 的含义已不是指存储方式,而是指对函数的调用范围只局限于本文件。 因此在不同的源文件中定义同名的静态函数不会引起混淆。

外部函数 在C中,所有凡未加存储类别说明的函数均视为外部函数。外部函数意味着,函数在一个源程序文件中被定义,在其它所有源程序文件中都可以使用它。C使用外部函数采用如下程序结构: 在一个源程序文件中声明该函数,附加或不附加extern存储类别说明符,例如 int f ( float x ) { ... } 在其他所有使用f的源程序文件中用函数原型说明同一个函数,并且在前边附加 extern 存储类别说明符,例如 extern int f ( float x ) ; 如此结构,各个源程序文件分别编译,每个文件中函数f都有定义。连接时,由连接程序实现各个文件中f函数的协调和统一。 在一个源文件的函数中要调用其它源文件中定义的外部函数时,必须用extern说明被调函数为外部函数。

【例14.16】外部函数的使用。 /* 源文件 main.c */ extern int max (int a , int b ); /*函数原型,外部的,表示max在其它源文件中*/ void main(){ /* 主函数 */ int x , y , r; x=9; y=6; r=max( x , y ); /* 调用函数max */ printf(“The max of x=%d and y=%d is %d \n”, x , y , r ); } 在该程序中,源文件 max.c 中声明函数 max ;源文件 main.c 中调用函数max 。max 的函数原型声明被指定为 extern 类别的,保证了在源文件 main.c 中调用的 max 就是在 max.c 中定义的函数max ,并且不发生声明冲突。 /* 源文件 max.c */ extern int max (int a , int b ) { // 外部函数定义,其中extern可以省略 if(a>b) return a; else return b; }

14.7.7 类型定义符 类型定义符的实质是定义类型的同义词,用来把标识符定义为类型名。把一个标识符定义为类型名之后,它就可以出现在允许使用类型说明符的任何地方。这样就可以用简单的名字替代复杂的类型声明。 【例14.17】类型定义举例 typedef bool * BP; typedef int (*IFP)(); typedef int IF2I(int,int); typedef int IA[5]; 有了这些定义之后就可以进行如下的声明。 BP bp; /* bp是一个布尔型指针 */ BP fbp(); /* fbp是一个返回布尔型指针的函数 */ IFP ifp; /* ifp是一个返回类型为整型的函数指针 */ IF2I *fp; /* fp是一个返回类型为整型且有两个整型参数的函数指针 */ IA ia; /* ia是一个长度为5的一维整型数组 */ IA ia2[2]; /* ia2是一个2行5列的二维整型数组 */ BP是布尔型指针类型 IFP是指向返回值为整型的函数的指针类型 IF2I是有两个整型参数且返回整型值的函数类型 IA是长度为5的一维整型数组类型

14.8 编译预处理 C语言的预处理器是一个简单的宏处理器,源程序必须经过这个宏处理器处理之后才能让编译器正确处理。 14.8.1 宏定义 14.8 编译预处理 C语言的预处理器是一个简单的宏处理器,源程序必须经过这个宏处理器处理之后才能让编译器正确处理。 14.8.1 宏定义 C语言源程序中允许用一个标识符来表示一个字符串,称为“宏”。被定义为“宏”的标识符称为“宏名”。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,称为“宏代换”或“宏展开”。 宏定义由源程序中的宏定义命令完成。宏展开由编译预处理程序自动完成。在C中,“宏”分为有参数宏和无参数宏两种,简称有参宏和无参宏。

无参数宏: 形式为: #define 标识符 字符串 其中: #代表本行是编译预处理命令 define是宏定义命令 标识符是所定义的宏名 字符串是宏名所代替的内容,可以是常数、表达式等等。

【例14. 18】计算半径为10米的圆的周长,其中用宏定义了圆周率PI 。当编译预处理时,将用3 【例14.18】计算半径为10米的圆的周长,其中用宏定义了圆周率PI 。当编译预处理时,将用3.1415926来替代程序中出现的所有PI 。相当于在所有出现PI的地方全部写3.1415926一样。程序运行时自然使用3.1415926参与运算。 #define PI 3.1415926 main (){ int r = 10; int l; l = 2 * PI * r ; printf(“The perimeter of a circle with %d meter radius is %d \n” , r , l ); }

如要终止宏定义作用域可使用 “#undef”命令。例: #define PI 3.1415926 main( ) { …… #undef PI /* 终止PI的作用定义 */ }

宏定义是用宏名来代替一个字符串,编译预处理程序对它不做任何检查,如果有错误,只能在已经展开宏的源程序中发现。 说明: 宏定义是用宏名来代替一个字符串,编译预处理程序对它不做任何检查,如果有错误,只能在已经展开宏的源程序中发现。 习惯上宏名用大写字母表示,但也允许用小写字母。 宏定义必须写在函数之外,作用域从宏定义命令开始直到源程序结束。 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。例: #define PI 3.1415926 #define CIRCLE_L 2*PI*r /* PI是已定义的宏名 */

有参数宏: 它的定义形式为: #define 标识符 ( 形式参数表 ) 字符串  它的定义形式为: #define 标识符 ( 形式参数表 ) 字符串 形式参数表由逗号分隔开的标识符组成,这些标识符在字符串中出现。  宏调用形式是: 标识符(实在参数表) 带参宏调用不仅要宏展开而且要用实在参数去代换形式参数。宏展开时, 一方面用宏定义中的字符串替换宏调用中的标识符; 另一方面,同时用诸实在参数替换字符串中的形式参数标识符。 下边以例说明该过程。

【例14.19】判断a、b大小,用“宏”定义比较表达式。 #define MAX(x,y) ( x>y ? x : y ) /* 1 */ main ( ) { /* 2 */ int a , b , max ; /* 3 */ printf("input two numbers: "); /* 4 */ scanf("%d%d",&a,&b); /* 5 */ max = MAX( a , b ) ; /* 6 */ printf("max=%d\n",max); /* 7 */ } /* 8 */ 程序的第 1 行是带参宏定义,用宏名 MAX 表示条件表达式 “x > y ? x : y” 形式参数 x , y 均出现在条件表达式中。程序第6行 “max=MAX( a , b )” 为宏调用,实在参数 a , b 对应形式参数 x , y 。宏展开后该语句为: “max=( a > b ) ? a : b”; 用于求a , b中较大的数。

【例14.20】宏展开过程中的参数替换例题。 #define MULT( x,y ) x*y main( ){ int a,b,r; printf("input a number: "); scanf("%d %d",&a ,&b); r=MULT(a+1,b+1); } 在该程序中,宏 MULT 的形参是 x 和 y,宏调用的实在参数是 a+1 和 b+1。宏展开时用 x*y 代换 MULT,得到如下语句 r=a+1*b+1 可能与我们预期的 r=(a+1)*(b+1) 不一样。在该程序中,如果求形式参数 x、y 的积 x*y 必须用形式 #define MULT(x,y) (x)*(y) 定义宏。宏展开MULT(a+1 , b+1)才能得到正确结果 (a+1)*(b+1) 宏展开就是简单的符号替换,与函数调用有本质的不同。在例14-15中若把MULT定义为函数,则调用“MULT(a+1,b+1)”的操作步骤是首先计算实在参数表达式值,然后把相应值送入形式参数。产生的结果相当于r=(a+1)*(b+1)。可见看出宏调用很容易出错,建议使用函数.

使用带参数宏要注意: 带参宏定义中,宏名和形参表的圆括号之间不能有空格出现,否则会被认为是无参宏定义。例如在例14-14中 #define MAX( x , y ) (x > y ? x : y ) 若写成 #define MAX ( x , y ) (x > y ? x : y ) MAX变成无参宏,宏展开时将用“ ( x , y ) (x > y ? x : y )”替换max ! 在宏定义中的形式参数是标识符,而宏调用中的实在参数可以是任意字符串(包括表达式)。例如可以用“MAX( a+b , a-b )”调用宏MAX求a+b和a-b中较大者。 在宏定义中,字符串内的形式参数最好用括号括起来以避免出错。 带参宏和函数很相似,但有本质上的不同,如下表:

带参数宏 函数 形参    形式参数标识符不是变量,不分配内存单元,不作类型说明。    形式参数是局部于函数的变量,分配内存单元,必须作类型说明。 实参    实在参数是一个字符串,用它去代换形式参数标识符。    实在参数是表达式,它的值被传入形式参数。 结合    带参宏调用,十分类似换名参数,只作符号代换,不存在值传递的问题。    函数是值参数,调用时把实在参数的值赋予形式参数,进行“值传递”。

14.8.2 文件包含 预处理命令 #include 我们比较熟悉,它可以把指定源文件的全部内容括入现有源程序文件中,它的一般形式是: #include "文件名“ 或 #include <文件名>   文件包含命令的功能:    是把指定文件的全部内容括进来,插入到该命令行所在位置,取代该命令行。由当前源程序文件和指定文件组成一个文件,一起编译。    一个大的程序可以被分为多个模块,由多个程序员分别编写。公用信息可以单独组成一个文件,在其它文件的开头用文件包含命令将其括入。这样既可避免在每个文件开头都去书写那些公用量,节省时间,又可避免书写手误,减少出错。 尖括号和双引号区别:    尖括号只在省缺目录中查找指定文件,省缺目录由用户设置编程环境时设定。双引号表示首先在当前源文件所在文件目录中查找,如果没有找到,则在省缺目录中查找。

14.8.3 条件编译 条件编译命令,使编译器能够按照不同条件编译不同的程序部分,产生不同的目标代码文件。下表列出条件编译命令: 命令 含义 14.8.3 条件编译 条件编译命令,使编译器能够按照不同条件编译不同的程序部分,产生不同的目标代码文件。下表列出条件编译命令: 命令 含义 #if 根据常量表达式值有条件地包含文本 #ifdef 根据是否定义宏名有条件的包含文本 #ifndef 与#ifdef命令相反的测试,有条件包含文本 #elif 在#if、#ifdef、#ifndef、#elif测试失败时根据另一常量表达式值有条件包含文本 #else 在#if、#ifdef、#ifndef、#elif测试失败时包含的文本 #endif 结束条件编译 这些命令的一般组合使用的方式有两种:   1)使用常量表达式判断;   2)使用宏定义名判断。

使用常量表达式判断: 使用常量表达式判断的条件编译的功能是,首先求常量表达式值,然后根据常量表达式值是否为 0,进行下面的条件编译。设 ie 为整型常量表达式,使用常量表达式判断的条件编译形式是下述三种形式 : #if ie   文本1 #elif ie2   文本2 #else   其余文本 #endif #if ie       #if ie 其余文本

使用宏定义名判断: 使用宏定义名判断的形式是: “ #ifdef 标识符”的意义是: #ifdef 标识符 文本1 #else 文本2   使用宏定义名判断的形式是: #ifdef 标识符   文本1 #else   文本2 #endif #ifndef 标识符   文本 “ #ifdef 标识符”的意义是: 如果定义了标识符为宏(即使宏体为空),则为真,编译#if 后边的文本; 否则如果没有定义标识符为宏或者已经用 “#undef” 命令取消了标识符的宏定义,则为假,编译#else后边的文本。

【例14.21】条件编译例 #include "stdio.h" /*1*/ #define R 1 /*2*/ #define MAX(a,b) (a>=b?a:b) /*3*/ #define MIN(a,b) (a<=b?a:b) /*4*/ main(){ /*5*/ int x=0,y=0,t=0; /*6*/ printf("Please input 3 different integers:"); /*7*/ scanf("%d %d %d", &x, &y, &t); /*8*/ #if t /*9*/ t=MAX(x,y); /*10*/ printf(“MAX(%d,%d)=%d\n”,x,y,t); /*11*/ #else /*12*/ t=MIN(x,y); /*13*/ printf(“MIN(%d,%d)=%d\n”,x,y,t); /*14*/ #endif /*15*/

#if 3 /*16*/ t=MAX(x,y); /*17*/ printf(“MAX(%d,%d)=%d\n”,x,y,t); /*18*/ #else /*19*/ t=MIN(x,y); /*20*/ printf(“MIN(%d,%d)=%d\n”,x,y,t); /*21*/ #endif /*22*/ #undef R /*23*/ #ifdef R /*24*/ printf("The result is %d\n", t); /*25*/ #else /*26*/ printf("cannot output\n"); /*27*/ #endif /*28*/ } /*29*/ 在这个例子中使用了上面介绍的两种条件编译形式。

#define MAX(a,b) (a>=b?a:b) /*3*/ 【例14.21】条件编译例 #include "stdio.h" /*1*/ #define R 1 /*2*/ #define MAX(a,b) (a>=b?a:b) /*3*/ #define MIN(a,b) (a<=b?a:b) /*4*/ main(){ /*5*/ int x=0,y=0,t=0; /*6*/ printf("Please input 3 different integers:"); /*7*/ scanf("%d %d %d", &x, &y, &t); /*8*/ #if t /*9*/ t=MAX(x,y); /*10*/ printf(“MAX(%d,%d)=%d\n”,x,y,t); /*11*/ #else /*12*/ t=MIN(x,y); /*13*/ printf(“MIN(%d,%d)=%d\n”,x,y,t); /*14*/ #endif /*15*/ 程序的第 9-15 行,用变量(注意这里是变量)t 作为条件编译的判断条件; 变量 t 的值是在程序运行时由 scanf 函数确定的,在编译预处理时 t的值并不起作用,因此编译了 13 和 14 行。

printf(“MAX(%d,%d)=%d\n”,x,y,t); /*18*/ #else /*19*/ #if 3 /*16*/ t=MAX(x,y); /*17*/ printf(“MAX(%d,%d)=%d\n”,x,y,t); /*18*/ #else /*19*/ t=MIN(x,y); /*20*/ printf(“MIN(%d,%d)=%d\n”,x,y,t); /*21*/ #endif /*22*/ #undef R /*23*/ #ifdef R /*24*/ printf("The result is %d\n", t); /*25*/ #else /*26*/ printf("cannot output\n"); /*27*/ #endif /*28*/ } /*29*/ 第 24-28 行,用宏 R 作为条件编译的判断条件。 由于第 23 行 #undef语 句取消了宏 R 的定义,所以编译了第 27 行。经过编译预处理后,等价的程序如下: 第 16-22 行用常量 3 作条件编译的判断条件; 根据规则,由常量 3 控制编译了 17 和 18 行。

#define MAX(a,b) (a>=b?a:b) /*3*/ 程序运行结果形式如下: Please input 3 different integers: 这时计算机等待输入三个整数,若输入; 1 2 0 则产生如下输出: MIN(1,2)=1 MAX(1,2)=2 cannot output 经过编译预处理后,等价的程序如下: #include "stdio.h" /*1*/ #define R 1 /*2*/ #define MAX(a,b) (a>=b?a:b) /*3*/ #define MIN(a,b) (a<=b?a:b) /*4*/ main(){ /*5*/ int x=0,y=0,t=0; /*6*/ printf("Please input 3 different integers:"); /*7*/ scanf("%d %d %d", &x, &y, &t); /*8*/ t=MIN(x,y); /*13*/ printf(“MIN(%d,%d)=%d\n”,x,y,t); /*14*/ t=MAX(x,y); /*17*/ printf(“MAX(%d,%d)=%d\n”,x,y,t); /*18*/ printf("cannot output\n"); /*27*/ } /*29*/

条件编译和条件语句的区别:    条件编译当然可以用条件语句来实现。但是用条件语句将会对整个源程序进行编译,生成的目标代码程序很长,而采用条件编译,则根据条件只编译其中的某段程序,生成的目标程序较短。如果条件选择的程序段很长,采用条件编译的方法是十分必要的。    我们已经简单的了解了C语言中编译预处理的指令及用法,此外还有如预定义宏、#line、#program、#error等预处理指令,在这里就不一一介绍了。

本章小结 本章主要介绍了C语言独有的特性: 重点掌握函数作参数、语句、存储类别。 函数指针 函数作参数 函数副作用 运算 语句 位段 编译预处理 重点掌握函数作参数、语句、存储类别。