My Blog

C_Code_Style

记录 C 语言的一些代码规范和安富莱 C 编写规范及学习建议

学习笔记,仅供参考

参考c_style_guide 参考安富莱C语言编码规范


第一部分为摘抄的一些 C 语言代码规范,如果实在规范比较纠结就参考 Linux 的代码规范

概览

  1. 变量,常量和函数名命名要有意义,不使用驼峰命名法,用下划线隔开单词。如用 hot_water_temperature 代替 hotWaterTemperature,单字符变量仅适用于迭代循环。
  2. 合适的缩进,开发多是 4 个空格
  3. 同类型变量尽可能在同一行声明
  4. 变量声明和函数内容代码要空一行
  5. 括号保持成对出现
  6. 左大括号必须与条件或函数位于同一行上
  7. 必要时将数字定义为常量,尽量减少数字出现在表达式中除了一些特殊意义的数字,如 3.1415926
  8. 定义常量时必须以大写的形式(如, #define MAX_LEN 10)
  9. 在操作符两边各空一格(x = 5 + 7;)
  10. 在逗号后留出一个空格(int num1, num2;)
  11. 如果代码太复杂而无法单独读取,请简化/拆分/重命名变量
  12. 条件判断时使用括号避免逻辑混乱
  13. 不必要时避免使用全局变量
  14. 若 p 是一个结构体的指针,应该使用 p->number 访问成员变量,而不是 (*p).member。并且 -> 两边不空格
  15. 避免一行代码的字符数超过 80 个
  16. 尽量避免重复代码

一些具体内容

代码组织

  • 使用统一规定组织代码:
    1. #include<>
    2. #include””
    3. #defines
    4. Data Types(e.g., structures)
    5. Globals(全局变量)
    6. Prototypes(函数原型声明)
    7. Code(逻辑操作等)
  • main 函数要么在开头要么在结尾
  • #include 和 #define 之间不要有代码

函数

  • 通过调用合适的函数来避免代码重复,而不是复制粘贴
  • Input parameters must appear before out parameters.
  • Annotate helper functions with static.
  • Annotate unmodified parameters with const.
  • And annotate functions intended to be used from outside with extern.
  • 函数应该表现为一个内部复杂的可读单元或是频繁使用的单元
  • 若函数的形参声明超过 80 个字符,可换行对齐书写,如下所示:

    void process(int my_first_value, int temperature, float pressure, 
             int altitude, char color) {
    /*code here*/
    }

注释

  • 所有的代码都必须要有一些说明性注释,描述一块代码的目的
  • 描述应该出现在每个功能的上面
  • 若变量名意义不明,应在声明处描述其使用的目的

杂项

  • Use assert - Macro that prints error message and terminates the program if the provided expression is false (e.g., assert(x != 0)). Assert can help with readability, mechanically exposing pre and post conditions more strongly than comments (which can become obsolete). Keep in mind that aborting execution may be inappropriate for submitted tests. We likely expect an error message.
  • About return statements:
    • Return at the end is OK.
    • Return at the beginning for erroneous input is OK.
    • Return in the middle of a loop is sketchy.
  • About loops:
    • If a loop should end by a condition, use loop control rather than break.
    • Use a for loop when initialization, comparison, and increment are appropriate (not a while loop).
  • Avoid using the indent command as a last step; it can undo any manual beautification.

注:暂时不能理解的内容,以原文的形式保留


代码示例

示例一:

/*
  Jane/Joe Doe                   
  ID: 12345679                    
 */

#include <stdio.h>

int main() {
   int temperature = 0, height;
   char gender;
   
   printf("Enter Gender (m/f): ");
   scanf("%c", &gender);

   printf("Enter temperature: ");
   scanf("%d", &temperature);

   printf("Enter height (inches): ");
   scanf("%d", &height);

   if (gender == 'f') {
      printf("Female\n");
   } else {
      printf("Male\n");
   }

   do {
      if (temperature % 2 == 0) {
         printf("Even temperature %d\n", temperature);
      }
      temperature--;
   } while (temperature >= 0);

   /* This is a cascaded if statement example */
   if (height > 0 && height <= 15) {
      printf("Type1\n");
   } else if (height > 15 && height <= 30) {
      printf("type2\n");
   } else {
      printf("Other type\n");
   }

   return 0;
}

示例二:

#include <stdio.h>

/* Prototypes */
char letter_grade(float score); 

/***************************************/
/* The program reads two values and    */
/* generates the average. You need to  */
/* provide two values separated by and */
/***************************************/
int main() {
   float score1 = 77, score2 = 88, avg;
   int values_read;

   /* Reading the values */
   printf("Enter two scores using <score1> and <score2> format: ");
   values_read = scanf("%f and %f", &score1, &score2);
   printf("The number of values read is %d\n", values_read);

   /* Computing and printing the average */
   avg = (score1 + score2) / 2;
   printf("Average for %f and %f is %.2f\n", score1, score2, avg);
   printf("Your letter grade is %c\n", letter_grade(avg));

   return 0;
}

/******************************************/
/* letter_grade returns 'A; if score >=90 */
/* 'B' if score >=80, 'F' otherwise       */
/******************************************/
char letter_grade(float score) {
   if (score >= 90) {
      return 'A';
   } else if (score >= 80) {
      return 'B';
   } else {
      return 'F';
   }
}

示例三:

#include <stdio.h>

typedef struct pixel {
   int x, y;
   char color;
} Pixel;

void print_pixel(Pixel p);
void increase_x_and_y(Pixel *p, int delta);

void print_pixel(Pixel p) {
   printf("x: %d, y: %d, color: %c\n", p.x, p.y, p.color);
}

void increase_x_and_y(Pixel *p, int delta) {
   p->x += delta;   /* Do not access member using (*p).x */
		    /* Do not leave space p -> x */
}

int main() {
   Pixel p1 = {1, 2, 'r'};
   
   increase_x_and_y(&p1, 300);
   print_pixel(p1);

   return 0;
}

更多内容参考 CCodingStandard cstyle


以下部分为 ArmFly 规范编写 C 程序的命名,特记录一些安富莱建议的规范

文件与目录

  1. 文件及目录的命名可用字符:[A-Z; a-z; 0-9; ._-]

  2. 源文件后缀用小写字母 .c 和 .h

  3. 文件命名要准确清晰表达其内容,且文件名应精炼防止名字过长造成使用不便,可适当使用缩写。以下为两种参考命名方式:

    • 各程序模块的文件名开头 2 个小写字母代表本模块功能:如主控程序为 mpMain.c, mpDisp.c …

    • 不屑模块功能标识:如主控程序:Main.c, Disp.c

  4. 一个软件包或一个逻辑组件的所有头文件和源文件防止一个单独目录下,有利于查找

  5. 对整个项目需要的公共头文件,应放在单独目录下(如:myProject/include),避免引用时目录过于分散

  6. 源码文件的段落安排:

    a. 文件头注释
    b. 防止重复引用头文件的设置
    c. #include 部分
    d. #define 宏定义部分
    e. enum 枚举定义部分
    f. 类型声明和定义,包括:structuniontypedef 
    g. 全局变量声明
    h. 文件级变量声明
    i. 全局/文件级函数声明
    j. 函数实现,按声明顺序排列
    k. 文件尾注释
  7. 引用头文件时不用绝对路径,使用相对路径;使用 <> 来引用预定义或特定目录头文件,用 "" 引用当前目录或相对当前目录的头文件

  8. 用 ifndef/define/endif 结构防止头文件重复引用

  9. 头文件只存放 “声明” 不存放 “定义”


排版

  1. 程序块采用缩进风格编写,缩进的空格数为 4 个
  2. 相对独立的程序块间、变量说明之后必须加空行

    void DemoFunc(void)
    {
    uint8_t i;
    
    // 局部变量和语句间空行
    
    for (i = 0; i < 10; i++)    // 功能块1
    {
        ...
    }
    
    // 不同功能块间空行
    
    for (i = 0; i < 20; i++)    // 功能块2
    {
        ...
    }
    }
  3. 长表达式可换行编写,操作符放在新行之首,新行可适当缩进使得排版整齐

    if ((ucParam1 == 0) && (ucParam2 == 0) && (ucParam3 == 0) 
    || (ucParam4 == 0))
    {
    ...
    }
  4. 不允许将多个短语句写在同一行,即一行只写一条语句

    rect.length = 0; rect.width = 0;    // 不正确写法
    
    rect.length = 0;    // 正确写法
    rect.width = 0;
  5. 操作符格式

    (1) 逗号、分号只在后面加空格
    int32_t a, b, c;
    
    (2) 比较操作符、赋值操作符、算数操作符、逻辑操作符、位域操作符前后需加空格
    if (current_time >= MAX_TIME_VALUE)
    a = b + c;
    a *= 2;
    a = b ^ 2;
    
    (3) "!, ~, ++, --, &, *"等单目操作符前后不加空格
    *p = 'a';
    flag = !isEmpty;
    p = &mem;
    i++;
    
    (4) "->, ." 前后不加空格
    p->id = pid;
    
    (5) if, for, while, switch 等与后面括号间应加空格
    if (a >= b && c > d)

注释

  1. 一般情况下,源程序有效注释量必须在 20% 以上

  2. 在文件开头应给出关于文件版权、内容简介、修改历史等项目说明

    具体的格式请参见如下的说明。在创建代码和每次更新代码时,都必须在
    文件的历史记录中标注版本号、日期、作者、更改说明等项目。其中的
    版本号的格式为两个数字字符和一个英文字母字符。数字字符表示大的改变,
    英文字符表示小的修改。如果有必要,还应该对其它的注释内容也进行同步的更改。
    注意:注释第一行星号要求为 76 ,结尾行星号为 1 
    /****************************************************************************
    
    * Copyright (C), 2010-2011,武汉汉升汽车传感系统有限责任公司
    
    * 文件名: main.c
    
    * 内容简述:
    
    *
    
    * 文件历史:
    
    * 版本号 日期 作者 说明
    
    * 01a 2010-07-29 王江河 创建该文件
    
    * 01b 2010-08-20 王江河 改为可以在字符串中发送回车符
    
    * 02a 2010-12-03 王江河 增加文件头注释
    
    */
  3. 对于函数应在函数实现前给出和函数实现相关的足够而精炼的注释信息。包含功能介绍、形参、全局变量、返回值等

    下面这段函数的注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内
    /****************************************************************************
    
    * 函数名: SendToCard()
    
    * 功 能: 向读卡器发命令,如果读卡器进入休眠,则首先唤醒它
    
    * 输 入: 全局变量 gaTxCard[]存放待发的数据
    
    * 全局变量 gbTxCardLen 存放长度
    
    * 输 出: 无
    
    */
  4. 最好编写代码边注释,以保证注释与代码的一致性,不再有用的注释应删除

  5. 注释内容要言简意赅,防止有歧义。注释注意阐述代码做了什么,必要时阐述为何如此做,而不是阐述它究竟如何实现算法的

  6. 避免在注释中使用缩写,特别是常用缩写

  7. 注释应与其描述的代码靠近,放在代码上方或右方(单条语句),不可放在下面,上方注释要与之前的代码用空行隔开

  8. 注释要与其修饰的代码保持同样的缩进

  9. 对变量的定义和分支语句(条件、循环等)必须编写注释,这些语句往往是程序实现某一特定功能的关键

  10. 注释格式尽量统一,建议使用段注释 /*****/,另外英文表达不准确的情形下,建议多使用中文


可读性

  1. 注意运算符的优先级,并用括号明确表达式的操作顺序,避免使用默认优先级

    // 正确示例
    word = (high << 8) | low;
    if ((a | b) && (a & c))
    if ((a | b) > (c & d))
    
    // 错误示例
    word = high << 8 | low;
    if (a | b && a & c)
    if (a | b > c & d)
  2. 避免使用不易理解的数字,用有意义的标识代替。涉及物理状态或物理意义的常量可用枚举或宏代替

    // 错误示例 - 0,1 不知代表何意
    if (Trunk[index].trunk_state == 0) {
    Trunk[index].trunk_state == 1;
    }
    
    // 正确示例
    enum trunk_state_e {
    TRUNK_IDLE = 0,
    TRUNK_BUSY = 1
    };
    
    if (Trunk[index].trunk_state == TRUNK_IDLE) {
    Trunk[index].trunk_state == TRUNK_BUSY;
    }
  3. 不使用难懂的技巧性很高的语句,除非很有必要时

    // 错误示例
    * stat_poi ++ += 1;
    * ++ stat_poi += 1;
    
    // 正确示例
    *stat_poi += 1;
    stat_poi++;
    
    ++stat_poi;
    *stat_poi += 1;

变量、结构、常量、宏

  1. 为书写及记忆,变量类型均采用 C99 的 stdint.h 所定义的

    typedef unsigned char uint8_t;
    
    typedef unsigned short uint16_t;
    
    typedef unsigned long int uint32_t;
    
    typedef signed char int8_t;
    
    typedef signed short int16_t;
    
    typedef signed long int int32_t;
    
    #define __IO volatile
  2. 常见类型前缀

var_type

- 对于一些常见类型的变量,在变量名前标注其类型的前缀,前缀用小写字母表示

- 对于几种变量类型组合,前缀可叠加
  1. 变量作用域的前缀

    为清晰标识变量的作用域,减少发生命名冲突,应在变量类型前缀之前再加上表示变量作用域的前缀,
    并在前缀与作用域前缀之间用下划线 '_' 隔开
    
    (1) 全局变量,可用 'g' 表示
    uint32_t g_ulParaWord;
    uint8_t g_ucByte;
    
    (2) 静态变量,可用 's' 表示
    static uint32_t s_ulParaWord;
    static uint8_t s_ucByte;
    
    (3) 函数内部等局部变量前可不加作用域前缀
    (4) 对于常量,当发生作用域或命名冲突时,以上几条规则也适用,
    常量的核心部分全部大写,前缀部分仍用小写字母,以保持前缀一致性
  2. 对于结构体类型命名,以小写字母 ‘tag’ 作为前缀标识,且类型名以大驼峰命名法,结尾用 ‘_T’;结构体变量以 ’t’ 作为前缀标识

    /* 结构体类型命名 */
    typedef struct tagBillQuery_T {
    ...
    } BillQuery_T;
    
    /* 结构体变量命名 */
    BillQuery_T tBillQuery;
    
    /* 枚举类型命名全大写,以 _E 结尾 */
    typedef enum {
    KB_F1 = 0,
    KB_F2,
    KB_F3
    } KEY_CODE_E;
  3. 常量、宏、模板命名应全部大写,多个单词组成以下划线隔开,如:#define LOG_BUF_SIZE 8000

  4. 对于变量命名,禁止取单个字符(如 i, j, k..),建议除要有具体含义外,还能表明变量类型、数据结构等,但 i, j, k 作局部循环变量是允许的

  5. 命名规范必须与所使用的系统风格保持一致,并在同一项目中统一,如采用 UNIX 的全小写加下划线的风格或驼峰命名的形式


函数

  1. 函数命名规则,每个函数名前缀需包含模块名,模块名小写,与函数名区别开。如 uartReceive,对于非常简单的函数可不加模块名

  2. 一个函数仅完成一件功能

  3. 函数名应准确描述函数功能,避免使用无意义或含义不清的动词作为函数名。使用动宾词组作为某操作的函数名

    void PrintRecord(uint32_t RecInd);
    int32_t InputRecord(void);
    uint8_t GetCurrentColor(void);

最后,自己特有的命名风格,要自始至终保持一致,不可变来变去。另外个人的命名风格在符合所在项目组或产品组的前提下才可使用。(即命名规则中没有规定到的地方才可有个人命名风格)


如何学习

本部分摘自 ARMFLY STM32-V5 开发板用户手册 第 40 章(附件 A)

学习观点

不排斥英文资料:在有中文资料的情况下,我们当然是看中文资料学习起来效率高一些。但是大部分资料是没有中文译稿的。比如 CPU 的数据手册只有英文的,固件库源代码中的注释也都是英文的。因此一定要习惯阅读英文手册,不认识的单词可以安装翻译软件协助阅读。

选择性学习:STM32 的功能实在太多,和 STM32 相关的软件组件也多如牛毛。而人的精力是有限的,你就算学习一辈子,也不可能学会所有的东西。 一些不常用的功能和知识点可以先放放,等待使用的时候再学习。几个必须了解的知识点:GPIO、TIM(基本定时器、脉宽测量、PWM输出)、UART(中断模式)、I2C、SPI、ADC(DMA)、DAC、FMC、看门狗等。 如果你已经接到一些项目和产品开发,那么恭喜你,你一定会选择性的学习,因为时间不允许你全面学习。

拿来主义观点:人的一生,时间是最为宝贵的,我们一定要将时间用在刀刃上。别人做好的开源的第三方的代码,如果你测试应用过没有问题,那就属于你的了,你只要学会怎么移植和使用它就足够了。如果别人已经移植好了,那就更方便了,直接拿来使用即可。没有必要再花精力深入代码去研究别人的程序结构了。比如FatFS、uIP、ST固件库、uCOS、emWin。除非你是做基础研究或者查找BUG。 我们一定要将时间腾出来去学习和掌握更多的软件模块。这样,当项目来临时,你就可以从容应对。好比盖房子,你可以直接拿砖砌,而不用在花时间去造砖。

构建自己的代码库:要学会收集整理自己验证过的代码,变为自己代码库的一部分,日积月累。等到做产品和项目时,80% 以上的代码来自于代码库。做产品很多时候就是搭积木。你的代码库越丰富,做起产品来也就越快越好。 一些好的工程框架也要学会收集整理。我们在很多时候做项目,都是直接复制某个相近的工程。不可能每次都去重新新建一个工程。

多动手:光看不练是成为不了程序高手的。我们的资料只能帮助你尽快掌握 STM32 的开发方法,带你尽快入门,同时告诉你大家使用的常用组件,避免你闭门造车。大家一定要动手修改程序,独立设计自己的程序。 如果你没有机会参与项目开发,完全没有目标。那我们建议你多上上论坛,看下别人遇到的问题,尝试自己解决下,这也是一种不错的锻炼方法。

培养兴趣:做程序员这一行,一定得培养起兴趣来。在学习的前期是最容易打退堂鼓的阶段,如果这个时候你放弃了,你的板子可能会开始吃灰。在前期,千万不要被复杂的程序结构所吓倒,一定要反复阅读,多调试跟踪进行分析。当你看懂了,你就会有成就感,才有继续学习的动力。

学会分享:分享也是学习的一部分,养成和大家分享成果的习惯。你可以分享自己的学习心得、调通的代码、帮别人解答问题,当这些内容获得网友认可的时候,你就会有成就感。分享的过程也在培养你的文笔、归纳总结能力。当你分享的内容到达一定数量时,量变到质变,不知不自觉中,你就会成为某一行的专家,你就有可能独立接到项目开发。如果没人找你,那说明你的能力还不够级别,或者不知道通过分享来宣传自己。

学习步骤

对于有一定C语言基础和ARM开发基础的人员,首要目的是快速熟悉STM32固件库的用法,推荐的学习步骤: 1. 快速浏览CPU参考手册中的 FSMC,PWR,RCC,GPIO、USART、interrupts and events 等章节。虽然看 CPU 的 DataSheet 很枯燥,但是这是最权威的信息,挑重点章节多看几遍比去网上搜索答案更有效,可以毫不夸张地说“阅读 DataSheet 也是一种技能”。ST 的 CPU 手册分两个文档,一个叫数据手册(描述不同封装 CPU 间的差异以及管脚定义),另外一个叫参考手册(详细描述CPU内部结构、外设功能以及各个寄存器的用法)。如果你对Cortex核心感兴趣,可以阅读 《STM32权威指南(中文).pdf》 2. 把一些自己感兴趣的例程跑一遍,主要是熟悉ST固件库中常用函数的用法。顺序可以按 GPIO、USART、TIM、ADC 等。 3. 在学习的过程中,要逐步掌握开发工具的用法。主要是 KEIL 或 IAR 集成环境、 J-Link/STLINK 仿真器。有一定基础后,可以自己设计或者在别人的基础上设计一个有使用价值的完整的程序,然后共享你的成果,和大家一起分享成功的喜悦。