【续】C语言的一些“骚操作”及其深层理解|环球速读
此系列文章,于振南老师向大家讲述嵌入式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,关于语言方面的一些常见问题、非常规操作以及认知误区振南就讲这么多。
语言其实是博大精深,还是那句话:“学得多精都不为过!”我一直把嵌入式工程师比喻成“能与硬件对话的灵媒”,我们所使用的语言就是语言。我们自认为对语言已经足够了解了,足够精通了,但我们又会发现在实际开发过程中,会遇到很多新的问题,很多问题是与语言本身相关的。
关键词:
上一篇:怒骂“金主”后悔过,粉笔CEO张小龙又双叒叕“过嘴瘾”?
下一篇:最后一页
广告
X 关闭
X 关闭
-
京张高铁每日开行17对冬奥列车
京张高铁每日开行17对冬奥列车 预计冬奥服务保障期运送运动员、技术官员、持票观众等20万人次 2月6日,2022北京新闻中心举行“北
-
北京冬奥会开幕式上 小学生朱德恩深情演绎《我和我的祖国》
北京冬奥会开幕式上 小学生朱德恩深情演绎《我和我的祖国》 9岁小号手苦练悬臂吹响颂歌 2月4日晚,在北京冬奥会开幕式上,9岁的
-
2022北京冬奥会开幕式这19首乐曲串烧不简单
多名指挥家列曲目单 再由作曲家重新编曲 本报专访冬奥开幕式音乐总监赵麟 开幕式这19首乐曲串烧不简单 “二十四节气”倒计时、
-
“一墩难求” 冰墩墩引爆购买潮
设计师:没想到冰墩墩成爆款一墩难求冰墩墩引爆购买潮 北京冬奥组委:会源源不断供货北京冬奥会吉祥物冰墩墩近日引爆购买潮,导致一墩难求
- 1、【续】C语言的一些“骚操作”及其深层理解|环球速读
- 2、怒骂“金主”后悔过,粉笔CEO张小龙又双叒叕“过嘴瘾”?
- 3、每日快讯!“这一刻,他们让世界看到了中国”
- 4、厦门大学2023年强基计划招生考试公告:公布录取结果及录取标准_环球热点评
- 5、普里戈任:我们将在前线取得新胜利|世界讯息
- 6、观热点:兴海龙藏乡:巩固和提升党内组织生活规范化水平
- 7、今日热门!洪江区:瞄准“百亿园区”目标 高质高效推进调区扩区工作落地落实
- 8、港股异动 | 中国稀土(00769)涨超10%领涨稀土概念 机构指人形机器人产业化加速有望带动稀土永磁材料需求改善
- 9、世界信息:防晒市场如何靠差异化“破圈”?品牌方积极布局
- 10、为何说朱元璋是文盲的说法在今天看来是一个谣言?-世界热推荐