做到专业,应该是每个职业程序员应该要求自己做到的。
1.License Agreement
License Agreement应该加在每个头文件的顶部。
Lua Sample:
** $Id: lua.h,v 1.175b 2003/03/18 12:31:39 roberto Exp $
** Lua - An Extensible Extension Language
** Tecgraf: Computer Graphics Technology Group, PUC-Rio, Brazil
** http://www.lua.org mailto:info@lua.org
** See Copyright Notice at the end of this file
*/
2.guard define
整个头文件应该在guard define之间
#define lua_h
#endif
另外,如果这个头文件可能给c++使用,要加上
extern "C" {
#endif
/*The lines within extern "C" */
#ifdef __cplusplus
}
#endif
3.尽量不要在头文件中暴露数据结构
这样可以用户对你的实现的依赖,也减少了用户的编译时间
LUA_API lua_State *lua_open (void );
LUA_API void lua_close (lua_State * L);
可以看到虽然用户会一直使用lua_State,但是并不知道lua_State的结构是什么
从一个使用lua的例子程序可以看出:
#include "lauxlib.h"
#include "lualib.h"
int main(int argc, char * argv[])
{
lua_State *L = lua_open();
const char *buf = "var = 100" ;
int var ;
luaopen_base(L);
luaopen_io(L);
lua_dostring(L, buf);
lua_getglobal(L, "var" );
var = lua_tonumber(L, -1 );
lua_close(L);
return 0 ;
}
4.函数声明前加XXX_API已利于拓展
Lua的例子
#define LUA_API extern
#endif
LUA_API lua_State *lua_open (void );
如果定义了LUA_API就是给LUA内部使用的
如果没定义LUA_API就是for user 的
写Window dll程序经常会用到
#define DLLTEST_API __declspec(dllexport)
#else
#define DLLTEST_API __declspec(dllimport)
#endif
5.宏的定义
尽量使用括号来包住所定义的对象
#define lua_register(L,n,f) /
(lua_pushstring(L, n), /
lua_pushcfunction(L, f), /
lua_settable(L, LUA_GLOBALSINDEX))
6.目录结构
一般应该使用一个单独的include目录来包含要发布的头文件,但不应该把内部使用的头文件包含进去。
Lua 的 include目录只包含了三个头文件
lauxlib.h , lua.h, lualib.h
非常简洁
----------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------
C语言中的.h文件和我认识由来已久,其使用方法虽不十分复杂,但我却是经过了几个月的“不懂”时期,几年的“一知半解”时期才逐渐认识清楚他的本来面目。揪其原因,我的驽钝和好学而不求甚解固然是原因之一,但另外还有其他原因。原因一:对于较小的项目,其作用不易被充分开发,换句话说就是即使不知道他的详细使用方法,项目照样进行,程序在计算机上照样跑。 原因二:现在的各种C语言书籍都是只对C语言的语法进行详细的不能再详细的说明,但对于整个程序的文件组织构架却只字不提,找了好几本比较著名的C语言著作,却没有一个把.h文件的用法写的比较透彻的。下面我就斗胆提笔,来按照我对.h的认识思路,向大家介绍一下。
让我们的思绪乘着时间机器回到大学一年级。C原来老师正在讲台上讲着我们的第一个C语言程序: Hello world!
文件名 First.c
main()
{
printf(“Hello world!”);
}
例程-1
看看上面的程序,没有.h文件。是的,就是没有,世界上的万物都是经历从没有到有的过程的,我们对.h的认识,我想也需要从这个步骤开始。这时确实不需要.h文件,因为这个程序太简单了,根本就不需要。那么如何才能需要呢?让我们把这个程序变得稍微复杂些,请看下面这个,
文件名 First.c
printStr()
{
printf(“Hello world!”);
}
main()
{
printStr()
}
例程-2
还是没有, 那就让我们把这个程序再稍微改动一下.
文件名 First.c
main()
{
printStr()
}
printStr()
{
printf(“Hello world!”);
}
例程-3
等等,不就是改变了个顺序嘛, 但结果确是十分不同的. 让我们编译一下例程-2
和例程-3,你会发现例程-3是编译不过的.这时需要我们来认识一下另一个C语言中的概念:作用域.
我们在这里只讲述与.h文件相关的顶层作用域, 顶层作用域就是从声明点延伸到源程序文本结束, 就printStr()这个函数来说,他没有单独的声明,只有定义,那么就从他定义的行开始,到first.c文件结束, 也就是说,在在例程-2的main()函数的引用点上,已经是他的作用域. 例程-3的main()函数的引用点上,还不是他的作用域,所以会编译出错. 这种情况怎么办呢? 有两种方法 ,一个就是让我们回到例程-2, 顺序对我们来说没什么, 谁先谁后不一样呢,只要能编译通过,程序能运行, 就让main()文件总是放到最后吧. 那就让我们来看另一个例程,让我们看看这个方法是不是在任何时候都会起作用.
文件名 First.c
play2()
{
play1()
}
play1()
{
play2()
}
main()
{
play1()
}
例程-4
也许大部分都会看出来了,这就是经常用到的一种算法, 函数嵌套, 那么让我们看看, play1和play2这两个函数哪个放到前面呢?
这时就需要我们来使用第二种方法,使用声明.
文件名 First.c
play1();
play2();
play2()
{
play1()
}
play1()
{
play2()
);
}
main()
{
play1()
}
例程-4
经历了我的半天的唠叨, 加上四个例程的说明,我们终于开始了用量变引起的质变, 这篇文章的主题.h文件快要出现了。
一个大型的软件项目,可能有几千个,上万个play, 而不只是play1,play2这么简单, 这样就可能有N个类似 play1(); play2(); 这样的声明, 这个时候就需要我们想办法把这样的play1(); play2(); 也另行管理, 而不是把他放在.c文件中, 于是.h文件出现了.
文件名 First.h
play1();
play2();
文件名 First.C
#include “first.h”
play2()
{
play1()
}
play1()
{
play2()
}
main()
{
play1()
}
例程-4
各位有可能会说,这位janders大虾也太罗嗦了,上面这些我也知道, 你还讲了这么半天, 请原谅, 如果说上面的内容80%的人都知道的话,那么我保证,下面的内容,80%的人都不完全知道. 而且这也是我讲述一件事的一贯作风,我总是想把一个东西说明白,让那些刚刚接触C的人也一样明白.
上面是.h文件的最基本的功能, 那么.h文件还有什么别的功能呢? 让我来描述一下我手头的一个项目吧.
这个项目已经做了有10年以上了,具体多少年我们部门的人谁都说不太准确,况且时间并不是最主要的,不再详查了。 是一个通讯设备的前台软件, 源文件大小共 51.6M, 大小共1601个文件, 编译后大约10M, 其庞大可想而知, 在这里充斥着错综复杂的调用关系,如在second.c中还有一个函数需要调用first.c文件中的play1函数, 如何实现呢?
Sencond.h 文件
play1();
sencond.c文件
***()
{
Play();
}
例程-5
在sencond.h文件内声明play1函数,怎么能调用到first.c文件中的哪个play1函数中呢? 是不是搞错了,没有搞错, 这里涉及到c语言的另一个特性:存储类说明符.
C语言的存储类说明符有以下几个, 我来列表说明一下
说明符 用法
Auto 只在块内变量声明中被允许, 表示变量具有本地生存期.
Extern 出现在顶层或块的外部变量函数与变量声明中,表示声明的对象
Static 具有静态生存期, 连接程序知道其名字.可以放在函数与变量声明中. 在函数定义时, 其只用于指定函数名,而不将函数导出到连接程序. 在函数声明中,表示其后面会有定义声明的函数, 存储类为static. 在数据声明中, 总是表示定义的声明不导出到连接程序.
无疑, 在例程-5中的second.h和first.h中,需要我们用extern标志符来修饰play1函数的声明,这样,play1()函数就可以被导出到连接程序, 也就是实现了无论在first.c文件中调用,还是在second.c文件中调用,连接程序都会很聪明的按照我们的意愿,把他连接到first.c文件中的play1函数的定义上去, 而不必我们在second.c文件中也要再写一个一样的play1函数.
但随之有一个小问题, 在例程-5中,我们并没有用extern标志符来修饰play1啊, 这里涉及到另一个问题, C语言中有默认的存储类标志符. C99中规定, 所有顶层的默认存储类标志符都是extern . 原来如此啊, 哈哈. 回想一下例程-4, 也是好险, 我们在无知的情况下, 竟然也误打误撞,用到了extern修饰符, 否则在first.h中声明的play1函数如果不被连接程序导出,那么我们在在play2()中调用他时, 是找不到其实际定义位置的 .
那么我们如何来区分哪个头文件中的声明在其对应的.c文件中有定义,而哪个又没有呢? 这也许不是必须的,因为无论在哪个文件中定义,聪明的连接程序都会义无返顾的帮我们找到,并导出到连接程序, 但我觉得他确实必要的. 因为我们需要知道这个函数的具体内容是什么,有什么功能, 有了新需求后我也许要修改他, 我需要在短时间内能找到这个函数的定义, 那么我来介绍一下在C语言中一个人为的规范:
在.h文件中声明的函数,如果在其对应的.c文件中有定义,那么我们在声明这个函数时,不使用extern修饰符, 如果反之,则必须显示使用extern修饰符.
这样,在C语言的.h文件中,我们会看到两种类型的函数声明. 带extern的,还不带extern的, 简单明了,一个是引用外部函数,一个是自己生命并定义的函数.
最终如下:
Sencond.h 文件
Extern play1();
上面洋洋洒洒写了那么多都是针对函数的,而实际上.h文件却不是为函数所御用的. 打开我们项目的一个.h文件我们发现除了函数外,还有其他的东西, 那就是全局变量.
在大型项目中,对全局变量的使用不可避免, 比如,在first.c中需要使用一个全局变量G_test, 那么我们可以在first.h中,定义 TPYE G_test. 与对函数的使用类似, 在second.c中我们的开发人员发现他也需要使用这个全局变量, 而且要与first.c中一样的那个, 如何处理? 对,我们可以仿照函数中的处理方法, 在second.h中再次声明TPYE G_test, 根据extern的用法,以及c语言中默认的存储类型, 在两个头文件中声明的TPYE G_test,其实其存储类型都是extern, 也就是说不必我们操心, 连接程序会帮助我们处理一切. 但我们又如何区分全局变量哪个是定义声明,哪个是引用声明呢?这个比函数要复杂一些, 一般在C语言中有如下几种模型来区分:
1、 初始化语句模型
顶层声明中,存在初始化语句是,表示这个声明是定义声明,其他声明是引用声明。C语言的所有文件之中,只能有一个定义声明。
按照这个模型,我们可以在first.h中定义如下TPYE G_test=1;那么就确定在first中的是定义声明,在其他的所有声明都是引用声明。
2、 省略存储类型说明
在这个模型中,所有引用声明要显示的包括存储类extern, 而每个外部变量的唯一定义声明中省略存储类说明符。
这个与我们对函数的处理方法类似,不再举例说明。
这里还有一个需要说明,本来与本文并不十分相关,但前一段有个朋友遇到此问题,相信很多人都会遇到, 那就是数组全局变量。
他遇到的问题如下:
在声明定义时,定义数组如下:
int G_glob[100];
在另一个文件中引用声明如下:
int * G_glob;
在vc中,是可以编译通过的, 这种情况大家都比较模糊并且需要注意,数组与指针类似,但并不等于说对数组的声明起变量就是指针。 上面所说的的程序在运行时发现了问题,在引用声明的那个文件中,使用这个指针时总是提示内存访问错误,原来我们的连接程序并不把指针与数组等同,连接时,也不把他们当做同一个定义,而是认为是不相关的两个定义,当然会出现错误。正确的使用方法是在引用声明中声明如下:
int G_glob[10];
并且最好再加上一个extern,更加明了。
extern int G_glob[10];
另外需要说明的是,在引用声明中由于不需要涉及到内存分配,可以简化如下,这样在需要对全局变量的长度进行修改时,不用把所有的引用声明也全部修改了。
extern int G_glob[];
C语言是现今为止在底层核心编程中,使用最广泛的语言,以前是,以后也不会有太大改变,虽然现在java,.net等语言和工具对c有了一定冲击,但我们看到在计算机最为核心的地方,其他语言是无论如何也代替不了的,而这个领域也正是我们对计算机痴迷的程序员所向往的。
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=611081
tianlebo补上:我最近也碰到类似的问题,在DSP2407开发过程中有一个头文件是定义寄存器地址的文件,这个文件在很多.c文件中都是要包含的,所以我就在相应要包含的的地方用include"***.h",
这个***.h文件中是这样写的
#ifndef _SEED_DEC_2407_H
#define _SEED_DEC_2407_H
volatile unsigned int* IMR = (volatile unsigned int *) 0x0004;
volatile unsigned int* IFR = (volatile unsigned int *) 0x0006;
volatile unsigned int* SCSR1 = (volatile unsigned int *) 0x7018;
................
..............
#endif
结果在CCS2.0中编译:是每一个C文件编译都能过,但是.obj文件链接时就报错,说重复定义寄存器,搞了一天多都没搞定,我就换到VC6.0中编译也一样通不过。最后一个朋友告诉我,当第一个头文件用到***.h时相当于定义了全局变量,并初始化了,第二个c文件再用***.h时应该用extern +寄存器了,按他一试,还真行啊!但是我不清楚,为何不能再在其他c文件中包含***.h,知道的朋友可以讲讲!!!
----------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------------------
每个C++/C程序通常分为两个文件。一个文件用于保存程序的声明(declaration),称为头文件。另一个文件用于保存程序的实现(implementation),称为定义(definition)文件。
C++/C程序的头文件以“.h”为后缀,C程序的定义文件以“.c”为后缀,C++程序的定义文件通常以“.cpp”为后缀(也有一些系统以“.cc”或“.cxx”为后缀)。
1.1 版权和版本的声明
版权和版本的声明位于头文件和定义文件的开头(参见示例1-1),主要内容有:
(1)版权信息。
(2)文件名称,标识符,摘要。
(3)当前版本号,作者/修改者,完成日期。
(4)版本历史信息。
/*
* Copyright (c) 2001,老妖工作室
* All rights reserved.
*
* 文件名称:filename.h
* 文件标识:见配置管理计划书
* 摘 要:简要描述本文件的内容
*
* 当前版本:1.1
* 作 者:输入作者(或修改者)名字
* 完成日期:2001年7月20日
*
* 取代版本:1.0
* 原作者 :输入原作者(或修改者)名字
* 完成日期:2001年5月10日
*/
示例1-1 版权和版本的声明
1.2 头文件的结构
头文件由三部分内容组成:
(1)头文件开头处的版权和版本声明(参见示例1-1)。
(2)预处理块。
(3)函数和类结构声明等。
假设头文件名称为 graphics.h,头文件的结构参见示例1-2。
【规则1-2-1】为了防止头文件被重复引用,应当用ifndef/define/endif结构产生预处理块。
【规则1-2-2】用 #include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。
【规则1-2-3】用 #include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。
【建议1-2-1】头文件中只存放“声明”而不存放“定义”
在C++ 语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。
【建议1-2-2】不提倡使用全局变量,尽量不要在头文件中出现象extern int value 这类声明。
// 版权和版本声明见示例1-1,此处省略。
#ifndef GRAPHICS_H // 防止graphics.h被重复引用
#define GRAPHICS_H
#include <math.h> // 引用标准库的头文件
…
#include “myheader.h” // 引用非标准库的头文件
…
void Function1(…); // 全局函数声明
…
class Box // 类结构声明
{
…
};
#endif
示例1-2 C++/C头文件的结构
1.3 定义文件的结构
定义文件有三部分内容:
(1) 定义文件开头处的版权和版本声明(参见示例1-1)。
(2) 对一些头文件的引用。
(3) 程序的实现体(包括数据和代码)。
假设定义文件的名称为 graphics.cpp,定义文件的结构参见示例1-3。
// 版权和版本声明见示例1-1,此处省略。
#include “graphics.h” // 引用头文件
…
// 全局函数的实现体
void Function1(…)
{
…
}
// 类成员函数的实现体
void Box::Draw(…)
{
…
}
示例1-3 C++/C定义文件的结构
1.4 头文件的作用
早期的编程语言如Basic、Fortran没有头文件的概念,C++/C语言的初学者虽然会用使用头文件,但常常不明其理。这里对头文件的作用略作解释:
(1)通过头文件来调用库功能。在很多场合,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
(2)头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
=======================================================
关于多文件程序结构的头文件设计的若干问题研究(转)
通过一段时间的编程实践,对较大的项目组织有一些心得了,那天又看到钱能的<<C++程序手机教程>>中关于头文件设计的建议,于是总结了以下几点头文件设计方法及文件结构组织方法:
1. 怎么组织头文件和源文件的结构。
本文中的头文件专指.h文件,源文件专指.c或.cpp文件,模块指完成一定功能的程序文件的组合。每模块仅包含唯一的源文件及唯一的同名头文件。该头文件中可包含其他头文件。
头文件遵循"界面头文件"的思想,充分表达该模块的功能。用于声明extern全局函数,声明extern全局变量,类型定义,包含其他头文件等。
源文件用于声明static静态函数,定义函数,定义全局变量,定义static静态全局变量。
2. 全局变量的声明和定义问题。
全局变量可以使用extern关键字在多个模块中声明,但是只能且必须在其中一个模块中定义。比较好的一个解决方案是,将全局变量extern声明到一个公共头文件中,要使用该全局变量的模块都include它,然后在main所在的源文件中定义这个全局变量。这个方案可以比较好地兼顾模块独立性并解决变量重复定义的问题。
3. 全局常量的声明和定义问题。
全局常量在声明时就必须定义,所以其声明和定义实际上是同一个问题。每个模块都可以最多定义一次同一个全局常量。全局常量也可以放在公共头文件中,每个模块都include它。
4. 静态全局变量和常量的声明和定义问题。
静态全局变量和常量的作用域仅在模块内部,使用static关键字在本模块的源文件中声明。
5. 函数的声明和定义问题。
全局函数在头文件中声明,而在源文件中定义;静态函数的声明和定义均在源文件中。
6. 保护性的预编译代码问题。
头文件要使用#ifndefine…#define…#endif做保护,以免被多次包含。
后来又向程序设计方法学的老师求证了一下,如果项目中确实能严格按照这几条组织文件,可以很好的解决模块独立和头文件互相包含的问题
=======================================================
19号下午到学校,用了2天多的时间将自己的挑战杯项目中的SMS模块,作了一个大幅度的修改。重写了短信PDU的编码解码代码,修改了很多接口的设计,功能比之前强大了很多,代码的结构比之前也清晰了很多,但是由于时间的限制,没有全部重写,所以也不是很完美。2天中重写了尽1000行代码,修改的也将近有1000行,总共2000行左右,每天超过12小时的编码,有点吃不消,不知道真正的工作中,面对实际项目的压力时,自己能不能跟的上。在更改的过程中,最后的接口的设计、C文件和H文件的组织问题,我不是特别的在行,毕竟没有写过特别大的程序,没有设计的经验。我在网上查了很多方面的资料,小小学习了一下,当然这些肯定是不够的。下面开始正题
————————————————————————————————————————————————————
在我们语言的初学阶段,往往我们的程序只有一个.c的文件或这很少的几个,这时我们就很少遇到头文件组织这个头疼的问题,随着我们程序的增加,代码量到了几千行甚至几万行,文件数也越来越多。这时这些文件的组织就成了一个问题,其实说白了这些文件的组织问题从理论上来说是软件工程中的模块设计等等的问题,但是这次我不说这些,这次从C语言的语法机制和编译原理上来说说文件的组织问题。
C语言中分两种文件.c 和 .h 。分别叫作源文件和头文件,简单的说是.c中放定义,.h中放声明,初学C语言的人都会将c文件中所有的函数都在h文件中声明一下,这样好让c文件中的其他函数知道有这个函数,main函数也可以写在c文件中的最前面(在预处理之后),其实这并不是一个很好的方法。大家都知道C语言中没有C++中类的机制,可以将几个函数设为private,另外几个设为public,对外公布为类接口。但是C语言中static定义,在函数前加上static定义如
1 | static int get( int x) ; |
这样这个函数就只能在被c文件中的函数访问,外部文件是不能访问的。这个特性可以让我们用一个c和h文件来代表一个类或者模块。c文件中包括所有的函数定义,h文件中不书写所有的函数声明,只写想留出的接口,当然这些接口没有static属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 | test.h int ok_get( ) ;/* 接口 */ test.c #include "test.h" static int get( int x) //不能被外部文件访问,相当于被隐藏了 { //............ } int ok_get( ) { //......... } |
.h 被.c引用和 .h 互相引用的问题
这里我们还先理解下,C编译器的编译机制。C语言的编译可以分为预处理、编译、汇编、链接(test.c test.h => test.i => test.s => test.o => test)四个大的阶段。c文件中的#include宏处理,会在预处理的阶段将c中引用的h文件的内容全部写到c文件中,最后生成.i中间文件,这时h文件中的内容就相当于被写道c文件中。这也为代码的复用提供了渠道,很多的c文件可以去引用同一个h文件,这样这个h文件就会被放到多个c文件中被编译多次,这也是h文件中不能放定义只能放声明的原因,放定义时被编译多次,在程序链接的时候(系统中定义了多个int a;强符号定义)会出现错误, 声明就不一样,声明表示对定义的扩展,最终都会终结到一个定义上,所以不会出现link时重复定义的错误。
编程中我们在h文件中肯定都用过一下的格式
1 2 3 4 | #ifndef XXX_H #define XXX_H //…… #endif |
呵呵,那他到底有什么用呢,在h文件互相引用时,消除重复定义。当然宏定义是在预处理阶段发挥作用的,编译方后的过程是没有宏的影子的。
1 2 3 4 5 6 7 8 9 10 11 12 | A.h int a( ) ; B.h #include "A.h" C.h #include "A.h" D.h #include "A.h" #include "B.h" |
上面的D.h文件中就会重复出现两个int a();的声明阿,这样就有点重复了,这时条件编译宏就派上了用场
1 2 3 4 5 | A.h #ifndef A_H #define A_H int a( ) ; #endif |
这样就不会重复定义了。