热门文章
大家好,我是张飞实战电子张角老师!
同学们好,我们从今天开始探讨单片机C语言,我们首先从if和for等基本语句结构开始。
if和for,这两个从本质上来说,是不是C语言的两个关键字呀。那么我们为什么又把它称之为C语言的基本结构的一部分呢。要回答这个问题,我们是不是首先要搞清楚C语言的基本结构是什么?
实际上,任何结构化编程语言的基本结构都是相同的,也就是三种基本的程序结构:顺序,分支和循环。由这三者最基本的结构,可以搭建出任何我们想实现的程序结构。在狄杰斯特拉(Edsger W. Dijkstra)反复研究面条式代码(spaghetti code),并在1968年给某位编辑写了一篇著名的简信,题为《Go to语句是有害的》之后,计算机科学家Corrado Bohm和Giuseppe Jacopini证明,使用顺序(sequencing),分支/选择(alternation)和循环(iteration)这三种流程结构就足以表达所有程序的本质。C语言作为结构化编程语言的一种,其程序结构,自然也是由这三种最基本的程序结构组成。
顺序执行程序,这个很好理解,一条语句接着一条语句执行就可以了。那么C语言的分支和循环是如何实现的呢?
对于分支,我们常见的关键词是不是有if/else和switch/case两种组合呀。if/else翻译过来,是不是就是“如果…,否则…”,是一个条件判断。如果用伪代码的方式来进行表达的话,一般有这两种形式。
第一种形式如下:
if(条件为真)
{
代码段1;
}
else
{
代码段2;
}
这里else的含义其实就是条件不为真,那么也就是条件为假。
第二种形式是这样的,
if(条件1为真)
{
代码段1;
}
else if(条件2为真)
{
代码段2;
}
else
{
代码段3;
}
第一种形式和第二种形式本质上的区别,其实就是第一个是双分支,第二个是多分支。两种不同的分支,我们要根据具体情况去使用。某种意义上多分支模式可以由双分支演变而来,比如我们可以在双分支模式里面的else里嵌套一个双分支结构就可以了。
if(条件1为真)
{
代码段1;
}
else
{
if(条件2为真)
{
代码段2;
}
else
{
代码段3;
}
}
从这个意义上看,双分支和多分支其实是一回事情,本质上都是分支。分支这个概念,大家应该都是相对容易接受的,任何一件事情总有它的对立面,高对低,胖对瘦,大对小,物质对暗物质等等。分支这个概念应该是反映了事物的一种本源的状态,是描述程序是不可再进行切分的维度,也就是说分支成为了任何程序的三种基本结构之一。
其实对于多分支的情况,C语言有另外一套关键字组合switch/case,写成伪代码的形式,大概是这样的。
switch(变量){
case 常数1:
代码段1;
break;
case 常数2:
代码段2;
break;
case 常数3:
代码段3;
break;
。。。
default:
代码段n;
break;
}
大家注意没,对于switch/case组合来说,它的条件一定要是常量,而且要是整数。这个是不是对判断的条件作出了限制呀。这里的default关键字,是和if/else里面的else对应的,表示意外情况。从表面看,switch/case适用于逻辑条件简单,但是分类较多的情况;if/else适用于判断条件复杂,但是分支较少的情况。但是从另外一个层面看来,switch/case所具备的功能,if/else完成起来,完全没有问题呀。那为什么还要搞出来这一个关键字组合呢?
我个人的理解是switch/case关键字的执行效率,在某些情况下,要比if/else要高。
int a = 0;
switch(a)
{
case -1:
break;
case -2:
break;
case -3:
break;
case -4:
break;
case 0:
printf("I am in switch case!n");
break;
default:
break;
}
printf("I am between the switch case and if/else if/else!n");
if (-1 == a)
{
}
else if (-2 == a)
{
}
else if (-3 == a)
{
}
else if (-4 == a)
{
}
else if (-5 == a)
{
}
else
{
printf("I am in if/else if/else!n");
}
比如对于上面的代码段,对于switch/case关键词对来说,程序是直接跳到case为0的情况下的;但是对于if/else而言,程序则是一句一句比较之后才达到了“else”这一句,程序执行效率的高低是显而易见的。
但是我们说,switch/case的程序执行效率可以比较高,并不是没有条件的。从汇编语言的层面来看,switch/case是建立了一张跳转表,因此需要一定的空间才行。这里某种程度上有以空间换时间的意思。
因此,如果程序可以使用switch/case尽量使用这个,以便提高它的执行效率。其实,我们这样比对一番之后,自己也就轻而易举地牢牢记住了它们,这个可能也就是知其然知其所以然的效果,符合人的记忆规律。
讲完了分支,我们来看一下循环。循环这个基本结构,在C语言里面,一共有两种实现方式,for循环和while循环,其中while循环还可以分为两种,一种是while循环,一种是do/while循环。我们下面分别看一下,这三种结构的程序表达大概是什么模样。
for(循环控制变量初始化;循环终止条件;循环控制变量增量){
循环体;
}
for循环的执行步骤是:首先执行循环变量的初始化,然后执行循环终止条件;如果判断条件为假(不符合终止条件),那么就开始执行循环体;然后执行循环控制变量的增量程序,执行完以后,再去判断是不是符合循环终止条件;如何符合条件,那么就退出循环;如果不符合条件,那么就继续执行循环体,并重复执行上述步骤。
感觉用第二种方式来描述这个循环体的执行过程,更为清晰。第一,先进行循环控制变量初始化;第二,执行循环终止条件,如果判断结果为假,则进入第三步;如果为假则循环终止并退出;第三,执行循环体;第四,执行循环控制变量增量,转入第二步;
注意,其实for循环括号中的三部分其实都可以省略,如果全部省略了,就变成了一个无限循环的死循环,跳不出来了。无限循环在操作系统中使用的是非常多的,每一个任务都是一个无限循环体,包括main函数也是一个无限循环体。
下面,我们来看一下while循环的代码格式。
循环控制变量初始化;
while(循环终止条件){
循环体;
循环控制变量增量;
}
while循环的执行步骤可以描述为如下的样子。
第一在while之前,先执行循环控制变量的初始化;第二,判断循环终止条件是否成立,如果成立那么就跳出循环,如果不成立就进入第三步;第三步,执行循环体;第四步,执行循环控制变量增量,然后进入第二步。
do/while的代码格式是什么样子呢?
循环控制变量初始化;
do{
循环体;
循环控制变量增量;
}while(循环终止条件)
while循环的执行步骤可以描述为如下的样子。第一,在do(也就是执行)之前,先执行循环控制变量的初始化;第二,先执行一次循环体和循环控制变量的增量;第三步,判断循环终止条件是否成立,如果成立那么就跳出循环,如果不成立就进入第二步;
我们来看一下呀,while和do/while主要的区别,一个是先判断再执行,一个是先执行再判断,那么也就是说do/while这个关键字组合中,函数体至少执行一次。但是这并不影响while和do/while之间的转化呀,同样的功能绝大多数情况下,可以用while也可以使用do/while来实现。那么有没有必须要用do/while的时候呢?答案是有的,在linux编程中,do/while常用的一个方式是do while(0)。
比如,我需要定义一个宏:
#define SAFE_FREE(p) do{free(p); p=NULL} while(0)
假设这里去掉do{....} while(0),及定义为:
#define SAFE_FREE(p) free(p); p=NULL;
那么以下代码:
If(NULL!=p)
SAFE_FREE(p)
else
.......
会被展开成如下的有仇
If(NULL!=p)
free(p); p=NULL;
else
.......
展开之后,存在两个问题。因为if分支后面有两个语句,会导致else分支没有对应的if,编译失败。即使假设没有else分支,SAFE_FREE中的第二句,无论if测试是否通过,这个都会被执行。那么如何解决上述问题呢?有人说给SAFE_FREE的定义加上{}就可以了,比如如下的样子:
#define SAFE_FREE(p) { free(p); p=NULL; }
代码展开如下:
If(NULL!=p)
{ free(p); p=NULL; };
else
.......
这样问题又来了,else 没有对应的if了,编译还是会失败。但是使用了do while(0)则可以完美解决上述问题。代码展开如下,
If(NULL!=p)
do { free(p); p=NULL; } while(0);
else
.......
所以,do while(0)的使用是为了保证宏定义的使用者能无编译错误的使用宏。
下面我们讨论一下,为什么C语言有了for,还需要while呢?除了上面的do while(0)的必要性之外,好像还真得没有确切的其他原因。比如在golang编译其中,就只有for,没有while了;再比如,java的源代码里面,就是有一堆的for(;;),据说可以提高性能。所以更大程度上讲,C语言只是为了,兼容程序员的编码习惯,保留了while这个关键字而已。
一些更现代的语言还加入了foreach、for in这种专门遍历集合的语法糖,也有python、ruby这种直接抛弃了C语言三段式for,只保留了for in,而把非遍历型的循环统统放到while循环里面的做法。
我们这次关于程序基本结构的探讨就先到这里。
参考资料:
① 基础学习C语言第四章:三种基本结构 https://zhuanlan.zhihu.com/p/97629275
② Linux - switch/case与if/else if/else的效率比较
http://blog.sina.com.cn/s/blog_5acb430f0100ael9.html
https://www.cnblogs.com/anlia/p/11685639.html
④ 深度理解do{} while(0)
https://blog.csdn.net/weibo1230123/article/details/81904498
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表乌云踏雪网立场。
文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。