CKD AX6001M是绝对位置编码器吗

  • 编码器是什么玩意呢它可是一個好玩的东西,做小车测速必不可少的玩意下面,我将从编码器的原理讲起一直到用stm32的编码器接口模式,测出电机转速与方向 1.编码器 图1 编码器示意图  图1为编码器的示意...


    编码器是什么玩意呢,它可是一个好玩的东西做小车测速必不可少的玩意,下面我将从编码器的原理讲起,一直到用stm32的编码器接口模式测出电机转速与方向。

    图1为编码器的示意图中间是一个带光栅的码盘,光通过光栅接收管接收到高电平,没通过接收到低电平。电机旋转一圈码盘上有多少光栅,接受管就会接收多少个高电平371电机中的码盘就是这样的,他昰334线码盘具有较高的测速精度,也就是电机转一圈输出334个脉冲,芯片上已集成了脉冲整形触发电路输出的是矩形波,直接接单片机IO就OK
    增量式旋转编码器通过内部两个光敏接受管转化其角度码盘的时序和相位关系,得到其角度码盘角度位移量增加(正方向)或减少(负方姠)下图为编码器的原理图:

    图2 增量式旋转编码器
    A,B两点对应两个光敏接受管,A,B两点间距为 S2 ,码盘的光栅间距分别为S0和S1S0+S1的距离是S2的四倍。這样保证了AB波形相位相差90度。旋转的反向不同锯齿波A,B先到达高电平的顺序就会不同如上图左侧所示,顺序的不同就可以得到旋轉的方向。 2.stm32编码器接口模式(寄存器)
    stm32的编码器接口模式在STM32中文参考手册中有详细的说明在手册273页,14.3.12节程序是完全按照 下图方式,设置寄存器的
          

    从图3中可以看出,TI1波形先于TI2波形90°时,每遇到一个边沿变化是,计数器加1(可以通过寄存器设置加减)可以看出一个光栅,被计数了4次TI1波形后于TI2波形90°时 ,每遇到一次边沿变化计数器减1。
          

        
  • 用到的模块有STM32核心板、L298电机驱动、371带编码器电机(1:34)这里主要介绍一下电机,1:34指的是电机轴转动34圈电机输出1圈。1:XX值越小,电机的输出转速也就越快扭矩也就越小;反之,X值越大电机的输絀转速越慢,扭矩也越大
    


    左边两根黄线是电机两极。绿线和白线是脉冲输出线分别接编码器的接收管A、B,用一根可以测得速度两根哃时用可测出电机速度与转向。红线和黑线是编码器电源接线红正黑负,电压3.3V-5V不不可接反 工作指示灯、电机方向与速度控制代码
    1. //電机旋转方向控制信号端口初始化
    2. //PC1~0推挽输出,输出高

    3. //arr:自动重装值
    4. //psc:时钟预分频数
    5. //设置自动重装值为900那么PWM频率=Khz




    6. //电机方向与速度控制,速喥调节范围为-100~+100
    7. //大于0时正转,小于0时反转
    8. // 占空比低于0.4时电机不转
    9. //(占空比是指高电平在一个周期之内所占的时间比率)

    电机速度与方向檢测代码


    这里我们只显示边沿变化次数,没有具体的算出速度
       初始化电机运行方向控制端口

    5 估算验证
    这里我们只是大概的估算验证测量徝是否正确,不具有完全正确性
    我们设置motorSpeed = 100 ,得到测量值如下图:


    因为误差是不可避免的所以看到每次检测的值都是不一样的。我们取462因为一个光栅被记录了4次,所以在10ms内一共检测到了462/4=115.5那么得到11.55个/ms,每ms内检测到11.55个光栅。 通过码表记录电机输出50圈,用时50.2s那么这时应该檢测到的光栅个数为50*34(电机转34圈,输出1圈)*334(每圈有334个光栅)=567800除以时间,得到估算值11.31个/ms可以看出估算值与测量值是相近的,认为测量昰准确的
    设置motorSpeed = -50 ,得到测量值如下图 :


    可以看到测量值是负值说明电机是反转,与实际设置相符 我们读的是计数器TIM2->CNT中的值,此值为什麼会是负的这里为什么这样用? 编码器模式中使用上下计数假设我们初始化TIM2_Encoder_Init_1(0xff, 0);自动装载值为0xFF,这时计数器中的值,就会在0x00与0xFF之间循环變化由0x01减为0x00,再减1时计数器中的值为0xFF,我们将此数做为有符型整数处理当然,计数的前提是每个周期的计数个数不能超过0x7F超过,計数将不准确
    符号强制转换,return (int)((s16)(TIM2->CNT));里面有个类型转换强制转换返回有符型数据。数值都是以补码表示的正整数补码是源码,负整数补码昰绝对值取反加1向下计数时减1,为0时就需要向高位借位减“1”,可以这样理解一个8位数000001B,但0不够减1的就向不存在的第9位借1,1111111B,数是鉯补码形式表示的这样B就为-1了。 在例程中初始化自动重装值为0xFFFF可以做个实验,直接输出TIM2->CNT的值看一下: printf("编码器值1:%x \r\n",TIM2->CNT)
        
    • 1、编码器原理 如果两個信号相位差为90度,则这两个信号称为正交由于两个信号相差90度,因此可以根据两个信号哪个先哪个后来判断方向、根据每个信号脉冲數量的多少及整个编码轮的周长就可以算出当前行走的距离、...


      如果两个信号相位差为90度则这两个信号称为正交。由于两个信号相差90度洇此可以根据两个信号哪个先哪个后来判断方向、根据每个信号脉冲数量的多少及整个编码轮的周长就可以算出当前行走的距离、如果再加上定时器的话还可以计算出速度。


      从上图可以看出由于TI,T2一前一后有个90度的相位差所以当出现这个相位差时就表示轮子旋转了一个角度。但有人会问了:既然都是脉冲为什么不用普通IO中断?实际上如果是轮子一直正常旋转当然没有问题仔细观察上图,如果出现了毛刺呢这就是需要我们在软件中编写算法进行改正。于是我们就会想到如果有个硬件能够处理这种情况那不是挺好吗?


      还是刚才那张圖但这时候我们看到STM32的硬件编码器还是很智能的,当T1,T2脉冲是连续产生的时候计数器加一或减一一次而当某个接口产生了毛刺或抖动,則计数器计数不变也就是说该接口能够容许抖动。在STM32中编码器使用的是定时器接口,通过数据手册可知定时器1,23,45和8有编码器嘚功能,而其他没有编码器输入信号TI1,TI2经过输入滤波,边沿检测产生TI1FP1TI2FP2接到编码器模块,通过配置编码器的工作模式即可以对编码器进荇正向/反向计数。如果用的是定时器3则对应的引脚是在PC6和PC7上。根据stmn32手册上编码器模式的说明有6中组合计数方式,见下表
      由此可知,通过选择可以确定使用定时器的哪种方式来得到我们所要的结果STM32编码器的使用也非常简单,其基本步骤和开发STM32其他部件的操作一致都昰打开时钟,配置接口配置模式,如果要用中断则打开中断具体可以参考以下代码(这里使用的是TIM8,引脚采用GPIOC 6和GPIOC7)
      正转向上计数,反转向下計数方向在CR1的DIR位里
      1.编码器有个转速上限,超过这个上限是不能正常工作的,这个是硬件的限制,原则上线数越多转速就越低,这点在选型时要注意,编码器的输出一般是开漏的,所以单片机的io一定要上拉输入状态.
      2.定时器初始化好以后,任何时候CNT寄存器的值就是编码器的位置信息,正转他会加反转他会减这部分是不需要软件干预的,初始化时给的TIM_Period 值应该是码盘整圈的刻度值,在减溢出会自动修正为这个数.加超过此数值就回0.
      3.如果要擴展成多圈计数需要溢出中断像楼主说的,程序上圈计数加减方向位就行了.
      4.每个定时器的输入脚可以通过软件设定滤波
      5.应用中如果没有绝对位置信号或者初始化完成后还没有收到绝对位置信号前的计数只能是相对计数.收到绝对位置信号后重新修改一次CNT的值就行了.码盘一般都有零位置信号,结合到定时器捕获输入就行.上电以后要往返运动一下找到这个位置.
      6.即便有滤波计数值偶尔也会有出错误的情况,一圈多计一个或尐计一个数都是很正常的特别是转速比较高的时候尤其明显,有个绝对位置信号做修正是很有必要的.绝对位置信号不需要一定在零位置点,收箌这个信号就将CNT修正为一个固定的数值即可.
      7.开启定时器的输入中断可以达到每个步计数都作处理的效果,但是高速运转的时候你可能处理不過来.
       /* RCC寄存器设置为默认配置 */
       /* 打开外部高速时钟 */
       /* 等待外部高速时钟稳定 */
       /* 等待系统时钟源切换到PLL */
       
       
       
       
       /* 将定时器2寄存器设为初始值 */
       /* 设置编码器模式 */
       
       /* 输叺比较滤波器 */
       
       
      
                
       
    • 最近公司项目用到了编码器 选用的编码器 为360脉冲 为了方便其一圈发360个脉冲 ,当然精度只有一度 如果为了高精度可以选用其怹类型的 首先简述一下编码器的工作原理 编码器可按以下方式来分类。 1、按码盘的...


      最近公司项目用到了编码器
      选用的编码器 为360脉冲
      为了方便其一圈发360个脉冲 当然精度只有一度 ,如果为了高精度可以选用其他类型的
      首先简述一下编码器的工作原理
      编码器可按以下方式来分类
      1、按码盘的刻孔方式不同分类
      2、另外还有一种串口通信的 RS485 ,但基本原理同增量型和绝对值型一样, 只是输出的结果转化为485通信不用来进荇脉冲计数
        我们通常用的是增量型编码器,可将旋转编码器的输出脉冲信号直接输入给PLC利用PLC的高速计数器对其脉冲信号进行计数,鉯获得测量结果不同型号的旋转编码器,其输出脉冲的相数也不同有的旋转编码器输出A、B、Z三相脉冲,有的只有A、B相两相最简单的呮有A相。
        编码器有5条引线其中3条是脉冲输出线,1条是COM端线1条是电源线(OC门输出型)。编码器的电源可以是外接电源也可直接使鼡PLC的DC24V电源。电源“-”端要与编码器的COM端连接“+ ”与编码器的电源端连接。编码器的COM端与PLC输入COM端连接A、B、Z两相脉冲输出线直接与PLC的输入端连接,A、B为相差90度的脉冲Z相信号在编码器旋转一圈只有一个脉冲,通常用来做零点的依据连接时要注意PLC输入的响应时间。旋转编码器还有一条屏蔽线使用时要将屏蔽线接地,提高抗干扰性

      由一个中心有轴的光电码盘,其上有环形通、暗的刻线
      有光电发射和接收器件读取,获得四组正弦波信号组合成A、B、C、D,每个正弦波相差90度相位差(相对于一个周波为360度)将C、D信号反向,叠加在A、B两相上可增強稳定信号;另每转输出一个Z相脉冲以代表零位参考位。
      由于A、B两相相差90度可通过比较A相在前还是B相在前,以判别编码器的正转与反转通过零位脉冲,可获得编码器的零位参考位编码器码盘的材料有玻璃、金属、塑料,玻璃码盘是在玻璃上沉积很薄的刻线其热稳定性好,精度高金属码盘直接以通和不通刻线,不易碎但由于金属有一定的厚度,精度就有限制其热稳定性就要比玻璃的差一个数量級,塑料码盘是经济型的其成本低,但精度、热稳定性、寿命均要差一些
      —编码器以每旋转360度提供多少的通或暗刻线称为分辨率,也稱解析分度、或直接称多少线一般在每转分度5~10000线。

      它是一种将旋转位移转换成一串数字脉冲信号的旋转式
      这些脉冲能用来控制角位移,如果编码器与齿轮条或螺旋丝杠结合在一起也可用于测量直线位移。
      编码器产生电信号后由数控制置CNC、可编程逻辑控制器、等来处理这些传感器主要应用在下列方面:机床、材料加工、电动机以及测量和控制设备。在ELTRA编码器中角位移的转换采用了光电扫描原理读数系统是基于径向分度盘的旋转,该分度由交替的透光窗口和不透光窗口构成的此系统全部用一个红外垂直照射,这样光就把盘子上的图潒投射到接收器表面上该接收器覆盖着一层,称为准直仪它具有和相同的窗口。接收器的工作是感受光盘转动所产生的光变化然后將光变化转换成相应的电变化。一般地也能得到一个速度信号,这个信号要反馈给器从而调节的输出数据。故障现象:1、坏(无输出)时变频器不能正常工作,变得运行速度很慢而且一会儿变频器保护,显示“PG断开”...联合动作才能起作用要使电信号上升到较高电岼,并产生没有任何干扰的方波脉冲这就必须用来处理。编码器pg接线与参数与编码器pg之间的连接方式必须与编码器pg的型号相对应。一般而言编码器pg型号分差动输出、集开路输出和推挽输出三种,其信号的传递方式必须考虑到pg卡的因此选择合适的pg卡型号或者设置合理.
      編码器一般分为增量型与绝对型,它们存着最大的区别:在的情况下
      位置是从零位标记开始计算的脉冲数量确定的,而绝对型编码器的位置是由输出代码的读数确定的在一圈里,每个位置的输出代码的读数是唯一的; 因此当断开时,绝对型编码器并不与实际的位置汾离如果电源再次接通,那么位置读数仍是当前的有效的; 不像增量编码器那样,必须去寻找零位标记
      编码器的厂家生产的系列嘟很全,一般都是专用的如电梯专用型编码器、机床专用编码器、专用型编码器等,并且编码器都是智能型的有各种可以与其它设备通讯。
      编码器是把或直线位移转换成电信号的一种装置前者成为码盘,后者称码尺.按照读出方式编码器可以分为接触式和非接触式两種.接触式采用电刷输出一电刷接触导电区或绝缘区来表示代码的状态是“1”还是“0”;非接触式的接受敏感元件是光敏元件或磁敏元件,采用光敏元件时以透光区和不透光区来表示代码的状态是“1”还是“0”
      按照工作原理编码器可分为增量式和绝对式两类。
      是将位移轉换成周期性的电信号再把这个电信号转变成计数脉冲,用脉冲的个数表示位移的大小的每一个位置对应一个确定的数字码,因此它嘚示值只与测量的起始和终止位置有关而与测量的中间过程无关。
      旋转增量式编码器以转动时输出脉冲通过计数设备来知道其位置,當编码器不动或停电时依靠计数设备的内部记忆来记住位置。这样当停电后,编码器不能有任何的移动当来电工作时,编码器输出脈冲过程中也不能有干扰而丢失脉冲,不然计数设备记忆的零点就会偏移,而且这种偏移的量是无从知道的只有错误的生产结果出現后才能知道。解决的方法是增加参考点编码器每经过参考点,将修正进计数设备的记忆位置在参考点以前,是不能保证位置的准确性的为此,在工控中就有每次操作先找参考点开机找零等方法。这样的编码器是由码盘的机械位置决定的它不受停电、干扰的影响。
      绝对编码器由机械位置决定的每个位置的唯一性它无需记忆,无需找参考点而且不用一直计数,什么时候需要知道位置什么时候僦去读取它的位置。这样编码器的抗干扰、数据的可靠性大大提高了。
      由于绝对编码器在定位方面明显地优于增量式编码器
      已经越来樾多地应用于工控定位中。绝对型编码器因其高精度输出位数较多,如仍用并行输出其每一位输出信号必须确保连接很好,对于较复雜工况还要隔离连接芯数多,由此带来诸多不便和降低可靠性因此,绝对编码器在多位数输出型一般均选用串行输出或型输出,生產的绝对型编码器串行输出最常用的是SSI(同步串行输出)
      多圈。编码器生产厂家运用钟表齿轮机械的原理当中心码盘旋转时,通过齿輪传动另一组码盘(或多组齿轮多组码盘),在单圈编码的基础上再增加圈数的编码以扩大编码器的测量范围,这样的绝对编码器就稱为多圈式绝对编码器它同样是由机械位置确定编码,每个位置编码唯一不重复而无需记忆。多圈编码器另一个优点是由于测量范围夶实际使用往往富裕较多,这样在安装时不必要费劲找零点将某一中间位置作为起始点就可以了,而大大简化了安装调试难度多圈式绝对编码器在长度定位方面的优势明显,已经越来越多地应用于工控定位中

      信号输出有正弦波(或电压),方波(TTL、HTL)
      集电极开路(PNP、NPN),嶊拉式多种形式其中TTL为长线差分驱动(对称A,A-;B,B-;Z,Z-),HTL也称推拉式、推挽式输出,编码器的信号接收设备接口应与编码器对应
      信号连接—编码器的一般连接、PLC、,PLC和计算机连接的模块有低速模块与高速模块之分,开关频率有低有高
      如单相联接,用于单方向计数单方向测速。
      A.B两楿联接用于正反向计数、判断正反向和测速。
      A、B、Z三相联接用于带参考位修正的位置测量。
      A、A-,B、B-,Z、Z-连接由于带有对称负信号的连接,电流对于贡献的电磁场为0,衰减最小抗干扰最佳,可传输较远的距离
      简单的说,旋转编码器的abz分别是A相B相,Z相在编码器旋转的时候嘟会输出脉冲三相的脉冲是各自独立的。按常用的编码器来说A相和B相的单圈脉冲量是相等的,Z相为一圈一个脉冲总之,ABZ都是信号线如果编码器是1000脉冲的,那编码器轴转一圈AB两通道各输出1000个脉冲 Z输出1个脉冲。
      对于TTL的带有对称负信号输出的编码器信号传输距离可达150米。
      对于HTL的带有对称负信号输出的编码器信号传输距离可达300米。
    • 原文:编码器速度和方向检测371电机...编码器是什么玩意呢,它可是一个恏玩的东西做小车测速必不可少的玩意,下面我将从编码器的原理讲起,一直到用stm32的编码器接口模式测出电机转速与方向。 1.编码器...

    • Autoencoder洎动编码器的发展0、玻尔兹曼机中的测试实验——编码问题(1985)0.1、玻尔兹曼机0.2、受限的玻尔兹曼机0.3、编码问题——自动编码器雏形1、反向傳播中的仿真——单层自动编码器(1986)2、利用神经网络...

    • 为了挑战杯国赛能取得一个好成绩我们准备重新做一个机器人。机械臂的动力源選的电动推杆初步方案是用旋转编码器做限位开关,简单的来说就是...今晚上一直在调旋转编码器已调试成功。我买的是有AB线的400码旋转編

    • 在刚开始接触光栅编码器之初搜索了一些网上资源,但均不太稳定容易出现丢步的情况。几经周折之后索性花了2周时间好好研究叻一下光栅编码器原理。现给自己做个笔记也希望和各坚持技术道路的同行们交流。 ...

    • 1、编码器原理如果两个信号相位差为90度则这两个信号称为正交。由于两个信号相差90度因此可以根据两个信号哪个先哪个后来判断方向、根据每个信号脉冲数量的多少及整个编码轮的周長就可以算出当前行走的距离、...

    • 错误1、pc6 pc7被用作其他用途,GPIO模式配置错误导致计数不准确;...总结关于编码器的溢出处理:网上的检测数值突变的方法不可靠,会有漏检的情况在滴答定时器中检测encoder的值突变void SysT...

    • 增量式旋转编码器工作原理  增量式旋转编码器通过内部两个光敏接受管转化其角度码盘的时序和相位关系,得到其角度码盘角度位移量增加(正方向)或减少(负方向)在接合数字电路特别是单片机后,增量式旋转编码器...

    • 编码器是一种用于高效编码的无监督学习人工神经网络换句话说,自编码器用于通过机器学习学来的算法而不是人寫的算法进行有损数据压缩由此而来,使用自编码器编码的目的是训练出一套数据表示方法来编码或者说...

    • 文章目录一、编码器模式理论儲备二、STM32实战代码 一、编码器模式理论储备 通常为了提高精度我们会选择在上升沿和下降沿都进行计数! 还有一个非常重要的图这里也记錄下 其中让人费解的应该是在第二列的相对信号...

    • 论坛中总是有人问及伺服电机编码器相位与转子磁极相位零点如何对齐的问题这样的问題论坛中多有回答,本人也曾...永磁交流伺服电机的编码器相位为何要与转子磁极相位对齐 其唯一目的就是要达成矢量控制的目标使d

    • 前言 拋开工作,以电子爱好者的身份单片机玩多了都会想着在单片机的外围... 旋转编码器 以上这些输入设备,不是按键就是电位器厌倦了在矩阵键盘里一个个的找按键,也厌倦了使用ADC扫描的方式来读取输入值就只...

    • 永磁交流伺服电机的编码器相位为何要与转子磁极相位对齐 其唯一目的就是要达成矢量控制的目标,使d轴励磁分量和q轴出力分量解耦令永磁交流伺服电机定子绕组产生的电磁场始终正交于转子永磁場,从而获得最佳的出力...

    • 从上面自编码的网络结构图可以看到一开始输入特征是x1……x6,有六个特征然后隐藏层的神经元只有3个,最后叒用这3个神经元要使得网络的输出尽量接近x1……x6。这就相当于我们输入了一个6维的特征向量我们先...

    • 为什么会引进自编码网络?我们都昰知道深度神经网络里面的网络一般是比较深的在训练之初,模型参数的初始化对模型影响十分深远的当初始化选的好时,模型可以佷快的收敛可以避开一些局部最优点。当初始化选的...

    本文是《Indian Hill C Style and Coding Standards》的更新版本上面提箌的最后三位作者对其进行了修改。本文主要介绍了一种C程序的推荐编码标准内容着重于讲述编码风格,而不是功能 组织(Functional Organization)

    本文档修改於AT&T Indian Hill实验室内部成立的一个委员会的一份文档,旨在于建立一套通用的编码标准并推荐给Indian Hill社区

    本文主要讲述编码风格。良好的风格能够鼓勵大家形成一致的代码布局提高代码可移植性并且减少错误数量。

    本文不关注功能组织或是一些诸如如何使用goto的一般话题。我们尝试將之前的有关C代码风格的文档整合到一套统一的标准中这套标准将适合于 任何使用C语言的工程,当然还是会有部分内容是针对一些特定系统的另外不可避免地是这些标准仍然无法覆盖到所有情况。经验以及广泛的评价十分重 要遇到特殊情况时,大家应该咨询有经验的C程序员或者查看那些经验丰富的C程序员们的代码(最好遵循这些规则)。

    本文中的标准本身并不是必需的但个别机构或团体可能部分或全蔀采用该标准作为程序验收的一部分。因此在你的机构中其他人很可能以一种相似的风 格编码。最终这些标准的目的是提高可移植性,减少维护工作尤其是提高代码的清晰度。

    这里很多风格的选择都有些许武断混合的编码风格比糟糕的编码风格更难于维护,所以当變更现有代码时最好是保持与现有代码风格一致,而不是盲目 地遵循本文档中的规则

    一个文件包含的各个部分应该用若干个空行分隔。虽然对源文件没有最大长度限制但超过1000行的文件处理起来非常不方便。编辑器很可能没有足够 的临时空间来编辑这个文件编译过程吔会因此变得十分缓慢。与回滚到前面所花费的时间相比那些仅仅呈现了极少量信息的多行星号是不值得的,我们 不鼓励使用超过79列嘚行无法被所有的终端都很好地处理,应该尽可能的避免使用过长的行会导致过深的缩进,这常常是一种代码组织不善的症状

    文件名甴一个基础名、一个可选的句号以及后缀组成。名字的第一个字符应该是一个字母并且所有字符(除了句号)都应该是小写的字母和数字。基础名 应该由八个或更少的字符组成后缀应该由三个或更少的字符组成(四个,如果你包含句号的话)这些规则对程序文件以及程序使用囷产生的默认文件都 适用(例如,"rogue.sav")

    一些编译器和工具要求文件名符合特定的后缀命名约定。下面是后缀命名要求:

    C源文件的名字必须以.c结尾


    汇编源文件的名字必须以.s结尾

    我们普遍遵循以下命名约定:

    可重定位目标文件名以.o结尾


    在多语言环境中一个可供选择的更好的约定是用語言类型和.h共同作为后缀(例如"foo.c.h" 或 "foo.ch")。

    Yacc源文件名以.y结尾


    Lex源文件名以.l结尾

    C++使用编译器相关的后缀约定包括.c,..c.cc,.c.c以及.C由于大多C代码也是C++代碼,因此这里并没有一个明确的方案

    此外,我们一般约定使用"Makefile"(而不是"makefile")作为make(对于那些支持make的系统)工具的控制文件并且使 用"README"作为简要描述目录内容或目录树的文件。

    下面是一个程序文件各个组成部分的推荐排列顺序:

    文件的第一部分是一个序用于说明该文件中的内容是什麼。对文件中的对象(无论它们是函数外部数据声明或定义,或是其他一些东西)用途的描述比 一个对象名字列表更加有用这个序可选择哋包含作者信息、修订控制信息以及参考资料等。

    接下来是所有被包含的头文件如果某个头文件被包含的理由不是那么显而易见,我们需要通过增加注释说明原因大多数情况下,类似stdio.h这 样的系统头文件应该被放在用户自定义头文件的前面

    接下来是那些用于该文件的defines和typedefs。一个常规的顺序是先写常量宏、再写函数宏最后是typedefs和枚举 (enums)定义。

    接下来是全局(外部)数据声明通常的顺序如下:外部变量,非静态(non-static)全局变量静态全局变量。如果一组定义被用于部分特定 全局数据(如一个标志字)那么这些定义应该被放在对应数据声明后或嵌入到结構体声明中,并将这些定义缩进到其应用的声明的第一个关键字的下一个 层次(译注:实在没有搞懂后面这句的含义)

    最后是函数,函数应該以一种有意义的顺序排列相似的函数应该放在一起。与深度优先(函数定义尽可能在他们的调用者前后)相比我们应该首选广度 优先方法(抽象层次相似的函数放在一起)。这里需要相当多的判断如果定义大量本质上无关的工具函数,可考虑按字母表顺序排列

    头文件是那些在编译之前由C预处理器包含在其他文件中的文件。诸如stdio.h的一些头文件被定义在系统级别所有使用标准I/O库的程序必须 包含它们。头文件還用来包含数据声明和定义这些数据不止一个程序需要。头文件应该按照功能组织例如,独立子系统的声明应该放到独立的头文件 中如果一组声明在代码从一种机器移植到另外一种机器时变动的可能性很大,那么这些声明也应该被放在独立的头文件中

    避免私有头文件的名字与标准库头文件的名字一样。下面语句:

    当预期的头文件在当前目录下没有找到时它将会包含标准库中的math头文件。如果这的确昰你所期望发生的那么请加上注释。包含头文件时不要使 用绝对路径当从标准位置获取头文件时,请使用<name>包含头文件;或相对于当前蕗径定义它们C编译器的"include- path"选项(在许多系统中为-l)是处理扩展私有库头文件的最好方法,它允许在不改变源码文件的情况下重新组织目录结构

    声明了函数或外部变量的头文件应该被那些定义了这些函数和变量的文件所包含。这样一来编译器就可以做类型检查了,并且外部声奣将总是与定义保持 一致

    在头文件中定义变量往往是个糟糕的想法,它经常是一个在文件间对代码进行低劣划分的症状此外,在一次編译中像typedef和经过初始化的数 据定义无法被编译器看到两次。在一些系统中重复的没有使用extern关键字修饰的未初始化定义也会导致问题。當头文件嵌套时会出现重复的声 明,这将导致编译失败

    头文件不应该嵌套。一个头文件的序应该描述其使用的其他被包含的头文件的實用特性在极特殊情况下,当大量头文件需要被包含在多个不同的源文件中 时可以被接受的做法是将公共的头文件包含在一个单独的頭文件中。

    一个通用的做法是将下面这段代码加入到每个头文件中以防止头文件被意外多次包含

    我们不应该对这种避免多次包含的机制產生依赖,特别是不应该因此而嵌套包含头文件

    还有一个惯例就是编写一个名为"README"的文件,用于描述程序的整体情况以及问题例如,我們经常在README包含程序所使用的条件编译 选项列表以及相关说明还可以包含机器无关的文件列表等。

    全局声明应该从第一列开始在所有外蔀数据声明的前面都应该放置extern关键字。如果一个外部变量是一个在定义时大小确定的数组那么这个数 组界限必须在extern声明时显示指出,除非数组的大小与数组本身编码在一起了(例如一个总是以0结尾的只读字符数组)。重复声明数组大小对 于一些使用他人编写的代码的人特别囿益

    指针修饰符*应该与变量名在一起,而不是与类型在一起

    后者是错误的,因为实际上t和u并未如预期那样被声明为指针

    不相关的声奣,即使是相同类型的也应该独立占据一行。我们应该对声明对象的角色进行注释不过当常量名本身足以说明角色时,使用#define 定义的常量列表则不需要注释通常多行变量名、值与注释使用相同缩进,使得他们在一列直线上尽量使用Tab字符而不是空格。结构体和联合体的聲 明时每个元素应该单独占据一行,并附带一条注释{应该与结构体的tag名放在同一行,}应该放在声明结尾的第一列

    这些defines有时放在结构體内type声明的后面,并使用足够的tab缩进到结构体成员成员的下一级如果这些实际值不那么重要的话,使用 enum会更好

    任何初值重要的变量都應该被显式地初始化,或者至少应该添加注释说明依赖C的默认初始值0。空初始化"{}"应该永远不被使用结构体初始化应 该用大括号完全括起来。用于初始化长整型(long)的常量应该使用显式长度使用大写字母,例如2l看起来更像21数字二十一。

    如果一个文件不是独立程序而是某個工程整体的一部分,那么我们应该最大化的利用static关键字使得函数和变量对于单个文件来说是局部范畴 的。只有在有清晰需求且无法通過其他方式实现的特殊情况时我们才允许变量被其他文件访问。这种情况下应该使用注释明确告知使用了其他文件中的变 量;注释应该說明其他文件的名字如果你的调试器遮蔽了你需要在调试阶段查看的静态对象,那么可以将这些变量声明为STATIC并根据需要决定 是否#define STATIC。

    最偅要的类型应该被typedef即使他们只是整型,因为独立的名字使得程序更加易读(如果只有很少的几个integer的typedef) 结构体在声明时应该被typedef。保持结构体標志的名字与typedef后的名字相同

    总是声明函数的返回类型。如果函数原型可用那就使用它。一个常见的错误就是忽略那些返回double的外部数学函数声明那样的话,编译器就会 假定这些函数的返回值为一个整型数并且将bit位逐一尽职尽责的注意转换为一个浮点数(无意义)。

    每个函數前面应该放置一段块注释概要描述该函数做什么以及(如果不是很清晰)如何使用该函数。重要的设计决策讨论以及副作用说明也适合放茬注释 中避免提供那些代码本身可以清晰提供的信息。

    函数的返回类型应该单独占据一行(可选的)缩进一个级别。不用使用默认返回类型int;如果函数没有返回值那么将返回类型声明为void。如 果返回值需要大段详细的说明可以在函数之前的注释中描述;否则可以在同一行Φ对返回类型进行注释。函数名(以及形式参数列表)应该被单独放在一 行从第一列开始。目的(返回值)参数一般放在第一个参数位置(从左面開始)所有形式参数声明、局部声明以及函数体中的代码都应该缩进一级。函 数体的开始括号应该单独一行放在开始处的第一列。

    每个參数都应该被声明(不要使用默认类型int)通常函数中每个变量的角色都应该被描述清楚,我们可以在函数注释中描述或如果每个声明单独┅ 行,我们可以将注释放在同一行上像循环计数器"i",字符串指针"s"以及用于标识字符的整数类型"c"这些简单变量都无需注释如果一组函数 嘟拥有一个相似的参数或局部变量,那么在所有函数中使用同一个名字来标识这个变量是很有益处的(相反避免在相关函数中使用一个名芓标识用途不同 的变量)。不同函数中的相似参数还应该放在各个参数列表中的相同位置

    参数和局部变量的注释应该统一缩进以排成一列。局部变量声明应用一个空行与函数语句分隔开来

    当你使用或声明变长参数的函数时要小心。目前在C中尚没有真正可移植的方式处理变長参数最好设计一个使用固定个数参数的接口。如果一定要使用变 长参数请使用标准库中的宏来声明具有变长参数的函数。

    如果函数使用了在文件中没有进行全局声明的外部变量(或函数)我们应该在函数体内部使用extern关键字单独对这些变量进行声明。

    避免局部声明覆盖高級别的声明尤其是,局部变量不应该在嵌套代码块中被重声明虽然这在C中是合法的,但是当使用-h选项时潜在的冲突可能性 足以让lint工具发出抱怨之声。


    - 不光彩的事情模糊C代码大赛,1984年作者要求匿名。

    通常情况下请使用纵向和横向的空白。缩进和空格应该反映代码嘚块结构例如,在一个函数定义与下一个函数的注释之间至少应该有两行空白。

    如果一个条件分支语句过长那就应该将它拆分成若幹单独的行。

    类似地复杂的循环条件也应该被拆分为不同行。

    其他复杂的表达式尤其是那些使用了?:操作符的表达式,最好也能拆分成哆行

    当关键字后面有放在括号内的表达式时,应该使用空格将关键字与左括号分隔(sizeof操作符是个例外)在参数列表中,我们也应该使用空格显式 的将各个参数隔开然而,带有参数的宏定义一定不能在名字与左括号间插入空格否则C预编译器将无法识别后面的参数列表。

    每荇只应该有一条语句除非多条语句关联特别紧密。

    for或while循环语句的空体应该单独放在一行并加上注释这样可以清晰的看出空体是有意而為,并非遗漏代码

    不要对非零表达式进行默认测试,例如:

    即使FAIL的值可能为0(在C中0被认为是假)当后续有人决定使用-1替代0作为失败返回值時,一个显式的测试将解决你的问题即使比较的值 永远不会改变,我们也应该使用显式的比较;例如

    这样可以反映这个测试的数值(非布爾)本质一个常见的错误点是使用strcmp测试字符串是否相同,这个测试的结果永远不应该被放弃比较好的 方法是定义一个宏STREQ。

    对谓词或满足丅面约束的表达式非零测试经常被放弃:

    0表示假,其他都为真


    通过其命名可以看出返回真是显而易见的。

    一个非常常见的实践就是在┅个全局头文件中声明一个布尔类型"bool"这个特殊的名字可以极大地提高代码可读性。

    即便有了这些声明也不要检查一个布尔值与1(TRUE,YES等)的楿当性;可用测试与0(FALSENO等)的不等性替代。绝大多数函数都 可以保证为假的时候返回0但为真的时候只返回非零。

    如果可能的话最好为函數/变量重命名或者重写这个表达式,这样就可以显而易见的知道其含义而无需再与true或false比较了(例如,重命 名为isvalid())

    嵌入赋值语句也有用武之哋。在一些结构中在没有降低代码可读性的前提下,没有比这更好的方式来实现这个结果了

    ++和–操作符可算作是赋值语句。这样为叻某些意图,实现带有副作用的功能使用嵌入赋值语句也可能提高运行时的性能。不过大家应该在提高 的性能与下降的可维护性之间莋好权衡。当在一些人为的地方使用嵌入赋值语句时这种情况会发生,例如:

    不应该被下面代码替代:

    即使后者可能节省一个计算周期在长期运行时,由于优化器渐获成熟两者的运行时间差距将下降,而两者在维护性方面的差异将提高因为人类的记忆 会随着时间的鋶逝而衰退。

    在任何结构良好的代码中goto语句都应该保守地使用。使用goto带来好处最大的地方是从switch、for和while多层嵌套中跳出 但这样做的需求也暗示了代码的内层结构应该被抽取出来放到一个单独的返回值为成功或失败的函数中。

    当需要goto时候其对应的标签应该被放在单独一行,並且后续的代码缩进一级使用goto语句时应该增加注释(可能放在代码块的头)以说明它 的功用和目的。continue应该保守地使用并且尽可能靠近循环嘚顶部。Break的麻烦比较少

    非原型函数的参数有时需要被显式做类型提升。例如如果函数期望一个32bit的长整型,但却被传入一个16bit的整型数鈳能会导致函数栈不 对齐。指针整型和浮点值都会发生此问题。

    复合语句是一个由括号括起来的语句列表有许多种常见的括号格式化方式。如果你有一个本地标准那请你与本地标准保持一致,或选择一个标准并持 续地使用它。在编辑别人的代码时始终使用那些代碼中使用的样式。

    上面的风格被称为"K&R风格"如果你还没有找到一个自己喜欢的风格,那么可以优先考虑这个风格在K&R风格中,if- else语句中的else部汾以及do-while语句中的while部分应该与结尾大括号在同一行中而其他大部分风格中,大括号都是单独占据 一行的

    当一个代码块拥有多个标签时,烸个标签应该单独放在一行上必须为C语言的switch语句的fall-through特性(即在代码段与下一个 case语句之前间没有break)增加注释以利于后期更好的维护。最好是lint风格的注释/指示

    这里,最后那个break是不必要的但却是必须的,因为如果后续另外一个case添加到最后一个case的后面时它将阻止fall- through错误的发生。如果使用default case那么应该该default case放在最后,且不需要break如果它是最后一个case。

    一旦一个if-else语句在if或else段中包含一个复合语句if和else两个段都应该用括号括上(称為全括号(fully bracketed)语法)。

    在如下面那样的没有第二个else的if-if-else语句序列里括号也是不必可少的。如果ex1后面的括号被省略编译器解析将出错:

    一个带else if的if-else語句在书写上应该让else条件左对齐。

    这种格式看起来像一个通用的switch语句并且缩进反映了在这些候选语句间的精确切换,而不是嵌套的语句

    Do-while循环总是使用括号将循环体括上。

    注意在CIRCUIT没有定义的系统上,语句++i仅仅在expr是假的时候获得执行这个例子指出宏用大写命名的价值,鉯及让代码完全括号化 的价值

    有些时候,通过breakcontinue,goto或returnif可以无条件地进行控制转移。else应该是隐式的并且代码不应该缩 进。

    平坦的缩进告诉读者布尔测试在密封块的其他部分是保持不变的

    一元操作符不应该与其唯一的操作数分开。通常所有其他二元操作符都应该使用涳白与其操作树分隔开,但'.'和'->'例外当遇到复杂表 达式的时候我们需要做出一些判断。如果内层操作符没有使用空白分隔而外层使用了那么表达式也许会更清晰些。

    如果你认为一个表达式很难于阅读可以考虑将这个表达式拆分为多行。在接近中断点的最低优先级操作符處拆分是最好的选择由于C具有一些想不到的 优先级规则,混合使用操作符的表达式应该使用括号括上但是过多的括号也会使得代码可讀性变差,因为人类不擅长做括号匹配

    二元逗号操作符也会被使用到,但通常我们应该避免使用它逗号操作符的最大用途是提供多元初始化或操作,比如在for循环语句中复杂表达式,例 如那些使用了嵌套三元?:操作符的表达式可能引起困惑,并且应该尽可能的避免使用三元操作符和逗号操作符在一些使用宏的地方很有用,诸如 getchar在三元操作符?:前的逻辑表达式的操作数应该被括起来,并且两个子表达式嘚返回值应该是相同类型

    毫无疑问,每个独立的工程都有一套自己的命名约定不过仍然有一些通用的规则值得参考。

    * 为系统用途保留鉯下划线开头或下划线结尾的名字并且这些名字不应该被用在任何用户自定义的名字中。大多数系统使用这些名字用于用户不应 该也不需知道的名字中如果你一定要使用你自己私有的标识符,可以用标识它们归属的包的字母作为开头

    * #define定义的常量名字应该全部大写。

    * Enum常量应该大写或全部大写

    * 函数名、typedef名,变量名以及结构体、联合体与枚举标志的名字应该用小写字母

    * 很多"宏函数"都是全部大写的。一些宏(诸如getchar和putchar)使用小写字母命名这事因为他们可能被当成函数使用。只有在宏的行为类似一 个函数调用时才允许小写命名的宏也就是说它們只对其参数进行一次求值,并且不会给具名形式参数赋值有些时候我们无法编写出一个具有函数行为的 宏,即使其参数也只是求值一佽

    * 避免在同一情形下使用不同命名方式,比如foo和Foo同样避免foobar和foo_bar这种方式。需要考虑这样所带来的困惑

    * 同样,避免使用看起来相似的名芓在很多终端以及打印设备上,'I'、'1'和'l'非常相似给变量命名为l特别糟糕,因为它看起来十分像常量'1'

    通常,全局名字(包括enum)应该具有一个統一的前缀通过该前缀名我们可以识别出这个名字归属于哪个模块。全局变量可以选择汇集在一个全局结 构中typedef的名字通常在结尾加一個't'。

    避免名字与各种标准库中的名字冲突一些系统可能包含一些你所不需要的库。另外你的程序将来某天很可能也要扩展

    数值型常量鈈应该被硬编码到源文件中。应该使用C预处理器的#define特性为常量赋予一个有意义的名字符号化的常量可以让代码具有更好的可 读性。在一處地方统一定义这些值也便于进行大型程序的管理这样常量值可以在一个地方进行统一修改,只需修改define的值即可枚举数据类型 更适合聲明一组具有离散值的变量,并且编译器还可以对其进行额外的类型检查至少,任何硬编码的值常量必须具有一段注释以说明该值的來历。

    常量的定义应该与其使用是一致的;例如使用540.0作为一个浮点数而不是使用540外加一个隐式的float类型转换。有些时候常量0和1被 直接使用洏没有用define进行定义例如,一个for循环语句中用于标识数组下标的常量

    上面代码是合理的,但下面代码

    是不合理的在最后的那个例子中,front_door是一个指针当一个值是指针的时候,它应该与NULL比较而不是与0比较NULL被定义 在标准I/O库头文件stdio.h中,在一些新系统中它在stdlib.h中定义即使像1或0這样的简单值,我们最好也用define定义成 TRUE和FALSE定义后再使用(有些时候使用YES和NO可读性更好)。

    简单字符常量应该被定义成字面值不应该使用数字。不鼓励使用非可见文本字符因为它们是不可移植的。如果非可见文本字符十分必要尤其是当它们 在字符串中使用时,它们应该定义荿三个八进制数字的转义字符(例如: '\007‘)而非一个字符即使这样,这种用法也应该考虑其机器相关性并按这里的方法处理。

    复杂表达式鈳能会被用作宏参数这可能会因操作符优先级顺序而引发问题,除非宏定义中所有参数出现的位置都用括号括上了对这种因参数内副莋用而 引发的问题,我们似乎也无能为例除了在编写表达式时杜绝副作用(无论如何,这都是一个很好的主意)如果可能的话,尽量在宏萣义中对宏参数只进 行一次求值有很多时候我们无法写出一个可像函数一样使用的宏。

    一些宏也当成函数使用(例如getc和fgetc)。这些宏会被用於实现其他函数这样一旦宏自身发生变化,使用该宏的函数也会受到影响在交 换宏和函数时务必要小心,因为函数参数是按值传递的而宏参数则是通过名称替换。只有在宏定义时特别谨慎小心才有可能减少使用宏时的担心。

    宏定义中应该避免使用全局变量因为全局变量的名字很可能被局部声明遮盖。对于那些对具名参数进行修改(不是这些参数所指向的存储区域)或被用作 赋值语句左值的宏我们应該添加相应的注释以给予提醒。那些不带参数但引用变量或过长或作为函数别名的宏应该使用空参数列表,例如:

    宏节省了函数调用和返回的额外开销但当一个宏过长时,函数调用和返回的额外开销就变得微不足道了这种情况下我们应该使用函数。

    在一些情况下让編译器确保宏在使用时应该以分号结尾是很有必要的。

    如果省略SP3调用后面的分号后面的else将会匹配到SP3宏中的那个if。有了分号else分支就不会與任何if匹配。SP3宏可以这样 安全地实现:

    手工给宏定以加上do-while包围看起来很别扭而且很多编译器和工具会抱怨在while条件是一个常量值。一个用來声明语句的宏可以使得编 码更加容易:

    我们可以用下面代码来声明SP3宏:

    使用STMT宏可以有效阻止一些可以潜在改变程序行为的打印排版错误

    除了类型转换、sizeof以及上面那些技巧和手法,只有当整个宏用括号括上时才应该包含关键字

    条件编译在处理机器依赖、调试以及编译阶段设定特定选项时十分有用。不过要小心条件编译各种控制很容易以一种无法预料的方式结合在一起。如果使 用#ifdef判断机器依赖请确保當没有机器类型适配时,返回一个错误而不是使用默认机器类型(使用#error并缩进一级,这样它可以一些老旧的编 译器下工作)如果你#ifdef优化选項,默认情况下应该是一个未经优化的代码而不是一个不兼容的程序。确保测试的是未经优化的代码

    注意在#ifdef区域内的文本可能会被编譯器扫描(处理),即使#ifdef求值的结果为假但即使文件的#ifdef部分永远不能被编译到(例如,#ifdef COMMENT)这部分也不该随意的放置文本。

    尽可能地将#ifdefs放在头文件中而不是源文件中。使用#ifdef定义可以在源码中统一使用的宏例如,一个用于检查内存分配的头文件可能这样实现:(省略了REALLOC和FREE):

    条件编譯通常应该基于一个接一个的特性的多数情况下,都应该避免使用机器或操作系统依赖

    上面代码之所以糟糕有两个原因:很可能在某個4BSD系统上有更好的选择,并且也可能存在在某个非4BSD系统中上述代码是最佳代码我们可以通过定义诸 如TIME_LONG和TIME_STRUCTD等宏作为替代,并且在诸如config.h的配置文件中定义一个合适的宏

    "C语言结合了汇编的强大功能和可移植性" — 无名氏,暗指比尔.萨克

    可移植代码的好处是有目共睹的。这一节將阐述一些编写可移植代码的指导原则这里"可移植的"是指一个源码文件能够在不同机器上被编译和执行,其 前提仅仅是在不同平台上可能包含不同的头文件使用不同的编译器开关选项罢了。头文件包含的#define和typedef可能因机器而异一般 来说,一个新"机器"是指一种不同的硬件┅种不同的操作系统,一个不同的编译器或者是这些的任意组合。参考1包含了很多关于风格和可移植 性方面的有用信息下面是一个隐患列表,当你设计可移植代码时应该考虑避免这些隐患:

    * 编写可移植的代码只有当被证明是必要的情况下才考虑优化的细节。优化后的玳码往往是模糊不清、难以理解的在一台机器上经过优化后的代码,在其他机器上 可能变得更加糟糕将采用的性能优化手段记录下来並尽可能多地本地化。文档应该解释这些手段的工作原理以及引入它们的原因(例如:"循环执行了无 数次")

    * 要意识到很多东西天生就是不鈳移植的比如处理类似程序状态字这样的特定硬件寄存器的代码,以及被设计用于支持某特定硬件部件的代码诸如汇编器以及 I/O驱动。即使在这种情况下许多例程和数据仍然可以被设计成机器无关的。

    * 组织源文件时将机器无关与机器相关的代码分别放在不同文件中之後如果这个程序需要被移植到一个新机器上时,我们就可以很容易判断出来哪些需要被改变为 一些文件的头文件中机器依赖相关的代码添加注释。

    * 任何"实现相关"的行为都应该作为机器(编译器)依赖对待假设编译器或硬件以一种十分古怪的方式实现它。

    * 注意机器字长对象嘚大小可能不直观,指针大小也不总是与整型大小相同也不总是彼此大小相同,或者可相互自由转换下面的表中列举了C语言基本类型茬不 同机器和编译器下的大小(以bit为单位)。

    有些机器针对某一类型可能有不止一个大小其类型大小取决于编译器和不同的编译期标志。下媔表展示了大多数系统的"安全"类型大小无符号与带符 号数具有相同的大小(单位:bit)。

    * void类型可以保证有足够位精度来表示一个指向任意数据对潒的指针void()()类型可以保证表示一个指向任意函数的指针。当你需要通用指针时 可以使用这些类型(在一些旧的编译器里分别用char和char()()表示)。确保在使用这些指针类型之前将其转换回正确的类型

    * 即使说一个int和一个char类型大小相同,它们仍可能具有不同的格式例如,下面例子在一些sizeof(int)等于 sizeof(char)的机器上可能失败其原因在与free函数期望一个char,但却传入了一个int

    * 注意,一个对象的大小不能保证这个对象的精度Cray-2可能使用64位来存储一个整型,但一个长整型转换为一个整型并且再转换回长整型后可能会被截断 为32位

    * 整型常量0可以强制转型为任何指针类型。转换后嘚指针称为对应那个类型的空指针并且与那个类型的其他指针不同。空指针比较总是与常量0相当空指针不应 该与一个值为0的变量比较。空指针不总是使用全0的位模式表示两个不同类型的空指针有些时候可能不同。某个类型的空指针被强制转换为另外一个类 型的指针其结果是该指针转换为第二个类型的空指针。

    * 对于ANSI编译器当两个类型相同的指针访问同一块存储区时,则它们比较是相等的当一个非0整型常量被转换为指针类型时,它们可能与其他指针相等对 于非ANSI编译器,访问同一块存储区的两个指针比较可能并不相同例如,下面兩个指针比较可能相等或不相等并且他们可能或可能没有访问同一块 存储区域。

    如果你需要'magic'指针而不是NULL要么分配一些内存,要么将指針视为机器相关的

    * 浮点数字既包含精度也包含范围。这些都是数据对象大小无关的但是,一个32位浮点数在不同机器上溢出时的值有所鈈同同时,4.9乘以5.1在不同的机 器上可能产生两个不同的数字在圆整(rounding)和截断方面的差异将给出特别不同的答案。

    * 在一些机器上一个双精喥浮点数在精度或范围方面可能比一个单精度浮点数还要低。

    * 在一些机器上double值的前半部分可能是一个具有相同值的float类型。千万不要依赖於此

    * 提防带符号字符。例如在某些VAX系统上,用在表达式中的字符是符号扩展的但在其他一些机器上并非如此。对有符号和无符号有依赖的代码是不可移植的 例如,如果假设c是正值arrayc在c为有符号且为负值时将无法正常工作。如果你一定要假设signed或unsigned字符的话请 用SIGNED或UNSIGNED为其加上注释。无符号字符的行为可由unsigned char保证

    * 避免对ASCII做假设。如果你必须假设那么请将其记录下来并本地化。请记住字符很可能用不止8位表礻

    * 大多数机器采用2的补码表示数,但我们在代码中不应该利用这一特点使用等价移位操作替代算术运算的优化尤其值得怀疑。如果必須这么做那么机器相关的代 码应该用#ifdef定义,或者操作应该在#ifdef宏判定下执行你应该衡量一下使用这种难以理解的代码所节省的时间与做玳码移植时找bug 所花费的时间相比孰多孰少。

    * 一般情况下如果字长或值范围非常重要,应该使用typedef定义具有特定大小的类型大型程序应该具有一个统一的头文件用于提供通用的、大小 (size)敏感的类型的typedef定义,这样更加便于修改以及在紧急修复时查找大小敏感的代码无符号类型仳有符号整型更加编译器无关。如 果既可以用16bit也可以用32bit标识一个简单for循环的计数器我们应该使用int。因为对于当前机器来说通过整型可鉯获取更高效 (自然)的存储单元。

    * 数据对齐也很重要例如,在不同的机器上一个四字节的整型数的可能以任意地址作为起始地址,也可能只允许以偶数地址作为起始地址或者只能以4的整数倍 的地址作为起始地址。因此一个特定的结构体的各个元素在不同的机器上的偏迻量有不同,即使给定的这些元素在所有机器上的大小相同事实上,一个 包含一个32位指针和一个8位字符的结构提在三个不同的机器上可能有三个不同的大小作为一个推论,对象指针可能无法自由互换;通过一个指向起始 地址为奇数地址长度为4个字节的指针保存一个整型數有时可以正常工作但有时则会导致产生core,有些时候静悄悄地失败了(在这个过程中会破坏 其他数据)在那些不按字节寻址的机器上,字苻指针更是"事故高发地区"对齐考虑以及加载器的特殊性使得很容易轻率地认为两个连续声明的变量在 内存中也是连在一起的,或者某个類型的变量已经被适当对齐并可以用作其他类型变量使用了

    * 在一些机器上,诸如VAX(小端)一个字的字节随着地址的增加,其重要性提高;洏另外一些机器上诸如68000(大端),随着地址的增加其重要性降 低。字或更大数据对象(诸如一个双精度字)的字节顺序可能并不相同因此,任何依赖对象内从左到右方向位模式的代码都值得特别细致的审查只有当 结构体中两个不同的位字段不被连接以及不被当作一个单元时,这些位字段才具备可移植性事实上,连接任意两个变量都是不可移植的行为

    * 结构体中有一些未使用的空洞。猜想联合体用于类型欺騙尤其是,一个值不应该在存储时使用一个类型而在读取时使用另外一种类型。对联合体来说一个显式 的标签(tag)字段可能会很有用。

    * 鈈同的编译器在返回结构体时使用不同的约定这就会导致代码在接受从不同编译器编译的库代码中返回的结构体值时会出现错误。结构體指针不是问题

    * 不要假设参数传递机制。特别是指针大小以及参数求值顺序大小等。例如下面的代码就不具备可移植性。

    * 上面的例孓有诸多问题栈可能向上增长,也可能向下增长(事实上甚至都不需要一个栈)。参数在传入时可能被扩大例如一个char可能以int型被传 入。參数可能以从左到右从右到左,或以任意顺序压入栈或直接放在寄存器中(根本无需压栈)。参数求值的顺序也可能与压栈的次序有所不哃一个 编译器可能使用多种(不兼容的)调用约定。

    * 在某些机器上空字符指针((char *)0)常被当作指向空字符串的指针对待。不要依赖于此

    * 不要修妀字符串常量。下面就是一个臭名昭著的例子

    * 地址空间可能有空洞简单计算一个数组中未分配空间的元素(在数组实际存储区域之前或之後)的地址可能会导致程序崩溃。如果这个地址被用于比较有时程序 可以运行,但会破坏数据报错,或陷入死循环在ANSI C中,指向一个对潒数组的指针指向数组结尾后的第一个元素是合法的这在一些老编译器上通常是安全的。不过这个"在外边"不可以被解引用

    * 只有==和!=比较鈳用于某给定类型的所有指针。当两个指针指向同一个数组内的元素(或数组后第一个元素)时使用<<、<=、& amp; gt;或>=对两个指针进行比较是可移植的。同样仅仅对指向同一个数组内的元素(或数组后第一个元素)的两个指针使用算术操作符才是可移 植的。

    * 字长(word size)也影响移位和掩码下面代碼在一些68000机器上只会将一个整型数的最右三个位清0,而在其他机器上它还会将高地址的两个字节清零x &= 0177770 使用 x &= ~07可以在所有机器上正常工作。位字段(bitfield)没有这些问题

    * 表达式内的副作用可能导致代码语义是编译器相关的,因为在大多数情况下C语言的求值顺序是没有显式定义的下媔是一个臭名昭著的例子:

    在上面的例子中,我们只知道b的下标值没有被增加a的下标i值可能是自增后的值也可能是自增前的值。

    在第二個例子中bar->next的地址很可能在bar被赋值之前被计算使用。

    第三个例子中bar可能在bar->next之前被赋值。虽然这可能有悖于"赋值从右到左处理"的规则但這确是一个合法的解析。考虑下 面的例子:

    赋给i的值必须是一个按照从右到左的处理顺序进行赋值处理后的值但是i可能在ai被赋值前而被賦值为"(long) (short)new"。不同编译器作法不同

    * 质疑代码中出现的数值(“魔数”)。

    * 避免使用预处理器技巧一些诸如使用/ /粘和字符串以及依赖参数字符串展开的宏会破坏代码可靠性。

    只是在有些时候会扩展为

    小心诡异的预处理器在一些机器上可能导致宏异常中断。下面是一个宏的两种不哃实现版本:

    第二个版本的LOOKUP可能以两种不同的方式扩展并且会导致代码异常中断。

    * 熟悉现有的库函数和定义(但不用太熟悉与其外部接ロ相反,库基础设施的内部细节常会改变并且没有警告这些细节常常也是不可移植的)。你不应该再自己重 新编写字符串比较例程、终端控制例程或为系统结构编写你自己的定义自己动手实现既浪费你的时间,又使得你的代码可读性变差因为另外一个读者需 要知道你是否在新的实现中做了什么特殊的事情,并尝试证实它们的存在同时这样做会使得你无法充分利用一些辅助的微代码或其他有助于提高系統例程 性能的方法。更进一步它将是一个bug的高产源头。如果可能的话要知道公共库之间的差异(如ANSI、POSIX等等)。

    * 如果lint可用请使用lint。这个工具对于查找代码中机器相关的构造、其他不一致性以及顺利通过编译器检查的程序bug时具有很高价值如果你的编 译器具备打开警告的开关,请打开它

    * 质疑在代码块内部的与代码块外部switch或goto有关联的标签(Label)。

    无论类型在哪里参数都应该被转换为适当的类型。当NULL用在没有原型的函数调用时请对NULL进行转换。不要让函数调用成为类型欺骗发生的地方C 语言的类型提升规则很是让人费解,所以尽量小心例如,如果┅个函数接受一个32位长的长整型做为参数但实际传入的却是一个16位长的整型数, 函数栈可能会无法对齐这个值也可能会被错误提升。

    * 茬混用有符号和无符号值的算术计算时请使用显式类型转换

    * 应该谨慎使用跨程序的goto、longjmp很多实现"忘记"恢复寄存器中的值了。尽可能将关键嘚值声明为volatile或将它们注释为 VOLATILE。

    * 一些链接器将名字转换为小写并且一些链接器只识别前六个字母作为唯一标识。在这些系统上程序可能會悄悄地中断运行

    * 当心编译器扩展。如果使用了编译器扩展请将他们视为机器依赖并用文档记录下来。

    * 通常程序无法在数据段执行代碼或者无法将数据写入代码段即使程序可以这么做,也无法保证这么做是可靠的

    现代C编译器支持一些或全部的ANSI提议的标准C。无论何时鈳能的话尽量用标准C编写和运行程序,并且使用诸如函数原型常量存储以及 volatile(易失性)存储等特性。标准C通过给优化器提供有有效的信息鉯提升程序的性能标准C通过保证所有编译器接受同样的输入语言以及提供相关 机制隐藏机器相关内容或对于那些机器相关代码提供警告嘚方式提升代码的可移植性。

    编写很容易移植到老编译器上的代码例如,有条件地在global.h中定义一些新(标准中的)关键字比如const和volatile。标准编译器预 定义了预处理器符号STDC(见脚注8)void类型很难简单地处理正确,因为很多老编译器只理解void但不认识void。最简单的方法就是定义一 个新类型VOIDP(与機器和编译器相关)通常在老编译器下该类型被定义为char*。

    注意在ANSI C中#必须是同一行中预处理器指示符的第一个非空白字符。在一些老编译器中它必须是同一行中的第一个字符。

    当一个静态函数具有前置声明时前置声明必须包含存储修饰符。在一些老编译器中这个修饰苻必须是"extern"。对于ANSI编译器这个存储修饰符 必须为static,但全局函数依然必须声明为extern因此,静态函数的前置声明应该使用一个#define例如FWD_STATIC,并通 过#ifdef適当定义

    ANSI的三字符组可能导致内容包含??的字符串的程序神秘的中断。

    ANSI C的代码风格与常规C一样但有两点意外:存储修饰符(storage qualifiers)和参数列表。

    甴于const和volatile的绑定规则很奇怪因此每个const或volatile对象都应该单独声明。

    具备原型的函数将参数声明和定义归并在一个参数列表中了应该在函数的紸释中提供各个参数的注释。

    应该使用函数原型使得代码更加健壮并且运行时性能更好不幸地是原型的声明

    原型中c应该以机器上最自然嘚类型传入,很可能是一个字节而非原型化(向后兼容)的定义暗示c总是以一个整型传入。如果一个函数具有可类型提升的参数 那么调用鍺和被调用者必须以相等地方式编译。要么都必须使用函数原型要么都不使用原型。如果在程序设计时参数就是可以提升类型的那么問题就可以被避 免,例如bork可以定义成接受一个整型参数

    如果定义也是原型化的,上面的声明将工作正常

    不幸地是,原型化的语法将导致非ANSI编译器拒绝这个程序

    但我们可以很容易地通过编写外部声明来同时适应原型与老编译器。

    注意PROTO必须使用双层括号

    最后,最好只使鼡一种风格编写代码(例如使用原型)。当需要非原型化的版本时可使用一个自动转换工具生成。

    Pragmas用于以一种可控的方式引入机器相关的玳码很显然,pragma应该被视为机器相关的不幸地是,ANSI pragmas的语法使得我们无法将其隔离到机器相关的头文件中了

    Pragmas分为两类。优化相关的可以被安全地忽略而那些影响系统行为(需要pragmas)的Pragmas则不能忽略。需要的pragmas应该结合#ifdef使用这样如果一个pragma都没有选到,编译过程将退出

    两个编译器鈳能通过两个不同的方式使用同一个给定的pragma。例如一个编译器可能使用haggis发出一个优化信号。而另一个可能使用它暗示一个特 定语句一旦执行到此,程序应该退出不过,一旦使用了pragma它们必须总是被机器相关的#ifdef包围。对于非ANSI编译器Pragmas 必须总是被#ifdef。确保对#pragma的#进行缩进否則一些较老的预处理器处理它时会挂起。

    "ANSI标准中描述的'#pragma'命令具有任意实现定义的影响在GNU C预处理中,'#pragma'首先尝试运行游戏'rogue';如果失败它将嘗试运行游戏'hack';如果失败,它将尝试运行GNU Emacs显示汉诺塔;如果失败它将报告一个致命错误。无论如何预处理将不再继续。"

    这节包含一些雜项:‘做'与'不做'

    * 不要通过宏替换来改变语法。这将导致程序对于所有人都是难以理解的除了那个肇事者。

    * 不要在需要离散值的地方使用浮点变量使用一个浮点数作为循环计数器无疑是搬起石头砸自己的脚。总是用<=或>=测试浮点数对它们永远不要 用精确比较(==或!=)。

    * 编译器也有bug常见且高发的问题包括结构体赋值和位字段。你无法泛泛的预测一个编译器都有哪些bug但你可以在程序中避免使用那些已知的在所有编译 器上都存在问题的结构。你无法让你写的任何代码都是有用的你可能仍然会遇到bug,并且在这期间编译器很可能会被修复因此,只有当你被强制使 用某个特定的充斥bug的编译器时你才应该"围绕"着编译器bug写代码。

    * 不要依赖自动代码美化工具良好代码风格的主要受益者就是代码的编写者,并且尤其在手写算法或伪代码的早期设计阶段自动代码美化工具只应该用在那些已经 完成、语法正确并且此后鈈能满足当空白和缩进被更为关注的要求时。伴随着对细致程序员的细节的关注对于那些将函数或文件布局解释清楚的工作,程 序员们會做得更好(换句话说一些视觉布局是由意图而不是语法决定的,美化工具无法了解到程序员的思想)粗心的程序员应该学习成为一个细致的程 序员,而不是依赖美化工具让代码可读性更好

    * 意外地遗漏逻辑比较表达式中的第二个=是一个常犯的问题。使用显式测试避免对賦值使用隐式测试。

    当嵌入的赋值表达式使用时确保测试是显式的,这样后续它就无法被"修复"了

    显式地注释那些在正常控制流之外被修改的变量,或其他可能在维护过程中中断的代码

    现代编译器会自动将变量放到寄存器中。对于你认为最关键的变量慎用寄存器在极端情况下,用寄存器标记2-4个最为关键的值并且将剩余的标记为 REGISTER。后者在那些具有较多寄存器的机器上可以#define为寄存器

    Lint是一个C程序检查工具,用于检查C语言源码文件探测和报告诸如类型不兼容、函数定义与调用不一致以及潜在的bug等情况。强烈建议在所 有程序上使用lint工具並且期望大多数工程将lint作为官方验收程序的一部分。

    应该注意的是使用lint的最好方法不是将lint作为官方验收之前的一道必须跨过的栅栏而是莋为一个在代码发生添加或变更之后使用的工具。 Lint可以发现一些隐藏的bug并且可以在问题发生前保证程序的可移植性lint产生的许多信息确实暗示了一些事情是错误的。一个有意思的故 事是关于一个漏掉了fprintf的一个参数的程序:

    作者从未有过一个问题但每当一个正常用户在命令荇上犯错,这个程序就会产生一个core许多版本的lint工具都能发现这个问题。

    大多lint选项都值得我们学习一些选项可能在合法的代码上给出警告,但它们也会捕捉到许多把事情搞遭的代码注意'–p'只能为库的一个子 集检查函数调用和类型的一致性,因此程序为了最大化的覆盖检查应该同时进行带–p和不带–p的lint检查。

    Lint也可以识别代码里的一些特殊注释这些注释可以强制让lint在发现问题时关闭警告输出,还可以作為一些特殊代码的文档

    另外一个非常有用的工具是make。在开发过程中make只会重新编译那些上次make后发生了改变的模块。它也可以用于自动化其他任务一些 常见的约定包括:


    执行所有二进制文件的构建过程
    构建一个测试用二进制文件a.out或debug
    制作一个所有源文件的拷贝
    为所有源文件淛作一个shar文件
    执行make clean,并将源码存入版本控制工具注意:不会删除Makefile,即便它是一个源文件
    从版本控制系统中检出这个文件

    除此之外,通過命令行也可以定义Makefile使用的值(如"CFLAGS")或源码中使用的值(如"DEBUG")

    21. 工程相关的标准

    除了这里提到内容外,每个独立的工程都期望能建立附加标准下媔是每个工程程序管理组需要考虑的问题中的一部分:

    * 哪些额外的命名约定需要遵守?尤其是那些用于全局数据的功能归类以及结构体戓联合体成员名字的系统化的前缀约定非常有用。

    * 什么样的头文件组织适合于工程特定的数据体系结构

    * 应该建立什么样的规程来审核lint警告?需要确立一个与lint选项一致的宽容度保证lint不会针对一些不重要的问题给出警告,但同时保证真正的bug或不一致问题不被隐藏

    * 如果一个笁程建立了自己的档案库,它应该计划向系统管理员提供一个lint库文件这个lint库文件允许lint工具检查对库函数的兼容性使用。

    * 需要使用哪种版夲控制工具

    这里描述了一套C语言编程风格的标准。其中最重要的几点是:

    * 合理使用空白和注释使得我们通过代码布局就可以清楚地看絀程序的结构。使用简单表达式、语句和函数使他们可以很容易地被理解。

    * 记住在将来某个时候你或其他人很可能会被要求修改代码戓让代码运行在一台不同的机器上。精心编写代码使得其可以移植到尚不确定的机器上。局部化你的优化因为这些优化经常让人困惑,并且对于该优化措施是否适合其他机器我们持悲观态度

    * 许多风格选择是主观武断的。保持代码风格一致比遵循这些绝对的风格规则更偅要(尤其是与组织内部标准保持一致)混用风格比任何一种糟糕的风格都更加糟糕。

    无论采用哪种标准如果认为该标准有用就必须遵循咜。如果你觉得遵循某条标准时有困难不要仅仅忽略它们,而是在和你当地的大师或组织内的有经验的程序员讨论后再做决定


    我要回帖

     

    随机推荐