总结一下我个人的编程风格及这样做的原因吧,其实是为了给实验室写一个统一的C语言编程规范才写的。首先声明,我下面提到的编程规范,是自己给自己定的,不是c语言里面规定的。
一件事情,做成和做好中间可能隔了十万八千里。
同样的,代码的质量也极大程度上反映了编程者的水平高低。为了让大家从学习的开始就养成良好的编程习惯,创作出优质的代码,实验室编辑这个文档,作为大家编程的参考,同时也是对以后编程风格的硬性规定。
对于一个团队来讲,制定统一的编程规范,好处是显而易见的。通常一个项目是由多个成员共同完成,在项目中,经常互相调用组内成员的代码。如果两个人的编程习惯和风格差异显著,那么将会浪费大量时间在读懂代码上。相反,一致而良好的编程规范,会让合作开发变得轻松而高效。
众所周知,C语言是面向过程的语言。也就是说,程序员要对程序的每一步有精准的把握,知道每一条程序语句的执行内容及其结果。因而,代码的可读性就显得尤为重要。这里的可读,不仅仅是对自己可读,也要对其他人可读。一段只有自己能读懂的代码,可以说价值很低,而且这样的代码随着时间的推移往往自己也读不懂。而可读性强的代码,不仅方便移植与交流,更给调试带来了难以估量的便利。
读一段好的代码,会有一种读英语文章的流畅感。尽管C语言提供了有限的32个关键字,但是变量、函数等的命名却提供了较大的*,这也是我们将代码语句化的基础。试想,如果一段代码有了主谓宾结构,即使不懂编程的人,也能明白代码的功能。而这正是我们代码编辑者追求的目标。
所以,写好一段代码,从把你的代码读者当编程小白开始!
一、文件管理
每一个做技术的人,无论软硬件,计算机里都应该有一个纯英文的盘符,注意我是说英文,而不是pinyin。在这个纯英文的盘符里,当然是存放各种技术相关的软件、程序以及文档。而这些内容的命名也应该是英文的,包括各个子文件夹。其他诸如即时通讯软件、游戏文件等应该放在其他盘符内。一方面,这样是对自己英文水平的锻炼;另一方面,也能避免很多在使用国外软件的时候出现的各种BUG。
每一个软件,都应该放在一个独立的文件夹中。这样既方便查找,又避免混乱。因为我们都知道每一个软件完成后,都不仅仅是一个exe文件那么简单,通常还有各种后缀的文件,而这些我们都不能删除。如果打开D盘时,映入眼帘的是几万个由不同软件安装时生成的各种文件,相信给谁都会一脸大写的懵逼。因此,将不同的软件放在单独的文件夹下是非常有必要的。
不同IDE下编写的程序,也都应该存放在独立的一个文件夹下。文件夹内,不同的工程也应该分别建立文件夹,并合理而精准命名。这样为日后的查找带来极大的便利。
很多IDE在编写程序文件时,除了要建立Project(工程),还要建立Workspace,即工作空间。工作空间通常是指定一个空间(也就是文件夹),IDE启动时,自动打开该空间下的各个Project。因此,一个Workspace可以存放多个Project。这样我们就可以利用Workspace管理自己在该IDE下编写的各个Project。前提是你建立了Workspace,而Project存放在这个Workspace下。
每一个独立的项目都应该是一个独立的Project。例如,分别练习编写流水灯和数码管的程序时,要分别建立Project,而不能放在一个Project下,除非你的项目同时用到了流水灯和数码管。这样做的好处是你可以Project名称上精确获得其内容信息,而不会出现程序写完过一段时间后无从查找的情况。
二、命名规则
首先说一下总的命名规则:命名一定要用英文。并不是因为拼音不可以,而是因为我们要与国际接轨,要养成良好的英文书写习惯。其次,命名中除了“\/:*?”<>|“等系统不允许的字符外,也不能出现除英文字母、下划线、数字外的其他字符。如果你想命名成flash LED.c,中间的空格要用下划线”_“来代替,写成flash_LED.c。另外,命名中可以出现必要的数字。
1、文件/文件夹命名
文件命名要精确,文件名要准确反映文件内容。写的是
文件命名一律使用小写字母,如keyboard.c。
如有缩写单词,则必须大写,如flash_LED.c、UART.c。其中LED是Light-Emitting Diode(发光二极管)的缩写,UART是Universal Asynchronous Receiver/Transmitter(通用异步收发器,也就是串口)的缩写。对于有约定俗成缩写的单词,就使用缩写词汇。
文件名应使用名词,而不应该使用动词。如果文件内容是数据采集,应该命名为data_collection.c而非data_collect.c。
2、标识符命名
C语言中,可以定义各种标识符作为变量名、数组名、函数名、标号及用户定义对象的名称。ANSI C规定标识符必须由字母和下划线开始,随后可以出现字母、下划线和数字。
1)变量命名
变量命名一律小写,缩写词汇用大写,且全部使用名词,可以使用形容词修饰,用“_”表从属关系。因为变量名作为一个变量的名字,就应该是一个名词。
局部循环体控制变量用i,j,k。如for(i=0;i<100;i++)。
指针变量用“p_”开头,后面接指向内容。如指向高度变量的指针,命名为“*p_height”。请读者自行区分指针和指针变量的区别。
局部变量尽量用一个单词表达清其含义。
全局变量命名时首先写所属模块名称。例如如一个传感器文件sensor.c里面的一个全局变量要代表温度,则命名为sensor_temperature。又例如LCD(液晶显示屏)文件LCD.c中表示LCD状态的全局变量命名为LCD_status。因为全局变量往往跨文件调用,如不写清变量定义位置,当程序庞大,而IDE又不支持一键定位时,查找起来很麻烦。即使IDE支持一键定位,一个清楚明白的命名,能让人瞬间读懂该变量的含义。
2)数组命名
数组命名各单词首字母大写,其他同变量。
读者可能会有疑问,数组名后面会有[]符号,与变量区别明显,为什么要用首字母大写的方式。实际上,在数组名作为实参传递数组首地址时,往往会省略[]符号,应该数组名就是数组的首地址。例如:
unsigned char string[]=”abcdefg”;
printf(“%s”,string);
在以上代码中,string是一个8位数组(为什么是8位?),在使用printf()函数输出时,只写了数组名,显然这种方式是被允许的。而此时就没有写[],在这种情况下,并不能瞬间知道string是变量还是数组,而需要参考前面的格式控制符“%s”。在其他函数中,或许没有“%s”这样的格式控制符帮助我们判断string到底是数组还是变量,我们只有找到函数的声明或定义才能知道答案,严重影响阅读。因此有必要对数组和变量加以区分。
3)函数命名
函数命名各单词首字母大写,写成主谓语形式,主语用名词,谓语用动词,缩写词汇用大写,用“_”表从属关系。主语通常为模块名,而谓语是描述模块的动作。因为函数本身就是用来执行一系列的动作的, 结合函数参数,可以表达通顺的语句。举个简单的例子:延时函数。定义一个ms级延时函数为:
void Delay(unsigned int ms);(这个其实是声明,函数体不想写了)
调用时写:
Delay(500);
很显然是延时了500ms。而如果再用个宏定义:
#define MS500 500
Delay(MS500);
是不是更一目了然呢?
另外还比如串口发送函数命名UART_TX( ),调用时写成:
UART_TX(time); (通常发送数据Transmit Data简写为TXD)
显然意思是串口发送时间数据。
再比如设置参考值的函数命名为REF_Set( ),调用时写成:
REF_Set(current_voltage);(通常参考值Reference简写为REF)
显然意思是将当前的电压设置为参考值。
主谓格式的命名大大增加了代码的可能性。
当然,函数命名中必要时可以出现宾语。这种情况多出现在函数没有参数的情况下。如一个函数的功能是LCD显示时间,而时间是全局变量,因此这个函数就不需要参数,此时直接定义成void LCD_Display_Time(void)(其实是声明,因为没写函数体)。
命名时首字母大写不会和数组混淆吗?显然不会,因为函数不论是在定义、声明还是调用的时候后面都必须跟着”( )”。
4)标号命名
由于在硬件编程中标号可以用循环来代替,所以很少用到。我们规定标号的命名格式基本同变量,使用全部小写的名词,但是只用一个单词表示即可。因为标号时候的时候或者前面加了goto,或者后面加了“:”,很容易与变量区分开。况且只是一个定位标志,所以一个单词足够了。
5)自定义类型命名
自定义类型命名主要指使用typedef定义的新类型名,以及结构体类型、共用体类型的类型名(而非该类型的变量名)。
自定义的新类型名,只用一个单词,首字母大写。但是定义这种新类型的变量时,命名规则与变量命名规则完全相同。
请自行体会新类型名与新类型变量的区别。
6)宏定义命名
宏定义命名全部使用大写字母,单词数不限。可以加入数字和下划线,但是数字不能开头 。
由于宏定义的特殊性,对其使用名词或动词不作规定。因为宏定义一个函数时,应该是动词性质,而宏定义一个常数时,应该是名词性质。
三、表达式书写
表达式书写时,最重要的是意义明确。由于C语言不同运算符有着不同的结合顺序和优先级,因此很容易造成歧义,即实际运算顺序与设想运算顺序不同。除了完全理解并熟记结合顺序与优先级,最简单的方法就是用括号来明确运算顺序——在表达式中,括号的优先级是最高的。
另外,运算符与其操作数之间要空格。如:
a=a+b;
应写成:
a = a + b;
这样做可以让表达式显得不那么拥挤而增加可读性,但这不是重点。这样做的重点是帮我们避免很多不易识别的错误。如:
a=a/*b;
我们的本意是a除以指针变量b指向的内容,然后将商赋给a。然而残酷的现实是,编译器发现了连起来的“/*”,没错,这是注释符。所以,后面的内容都会被注释掉,直到找到最近的“*/”。
所以我们应该写成:
a = a / *b; //指针运算符*应该紧跟指针变量b
或者:
a=a/(*b); //不过即便这样写也应该加入空格,便于阅读
有人会说,现在的IDE会用不同的颜色提示注释内容,所以这样的错误应该不会出现。但是我想说的是,作为一个立志做合格的工程师的你,会允许自己有不严谨的习惯吗?况且本身我们的文档是为了在C语言语法、词法基础上,制定一个编程规范。
另外,有些老版本的C编译器允许用=+来代替+=的含义,即复合赋值号的两个符号顺序可以是反的。这样的话,如果写出:
a=-1;
本意是将-1赋给a,但是编译器却会理解成:
a = a - 1;
显然意义完全变了。
有人又会说了,你不是说老版本的C编译器嘛,我不用不就行了吗。然而,我们要考虑代码的可移植性,就绝不应该允许这样的想法。
因此,在书写表达式的时候,不要吝惜你的空格和括号。
还有一点值得说明的是,复合赋值运算符的两个运算符不能分开。如“+=”不能写成“+ =”。
四、文件编写
1、文件划分
一个简单的程序,只有几行到几十行,放在一个文件内一目了然。但是一个较大的项目中可能会有成千上万行代码,更有大型程序代码数以百万行计。这样规模的代码,存放在一个文件内,其恐怖程度请自行想象。
当一个函数的代码量超过几十行时,就应该考虑有没有可能把其中某些代码提取出来打包成另一个函数然后调用。同样的,当一个文件的代码量超过几百行时,就应该考虑有没有可能把一些函数分出来放到别的文件中去。这样做都是为了程序的可读性和方便调试,毕竟一个较短的函数功能测试要比一个长函数容易得多。
然而,一个更好的划分文件的依据应该是按模块划分。当然,相应的划分函数的依据应该是按功能划分。也就是说,一个文件存放一个模块的内容,一个函数完成单一的功能。
2、文件内容
在C语言编程时,有两种文件。一种是源文件(source file,后缀为.c),另一种是头文件(head file,后缀为.h)。
C语言的编译是以c文件为单位的,因此只有h文件时是无法编译的。根据项目规模大小,一个项目可以由单个c文件构成,也可以有多个c文件和h文件共同构成。
C语言编译器在编译时,通常经历以下步骤:
预处理→语法、词法分析→编译→汇编→链接。
预处理阶段,将根据预处理指令来修改c文件内容。其中,预处理指令包括宏定义(#define)、条件编译指令(#ifdef、#ifndef、#endif等)、头文件包含指令(#include)、特殊符号(LINE、FILE等)。对于头文件包含指令来讲,其作用是将所包含h文件中的内容替换到包含指令处,当然如果内容中有其他预处理指令,也会做相应处理。
因此,h文件在编译时将插入到c文件中。由此可见,h文件可以出现任何符合c语言语法的内容,但是在实际编程中,我们显然不会这样做,因为这样做就失去了区分c文件和h文件的意义。
h文件最大的意义是作为对外接口使用,在发布库文件时作用更是明显。也就是说,h文件的内容用来提供供其他文件或函数调用的函数原型、变量等内容。下面具体来规定c文件和h文件中应该出现的内容:
源文件(.c) |
头文件(.h) |
头文件包含指令(#include) |
头文件包含指令(#include) |
宏定义(#define) |
|
所有函数定义(必须有函数体,即{ }) 内部函数声明(static,不能有函数体) |
外部函数声明(extern,不能有函数体) |
外部变量定义(必须赋初值) 静态外部变量定义(static,必须赋初值) |
外部变量声明(extern,不能赋值) |
自定义类型(typedef) |
|
外部数组定义 静态内部数组定义(static) |
外部数组声明(const) |
条件编译 |
条件编译 |
由上表可以看出,h文件内存放的都是对外可见的变量、函数数组等的声明,宏定义则是对内对外都可以使用,放在这里主要为了修改方便。在定义外部变量、数组和函数时,不需要写extern,因为缺省时默认extern。而声明外部变量、数组和函数时,必须用extern显式声明,这样是为了让代码更直观。
函数说明是必须要写的,写清函数的入口、出口参数及其功能,以及其它说明,对于代码维护和改写能带来极大的方便。
通常,如果h文件中全部是对外接口,而对应c文件中各函数均不调用本文件中的其他内容(变量、函数等),也可以不用包含自身的h文件。
另外,程序编写时,缩进要规范,要能表达所属层次关系。每次缩进4个字符,不能随意缩进。
关于函数体或组合语句使用{}的格式,常见的有两种格式:
int main( ){
}
或者:
int main( )
{
}
本人比较偏向第一种,因为可以节省行数,让程序紧凑。但是这个问题见仁见智,有人觉得第一种不如第二种对齐方式层次分明。所以这个就让两种方式并存吧。因为其他问题不涉及审美习惯,只要规定好大家执行就好了,这个毕竟涉及到每个人的审美不同。
h文件中必须在开头和末尾写条件编译:
#ifndef __全大写文件名_H__ (或者写成:全大写文件名_H__)
#ifndef __全大写文件名_H__
…(文件内容)
#enif
这样做是为了防止多次包含,保证在编译时前面已经替换过该头文件,后面将不再替换,否则有些内容可能重复定义。
下面用代码示例:
<protocol.h>:(每一个h文件中必须有√标注的内容)
#include <msp430x14x.h> //头文件包含
#ifndef __PROTOCOL_H__ //条件编译 √
#define __PROTOCOL_H__ //条件编译 √
//#define MONITOR_TERMINAL //条件编译
#define MONITOR_NODE1 //条件编译
//#define MONITOR_NODE2 //条件编译
#define MATCHING_CODE 0x55 //宏定义
#define HOST_ADDRESS 0x40 //宏定义
#define NODE_1 0x41 //宏定义
#define NODE_2 0x42 //宏定义
typedef struct { //自定义类型
float start_bit;
float TXD_data;
float stop_bit;
}TX_Data;
extern unsigned char Tx_Data_Packet[]; //外部数组声明
extern unsigned char Rx_Data_Packet[]; //外部数组声明
extern unsigned char protocol_set_flag; //外部变量声明
extern unsigned char Extract_Data(void); //外部函数声明
#endif //条件编译 √
<protocol.c>:
#include<math.h> //头文件包含,系统库函数用<>
#include"protocol.h" //头文件包含,系统库函数用“”
Static unsigned char easy_delay(void); //内部函数声明
unsigned char protocol_set_flag = 0; //外部变量定义
unsigned char Tx_Data_Packet[6] = {'0','1','2','3','4','5'}; //外部数组定义
unsignedchar Rx_Data_Packet[6] = {'0','1','2','3','4','5'}; //外部数组定义
Static char temp_Packet[6] = {'0','1','2','3','4','5'}; //静态外部数组定义,只能本文件使用
/********************************************************
*名 称:Extract_Data()
*功 能:提取接收到的数据帧
*入口参数:无
*出口参数:1-成功,0-失败
*说 明:
********************************************************/
unsigned char Extract_Data(void){
unsigned char temp = 0;
temp = Rx_FIFO_ReadChar();
if( temp == MATCHING_CODE ){
UART_TX_OPEN();
Rx_Data_Packet[0] = temp;
Rx_Data_Packet[1] = Rx_FIFO_ReadChar(); //来源
Rx_Data_Packet[2] = Rx_FIFO_ReadChar(); //去向
Rx_Data_Packet[3] = Rx_FIFO_ReadChar(); //光照+温度高
Rx_Data_Packet[4] = Rx_FIFO_ReadChar(); //温度低
Rx_Data_Packet[5] = '\0';
return (1);
}
else return (0);
} //外部函数定义,必须在前面写函数说明
/********************************************************
*名 称: easy_delay()
*功 能:简单延时
*入口参数:无
*出口参数:无
*说 明:
********************************************************/
Static unsigned char easy_delay(void){
unsigned int i = 0;
for( i=0; i<1000 ; i++);
} //内部函数定义,必须在前面写函数说明,且在本文件前部声明以便阅读
这两个文件都是从编者曾经写的代码中截取出来的,有些部分是为了演示内容现在添加进去的,源代码中不存在,请大家不必在意细节,关键领会两个文件中应该出现的内容,均在后面用注释的方式作了说明。
Notice:
本文中出现的很多字符,为了美观和直观,中英文输入法混用,或者加多个空格。大家在编程时,切记使用英文半角输入法,而且不管你加多少空格或制表符,编译器都按一个处理。