打印
[51单片机]

从单片机基础到程序框架(连载)

[复制链接]
楼主: jianhong_wu
手机看帖
扫描二维码
随时随地手机跟帖
221
持续关注,LZ 辛苦

使用特权

评论回复
222
jianhong_wu|  楼主 | 2017-1-23 14:11 | 只看该作者
第五十四节: 从全局变量和局部变量中感悟“栈”为何物。
第五十四节_pdf文件.pdf (104.82 KB)
【54.1   本节阅读前的名词约定。】

       变量可以粗略的分成两类,一类是全局变量,一类是局部变量。如果更深一步精细划分,全局变量还可以分成“普通全局变量”和“静态全局变量”,局部变量也可以分成“普通局部变量”和“静态局部变量”,也就是说,若精细划分,可以分成四类。其中“静态全局变量”和“静态局部变量”多了一个前缀“静态”,这个前缀“静态”是因为在普通的变量前面多加了一个修饰关键词“static”,这部分的内容后续章节会讲到。本节重点为了让大家理解内存模型的“栈”,暂时不考虑“静态变量”的情况,人为约定,本节所涉及的“全局变量”仅仅默认为“普通全局变量”,“局部变量”仅仅默认为“普通局部变量”。

【54.2   如何判定全局变量和局部变量?】

       全局变量就是在函数外面定义的变量,局部变量就是在函数内部定义的变量,这是最直观的判定方法。下面的例子能很清晰地说明全局变量和局部变量的判定方法:

unsigned char a;     //在函数外面定义的,所以是全局变量。
void main()  //主函数
{
    unsigned char b; //在函数内部定义的,所以是局部变量。
    b=a;
    while(1)
    {

    }
}

【54.3   全局变量和局部变量的内存模型。】

       单片机内存包括ROM和RAM两部分,ROM存储的是单片机程序中的指令和一些不可更改的常量数据,而RAM存放的是可以被更改的变量数据,也就是说,全局变量和局部变量都是存放在RAM,但是,虽然都是存放在RAM,全局变量和局部变量之间的内存模型还是有明显的区别的,因此,分了两个不同的RAM区,全局变量占用的RAM区称为“全局数据区”,局部变量占用的RAM区称为“栈”,因为我后面会用宾馆来比喻“栈”,为了方便**,大家可以把“栈”想象成 “客栈”来**。它们的内存模型到底有什么本质的区别呢?“全局数据区”就像你自己家的房间,是唯一的,一个房间的地址只能你一个人住(假设你还没结婚的时候),而且是永久的,所以说每个全局变量都有唯一对应的RAM地址,不可能重复的。而“栈”就像宾馆客栈,一年下来每天晚上住的人不一样,每个人在里面居住的时间是有期限的,不是长久的,一个房间的地址一年下来每天可能住进不同的人,不是唯一的。“全局数据区”的全局变量拥有永久产权,“栈”区的局部变量只能临时居住在宾馆客栈,地址不是唯一的,有期限的。全局变量像私人区,局部变量像公共区。“栈”的这片公共区,是给程序里所有函数内部的局部变量共用的,函数被调用的时候,该函数内部的每个局部变量就会被分配对应到“栈”的某个RAM地址,函数调用结束后,该局部变量就失效,因此它对应的“栈”的RAM空间就被收回以便给下一个被调用的函数的局部变量占用。请看下面这个例子,我借用“宾馆客栈”来比喻局部变量所在的“栈”。
void HanShu(void);   //子函数的声明
void HanShu(void)    //子函数的定义
{
    unsigned char a;   //局部变量
    a=1;
}
void main() //主函数
{
    HanShu() ;      //子函数的调用
}


      分析:上述例子,单片机从主函数main往下执行,首先遇到HanShu子函数的调用,所以就跳到HanShu函数的定义那里开始执行,此时的局部变量a开始被分配在RAM的“栈区”的某个地址,相当于你入住宾馆被分配到某个房间。单片机执行完子函数HanShu后,局部变量a在RAM的“栈区”所分配的地址被收回,局部变量a消失,被收回的RAM地址可能会被系统重新分配给其它被调用的函数的局部变量,此时相当于你离开宾馆,从此你跟那个宾馆的房间没有啥关系,你原来在宾馆入住的那个房间会被宾馆老板重新分配给其他的客人入住。全局变量的作用域是永久性不受范围限制的,而局部变量的作用域就是它所在函数的内部范围。全局变量的“全局数据区”是永久的私人房子(这里的“永久”仅仅是举一个例子,别拿“70年产权”来抬杠),局部变量的“栈”是临时居住的“客栈”。重要的事情说两遍,再次总结如下:
    (1)每定义一个新的全局变量,就意味着多开销一个新的RAM内存。而每定义一个局部变量,只要在函数内部所定义的局部变量总数不超过单片机的“栈”区,此时的局部变量不开销新的RAM内存,因为局部变量是临时借用“栈”区的,使用后就还给“栈”,“栈”是公共区,可以重复利用,可以服务若干个不同的函数内部的局部变量。
    (2)单片机每次进入执行函数时,局部变量都会被初始化改变,而全局变量则不会被初始化,全局变量是一直保存之前最后一次更改的值。

【54.4   三个常见疑问。】

       第一个疑问:
       问:“全局数据区”和“栈区“是谁在幕后分配的,怎么分配的?
       答:是C编译器自动分配的,至于怎么分配,谁分配多一点,谁分配少一点,C编译器会有一个默认的比例分配,我们一般都不用管。

       第二个疑问:
       问:“栈”区是临时借用的,子函数被调用的时候,它内部的局部变量才会“临时”被分配到“栈”区的某个地址,那么问题来了,谁在幕后主持“栈区”这些分配的工作,难道也是C编译器?C编译器不是在编译程序的时候一次性就做完了编译工作然后就退出历史舞台了吗?难道我们程序已经在单片机内部运转的时候,编译器此时还在幕后指手画脚的起作用?
       答:单片机已经上电开始运行程序的时候,编译器是不可能起作用的。所以,真相只有一个,“栈区”分配给函数内部局部变量的工作,确实是C编译器做的,唯一需要注意的地方是,它不是“现炒现卖”,而是在单片机上电前,C编译器就把所有函数内部的局部变量的分配工作就规划好了,都指定了如果某个函数一旦被调用,该函数内部的哪个局部变量应该分到“栈区”的哪个地址,C编译器都是事先把这些“后事”都交代完毕了才“结束自己的生命”,后面,等单片机上电开始工作的时候,虽然C编译器此时“不在”了,但是单片机都是严格按照C编译器交代的“遗嘱”开始工作和分配“栈区”的。因此,“栈区”的“临时分配”非真正严格意义上的“临时分配”。

        第三个疑问:
        问:函数内部所定义的局部变量总数不超过单片机的“栈”区的RAM数量,那,万一超过了“栈”区的RAM数量,后果严重吗?
        答:后果特别严重。这种情况,专业术语叫“爆栈”。程序会出现异常,而且是莫名其妙的异常。为了避免这种情况,一般在编写程序的时候,函数内部都不能定义大数组的局部变量,局部变量的数量不能定义太多太大,尤其要避免刚才所说的定义开辟大数组局部变量这种情况。大数组的定义应该定义成全局变量,或者定义成“静态的局部变量”(“静态”这部分相关的内容后面章节会讲到)。有一些C编译器,遇到“爆栈”的情况,会好心跟你提醒让你编译不过去,但是也有一些C编译器可能就不会给你提醒,所以大家以后做项目写函数的时候,要对“爆栈”心存敬畏。

【54.5   全局变量和局部变量的优先级。】

        刚才说到,全局变量的作用域是永久性并且不受范围限制的,而局部变量的作用域就是它所在函数的内部范围,那么问题来,假如局部变量和全局变量的名字重名了,此时函数内部执行的变量到底是局部变量还是全局变量?这个问题就涉及到优先级。注意,当面对同名的局部变量和全局变量时,函数内部执行的变量是局部变量,也就是局部变量在函数内部要比全局变量的优先级高。为了深刻理解“全局变量和局部变量的优先级”,强烈建议大家必须仔细看完下面列举的三个练习例子。

【54.6   例程练习和分析。】

        请看下面第一个例子:

/*---C语言学习区域的开始。-----------------------------------------------*/

unsigned char a=5;      //此处第1个a是全局变量。

    void main() //主函数
{
    unsigned char a=2;  //此处第2个a是局部变量。跟上面全局变量的第1个a重名了!

    View(a);  //把a发送到电脑端的串口助手软件上观察。
    while(1)  
    {

    }
}
/*---C语言学习区域的结束。-----------------------------------------------*/


分析:
      上述例子,有2个变量重名了!其中一个是全局变量,另外一个是局部变量。此时输出显示的结果是5还是2?正确的答案是2。因为在函数内部,函数内部的局部变量比全局变量的优先级更加高。此时View(a)是第2个局部变量的a,而不是第1个全局变量的a。虽然这里的两个a重名了,但是它们的内存模型不一样,第1个全局变量的a是分配在“全局数据区”是具有唯一的地址的,而第2个局部变量的a是被分配在临时的“栈”区的,寄生在main函数内部。


       再看下面第二个例子:

/*---C语言学习区域的开始。-----------------------------------------------*/
void HanShu(void); //函数声明
unsigned char a=5;      //此处第1个a是全局变量。
void HanShu(void)   //函数定义
{
    unsigned char a=3;  //此处第2个a是局部变量。
}
    void main() //主函数
{
    unsigned char a=2;  //此处第3个a也是局部变量。
    HanShu();  //子函数被调用
    View(a);  //把a发送到电脑端的串口助手软件上观察。
    while(1)  
    {

    }
}
/*---C语言学习区域的结束。-----------------------------------------------*/


分析:
      上述例子,有3个变量重名了!其中一个是全局变量,另外两个是局部变量。此时输出显示的结果是5还是3还是2?正确的答案是2。因为,HanShu这个子函数是被调用结束之后,才执行View(a)的,就意味HanShu函数内部的局部变量(第2个局部变量a)是在执行View(a)语句的时候就消亡不存在了,所以此时View(a)的a是第3个局部变量的a(在main函数内部定义的局部变量的a)。


       再看下面第三个例子:

/*---C语言学习区域的开始。-----------------------------------------------*/
void HanShu(void); //函数声明
unsigned char a=5;      //此处第1个a是全局变量。
void HanShu(void)   //函数定义
{
    unsigned char a=3;  //此处第2个a是局部变量。
}
    void main() //主函数
{
    HanShu();  //子函数被调用
    View(a);  //把a发送到电脑端的串口助手软件上观察。
    while(1)  
    {

    }
}
/*---C语言学习区域的结束。-----------------------------------------------*/


分析:
       上述例子,有2个变量重名了!其中一个是全局变量,另外一个是局部变量。此时输出显示的结果是5还是3?正确的答案是5。因为,HanShu这个子函数是被调用结束之后,才执行View(a)的,就意味HanShu函数内部的局部变量(第2个局部变量)是在执行View(a)语句的时候就消亡不存在了,同时,因为此时main函数内部也没有定义a的局部变量,所以此时View(a)的a是必然只能是第1个全局变量的a(在main函数外面定义的全局变量的a)。

【54.7   如何在单片机上练习本章节C语言程序?】

       直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
评论
qgbgzp 2024-5-20 14:18 回复TA
感谢无私奉献,学到很多 
zhangtao3b608 2021-5-15 16:37 回复TA
超赞!! 
223
21ID| | 2017-1-23 17:03 | 只看该作者
必须为楼主点赞

使用特权

评论回复
224
liubeihua| | 2017-1-24 00:03 | 只看该作者
要与时俱进现在已是keil5v9.56了,到2020年不知keil? mdk与C51区别就是新建一个工程时选51还是选arm的区别,不存在兼容的问题

使用特权

评论回复
225
jianhong_wu|  楼主 | 2017-1-24 16:49 | 只看该作者
第五十五节: 函数的作用和四种常见书写类型。
第五十五节_pdf文件.pdf (84.39 KB)
【55.1   函数和变量的命名规则。】

       函数的名字和变量的名字一样,一般是由“字母,数字,下划线”三者组成。第1个字符不能是数字,必须是字母或者下划线“_”,后面紧跟的第2个字符开始可以是数字。在C语言中名字所用的字母是区分大小写的。可以用下划线“_”,但是不可以用横杠“-”。名字不能跟C编译系统已经征用的关键字重名,比如不能用“unsigned ”,“char”,“static”等系统关键词,跟古代时不能跟皇帝重名一样,要避尊者讳。

【55.2   函数的作用和分类。】

       函数的作用。通常把一些可能反复用到的算法或者过程封装成一个函数,函数就是一个模块,给它输入特定的参数,就可以输出想要的结果,比如一个加法函数,只要输入加数和被加数,然后就会输出相加结果之和,里面具体的算法过程只要写一次就可以重复调用,极大的节省单片机程序容量,也节省程序开发人员的工作量。还有一类函数,它从封装上看无所谓“输入输出”,这类函数往往是针对某一种可能重复使用的“过程”。
       函数的分类。暂时排除指针的情况下(指针的内容后续章节会讲到),从输入输出的角度来看,有四种常见的书写类型。分别是“无输出无输入,无输出有输入,有输出无输入,有输出有输入”。“输出”是看函数名的前缀,前缀如果是void表示“无输出”,否则就是“有输出”。“输入”是看函数名括号里的内容,如果是void或者是空着就表示“无输入”,否则就是“有输入”。“输出”和“输入”是比较通俗的说法,专业一点的说法是,“有输出”表示函数“有返回”,“无输出”表示函数“无返回”。“有输入”表示函数“有形参”,“无输入”表示函数“无形参”。下面举一个加法函数的例子,分别用四种不同的函数类型来实现,通过对比它们之间的差别,来体会它们在书写方面有哪些不同,又有哪些规律。

【55.3   第1类:“无输出”“无输入”的函数。】

    unsigned char a;  //此变量用来接收最后相加结果的和。
unsigned char g=2;
unsigned char h=3;
void HanShu(void)  //“无输出”“无输入”函数的定义。
{
   a=g+h;
}
main()
{
    HanShu();     //函数的调用。此处括号内的形参void要省略,否则编译不通过。
}


      分析:void HanShu(void),此函数名的前缀是void,括号内也是void,属于“无输出”“无输入”函数。这类函数表面看是“无输出”“无输入”,其实内部是通过全局变量来输入输出的,比如上面的例子就是靠a,g,h这三个全局变量来传递信息,只不过这类表达方式比较隐蔽,没有那么直观。

【55.4   第2类:“无输出”“有输入”的函数。】
unsigned char b;  //此变量用来接收最后相加结果的和。
void HanShu(unsigned char i,unsigned char k)   //“无输出”“有输入”函数的定义。
{
   b=i+k;
}
main()
{
    HanShu(2,3);  //函数的调用。
}


       分析:void HanShu(unsigned char i,unsigned char k),此函数名的前缀是void,括号内是(unsigned char i,unsigned char k),属于“无输出”“有输入”的函数。括号的两个变量i和k是函数内的局部变量,也是跟对外的桥梁接口,它们有一个专业的名称叫形参。外部要调用此函数时,只要给括号填入对应的变量或者数值,这些变量和数值就会被复制一份传递给作为函数形参的局部变量(比如本例子中的i和k),从而外部调用者跟函数内部就发生了数据信息的传递。这种书写方式的特点是把输入接口封装了出来。

【55.5   第3类:“有输出”“无输入”的函数。】

unsigned char c;   //此变量用来接收最后相加结果的和。
unsigned char m=2;
unsigned char n=3;
unsigned char HanShu(void)     //“有输出”“无输入”函数的定义。
{
   unsigned char p;
   p=m+n;
   return p;
}
main()
{
    c=HanShu();  //函数的调用。此处括号内的形参void要省略,否则编译不通过。
}


       分析:unsigned char HanShu(void),此函数名的前缀是unsigned char类型,括号内是void,属于“有输出”“无输入”的函数。函数前缀的unsigned char表示此函数最后退出时会返回一个unsigned char类型的数据给外部调用者。而且这类函数内部必须有一个return语句配套,表示立即退出当前函数并且返回某个变量或者常量的数值给外部调用者。这种书写方式的特点是把输出接口封装了出来。

【55.6   第4类:“有输出”“有输入”的函数。】

unsigned char d;    //此变量用来接收最后相加结果的和。
unsigned char HanShu(unsigned char r,unsigned char s)     //“有输出”“有输入”函数的定义
{
    unsigned char t;
    t=r+s;
    return t;
}
main()
{
    d=HanShu(2,3);  //函数的调用。
}

       分析:unsigned char HanShu(unsigned char r,unsigned char s),此函数名的前缀是unsigned char类型,括号内是(unsigned char r,unsigned char s),属于“有输出”“有输入”的函数。输入输出的特点跟前面介绍的函数一样,不多讲。这种书写方式的特点是把输出和输入接口都封装了出来。

【55.7   函数在被“调用”时需要注意的地方。】

        函数的三要素是“声明,定义,调用”。函数在被“调用”的时候,对于“无输入”的函数,形参的void关键词要省略,否则编译不通过,这里仅仅是指在函数在被“调用”的时候。

【55.8   例程练习和分析。】

        现在编写一个练习程序,要求编写4个不同“输入输出”封装的函数,它们每个函数所实现的功能都是一样的,都是加法的算法函数,它们之间仅仅是外观的封装接口不同而已。

/*---C语言学习区域的开始。-----------------------------------------------*/

void hanshu_1(void);  
void hanshu_2(unsigned char i,unsigned char k);   
unsigned char hanshu_3(void);     
unsigned char hanshu_4(unsigned char r,unsigned char s);  

unsigned char a;    //此变量用来接收第1个函数最后相加结果的和。
unsigned char g=2;
unsigned char h=3;

unsigned char b;    //此变量用来接收第2个函数最后相加结果的和。

unsigned char c;    //此变量用来接收第3个函数最后相加结果的和。
unsigned char m=2;
unsigned char n=3;

unsigned char d;    //此变量用来接收第4个函数最后相加结果的和。

void hanshu_1(void)  //第1类:“无输出”“无输入”。
{
   a=g+h;
}
void hanshu_2(unsigned char i,unsigned char k)  //第2类:“无输出”“有输入”。
{
   b=i+k;
}

unsigned char hanshu_3(void)   //第3类:“有输出”“无输入”。
{
   unsigned char p;
   p=m+n;
   return p;
}

unsigned char hanshu_4(unsigned char r,unsigned char s)  //第4类:“有输出”“有输入”。
{
   unsigned char t;
   t=r+s;
   return t;
}

    void main() //主函数
{
    hanshu_1();       //第1类:“无输出”“无输入”的函数调用。这里的形参的void要省略。
    hanshu_2(2,3);    //第2类:“无输出”“有输入”的函数调用。
    c=hanshu_3();     //第3类:“有输出”“无输入”的函数调用。这里的形参的void要省略。
    d=hanshu_4(2,3);  //第4类:“有输出”“有输入”的函数调用。
    View(a);  //把a发送到电脑端的串口助手软件上观察。
    View(b);  //把b发送到电脑端的串口助手软件上观察。
    View(c);  //把c发送到电脑端的串口助手软件上观察。
    View(d);  //把d发送到电脑端的串口助手软件上观察。
    while(1)  
    {
    }
}
/*---C语言学习区域的结束。-----------------------------------------------*/


       在电脑串口助手软件上观察到的程序执行现象如下:

开始...

第1个数
十进制:5
十六进制:5
二进制:101

第2个数
十进制:5
十六进制:5
二进制:101

第3个数
十进制:5
十六进制:5
二进制:101

第4个数
十进制:5
十六进制:5
二进制:101


分析:
       变量a为5。
       变量b为5。
       变量c为5。
       变量d为5。

【55.9   如何在单片机上练习本章节C语言程序?】

       直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
226
nwjjdwx| | 2017-1-24 21:28 | 只看该作者
我也来学习,感谢楼主

使用特权

评论回复
227
炽天使11233| | 2017-2-5 10:10 | 只看该作者
吴老师,新年好

使用特权

评论回复
228
静默的羔羊| | 2017-2-5 16:49 | 只看该作者
怎么收藏啊

使用特权

评论回复
229
sgotte| | 2017-2-6 09:47 | 只看该作者
新手很实用

使用特权

评论回复
230
yixiaoxiao| | 2017-2-7 11:25 | 只看该作者
讲解很细致,很清晰,很亲和,赞

使用特权

评论回复
231
rdong| | 2017-2-10 16:44 | 只看该作者
支持一下,

使用特权

评论回复
232
jianhong_wu|  楼主 | 2017-2-12 10:09 | 只看该作者
第五十六节: return在函数中的作用以及四个容易被忽略的功能。
第五十六节_pdf文件.pdf (80.23 KB)
【56.1   return深入讲解。】

       return在英语单词中有“返回”的意思,上一节提到,凡是“有输出”的函数,函数内部必须有一个“return+变量或者常量”与之配套,表示返回的结果给外部调用者接收,这个知识点很容易理解,但是容易被忽略的是另外四个功能:
       第一个是return语句隐含了立即退出的功能。退出哪?退出当前函数。只要执行到return语句,就马上退出当前函数。即使return语句身陷多层while或者for的循环中,它也毫不犹豫立即退出当前函数。
       第二个是return语句可以出现在函数内的任何位置。可以出现在第一行代码,也可以出现在中间的某行代码,也可以出现在最后一行的代码,它的位置不受限制。很多初学者有个错觉,以为return只能出现在最后一行,这是错的。
       第三个是return语句不仅仅可以用在“有输出”的函数,也可以用在“无输出”的函数,也就是可以用在前缀是void的函数里。回顾上一节,在“有输出”的函数里,return后面紧跟一个变量或者常量,表示返回的数,但是在“无输出”的函数里,因为是“无输出”,此时return后面不用跟任何变量或者常量,这种写法也是合法的,表示返回的是空的。此时return主要起到立即退出当前函数的作用。
       第四个是return语句可以在一个函数里出现N多次,次数不受限制,不一定必须只能一次。不管一个函数内有多少个return语句,只要任何一个return语句被单片机执行到,就立即退出当前函数。

【56.2   中途立即退出的功能。】

       下面的书写格式是合法的:

void HanShu(void)  //“无输出”函数的定义。
{
        语句1;
        return; //立即退出当前函数。对于这类“无输出”函数,return后面没有跟任何变量或者常量。
        语句2;
        return; //立即退出当前函数。对于这类“无输出”函数,return后面没有跟任何变量或者常量。
        语句3;
        return; //立即退出当前函数。对于这类“无输出”函数,return后面没有跟任何变量或者常量。

}


       分析:当HanShu此函数被调用时,单片机从“语句1”往下执行,当遇到第一个return语句后,马上退出当前函数。后面的“语句2”和“语句3”等代码永远不会被执行到。多说一句,大家仔细看看return后面跟了什么数没有?什么都没有。因为此函数的前缀是void的,是“无输出”的。

【56.3   身陷多层while或者for的循环时的惊人表现。】

      下面的书写格式是合法的:

void HanShu(void)  //“无输出”函数的定义。
{
    语句1;
    while(1)  //第一个循环
    {
        while(1)  //第二个循环中的循环
       {
          return; //立即退出当前函数。
       }
       语句2;
       return; //立即退出当前函数。
    }
    语句3;
    return; //立即退出当前函数。
}


       分析:当HanShu此函数被调用时,单片机从“语句1”往下执行,先进入第一个循环,接着进入第二个循环中的循环,然后遇到第一个return语句,于是马上退出当前函数。后面的“语句2”和“语句3”等代码永远不会被执行到。此函数中,虽然表面看起来有那么多可怕的循环约束着,但是一旦碰上return语句都是浮云,立刻退出当前函数。

【56.4   在“有输出”函数里的书写格式。】

        把上面例子中“无输出”改成“有输出”的函数后:

unsigned char HanShu(void)  //“有输出”函数的定义。
{
    unsigned char a=9;
    语句1;
    while(1)  //第一个循环
    {
        while(1)  //第二个循环中的循环
        {
          return a; //返回a变量的值,并且立即退出当前函数。
        }
        语句2;
         return a; //返回a变量的值,并且立即退出当前函数。
    }
    语句3;
    return a; //返回a变量的值,并且立即退出当前函数。
}


       分析:因为此函数是“有输出”的函数,所以return语句后面必须配套一个变量或者常量,此例子中配套的是a变量。当HanShu函数被调用时,单片机从“语句1”往下执行,先进入第一个循环,接着进入第二个循环中的循环,然后遇到第一个“return a”语句,马上退出当前函数。而后面的“语句2”和“语句3”等代码是永远不会被执行到的。再一次说明了,return语句不仅有返回某数的功能,还有立即退出的重要功能。

【56.5   项目中往往是跟if语句搭配使用。】

       前面的例子只是为了解释return语句的执行顺序和功能,实际项目中,如果中间有多个return语句,中间的return语句不可能像前面的例子那样单独使用,它往往是跟if语句一起搭配使用,否则单独用return就没有什么意义。比如:

void HanShu(void)  //“无输出”函数的定义。
{
    语句1;
    if(某条件满足)
    {
       return; //立即退出当前函数。
    }
    语句2;
    if(某条件满足)
    {
       return; //立即退出当前函数。
    }
    语句3;
}


       分析:单片机从“语句1”开始往下执行,至于在哪个“return”语句处退出当前函数,就要看哪个if的条件满不满足了,如果所有的if的条件都不满足,此函数会一直执行完最后的“语句3”才退出当前函数。

【56.6   例程练习和分析。】

        写一个简单的除法函数,在除法运算中,除数不能为0,如果发现除数为0,就立即退出当前函数,并且返回运算结果默认为0。

/*---C语言学习区域的开始。-----------------------------------------------*/

//函数的声明。
unsigned int ChuFa(unsigned int BeiChuShu,unsigned int ChuShu);

//变量的定义。
unsigned int a;//此变量用来接收除法的运算结果。
unsigned int b;//此变量用来接收除法的运算结果。

//函数的定义。
unsigned int ChuFa(unsigned int BeiChuShu,unsigned int ChuShu)
{
    unsigned int Shang;  //返回的除法运算结果:商。
    if(0==ChuShu)   //如果除数等于0,就立即退出当前函数,并返回0
    {
        return 0; // 退出当前函数并且返回0.此时后面的代码不会被执行。
    }

    Shang=BeiChuShu/ChuShu;  //除法运算的算法
    return Shang;  //返回最后的运算结果:商。并且退出当前函数。
}

    void main() //主函数
{
    a=ChuFa(128,0);  //函数调用。128除以0,把商返回给a变量。
    b=ChuFa(128,2);  //函数调用。128除以2,把商返回给b变量。

    View(a);  //把a发送到电脑端的串口助手软件上观察。
    View(b);  //把b发送到电脑端的串口助手软件上观察。
    while(1)  
    {
    }
}
/*---C语言学习区域的结束。-----------------------------------------------*/


        在电脑串口助手软件上观察到的程序执行现象如下:

开始...

第1个数
十进制:0
十六进制:0
二进制:0

第2个数
十进制:64
十六进制:40
二进制:1000000


分析:
       变量a为0。
       变量b为64。

【56.7   如何在单片机上练习本章节C语言程序?】

       直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
评论
791314247 2021-1-18 23:22 回复TA
return只用在末尾的,不是初学者就是大师 
233
xianglin| | 2017-2-12 20:49 | 只看该作者
顶。 ding

使用特权

评论回复
234
taizai1| | 2017-2-14 19:42 | 只看该作者
jianhong_wu 发表于 2016-3-27 23:10
第十一节:一个在单片机上练习C语言的模板程序。

可以提问个问题吗
发现你重新更新了源文件
我之前下载的是老的源文件,发现第N个数老是显示不正常,不管是软件仿真还是硬件都不正确,如图 思来想去,最多把十进制的数显示调好,如图
后面我把汉字,换成字符abc这些就没有问题,汉字始终不行,这是什么原因啊,昨天想了一晚没想明白,

使用特权

评论回复
235
jianhong_wu|  楼主 | 2017-2-15 07:30 | 只看该作者
本帖最后由 jianhong_wu 于 2017-2-15 07:35 编辑

某些版本的C51编译器本身的漏洞。它对一部分汉字不能自动正确的翻译成对应的汉字机内码,“数”这个字恰好在某些版本的C51里无法翻译。至于哪个版本的最完美的,我也不清楚。有的版本可能支持“数”这个字,但是未必支持其它某些汉字。大家以后用C51编译器的时候,要有这个心理准备。如果自己在数组里先把汉字转成机内码,就可避免这个问题。

使用特权

评论回复
236
taizai1| | 2017-2-17 15:47 | 只看该作者
jianhong_wu 发表于 2017-2-15 07:30
某些版本的C51编译器本身的漏洞。它对一部分汉字不能自动正确的翻译成对应的汉字机内码,“数”这个字恰好 ...

明白

使用特权

评论回复
237
stackdog| | 2017-2-18 10:15 | 只看该作者
666

使用特权

评论回复
238
batiafu| | 2017-2-18 14:52 | 只看该作者
佩服楼主

使用特权

评论回复
239
jianhong_wu|  楼主 | 2017-2-19 10:10 | 只看该作者
第五十七节: static的重要作用。
第五十七节_pdf文件.pdf (91.71 KB)
【57.1   变量前加入static后发生的“化学反应”。】

       有两类变量,一类是全局变量,一类是局部变量。定义时,在任何一类变量前面加入static关键词,变量原有的特性都会发生某些变化,因此,static像化学的催化剂,具有神奇的功能。加static关键词的书写格式如下:
    static unsigned char a;       //这是在全局变量前加的static关键词
void HanShu(void)
{
    static unsigned char i;   //这是在局部变量前加的static关键词
    }


【57.2   在全局变量前加static。】

       static读作“静态”,全局变量前加static,称为静态全局变量。静态全局变量和普通全局变量的功能大体相同,仅在有效范围(作用域)方面有差异。假设整个工程有多个文件组成,普通全局变量的有效范围能覆盖全部文件,在任何一个文件里,以及跨文件与文件之间,在传递信息的层面上都畅通无阻。而静态全局变量只能在当前定义的那个文件里起作用,活动范围完全被限定在一个文件,彷佛被加了紧箍咒,由不得你任性,在传递信息的层面上仅仅局限于定义变量时所在的那一个文件。这部分的内容有个大致印象就可以,暂时不用深入研究,等以后学到“多文件编程”时再关注,因为我当前的程序例子只有一个源文件,还没涉及“多文件编程”。

【57.3   在局部变量前加static。】

      这是本节重点。我常把局部变量比喻宾馆的客房,客人入住时被分配在哪间客房是随机临时安排的,第二天退房时宾馆会把客房收回继续分配给下一位其他的客人,是临时公共区。而加入static后的局部变量,发生了哪些变化?加入static后的局部变量,称为静态局部变量。静态局部变量就像宾馆的VIP客户,VIP客户财大气粗,把宾馆分配的客房永远包了下来,永远不许再给其它客人入住。总结了静态局部变量的两个重要特性:
      第一个,静态局部变量不会在函数调用时被初始化,它只在单片机刚上电时被初始化了一次,因为它的内存模型不是分配在“栈”,而是跟全局变量一样放在“全局数据区”,拥有自己唯一的地址。因此,静态局部变量的数值跟全局变量一样,具有“**”功能,你每次调用某个函数,函数内部的静态局部变量的数值是维持最后一次被更改的数值,不会被“清零”的。但是跟全局变量又有差别,全局变量的有效范围(作用域)是整个工程,而静态局部变量毕竟是“局部”,在传递信息的层面仅局限于当前函数内。而普通局部变量,众所周知,每次被函数调用时,都会被重新初始化,会被“清零”的,没有“**”功能的。
      第二个,每次函数调用时,静态局部变量比普通局部变量少开销一条潜在的“初始化语句”,原因是普通局部变量每次被函数调用时都要重新初始化,而静态局部变量不用进行这个操作。也就是说,静态局部变量比普通局部变量的效率高一点,虽然这个“点”的时间开销微不足道,但是写程序时不能忽略这个“点”。静态局部变量用到好处之时,能体现一个工程师的功力。

【57.4   静态局部变量的应用场合。】

       静态局部变量适用在那些“频繁调用”的函数,比如main函数主循环while(1)里直接调用的所有函数,还有以后讲到的定时器中断函数,等等。因为静态局部变量每次被调用都不会被重新初始化,用在这类函数时就省去了每次初始化语句的时间。还有一类用途,就是那些规定不能被函数初始化的场合,比如在很多用switch搭建程序框架的函数里,这类switch程序框架俗称为状态机思路。

【57.5   能用全局变量替代静态局部变量吗?】

       能用全局变量替代静态局部变量吗?能。哪怕在整个程序里全部用全局变量都可以。全局变量是一把牛刀,什么场合都用牛刀虽然也能解决问题,但是显得鲁莽没有条理。尽量把全局变量,普通局部变量,静态局部变量各自优势充分发挥出来才是编程之道。能用局部变量的尽量用局部变量,这样可以减少全局变量的使用。当局部变量帮分担一部分工作时,最后全局变量只起到一个作用,那就是在各函数之间传递信息。局部变量与全局变量的分工定位明确了,程序代码阅读起来就没有那么凌乱,思路也清晰很多。

【57.6   例程练习和分析。】

       现在编写一个程序来熟悉static的性能。

/*---C语言学习区域的开始。-----------------------------------------------*/

//函数的声明。
unsigned char HanShu(void);        
unsigned char HanShu_static(void);  

//变量的定义。
unsigned char a; //用来接收函数返回的结果。
unsigned char b;
unsigned char c;
unsigned char d;
unsigned char e;
unsigned char f;

//函数的定义。
unsigned char HanShu(void)  
{
   unsigned char i=0;   //普通局部变量,每次函数调用都被初始化为0.
   i++;  //i自加1
   return i;
}

unsigned char HanShu_static(void)  
{
   static unsigned char i=0;   //静态局部变量,只在上电是此初始化语句才起作用。
   i++;  //i自加1
   return i;
}

    void main() //主函数
{
    //下面函数内的i是普通局部变量,每次调用都会被重新初始化。
    a=HanShu();  //函数内的i每次重新初始化为0,再自加1,所以a等于1。
    b=HanShu();  //函数内的i每次重新初始化为0,再自加1,所以b等于1。
    c=HanShu();  //函数内的i每次重新初始化为0,再自加1,所以c等于1。

    //下面函数内的i是静态局部变量,第一次上电后默认为0,就不会再被初始化,
    d=HanShu_static(); //d由0自加1后等于1。
    e=HanShu_static(); //e由1自加1后等于2。
    f=HanShu_static(); //f由2自加1后等于3。

    View(a);  //把第1个数a发送到电脑端的串口助手软件上观察。
    View(b);  //把第2个数b发送到电脑端的串口助手软件上观察。
    View(c);  //把第3个数c发送到电脑端的串口助手软件上观察。
    View(d);  //把第4个数d发送到电脑端的串口助手软件上观察。
    View(e);  //把第5个数e发送到电脑端的串口助手软件上观察。
    View(f);  //把第6个数f发送到电脑端的串口助手软件上观察。

    while(1)  
    {
    }
}
/*---C语言学习区域的结束。-----------------------------------------------*/


       在电脑串口助手软件上观察到的程序执行现象如下:

开始...

第1个数
十进制:1
十六进制:1
二进制:1

第2个数
十进制:1
十六进制:1
二进制:1

第3个数
十进制:1
十六进制:1
二进制:1

第4个数
十进制:1
十六进制:1
二进制:1

第5个数
十进制:2
十六进制:2
二进制:10

第6个数
十进制:3
十六进制:3
二进制:11


分析:
       变量a为1。
       变量b为1。
       变量c为1。

       变量d为1。
       变量e为2。
       变量f 为3。

【57.7   如何在单片机上练习本章节C语言程序?】

       直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
240
gdysg| | 2017-2-23 17:29 | 只看该作者
写的不错,支持楼主!

使用特权

评论回复
发新帖 本帖赏金 72.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则