高质量编程指南V1.0阅读笔记
这本书是林锐老师写的,第一版篇幅比较短,算是我第一个认真阅读的专业性比较强的电子书,内容是讲述一些代码规范以及错误
文件结构
声明文件
声明文件由三部分组成:
- 版权声明和函数功能说明
- 预处理块
- 函数和类结构声明(只存放声明,不存放定义)
/*
* 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连续操作两次就会出现错误