@taqikema
2020-02-03T12:41:58.000000Z
字数 18402
阅读 2233
华为
C语言
规范
笔记
此文档虽说是阅读《华为 C语言编程规范》的笔记,但更多的是原文的提纲+阅读感受,建议大家还是阅读原文,毕竟原文本身也不多,很快就能看完,重点是要在自己平时写代码时能够想到、坚持这样的代码风格!
1.清晰第一
代码首先是给人读的,一般情况下,代码的可阅读性高于性能,只有确定性能是瓶颈时,才应该主动优化。
2.简洁为美
废弃的代码(没有被调用的函数和全局变量)要及时清除,重复代码应该尽可能提炼成函数。
3.选择合适的风格,与代码原有风格保持一致
如果重构/修改其他风格的代码时,比较明智的做法是根据现有代码的现有风格继续编写代码,或者使用格式转换工具进行转换成公司内部风格。
对于C语言来说,头文件的设计体现了大部分的系统设计。不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上不合理的设计。想要设计出合理的头文件,需要考虑到实现者(做出来是否方便)和使用者(用起来是否方便)。
依赖将导致编译时间的上升。
头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等,而*内部使用的函数(相当于类的私有方法)声明、内部使用的宏、枚举、结构定义、变量定义不应放在头文件中。
变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。
头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。
头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。产品依赖于平台,平台依赖于标准库。
简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。
1.所有头文件都应当使用#define 防止头文件被多重包含,命名格式为FILENAME_H,为了保证唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H。
2.没有在宏最前面加上“_",即使用FILENAME_H代替_FILENAME_H,是因为一般以"_"和”__"开头的标识符为系统保留或者标准库使用,在有些静态检查工具中,若全局可见的标识符以"_"开头会给出告警。
在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。
extern写法容易在函数定义改变时可能导致声明和定义不一致。
示例:错误的使用方式:
extern "C"
{
#include "xxx.h"
...
}
正确的使用方式:
#include "xxx.h"
extern "C"
{
...
}
可以的情况下,对于使用者仅提供一个 .h文件,这样,用户不必知道模块内部各个文件的关系,也方便开发人员能在不改变用户使用接口的同时进行后续维护。
一些IDE工具无法识别其为头文件,导致很多功能不可用。
常见的包含头文件排列方式:功能块排序、文件名升序、稳定度排序。
1.以升序方式排列头文件可以避免头文件被重复包含。
2.以稳定度排序,建议将不稳定的头文件放在前面,如把产品的头文件放在平台的头文件前面。
函数设计的精髓:编写整洁函数,同时把代码有效组织起来。
整洁函数要求:简单直接、直截了当的控制语句
代码的有效组织:逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构;物理层,用一种标准的方法将函数组织起来,如目录结构、函数命名。
将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动。
项目组应当使用代码重复度检查工具,在持续集成环境中持续检查代码重复度指标变化趋势,并对新增重复代码及时重构。当一段代码重复两次时,即应考虑消除重复,当代码重复超过三次时,应当立刻着手消除重复。
过长的函数往往意味着函数功能不单一,过于复杂,但算法实现类函数除外。
函数的代码块嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch等)之间互相包含的深度。每级嵌套都会增加阅读代码时的脑力消耗,因为需要在脑子里维护一个“栈”(比如,进入条件语句、进入循环……)。应该做进一步的功能分解,从而避免使代码的阅读者一次记住太多的上下文。
共享变量指的全局变量和static变量
对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。
一个函数(标准库中的函数/第三方库函数/用户定义的函数)要能够提供一些指示错误发生的方法,这可以通过使用错误标记、特殊的返回数据或者其他手段。
扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。
扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,例如:总是1,表明函数的调用层次可能过多,这样不利于程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。通常函数比较合理的扇出(调度函数除外)通常是3~5。
较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。
不仅使代码更安全,也更易于源码的阅读和理解。
带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。
可变长参函数的处理过程比较复杂容易引入错误,而且性能也比较低,使用过多的可变长参函数将导致函数的维护难度大大增加。
建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打热补丁等操作
unix like风格:单词用小写字母,每个单词直接用下划线‘_’分割,例如text_mutex。
Windows风格:大小写字母混用,单词连在一起,每个单词首字母大写。
匈牙利命名法:基本类型、一个或更多的前缀、一个限定词。
对标识符定义主要是为了让团队的代码看起来尽可能统一,有利于代码的后续阅读和修改,产品可以根据自己的实际需要指定某一种命名风格,规范中不再做统一的规定。
尽可能给出描述性名称,不要节约空间,让别人很快理解你的代码更重要。
好的命名:
int error_number;
不好的命名:使用模糊的缩写或随意的字符:
int n;
int nerr;
较短的单词可通过去掉“元音”形成缩写,较长的单词可取单词的头几个字母形成缩写,一些单词有大家公认的缩写,常用单词的缩写必须统一。协议中的单词的缩写与协议保持一致。
常见可以缩写的例子:
argument 可缩写为 arg
buffer 可缩写为 buff
clock 可缩写为 clk
command 可缩写为 cmd
compare 可缩写为 cmp
configuration 可缩写为 cfg
device 可缩写为 dev
error 可缩写为 err
hexadecimal 可缩写为 hex
increment 可缩写为 inc、
initialize 可缩写为 init
maximum 可缩写为 max
message 可缩写为 msg
minimum 可缩写为 min
parameter 可缩写为 para
previous 可缩写为 prev
register 可缩写为 reg
semaphore 可缩写为 sem
statistic 可缩写为 stat
synchronize 可缩写为 sync
temp 可缩写为 tmp
示例:
add/remove begin/end create/destroy insert/delete first/last get/release increment/decrement put/get add/delete lock/unlock open/close min/max old/new start/stop next/previous source/target show/hide send/receive source/destination copy/paste up/down
很多已有代码中已经习惯在文件名中增加模块名,这会导致文件名太长,并且不利于维护和代码的移植。
因为不同系统对文件名大小写处理会不同(如MS的DOS、Windows系统不区分大小写,但是Linux系统则区分),所以代码文件命名建议统一采用全小写字母命名。
变量命名需要说明的是变量的含义,而不是变量的类型。在变量命名前增加类型说明,反而降低了变量的可读性;更麻烦的问题是,如果修改了变量的类型定义,那么所有使用该变量的地方都需要修改。
如 GetCurrentDirectory。
相关的一组信息才是构成一个结构体的基础,结构的定义应该可以明确的描述一个对象,而不是一组相关性不强的数据的集合。
直接使用其他模块的私有数据,将使模块间的关系逐渐走向“剪不断理还乱”的耦合状态,这种情形是不允许的。
通讯报文中,字节序是一个重要的问题,我司设备使用的cpu类型复杂多样,大小端、32位/64位的处理器也都有,如果结构会在报文交互过程中使用,必须考虑字节序问题。
数据成员发送前,都应该进行主机序到网络序的转换;接收时,也必须进行网络序到主机序的转换。
避免直接暴露内部数据给外部模型使用,是防止模块间耦合最简单有效的方法。
未初始化变量是C和C++程序中错误的常见来源。在变量首次使用前确保正确初始化。在较好的方案中,变量的定义和初始化要做到亲密无间。
有符号和无符号类型的相互转换,在平台迁移时有时会出现问题。建议变量定义可以使用类似 S8/U8的宏进行,这样也便于变量类型的修改。
防止在 if或 for中直接使用宏而不添加大括号时出错,更好的方法是多条语句写成do while(0)的方式。
对于广泛使用的数字,必须定义const全局变量/宏;同样变量/宏命名应是自注释的。
宏对比函数,有一些明显的缺点:宏缺乏类型检查,不如函数调用检查严格;宏展开可能会产生意想不到的副作用;以宏形式写的代码难以调试难以打断点,不利于定位问题。
尽量用编译器而不用预处理。
#define ASPECT_RATIO 1.653
,编译器会永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO。如果ASPECT_RATIO不是在你自己写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去。
如果在宏定义中使用这些改变流程的语句,很容易引起资源泄漏问题,使用者很难自己察觉。
易混淆的操作符,如:赋值操作符“=” 逻辑操作符“==” 关系操作符“<” 位操作符"<<" 关系操作符“>” 位操作符“>>” 逻辑操作符“||” 位操作符"|" 逻辑操作符“&&” 位操作符"&" 逻辑操作符"!" 位操作符“~”。
易用错的操作符,如:除操作符"/"、求余操作符"%"、自加、自减操作符“++”、“--”。
此类错误一般是由于把“<=”误写成“<”或“>=”误写成“>”等造成的,由此引起的后果,很多情况下是很严重的,所以编程时,一定要在这些地方小心。
有很多函数申请内存,保存在数据结构中,要在申请处加上注释,说明在何处释放。
goto语句会破坏程序的结构性,所以除非确实需要,最好不使用goto语句。但好处是可以利用goto语句方面退出多重循环。
此种问题一般是出现在使用无符号变量时可能会出现边界i溢出情况。
让一个正确的程序更快速,比让一个足够快的程序正确,要容易得太多。大多数时候,不要把注意力集中在如何使代码更快上,应首先关注让代码尽可能地清晰易读和更可靠。
例如,使用线程池机制,避免线程频繁创建、销毁的系统调用;使用内存池,对于频繁申请、释放的小块内存,一次性申请一个大块的内存,当系统申请内存时,从内存池获取小块内存,使用完毕再释放到内存池中,避免内存申请释放的频繁系统调用
inline函数的优点:其一编译时不用展开,代码SIZE小。其二可以加断点,易于定位问题,例如对于引用计数加减的时候。其三函数编译时,编译器会做语法检查。
优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,需要很多注释来解释的代码往往存在坏味道,需要重构。
有歧义的注释反而会导致维护者更难看懂代码,正如带两块表反而不知道准确时间。
注释不是为了名词解释(what),不是为了重复描述代码,而是说明用途(why)。
这个要求本身不难,但是却是在开发过程中很难坚持做到的一点,也是现在我们公司代码里面存在较为广泛的现象。
通常头文件要对功能和用法作简单说明,源文件包含了更多的实现细节或算法讨论。
重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。
这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。
注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。
以doxygen格式为例,文件头,函数和全部变量的注释的示例如下:
文件头注释:
/**
* @file (本文件的文件名eg:mib.h)
* @brief (本文件实现的功能的简述)
* @version 1.1 (版本声明)
* @author (作者,eg:张三)
* @date (文件创建日期,eg:2010年12月15日)
*/
函数头注释:
/**
*@ Description:向接收方发送SET请求
* @param req - 指向整个SNMP SET 请求报文.
* @param ind - 需要处理的subrequest 索引.
* @return 成功:SNMP_ERROR_SUCCESS,失败:SNMP_ERROR_COMITFAIL
*/
Int commit_set_request(Request *req, int ind);
全局变量注释:
/** 模拟的Agent MIB */
agentpp_simulation_mib * g_agtSimMib;
函数头注释建议写到声明处。并非所有函数都必须写注释,建议针对这样的函数写注释:重要的、复杂的函数,提供外部使用的接口函数。
宏定义、编译开关、条件预处理语句可以顶格(或使用自定义的排版方案,但产品/模块内必须保持一致)。
1.在已经非常清晰的语句中没有必要再留空格,如括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C语言中括号已经是最清晰的标志了。
2.逗号、分号只在后面加空格
3.比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格。
4."!"、"~"、"++"、"--"、"&"(地址操作符)等单目操作符前后不加空格。
5."->"、"."前后不加空格。
6. if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。
本小节内容虽少,但却是平时写代码过程中容易忽略并且会产生较大影响的问题,需要额外注意。
说明:除了逗号(,),逻辑与(&&),逻辑或(||)之外,C标准没有规定同级操作符是从左还是从右开始计算,需要保证一个表达式有且只有一个计算结果,较好的方法就是将复合表达式分开写成若干个简单表达式,明确表达式的运算次序,就可以有效消除非预期副作用。
1.自增或自减操作符
x = b[i] + i++;
b[i] 的运算是先于还是后于 i++ 的运算,表达式会产生不同的结果,把自增运算做为单独的语句,可以避免这个问题。
2.函数参数,函数参数通常从右到左压栈,但函数参数的计算次序不一定与压栈次序相同。
示例:
x = func( i++, i);
应该修改代码明确先计算第一个参数:
i++;
x = func(i, i);
3.函数指针
示例:
p->task_start_fn(p++);
求函数地址p与计算p++无关,结果是任意值。必须单独计算p++:
p->task_start_fn(p);
p++;
4.函数调用
示例:
int g_var = 0;
int fun1()
{
g_var += 10;
return g_var;
}
int fun2()
{
g_var += 100;
return g_var;
}
int x = fun1() + fun2();
编译器可能先计算fun1(),也可能先计算fun2(),由于x的结果依赖于函数fun1()/fun2()的计算次序(fun1()/fun2()被调用时修改和使用了同一个全局变量),则上面的代码存在问题。
5.嵌套赋值语句
6.volatile访问
限定符volatile表示可能被其它途径更改的变量,例如硬件自动更新的寄存器。编译器不会优化对volatile变量的读取。
如下代码不合理,仅用于说明当函数作为参数时,由于参数压栈次数不是代码可以控制的,可能造成未知的输出:
int g_var;
int fun1()
{
g_var += 10;
return g_var;
}
int fun2()
{
g_var += 100;
return g_var;
}
int main(int argc, char *argv[], char *envp[])
{
g_var = 1;
printf("func1: %d, func2: %d\n", fun1(), fun2());
g_var = 1;
printf("func2: %d, func1: %d\n", fun2(), fun1());
}
1.因为if语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行。
2.作用函数参数来使用,参数的压栈顺序不同可能导致结果未知。
int g_var;
int main(int argc, char *argv[], char *envp[])
{
g_var = 1;
printf("set 1st: %d, add 2nd: %d\n", g_var = 10, g_var++);
g_var = 1;
printf("add 1st: %d, set 2nd: %d\n", g_var++, g_var = 10);
}
1.一元操作符,不需要使用括号
2.二元以上操作符,如果涉及多种操作符,则应该使用括号
3.即使所有操作符都是相同的,如果涉及类型转换或者量级提升,也应该使用括号控制计算的次序
/* 除了逗号(,),逻辑与(&&),逻辑或(||)之外,C标准没有规定同级操作符是从左还是从右开始计算,以上表达式存在种计算次序:f4 = (f1 + f2) + f3 或f4 = f1 + (f2 + f3),浮点数计算过程中可能四舍五入,量级提升,计算次序的不同会导致f4的结果不同,以上表达式在不同编译器上的计算结果可能不一样,建议增加括号明确计算顺序*/
f4 = f1 + f2 + f3;
某些语句经编译/静态检查产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。
及时签入代码降低集成难度。
单元测试实施依赖于:
本规则是针对项目组或产品组的。代码至始至终只有一份代码,不存在开发版本和测试版本的说法。测试与最终发行的版本是通过编译开关的不同来实现的。并且编译开关要规范统一。统一使用编译开关来实现测试版本与发行版本的区别,一般不允许再定义其它新的编译开关。
统一的调测日志记录便于集成测试,具体包括:
断言的使用是有条件的。断言只能用于程序内部逻辑的条件判断,而不能用于对外部输入数据的判断,因为在网上实际运行时,是完全有可能出现外部输入非法数据的情况。
以下场景需要对用户输入进行检验,以确保安全:
这些情况下如果不对用户数据做合法性验证,很可能导致DOS、内存越界、格式化字符串漏洞、命令注入、SQL注入、缓冲区溢出、数据破坏等问题。
可采取以下措施对用户输入检查:
* 用户输入作为数值的,做数值范围检查
* 用户输入是字符串的,检查字符串长度
* 用户输入作为格式化字符串的,检查关键字“%”
* 用户输入作为业务数据,对关键字进行检查、转义
C语言中’\0’作为字符串的结束符,即NULL结束符。标准字符串处理函数(如strcpy()、strlen())依赖NULL结束符来确定字符串的长度。没有正确使用NULL结束字符串会导致缓冲区溢出和其它未定义的行为。
为了避免缓冲区溢出,常常会用相对安全的限制字符数量的字符串操作函数代替一些危险函数。如:
错误示例:
char a[16];
strncpy(a, "0123456789abcdef", sizeof(a));
正确示例:
char a[16];
strncpy(a, "0123456789abcdef", sizeof(a) - 1 );
a[sizeof(a) - 1] = '\0';
边界不明确的字符串(如来自gets()、getenv()、scanf()的字符串),长度可能大于目标数组长度,直接拷贝到固定长度的数组中容易导致缓冲区溢出。
错误示例:
char buff[256];
char *editor = getenv("EDITOR");
if (editor != NULL)
{
strcpy(buff, editor);
}
正确示例,使用malloc分配指定长度的内存:
char *buff;
char *editor = getenv("EDITOR");
if (editor != NULL)
{
buff = malloc(strlen(editor) + 1);
if (buff != NULL)
{
strcpy(buff, editor);
}
}
当一个整数被增加超过其最大值时会发生整数上溢,被减小小于其最小值时会发生整数下溢。带符号和无符号的数都有可能发生溢出。
带符号整型转换到无符号整型,最高位(high-order bit)会丧失其作为符号位的功能。如果该带符号整数的值非负,那么转换后值不变;如果该带符号整数的值为负,那么转换后的结果通常是一个非常大的正数。
错误示例,符号错误绕过长度检查:
#define BUF_SIZE 10
int main(int argc,char* argv[])
{
int length;
char buf[BUF_SIZE];
if (argc != 3)
{
return -1;
}
length = atoi(argv[1]); //如果atoi返回的长度为负数
if (length < BUF_SIZE) // len为负数,长度检查无效
{
memcpy(buf, argv[2], length); /* 带符号的len被转换为size_t类型的无符号整数,负值被解释为一个极大的正整数。memcpy()调用时引发buf缓冲区溢出 */
printf("Data copied\n");
}
else
{
printf("Too many data\n");
}
}
正确示例,将len声明为无符号整型:
#define BUF_SIZE 10
int main(int argc, char* argv[])
{
unsigned int length;
char buf[BUF_SIZE];
if (argc != 3)
{
return -1;
}
length = atoi(argv[1]);
if (length < BUF_SIZE)
{
memcpy(buf, argv[2], length);
printf("Data copied\n");
}
else
{
printf("Too much data\n");
}
return 0;
}
将一个较大整型转换为较小整型,并且该数的原值超出较小类型的表示范围,就会发生截断错误,原值的低位被保留而高位被丢弃。截断错误会引起数据丢失。
错误示例,符号错误绕过长度检查:
int main(int argc, char* argv[])
{
unsigned short total = strlen(argv[1]) + strlen(argv[2]) + 1;
char* buffer = (char*)malloc(total);
strcpy(buffer, argv[1]);
strcat(buffer, argv[2]);
free(buffer);
return 0;
}
示例代码中total被定义为unsigned short,相对于strlen()的返回值类型size_t(通常为unsigned long)太小。如果攻击者提供的两个入参长度分别为65500和36,unsigned long的65500+36+1会被取模截断,total的最终值是(65500+36+1)%65536 = 1。malloc()只为buff分配了1字节空间,为strcpy()和strcat()的调用创造了缓冲区溢出的条件。
正确示例,将涉及到计算的变量声明为统一的类型,并检查计算结果:
int main(int argc, char* argv[])
{
size_t total = strlen(argv[1]) + strlen(argv[2]) + 1;
if ((total <= strlen(argv[1])) || (total <= strlen(argv[2])))
{
/* handle error */
return -1;
}
char* buffer = (char*)malloc(total);
strcpy(buffer, argv[1]);
strcat(buffer, argv[2]);
free(buffer);
return 0;
}
使用格式化字符串应该小心,确保格式字符和参数之间的匹配,保留数量和数据类型。格式字符和参数之间的不匹配会导致未定义的行为。大多数情况下,不正确的格式化字符串会导致程序异常终止。大部分格式化字符串出问题,都是由于 copy-paste省事导致的,需要格外注意!
调用格式化I/O函数时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。攻击者对一个格式化字符串拥有部分或完全控制,存在以下风险:进程崩溃、查看栈的内容、改写内存、甚至执行任意代码。
错误示例:
char input[1000];
if (fgets(input, sizeof(input) - 1, stdin) == NULL)
{
/* handle error */
}
input[sizeof(input)-1] = '\0';
printf(input);
上述代码input直接来自用户输入,并作为格式化字符串直接传递给printf()。当用户输入的是“%s%s%s%s%s%s%s%s%s%s%s%s”,就可能触发无效指针或未映射的地址读取。格式字符%s显示栈上相应参数所指定的地址的内存。这里input被当成格式化字符串,而没有提供参数,因此printf()读取栈中任意内存位置,指导格式字符耗尽或者遇到一个无效指针或未映射地址为止。
正确示例,给printf()传两个参数,第一个参数为”%s”,目的是将格式化字符串确定下来;第二个参数为用户输入input:
char input[1000];
if (fgets(input, sizeof(input)-1, stdin) == NULL)
{
/* handle error */
}
input[sizeof(input)-1] = '\0';
printf(“%s”, input);
strlen()函数用于计算字符串的长度,它返回字符串中第一个NULL结束符之前的字符的数量。因此用strlen()处理文件I/O函数读取的内容时要小心,因为这些内容可能是二进制也可能是文本。
错误示例:
char buf[BUF_SIZE + 1];
if (fgets(buf, sizeof(buf), fp) == NULL)
{
/* handle error */
}
buf[strlen(buf) - 1] = '\0';
上述代码试图从一个输入行中删除行尾的换行符(\n)。如果buf的第一个字符是NULL,strlen(buf)返回0,这时对buf进行数组下标为[-1]的访问操作将会越界。
正确示例,在不能确定从文件读取到的数据的类型时,不要使用依赖NULL结束符的字符串操作函数:
char buf[BUF_SIZE + 1];
char *p;
if (fgets(buf, sizeof(buf), fp))
{
p = strchr(buf, '\n');
if (p)
{
*p = '\0';
}
}
else
{
/* handle error condition */
}
字符I/O函数fgetc()、getc()和getchar()都从一个流读取一个字符,并把它以int值的形式返回。如果这个流到达了文件尾或者发生读取错误,函数返回EOF。fputc()、putc()、putchar()和ungetc()也返回一个字符或EOF。
如果这些I/O函数的返回值需要与EOF进行比较,不要将返回值转换为char类型。因为char是有符号8位的值,int是32位的值。如果getchar()返回的字符的ASCII值为0xFF,转换为char类型后将被解释为EOF。因为这个值被有符号扩展为0xFFFFFFFF(EOF的值)执行比较。
错误示例:
char buf[BUF_SIZE];
char ch;
int i = 0;
while ( (ch = getchar()) != '\n' && ch != EOF )
{
if ( i < BUF_SIZE - 1 )
{
buf[i++] = ch;
}
}
buf[i] = '\0'; /* terminate NTBS */
正确做法:使用int类型的变量接受getchar()的返回值。
char buf[BUF_SIZE];
int ch;
int i = 0;
while (((ch = getchar()) != '\n') && ch != EOF)
{
if (i < BUF_SIZE - 1)
{
buf[i++] = ch;
}
}
buf[i] = '\0'; /* terminate NTBS */
如果system()的参数由用户的输入组成,恶意用户可以通过构造恶意输入,改变system()调用的行为。
示例:
system(sprintf("any_exe %s", input));
如果恶意用户输入参数:
happy; useradd attacker
最终shell会将字符串解释为两条独立的命令:“any_exe happy; useradd attacker”。
使用标准的数据类型,有利于程序的移植。