0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

嵌入式C语言开发基础

玩转单片机与嵌入式 来源:玩转单片机与嵌入式 2023-02-16 11:10 次阅读

1 剑宗气宗之争

《笑傲江湖》中华山派的剑宗和气宗之争,可谓异常激烈。那么问题就来了,既然有剑宗气宗之争,到底应该先练剑,还是先练气呢?引申到软件开发行业有没剑气之争呢?

前面发布很多理论方面的文章,诸如4篇:基于RTOS的软件开发理论嵌入式软件的设计模式(上)嵌入式软件的设计模式(下)嵌入式软件分层隔离的典范这些都是具备一定基础后在架构上的描述,类似于气宗性质,这种比较抽象、见效慢。但高质量的软件开发,也是存在见效快的套路,针对有一定嵌入式C语言开发基础的,以剑宗之法进行描述,可重点关注if判断和内存管理相关的讲解,抛砖引玉。

2 文件结构

1、C 程序通常分为两类文件,一种是程序的声明称为头文件,以“.h”为后缀,另一种是程序的实现,以“.c”为后缀,一般每个c文件有个同名的h文件。

2、软件的头文件数目比较多,应将头文件和定义文件分别保存于不同的目录,例如将头文件保存于 include或者inc 目录,将定义文件保存于 source 或src目录;如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录,即私有的h文件放在src目录。

3、在文件头添加版权和版本的声明等信息,主要包括版权和功能,以及修改记录,必要时可以为整个功能文件夹单独新建readme说明文档。

4、为了防止头文件被重复引用,必须用 ifndef/define/endif 结构产生预处理块。

5、头文件中只存放“声明”而不存放“定义”,更别提放变量,这是严重的错误。

6、用 #include 格式来引用标准库的头文件,用 #include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。

7、文件可按层或者功能组件划分不同的文件夹,便于其他人阅读。

3 程序版式

版式虽然不会影响程序的功能,但会影响可读性,程序的风格统一则是赏心悦目。

代码排版在编码时确实很难把握,但可以编码完成后统一用工具格式化,不管编码使用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-xVfileName

效果如下

//微信公众号:嵌入式系统
intFoo(boolisBar)
{
if(isBar)
{
bar();
return1;
}
else
{
return0;
}
}

也可以参考 代码的保养第3章。关于注释,重要函数或段落必不可少,修改代码同时修改相应的注释,以保证注释与代码的一致性。

4 命名规则

比较著名的命名规则当推 Microsoft 公司的“匈牙利”法,该命名规则的主要思想是“在变量和函数名中加入前缀以增进人们对程序的理解”。例如所有的字符变量均以ch 为前缀,若是指针变量则追加前缀 p。但没有一种命名规则可以让所有的程序员满意,制定一种令大多数项目成员满意的命名规则,重点是在整个团队和项目中贯彻实施。

事实上开发大多数基于SDK,一般底层命名规则尽量与SDK风格保持一致,至于上层就按团队标准,个人比较倾向全部小写字母,用下划线分割的风格,例如 set_apn、timer_start。

不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解,全局变量也不要过于简短。

变量的名字应当使用“名词”或者“形容词+名词”,函数的名字应当使用“动词”或者“动词+名词”,用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。

5 基本语句

表达式和语句都属于C 语法基础,看似简单,但使用时隐患比较多,提供一些建议。

5.1 if

if 语句是 C 语言中最简单、最常用的语句,然而很多程序员却用隐含错误的方式,仅以不同类型的变量与零值比较为例,展开讨论。

1、布尔变量与零值比较

不可将布尔变量直接与 TRUE、FALSE 或者 1、0 进行比较。根据布尔类型的语义,零值为“假”(记为 FALSE),任何非零值都是“真”(记为TRUE)。TRUE 的值究竟是什么并没有统一的标准。

假设布尔变量名字为 flag,它与零值比较的标准 if 语句如下:

//微信公众号:嵌入式系统
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)//隐含错误的比较,错误

转化为

constfloatEPSINON=0.00001
if((x>=-EPSINON)&&(x<=EPSINON)) 
//其中EPSINON是允许的误差(即精度),即x无限趋近于0.0

4、指针变量与零值比较

指针变量用“==”或“!=”与 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)

5.2 for

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

//不良范例
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];
}
}

5.3 switch

switch 是多分支选择语句,而 if 语句只有两个分支可供选择;虽然可以用嵌套的if 语句来实现多分支选择,但那样的程序冗长难读。这是 switch 语句存在的理由。

switch-case 即使不需要 default 处理,也应该保留语句 default : break; 这样做并非多此一举,而是为了防止别人误以为你忘了 default 处理。确实不需要break的case,务必加上注释标明。

5.4 goto

很多人建议禁止使用 goto 语句,但实事求是地说,错误是程序员自己造成的,不是 goto 的过错。goto 语句至少有一处可显神通,它能从多重循环体中一下子跳到外面,特殊场景下可以使用,在很多if嵌套的场景,比如都有同样的错误处理,或者成对操作的文件开关,或者内存申请释放,就比较适合goto统一处理。

//微信公众号:嵌入式系统
//代码只是表意,可能无法编译
#include

voidtest(void)
{
char*p1,*p2;
p1=(char*)malloc(100);
p1=(char*)malloc(200);

if(0)
{
//dosomething
gotoexit;
}
elseif(0)
{
//dosomething
gotoexit;
}
//dosomething
//...
exit:
free(p1);
free(p2);
}

intmain()
{
goto_test();
return0;
}

对于内存申请释放、文件打开关闭这种成对操作,或者各种异常处理的统一支持场景,就比较适合goto。类似的还有do-while(0)这种语句。

关于运算优先级,熟记运算符优先级是比较困难的,如果代码行中的运算符比较多,为了防止产生歧义并提高可读性,全部加括号明确表达式的操作顺序,虽然愚笨但是可靠

6 常量

常量是一种标识符,它的值在运行期间恒定不变。C 语言用 #define 来定义常量(称为宏常量),但用 const 来定义常量(称为 const 常量)其实更佳。

#defineMAX100
constfloatPI=3.14159;

const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误,所以复杂参数宏必须为每个参数加上()限制。

但也有特例

constintSIZE=100;
intarray[SIZE];//有的编译器认为是错误,这就必须用define了

需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。

7函数

函数设计的细微缺点很容易导致该函数被错用,函数接口的两个要素是参数和返回值,C 语言中函数的参数和返回值的传递方式有值传递(pass by value)和指针传递(pass by pointer)两种。

7.1参数的规则

参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字,如果函数没有参数,则用 void 填充。

voidset_size(intwidth,intheight);//良好的风格
voidset_size(int,int);//不良的风格
intget_size(void);//良好的风格
intget_size();//不良的风格

参数命名要恰当,顺序要合理。例如字符串拷贝函数

char*strcpy(char*dest,constchar*src);

从名字上就可以看出应该把 src 拷贝到 dest。还有一个问题,两个参数哪个该在前哪个该在后?参数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面。

这里也说明下const的意义,如果参数仅作输入用,则应在类型前加 const,以防止在函数体内被意外修改。

避免函数有太多的参数,参数个数尽量控制在 5 个以内,如果参数太多,在使用时容易将参数类型或顺序搞错,可以定为结构体指针,但尽量带上参数注释。

除了printf、sprintf标准库或基于这类的日志输出接口,尽量不要使用类型和数目不确定的参数。

7.2 返回值的规则

不要省略返回值的类型,默认不加类型说明的函数一律自动按整型处理。为了避免混乱,如果函数没有返回值,应声明为 void 类型。

不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用 return 语句返回。

7.3 函数内部实现的规则

不同功能的函数其内部实现各不相同,看起来似乎无法就“内部实现”达成一致的观点。但根据经验,我们可以在函数体的“入口处”和“出口处”从严把关,从而提高函数的质量。

在函数体的“入口处”,对参数的有效性进行检查,很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”(assert)来防止此类错误。

在函数体的“出口处”,对 return 语句的正确性和效率进行检查。如果函数有返回值,那么函数的“出口处”是 return 语句。调用处应该尽量关注返回值,对异常进行处理

关于return的值,不可返回指向“栈内存”的“指针,该内存在函数体结束时被自动销毁。例如

char*Func(void)
{
charstr[]=“helloworld”;//str的内存位于栈上returnstr;//将导致错误
}

尽量避免函数带有“记忆”功能,相同的输入应当产生相同的输出。带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在 C语言中,函数的 static 局部变量是函数的“记忆”存储器。建议尽量少用 static 局部变量,除非必需。

7.4 断言

程序一般分为 Debug 版本和 Release 版本,Debug 版本用于内部调试,Release 版本发行给用户使用。断言 assert 是仅在 Debug 版本起作用的宏,它用于检查“不应该”发生的情况。在运行过程中,如果 assert 的参数为假,那么程序就会中止。

void*memcpy(void*pvTo,constvoid*pvFrom,size_tsize)
{
assert((pvTo!=NULL)&&(pvFrom!=NULL));//【使用断言】
byte*pbTo=(byte*)pvTo;//防止改变pvTo的地址
byte*pbFrom=(byte*)pvFrom;//防止改变pvFrom的地址
while(size-->0)
*pbTo++=*pbFrom++;
returnpvTo;
}

assert 不应该产生任何副作用。所以 assert 不是函数,而是宏。可以把assert 看成一个在任何系统状态下都可以安全使用的无害测试手段。如果程序在 assert处终止了,并不是说含有该 assert 的函数有错误,而是调用者出了差错,assert 有助于找到发生错误的原因。

软件有必要进行防错设计,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。

8 内存管理

C语言的内存管理既是它的优势,也是劣势。理解它的原理了才能更好的管理内存。

8.1 内存分配方式

内存分配方式有三种:

1、从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。

2、在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

3、从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但风险也大。

8.2 内存错误及其对策

发生内存错误是件非常麻烦的事情。编译器不能自动发现这些错误,通常是在程序运行时才能捕捉到,而这些错误大多没有明显的症状,时隐时现,增加了改错的难度。常见的内存错误及其对策如下:

1、内存分配未成功,却使用了它

编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为 NULL。如果指针 p 是函数的参数,可在函数的入口处用 assert(p!=NULL)进行检查,或者用 if(p==NULL)或 if(p!=NULL)进行防错处理。

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

犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误。内存的缺省初值究竟是什么并没有统一的标准(尽管有些时候为零值),为了安全,对分配的内存都进行清零。

3、内存分配成功并且已经初始化,但操作越过了内存的边界

数组使用时经常会发生下标“多 1”或“少 1”的操作。特别是在 for 循环语句中,循环次数很容易搞错,导致数组操作越界。

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

含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,运行正常,但随着运行时间加长,程序突然死掉,内存耗尽。动态内存的申请与释放必须配对,程序中 malloc 与 free 的成对使用。

5、已经释放的内存却继续使用它

程序中的调用关系过于复杂,逻辑顺序错误,或者使用了指向“栈内存”的“临时指针,使用 free 或 delete 释放了内存后,务必将指针设置为 NULL,使用前判断是否为NULL。

关于指针的使用建议,用 malloc 申请内存之后,应该立即检查指针值是否为 NULL,非NULL的赋初值;使用结束后用 free 释放,且将指针设置为 NULL,防止误用“野指针”。对动态内存的一些防护性操作,可以参考微信公众号【嵌入式系统】的文章动态内存管理及防御性编程

8.3 指针与数组的对比

C 程序中指针和数组在不少地方可以相互替换着用,让人产生一种错觉,以为两者是等价的。

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

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

下面以字符串为例比较指针与数组的特性。

1、修改内容

字符数组 a 的容量是 6 个字符,其内容为 hello。a 的内容可以改变,如 a[0]= ‘X’。指针 p 指向常量字符串“world”(位于静态存储区,内容为 world),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句 p[0]= ‘X’有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。

chara[]=“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 来比较。

//数组
chara[]="hello";
charb[10];
strcpy(b,a);//不能用b=a;
if(strcmp(b,a)==0)//不能用if(b==a)

//指针
intlen=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 语言没有办法知道指针所指的内存容量,只能在申请内存时记住它。

chara[]="helloworld";
char*p=a;
cout<< sizeof(a)<< endl;//12字节
cout<< sizeof(p)<< endl;//4字节

当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。不论数组 a 的容量是多少,sizeof(a)始终等于 sizeof(char *)。

voidFunc(chara[100])
{
cout<< sizeof(a)<< endl;//4字节而不是100字节
}

4、指针参数是如何传递内存

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

voidget_memory(char*p,intnum)
{
p=(char*)malloc(sizeof(char)*num);
}
voidtest(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 释放内存。

如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”,正确范例如下:

voidget_memory2(char**p,intnum)
{
*p=(char*)malloc(sizeof(char)*num);
}
voidtest2(void)
{
char*str=NULL;
get_memory2(&str,100);//注意参数是&str,而不是str
strcpy(str,"hello");
free(str);
}

由于“指向指针的指针”这个概念不容易理解,可以用函数返回值来传递动态内存,这种方法更加简单。

char*get_memory3(intnum)
{
char*p=(char*)malloc(sizeof(char)*num);
returnp;
}
voidtest3(void)
{
char*str=NULL;
str=get_memory3(100);
//建议增加str指针是否为NULL判断,并清零内容
strcpy(str,"hello");
free(str);
}

用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把 return 语句用错,不要用 return 语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡,错误范例如下:

//错误范例
char*get_string(void)
{
charp[]="helloworld";
returnp;//编译器将提出警告
}
voidtest4(void)
{
char*str=NULL;
str=get_string();//str的内容是随机垃圾
}

执行str = get_string()后 str 不再是 NULL 指针,但是 str 的内容不是“hello world”而是垃圾。

char*get_string2(void)
{
char*p="helloworld";
returnp;
}
voidtest5(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、动态内存会被自动释放吗

函数体内的局部变量在函数结束时自动消亡。

voidfunc(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、心得体会

很少有人能拍拍胸脯说通晓指针与内存管理,越是怕指针,就越要使用指针。不会正确使用指针,肯定算不上是合格的嵌入式程序员。

9 其它编程经验

9.1 使用 const 提高函数的健壮性

const 是 constant 的缩写,“恒定不变”的意思。被 const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。很多 C++程序设计书籍建议:“Use const whenever you need”。

1、用 const 修饰函数的参数如果参数作输出用,不论它是什么数据类型,都不能加 const 修饰,否则该参数将失去输出功能。const 只能修饰输入参数,如果输入参数采用“指针传递”,那么加 const 修饰可以防止意外地改动该指针,起到保护作用。例如 strcpy函数:

char*strcpy(char*dest,constchar*src);

其中 src是输入参数,dest是输出参数。给 src加上 const修饰后,如果函数体内的语句试图改动 src 的内容,编译器将指出错误。

2、如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加 const 修饰。

voidfunc1(intx)写成voidfunc1(constintx)//const无意义

3、对于非内部数据类型的参数而言,如 void func(A a) 这样声明的函数注定效率比较低,其中 A 为用户自定义的数据类型,可以理解为大结构。

函数体内将产生 A 类型的临时对象用于复制参数 a,而临时对象的构造、复制、析构过程都将消耗时间。为了提高效率,可以将函数声明改为:

voidfunc(A&a)

因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数 存在一个缺点,“引用传递”有可能改变参数 a,这是我们不期望的。解决这个问题很容易,加 const修饰即可,因此函数最终成为

voidfunc(constA&a)

4、用 const 修饰函数的返回值,如果给以“指针传递”方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加 const 修饰的同类型指针。例如函数

constchar*get_string(void);
char*str=get_string();//出现编译错误:
constchar*str=get_string();//正确的用法

9.2 提高程序的效率

程序的时间效率是指运行速度,空间效率是指程序占用内存或者外存的状况。

不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。

在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关紧要之处优化。有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。例如多花费一些内存来提高性能。

关于其它C关键字用法,可以参考C语言关键字应用技巧

10 小结

不论剑宗、气宗优劣,先把功能跑通再反推代码原理和实现流程,还是先理清时序和原理再编码实现功能,短期内剑宗效率高,加工资快,但后期发展有限;气宗则面临前期可能被淘汰,尤其在势利的小公司,不注重新人培养,但前期积累,后期融会贯通,在技术方面成为权威。如果合二为一,项目紧急则拿来就用,空闲时专研总结,取长补短,则是高级程序员的素质。

审核编辑 :李倩


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • C语言
    +关注

    关注

    180

    文章

    7598

    浏览量

    136188
  • RTOS
    +关注

    关注

    21

    文章

    809

    浏览量

    119433
  • 代码
    +关注

    关注

    30

    文章

    4748

    浏览量

    68349

原文标题:10 小结

文章出处:【微信号:玩转单片机与嵌入式,微信公众号:玩转单片机与嵌入式】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    嵌入式C语言开发教程之用c写CGI_程序简要指南

    嵌入式C语言开发教程之用c写CGI_程序简要指南
    发表于 08-20 09:55

    陈正冲—C语言深度解剖(第2版)(电子版)

    ,绝对受益匪浅。 嵌入式C语言开发经验和平时讲解C语言的心得体会整理而成,其中有很多作者独特的见
    发表于 12-26 13:20

    嵌入式C语言开发嵌入式Linux C开发的区别

    嵌入式Linux系统开发嵌入式Linux系统开发(应用软件开发):通过内核提供的服务实现相应功能一、嵌入
    发表于 11-05 08:12

    数据变量的相关资料推荐

    嵌入式C语言开发入门——数据变量变量的四个部分:空间、变量名、变量地址、变量类型C语言标识符命名
    发表于 12-15 07:19

    测试驱动的嵌入式C语言开发读书笔记分享

    测试驱动的嵌入式C语言开发读书笔记1.测试驱动开发瀑布模型的最后开发人员会乱作一团,而缩短
    发表于 12-15 07:25

    C语言的编译步骤

    嵌入式C语言开发入门——程序编译计算机语言发展过程C语言
    发表于 12-15 08:21

    嵌入式C 语言开发ADSP21XX 系列DSP

    详细介绍使用VisualDSP 开发工具进行ADSP21XX 的C 语言编程的方法;分析其C 语言运行库的结构,并且结合实例介绍
    发表于 05-15 14:42 21次下载

    基于C语言嵌入式软件开发中的错误追踪机制

      引言   本文针对嵌入式C语言开发的特点,提出一种基于堆栈模式的异常追踪编程模型,能够实现有效的异常现场保存与恢复,并为后期的问题分析与解决打好基础。
    发表于 08-19 09:25 729次阅读
    基于<b class='flag-5'>C</b><b class='flag-5'>语言</b>的<b class='flag-5'>嵌入式</b>软件<b class='flag-5'>开发</b>中的错误追踪机制

    嵌入式c语言编程(由浅入深)

    本内容详细介绍了嵌入式c语言编程的各项知识,包括嵌入式c语言编程,
    发表于 11-02 14:37 0次下载
    <b class='flag-5'>嵌入式</b><b class='flag-5'>c</b><b class='flag-5'>语言</b>编程(由浅入深)

    C语言深度解剖完美PDF电子书免费下载

    C语言深度解剖》是2012年出版的图书,作者是陈正冲。本书作者结合自身多年嵌入式C语言开发经验
    发表于 11-28 15:35 23次下载

    基于嵌入式C语言开发中的异常堆栈错误追踪机制的设计

    对于嵌入式软件来说,尽量节省内存资源、降低程序代码量是十分重要的。因此,将程序中所有错误、异常情况都进行了统一编码,提高了错误处理代码的规范化与可读性。设计8位整数编码格式如下:
    发表于 03-09 10:35 1192次阅读
    基于<b class='flag-5'>嵌入式</b><b class='flag-5'>C</b><b class='flag-5'>语言</b><b class='flag-5'>开发</b>中的异常堆栈错误追踪机制的设计

    汽车电子行业的MISRA C标准分享

    的、高可靠性的嵌入式软件。MISRA C则是由MISRA提出的针对嵌入式C语言开发标准,目的是提
    的头像 发表于 05-11 13:43 1887次阅读

    嵌入式linux c语言,嵌入式LinuxC语言开发工具.pdf

    2 章 嵌入式Linux C 语言开发工具本章目标任何应用程序的开发都离不开编辑器、编译器及调试器,嵌入
    发表于 11-01 17:38 12次下载
    <b class='flag-5'>嵌入式</b>linux <b class='flag-5'>c</b><b class='flag-5'>语言</b>,<b class='flag-5'>嵌入式</b>LinuxC<b class='flag-5'>语言</b><b class='flag-5'>开发</b>工具.pdf

    嵌入式系统设计--课堂总结(嵌入式Linux系统开发

    嵌入式Linux系统开发嵌入式Linux系统开发(应用软件开发):通过内核提供的服务实现相应功能一、嵌入
    发表于 11-02 12:21 21次下载
    <b class='flag-5'>嵌入式</b>系统设计--课堂总结(<b class='flag-5'>嵌入式</b>Linux系统<b class='flag-5'>开发</b>)

    嵌入式软件开发工程师要求

    年终奖金绩效奖金定期体检申请职位竞争力分析收藏职位信息1、2年以上嵌入式C语言开发经验或2年以上车身电子产品开发经验2、有良好的英语或日语文
    发表于 11-03 12:06 3次下载
    <b class='flag-5'>嵌入式</b>软件<b class='flag-5'>开发</b>工程师要求