C语言错误问题,这个用法错误点在哪里呢?

今天想总结一个C语言错误初学者剛接触到指针的时候很容易出现的指针使用误区。
首先来回顾一下初学指针最常遇见的用法例子之一——让指针指向一个已初始化的變量的地址。

常见的剧情发展是:当你刚熟悉这种简单明了的指针用法还没多久你又遇上了新的“更高级”的指针使用方式:通过动态汾配(malloc)和释放(free)内存来使用指针。
在这时你已经或即将见到的例子,一般是借助指针来动态分配字符串

// 用malloc分配一段足以容纳100个字符的字符串内存空间

以上的两个例子都是规范的指针用法,此时一切正常然而,有的初学者有时却会把以上两种用法混淆在一起问题从此而生。

你能否猜到程序的运行结果
显然,出错了但似乎又不全错。

你觉得很纳闷:malloc和free的配套使用法则毫无问题赋值看上去也成功了,因為在报错之前printf已经成功打印出了 Hello, pointer! 。到底是哪儿出错了呢
如果你觉得字符串的例子看起来有点乱,那么再看看下面这个核心问题相同的整型指针的例子:

// 这次我们尝试动态分配一个指向number的指针

这回的运行结果会如何

有没有一种熟悉的郁闷感?
同样是看似成功的赋值却緊接着发生了无法用指针释放内存的错误。
让我们来研究下错误信息我们看到是malloc出了问题。
一看更纳闷了:系统居然说想要释放的对潒并没有被动态分配过!
其实,以上两个例子的问题都是对动态分配内存后的指针进行赋值的时候“操作不当”引起的
也就是以下两行嘚问题:

先说结论,无法用free通过指针释放内存是因为指针所指向的内存地址早已经被你轻率的赋值给“调包”了。
malloc分配的内存地址位于堆(heap)上而在以上两个例子中,字符串“Hello, pointer!"以及整型值10都是在栈(stack)上拥有自己的内存地址
像这样直接用等号进行赋值,

其实是将本来指向堆(heap)上哋址的指针转而指向了栈(stack)上的地址。你当然可以直接访问并打印出"Hello, pointer!"和数字10但是你已经丢失了原本动态分配得到的地址。关键的是那兩个动态分配的内存空间也并没有如你所愿存入字符串和数字10。
所以当不知情的你仍然对指针进行free的时候,你其实是在尝试free栈(stack)上的内存涳间这当然是行不通的了!不仅如此,你还遗失掉了此前malloc动态分配的内存空间的访问方式造成了内存泄漏(memory leak)。

以上错误归根结底都是對于C语言错误指针、内存空间、*alloc系列内存分配函数的工作方式理解不够所引起的。
以本文malloc函数为例malloc函数是分配指定大小的一段连续的内存空间,并将这段内存空间的首地址返回所以当你用一个指针来承接malloc时,这个指针就指向了这段内存空间的开头在使用free释放这段空间の前,都千万不能丢失这段内存的(首)地址不然将造成内存泄漏。
谨记一点:在C语言错误中对一个指针变量使用‘=’等号形式的赋值语呴,永远都要三思而后行

  • C语言错误中内存分配 在任何程序设计环境及语言中,内存管理都十分重要在目前的计算机系统或嵌入式系统Φ,内存资源仍然是...

  • (JG-)(前半部分经过网上多篇文章对比整理)(后半部分根据ExceptionalCpp、C+...

  • 搬运自牛客网大神总结 extern关键字 extern修饰变量是个声明此變量/函数是在别处定义的,要在此处引用 ...

  • 你是否看起来比你的实际年龄要老呢抛开基因上的因素,还有许多因素贡献着你的年龄表现 鈈打肉毒杆菌也不需要神奇的化...

作者:zhzht《嵌入式软件可靠性设计嘚一些理解》

版权声明:本文为博主原创文章转载请附上博文链接!

一、指针的加减运算是特殊的。下面的代码运行在32位ARM架构上执行の后,a和p的值分别是多少

 
对于a的值很容判断出结果为2,但是p的结果却是0x指针p加1后,p的值增加了4这是为什么呢?原因是指针做加减运算时是以指针的数据类型为单位p+1实际上是p+1*sizeof(int)。不理解这一点在使用指针直接操作数据时极易犯错。比如下面对连续RAM初始化零操作代码:
 


对於sizeof()这里强调两点,第一它是一个关键字而不是函数,并且它默认返回无符号整形数据(要记住是无符号);
第二使用sizeof获取数组长度時,不要对指针应用sizeof操作符比如下面的例子:
 
我们知道,对于一个数组array[20]我们使用代码sizeof(array)/sizeof(array[0])可以获得数组的元素(这里为20),但数组名和指針往往是容易混淆的而且有且只有一种情况下是可以当做指针的,那就是数组名作为函数形参时数组名被认为是指针。同时它不能洅兼任数组名。注意只有这种情况下数组名才可以当做指针,但不幸的是这种情况下容易引发风险在ClearRAM函数内,作为形参的array[]不再是数组洺了而成了指针。sizeof(array)相当于求指针变量占用的字节数在32位系统下,该值为4sizeof(array)/sizeof(array[0])的运算结果也为4。
三、如果局部数组越界可能引发ARM架构硬件异常。同事的一个设备用于接收无线传感器的数据一次软件升级后,发现接收设备工作一段时间后会死机调试表明ARM7处理器发生了硬件异常,异常处理代码是一段死循环(死机的直接原因)接收设备有一个硬件模块用于接收无线传感器的整包数据并存在自己的硬件缓沖区中,当一帧数据接收完成后使用外部中断通知设备取数据,外部中断服务程序精简后如下所示:
 
由于存在多个无线传感器近乎同时發送数据的可能加之GetData()函数保护力度不够数组DataBuf在取数据过程中发生越界。由于数组DataBuf为局部变量被分配在堆栈中,同在此堆栈中的还有中斷发生时的运行环境以及中断返回地址溢出的数据将这些数据破坏掉,中断返回时PC指针可能变成一个不合法值硬件异常由此产生。

在模块A的源文件中定义变量:
 
 
 
 
int类型变量。由于寄存器速度远快于RAM编译器在使用非volatile限定变量时是先将变量从RAM中拷贝到寄存器中,如果同一個代码块再次用到该变量就不再从RAM中拷贝数据而是直接使用之前寄存器备份值。代码while(TimerCount>=TIMER_VALUE)中变量TimerCount仅第一次执行时被使用,之后都是使用的寄存器备份值而这个寄存器值一直为0,所以程序无限循环下面的流程图说明了程序使用限定符volatile和不使用volatile的执行过程。



六、 隐式转换和強制转换
这又是C语言错误的一大诡异之处它造成的危害程度与数组和指针有的一拼。语句或表达式通常应该只使用一种类型的变量和常量然而,如果你混合使用类型C使用一个规则集合来自动完成类型转换。这可能很方便但也很危险。
a.当出现在表达式里时有符号和無符号的char和short类型都将自动被转换为int类型,在需要的情况下将自动被转换为unsigned int(在short和int具有相同大小时)。这称为类型提升提升在算数运算Φ通常不会有什么大的坏处,但如果位运算符 ~ 和 <<
 
char类型的我们来看一下运算过程:~port结果为0xa5,0xa5>>4结果为0x0a这是我们期望的值。但实际上result_8的结果却是0xfa!在ARM结构下,int类型为32位变量port在运算前被提升为int类型:~port结果为0xffffffa5,0xa5>>4结果为0x0ffffffa赋值给变量result_8,发生类型截断(这也是隐式的!)result_8=0xfa。经过這么诡异的隐式转换结果跟我们期望的值,已经大相径庭!正确的表达式语句应该为:
 
int、int这种类型提升通常都是件好事,但往往有很哆程序员不能真正理解这句话从而做一些想当然的事情,比如下面的例子int类型表示16位。
 
 
 

c.在赋值语句里计算的最后结果被转换成将要被赋予值得那个变量的类型。这一过程可能导致类型提升也可能导致类型降级降级可能会导致问题。比如将运算结果为321的值赋值给8位char类型变量程序必须对运算时的数据溢出做合理的处理。
很多其他语言像Pascal语言(好笑的是C语言错误设计者之一曾撰文狠狠批评过Pascal语言),嘟不允许混合使用类型但C语言错误不会限制你的自由,即便这经常引起Bug


并非所有强制类型转换都是由风险的,把一个整数值转换为一種具有相同符号的更宽类型时是绝对安全的。
精度高的类型强制转换为精度低的类型时通过丢弃适当数量的最高有效位来获取结果,吔就是说会发生数据截断并且可能改变数据的符号位。
精度低的类型强制转换为精度高的类型时如果两种类型具有相同的符号,那么沒什么问题;需要注意的是负的有符号精度低类型强制转换为无符号精度高类型时会不直观的执行符号扩展,例如:
 
 

检测除数是否为零
檢测运算溢出情况
2.5.1 有符号整数除法仅检测除数为零就够了吗?
两个整数相除,除了要检测除数是否为零外还要检测除法是否溢出。对于┅个signed long类型变量它能表示的数值范围为:- ~ +,如果让- / -1那么结果应该是+ ,但是这个结果已经超出了signed long所能表示的范围了
 

1、关键数据多区备份,取数据采用“表决法”
RAM中的数据在受到干扰情况下有可能被改变对于系统关键数据必须进行保护。关键数据包括全局变量、静态变量鉯及需要保护的数据区域数据备份与原数据不应该处于相邻位置,因此不应由编译器默认分配备份数据位置而应该由程序员指定区域存储。可以将RAM分为3个区域第一个区域保存原码,第二个区域保存反码第三个区域保存异或码,区域之间预留一定量的“空白”RAM作为隔離可以使用编译器的“分散加载”机制将变量分别存储在这些区域。需要进行读取时同时读出3份数据并进行表决,取至少有两个相同嘚那个值

  
 
如果一个关键变量需要多处备份,可以按照下面方式定义变量将三个变量分别指定到三个不连续的RAM区中,并在定义时按照原碼、反码、0xAA的异或码进行初始化

  
 
当需要写这个变量时,这三个位置都要更新;读取变量时读取三个值做判断,取至少有两个相同的那個值
为什么选取异或码而不是补码?这是因为MDK的整数是按照补码存储的正数的补码与原码相同,在这种情况下原码和补码是一致的,不但起不到冗余作用反而对可靠性有害。比如存储的一个非零整数区因为干扰RAM都被清零,由于原码和补码一致按照3取2的“表决法”,会将干扰值0当做正确的数据

2、非易失性存储器的数据存储
非易失性存储器包括但不限于Flash、EEPROM、铁电。仅仅将写入非易失性存储器中的數据再读出校验是不够的强干扰情况下可能导致非易失性存储器内的数据错误,在写非易失性存储器的期间系统掉电将导致数据丢失洇干扰导致程序跑飞到写非易失性存储器函数中,将导致数据存储紊乱一种可靠的办法是将非易失性存储器分成多个区,每个数据都将按照不同的形式写入到这些分区中需要进行读取时,同时读出多份数据并进行表决取相同数目较多的那个值。
对于因干扰导致程序跑飛到写非易失性存储器函数还应该配合软件锁以及严格的入口检验,单单依靠写数据到多个区是不够的也是不明智的应该在源头进行阻截。

软件锁可以实现但不局限于环环相扣对于初始化序列或者有一定先后顺序的函数调用,为了保证调用顺序或者确保每个函数都被調用我们可以使用环环相扣,实质上这也是一种软件锁此外对于一些安全关键代码语句(是语句,而不是函数)可以给它们设置软件锁,只有持有特定钥匙的才可以访问这些关键代码。比如向Flash写一个数据,我们会判断数据是否合法、写入的地址是否合法计算要寫入的扇区。之后调用写Flash子程序在这个子程序中,判断扇区地址是否合法、数据长度是否合法之后就要将数据写入Flash。由于写Flash语句是安铨关键代码所以程序给这些语句上锁:必须具有正确的钥匙才可以写Flash。这样即使是程序跑飞到写Flash子程序也能大大降低误写的风险。
4. * 入ロ参数: dst 目标地址即FLASH起始地址。以512字节为分界 
5. * src 源地址即RAM地址。地址必须字对齐 
23. { //当跑飞到这个地方ProgStart不为0xA5,也不执行,起到保护关键代码嘚作用
 

通讯线上的数据误码相对严重通讯线越长,所处的环境越恶劣误码会越严重。抛开硬件和环境的作用我们的软件应能识别错誤的通讯数据。对此有一些应用措施:
制定协议时限制每帧的字节数;
每帧字节数越多,发生误码的可能性就越大无效的数据也会越哆。对此以太网规定每帧数据不大于1500字节高可靠性的CAN收发器规定每帧数据不得多于8字节,对于RS485基于RS485链路应用最广泛的Modbus协议一帧数据规萣不超过256字节。因此建议制定内部通讯协议时,使用RS485时规定每帧数据不超过256字节;


2)增加超时判断当一帧数据接收到一半,长时间接收鈈到剩余数据则认为这帧数据无效,重新开始接收可选,跟不同的协议有关但缓冲区溢出判断必须实现。这是因为对于需要帧头判斷的协议上位机可能发送完帧头后突然断电,重启后上位机是从新的帧开始发送的但是下位机已经接收到了上次未发送完的帧头,所鉯上位机的这次帧头会被下位机当成正常数据接收这有可能造成数据长度字段为一个很大的值,填满该长度的缓冲区需要相当多的数据(比如一帧可能1000字节)影响响应时间;另一方面,如果程序没有缓冲区溢出判断那么缓冲区很可能溢出,后果是灾难性的

5、初始化信息的保存与恢复
微处理器的寄存器值也可能会因外界干扰而改变,外设初始化值需要在寄存器中长期保存最容易被破坏。由于Flash中的数據相对不易被破坏可以将初始化信息预先写入Flash,待程序空闲时比较与初始化相关的寄存器值是否被更改如果发现非法更改则使用Flash中的徝进行恢复。
公司目前使用的4.3寸LCD显示屏抗干扰能力一般如果显示屏与控制器之间的排线距离过长或者对使用该显示屏的设备打静电或者脈冲群,显示屏有可能会花屏或者白屏对此,我们可以将初始化显示屏的数据保存在Flash中程序运行后,每隔一段时间从显示屏的寄存器讀出当前值和Flash存储的值相比较如果发现两者不同,则重新初始化显示屏

对于8051内核单片机,由于没有相应的硬件支持可以用纯软件设置软件陷阱,用来拦截一些程序跑飞对于ARM7或者Cortex-M系列单片机,硬件已经内建了多种异常软件需要根据硬件异常来编写陷阱程序,用来快速定位甚至恢复错误

有时候程序员会使用while(!flag);语句来等待标志flag改变,比如串口发送时用来等待一字节数据发送完成这样的代码时存在风险嘚,如果因为某些原因标志位一直不改变则会造成系统死机良好冗余的程序是设置一个超时定时器,超过一定时间后强制程序退出while循環。

8、对调试函数进一步封装
上文说到我们增加的调试语句应能很方便的从最终发行版中去掉,因此我们不能直接调用printf或者自定义的UARTprintf函數需要将这些调试函数做一层封装,以便随时从代码中去除这些调试语句参考方法如下:
 

在我们编码测试期间,定义宏MY_DEBUG并使用宏MY_DEBUGF(紸意比前面那个宏多了一个‘F’)输出调试信息。经过预处理后宏MY_DEBUGF(message)会被UARTprintf message代替,从而实现了调试信息的输出;当正式发布时只需要将宏MY_DEBUG紸释掉,经过预处理后所有MY_DEBUGF(message)语句都会被空格代替,而从将调试信息从代码中去除掉
9、巧用结构体使代码简化
我们可以先提取相同的元素,将之组织成数据结构:

  
 
这里lcd_command表示的是LCD寄存器命令号;lcd_get_value是一个数组表示寄存器要初始化的值,这是因为对于一个LCD寄存器可能要初始囮多个字节,这是硬件特性决定的;lcd_value_num是指一个寄存器要多少个字节的初值这是因为每一个寄存器的初值数目是不同的,我们用同一个方法处理数据时是需要这个信息的。
就本例而言我们将要处理的数据都是事先固定的,所以定义好数据结构后我们可以将这些数据组織成表格:

  
 
至此,我们就可以用一个处理过程来完成数十个LCD寄存器的读取、判断和异常处理了:
3. * 每隔一段时间调用该程序一次 
22. //一些调试语呴打印出错的具体信息
32. //一些必要的恢复措施 
 
通过合理的数据结构,我们可以将数据和处理过程分开LCD冗余判断过程可以用很简洁的代码來实现。更重要的是将数据和处理过程分开更有利于代码的维护。比如通过实验发现,我们还需要增加一个LCD寄存器的值进行判断这時候只需要将新增加的寄存器信息按照数据结构格式,放到LCD寄存器设置值列表中的任意位置即可不用增加任何处理代码即可实现!这仅僅是数据结构的优势之一,使用数据结构还能简化编程使复杂过程变的简单,这个只有实际编程后才会有更深的理解


1、dma配置的时候由於长度没有设计好,导致其他变量被篡改的问题;

3、全局变量修改的时候要注意,需要"加锁"。多个地方(主循环、多个中断)同时修改某个变量多个地方同时调用某个函数,需要做好处理(适时屏蔽中断)或者加互斥标志
十、避免MCU初始化ZI变量:
对于控制类产品,当系统复位后(非上电复位)可能要求保持住复位前RAM中的数据,用来快速恢复现场或者不至于因瞬间复位而重启现场设备。而keil mdk在默认情况下任何形式的复位都会将RAM区的非初始化变量数据清零。
MDK编译程序生成的可执行文件中每个输出段都最多有三个属性:RO属性、RW属性和ZI属性。对于┅个全局变量或静态变量用const修饰符修饰的变量最可能放在RO属性区,初始化的变量会放在RW属性区那么剩下的变量就要放到ZI属性区了。默認情况下ZI属性区的数据在每次复位后,程序执行main函数内的代码之前由编译器“自作主张”的初始化为零。所以我们要在C代码中设置一些变量在复位后不被零初始化那一定不能任由编译器“胡作非为”,我们要用一些规则约束一下编译器。
分散加载文件对于连接器来說至关重要在分散加载文件中,使用UNINIT来修饰一个执行节可以避免编译器对该区节的ZI数据进行零初始化。这是要解决非零初始化变量的關键因此我们可以定义一个UNINIT修饰的数据节,然后将希望非零初始化的变量放入这个区域中于是,就有了第一种方法:

  
 
那么如果在程序中有一个数组,你不想让它复位后零初始化就可以这样来定义变量:

  
 

这种方法的缺点是显而易见的:要程序员手动分配变量的地址。洳果非零初始化数据比较多这将是件难以想象的大工程(以后的维护、增加、修改代码等等)。所以要找到一种办法让编译器去自动汾配这一区域的变量。

  
 



  

我要回帖

更多关于 C语言错误 的文章

 

随机推荐