投资集团网站建设方案,邮箱网址注册免费,深圳建设品牌网站,百度风云榜游戏排行榜
修改记录
版本号日期更改理由V1.02022-03-15MD化
总则
仅总结一些常用且实用的编程规范和技巧#xff0c;且避免记忆负担#xff0c;聚焦影响比较大的20% !
编译器
打开全warning编译器开关
正例
gcc -W -Wall -g -o someProc main.c反例
gcc -g -o someProc main…
修改记录
版本号日期更改理由V1.02022-03-15MD化
总则
仅总结一些常用且实用的编程规范和技巧且避免记忆负担聚焦影响比较大的20% !
编译器
打开全warning编译器开关
正例
gcc -W -Wall -g -o someProc main.c反例
gcc -g -o someProc main.c建议增加-W -Wall -g编译器开关
打开-W -Wall 警告开关可以让编译器尽量将代码中的告警进行展示打开-g开关以利于在出错的情况下可以较为准确地获知堆栈信息
持续消除warning
在warning开关全开的情况下应检查warning的情况最好做到零warning。
建议将某些warning转为编译error
可以根据工程特点总结出来一批影响比较大而且容易出现问题的warning直接转化为编译器error选项以利于用编译工具保证质量。有条件的项目编译工程可以通过-Werror将全部warning转为编译error。
正例
gcc -W -Wall -Werroroverflow -g -o someProc main.c或gcc -W -Wall -Werror [-Wno-*...] -g -o someProc main.c即使在-Werror开关的情况下依然可以通过-Wno-*的编译选项进行定制排除将某些影响不大的warning排除 使用高版本编译器 借用高版本编译器往往能够检查出代码中重大隐患和产生更高质量的代码建议在能够使用高版本编译器的情况下尽量使用 建议使用docker构建高版本编译器环境
在docker某基础镜像中加入devtoolset-*组件对于CentOS系列发行版建议安装devtoolset-7或devtoolset-9。这样对于当前旧编译器依赖的工程可以无影响地进行高版本编译编译提前将一些隐患排除。
启动镜像时脚本
docker run -it --rm -h devtoolsetVersion -w /usegccversion -v $PWD:/usegccversion image bash使用高版本编译标准 在低版本编译器中尽量使用高版本编译标准可以使用到某些友好特性和错误检查 获得支持临时循环指示变量临近使用特性
正例
for(unsigned int i 0; i MAX; i)
{ ...
}反例
void f()
{unsigned int i 5;// some other code...for(i0; i MAX; i){...}}gcc编译器可以指定–stdgnu11等一些高版本编译标准。临近使用变量特性将使得封装性更好避免无效的提前干扰有利于阅读代码 建议O2优化标准
编译器优化可以将代码运行性能加速到极致但为了避免一些负优化建议使用与Linux内核一致的O2优化级别。 优化虽在我们仍提倡撰写高质量的代码作为比较好的编译器输入 适当地方使用register变量
C语言支持寄存器变量x86-64上又提供更多的寄存器可用所以在计算校验和等比较密集耗费CPU的场景对于关键变量建议使用register关键词修饰。
编程规范
以人为本
空格间隔操作符与左值右值
正例
void f(int * intPtr)
{if(NULL intPtr){...}...
}反例
void f(int * intPtr)
{if(NULLintPtr){...}...
}以人为本的理念在编程实践时如果对于机器阅读没有困难而对于程序员阅读存在困难的代码都应该通过空格、空行分割、打印日志分割、scope对齐等常用方式进行辅助编码。 源码编辑Tab转为空格
编辑器设定Tab键空格数量为4代码编辑器设置支持将Tab固定转为空格代码编辑器支持将旧文件中Tab批量转为空格
控制语句即使只有一行也使用花括号作为块分割
正例
void main(void)
{if(someConditionOk){someFlag TRUE;}...
}反例
void main(void)
{if(someConditionOk)someFlag TRUE;...
}{ }分割有助于人眼分辨而不是机器可以正确处理。在后期if分支修改时不容易引起维护上的问题。 用const关键词修饰栈变量和函数入参指针参数
用编译器拦截一部分意外修改增加编译器优化的深度例如对于不变量可以进行深度优化对于const类型的数据量熟练阅读代码的人可以有选择地忽略和选择重点变化代码部分 多数较为新的C语言开源组件在API接口设计多遵从const 修饰符原则 巧用和包含头文件区别依赖可变性
正例
// system api
#include sys/time.h// third-party api
#include third-party/time.h//self module api
#include otherApiInModule.h反例
// system api
#include sys/time.h// third-party api
#include third-party/time.h//self module api
#include otherApiInModule.h原则上有区别的差别最好采用有区别的写法。从书写字面区分头文件组件内外不同显示引用API的可变程度不同对于后续阅读代码有利。 分支语句尽量采用likely/unlikely标注
正例
// system api
if(likely(conditiontest))
{//most hit code...
}
else
{//less hit code...
}反例
if(conditiontest)
{...
}
else
{...
}likeyly/unlikely提高代码可阅读性自然关注主要逻辑流程 让编译器生成代码对于分支预测机制更为友好
异常分支必须打印异常日志
正例
int f(int * intPtr)
{if(NULL intPtr){exception_log(some must parameter is NULL);return errorValue;}//normal flow...
}反例
int f(int* intPtr)
{if(NULL intPtr){return errorValue;}//normal flow...
}typedef有名结构体
正例
typedef struct tagPerson
{unsigned char name[MAX_NAME];int age;
} Person;反例
typedef struct
{unsigned char name[MAX_NAME];int age;
} Person;typedef结构体不具有名称则在旧版IDE编辑器中会造成输入辅助提示的混乱 在typedef命名的新名称前加入tag作为结构体的名称也是一种惯用法
代码体行数限制
200行为上限太长则需分拆 首先是代码逻辑上梳理清晰然后就可以分步骤、分组件分为3~5个概念步骤理解最顺畅 让规范代码模式重复出现
在维护过程中由于历史存量的原因规范的代码和不规范的代码并存应尽量批量修改掉某种范式的不规范代码以避免将来拷贝粘贴代码进一步扩大污染。 例如sprintf字符串函数可以自动补零但是对于拷贝目的地的长度没有保护所以在涉及的地方几乎可以无危险代价地替换为snprintf函数 。 可以使用代码编辑器中提供的调用堆栈、正则表达式等工具批量发现修改涉及点
避免简单重复代码出现
此条建议看似与前面建议冲突实则不然。简单重复的代码是代码中最大的坏味道和体现程序员的懒惰。 当某些代码重复出现三次以上时是你应该考虑用公共组件来代替简单重复的时候了。
敢为人先避免破窗效应
破窗效应是指某个地方出现了坏的味道后后面这种坏的情况在自然情况下没有人愿意改变只能变得更糟。 作为程序员应该敢于迈出第一步让代码变得比昨天更好。
注重重构技巧
重构代码也是有方法论的具体可见《重构改善即有代码的设计》。 在编译器、自动化测试用例保护、以及重构指导步骤的导引下进行代码重构尽量降低出错概率给自己或他人增强修改代码的信心。
注释
进行必要的简短注释
注释的两个极端是没有注释或充满注释都不是很好的策略仅进行必要的注释。
不简明代码的注释等同于一句道歉
对于不简明的代码进行注释实际上等同于道歉说明这些代码太复杂了作者怕后来阅读者不能尽懂不得不用注释进行特殊说明。 可以考虑是否有简明的实现进行代码重构。 来自网络观点的启发 如果注释的地方可以增补日志用日志代替
在避免重复的原则下如果能够同时出现注释和日志那么用日志代替注释即可而且日志可以打印更完整的运行时信息利于后期分析问题。
编码技巧
使用空{}清零初始化结构和数组栈变量
正例
void f()
{struct A a {};unsigned char buffer[512] {};//some code using the above vars...
}反例
void f()
{struct A a ;unsigned char buffer[512] ;memset(a, 0, sizeof(a));memset(buffer, 0, sizeof(buffer));//some code using the above vars...
}空花括号清零简洁、美观、高效 小块内存清零应避免调用函数的代价。在C23 C语言建议标准中已作为标准建议替代memset空花括号和花括号带零适应不同的情况空花括号清零应对结构体和结构体内含有结构体成员的复杂结构体
内存操作长度sizeof(var)变量化
正例
void f(const unsigned char* pBuffer)
{struct A a ;struct A aa;memcpy(a, pBuffer, sizeof(a));struct A* ptA aa;memcpy(ptA, pBuffer, sizeof(*ptA));
}反例
void f(const unsigned char* pBuffer)
{struct A a ;struct A aa;memcpy(a, pBuffer, sizeof(struct A));struct A* ptA aa;memcpy(ptA, pBuffer, sizeof(struct A));
}此条建议可能有所争议但变量化存在至少两个好处 自适应类型变化避免书写类型如果sizeof后面参数填写错误利于立即发现问题使用不匹配类型一些少量的内存操作越界不易被发现 return卫语句缓解复杂度
正例
if(conditionTest)
{exception_log(...);return flow_end;
}//normal code flow
...反例
void f()
{if(condition TRUE){return flow_end;}else{...}return end_of_code;
}就近访存
原地化访问
从计算机机器结构上来看计算机访问存储器偏向于局部访问CPU可以在不改变基地址的情况下用小范围的地址偏移增减就可以获得快速访问。 如果访问存储器位置在非常局部的内存也可从计算机缓存体系得到受益。
远端访问栈变量化
正例
struct A* const ptA getASingleton();while(TRUE)
{//some usagef(ptA);
}
反例
extern struct A* getASingleton();while(TRUE)
{//some usagef(getASingleton());
}应使用简明数值操作写法
在算术操作尽量使用、–、、*等简写语言操作符一方面书写简明另一方面以利于编译做优化。
反例
int offset initValue;...//在编译器不能很好优化代码情况下可以能会多一次访存操作
offset offset 1;使用匿名联合代替单一命名以利于多用途操作
正例
typedef struct tagIPv4_Addr
{union {unsigned char abAddr[4];unsigned int dwAddr;};
} IPv4_Addr;void f()
{IPv4_addr ip ;unsigned char buffer[512];//for whole value usageprintf(encoding IPv4 Addr Value:%u\n, ip.dwAddr);//for byte operationmemcpy(buffer, ip.abAddr, sizeof(ip.abAddr));
}反例
void f()
{unsigned char ip[4] ;unsigned char buffer[512];//for whole value usageprintf(encoding IPv4 Addr Value:%u\n, *(unsigned int *)ip);//for byte operationmemcpy(buffer, ip, sizeof(ip));
}使用匿名联合几乎无代价和更简明。在某些不便修改的地方可以用匿名联合做别名访问 准确使用类型让编译器做更多事情
数组下标应使用无符号数如果输入可以限定在某一范围请使用枚举类型也利于调试时将值显示为更有意义的命名而非数值。能用枚举类型的场景尽量用枚举。
数值型宏定义指定明确类型
正例
#define MAX_CAPACITY (unsigned char)64反例
#define MAX_CAPACITY 64注意类型比较类型一致
正例
#define MAX_CAPACITY (unsigned char)64unsigned char bStartIndex 12;
unsigned char bEndIndex 36;if((unsigned char )(bEndIndex - bStartIndex) MAX_CAPACITY)
{exception_log(...);return FALSE;
}反例
#define MAX_CAPACITY 64unsigned char bStartIndex 12;
unsigned char bEndIndex 11;//编译器生成代码有点出乎意料允许负值的产生
if((bEndIndex - bStartIndex ) MAX_CAPACITY)
{exception_log(...);return FALSE;
}//error 无法防止负值的出现
... 头文件
头文件设计内外有别
头文件区分对外部使用头文件和对内使用头文件对外API头文件仅提供少量公共、必要的声明以利于模块间的隔离和API接口Bridge桥模式独立演化演化。
正例 -- someComponent-- include component.h component_internal.h ...-- src ...反例 -- someComponent-- include component_api_all_in_one.h -- src头文件加入C支持和头宏定义避免重复包含支持
正例
#ifndef __XX_YY_h
#define __XX_YY_h#ifdef __cplusplus
extern C {
#endif//some declarations...#ifdef __cplusplus}
#endif#endif反例
// a header file
//some C language declarations
...稳定API设计技巧利用上下文对象指针和操作函数
正例
#ifndef __XX_YY_h
#define __XX_YY_h#ifdef __cplusplus
extern C {
#endif//some declarations
typedef struct tag_T_Context { ... } T_Context;T_Context* api_Alloc_Context();
int api_set_Context_Option(T_Context* ptContext, someType optionPara);
int api_request(T_Context* ptContext, someType optionParas ...);
int api_responset(T_Context* ptContext, someType optionParas ...);
int api_setCallback(T_Context* ptContext, someCallbackFunction fn ...);#ifdef __cplusplus}
#endif#endif这样的设计让上下文对象成为可以保证不同实现间的相互隔离、独立、并发并行而且不操作具体内存相关的字段具有二进制布局依赖独立性可以视作C语言编程领域内对接口进行编程。 日志规范
日志信息中提供必要信息
提供必要的信息以利于从日志中获取代码运行时走入的逻辑分支
正例
void f(int indicator)
{log(%s some flow info indicator:%d, __func__, indicator);if(indicator someValue){...}else{...}}反例
void f(int indicator)
{log(%s begin run ..., __func__);//看日志并不容易得到运行分支信息 ???if(indicator someValue){...}else{...}}不同分支日志信息尽量避免重复信息
正例
if(conditiontest){log(process setup gracefully ...);...}
else{log(process setup forcely ...);...}反例
if(conditiontest){log(the same print info ...);...}
else{log(the same print info ...);...}更大范围的日志信息重复也应该被注意这样可以保证日志信息的相对唯一、精准利于准确分析代码位置和运行逻辑. 函数内日志提供函数名称等信息
正例
void f(int indicator)
{log(%s: enter in with indicator:%d, __func__, indicator);if(indicator someValue){...}else{...}
}反例
void f(int indicator)
{log(enter in with indicator:%d, indicator)if(indicator someValue){...}else{...}
}日志中甚至可以提供__func__、 __file__、__line__等编译信息打印 杂项
频繁使用的短函数建议inline化
在1~10行之内的短小函数如果频繁调用可以考虑inline化
static inline __attribute__((__always_inline__)) f(...)
{ ...
}使用并行利器静态线程变量
正例
//can be used by multiple threads
const char* f(unsigned char bType)
{static __thread char buffer[512];// format information into buffer using bType para...return buffer;//线程间使用内存区别开
}反例
//maybe used by multiple threads
const char* f(unsigned char bType)
{static char buffer[512];// format information into buffer using bType para...return buffer;//多线程场景存在多线程竞争
}使用静态线程变量具有静态变量和栈变量的双重优点在多线程编程场景可有利于充分并行 多核编程考虑到是核的亲和性以及内存访问的亲和性与多线程考量问题有别
GNU构造函数扩展
static void __attribute__((__constructor__)) f(...)
{...
}可以对于一些无依赖和无先后顺序的初始化场景在main函数运行前自动被初始化 版本管理
应该小步提交并提交前走查代码
程序员应学会应版本化管理思想每一步一个小的变更逐渐螺旋迭代将提交代码限定在一个小的变化中也有利于提交前进行代码走查原子小步提交代码的工作方法也有利于后期使用版本管理软件进行合并、回退变更、代码走查等活动避免代码意外丢失或损坏也应该及时提交版本服务器而非累积大的变化提交 无论Git或SVN均支持本地提交某些不适合提交正规版本服务器的变化可以用本地版本库进行变更管理 自测
应该注重测试
只有编码水平非常高的人才可以一次性将代码编写正确。一般情况下普通程序员都很难达到一次编码正确所以应该注重对于代码的测试。
尽可能用自动化测试
对于可以自动化测试保证接口或流程正确的场景应该尽可能地形成自动化测试用例避免重复劳作。 尽量借助于测试框架进行测试代码开发以利于代码的规范和复用 Linux QT Creator GUI调试
在Linux图形化环境中可以使用开源QTCreator IDE编码工具依赖工程的Makefile文件就可以建立起来调试环境。
GDB调试
在~/.gdbinit文件扩展全局常用命令
在此文件中增加的gdb指令会在GDB打开过程中被自动执行例如设置库加载路径、代码搜索路径、常用命令别名等
define ab
thread apply all bt
enddefine ff
set confirm off
file a.out
set confirm on
enddefine dasm
disassemble /m
enddefine exit
quit
end多线程程序获知全部堆栈
thread apply all bt对于CPU挂死的线程其运行堆栈在数次输出下均稳定在某一个堆栈跳动范围内即可比较准确地定位到死循环或阻塞的发生地方 对于调试程序定制化初始化设定
gdb --commandself.gdb -p $(pidof selfApp)通过–command参数项设定被调试程序的自动化执行gdb指令 self.gdb 定制样例
cd /path/to/proc
set env LD_LIBRARY_PATH/path/to/so:$LD_LIBRARY_PATH
set args 1 57 1 1 2 somePara
file selfAppwatch内存断点识别意外内存修改
watch var
watch *(int*)0x22cbc0断点自动执行命令序列
对于频繁在断点需要执行的的手工输入命令操作可以通过command breakpoint_num的方法提前设定制动执行的命令避免重复。
(gdb) b do_mmap_pgoff
Breakpoint 1 at 0xffffffff8111a441: file mm/mmap.c, line 940.
(gdb) command 1
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just end.
print addr
print len
print proto
end
(gdb)快速选择已输入历史命令 Ctrl R 输入关键词匹配到历史命令要再次运行该匹配命令请按Enter。 要查找下一个匹配的命令请再次单击Ctrl R 要编辑命令请按左或右光标键相当于选中后编辑
内存故障定位
valgrind辅助定位
valgrind --leak-checkfull --track-originsyes ./someProc [para...]调优
总则
除了常识性、规范性的优化其它性能优化应该尽量地晚不应该提前优化
perf程序性能分析工具
通过perf 工具可以对程序运行期的动态行为进行记录通过量化的方式发现可以优化的代码位置特别推荐!
perf record
perf record -g -p ${pid} [-o /path/to/output_perfdata_file]如果不指定-o 输出文件参数将默认输出到perf.data文件中。 如果想形成多份不同的输出文件易于后期比较可以指定-o参数设定不同的输出文件名字例如a.perf.data。 补充perf支持工具级不同性能采集数据之间的diff差异法分析。
perf report
perf report [-i /path/to/perfdata_file]如果不指定性能统计分析数据则默认寻找perf.data文件即perf record默认输出的文件报告应关注两列均是红色的警醒 perf archive脱机分析
在某些特殊场景我们可能需要在A机器采集的性能统计数据在B机器上分析那么不单单需要perf.data的文件还需要辅助性能采集统计数据需要利用用perf archive子命令形成辅助文件后一并在B机器上部署才可以进行分析。
perf archive [/path/to/perfdata_file]其他机器分析
采集机器上收集
# at A machine has perf.data
perf archivescp perf.data perf.data.tar.bz2 rootother_B_machine:~/在其他机器上重新部署
# at other B machine
cd ~
tar xvf perf.data.tar.bz2 -C ~/.debug# 进行查看分析
perf report参考
我在Linux中C语言编程经验总结