C语言大型程序的项目管理与实现
当程序复杂时源代码会很长,如果把全部代码放在一个源文件里,写程序,修改、加工程序都会很不方便。程序文件很大时,装入编辑会遇到困难;在文件中找位置也不方便;对程序做了一点修改,调试前必须对整个源文件重新编译;如果不慎把已经调试确认的正确部分改了,又会带来新的麻烦。在实践中人们体会到:应当把大软件(程序)的代码分成一些部分,分别放在一组源程序文件中,分别进行开发、编译、调试,然后把它们组合起来,形成整个软件(程序)。C语言本身支持这种开发方式。当我们写的程序较大时,上述问题就会反应出来,因此应当学习“大程序”的开发方法。
把一个程序分成几个源程序文件,显然这些源文件不是互相独立的。一个源文件里可能使用其他源文件定义的程序对象(外部变量、函数、类型等),这实际上在不同源文件间形成了一种依赖关系。这样,一个源文件里某个程序对象的定义改动时,使用这些定义的源文件也可能要做相应修改。在生成可执行程序时,应该重新编译改动过的源文件,而没改过的源文件就不必编译了。在连接生成可执行程序时,要把所有必要的模块装配在一起。这些管理工作可以由人自己做,但是很麻烦。 TURBO C集成开发环境的项目管理功能能帮助我们处理这些问题。利用这种功能,开发大程序的工作将更加方便。今天的各种程序开发环境都提供了类似的管理功能。
用C语言写大程序,应当把源程序分成若干个源文件。其中有:
(1)一个或几个自定义的头文件,通常用 .h 作为扩展名。头文件里一般放:
#include预处理命令,引用系统头文件和其他头文件;
用#define定义的公共常量和宏;
数据类型定义,结构、联合等的说明;
函数原型说明,外部变量的extern说明;等等。
(2)一个或几个程序源文件,通常用 .c 作为扩展名。这些文件中放:
对自定义头文件的使用(用#include命令);
源文件内部使用的常量和宏的定义(用#define命令);
外部变量的定义;
各函数的定义,包括main函数和其他函数。
不提倡在一个 .c 文件里用#include命令引入另一个 .c 文件的做法。这样往往导致不必要的重新编译,在调试程序查错时也容易引起混乱。应该通过头文件里的函数原型说明和外部变量的extern说明,建立起函数、外部变量的定义(在某个源程序文件中)与它们的使用(可能在另一个源程序文件中)之间的联系,这是正确的做法。
大型项目中C语言的模块化建议
一个大型的软件项目通常包含很多复杂的功能,实现这个项目不是一个程序员单枪匹马可以胜任的,往往
需要一个团队的有效分工合作,另外,在一个以C代码为主的完整的项目中,经常也需要加入一些其他语言的代码,例如,C代码和汇编代码的混合使用,C文件和C++的同时使用。这些都增加了一个软件项目的复杂程度,为了提高软件质量,合理组织的各种代码和文件是非常重要的。组织代码和文件的目的是为了使团队合作更加有效,
使软件项目有良好的可扩展性、可维护性、可移植性、可裁减、可测试性,防止错误发生,提高软件的稳定性。
软件项目通常采用
层次化结构开发和模块化开发,例如,一个嵌入式软件项目可能有驱动层,操作系统层,功能层,应用程序层,每一个层使用它的
下层提供的接口,并为它的上层提供调用接口;模块则是每一个层中完成一个功能的单元,例如驱动层的每一个设备的驱动就是一个模块,应用层的每个应用程序就是一个模块,模块使用
下层提供的接口和
同层其他模块提供的接口,完成特定功能,为上层和同层的其他模块提供调用接口。
这里的接口是指一个功能模块暴露出来的,提供给其他模块的访问具体功能的方法。
根据C语言的特点,使用*.c文件实现模块的功能,使用*.h文件暴露单元的接口,在*.h文件里声明外部其他模块可能是用的函数,数据类型,全局变量,类型定义,宏定义和常量定义.外部模块只需包含*.h文件就可以使用相应的功能.当然,模块可以在细化为子模块.虽然我们这里说的接口和COM(通用组件模型)里定义的接口不同,但是,根据COM里对接口的讨论,
为了使软件在修改时,一个模块的修改不会影响到其他模块的一个模块的修改不会导致其他模块也需要修改,所以,接口第一次发布后,修改*.h文件不能导致使用这个接口的其他模块需要重新编写.
文件组织的基本建议
使用层次化和模块化的软件开发模型.
每一个模块只能使用所在层和下一层模块提供的接口.
每个模块的文件包存在独立的一个文件夹中.通常情况下,实现一个模块的文件不止一个,这些相关的 文件应该保存在一个文件夹中.
用于模块裁减的条件编译宏保存在一个独立的文件里,便于软件裁减。
硬件相关代码和操作系统相关代码与纯C代码相对独立保存,以便于软件移植.
声明和定义分开,使用*.h文件暴露模块需要提供给外部的函数,宏,类型,常量,全局变量,尽量做到模块对外部透明,用户在使用模块功能时不需要了解具体的实现,文件一旦发布,要修改一定要很慎重,
文件夹和文件命名要能够反映出模块的功能,所以命名要用意义的名字。
正式版本和测试版本使用统一文件,使用宏控制是否产生测试输出。
必要的注释不可缺少,提高文件的可读性。
头文件建议参考以下的规则
头文件中不能有可执行代码,也
不能有数据的定义,只能有宏、类型(typedef,struct,union,
menu enum),数据和函数的声明。
例如以下的代码可以包含在头文件里:
#
define NAMESTRING “name”
typedef unsign long word;
enum{
flag1;
flag2;
};
typedef struct
{
int x;
int y;
typedef unsign long word;
enum{
flag1;
flag2;
};
typedef struct
{
int x;
int y;
}Piont;
extent Fun( void);
extent Fun( void);
extent
int a;
全局变量和函数的定义不能出现在*.h文件里。例如下面的代码不能包含在头文件:
int a;
void Fun1( void)
{
a ++;
}
void Fun1( void)
{
a ++;
}
头文件中不能包
本地数据(模块自己使用的数据或函数,不被其他模块使用)。这一点相当于面向对象程序设计里的私有成员,即只有模块自己使用的函数,数据,不要用extent在头文件里声明,
只有模块自己使用的宏,常量,类型也不要在头文件里声明,应该在自己的*.c文件里声明。
含一些需要使用的声明。在头文件里声明外部需要使用的数据,函数,宏,类型。
防止被重复包含。使用下面的宏防止一个头文件被重复包含。
#
ifndef MY_INCLUDE_H
# define MY_INCLUDE_H
<头文件内容 >
# endif
# define MY_INCLUDE_H
<头文件内容 >
# endif
包含extern "C",使
的 得程序可以在C++编译器被编译
#
ifdef __cplusplus
extern "C"{
# endif
<函数声明 >
# ifdef __cplusplus
}
#enfif
extern "C"{
# endif
<函数声明 >
# ifdef __cplusplus
}
#enfif
被extern "C"修饰的变量和函数是按照C语言方式编译和连接的;未加extern“C”声明时的编译方式,作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo(int x,int y);该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。_foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo(int x,int y)与void foo(int x,float y)编译生成的符号是不相同的,后者为_foo_int_float。 同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。加extern "C"声明后的编译和连接,强制C++连接器按照C编译器产生的符号_foo链接。
结合起来就是:
#
ifndef MY_INCLUDE_H
# define MY_INCLUDE_H
# ifdef __cplusplus
extern "C"{
# endif
<函数声明 >
# ifdef __cplusplus
}
#enfif
# endif
# define MY_INCLUDE_H
# ifdef __cplusplus
extern "C"{
# endif
<函数声明 >
# ifdef __cplusplus
}
#enfif
# endif
保证在使用这个头文件时,用户不用再包含使用此头文件的其他前提头文件,即要使用的头文件已经包含在此头文件里。例如:area.h头文件包含了面积相关的操作,要使用这个头文件不需同时包含了关于点操作的头文件piont.h。用户在使用area.h时不需要手动包含piont.h,因为我们已经在 area.h中用#include “point.h”包含了这个头文件。
用来暴露接口的头文件还需要参考更多的规则:
1,
一个模块一个接口,不能几个模块用一个接口。
2,
文件名为和实现模块的c文件相同。abc.c--abc.h
3,尽量不要使用extern来声明一些共享的数据。因为这种做法是不安全的,外部其他模块的用户可能不能完全理解这些变量的含义,最好提供函数GetPut访问这些变量。
4,
尽量避免包含其他的头文件,除非这些头文件是独立存在的。这一点的意思是,在作为接口的头文件中,尽量不要包含其他模块的那些暴露*.C文件中内容的头文件,但是
可以包好含一些不是用来暴露接口的头文件。
5,
不要包含那些只有在可执行文件中才使用的头文件,这些头文件应该在*.c文件中包含。这一点如同上一点,为了提高接口的独立性和透明度。
6,
接口文件要有面向用户的充足的注释。从应用角度描述个暴露的内容。
7,
接口文件在发布后尽量避免修改,即使修改也要保证不影响用户程序。
多个代码文件使用一个接口文件:这种头文件用于那些认为一个模块使用一个文件太大的情况。增加以下建议。
1,
多个代码文件组成的一个模块只有一个接口文件。因为这些文件完成的是一个模块。
2,
使用模块下文件命名 <系统名 > <模块名命名>
// ov7620_reg.h
3,不要滥用这种文件。
4,有时候也会出现几个*.c文件用于共
向享数据的*.h文件,这种文件的特点是
在一个*.c文件里定义全局变量,而在其他*.c文件里使用,要将这种文件和用于暴露模块接口的文件区别。
5,一个模块如果有几个子模块,可以用一个*.h文件暴露接口,在这个文件里用
#include包含每个子模块的接口文件。
还有一种头文件,说明性头文件,这种头文件不需要有一个对应的代码文件,
在这种文件里大多包含了大量的宏定义,没有暴露的数据变量和函数。这些文件给出以下建议:
1,包含一些需要的概念性的东西.
2,命名方式,定义的功能.h
3,不包含任何其他的头文件.
4,不定义任何类型.
5,不包含任何数据和函数声明.
上面介绍了C头文件的一些建议,下面介绍C代码文件*.c文件的一些建议,*.c文件是C语言中生成汇编代码和机器码的内容,要注意以下建议:
1.命名方式 模块名.c
2,
用static修饰本地的数据和函数。
3,
不要使用external。这是在*.h中使用的,可以被包含进来。
4,无论什么时候定义内部的对象,确保独立与其他执行文件。
5,这个文件里必须包含相应功能函数。