这本书是林锐老师写的,第一版篇幅比较短,算是我第一个认真阅读的专业性比较强的电子书,内容是讲述一些代码规范以及错误

文件结构


声明文件

声明文件由三部分组成:

  • 版权声明和函数功能说明
  • 预处理块
  • 函数和类结构声明(只存放声明,不存放定义)
/*
 * Copyright (C) 2024 EternalChip Co.£¬Ltd. or its affiliates.
 *
 * All Rights Reserved.
 *
 * File name: xxxx.h
 *
 *  @addtogroup  
 *  @brief       
 *
 *  @{
 *      @file       xxxx.h
 *      @brief      
 *      @details    
 *
 */
#ifndef XXXX_H // 防止 xxxx.h 被重复引用
#define XXXX_H 
#include <math.h>

#include "myheader.h" // 引用非标准库的头文件

void Function1( ⋯ ); // 全局函数声明

class Box // 类结构声明
{
    ...
};
#endif

在#include的时候是有讲究的,标准库使用<>(编译器从标准库目录开始搜索),非标准库使用””(编译器从用户的工作目录开始搜索)

除此以外,尽量不要在头文件出现extern int x;第一是违背了声明文件的初衷,定义应该出现在源文件或定义文件中;第二是污染命名空间和降低代码可读性

定义文件

定义文件由三部分组成:

  • 版权声明和函数功能说明
  • 头文件引用:分为三个部分,第一个部分是编译器库,例如stdio,string;第二部分是系统库,例如freertos,rtthread;第三部分是自定义库
  • 程序的实现体
/*
 * Copyright (C) 2024 EternalChip Co.£¬Ltd. or its affiliates.
 *
 * All Rights Reserved.
 *
 * File name: xxxx.h
 *
 *  @addtogroup
 *  @brief
 *
 *  @{
 *      @file       xxxx.c
 *      @brief
 *      @details
 *
 */
#include "graphics.h" // 引用头文件

int test = 0;

void Function1() // 函数的实现体
{
    ⋯ ⋯
}

代码的占用尽量是1/3-1/2个屏幕,方便他人查看阅读

实现的时候,如果有for/switch/while这种大型循环的时候,可以在循环体的结尾补一个end of xxx

打印的时候,出现多变量,按行打印

printf("1.%d"
	   "2.%d"
		,1
		,2)

命名规则

命名规则按照windows和linux分类,主要区别是windows大多用的驼峰式,也就是大小写混合,linux用的是小写+_组合,这本书只讲述了windows命名

1.命名不要出现仅靠大小写区分的相似的标识符,例如x和X,大写i和小写L等

2.全局函数的名字应当使用“动词”或者“动词+名词”;类或结构体的成员函数应当只使用“动词”,被省略掉的名词就是对象本身

3.根据类型不同,命名结构不同

  • 类名和函数名用大写字母开头的单词组合而成
  • 变量和参数用小写字母开头的单词组合而成
  • 常量全用大写的字母,用下划线分割单词
  • 静态变量加前缀 s_
  • 如果不得已需要全局变量,则使全局变量加前缀 g_
  • 类或结构体的数据成员加前缀 m_ ,这样可以避免数据成员与成员函数的参数同名
  • 防止某一软件库中的一些标识符和其它软件库中的冲突,可以为各种标识符加上能反映软件性质的前缀;例如三维图形标准 OpenGL 的所有库函数均以的所有库函数均以 gl 开头,所有常量(或宏定义)均以 GL 开头

表达式和基本语句


基本语句

1.需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部

2.运算符过多的时候,尽量使用括号括起来,避免使用默认优先级

3.不要编写太复杂和多用途的表达式,例如d = (a = b+c)+r ;或i = a >= b && c < d && c+f <= g+h ;

if语句

1.不可将布尔变量直接与 TRUE 、FALSE 或者 1 、0 进行比较;因为不同编译器标准不同,例如 Visual C++ 将 将 TRUE 定义为1 ,而 Visual Basic 则将 TRUE 定义为-1

2.不同类型if语句编写不同

  • 整型直接与目标值比较:if (value == 0)
  • 布尔型无需参考值:if(!bool)
  • 浮点数由于有精度限制,所以不能使用==或!=:if ((x>=-EPSINON) && (x<=EPSINON)),EPSINON是允许的误差
  • 指针类型是与NULL比较:if (value == NULL)

补充:判断语句中,表达式写为 0 != XXX或0 == XXX,方便判断是否误写为赋值语句=

for循环和switch语句


for循环

1.在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数

2.for循环变量按半闭半开区间写法,这样方便确定循环次数,例如:for(i = 0; i < N; i++),次数为N

3.如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面,具体情况按循环次数分

for (i=0; i<N; i++)//循环次数较少
{
	if (condition)
	{
		DoSomething();
	}
	else
	{
		DoOtherthing();
	}
}

if (condition)//循环次数较多
{
	for (i=0; i<N; i++)
	{
		DoSomething();
	}
}
else
{
	for (i=0; i<N; i++)
	{
		DoOtherthing();
	}
}

switch语句

1.每个 e case 语句的结尾不要忘了加 break,否则将导致多个分支重叠

2.不要忘记最后那个 t default 分支。即使程序真的不需要 default 处理,也应该保留语句default

函数设计


参数

1.参数传入顺序要合理,一般地,应将目的参数放在前面,源参数放在后面

2.如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改

3.如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递

内部

1.在函数体的“入口处”,对参数的有效性进行检查

2.在函数体的“出口处”,对 return 语句的正确性和效率进行检查

其他建议

1.函数的功能要单一,不要设计多用途的函数

2.函数体的规模要小,尽量控制在 50 行代码之内

3.尽量避免函数带有“记忆”功能

4.不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等

5.用于出错处理的返回值一定要清楚

断言

断言assert是需要#include <assert.h>,用法是assert(x),如果x为0或不合法,那么会终止程序,如果去#define NDEBUG,断言则会失效

用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的;一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警

内存管理


常见的内存错误及其对策

内存分配方式有三种

  • 静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量
  • 上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限
  • 上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 或 new 申请任意多少的内存,程序员自己负责在何时用申请任意多少的内存,程序员自己负责在何时用 free 或 或 delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多

1.内存分配未成功,却使用了它;解决办法是在使用内存之前检查指针是否为NULL

2.内存分配虽然成功,但是尚未初始化就引用它

3.内存分配成功并且已经初始化,但操作越过了内存的边界,例如指向局部变量,以及循环中数组误操作

4.忘记了释放内存,造成内存泄露

5.释放了内存却继续使用它

指针与数组的对比

数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变

指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存;指针远比数组灵活,但也更危险

主要区别分为三个

一个是修改;

    char a[] = "hello";
    a[0] = 'X';
    char* p = "world";
    printf("%s", p);
    char* p = "world";//p指向的是常量字符串
    p[0] = 'X'; 
    printf("%s", p);

二是复制;指针可以使用=赋值,而数组得使用库函数string中函数赋值

三是计算内存容量;指针始终与位数有关,32位就是32/8=4字节,数组是按个数*字节数计算的;除此以外,数组在函数会出现数组退化指针

指针参数传递内存

如果函数的参数是一个指针,不要指望用该指针去申请动态内存

错误示例:

void Test(void)
{
    char* str = NULL;
    GetMemory(str, 100);
    strcpy(str, "hello");
}

void GetMemory(char* p, int num)//str依旧是NULL
{
    p = (char*)malloc(sizeof(char) * num);
}

正确示例:

void Test(void)
{
    char* str = NULL;
    GetMemory(&str, 100);//参数是&str
    strcpy(str, "hello");
}

void GetMemory2(char** p, int num)
{
    *p = (char*)malloc(sizeof(char) * num);
}

毛病出在函数 y GetMemory 中。编译器总是要为函数的每个参数制作临时副本,指针参数p 的副本是 _p ,编译器使 _p = p 。如果函数体内的程序修改了 _p 的内容,就导致参数 p 的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中, _p 申请了新的内存,只是把p _p 所指的内存地址改变了,但是 p 丝毫未变

除此以外还有通过返回值来传递动态内存的

错误示例:

void Test3(void)
{
    char* str = NULL;
    str = GetString();
    free(str);
}

char* GetString(void)
{
    char p[] = "hello world";
    return p; 
}

正确示例:

void Test4(void)
{
    char* str = NULL;
    str = GetMemory3(100);
    strcpy(str, "hello");
    free(str);
}

char* GetMemory3(int num)
{
    char* p = (char*)malloc(sizeof(char) * num);
    return p;
}

Test3错误的原因是return 语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡

另外,还有一种容易混淆的但不会保错的示例

void Test5(void)
{
    char* str = NULL;
    str = GetString();
    free(str);
}

char* GetString(void)
{
    char *p = "hello world";
    return p; 
}

这个运行虽然不会报错,但是p指向的常量,位于静态存储区,生命周期不变,返回的只能是hello world

动态内存的释放


free释放

free只是把指针所指的内存给释放掉,但并没有把指针本身干掉

p被 free 以后其地址仍然不变(非 NULL),只是该地址对应的内存是垃圾,p 成了“野指针”。如果此时不把 p 设置为 NULL,会让人误以为 p 是个合法的指针

char *p = (char *) malloc(100);
strcpy(p, “ hello”);
free(p); // p 所指的内存被释放,但是 p 所指的地址仍然不变
if(p != NULL) // 没有起到防错作用
{
	strcpy(p, “ world” ); // 出错
}

局部变量释放

局部变量的指针变量消亡了,并不表示它所指的内存会被自动释放;内存被释放了,并不表示指针会消亡或者成了 NULL 指针

malloc/free使用


malloc

malloc原型:void * malloc(size_t size);→示例:int *p = (int *) malloc(sizeof(int) * length);

  • malloc 返回值的类型是 void * ,所以在调用malloc 时要显式地进行类型转换,将void * 转换成所需要的指针类型
  • malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。我们通常记不住 int, float 等数据类型的变量的确切字节数,使用的是关键字sizeof

free

free原型:void free( void * memblock );

为什么free 函数不象 c malloc 函数那样复杂呢?这是因为指针 p p 的类型以及它所指的内存的容量事先都是知道的,语句 free(p) 能正确地释放内存

如果p是NULL 指针,那么 free对p 无论操作多少次都不会出问题;如果p不是NULL指针,那么free 对 对p连续操作两次就会出现错误