目录

前言

易错点总结

1、关于浮点数比较及交换

2、关于strlen()使用

3、关于自动转换规则

4、关于数据类型长度

5、关于字长

6、关于续行符

 7、bool类型与“零值”判断

8、指针常见大坑

8.1指针传递

开发技巧总结

1、数据取余

2、宏的高级用法

3、临界区保护(裸机)


前言

        本篇内容主要列举一些自己在嵌入式开发过程中踩过的大坑(还有平时收集的一些易错点),希望能对遇到类似问题的开发者有所启发。

易错点总结

1、关于浮点数比较及交换

        第一次碰到这个问题是在刚毕业面试那会,当时有一个面试题:怎么判断一个float类型数据是否为0?一开始看到这个问题的时候还真没当回事,不就 return (f == 0);就完事了吗?结果可想而知。现在回过头来看,确实这个回答略显业余。

 void main()
 {
      float x =1.2; float y = 3.2;
      if((x + y)==4.4)  {  printf(“correct\n”);  }
      else  {  printf(“ error \n”);  }
 } 

结果是error!!!浮点数是为了能用二进制表达小数而引入的一种量化形式,转换时存在舍入差,因此不能直接用“==”比较浮点数,正确的做法应该是确定一个误差范围,通过比较两个浮点数的差值是否在这个误差范围内来判断是否相等。

 const float delta = 0.1
    void main()
    {
      float x =1.2;  float y = 3.2;
      float z = x + y;
      if((z < (4.4+delta))&&(z > (4.4-delta))) {  printf(“correct\n”);  }
      else  {  printf(“ error \n”);  }
    }

那么很显然判断一个float数据是否等于0应该是如下表达

int isZero(float data)
{
    //float类型一般精确到小数点后5位或6位
    return(data > -0.000001 && data < 0.000001);
}

2、关于strlen()使用

如下列代码所示

/*判断一*/
if(strlen(x)>= strlen(y))
{
}
/*判断二*/
if(strlen(x)- strlen(y)>= 0)
{
}

        从表面上看,上面的两个判断表达式“完全相等”,但实际情况并非如此。其中,判断表达式一没什么问题,程序也能够完全按照预想的那样工作; 但判断表达式二的结果就不一样了,它将永远是真,这是为什么呢? 原因很简单,因为函数 strlen 的返回结果是 size_t 类型(即无符号整型)而 size_t 类型绝不可能是负的。因此,语句“if(strlen(x)-strlen(y)>=0)”将永远为真。

3、关于自动转换规则

        这个问题是在几天前碰到的,类似代码如下

unsigned char foo(void)
{
    unsigned int a = 6;
    int b = -20;
    return (a+b > 6);
}

以上函数返回值为真!原因:当表达式中同时存在有符号类型和无符号类型时,所有的操作数都自动转换为无符号类型上式中-20会被自动转换为一个非常大的正整数,所以结果会远远大于6。(如果这个问题出现在上大学那会,我想肯定不会出错,其实很多问题的解决方法往往是最基础的理论知识,像运算符优先级、自动转换这些概念往往容易出现问题,但却都是最基础的代码规则,所以大家在写代码时切勿眼高手低,遇到拿不准的地方及时巩固)

4、关于数据类型长度

        还是一个面试,当时面试官问我:int类型数据占用几个字节?4个字节!接着他问我确定吗?我犹豫了,因为我觉得他不会问这么简单的问题!那是不是就要分情况分析?不同编译器、不同位数的处理器是不是就不一样?我们常用的处理器都是32位,int类型默认占用4个字节,那如果是64位处理器是不是就是8个字节?float类型是不是也会有变化?究其原因还是对底层概念的模糊。

        那么今天在这里做一个简单的归纳:数据类型的长度与编译器规则密不可分,尽管同样的类型在不同位数的系统上所代表的位数也可能不同,但是总归是有一些规则约束的。编译器可以根据自身硬件来选择合适的大小,但是需要满足约束:short和int型至少为16位,long型至少为32位,并且short型长度不能超过int型,而int型不能超过long型。这即是说各个类型的变量长度是由编译器来决定的,下面列举在GCC编译器下16位机器、32位机器和64位机器各个类型变量所占字节数:

C类型163264
char111
short int222
int244
long long888
char*248
float444
double888

5、关于字长

        字长涉及到的易错点有很多,数据的字节对齐、结构体数据的步进值、指针移位等等,但这些相信大家平时接触的都比较多,这里不再详细列举,请看下列示例

unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */

如上代码,在一个int类型不是16位的处理器中表达就是不正确的,应该表示为

unsigned int zero = 0;
unsigned int compzero = ~0;
/*1's complement of zero */

(在单片机中,常采用“一补”(1's complement)格式,也即在16位处理器中,0xffff就是0,在32位单片机中,0xffffffff就是0;在手机端或者电脑端一般采用“二补”(2's complement),即0xffffffff就是-1)

6、关于续行符

        续行符'\' 用于将宏定义、字符串或代码跨行书写,提升可读性。
        核心规则:'\' 必须是行的最后一个字符,后面不能有任何空格、制表符或注释,否则会导致预处理错误。

正确用法:

#define ADD(a, b) \
    ((a) + (b))   // \后直接换行,无空格/注释

int sum = ADD(3, 4);  // 展开为 ((3) + (4))

错误用法:

#define ADD(a, b) \ //a+b 错误!!! 续行符后不能追加注释或空格
    ((a) + (b))   

int sum = ADD(3, 4);  // 展开为 ((3) + (4))

 特殊场景处理:注释与续行符共存,需要把注释写在续行符前面。

#define ADD(a, b)   /* 注释写在\前 */ \
    ((a) + (b))

 7、bool类型与“零值”判断

乍一看是不是很简单?是的,但你想过深层的逻辑没?

bool b;
if(b == 1)
if(b == TRUE)

 以上这个答案是有问题的!为什么?首先要清楚什么bool类型?什么是真?什么是假?

很简单,0是假,非0是真!!!

那么,什么叫非0?-1,算不算,1算不算,10000算不算?都算!反正不是0就是真。

所以,如果有个值i = 2,那么if(i == TRUE)就不灵了。正确的应该这样:

bool b;
if(b)
if(!b)

8、指针常见大坑

8.1指针传递

请问下列代码有什么问题?

void getptr(char *ptr)
{
	ptr = (char *)malloc(100);
}

void test(void)
{
	char *ptr = null;
	getptr(ptr);
	strcpy(ptr, "hello");
	printf("%s", ptr);
	free(ptr);
}

乍一看似乎没什么问题,但总觉得哪里有点奇怪。

指针是什么?
指针本质上是一个变量,用于存储内存地址。

问题分析:

void test(void) {
    char *ptr = NULL;        // ptr变量存储在test的栈帧中
    getptr(ptr);             // 传递的是ptr的值(NULL)的副本
    
    // 这里ptr仍然是NULL!
    // strcpy将尝试向地址0写入数据 → 段错误
}

如果非要使用指针传递,则应该写成这样

void getptr(char **pptr)      // pptr是指向指针的指针
{    
    *pptr = malloc(100);      // *pptr就是原ptr变量
}

void test(void) 
{
    char *ptr = NULL;
    getptr(&ptr);             // 传递ptr的地址
    // 现在ptr指向有效内存
}

不妨再来看一个更直观的例子:

void cannot_change(int x) 
{
    x = 100;  // 只修改副本
    printf("函数内: x = %d\n", x);  // 输出 100
}

void can_change(int *x) 
{
    *x = 100;  // 通过地址修改原变量
    printf("函数内: *x = %d\n", *x);  // 输出 100
}

void test(void) 
{
    int a = 10;
    
    printf("调用前: a = %d\n", a);  // 输出 10
    
    cannot_change(a);
    printf("调用后: a = %d\n", a);  // 输出 10(没变!)
    
    can_change(&a);
    printf("调用后: a = %d\n", a);  // 输出 100(变了!)
}

这样是不是就豁然开朗了?!所以,不要看到数据带星号就开始发懵,想想清楚本质,你就会发现并没有什么大不了事情。

总结:

  1. 指针也是变量,它存储的是地址值

  2. C语言总是按值传递,包括指针本身

  3. 要修改函数外的变量(包括指针变量),必须传递它的地址

  4. 对于指针变量来说:

    • 要修改指针指向的内容 → 传递指针 char *ptr

    • 要修改指针本身(让它指向新的地方)→ 传递指针的地址 char **ptr

开发技巧总结

1、数据取余

value % x 等同于 value & (x-1),仅限于对偶数取余

2、宏的高级用法

        使用场景:假如我们有很多种动物(猫,狗,鸡等等),都需想用一个结构体来定义,再假如结构体都只想描述动物的重量和身高,但又不想所有动物的结构体名称一致。假如结构体都想用Animal_打头,如Animl_Cat,同样我们用宏定义来实现:

#define StructureType(type)                           typedef struct{ \
                                                            float weight; \
                                                            float height; \
                                                        }Animal_ ## type;
使用:
StructureType(Cat);// 定义一个Animl_Cat结构体
Animal_Cat cat;// 实例化一个Animl_Cat
cat.weight = 10;
cat.height = 30;

你可能在想,我想定义一个新的动物结构体,增加一个动物属性,那就结合上面的组合方式
#define StructureTypeEx(type, member)                  typedef struct { \
															float weight; \
															float height; \
															char* ## member; \
														}Animal_ ## type;

使用:
StructureTypeEx(Dog, Name);// 定义了一个动物名
Animal_Dog dog;
dog.height = 60;
dog.weight = 100;
dog.Name = (char *)malloc(32);
memset(dog.Name, 0, 32);
strcpy(dog.Name, 7, "HuaHua");

3、临界区保护(裸机)

        裸机中的临界区保护实现,分享常用的两种方式。

        方式1:直接调用ARM内核接口,简单粗暴。

uint32_t enter_critical(void)
{
    // 保存当前 PRIMASK 值
    uint32_t regPrimask = __get_PRIMASK();

    // 关闭系统全局中断(其实就是将 PRIMASK 设为 1)
    __disable_irq();
 
    return regPrimask;
}
 
void exit_critical(uint32_t primask)
{
    // 恢复 PRIMASK
    __set_PRIMASK(primask);
}

         方式2:使用汇编实现,使用时需要定义一个uing32_t的变量用来保存中断状态。

//用于临界态
__asm unsigned int OS_CPU_SR_Save(void)
{
    MRS     R0, PRIMASK           					; Read PRIMASK to R0
    CPSID   I							   			;Disable interrupts and configurable fault handlers (set PRIMASK)
    BX      LR										;Return from function call
}
__asm void OS_CPU_SR_Restore(unsigned int Arg)
{
    MSR     PRIMASK, R0
    BX      LR
}

//调用
#define  OS_ENTER_CRITICAL()  {cpu_sr = OS_CPU_SR_Save();}			/* disable interrupt*/
#define  OS_EXIT_CRITICAL()   {OS_CPU_SR_Restore(cpu_sr);}			/* enable interrupt*/

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐