01
剑宗气宗之争 《笑傲江湖》中华山派的剑宗和气宗之争,可谓异常激烈。那么问题就来了,既然有剑宗气宗之争,到底应该先练剑,还是先练气呢?引申到软件开发行业有没剑气之争呢? 前面发布很多理论方面的文章,高质量的软件开发,也是存在见效快的套路,针对有一定嵌入式C语言开发基础的,以剑宗之法进行描述,可重点关注if判断和内存管理相关的讲解,抛砖引玉。02
文件结构 1、C 程序通常分为两类文件,一种是程序的声明称为头文件,以“.h”为后缀,另一种是程序的实现,以“.c”为后缀,一般每个c文件有个同名的h文件。 2、软件的头文件数目比较多,应将头文件和定义文件分别保存于不同的目录,例如将头文件保存于 include或者inc 目录,将定义文件保存于 source 或src目录;如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录,即私有的h文件放在src目录。 3、在文件头添加版权和版本的声明等信息,主要包括版权和功能,以及修改记录,必要时可以为整个功能文件夹单独新建readme说明文档。 4、为了防止头文件被重复引用,必须用 ifndef/define/endif 结构产生预处理块。 5、头文件中只存放“声明”而不存放“定义”,更别提放变量,这是严重的错误。 6、用 #include03
程序版式 版式虽然不会影响程序的功能,但会影响可读性,程序的风格统一则是赏心悦目。 代码排版在编码时确实很难把握,但可以编码完成后统一用工具格式化,不管编码使用Keil/MDK、Qt等集成工具,或者纯粹的代码编辑工具Source Insight,一般都支持自定义运行可执行文件,如Astyle。可以客制化新菜单,一键执行Astyle,将代码一键格式化,排版统一、层次分明。 Astyle官网 http://astyle.sourceforge.net/ 按要求下载安装,只需要AStyle.exe即可。关于其使用和参数,可以再进入Documentation。对代码基本风格,{}如何对齐、是否换行,switch-case如何排版,tab键占位宽度,运算符或变量前后的空格等等,基本上代码排版涉及的方方面面都有参数说明。个人选择的编码参数是--style=allman -S -U -t -n -K -p -s4 -j -q -Y -xW -xV fileName 效果如下
//微信公众号:嵌入式系统 int Foo(bool isBar) { if (isBar) { bar(); return 1; } else { return 0; } } 关于注释,重要函数或段落必不可少,修改代码同时修改相应的注释,以保证注释与代码的一致性。
04
命名规则 比较著名的命名规则当推 Microsoft 公司的“匈牙利”法,该命名规则的主要思想是“在变量和函数名中加入前缀以增进人们对程序的理解”。例如所有的字符变量均以ch 为前缀,若是指针变量则追加前缀 p。但没有一种命名规则可以让所有的程序员满意,制定一种令大多数项目成员满意的命名规则,重点是在整个团队和项目中贯彻实施。 事实上开发大多数基于SDK,一般底层命名规则尽量与SDK风格保持一致,至于上层就按团队标准,个人比较倾向全部小写字母,用下划线分割的风格,例如 set_apn、timer_start。 不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解,全局变量也不要过于简短。 变量的名字应当使用“名词”或者“形容词+名词”,函数的名字应当使用“动词”或者“动词+名词”,用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。05
基本语句 表达式和语句都属于C 语法基础,看似简单,但使用时隐患比较多,提供一些建议。//微信公众号:嵌入式系统 if (flag) // 表示 flag 为真 if (!flag) // 表示 flag 为假 其它的用法都属于不良风格,例如:
//错误范例 if (flag == TRUE) if (flag == 1 ) if (flag == FALSE) if (flag == 0) 2、整型变量与零值比较 整型变量用“==”或“!=”直接与 0 比较,假设整型变量的名字为 value,它与零值比较的标准 if 语句如下:
if (value == 0) if (value != 0) 不可模仿布尔变量的风格而写成
//错误范例 if (value) // 会让人误解 value 是布尔变量 if (!value) 3、 浮点变量与零值比较 不可将浮点变量用“==”或“!=”与任何数字比较,无论是 float 还是 double 类型的变量,都有精度限制。不能将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。假设浮点变量的名字为 x,应当将
if (x == 0.0) // 隐含错误的比较,错误 转化为
const float EPSINON = 0.00001 if ((x>=-EPSINON) && (x<=EPSINON)) //其中 EPSINON 是允许的误差(即精度),即x无限趋近于0.04、指针变量与零值比较 指针变量用“==”或“!=”与 NULL 比较, 指针变量的零值是“空”(记为 NULL),尽管 NULL 的值与 0 相同,但是两者意义不同。假设指针变量的名字为 p,它与零值比较的标准 if 语句如下:
if (p == NULL) // p 与 NULL 显式比较,强调 p 是指针变量 if (p != NULL) 不要写成
if (p == 0) // 容易让人误解 p 是整型变量 if (p != 0) if (p) // 容易让人误解 p 是布尔变量 if (!p)
//不良范例 for (row=0; row<100; row++) { for ( col=0; col<5; col++ ) { sum = sum + a[row][col]; } } //微信公众号:嵌入式系统 较高效率 for (col=0; col<5; col++ ) { for (row=0; row<100; row++) { sum = sum + a[row][col]; } }
//微信公众号:嵌入式系统 //代码只是表意,可能无法编译 #include
06
常量 常量是一种标识符,它的值在运行期间恒定不变。C 语言用 #define 来定义常量(称为宏常量),但用 const 来定义常量(称为 const 常量)其实更佳。#define MAX 100 const float PI = 3.14159; const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误,所以复杂参数宏必须为每个参数加上()限制。 但也有特例
const int SIZE = 100; int array[SIZE]; // 有的编译器认为是错误,这就必须用define了 需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
07
函数 函数设计的细微缺点很容易导致该函数被错用,函数接口的两个要素是参数和返回值,C 语言中函数的参数和返回值的传递方式有值传递(pass by value)和指针传递(pass by pointer)两种。void set_size(int width, int height); // 良好的风格 void set_size(int, int); // 不良的风格 int get_size(void); // 良好的风格 int get_size(); // 不良的风格 参数命名要恰当,顺序要合理。例如字符串拷贝函数
char *strcpy(char* dest, const char *src); 从名字上就可以看出应该把 src 拷贝到 dest。还有一个问题,两个参数哪个该在前哪个该在后?参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。 这里也说明下const的意义,如果参数仅作输入用,则应在类型前加 const,以防止在函数体内被意外修改。 避免函数有太多的参数,参数个数尽量控制在 5 个以内,如果参数太多,在使用时容易将参数类型或顺序搞错,可以定为结构体指针,但尽量带上参数注释。 除了printf、sprintf标准库或基于这类的日志输出接口,尽量不要使用类型和数目不确定的参数。
char * Func(void) { char str[] = “hello world”; // str 的内存位于栈上 … return str; // 将导致错误 } 尽量避免函数带有“记忆”功能,相同的输入应当产生相同的输出。带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在 C语言中,函数的 static 局部变量是函数的“记忆”存储器。建议尽量少用 static 局部变量,除非必需。
void *memcpy(void *pvTo, const void *pvFrom, size_t size) { assert((pvTo != NULL) && (pvFrom != NULL)); // 【使用断言】 byte *pbTo = (byte *) pvTo; // 防止改变 pvTo 的地址 byte *pbFrom = (byte *) pvFrom; // 防止改变 pvFrom 的地址 while(size -- > 0 ) *pbTo ++ = *pbFrom ++ ; return pvTo; } assert 不应该产生任何副作用。所以 assert 不是函数,而是宏。可以把assert 看成一个在任何系统状态下都可以安全使用的无害测试手段。如果程序在 assert处终止了,并不是说含有该 assert 的函数有错误,而是调用者出了差错,assert 有助于找到发生错误的原因。 软件有必要进行防错设计,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。
char a[] = “hello”; a[0] = ‘X’; cout << a << endl; char *p = “world”; // 注意 p 指向常量字符串 p[0] = ‘X’; // 编译器不能发现该错误 cout << p << endl;2、 内容复制与比较 不能对数组名进行直接复制与比较,若想把数组 a 的内容复制给数组 b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数 strcpy 进行复制。同理,比较 b 和 a 的内容是否相同,不能用 if(b == a) 来判断,应该用标准库函数 strcmp进行比较。 语句 p = a 并不能把 a 的内容复制指针 p,而是把 a 的地址赋给了 p。要想复制 a的内容,可以先用库函数 malloc 为 p 申请一块容量为 strlen(a)+1 个字符的内存,再用 strcpy 进行字符串复制。同理,语句 if(p==a) 比较的不是内容而是地址,应该用库函数 strcmp 来比较。
// 数组 char a[] = "hello"; char b[10]; strcpy(b, a); // 不能用 b = a; if(strcmp(b, a) == 0 ) // 不能用 if ( b == a) // 指针 int len = strlen(a); char *p = (char *)malloc(sizeof(char)*(len+1)); strcpy(p,a); // 不要用 p = a; if(strcmp(p, a) == 0) // 不要用 if (p == a) 3、计算内存容量 用运算符 sizeof 可以计算出数组的容量(字节数)。sizeof(a)的值是 12(注意别忘了’’)。指针 p 指向 a,但是 sizeof(p)的值却是 4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于 sizeof(char*),而不是 p 所指的内存容量。/C 语言没有办法知道指针所指的内存容量,只能在申请内存时记住它。
char a[] = "hello world"; char *p = a; cout<< sizeof(a) << endl; // 12 字节 cout<< sizeof(p) << endl; // 4 字节 当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。不论数组 a 的容量是多少,sizeof(a)始终等于 sizeof(char *)。
void Func(char a[100]) { cout<< sizeof(a) << endl; // 4 字节而不是 100 字节 } 4、指针参数是如何传递内存 如果函数的参数是一个指针,不要指望用该指针去申请动态内存。
void get_memory(char *p, int num) { p = (char *)malloc(sizeof(char) * num); } void test(void) { char *str = NULL; get_memory(str, 100); // str 仍然为 NULL strcpy(str, "hello"); // 运行错误 } test 函数的get_memory(str, 100) 并没有使 str 获得期望的内存,str 依旧是 NULL,为什么? 问题出在函数 get_memory,编译器总是要为函数的每个参数制作临时副本,指针参数 p 的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p 的内容,就导致参数 p 的内容作相应的修改。这就是指针可以用作输出参数的原因。而范例中_p 申请了新的内存,只是把_p 所指的内存地址改变了,但是 p 丝毫未变。所以函数 get_memory并不能输出任何东西。事实上,每执行一次 get_memory就会泄露一块内存,因为没有用free 释放内存。 如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,正确范例如下:
void get_memory2(char **p, int num) { *p = (char *)malloc(sizeof(char) * num); } void test2(void) { char *str = NULL; get_memory2(&str, 100); // 注意参数是 &str,而不是 str strcpy(str, "hello"); free(str); } 由于“指向指针的指针”这个概念不容易理解,可以用函数返回值来传递动态内存,这种方法更加简单。
char *get_memory3(int num) { char *p = (char *)malloc(sizeof(char) * num); return p; } void test3(void) { char *str = NULL; str = get_memory3(100); //建议增加str指针是否为NULL判断,并清零内容 strcpy(str, "hello"); free(str); } 用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把 return 语句用错,不要用 return 语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,错误范例如下:
//错误范例 char *get_string(void) { char p[] = "hello world"; return p; // 编译器将提出警告 } void test4(void) { char *str = NULL; str = get_string(); // str 的内容是随机垃圾 } 执行str = get_string()后 str 不再是 NULL 指针,但是 str 的内容不是“hello world”而是垃圾。
char *get_string2(void) { char *p = "hello world"; return p; } void test5(void) { char *str = NULL; str = get_string2(); } 函数 test5 运行虽然不会出错,但是函数 get_string2的设计概念却是错误的。因为 get_string2内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用 get_string2,它返回的始终是同一个“只读”的内存块,也就是test5是无法修改str的。5、 free 把指针怎么了 free 只是把指针所指的内存给释放掉,但并没有把指针本身干掉;指针 p 被 free 以后其地址仍然不变(非 NULL),只是该地址对应的内存是垃圾,p 成了“野指针”。如果此时不把 p 设置为 NULL,会让人误以为 p 是个合法的指针。 如果程序比较长,我们有时记不住 p 所指的内存是否已经被释放,在继续使用 p 之前,通常会用语句 if (p != NULL)进行防错处理。很遗憾,此时 if 语句起不到防错作用,此时 p 不是 NULL 指针,但它也不指向合法的内存块。
char *p = (char *) malloc(100); strcpy(p, “hello”); free(p); // p 所指的内存被释放,但是 p 所指的地址仍然不变 if(p != NULL) // 没有起到防错作用 { strcpy(p, “world”); // 出错 } 6、动态内存会被自动释放吗 函数体内的局部变量在函数结束时自动消亡。
void func(void) { char *p = (char *) malloc(100); // 动态内存会自动释放吗? } 但是,变量p 是局部的指针变量,它消亡的时候并不会让它所指的动态内存一起完蛋。发现指针有一些“似是而非”的特征: (1)指针消亡了,并不表示它所指的内存会被自动释放。 (2)内存被释放了,并不表示指针会消亡或者成了 NULL 指针。7、杜绝“野指针” “野指针”不是 NULL 指针,是指向“垃圾”内存的指针。人们一般不会错用 NULL指针,因为用 if 语句很容易判断;但是“野指针”是很危险的,if 语句对它不起作用。“野指针”的成因主要有三种: (1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为 NULL 指针,它的缺省值是随机的,所以,指针变量在创建的同时应当被初始化。 (2)指针 p 被 free 或者 delete 之后,没有置为 NULL,让人误以为 p 是个合法的指针。 (3)指针操作超越了变量的作用范围。这种情况让人防不胜防。8、内存耗尽怎么办 如果在申请动态内存时找不到足够大的内存块,malloc 将返回 NULL 指针,宣告内存申请失败。判断指针是否为 NULL,如果是则马上用 return 语句终止本函数,或者用 exit(1)终止整个程序的运行。如果发生“内存耗尽”,一般说来应用程序已经无药可救,嵌入式设备只能重启了。9、心得体会 很少有人能拍拍胸脯说通晓指针与内存管理,越是怕指针,就越要使用指针。不会正确使用指针,肯定算不上是合格的嵌入式程序员。 审核编辑 :李倩
全部0条评论
快来发表一下你的评论吧 !