首页 资讯 > > 正文

【世界播资讯】【续】C语言的一些“骚操作”及其深层理解

来源:面包芯语 发布日期:2023-07-04 12:35:01 分享到:

此系列文章,于振南老师向大家讲述嵌入式C语言的一些高阶知识,俗称“骚操作”,助你水平再上一个台阶!

在第一篇文章中,给大家列举了一些我曾经见过和使用过的编程技巧。今天这篇文章,我们继续讲解嵌入式C语言的编程技巧,并进行深入解析。

一、隐藏的死循环

有时候,我们会发现for循环变成了一个死循环:


(资料图片)

unsigned char i;for(i = 4; i >= 0; i--){....}
signed char i;for(i = 4; i >= 0; i--){....}

下面的两个例子中for循环也是死循环,请自行分析:

例1:

unsigned char i;for(i=0;i<256;i++) {...}

例2:

char str[20];char \*p;unsigned char n=0;for(p = strcpy(str,"      abcd"); ((*p)=" "); p++,n++);

提示:这个例子,不光会死循环,而且还可能会让程序直接崩溃。判等的 == 你会不会经常直接写错成 =(赋值表达式)。

二、看似多余的空循环

有时我们会看到这样的代码:

do{ ...... //do something}while(0);

代码本身实际只运行了一次,为什么要在它外面加一层do while呢?这看似是多余的。其实不然,我们来看下面例子:

#define DO_SOMETHING fun1();fun2();void main(void){ while(1)  DO_SOMETHING;}

所以,我们可以这样来写:

#define DO_SOMETHING do{ fun1();fun2();}while(0);

三、独立执行体

我在C语言编程的过程中,经常乐于使用一种“局部独立化”的方式,我称之为“独立执行体”,如下例:

void fun(int a,int b,int c){ Int tmp=0; //**主体计算  { //**独立执行体,解决临时性问题  int c=0;  c=(a>b)?a:b;  printf("max:%d\r\n",c);  }   { //**独立执行体*  int c=0,d=0,.....,res=0.;  //**数据处理算法  printf("result:%d\r\n",res);  } //**进一步计算}

这过程中可能需要独立的变量,以及独立于主体程序的执行逻辑,但又觉得不至于去专门定义一个函数,只是想一带而过。比如上例,函数fun主要对a、b、c这3个参数进行计算(使用某种算法),过程中想临时看一下a和b谁比较大,由第一个“独立执行体”来完成,其中的代码由自己的{}扩起来。

其实我们可以更深层的去理解C语言中的{},它为我们开辟了一个可自由编程的独立空间。在{}里,可以定义变量,可以调用函数以及访问外层代码中的变量,可以作宏定义等等。平时我们使用的函数,它的{}部分其实就是一个“独立执行体”。

“独立执行体”的思想,也许可以让我们编程更加灵活方便,可以随时让我们直接得到一块自由编程的静土。

上一节中的do while(0),其实完全可以把do while(0)去掉,只用{}即可:

#define DO_SOMETHING {fun1();fun2();}

另外,它还有一个好处,那就是当你不需要这段代码的时候,你可以直接在{}前面加上if(0)即可。一个“独立执行体”的外层是可以受if、do while、while、for等这些条件控制的。

四、多用()无坏处

!0+1,它的值等于多少?其实连我这样的老手也不能马上给出答案,2还是0?按C语言规定的运算符优先级来说,应该是!大于+,所以结果应该是2。

但如果把它放在宏里,有时候就开始坑人了:

#define VALUE !0+1int a;a=VALUE&0;

这里出现了一些运算优先级和结合律的差错。为了让我们的语义和意图正确的得以表达,所以建议多用一些()。

#define VALUE ((!0)+1)int a;a=VALUE&0;

另外,有时候优先级还与 C 语言编译器有关,同一个表达式在不同的平台上,可能表达的意义是不同的。所以,为了代码的可植移性、正确性以及可读性,强烈建议多用一些()。

五、==的反向测试

C语言中的 = 与 ==,有时候是一个大坑。主要体现在条件判断时的值比较,如下例:

int a=0;If(a=1){ //**代码}

C语言中的赋值操作也是一种表达式,称为赋值表达式,它的值即为赋值后变量的值。而C语言中条件判断又是一种宽泛的判断,即非0为真,为0则假。所以if(a=1)这样的代码编译是不会报错的。

这种错误通常是很难排查出来的,尤其是在复杂的算法中,只能一行行代码的跟踪。所以对于变量值的比较判断,振南建议使用“==的反向测试”,并养成习惯。

int a=0; if(1==a){ //**代码}

如果把 == 错写成了 =,因为常量无法被赋值,所以编译时会报错。

六、赋值操作的实质

原来一位哈工程理学院教授(搞数学的)讲述了自己的一个困惑,一直以来都被我们当成一个笑话在说。他学C语言的时候,首先a=1,然后后面又来一个a=2,这让他非常不解,a 怎么可能同样等于 1 又等于 2 呢?

其实这是因为他对计算机运行机制不了解,这个a不是他数学稿纸上的代数变量,而是计算机中实实在在的“电”,或者说“信号”,如下图所示。

其实不限于C语言,所有编程语言的目的都是控制计算机硬件,实现电信号的传输、存储等操作,最终达成某一功能。

变量是存储器中的一小块空间,它源自于形如int a这样的代码编译后由编译器所作的存储器分配。对变量的赋值就是CPU内核通过三总线将数据传输到存储器特定地址单元上的过程。所以,a=1;a=2;只是两次数据传输过程而已。

这个教授当时算是个外行,其实对于我们也是一样的,想要真正掌握编程语言,只流于代码表面的意思是不行的,必须对它在硬件上产生的操作有明确的认识,对计算机内部的运行机理有深入理解才可以。

七、关于补码

补码是一个很基础的概念,但对于很多人来说,其实有些迷糊,这里对补码进行一些通俗而深刻的讲解。

C语言中的整型类型有两种,无符号与有符号。无符号比较好理解,如下图所示。

只需要将每一个位乘以它的权值,再求和即是其所表达的数值。它所有的位都用来表达数值,因此上图中类型能表达的范围为0~255(8个位)。但如何表达负数,比如-10,这个时候就涉及到补码了,如下图所示。

有符号整型的最高位被定义为符号位,0 为正数,1为负数。上图中前一行等于+76,后一行等于多少?-76?那就错了。

对于负数的数值要按其补码来计算,如下图所示。

为什么要引入补码的概念,符号位表示符号,其它位直接表示其绝对值,不是更好吗?这其实是一个数字游戏。我们要知道一个前提:CPU中只有加法器,而没有减法器。OK,我们看下面的例子。

可以看到,补码将符号位也统一到了计算过程中,并且巧妙地使用加法实现了减法操作。这对于简化CPU中的算术逻辑电路(ALU)具有重要意义。

八、关于-1

为了说明关于 -1 的问题,我们先来看一个例子:

signed short a = -1;if(-1==a){ //....}

我们要知道C语言中的判等 == 运算是一种强匹配,也就是比较的双方必须每一个位都匹配才被认为相等。在上例中,a在内存中的表示是 0XFFFF(补码),但 -1 这个常量在内存中的表示在不同的硬件平台上却不尽相同,在16位CPU平台上是0XFFFF,它们是相等的。而在32位CPU平台上则是0XFFFFFFFF,它们就不相等。

所以,稳妥的办法是:

signed short a=-1;if(((signed short)-1) == a){ //....}

九、字节快速位逆序

一道有意思的题目:如何快速得到一个字节的位逆序字节。比如 0X33 的位逆序字节是 0XCC。

有人给了我这样一段代码:

unsigned char reverse_byte(unsigned char byte){ unsigned char i=0; unsigned char temp=0; for(i=0;i<8;i++) {  if(byte&(0x01<{  temp|=(0x80>>i);  } } return temp;}

这段代码很简洁,也很巧妙。但它却不是最快的。后来作了改进:

unsigned char reverse_byte(unsigned char byte){unsigned char temp=0;if(byte&0x01) temp|=0x80;if(byte&0x02) temp|=0x40;if(byte&0x04) temp|=0x20;if(byte&0x08) temp|=0x10;if(byte&0x10) temp|=0x08;if(byte&0x20) temp|=0x04;if(byte&0x40) temp|=0x02;if(byte&0x80) temp|=0x01; return temp;}

这样把循环打开,确实会提速不少。但它仍不是最快的实现方案。请看如下代码:

unsigned char rbyte[256]={0x00,0x80,0x40,0xc0,0x20,........};#define REVERSE_BYTE(x) rbyte[x]

这个问题我问过很多人,多数人并不能直接给出最佳方案。倒是有不少人问我这个问题有什么实际意义,为什么要去计算位逆序字节?请大家想想,如果我们把电路上的数据总线焊反或插反了该怎么解决。

十、关于volatile

现在的编译器越来越智能,它们会对我们的代码进行不同程度的优化。请看下例:

unsigned char a; a=1;a=2;a=3;
unsigned char xdata a _at_ 0X1111;a=1;a=2;a=3;

这种时候,a=1和a=2不能被优化掉。举个例子:a所指向的外部总线端口,是一个电机控制器的接口,向它写入1是加速,写入2是减速,写入3是反向。那么上面的代码就是加速->减速->反向,这样一个控制过程。如果被优化的话,那最后就只有反向了。

为了防止这种被“意外”伦的情况发生,我们可以在变量的定义上加一个修饰词volatile。

volatile unsigned char xdata a _at_ 0X1111;a=1;a=2;a=3;

这样,编译器就会对它单独对待,不再优化了。

#define __IO volatiletypedef struct{  __IO uint32_t CRL;  __IO uint32_t CRH;  __IO uint32_t IDR;   __IO uint32_t ODR;  __IO uint32_t BSRR;  __IO uint32_t BRR;  __IO uint32_t LCKR;} GPIO_TypeDef;

这是对 STM32 的 GPIO 寄存器组的定义,每一项都是一个__IO类型,其实就是 volatile。这样是为了对片内外设的物理寄存器的访问一定是真正落实的,而不是经过编译器优化,而变成去访问缓存之类的东西。

十一、关于变量互换

int a,b;int temp;temp=a;a=b;b=temp;
int a,b;a=a+b;b=a-b;a=a-b;
int a,b;a=a^b;b=a^b;a=a^b;

异或运算有一个性质叫自反性,这个可以实现很多巧妙的操作,大家可以深入研究一下(异或位运算比上面的加减法更严谨,因为加减法是可能会溢出的)。

十二、关于sizeof

C语言中的sizeof我们应该是非常熟悉的,它的作用就是用来计算一个变量或类型所占用的字节数。

sizeof(int) //**如果是32位CPU平台,值为4,即4个字节int a; sizeof(a) //**同上sizeof(struct ...) //**计算某结构体的大小

这个很简单,我们再来看下面的代码:

char *pc="abc";sizeof(pc) //指针的sizeof等于多少?sizeof(*pc) //指针指向的单元的sizeof等于多少?

pc 本身是一个指针类型,在32位平台上sizeof(pc)的值为4,即指针类型占用4个字节(与CPU平台有关)。*pc是pc所指向的变量,所以sizeof(*pc)的值为1。

好,还能理解吧,那我们再来看:

char a1[]="abcd";sizeof(a1) //数组的sizeof等于多少?void fun(char a1[]) //形参a1的sizeof等于多少?{ //....}

第一个sizeof(a1)等于5,因为它是一个数组(最后还有一个字符串结束符’\0’)。第二个sizeof(a1)等于4,形参中的a1不再是一个数组,而是一个指针。

struct {} a,b,c;sizeof(a) //空结构体的sizeof等于多少? 

空结构体类型变量的大小是多少?这个问题似乎有些奇葩,没什么实用性。空结构体有什么用?

十三、memcpy的效率

void memcpy(unsigned char *pd,const unsigned char *ps,unsigned int len){ unsigned int i=0; for(i=0;i}
void memcpy(unsigned char *pd,const unsigned char *ps,unsigned int len){ unsigned int i=0; unsigned int temp=len/sizeof(unsigned int); for(i=0;ii*=sizeof(unsigned int); for(;i}

改进后的代码最大限度的利用了CPU数据总线带宽,每次传输多个字节(如32位平台为4字节)。这一实例告诉我们:C语言,尤其是嵌入式C语言很多时候需要考虑硬件层面的因素,如CPU总线、内存结构等。

十四、[]的本质

当我们想取出一个数组中的某个元素时,我们会用到[],采用下标的方式。如下例:

int a[3]={1,2,3};a[1]; //数组a的第2个元素 

上图可能颠覆了一些人对[]的认识,下标还能是负数?[]可以在一个开始地址后面去取数据,为什么不能在它前面取数据呢?我们可以理解[]是对指针加减和取值操作的综合。

认清了[]的实质,再加上对C语言的精髓--指针深刻的理解,我们编程将会非常灵活,肆意挥洒。

十五、#与##(串化与连接)

C语言中的#与##可能很多人都不了解,更没有用过,因为在一般的教材上都没有对它们的介绍。但把它们用好了,也能使我们的代码别有一番格调。来看下面的例子:

#define STR(s) #sprintf("%s", STR(www.znmcu.com);printf("%s", “www.znmcu.com”); //**宏展开之后的效果

再来看一下连接符 ##,它用来将参数和其它的内容连接起来,如下例:

#define CON1(a, b) a##e##b     #define CON2(a, b) a##b##00printf("%f\n", CON1(8.5, 2));         printf("%d\n", CON2(12, 34)); printf("%f\n", 8.5e2); //**展开后的效果printf("%d\n", 123400); //**展开后的效果

所以,C语言学得多精都不为过,很多知识我们可能一辈子都不会用到,但不代表我们可以不知道,因为别人在用。

OK,关于语言方面的一些常见问题、非常规操作以及认知误区振南就讲这么多。

语言其实是博大精深,还是那句话:“学得多精都不为过!”我一直把嵌入式工程师比喻成“能与硬件对话的灵媒”,我们所使用的语言就是语言。我们自认为对语言已经足够了解了,足够精通了,但我们又会发现在实际开发过程中,会遇到很多新的问题,很多问题是与语言本身相关的。

关键词:

x 广告

复原民国旧菜单 一批“消失的名菜”重现羊城

  中新网广州12月5日电 (记者 程景伟)“粤宴中国·消失的名菜”活动4日晚在广州博物馆镇海楼广场举行,一批业已失传或十分罕见的传统粤

青海再度“双清零”:战“疫”催生定点救治医院反思与成长

  中新网西宁12月5日电 题:青海再度“双清零”:战“疫”催生定点救治医院反思与成长  作者 潘雨洁  全面停诊、四下无人;火线冲

世界海拔最高高铁客运站山丹马场站运营

  中新网兰州12月5日电 (记者 杨艳敏)记者从中国铁路兰州局集团有限公司获悉,12月5日10时29分随着嘉峪关南至西安北D2696次动车组列车

千年古都洛阳为何要建青年友好型城市?

  中新网洛阳12月5日电 题:千年古都洛阳为何要建青年友好型城市?  记者 肖开霖 李贵刚  千年古都洛阳日前公布《洛阳市建设青年

甘肃万余河长公示牌拥有“电子身份证” 局地启“千里眼”治水

  中新网兰州12月5日电 (记者 冯志军)记者5日从甘肃省水利厅获悉,今年以来,甘肃全面推动河长公示牌信息化建设,为全省河流换发“电子

满洲里市向呼伦贝尔市“手递手”异地转运3批次隔离人员

  (抗击新冠肺炎)满洲里市向呼伦贝尔市“手递手”异地转运3批次隔离人员  中新网呼伦贝尔12月5日电 (记者 张玮)5日,内蒙古自治区呼

2021年度法治人物沈云如:让群众过上“有身份的生活”

  中新网杭州12月5日电 题:2021年度法治人物沈云如:让群众过上“有身份的生活”  作者 郭其钰 张先登  行程10余万公里,为辖区3

全国妇联在北京组织巾帼志愿者服务社区“一老一小”

  中新网北京12月5日电 (记者 余湛奕)在12月5日国际志愿者日前夕,全国妇联4日在北京组织机关党员干部以巾帼志愿者身份,聚焦“一老一

x 广告

Copyright   2015-2022 青年海洋网版权所有  备案号:皖ICP备2022009963号-20   联系邮箱:39 60 291 42@qq.com