程序设计专题一 结构化程序设计与递归函数 主讲教师: 刘新国
专题要点 用结构化程序设计的思想解决问题 将多个函数组织起来,将多个源程序文件组织起来 理解程序设计规范及其重要性 函数嵌套求解复杂的问题 理解和使用函数递归 类型定义与宏定义 编译预处理
程序结构 main( ) 函数1 函数2 …… 函数m 函数1_1 函数1_2 函数m_1 函数m_n
函数定义 好的函数名字:描述函数所做的所有事情。如: 一个函数只实现一个功能 函数参数: checkOrderInfo(...) calcMonthlyRevenues(...) 一个函数只实现一个功能 函数参数: 按照输入-修改-输出的顺序排列参数 考虑对参数采用某种表示输入、修改、输出的命名规则 使用所有的参数 把状态或出错变量放在最后 不要把函数的参数用作工作变量 在接口中对参数的假定加以说明 尽可能少的参数(限制在大约7个以内)
2. 源程序文件组织 .c文件:实现文件 .h文件:自定义的头文件 一个程序通常包含多个.c和.h文件 函数实现 全局变量定义 类型定义,函数说明 一个程序通常包含多个.c和.h文件 几个到几千个 系统越大越复杂,文件越多
头文件( .h 文件) 为了方便模块函数被其他模块调用,编写头文件,保存: 正确使用头文件可提高代码的可读性、可维护性、以及开发效率 宏定义 常量定义 数据类型定义 外部变量声明 函数声明(函数原型要求) 正确使用头文件可提高代码的可读性、可维护性、以及开发效率 在.c文件中通过命令包含.h头文件。例如: #include <stdio.h> #include "mydata.h" 包含自定义的头文件时,一般用" "
文件模块(.c文件) 编译单元 连接程序 在编译一个.c文件时,预处理器首先递归包含头文件,形成一个含有所有必要信息的单个源文件。 把各个目标程序中产生的符号联系起来,构成一个可执行程序。
模块编译链接 prog1.o 编译 prog1.c a1.h, …, b1.h prog2.c a2.h, …, c2.h progM.c a.h, b.h, c.h …… 链接 prog2.o 编译 可执行文件 progM.o …… 编译
头文件编写 头文件由三部分内容组成: 头文件应该只用于声明,不应该包含 版权声明和版本信息(起始处) 预处理块 定义和申明 结构和枚举类型定义 typedef定义和宏定义 具名常量定义 外部变量声明 函数申明 头文件应该只用于声明,不应该包含 “占据存储空间的变量或函数”的定义
包含(#include)头文件 头文件可以嵌套包含 避免循环包含(为什么),避免头文件被多重复包含 A.h包含B.h,B.h包含C.h,。。。 避免循环包含(为什么),避免头文件被多重复包含 虽然函数、变量的声明都可以重复,不会影响程序编译和运行,但会增加编译处理的时间 当头文件中包含结构的定义、枚举定义等一些定义时,这些定义是不可以重复的,必须通过一定措施防止发生
#define保护 #ifndef _HEADERNAME_H #define _HEADERNAME_H vecmat.h <头文件内容> #endif 注意宏定义_HEADERNAME_H 在模块中 必须是唯一的 不要重复 vecmat.h #ifndef _VEC_MAT_H #define _VEC_MAT_H typedef struct { float x, y, z; } vec; float m[3][3]; } mat; #endif // ifndef _VEC_MAT_H
编译预处理 编译器对源代码开始编译之前,首先对.c文件进行预处理 预处理指令 编译器通过预处理指令,对源代码做相应转换。 或者说,扫描源代码,对其进行初步的转换,生成新的源代码提供给编译器。 预处理指令 以#号开头的代码行 #号必须是该行除了空白字符外的第一个字符。 #号后是指令关键字 在关键字和#号之间允许存在任意个数的空白字符。 整行语句构成了一条预处理指令 编译器通过预处理指令,对源代码做相应转换。
常用的预处理指令 #include 文件包含指令 #define 宏定义指令 #undef 宏定义取消指令 条件编译指令 #ifdef/#ifndef <条件对应的代码> #endif #if表达式 <表达式非零时编译的代码> #else <否则编译的代码> #elif #error 停止编译预处理并输出错误信息 #include <stdio.h> #define _LIB_VERSION 2 #undef _linux #ifndef M_PI #define M_PI 3.1415926 #endif #if _LIB_VERSION ==2 <适合版本2的代码> #else <适合其他版本的代码> #error "Only support V 2"
#define保护 通过#define保护,当头文件被重复include的时候, 内容不会被重复include #ifndef _VEC_MAT_H #define _VEC_MAT_H typedef struct { float x, y, z; } vec; float m[3][3]; } mat; #endif // ifndef _VEC_MAT_H 通过#define保护,当头文件被重复include的时候, 内容不会被重复include <在同一个程序模块中不重复>
#define保护 保证了在编译一个c文件时,头文件的不被重复编译 在编译其他.c文件时,该头文件任然会被编译 因此,一般地,头文件中不能出现函数定义和变量的定义。 否则如果有多个c文件包含它时,仍然会编译生成重复的变量和函数定义。 重复的变量或函数定义会导致Link错误!
举例:不规范的头文件 // in header.h #ifndef _HEADER_H #define _HEADER_H extern void Foo1(); /*函数声明 */ extern int a1; /*外部变量声明 */ struct A; /*前置声明结构A */ int a2; //全局变量定义,应当避免 void Foo2() //函数定义,应当避免 {} typedef struct /* 结构B定义 */ { int i; struct A m; }B; #endif 如果有多个程序模块文件,例如A.c和B.c,他们都include了该头文件。那么当他们被分别编译的,都生成了一份全局变量a2和函数Foo2。编译是ok的,但是在Link阶段,会出现重复定义的冲突,导致失败。
头文件的作用 通过头文件了解函数功能 通过头文件能加强类型安全检查 用户经常不能拿到源代码(涉密),但是可以拿到头文件和库文件(编译后的二进制文件)。 用户可以按照头文件中的(接口)函数声明,调用库函数,而不必关心接口怎么实现的。 编译器会从库中提取相应的二进制代码。 例如我们使用printf,scanf标准库函数 通过头文件能加强类型安全检查 如果某个接口/函数被实现或被使用时,其方式与头文件中的声明不一致,编译器就会报错 该简单规则能大大减轻程序员调试和改错负担
宏定义#define命令 #define 宏名标识符 宏定义字符串 说明: #define PI 3.14 编译时,把程序中所有与宏名相同的标识符,用宏定义字符串替代 说明: 宏名一般用大写字母,以与变量名区别 宏定义不是C语句,后面不得跟分号 宏定义可以嵌套使用 多用于符号常量、简单的操作和函数等 #define MAX(a, b) ((a)>(b) ? (a) : (b))
宏基本定义 宏定义可以写在程序中任何位置,它的作用范围从定义书写处到文件尾。 可以通过“#undef”强制指定宏的结束范围。
宏的作用范围 #define A “This is the first macro” void f1() { printf( “A\n” ); printf(“%s\n”, A); } #define B “This is the second macro” A 的有效范围 void f2( ) printf( B ) ; B 的有效范围 #undef B int main(void) f1( ); f2( ); return 0;
x*x z+x*z+x x>y ? x : y 10.3.2 带参数的宏定义 10.3.2 带参数的宏定义 #define MAX(a,b) a>b ? a : b #define SQR(x) x*x … int x, y, z x = MAX(x,y); y = SQR(x); y = SQR(z+x); x>y ? x : y x*x z+x*z+x
(x)*(x) (z+x)*(z+x) 10.3.2 带参数的宏定义 建议使用(),减少麻烦 (x)>(y) ? (x) : (y) 10.3.2 带参数的宏定义 建议使用(),减少麻烦 #define MAX(a,b) (a)>(b) ? (a) : (b) #define SQR(x) (x)*(x) … int x, y, z x = MAX(x,y); y = SQR(x); y = SQR(z+x); (x)>(y) ? (x) : (y) (x)*(x) (z+x)*(z+x)
用宏实现两个变量的交换 #define f(a, b, t) (t)=(a),(a)=(b),(b)=(t) void main( void ) { int x,y,t ; scanf(“%d%d”, &x, &y); f(x,y,t);/*使用宏*/ printf(“%d %d\n”, x, y) ; } (t)=(x),(x)=(y),(y)=(t);
嵌套的宏定义与调用 #define F(x) x - 2 #define D(x) x*F(x) void main() { printf("%d,%d", D(3), D(D(3))) ; }
嵌套的宏定义与调用 #define F(x) x - 2 #define D(x) x*F(x) 计算 D(3), D(D(3)) 先全部替换好,最后再统一计算 不可一边替换一边计算,更不可以人为添加括号 D(3) = 3*F(3) = 3*3-2 = 7 D(D(3)) = D(3)*F(D(3)) = 3*F(3)*D(3)-2 = 3*3-2*3*F(3)-2 = 3*3-2*3*3-2-2 = -13
宏定义应用示例 判断字符c是否为小写字母。 将数字字符(‘0’~‘9’)转换为相应的十进制整数,-1表示出错。 最大值、最小值 #define LOWCASE(c) (((c) >= 'a') && ((c) <= 'z') ) 将数字字符(‘0’~‘9’)转换为相应的十进制整数,-1表示出错。 #define CTOD(c) (((c) >= '0') && ((c) <= '9') ? c - '0' : -1) 最大值、最小值 #define MAX(a,b) ((a) >= (b) ? (a) : (b)) #define MIN(a,b) ((a) <= (b) ? (a) : (b))
文件包含#include命令 为了避免一个文件过长,可以把程序组织为多个文件。 程序文件程序文件模块。 每一个文件包含若干个函数。 程序文件程序文件模块。 程序 文件 函数 各程序文件模块分别编译,再连接 整个程序只允许有一个main()函数
文件包含 格式 例如 作用 注意 #include <需包含的文件名> #include "需包含的文件名" #include <stdio.h> #include "myfunc.h" 作用 把被包含的文件内容插入到#include命令所在位置 注意 编译预处理命令,以#开头。 行尾没有分号。
[例10-7] 文件包含举例 文件length.h /* 1英里=1609米 */ #define mile_to_meter 1609 /* 1英尺=30.48厘米 */ #define foot_to_centimeter 30.48 /* 1英寸=2.54厘米 */ #define inch_to_centimeter 2.54
[例10-7] 文件包含举例 文件 prog.c #include <stdio.h> #include "length.h" void main() { float foot, inch, mile; printf("%f miles = %f\n", mile, mile*mile_to_meter); … }
.h头文件常规用法 统一的定义和声明 好处 宏定义 变量声明 函数申明 外部变量申明 自定义的数据类型(结构) 避免多次重复定义 避免不一致 方便维护和修改
常用标准头文件 ctype.h 字符处理 math.h 与数学处理函数有关的说明与定义 stdio.h 输入输出函数中使用的有关说明和定义 string.h 字符串函数的有关说明和定义 stddef.h 定义某些常用内容 stdlib.h 杂项说明 time.h 支持系统时间函数
10.3.5 编译预处理 编译预处理是C语言编译程序的组成部分,它用于解释处理C语言源程序中的各种预处理指令。 10.3.5 编译预处理 编译预处理是C语言编译程序的组成部分,它用于解释处理C语言源程序中的各种预处理指令。 文件包含(#include)和宏定义(#define)都是编译预处理指令 在形式上都以“#”开头,不属于C语言中真正的语句 增强了C语言的编程功能,改进C语言程序设计环境,提高编程效率
编译预处理 由于#define等编译预处理指令不是C语句,不能被编译程序翻译 需要在真正编译之前作一个预处理,解释完成编译预处理指令 从而把预处理指令转换成相应的C程序段,最终成为由纯粹C语句构成的程序 经编译最后得到目标代码。
编译预处理功能 编译预处理的主要功能: 文件包含(#include) 宏定义(#define) 条件编译
编译预处理功能 编译预处理的主要功能: 文件包含(#include) 宏定义(#define) 条件编译
条件编译 #define _flag 1 #define _flag 0 #if _flag 程序段1 #else 程序段2 #endif
条件编译 #define _zhongwen #ifdef _zhongwen #define msg "早上好" #else #define msg "Good morning" #endif … main() { printf(msg); }
条件编译(用于调试) #define _mydebug 0 #if _mydebug #define _dbg(x) (x) #else #define _dbg(x) #endif main() { int x; … _dbg(printf("%d",x); }
大程序构成 - 多文件模块 学生信息库系统 文件模块 student.h input_output.c aver_sort.c 建立 new_student() 输出 output_student() 计算平均成绩 average() 平均成绩排序 sort() 修改 modify() 查询 search_student() 文件模块 student.h input_output.c aver_sort.c modify.c student_system.c
大程序构成 - 多文件模块 用户头文件 - student.h #include<stdio.h> 宏定义,数据类型定义(结构) #include<stdio.h> #include<string.h> #define MaxSize 50 struct student { int num; char name[10]; int computer, english, math; float average; };
大程序构成 - 多文件模块 用户头文件 - student.h《续》 extern int count; 外部变量,外部函数 extern int count; void new_student(struct student students[]); void output_student(struct student students[]); void average(struct student students[]); void sort(struct student students[]); void modify(struct student students[]); void search_student(struct student students[], int num);
大程序构成 - 多文件模块 输入/出程序文件 – input_output.c #include "student.h" void new_student(struct student students[]) { … } void output_student(struct student students[])
大程序构成 - 多文件模块 计算平均成绩及排序文件 – aver_sort.c #include "student.h" void average(struct student students[]) { … } void sort(struct student students[])
大程序构成 - 多文件模块 查询和修改文件 – modify.c #include "student.h" void modify(struct student students[]) { … } void search_student(struct student students[], int num)
大程序构成 - 多文件模块 主函数程序– student_system.c #include "student.h" int count = 0; int main() { struct student students[MaxSize]; … /* 调用函数,实现功能*/ }
10.4.3 文件模块之间的沟通 外部变量 声明格式如下: extern 类型名 变量名; 例如: extern int count; 10.4.3 文件模块之间的沟通 外部变量 在文件A中使用文件B中定义的全局变量c的时候,需要在使用之前进行声明 声明格式如下: extern 类型名 变量名; 例如: extern int count; 如果不希望其他文件使用某个全局变量,那么需要将其定义为static类型的全局变量
10.4.3 文件模块之间的沟通 外部函数 声明格式如下: extern 类型名 函数名(参数类型表); 关键字 extern可以省略 10.4.3 文件模块之间的沟通 外部函数 在文件A中使用文件B中定义的函数f的时候,需要在使用之前进行声明 声明格式如下: extern 类型名 函数名(参数类型表); 关键字 extern可以省略 例如: void new_student(struct student students[]); void output_student(struct student students[]);
10.4.3 文件模块之间的沟通 静态函数 声明格式如下: static 类型名 函数名(参数类型表) { … } 10.4.3 文件模块之间的沟通 静态函数 如果不希望文件中的某个函数在其他的文件中调用,那么可以将其定义为静态的 也称作:内部函数 声明格式如下: static 类型名 函数名(参数类型表) { … }
1. 编码规范 高质量的程序 正确性:语法正确、功能正确。使之可行 可读性:通用的、必需的习惯用语和模式可以使代码更加容易理解。使之优雅 可维护性:程序应对变化的能力。使之优化 ……
1. 编码规范 阅读附件(课程主页下载) 网上搜索资料(bing或者百度C编码规范) 仅供参考(标准不唯一,无需全盘接受) 《C编码规范》 http://cn.bing.com/search?q=c%E7%BC%96%E7%A0%81+%E8%A7%84%E8%8C%83&FORM=AWRE 仅供参考(标准不唯一,无需全盘接受)
若干C代码规范 不直接使用基础类型 利用typedef重新定义,大小和符号 代替基本数据类型
若干C代码规范 《MISRA—C-2008工业标准》建议为所有基本数值类型和字符类型使用typedef重新定义。 对于32位计算机,它们是: typedef char char_t; typedef signed char int8_t; typedef signed short int16_t; typedef signed int int32_t; typedef signed long int64_t; typedef unsigned char uint8_t; typedef unsigned short uint16_t; typedef unsigned int uint32_t; typedef unsigned long uint64_t; typedef float float32_t; typedef double float64_t; typedef long double float128_t;
若干C代码规范 变量、函数的命名符合编码规范 Pascal命名规则:当变量名和函数名称是由二个或二个以上单字连结在一起,而构成的唯一识别字时: 第一个单字首字母采用大写字母 后续单字的首字母亦用大写字母 例如:FirstName、LastName。
若干C代码规范 小心使用全局变量 用访问器子程序来取代全局数据 把数据隐藏到模块里面 写子程序读/写/初始化该数据 GetValue/SetValue 把数据隐藏到模块里面 用static关键字来定义该数据 写子程序读/写/初始化该数据 要求模块外部的代码使用该访问器子程序来访问该数据,而不直接(使用变量名字)操作它。
模块化开发 应用自顶向下的设计 一个函数实现一个简单的功能 一个源文件包含功能相对集中的若干函数定义 把一个相对复杂的功能,划分成相对独立的子功能,直到每个子功能相对简单 每个子功能用一个函数来实现 一个函数实现一个简单的功能 如果一个函数的代码很多(比如150行以上),最好把它分成可以调用的小函数来完成 一个源文件包含功能相对集中的若干函数定义 如果一个源文件中包含很多个函数(比如50个以上),最好再分成几个更小的源文件 每个源文件都包含一组功能相关的函数