jianhong_wu 发表于 2017-2-19 09:58:47

本帖最后由 jianhong_wu 于 2017-2-19 10:17 编辑

第五十七节: static的重要作用。

【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学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。


jianhong_wu 发表于 2017-2-26 12:11:07

本帖最后由 jianhong_wu 于 2017-2-26 12:29 编辑

第五十八节: const(或code)在定义数据时的作用。

【58.1   const与code的关系。】

       const与code都是语法的修饰关键词,放在所定义的数据前面时有“不可更改”之意。在C语言语法中,const像普通话全国通用,是标准的语言;而code像地方的方言,仅仅适合针对51单片机的C51编译器环境。而其它大多数单片机的C编译器并不支持code,只支持const。比如PIC,stm32等单片机的C编译器都是只认const而不认code的。通常情况下,const定义的数据都是放在ROM的,但是51单片机的C51编译器是例外,它并不把const定义的数据放在ROM区,只有用code关键词时它才会把数据放在ROM区,这一点相对其它大多数的单片机来说是不一样的。因为本教程是用51单片机的C51编译器,所以用code来替代const。本节教程所提到的const,在实际编程时都用code来替代。

【58.2   const(或code)在定义数据时的终极目的。】

       在数据定义分配的应用中,const的终极目的是为了节省RAM的开销。从“读”和“写”的角度分析,数据有两种:“能读能写”和“只能读”这两种。 “能读能写”的数据占用RAM内存,叫变量,C语言语法上定义此类数据时“无”const前缀。 “只能读”的数据占用ROM内存,叫常量, C语言语法上定义此类数据时“有”const前缀。单片机的ROM容量比RAM容量往往大几十倍甚至上百倍,相比之下,RAM的资源显得比较稀缺。因此,把某些只需“读”而不需“写”的数据定义成const放在ROM,就可以节省RAM的开销。

【58.3   const(或code)的应用场合。】

      const可以定义单个常量,也可以定义常量数组。定义单个常量时,通常应用在某个出现在程序多处并且需要经常调整的“阀值”参数,方便“一键更改”的操作。所谓“一键更改”,就是只要改一次const所定义初始化的某个常量,整个程序多次出现的此常量就自动更改了。定义常量数组时,通常应用在某个数据转换表,把某些固定的常量预先放到常量数组,通过数组的下标来“查表”。

【58.4   const(或code)的语法格式。】

      定义单个常量和常量数组时的语法是以下这个样子的:
const unsigned char x=10;//定义单个常量。加了const。
const unsigned char y={31,28,31,30,31,30,31,31,30,31,30,31}; //定义常量数组。加了const。

【58.5   const(或code)的“能读”和“不可写”概念】

      所谓“读”和“写”的能力,其实就是看某数能在赋值符号“=”的“右边”还是“左边”的能力。普通的变量,既可以在赋值符号“=”的“右边”(能读),也可以在赋值符号“=”的“左边”(能写)。比如,下面的写法是合法的:
unsigned char k=1;//这是普通的变量,无const前缀。
unsigned char n=2;//这是普通的变量,无const前缀。
n=k; //k出现在赋值符号“=”的右边,表示能读。合法。
k=n; //k出现在赋值符号“=”的左边,表示能写,可更改之意。合法。

      但是如果一旦在普通的变量前面加了const(或code)关键词,就会发生“化学变化”,原来的“变量”就变成了“常量”,常量只能“读”,不能“写”。比如:
const unsigned char c=1;//这是常量,有const前缀。
unsigned char n=2;//这是普通的变量,无const前缀。
n=c; //c是常量,能读,这是合法的。这行代码是语法正确的。
c=n; //c是常量,不能写,这是非法的,C编译器不通过。这行代码是语法错误的。

【58.6   const(或code)能在函数内部定义吗?】

       const(或code)能在函数内部定义吗?能。语法是允许的。当在函数内部定义数据成const(或者code),在数据的存储结构上,数据也是放在ROM区的(实际上在51单片机里想把数据放在ROM只能用code而不能用const),把数据定义在函数内部,就只能在这个函数里面用,不能被其它函数调用。在作用域的问题上,const(或者code)的常量数据跟其它变量的数据是一样的。比如:
void HanShu(void)
{
    const unsigned char c=1;//在函数内部定义的const常量也是放在ROM区存储。
    unsigned char n=2;
    n=c; //c是常量,在函数内部定义,只能在当前这个HanShu函数里调用。
}

【58.7   例程练习和分析。】

       本教程使用的是51单片机的C51编译器,编写程序时为了让常量数据真正存储在ROM区,因此,本教程的程序例子都是用code替代const。
本例程讲两个例子,一个是单个常量,一个是常量数组。
      (1)单个常量。举的例子是“阀值”的“一键更改”应用。根据考试的分数,分两个等级。凡是大于或者等于90分的就是“优”,串口助手输出显示“1”。凡是小于90分的就是“良”,串口助手输出显示“0”。这里的“90分”就是我所说的“阀值”概念,只要用一个const定义一个常量数据来替代“90”,当需要调整“阀值”时,只要更改一次此定义的常量数值就可以达到“一键更改”之目的。
      (2)常量数组。举的例子是,查询2017年12个月的某个月的总天数,用两种思路实现,一种是switch分支语句来实现,另一种是const常量数组的“查表”思路来实现。通过对比这两种思路,你会发现const常量数组在做“转换表”这类“查表”项目时的强大优越性。

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

//函数的声明。
unsigned char HanShu_switch(unsigned char u8Month);      
unsigned char HanShu_const(unsigned char u8Month);

//数据的定义。
code unsigned char Cu8Level=90;//需要调整“阀值”时,只需更改一次这里的“90”这个数值。
code unsigned char Cu8MonthBuffer= //每个月对应的天数。从数组下标0开始,0代表1月...
{31,28,31,30,31,30,31,31,30,31,30,31};


unsigned char a; //用来接收函数返回的结果。
unsigned char b;
unsigned char c;
unsigned char d;

//函数的定义。
unsigned char HanShu_switch(unsigned char u8Month)//用switch分支来实现。
{
   switch(u8Month)
   {
      case 1:   //1月份的天数
         return 31;
      case 2:   //2月份的天数
         return 28;
      case 3:   //3月份的天数
         return 31;
      case 4:   //4月份的天数
         return 30;
      case 5:   //5月份的天数
         return 31;
      case 6:   //6月份的天数
         return 30;
      case 7:   //7月份的天数
         return 31;
      case 8:   //8月份的天数
         return 31;
      case 9:   //9月份的天数
         return 30;
      case 10://10月份的天数
         return 31;
      case 11://11月份的天数
         return 30;
      case 12://12月份的天数
         return 31;
      default://万一输错了其它范围的月份,就默认返回30天。
          return 30;
   }
    }

unsigned char HanShu_const(unsigned char u8Month) //用const常量数组的“查表”来实现。
{
   unsigned char u8GetDays;
   u8Month=u8Month-1;//因为数组下标是从0开始,0代表1月份,1代表2月份。所以减去1。
u8GetDays=Cu8MonthBuffer; //这就是查表,马上获取常量数组表格里固定对应的天数。
return u8GetDays;
    }

    void main() //主函数
{
    //第(1)个例子
    if(89>=Cu8Level)//大于或者等于阀值,就输出1。
    {
       a=1;
    }
    else//否则输出0。
    {
       a=0;
    }

    if(95>=Cu8Level)//大于或者等于阀值,就输出1。
    {
       b=1;
    }
    else//否则输出0。
    {
       b=0;
    }

    //第(2)个例子
    c=HanShu_switch(2);//用switch分支的函数获取2月份的总天数。
    d=HanShu_const(2);   //用const常量数组“查表”的函数获取2月份的总天数。

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

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

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

开始...

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

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

第3个数
十进制:28
十六进制:1C
二进制:11100

第4个数
十进制:28
十六进制:1C
二进制:11100

分析:
       a为0。
       b为1。
       c为28。
       d为28。

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

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


jianhong_wu 发表于 2017-3-5 13:10:07

本帖最后由 jianhong_wu 于 2017-3-5 13:31 编辑

第五十九节: 全局“一键替换”功能的#define。

【59.1   #define作用和书写格式。】

       上一节讲const的时候,讲到了当某个常量在程序中是属于需要频繁更改的“阀值”的时候,用const就可以提供“一键更改”的快捷服务。本节的#define也具有此功能,而且功能比const更加强大灵活,它除了可以应用在常量,还可以应用在运算式以及函数的“一键更改”中。所谓“一键更改”,其实是说,#define内含了“替换”的功能,此“替换”跟word办公软件的“替换”功能几乎是一模一样的。#define的“替换”功能,除了在某些场合起到“一键更改”的作用,还可以在某些场合,把一些在字符命名上不方便阅读理解的常量、运算式或函数先“替换”成容易理解的字符串,让程序阅读起来更加清晰更加方便维护。#define的常见三种书写格式如下:

#define字符串常量   //注意,这里后面没有分号“;”
#define字符串运算式   //注意,这里后面没有分号“;”
#define字符串函数   //注意,这里后面没有分号“;”

      具体一点如下:

#defineAA1      //常量
#defineBB(a+b+c)//运算式
#defineC   add()    //函数

      需要注意的时候,#define后面没有分号“;”,因为它是C语言中的“预处理”的语句,不是单片机运行的程序指令语句。

【59.2   #define的编译机制。】

      #define是属于“预编译”的指令,所谓“预编译”就是在“编译”之前就开始的准备工作。编译器在正式编译某个源代码的时候,先进行“预编译”的准备工作,对于#define语句,编译器是直接把#define要替换的内容先在“编辑层面”进行机械化替换,这个“机械化替换”纯粹是字符串的替换,可以理解成word办公软件的“替换”编辑功能。比如以下程序:

#define A 3
#define B (2+6)   //有括号
#define C2+6   //无括号
unsigned long x=3;
unsigned long a;
unsigned long b;
unsigned long c;
void main() //主函数
{
    a=x*A;
    b=x*B;
    c=x*C;
    while(1)
    {
    }
}

      经过编译器“预编译”的“机械化替换”后,等效于以下代码:

unsigned long x=3;
unsigned long a;
unsigned long b;
unsigned long c;
void main() //主函数
{
    a=x*3;
    b=x*(2+6);
    c=x*2+6;
    while(1)
    {
    }
}

【59.3   #define在常量上的“一键替换”功能。】

      上一节讲const(或code)的时候,举了一个“阀值”常量的例子,这个例子可以用#define来替换等效。比如,原来const(或code)的例子如下:


code unsigned char Cu8Level=90;//需要调整“阀值”时,只需更改一次这里的“90”这个数值。

unsigned char a;
unsigned char b;
    void main() //主函数
{
    if(89>=Cu8Level)//大于或者等于阀值,就输出1。
    {
       a=1;
    }
    else//否则输出0。
    {
       a=0;
    }
    if(95>=Cu8Level)//大于或者等于阀值,就输出1。
    {
       b=1;
    }
    else//否则输出0。
    {
       b=0;
    }
    while(1)
    {
    }
}

       上述程序现在用#define来替换,等效如下:


#define Cu8Level 90//需要调整“阀值”时,只需更改一次这里的“90”这个数值。

unsigned char a;
unsigned char b;
    void main() //主函数
{
    if(89>=Cu8Level)//大于或者等于阀值,就输出1。
    {
       a=1;
    }
    else//否则输出0。
    {
       a=0;
    }
    if(95>=Cu8Level)//大于或者等于阀值,就输出1。
    {
       b=1;
    }
    else//否则输出0。
    {
       b=0;
    }
    while(1)
    {
    }
}

【59.4   #define在运算式上的“一键替换”功能。】

       #define在运算式上应用的时候,有一个地方要特别注意,就是必须加小括号“()”,否则容易出错。因为#define的替换是很“机械呆板”的,它只管“字符编辑层面”的机械化替换,举一个例子如下:

#define B (2+6)   //有括号
#define C2+6   //无括号
unsigned long x=3;
unsigned long b;
unsigned long c;
void main() //主函数
{
    b=x*B;//等效于b=x*(2+6),最终运算结果b等于24。因为3乘以8(2加上6等于8)。
    c=x*C;//等效于c=x*2+6,最终运算结果c等于12。因为3乘以2等于6,6再加6等于12。
    while(1)
    {
    }
}

      上述例子中,“有括号”与“没括号”的运算结果差别很大,第一个是24,第二个是12。具体的分析已经在源代码的注释了。

【59.5   #define在函数上的“一键替换”功能。】

      #define的应用很广,也可以应用在函数的“替换”上。例子如下:


void add(void);//函数的声明。
void add(void)   //函数的定义。
{
   a++;
}

#define a_zi_jiaadd()//用字符串a_zi_jia来替代函数add()。

    unsigned long a=1;
void main() //主函数
{
    a_zi_jia;//这里相当于调用函数add()。
    while(1)
    {
    }
}


【59.6   #define在常量后面添加U或者L的特殊写法。】

       有些初学者今后可能在工作中遇到#define以下这种写法:

#define字符串常量U
#define字符串常量L

      具体一点如下:

#defineAA6U
#defineBB6L

      常量加后缀“U”或者“L”有什么含义呢?字面上理解,U表示该常量是无符号整型unsigned int;L表示该常量是长整型long。但是在实际应用中这样“多此一举”地去强调某个常量的数据类型有什么意义呢?我自己私下也做了一些测试,目前我本人暂时还没有发现这个秘密的答案。所以对于这个问题,初学者现在只要知道这种写法在语法上是合法的就可以,至于它背后有什么玄机,有待大家今后更深的发掘。

【59.7   #define省略常量的特殊写法。】

      有些初学者今后在多文件编程中,在某些头文件.h中,会经常遇到以下这类代码:
#ifndef _AAA_
#define _AAA_
#endif
      其中第2行代码“#define _AAA_”后面居然没有常量,这样子的写法也行,到底是什么意思?在这类写法中,当字符串“_AAA_”后面省略了常量的时候,编译器默认会给_AAA_添加一个“非0”的常量,也许是1或者其它“非0”的值,多说一句,所谓“非0”值就是“肯定不是0”。上述代码等效于:
#ifndef _AAA_
#define _AAA_ 1//编译器会在这类默认添加一个1或者其它“非0”的常量
#endif

      这个知识点大家只要先有一个感性的认识即可,暂时不用深入了解。

【59.8   例程练习和分析。】

      现在编一个练习程序来熟悉#define的用法。

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

    //第1个:常量的例子
    #define Cu8Level 90//需要调整“阀值”时,只需更改一次这里的“90”这个数值。
    unsigned char a;
    unsigned char b;

    //第2个:运算式的例子
      #define C (2+6)   //有括号
      #define D2+6    //无括号
    unsigned char x=3;
    unsigned char c;
    unsigned char d;

    //第3个:函数的例子
    unsigned char e=1;
    void add(void);
    void add(void)   
    {
       e++;
    }
    #define a_zi_jiaadd()//用字符串a_zi_jia来替代函数add()。


void main() //主函数
{

    //第1个:常量的例子
    if(89>=Cu8Level)//大于或者等于阀值,就输出1。
    {
       a=1;
    }
    else//否则输出0。
    {
       a=0;
    }
    if(95>=Cu8Level)//大于或者等于阀值,就输出1。
    {
       b=1;
    }
    else//否则输出0。
    {
       b=0;
    }


    //第2个:运算式的例子
    c=x*C;//等效于c=x*(2+6),最终运算结果c等于24。因为3乘以8(2加上6等于8)。
    d=x*D;//等效于d=x*2+6,最终运算结果d等于12。因为3乘以2等于6,6再加6等于12。


    //第3个:函数的例子
    a_zi_jia;//这里相当于调用函数add()。e从1自加到2。
    a_zi_jia;//这里相当于调用函数add()。e从2自加到3。


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

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

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

开始...

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

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

第3个数
十进制:24
十六进制:18
二进制:11000

第4个数
十进制:12
十六进制:C
二进制:1100

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

分析:
         a为0。
         b为1。
         c为24。
         d为12。
         e为3。

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

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


jianhong_wu 发表于 2017-3-12 12:32:43

本帖最后由 jianhong_wu 于 2017-3-12 12:49 编辑

第六十节: 指针在变量(或常量)中的基础知识。

【60.1   指针与普通变量的对比。】

       普通变量和指针都是变量,都要占用RAM资源。普通变量的unsigned char类型占用1个字节,unsignedint类型占用2个字节,unsigned long类型占用4个字节。但是指针不一样,指针是一种特殊的变量,unsigned char*,unsigned int*,unsigned long*这三类指针都是一样占用4个字节。指针是普通变量的载体,平时我们处理普通变量,都是可以“直接”操作普通变量本身。而学了指针之后,我们就多一种选择,可以通过指针这个载体来“间接”操作某个普通变量。“直接”不是比“间接”更好更高效吗?为什么要用“间接”?其实在某些场合,指针的“间接”操作更加灵活更加高效,这个要看具体的应用。
指针既然是普通变量的“载体”,那么普通变量就是“物”。“载体”与“物”之间可以存在一对多的关系。也就是说,一个篮子(载体),可以盛放鸡蛋(物),也可以盛放青菜(物),也可以盛放水果(物)。
但是,在这里,一个篮子在一个时间段内,只能承载一种物品,如果想承载其它物品,必须先把当前物品“卸”下来,然后再“装”其它物品”。这里有两个关键动作“装”和“卸”,就是指针在处理普通变量时的“绑定”,某个指针与某个变量发生“绑定”,就已经包含了先“卸”后“装”这两个动作在其中。
      题外话多说一句,刚才提到,unsignedint类型占用2个字节,这个是在C51编译器下的情况。如果是在stm32单片机的编译器下,unsignedint类型是占用4个字节。而“凡是指针都是4个字节”,这个描述仅仅适用于32位以下的单片机编译器(包括8位的单片机),而在某些64位的PC机,指针可能是8个字节,这些内容大家只要有个大概的了解即可。

【60.2   指针的定义。】

      跟普通变量一样,指针也必须先定义再使用。为了与普通变量区分开来,指针在定义的时候多加了一个星号“*”,例子如下:

unsigned char* pu8;   //针对unsigned char类型变量的指针。凡是指针都是占4个字节!
unsigned int* pu16;   //针对unsigned int类型变量的指针。凡是指针都是占4个字节!
unsigned long* pu32;//针对unsigned long类型变量的指针。凡是指针都是占4个字节!

      既然指针都是4个字节,为什么还要区分unsigned char*,unsigned int* pu16,unsigned long* pu32这三种类型?因为指针是为普通变量(或常量)而生,所以要根据普通变量(或常量)的类型定义对应的指针。

【60.3   指针与普通变量是如何关联和操作的?】

      指针在操作某个变量的时候,必须先跟某个变量关联起来,这里的关联就是“绑定”。“绑定”后,才可以通过指针这个“载体”来“间接”操作变量。指针与普通变量在“绑定”的时候,需要用到“&”这个符号。例子如下:

unsigned char* pu8;   //针对unsigned char类型变量的指针。凡是指针都是占4个字节!
unsigned char a=0;    //普通的变量。
pu8=&a;//指针与普通变量发生关联(或者说绑定)。
*pu8=2;//通过指针这个载体来处理a这个变量,此时a从原来的0变成了2。

【60.4   指针处理“批量数据”的基础知识。】

       之所以有通过载体来“间接”操作普通变量的存在价值,其中很重要的原因是指针在处理“批量数据”时特别给力,这里的“批量数据”是有条件的,要求这些数据的地址必须挨家挨户连起来的,不能是零零散散的“散户”,比如说,数组就是由一堆在RAM空间里地址连续的变量组合而成,指针在很多时候就是为数组而生的。先看一个例子如下:

unsigned char* pu8;   //针对unsigned char类型变量的指针。凡是指针都是占4个字节!
unsigned char Buffer;    //普通的数组,内含3个变量,它们地址是相连的。

pu8=&Buffer;//指针与普通变量Buffer发生关联(或者说绑定)。
*pu8=1;          //通过指针这个载体来处理Buffer这个变量,此时Buffer变成了1。

pu8=&Buffer;//指针与普通变量Buffer发生关联(或者说绑定)。
*pu8=2;          //通过指针这个载体来处理Buffer这个变量,此时Buffer变成了2。

pu8=&Buffer;//指针与普通变量Buffer发生关联(或者说绑定)。
*pu8=3;          //通过指针这个载体来处理Buffer这个变量,此时Buffer变成了3。


       分析:上述例子中,并没有体现出指针的优越性,因为数组有3个元素,居然要绑定了3次,如果数组有1000个元素,难道要绑定1000次?显然这样是繁琐低效不可取的。而要发挥指针的优越性,我们现在必须深入了解一下指针的本质是什么,指针跟普通变量发生“绑定”的本质是什么。普通变量由“地址”和“地址所装的数据”构成,指针是特殊的变量,它是由什么构成呢?其实,指针是由“地址”和“地址所装的变量(或常量)的地址”组成。很明显,一个重要的区别是,普通变量装的数据,而指针装的是地址。正因为指针装的是地址,所以指针可以有两种选择,第一种可以处理“装的地址”,第二种可以处理“装的地址的所在数据”,这两种能力,就是指针的精华和本质所在,也是跟普通变量的区别所在。那么指针处理“装的地址”的语法是什么样子的?请看例子如下:

unsigned char* pu8;   //针对unsigned char类型变量的指针。凡是指针都是占4个字节!
unsigned char Buffer;   //普通的数组,内含3个变量,它们地址是相连的。

pu8=&Buffer;//处理“装的地址”。把 Buffer变量的地址装在指针这个载体里。
*pu8=1;   //处理“装的地址的所在数据”。此时Buffer变成了1。

pu8++;      //处理“装的地址”。这里是“地址”自加1,相当于指针此时装的是Buffer的地址。
*pu8=2;   //处理“装的地址的所在数据”。此时Buffer变成了2。

pu8++;      //处理“装的地址”。这里是“地址”自加1,相当于指针此时装的是Buffer的地址。
*pu8=3;   //处理“装的地址的所在数据”。此时Buffer变成了3。

      上述例子中,利用“地址”自加1的操作,省去了2条赋值式的“绑定”操作(比如像pu8=&Buffer这类语句),因此“绑定”本质其实就是更改指针所装的“变量(或常量)的地址”的操作。此例子中虽然还没体现了出指针在数组处理时的优越性,但是利用指针处理“装的地址”这项功能,在实际项目中很容易发现它的好处。

【60.5   指针与数组关联(绑定)时省略“&和下标”的写法。】

      指针与数组关联的时候,通常是跟数组的第0个元素的地址关联,此时,可以把数组的“&和下标”省略,比如:
unsigned char* pu8;
unsigned char Buffer;   
pu8=Buffer;   //此行代码省略了“&和下标”,等效于pu8=&Buffer;

【60.6   带const关键字的常量指针。】

      指针也可以跟常量关联起来,处理常量,但是常量只能“读”不能“写”,所以通过指针操作常量的时候也是只能“读”不能“写”。操作常量的指针用const关键词修饰,强调此指针只有“读”的操作。例子如下:
const unsigned char* pCu8;   //常量指针
code char Cu8Buffer={5,6,7};   //常量数组
unsigned char b;
unsigned char c;
unsigned char d;

pCu8=Cu8Buffer; //此行代码省略了“&和下标”,等效于pCu8=&Cu8Buffer;
b=*pCu8;      //读“装的地址的所在数据”。b等于5。

pCu8++;         //所装的地址自加1,跟Cu8Buffer关联
c=*pCu8;      //读“装的地址的所在数据”。c等于6。

pCu8++;         //所装的地址自加1,跟Cu8Buffer关联
d=*pCu8;      //读“装的地址的所在数据”。d等于7。



【60.7   例程练习和分析。】

       现在编一个练习程序来熟悉指针的基础知识。

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

    unsigned char* pu8;      //针对unsigned char类型变量的指针。凡是指针都是占4个字节!
    unsigned char a=0;         //普通的变量。
    unsigned char Buffer;   //普通的数组,内含3个变量,它们地址是相连的。

const unsigned char* pCu8;   //常量指针
code char Cu8Buffer={5,6,7};   //常量数组
unsigned char b;
unsigned char c;
unsigned char d;


void main() //主函数
{

    pu8=&a;//指针与普通变量发生关联(或者说绑定)。
    *pu8=2;//通过指针这个载体来处理a这个变量,此时a从原来的0变成了2。


    pu8=&Buffer;//处理“装的地址”。把 Buffer变量的地址装在指针这个载体里。
    *pu8=1;   //处理“装的地址的所在数据”。此时Buffer变成了1。

    pu8++;      //处理“装的地址”。这里是“地址”自加1,相当于指针此时装的是Buffer的地址。
    *pu8=2;   //处理“装的地址的所在数据”。此时Buffer变成了2。

    pu8++;      //处理“装的地址”。这里是“地址”自加1,相当于指针此时装的是Buffer的地址。
    *pu8=3;   //处理“装的地址的所在数据”。此时Buffer变成了3。

pCu8=Cu8Buffer; //此行代码省略了“&和下标”,等效于pCu8=&Cu8Buffer;
b=*pCu8;      //读“装的地址的所在数据”。b等于5。

pCu8++;         //所装的地址自加1,跟Cu8Buffer关联
c=*pCu8;      //读“装的地址的所在数据”。c等于6。

pCu8++;         //所装的地址自加1,跟Cu8Buffer关联
d=*pCu8;      //读“装的地址的所在数据”。d等于7。

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


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

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

开始...

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

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

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

第4个数
十进制:7
十六进制:7
二进制:111

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

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

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

分析:
      a为2。
      b为5。
      c为6。
      d为7。
      Buffer为1。
      Buffer为2。
      Buffer为3。

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

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


jianhong_wu 发表于 2017-3-20 10:15:38

本帖最后由 jianhong_wu 于 2017-3-20 10:50 编辑

第六十一节: 指针的中转站作用,地址自加法,地址偏移法。

【61.1   指针与批量数组的关系。】

       指针和批量数据的关系,更像领导和团队的关系,领导是团队的代表,所以当需要描述某个团队的时候,为了表述方便,可以把由N个人组成的团队简化成该团队的一个领导,用一个领导来代表整个团队,此时,领导就是团队,团队就是领导。指针也一样,指针一旦跟某堆数据“绑定”了,那么指针就是这堆数据,这堆数据就是该指针,所以在很多PC上位机的项目中,往往也把指针称呼为“句柄”,字面上理解,就是一句话由N个文字组成,而“句柄”就是这句话的代表,实际上“句柄”往往是某一堆资源的代表。不管是把指针比喻成“领导”、“代表”还是“句柄”,指针在这里都有“中间站”这一层含义。

【61.2   指针在批量数据的“中转站”作用。】

       指针在批量数据处理中,主要是能节省代码容量,而且是非常直观的节省代码容量。为什么能节省代码容量?是因为可以把某些重复性的具体实现的功能封装成指针来操作,请看下面的例子:

       程序要求:根据一个选择变量Gu8Sec的值,要从三堆数据中选择对应的一堆数据放到数组Gu8Buffer里。当Gu8Sec等于1的时候选择第1堆,等于2的时候选择第2堆,等于3的时候选择第3堆。也就是“三选一”。

       第1种实现的方法:没有用指针,最原始的处理方式。如下:
code unsigned char Cu8Memory_1={1,2,3};//第1堆数据
code unsigned char Cu8Memory_2={4,5,6};//第2堆数据
code unsigned char Cu8Memory_3={7,8,9};//第3堆数据

unsigned char Gu8Sec=2;//选择的变量
unsigned char Gu8Buffer;//根据变量来存放对应的某堆数据的数组
unsigned char i; //for循环用到的变量i

switch(Gu8Sec)//根据此选择变量来切换到对应的操作上
{
    case 1://第1堆
          for(i=0;i<3;i++)   //第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
          {
             Gu8Buffer=Cu8Memory_1;
          }
          break;

    case 2://第2堆
          for(i=0;i<3;i++)//第2次出现for循环,用来实现“赋值”的“搬运数据”的动作。
          {
             Gu8Buffer=Cu8Memory_2;
          }
          break;

    case 3://第3堆
          for(i=0;i<3;i++)   //第3次出现for循环,用来实现“赋值”的“搬运数据”的动作。
          {
             Gu8Buffer=Cu8Memory_3;
          }
          break;

}

       分析:上述程序中,没有用到指针,出现了3次for循环的“赋值”的“搬运数据”的动作。

      第2种实现的方法:用指针作为“中间站”。如下:

code unsigned char Cu8Memory_1={1,2,3};//第1堆数据
code unsigned char Cu8Memory_2={4,5,6};//第2堆数据
code unsigned char Cu8Memory_3={7,8,9};//第3堆数据

unsigned char Gu8Sec=2;//选择的变量
unsigned char Gu8Buffer;//根据变量来存放对应的某堆数据的数组
unsigned char i; //for循环用到的变量i
const unsigned char *pCu8; //引入一个指针作为“中间站”

switch(Gu8Sec)//根据此选择变量来切换到对应的操作上
{
    case 1://第1堆
          pCu8=&Cu8Memory_1;//跟第1堆数据“绑定”起来。
          break;

    case 2://第2堆
          pCu8=&Cu8Memory_2;//跟第2堆数据“绑定”起来。
          break;

    case 3://第3堆
          pCu8=&Cu8Memory_3;//跟第3堆数据“绑定”起来。
          break;
}

for(i=0;i<3;i++)//第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
{
   Gu8Buffer=*pCu8;//把“指针所存的地址的数据”赋值给数组
   pCu8++;//“指针所存的地址”自加1,为下一个数据的“赋值”的“搬运”作准备。
}


      分析:上述程序中,用到了指针作为中间站,只出现了1次for循环的“赋值”的“搬运数据”的动作。对比之前第1种方法,在本例子中,用了指针之后,程序代码看起来更加高效简洁清爽省容量。在实际项目中,数据量越大的时候,指针这种“优越性”就越明显。

【61.3   指针在书写上另外两种常用写法。】

       刚才61.2处第2个例子中,有一段代码如下:
for(i=0;i<3;i++)//第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
{
   Gu8Buffer=*pCu8;//把“指针所存的地址的数据”赋值给数组
   pCu8++;//“指针所存的地址”自加1,为下一个数据的“赋值”的“搬运”作准备。
}

       很多高手,喜欢把上面for循环内部的那两行代码简化成一行代码,如下:

for(i=0;i<3;i++)//第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
{
   Gu8Buffer=*pCu8++;//先把“数据”赋值给数组,然后“指针所存的地址”再自加1。
}

      上面这种写法也是合法的,而且在高手的代码中常见,据说也是最高效的写法。还有一种是利用“指针的偏移地址”的写法,我常用这种写法,因为感觉这种写法比较直观,而且跟数组的书写很像。如下:

for(i=0;i<3;i++)//第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
{
   Gu8Buffer=pCu8;//这类是“偏移地址”的写法,i在这里相当于指针的偏移地址。
}


      这种写法也是跟前面那两种写法在程序实现的功能上是一样的,是等效的,我常用这种写法。

【61.4   指针的“地址自加法”和“地址偏移法”的差别。】

       刚才61.3处讲了3个例子,其中前面的两个例子都是属于“地址自加法”,而最后的那一个是属于“地址偏移法”。它们的根本差别是:“地址自加法”的时候,“指针所存的地址”是变动的;而“地址偏移法”的时候,“指针所存的地址”是不变的,“指针所存的地址”的“不变”的属性,就像某个原点,原点再加上偏移,就可以寻址到某个新的RAM地址所存的数据。例子如下:

       第1种:“地址自加法”:


pCu8=&Cu8Memory_2;//假设赋值后,此时“指针所存的地址”是RAM的地址4。
for(i=0;i<3;i++)
{
   Gu8Buffer=*pCu8++;//先把“数据”赋值给数组,然后“指针所存的地址”再自加1。
}

       分析:上述代码,等程序执行完for循环后,指针所存的地址还是RAM地址4吗?不是。因为它是变动的,经过for循环,“指针所存的地址”自加3次后,此时“所存的RAM地址”从原来的4变成了7。

       第2种:“地址偏移法”:

pCu8=&Cu8Memory_2;//假设赋值后,此时“指针所存的地址”是RAM的地址4。
for(i=0;i<3;i++)
{
    Gu8Buffer=pCu8;//这类是“偏移地址”的写法,i在这里相当于指针的偏移地址。
}

       分析:上述代码,等程序执行完for循环后,指针所存的地址还是RAM地址4吗?是的。因为它存的地址是不变的,变的只是偏移地址i。此时“指针所存的地址”就像“原点”一样具有“绝对地址”的“参考点”的属性。

【61.5   例程练习和分析。】

       现在编一个练习程序。

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

code unsigned char Cu8Memory_1={1,2,3};//第1堆数据
code unsigned char Cu8Memory_2={4,5,6};//第2堆数据
code unsigned char Cu8Memory_3={7,8,9};//第3堆数据

unsigned char Gu8Sec=2;//选择的变量
unsigned char Gu8Buffer;//根据变量来存放对应的某堆数据的数组
unsigned char i; //for循环用到的变量i
const unsigned char *pCu8; //引入一个指针作为“中间站”

void main() //主函数
{

switch(Gu8Sec)//根据此选择变量来切换到对应的操作上
{
    case 1://第1堆
          pCu8=&Cu8Memory_1;//跟第1堆数据“绑定”起来。
          break;

    case 2://第2堆
          pCu8=&Cu8Memory_2;//跟第2堆数据“绑定”起来。
          break;

    case 3://第3堆
          pCu8=&Cu8Memory_3;//跟第3堆数据“绑定”起来。
          break;
}

// for(i=0;i<3;i++)//第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
// {
//   Gu8Buffer=*pCu8++; //先把“数据”赋值给数组,然后“指针所存的地址”再自加1。
// }

    for(i=0;i<3;i++)//第1次出现for循环,用来实现“赋值”的“搬运数据”的动作。
{
    Gu8Buffer=pCu8;//这类是“偏移地址”的写法,i在这里相当于指针的偏移地址。
}

    View(Gu8Buffer);//把第1个数Gu8Buffer发送到电脑端的串口助手软件上观察。
    View(Gu8Buffer);//把第2个数Gu8Buffer发送到电脑端的串口助手软件上观察。
    View(Gu8Buffer);//把第3个数Gu8Buffer发送到电脑端的串口助手软件上观察。

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

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

开始...

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

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

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

分析:
      Gu8Buffer为4。
      Gu8Buffer为5。
      Gu8Buffer为6。

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

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


jianhong_wu 发表于 2017-3-26 10:38:16

本帖最后由 jianhong_wu 于 2017-3-26 10:54 编辑

第六十二节: 指针,大小端,化整为零,化零为整。

【62.1   内存的大小端。】

       C51编译器的unsigned int占2字节RAM(也称为内存),unsigned long占4字节RAM,这两种数据类型所占的字节数都超过了1个字节,而RAM内存是每一个地址对应一个字节的RAM内存,那么问题就来了,比如像unsigned long这种占4个字节RAM的数据变量,它这4个字节在RAM中的地址是“连续”的“挨家挨户”的“连号”的,这4个字节所存的一个数据,它的数据高低位在地址的排列上,到底是从低到高还是从高到低,到底是“正向”的还是“反向”?这两种不同的排列顺序,在C语言里用“大端”和“小端”这两个专业术语来描述。“大端”的方式是将高位存放在低地址,“小端”的方式是将低位存放在低地址。比如:

       假设有一个unsigned long变量a等于0x12345678,是存放在RAM内存中的第4,5,6,7这四个“连号”的地址里,现在看看它在“大端”和“小端”的存储方式里的差别。如下:

      (1)在“大端”的方式里,将高位存放在低地址。
       0x12存在第4个地址,0x34存在第5个地址,0x56存在第6个地址,0x78存在第7个地址。

      (2)在“小端”的方式里,将低位存放在低地址。
       0x78存在第4个地址,0x56存在第5个地址,0x34存在第6个地址,0x12存在第7个地址。

       问题来了,在单片机里,内存到底是“大端”方式还是“小端”方式?答:这个跟C编译器有关。比如,在51单片机的C51编译环境里是“大端”方式,而在stm32单片机的ARM_MDK编译环境里则是“小端”方式。那么问题又来了?如何知道一个C编译器是“大端”还是“小端”?答:有两种方式,一种是看C编译器的说明书,另一种是自己编写一个小程序测试一下就知道了(这种方法最简单可靠)。那么问题又来了?讲这个 “大小端”有什么用?答:这个跟指针的使用密切相关。

【62.2   化整为零。】

       在数据的存储和通信中,往往要先把数据转换成以字节为单位的数组,才能进行数据存储和通信。比如unsigned long这种类型的数据,就要先转换成4个字节,这种把某个变量转换成N个字节的过程,就是“化整为零”。“化整为零”的过程,在代码上,有两种常见的方式,一种是原始的“移位法”,另一种是极具优越性的“指针法”。比如,现在以“大端”方式为例(因为本教程是用C51编译器,C51编译器是“大端”方式),有一个unsigned long变量a等于0x12345678,要把这个变量分解成4个字节存放在一个数组Gu8BufferA中,现在跟大家分享和对比一下这两种方法。

      (1)原始的“移位法”。
unsigned long a=0x12345678;
unsigned char Gu8BufferA;

Gu8BufferA=a>>24;
Gu8BufferA=a>>16;
Gu8BufferA=a>>8;
Gu8BufferA=a;

      (2)极具优越性的“指针法”。
unsigned long a=0x12345678;
unsigned char Gu8BufferA;
unsigned long *pu32;   //引入一个指针变量,注意,这里是unsigned long类型的指针。

pu32=(unsigned long *)&Gu8BufferA;//指针跟数组“绑定”(也称为“关联”)起来。
*pu32=a;//这里仅仅1行代码就等效于上述(1)“移位”例子中的4行代码,所以极具优越性。

      多说一句,“pu32=(unsigned long *)&Gu8BufferA”这行代码中,其中小括号“(unsigned long *)”是表示数据的强制类型转换,这里表示强制转换成unsigned long的指针方式,以后这类代码写多了,就会发现这种书写方法的规律。作为语言来解读先熟悉一下它的表达方式就可以了,暂时不用深究它的含义。

【62.3   化零为整。】

      从数据存储中提取数据出来,从通讯端接收到一堆数据,这里的“提取”和“接收”都是以字节为单位的数据,所以为了“还原”成原来的类型变量,就涉及“化零为整”的过程。在代码上,有两种常见的方式,一种是原始的“移位法”,另一种是极具优越性的“指针法”。比如,现在以“大端”方式为例(因为本教程是用C51编译器,C51编译器是“大端”方式),有一个数组Gu8BufferB存放了4个字节数据分别是:0x12,0x34,0x56,0x78。现在要把这4个字节数据“合并”成一个unsigned long类型的变量b,这个变量b等于0x12345678。现在跟大家分享和对比一下这两种方法。

       (1)原始的“移位法”。
unsigned char Gu8BufferB={0x12,0x34,0x56,0x78};
unsigned long b;

b=Gu8BufferB;
b=b<<8;
b=b+Gu8BufferB;
b=b<<8;
b=b+Gu8BufferB;
b=b<<8;
b=b+Gu8BufferB;

       (2)极具优越性的“指针法”。
unsigned char Gu8BufferB={0x12,0x34,0x56,0x78};
unsigned long b;
unsigned long *pu32;   //引入一个指针变量,注意,这里是unsigned long类型的指针。

pu32=(unsigned long *)&Gu8BufferB;//指针跟数组“绑定”(也称为“关联”)起来。
b=*pu32;//这里仅仅1行代码就等效于上述(1)“移位”例子中的7行代码,所以极具优越性。

【62.4   “指针法”要注意的问题。】

       “化整为零”和“化零为整”其实是一个“互逆”的过程,在使用“指针法”的时候,一定要注意“大小端”的问题。“化整为零”和“化零为整”这两个“互逆”过程要么同时为“大端”,要么同时为“小端”,否则会因字节的排列顺序问题而引起数据的严重错误。

【62.5   例程练习和分析。】

      现在编一个练习程序。

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

unsigned long a=0x12345678;
unsigned char Gu8BufferA;

unsigned char Gu8BufferB={0x12,0x34,0x56,0x78};
unsigned long b;

unsigned long *pu32;   //引入一个指针变量,注意,这里是unsigned long类型的指针。

void main() //主函数
{
pu32=(unsigned long *)&Gu8BufferA;//指针跟数组“绑定”(也称为“关联”)起来。
*pu32=a;//化整为零

pu32=(unsigned long *)&Gu8BufferB;//指针跟数组“绑定”(也称为“关联”)起来。
b=*pu32;//化零为整


    View(Gu8BufferA);//把第1个数Gu8BufferA发送到电脑端的串口助手软件上观察。
    View(Gu8BufferA);//把第2个数Gu8BufferA发送到电脑端的串口助手软件上观察。
    View(Gu8BufferA);//把第3个数Gu8BufferA发送到电脑端的串口助手软件上观察。
View(Gu8BufferA);//把第4个数Gu8BufferA发送到电脑端的串口助手软件上观察。

View(b);            //把第5个数b发送到电脑端的串口助手软件上观察。

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

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

开始...

第1个数
十进制:18
十六进制:12
二进制:10010

第2个数
十进制:52
十六进制:34
二进制:110100

第3个数
十进制:86
十六进制:56
二进制:1010110

第4个数
十进制:120
十六进制:78
二进制:1111000

第5个数
十进制:305419896
十六进制:12345678
二进制:10010001101000101011001111000

分析:
         Gu8BufferA为0x12。
         Gu8BufferA为0x34。
         Gu8BufferA为0x56。
         Gu8BufferA为0x78。
         b为0x12345678。

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

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


jianhong_wu 发表于 2017-4-2 10:16:02

本帖最后由 jianhong_wu 于 2017-4-2 10:34 编辑

第六十三节: 指针“化整为零”和“化零为整”的“灵活”应用。

【63.1   化整为零的“灵活”应用。】

       上一节讲“化整为零”的例子,指针是跟数组的首地址(下标是0)“绑定”的,这样,很多初学者就误以为指针跟数组“绑定”时,只能跟数组的“首地址”关联。其实,指针是可以跟数组的任何一个成员的地址“绑定”(只要不超过数组的长度导致越界),它不仅仅局限于首地址,指针的这个特征就是本节标题所说的“灵活”。请看下面这个例子:

       有3个变量,分别是单字节unsigned char a,双字节unsigned int b,四字节unsigned long c,它们加起来一共有7个字节,要把这7个字节放到一个7字节容量的数组里。除了用传统的“移位法”,还有一种更加便捷的“指针法”,代码如下:

unsigned char a=0x01;
unsigned int b=0x0203;
unsigned long c=0x04050607;

unsigned char Gu8BufferABC; //存放3个不同长度变量的数组

unsigned char *pu8;   //引入的unsigned char 类型指针
unsigned int *pu16;   //引入的unsigned int 类型指针
unsigned long *pu32;//引入的unsigned long 类型指针

pu8=&Gu8BufferABC; //指针跟数组的第0个位置“绑定”起来。
*pu8=a; //把a的1个字节放在数组第0个位置。

pu16=(unsigned int *)&Gu8BufferABC; //指针跟数组的第1个位置“绑定”起来。
*pu16=b;            //把b的2个字节放在数组第1、2这两个位置。

pu32=(unsigned long *)&Gu8BufferABC; //指针跟数组的第3个位置“绑定”起来。
*pu32=c;            //把c的4个字节放在数组第3、4、5、6这四个位置。

【63.2   化零为整的“灵活”应用。】

       刚才讲的是“化整为零”,现在讲的是“化零为整”。刚才讲的是“分解”,现在讲的是“合成”。请看下面这个例子:

       有一个容量为7字节数组,第0字节存放的是unsigned char d变量,第1、2字节存放的是unsigned int e变量,第3、4、5、6字节存放的是unsigned long f变量,现在要从数组中“零散”的字节里提取并且合成为“完整”的3个变量。代码如下:

    unsigned char Gu8BufferDEF={0x01,0x02,0x03,0x04,0x05,0x06,0x07}; //注意大小端的问题

unsigned char d;
unsigned int e;
unsigned long f;

unsigned char *pu8;   //引入的unsigned char 类型指针
unsigned int *pu16;   //引入的unsigned int 类型指针
unsigned long *pu32;//引入的unsigned long 类型指针

pu8=&Gu8BufferDEF; //指针跟数组的第0个位置“绑定”起来。
d=*pu8;          //从数组第0位置提取单字节完整的d变量。

pu16=(unsigned int *)&Gu8BufferDEF; //指针跟数组的第1个位置“绑定”起来。
e=*pu16;      //从数组第1,2位置提取双字节完整的e变量。

pu32=(unsigned long *)&Gu8BufferDEF; //指针跟数组的第3个位置“绑定”起来。
f=*pu32;      //从数组第3,4,5,6位置提取四字节完整的f变量。



【63.3   例程练习和分析。】

      现在编一个练习程序。

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

unsigned char a=0x01;
unsigned int b=0x0203;
unsigned long c=0x04050607;
unsigned char Gu8BufferABC; //存放3个不同长度变量的数组

    unsigned char Gu8BufferDEF={0x01,0x02,0x03,0x04,0x05,0x06,0x07}; //注意大小端的问题
unsigned char d;
unsigned int e;
unsigned long f;

unsigned char *pu8;   //引入的unsigned char 类型指针
unsigned int *pu16;   //引入的unsigned int 类型指针
unsigned long *pu32;//引入的unsigned long 类型指针


void main() //主函数
{
    //第1类例子:化整为零。
pu8=&Gu8BufferABC; //指针跟数组的第0个位置“绑定”起来。
*pu8=a; //把a的1个字节放在数组第0个位置。

pu16=(unsigned int *)&Gu8BufferABC; //指针跟数组的第1个位置“绑定”起来。
*pu16=b;            //把b的2个字节放在数组第1、2这两个位置。

pu32=(unsigned long *)&Gu8BufferABC; //指针跟数组的第3个位置“绑定”起来。
*pu32=c;            //把c的4个字节放在数组第3、4、5、6这四个位置。

    //第2类例子:化零为整。
pu8=&Gu8BufferDEF; //指针跟数组的第0个位置“绑定”起来。
d=*pu8;          //从数组第0位置提取单字节完整的d变量。

pu16=(unsigned int *)&Gu8BufferDEF; //指针跟数组的第1个位置“绑定”起来。
e=*pu16;      //从数组第1,2位置提取双字节完整的e变量。

pu32=(unsigned long *)&Gu8BufferDEF; //指针跟数组的第3个位置“绑定”起来。
f=*pu32;      //从数组第3,4,5,6位置提取四字节完整的f变量。


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

    View(d);               //把第8个数d发送到电脑端的串口助手软件上观察。
    View(e);               //把第9个数e发送到电脑端的串口助手软件上观察。
View(f);               //把第10个数f发送到电脑端的串口助手软件上观察。

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

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

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

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

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

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

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

第6个数
十进制:6
十六进制:6
二进制:110

第7个数
十进制:7
十六进制:7
二进制:111

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

第9个数
十进制:515
十六进制:203
二进制:1000000011

第:个数(这里是第10个数。本模块程序只支持显示第1到第9个,所以这里没有显示“10”)
十进制:67438087
十六进制:4050607
二进制:100000001010000011000000111


分析:
      Gu8BufferABC为0x01。
      Gu8BufferABC为0x02。
      Gu8BufferABC为0x03。
      Gu8BufferABC为0x04。
      Gu8BufferABC为0x05。
      Gu8BufferABC为0x06。
      Gu8BufferABC为0x07。
      d为0x01。
      e为0x0203。
      f为0x04050607。

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

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





jianhong_wu 发表于 2017-4-9 17:59:00

本帖最后由 jianhong_wu 于 2017-4-9 18:11 编辑

第六十四节: 指针让函数具备了多个相当于return的输出口。

【64.1   函数的三类输出渠道。】

       函数是模块,模块必须具备输入和输出的接口,从输入和输出的角度分析,函数对外部调用者传递信息主要有三类渠道,第一类是全局变量,第二类是return返回值,第三类是用指针。全局变量太隐蔽,没有那么直观,可读性稍差。return可读性强,缺点是一个函数只能有一个return,如果一个函数要输出多个结果,return就力不从心。指针作为函数的输出接口,就能随心所欲了,不但可读性强,而且输出的接口数量不受限制。

【64.2   只有一个输出接口的时候。】

       现在举一个例子,要用函数实现一个加法运算,输出“一个”加法运算的和,求3加上5等于8。下面三个例子中分别使用“全局变量,return,指针”这三类输出接口。

       第一类:全局变量。
unsigned char DiaoYongZhe;//调用者
unsigned char BeiJiaShu;    //被加数
unsigned char JiaShu;       //加数
unsigned char He;         //输出的接口,加法运算的"和"。

void JiaFa(void)   
{
   He=BeiJiaShu+JiaShu;
}

void main()
{
    BeiJiaShu=3;          //填入被加数3
    JiaShu=5;             //填入加数5
    JiaFa();            //调用一次加法运算的函数
    DiaoYongZhe=He;       //把加法运算的“和”赋值给调用者。
}

       第二类:return。
unsigned char DiaoYongZhe;    //调用者

unsigned char JiaFa(unsigned char BeiJiaShu,unsigned char JiaShu)   
{
   unsigned char He;
   He=BeiJiaShu+JiaShu;
   return He;
}

void main()
{
    DiaoYongZhe=JiaFa(3,5);   //把加法运算的“和”赋值给调用者,一气呵成。
}

       第三类:指针。
unsigned char DiaoYongZhe;    //调用者

void char JiaFa(unsigned char BeiJiaShu,unsigned char JiaShu,unsigned char *pu8He)   
{
    *pu8He=BeiJiaShu+JiaShu;
}

void main()
{
    JiaFa(3,5,&DiaoYongZhe);//通过指针这个输出渠道,把加法运算的“和”赋值给调用者,一气呵成。
}

【64.3   有多个输出接口的时候。】

       现在举一个例子,要用函数实现一个除法运算,分别输出除法运算的商和余数这“两个”数,求5除以3等于1余2。因为return只能输出一个结果,所以这里不列举return的例子,只使用“全局变量”和“指针”这两类输出接口。

       第一类:全局变量。
unsigned char DiaoYongZhe_Shang; //调用者的商
unsigned char DiaoYongZhe_Yu;    //调用者的余数

unsigned char BeiChuShu;         //被除数
unsigned char ChuShu;            //除数
unsigned char Shang;             //输出的接口,除法运算的"商"。
unsigned char Yu;                //输出的接口,除法运算的"余"。

void ChuFa(void)   
{
   Shang=BeiChuShu/ChuShu;   //求商。假设除数不会为0的情况。
   Yu=BeiChuShu%ChuShu;      //求余数。假设除数不会为0的情况。
}


void main()
{
    BeiChuShu=5;            //填入被除数5
    ChuShu=3;               //填入除数3
    ChuFa();                  //调用一次除法运算的函数
    DiaoYongZhe_Shang=Shang;//把除法运算的“商”赋值给调用者的商。
    DiaoYongZhe_Yu=Yu;      //把除法运算的“余数”赋值给调用者的余数。
}

       第二类:return。
       return只能输出一个结果,力不从心,所以这里不列举return的例子。

       第三类:指针。
unsigned char DiaoYongZhe_Shang;   //调用者的商
unsigned char DiaoYongZhe_Yu;      //调用者的余数

void ChuFa(unsigned char BeiChuShu,
         unsigned char ChuShu,
         unsigned char *pu8Shang,
         unsigned char *pu8Yu)   
{
      *pu8Shang=BeiChuShu/ChuShu;   //求商。假设除数不会为0的情况。
      *pu8Yu=BeiChuShu%ChuShu;      //求余数。假设除数不会为0的情况。
}

void main()
{
    ChuFa(5,3,&DiaoYongZhe_Shang,&DiaoYongZhe_Yu);//通过两个指针的输出接口,一气呵成。
}

【64.4   例程练习和分析。】

       现在编一个练习程序。

/*---C语言学习区域的开始。-----------------------------------------------*/
void ChuFa(unsigned char BeiChuShu,
         unsigned char ChuShu,
         unsigned char *pu8Shang,
         unsigned char *pu8Yu);   //函数声明   

unsigned char DiaoYongZhe_Shang;    //调用者的商
unsigned char DiaoYongZhe_Yu;       //调用者的余数

void ChuFa(unsigned char BeiChuShu,
         unsigned char ChuShu,
         unsigned char *pu8Shang,
         unsigned char *pu8Yu)    //函数定义
{
      *pu8Shang=BeiChuShu/ChuShu;   //求商。假设除数不会为0的情况。
      *pu8Yu=BeiChuShu%ChuShu;      //求余数。假设除数不会为0的情况。
}

void main() //主函数
{
ChuFa(5,3,&DiaoYongZhe_Shang,&DiaoYongZhe_Yu);//函数调用。通过两个指针的输出接口,一气呵成。

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

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

开始...

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

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

分析:
      DiaoYongZhe_Shang为1。
      DiaoYongZhe_Yu为2。

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

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


jianhong_wu 发表于 2017-4-16 11:37:43

本帖最后由 jianhong_wu 于 2017-4-16 12:03 编辑

第六十五节: 指针作为数组在函数中的入口作用。
【65.1   函数的参数入口。】
      要往函数内部传递信息,主要有两类渠道。第一类是全局变量。第二类是函数的参数入口,而参数入口可以分为“普通局部变量”和“指针”这两类。“普通局部变量”的参数入口一次只能传一个数据,如果一个数组有几十个甚至上百个数据,此时“普通局部变量”就无能为力,这时不可能也写几十个甚至上百个入口参数吧(这会累坏程序员),针对这种需要输入批量数据的场合,“指针”的参数入口就因此而生,完美解决了此问题,仅用一个“指针”参数入口就能解决一个数组N个数据的入口问题。那么,什么是函数的参数入口?例子如下:

//函数声明
unsigned long PinJunZhi(unsigned char a,unsigned char b,unsigned char c,unsigned char d);

//变量定义
unsigned char Gu8Buffer={2,6,8,4};//4个变量分别是2,6,8,4。
unsigned long Gu32PinJunZhi;//求平均值的结果

//函数定义
unsigned long PinJunZhi(unsigned char a,unsigned char b,unsigned char c,unsigned char d)
{
   unsigned long u32PinJunZhi;
   u32PinJunZhi=(a+b+c+d)/4;   
   return u32PinJunZhi;
}

    void main() //主函数
{
    //函数调用
    Gu32PinJunZhi=PinJunZhi(Gu8Buffer,Gu8Buffer,Gu8Buffer,Gu8Buffer);

}

       上面是一个求4个数据平均值的函数,在这个函数中,函数小括号的(unsigned char a,unsigned char b,unsigned char c,unsigned char d)就是4个变量的“普通局部变量”参数入口,刚才说到,如果一个数组有上百个变量,这种书写方式是很累的。如果改用“指针”入口参数的方式,例子如下:
//函数声明
unsigned long PinJunZhi(unsigned char *pu8Buffer);

//变量定义
unsigned char Gu8Buffer={2,6,8,4};//4个变量分别是2,6,8,4。
unsigned long Gu32PinJunZhi;//求平均值的结果

//函数定义
unsigned long PinJunZhi(unsigned char *pu8Buffer)
{
   unsigned long u32PinJunZhi;
   u32PinJunZhi=(pu8Buffer+pu8Buffer+pu8Buffer+pu8Buffer)/4;   
   return u32PinJunZhi;
}

    void main() //主函数
{
    //函数调用
    Gu32PinJunZhi=PinJunZhi(&Gu8Buffer);//也等效于Gu32PinJunZhi=PinJunZhi(Gu8Buffer)
}

      上面例子中,仅用一个(unsigned char *pu8Buffer)指针入口参数,就可以达到输入4个变量的目的(这4个变量要求是同在一个数组内)。
【65.2   const在指针参数“入口”中的作用。】
      指针在函数的参数入口中,既可以做“入口”,也可以做“出口”,而C语言为了区分这两种情况,提供了const这个关键字来限定权限。如果指针加了const前缀,就为指针的权限加了紧箍咒,限定了此指针只能作为“入口”,而不能作为“出口”。如果没有加了const前缀,就像本节的函数例子,此时指针参数既可以作为“入口”,也可以作为“出口”。加const关键字有两个意义,一方面是方便阅读,通过const就知道此接口的“入口”和“出口”属性,另一方面,是为了代码的安全,对于只能作为“入口”的指针参数一旦加了const限定,万一我们不小心在函数内部对const限定的指针所关联的数据进行了更改(“更改”就意味着“出口”),C编译器在编译的时候就会有提醒或者报错,及时让我们发现程序的bug(程序的漏洞)。这部分的内容后续章节会讲到,大家先有个大概的了解,本节暂时不深入讲。
【65.3   例程练习和分析。】
      现在编一个练习程序。
/*---C语言学习区域的开始。-----------------------------------------------*/

//函数声明
unsigned long PinJunZhi(unsigned char *pu8Buffer);

//变量定义
unsigned char Gu8Buffer={2,6,8,4};//4个变量分别是2,6,8,4。
unsigned long Gu32PinJunZhi;//求平均值的结果

//函数定义
unsigned long PinJunZhi(unsigned char *pu8Buffer)
{
   unsigned long u32PinJunZhi;
   u32PinJunZhi=(pu8Buffer+pu8Buffer+pu8Buffer+pu8Buffer)/4;   
   return u32PinJunZhi;
}

void main() //主函数
{
//函数调用
Gu32PinJunZhi=PinJunZhi(&Gu8Buffer);//也等效于Gu32PinJunZhi=PinJunZhi(Gu8Buffer)
    View(Gu32PinJunZhi); //把第1个数Gu32PinJunZhi发送到电脑端的串口助手软件上观察。
    while(1)
    {
    }
}
/*---C语言学习区域的结束。-----------------------------------------------*/

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

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

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

jianhong_wu 发表于 2017-4-23 09:46:07

本帖最后由 jianhong_wu 于 2017-4-23 10:07 编辑

第六十六节: 指针作为数组在函数中的出口作用。

【66.1   指针作为数组在函数中的出口。】

       函数对外部调用者传递信息主要有三类渠道,第一类是全局变量,第二类是return返回值,第三类是指针。之前讲指针对外传递信息的时候,只讲了单个变量的情况,现在重点讲讲数组的情况。要把一个四位数的个,十,百,千位分别提取出来成为4个数,依次存放在一个包含4个字节的数组里,代码如下:

void TiQu(unsigned int u16Data,unsigned char *pu8Buffer) //“提取”函数
{
   unsigned char u8Ge; //个位
   unsigned char u8Shi; //十位
   unsigned char u8Bai; //百位
   unsigned char u8Qian; //千位

   u8Ge=u16Data/1%10;       //提取个位
   u8Shi=u16Data/10%10;   //提取十位
   u8Bai=u16Data/100%10;    //提取百位
   u8Qian=u16Data/1000%10;//提取千位

   //最后,把所提取的数分别传输到“指针”这个“出口通道”
   pu8Buffer=u8Ge;
   pu8Buffer=u8Shi;
   pu8Buffer=u8Bai;
   pu8Buffer=u8Qian;

}

       上述代码,为了突出“出口通道”,我刻意多增加了u8Ge、u8Shi、u8Bai、u8Qian这4个局部变量,其实,这4个局部变量还可以省略的,此函数简化后的等效代码如下:

void TiQu(unsigned int u16Data,unsigned char *pu8Buffer) //“提取”函数
{

   pu8Buffer=u16Data/1%10;       //提取个位
   pu8Buffer=u16Data/10%10;      //提取十位
   pu8Buffer=u16Data/100%10;   //提取百位
   pu8Buffer=u16Data/1000%10;    //提取千位
}

【66.2   例程练习和分析。】

      现在编一个练习程序。

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

//函数声明
void TiQu(unsigned int u16Data,unsigned char *pu8Buffer);

//全局变量定义
unsigned char Gu8Buffer;//存放提取结果的数组

//函数定义
void TiQu(unsigned int u16Data,unsigned char *pu8Buffer) //“提取”函数
{

   pu8Buffer=u16Data/1%10;       //提取个位
   pu8Buffer=u16Data/10%10;      //提取十位
   pu8Buffer=u16Data/100%10;   //提取百位
   pu8Buffer=u16Data/1000%10;    //提取千位
}


void main() //主函数
{
TiQu(9876,&Gu8Buffer);//把9876这个四位数分别提取6、7、8、9存放在数组Gu8Buffer里

View(Gu8Buffer); //把第1个数Gu8Buffer)发送到电脑端的串口助手软件上观察
View(Gu8Buffer); //把第2个数Gu8Buffer)发送到电脑端的串口助手软件上观察
View(Gu8Buffer); //把第3个数Gu8Buffer)发送到电脑端的串口助手软件上观察
View(Gu8Buffer); //把第4个数Gu8Buffer)发送到电脑端的串口助手软件上观察

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

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

开始...

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

第2个数
十进制:7
十六进制:7
二进制:111

第3个数
十进制:8
十六进制:8
二进制:1000

第4个数
十进制:9
十六进制:9
二进制:1001

分析:
      Gu8Buffer为6。
      Gu8Buffer为7。
      Gu8Buffer为8。
      Gu8Buffer为9。

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

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


jianhong_wu 发表于 2017-4-30 08:39:57

本帖最后由 jianhong_wu 于 2017-4-30 08:57 编辑

第六十七节: 指针作为数组在函数中既“入口”又“出口”的作用。

【67.1   指针作为数组在函数中的“入口”和“出口”。】

       前面分别讲了指针的入口和出口,很多初学者误以为指针是一个“单向”的通道,其实,如果指针前面没有加const这个“紧箍咒”限定它的属性,指针是“双向”的,不是“单向”的,也就是说,指针是可以同时具备“入口”和“出口”这两种属性的。现在讲一个程序例子,求一个数组(内含4元素)的每个元素变量的整数倍的一半,所谓整数倍的一半,就是除以2,但是不带小数点,比如4的整数倍的一半是2,7的整数倍的一半是3(不是3.5),代码如下:

void Half(unsigned char *pu8Buffer) //“求一半”的函数
{
   unsigned char u8Data_0; //临时中间变量
   unsigned char u8Data_1; //临时中间变量
   unsigned char u8Data_2; //临时中间变量
   unsigned char u8Data_3; //临时中间变量

   //从指针这个“入口”里获取需要“被除以2”的数据。
   u8Data_0=pu8Buffer;
   u8Data_1=pu8Buffer;
   u8Data_2=pu8Buffer;
   u8Data_3=pu8Buffer;

   //求数据的整数倍的一半的算法
   u8Data_0=u8Data_0/2;
   u8Data_1=u8Data_1/2;
   u8Data_2=u8Data_2/2;
   u8Data_3=u8Data_3/2;

   //最后,把计算所得的结果分别传输到指针这个“出口”
   pu8Buffer=u8Data_0;
   pu8Buffer=u8Data_1;
   pu8Buffer=u8Data_2;
   pu8Buffer=u8Data_3;

}

       上述代码,为了突出“入口”和“出口”,我刻意多增加了u8Data_0,u8Data_1,u8Data_2,u8Data_3这4个临时中间变量,其实,这4个临时中间变量还可以省略的,此函数简化后的等效代码如下:

void Half(unsigned char *pu8Buffer) //“求一半”的函数
{
   pu8Buffer=pu8Buffer/2;
   pu8Buffer=pu8Buffer/2;
   pu8Buffer=pu8Buffer/2;
   pu8Buffer=pu8Buffer/2;
}

【67.2   例程练习和分析。】

      现在编一个练习程序。

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

//函数声明
void Half(unsigned char *pu8Buffer);

//全局变量定义
unsigned char Gu8Buffer={4,7,16,25};//需要“被除以2”的数组

//函数定义
void Half(unsigned char *pu8Buffer) //“求一半”的函数
{
   pu8Buffer=pu8Buffer/2;
   pu8Buffer=pu8Buffer/2;
   pu8Buffer=pu8Buffer/2;
   pu8Buffer=pu8Buffer/2;
}

void main() //主函数
{
    Half(&Gu8Buffer); //计算数组的整数倍的一半。这里的“入口”和“出口”是“同一个通道”。

View(Gu8Buffer); //把第1个数Gu8Buffer)发送到电脑端的串口助手软件上观察
View(Gu8Buffer); //把第2个数Gu8Buffer)发送到电脑端的串口助手软件上观察
View(Gu8Buffer); //把第3个数Gu8Buffer)发送到电脑端的串口助手软件上观察
View(Gu8Buffer); //把第4个数Gu8Buffer)发送到电脑端的串口助手软件上观察

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

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

开始...

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

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

第3个数
十进制:8
十六进制:8
二进制:1000

第4个数
十进制:12
十六进制:C
二进制:1100

分析:
      Gu8Buffer为2。
      Gu8Buffer为3。
      Gu8Buffer为8。
      Gu8Buffer为12。

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

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


jianhong_wu 发表于 2017-5-7 10:24:29

本帖最后由 jianhong_wu 于 2017-5-7 10:39 编辑

第六十八节: 为函数接口指针“定向”的const关键词。

【68.1   为函数接口指针“定向”的const关键词。】

       在函数接口处的指针,是一个双向口,既可以作为“输入”也可以作为“输出”,换句话说,既能“读”也能“写”(被更改),这样一来,当你把一个数组(或者某变量)通过指针引入到函数内部的时候,当执行完此函数,这个数组的数值可能已经悄悄发生了更改(“是否被更改”取决于函数内部的具体代码),进来时是“摩托”出来后可能已变成“单车”,而实际项目上,很多时候我们只想传递数组(或者某变量)的数值,并不想数组(或者某变量)本身发生变化,这个时候,本节的主角const关键词就派上用场了。
只要在函数接口的指针前面加上const关键词,原来双向的指针就立刻变成了单向,只能输入不能输出。这个const有两个好处。第一个好处是方便阅读,通过const就知道此接口的“入口”和“出口”属性,如果你是用别人已经封装好的函数,一旦发现接口指针带了const标签,就足以说明这个指针只能作为输入接口,不用担心输入数据被意外修改。第二个好处是确保数据的安全,函数接口指针一旦加了const限定,万一你不小心在函数内部对指针所关联的数据进行了更改(“更改”就意味着“出口”),C编译器在编译的时候就会报错让你编译失败,及时让你发现程序的bug(程序的漏洞),这是编译器层面的一道防火墙。例子如下:
unsigned char ShuRu(const unsigned char *pu8Data)
{
    unsigned char a;
    a=*pu8Data;//这行代码是合法的,是指针所关联数据的“读”操作。
    *pu8Data=a;//这行代码是非法的,是指针所关联数据的“写”操作,违背const的约束。
    return a;
}

【68.2   例程练习和分析。】

      在前面第65节讲函数入口的时候,用到一个求数组平均值的程序例子,这个数组是仅仅作为输入用的,不需要被更改,因此,现在借本节讲const的机会,为此函数的接口指针补上一个const关键词,让该函数更加科学规范,程序如下:

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

unsigned long PinJunZhi(const unsigned char *pu8Buffer);//指针前增加一个const关键词
unsigned char Gu8Buffer={2,6,8,4};
unsigned long Gu32PinJunZhi;

unsigned long PinJunZhi(const unsigned char *pu8Buffer)   //指针前增加一个const关键词
{
   unsigned long u32PinJunZhi;
   u32PinJunZhi=(pu8Buffer+pu8Buffer+pu8Buffer+pu8Buffer)/4;//求平均值
   return u32PinJunZhi;
}

void main() //主函数
{
Gu32PinJunZhi=PinJunZhi(&Gu8Buffer);//不用担心Gu8Buffer数组的数据被意外更改。

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

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

开始...

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

分析:
      平均值变量Gu32PinJunZhi为5。

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

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


jianhong_wu 发表于 2017-5-14 10:16:30

本帖最后由 jianhong_wu 于 2017-5-14 10:34 编辑

第六十九节: 宏函数sizeof()。

【69.1   宏函数sizeof()的基础知识。】

      宏函数sizeof()是用来获取某个对象所占用的字节数。既然是“宏”,就说明它不是单片机执行的函数,而是单片机之外的C编译器执行的函数(像#define这类宏语句一样),也就是说,在单片机上电之前,C编译器在电脑端翻译我们的C语言程序的时候,一旦发现了这个宏函数sizeof,它就会在电脑端根据C语言程序的一些关键字符(比如“unsigned char,[,]”这类字符)来自动计算这个对象所占用的字节数,然后再把我们C语言程序里所有的sizeof字符替换等效成一个“常量数字”,1代表1个字节,5代表5个字节,1000代表1000个字节。所谓在单片机之外执行的宏函数,就是说,在“计算”这些对象所占的字节数的时候,这个“计算”的工作只占用电脑的内存(C编译器是在电脑上运行的),并不占用单片机的ROM容量和内存。而其它在单片机端执行的“非宏”函数,是占用单片机的ROM容量和内存。比如:
unsgiend char a;            //变量。占用1个字节
unsgiend int b;             //变量。占用2个字节
unsgiend long c;            //变量。占用4个字节
code unsgiend char d;    //常量。占用9个字节

unsigned int Gu16GetBytes;//这个变量用来获取字节数

Gu16GetBytes=sizeof(a);   //单片机上电后,在单片机程序里等效于Gu16GetBytes=1;
Gu16GetBytes=sizeof(b);   //单片机上电后,在单片机程序里等效于Gu16GetBytes=2;
Gu16GetBytes=sizeof(c);   //单片机上电后,在单片机程序里等效于Gu16GetBytes=4;
Gu16GetBytes=sizeof(d);   //单片机上电后,在单片机程序里等效于Gu16GetBytes=9;

      上述的“sizeof字符”在进入到单片机的层面的时候,已经被编译器预先替换成对应的“常量数字”的,这个“常量数字”就代表所占用的字节数。

【69.2   宏函数sizeof()的作用。】

      在项目中,通常用在两个方面:一方面是用在求一个数组的大小尺寸,另一方面是用在计算内存分配时候的偏移量。当然,sizeof并不是“刚需”,如果没有sizeof宏函数,我们也可以人工计算出一个对象所占用的字节数,只是,人工计算,一方面容易出错,另一方面代码往往“动一发而牵全身”,改一个变量往往就会涉及很多地方需要配合调整更改,没法做到“自由裁剪”的境界。下面举一个程序例子:要把3个不同长度的数组“合并”成1个数组。

         第一种情况:在没有使用sizeof宏函数时,人工计算字节数和偏移量:

unsigned char a={1,2}; //占用2个字节
unsigned char b={3,4,5}; //占用3个字节
unsigned char c={6,7,8,9}; //占用4个字节
unsigned charHeBing;//合并a,b,c在一起的数组。这里的9是人工计算a,b,c容量累加所得。
unsigned char i; //循环变量i

for(i=0;i<2;i++)//这里的2,是人工计算出a占用2个字节
{
    HeBing=a;//从HeBing数组的偏移量第0个地址开始存放。
}

for(i=0;i<3;i++)//这里的3,是人工计算出b占用3个字节
{
    HeBing=b;//这里的2是人工计算出的偏移量。a占用了数组2个字节。
}

for(i=0;i<4;i++)//这里的4,是人工计算出c占用4个字节
{
    HeBing=c;//这里的2和3是人工计算出的偏移量,a和b占用了数组2+3个字节。
}

      第二种情况:在使用sizeof宏函数时,利用C编译器自动来计算字节数和偏移量:

unsigned char a={1,2}; //占用2个字节
unsigned char b={3,4,5}; //占用3个字节
unsigned char c={6,7,8,9}; //占用4个字节
unsigned charHeBing;//C编译器自动计算字节数
unsigned char i;

for(i=0;i<sizeof(a);i++)//C编译器自动计算字节数
{
    HeBing=a;
}

for(i=0;i<sizeof(b);i++)//C编译器自动计算字节数
{
    HeBing=b;//C编译器自动计算偏移量
}

for(i=0;i<sizeof(c);i++)//C编译器自动计算字节数
{
    HeBing=c; //C编译器自动计算偏移量
}

【69.3   例程练习和分析。】

      现在编写一个练习的程序:

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

unsigned char a={1,2}; //占用2个字节
unsigned char b={3,4,5}; //占用3个字节
unsigned char c={6,7,8,9}; //占用4个字节
unsigned charHeBing;//C编译器自动计算字节数
unsigned char i;

void main() //主函数
{
for(i=0;i<sizeof(a);i++)//C编译器自动计算字节数
{
    HeBing=a;
}

for(i=0;i<sizeof(b);i++)//C编译器自动计算字节数
{
    HeBing=b;//C编译器自动计算偏移量
}

for(i=0;i<sizeof(c);i++)//C编译器自动计算字节数
{
    HeBing=c; //C编译器自动计算偏移量
}

for(i=0;i<sizeof(HeBing);i++)//利用宏sizeof计算出HeBing数组所占用的字节数
    {
    View(HeBing);   //把HeBing所有数据挨个依次全部发送到电脑端观察
}

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

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

开始...

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

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

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

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

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

第6个数
十进制:6
十六进制:6
二进制:110

第7个数
十进制:7
十六进制:7
二进制:111

第8个数
十进制:8
十六进制:8
二进制:1000

第9个数
十进制:9
十六进制:9
二进制:1001

分析:
      HeBing为1。
      HeBing为2。
      HeBing为3。
      HeBing为4。
      HeBing为5。
      HeBing为6。
      HeBing为7。
      HeBing为8。
      HeBing为9。

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

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


jianhong_wu 发表于 2017-5-21 10:14:47

本帖最后由 jianhong_wu 于 2017-5-21 10:24 编辑

第七十节: “万能数组”的结构体。

【70.1   结构体与数组。】

       结构体是数组,但不是普通的数组,而是一种“万能数组”。普通数组,是依靠严格的数组下标(类似编号)来识别某个具体单元的(或者称“寻址”),期间,如果要往数组插入或者删除某些单元,后面所有单元的下标编号都会发生改变,牵一发而动全身,后面其它单元的下标序号自动重新排列,原来某个特定的单元的下标发生了改变,也就意味着“名字”发生了改变,这种情况在编写程序的时候,就意味着很多代码需要随着更改调整,给程序员带来很多不便。怎么办?结构体此时横空出世,扭转了这种“不便”的局面。之所以称结构体为“万能数组”,是因为结构体内部没有“下标编号”,只有名字。结构体与普通数组的本质区别是,结构体是靠“名字”来寻址的,不管你往结构体里插入或者删除某些单元,其它单元的“名字”不会发生改变,隔离效果好,左邻右舍不会受影响。除此之外,结构体内部的成员变量是允许出现不同的数据类型的,比如unsigned char,unsigned int,unsigned long这三种数据类型的变量都可以往同一个结构体里面“填充”,不受类型的局限,真正做到“万能”级。而普通数组就没有这个优越性,普通数组要么清一色都是unsigned char,要么清一色都是unsigned int,要么清一色都是unsigned long,不能像结构体这么“混合型”的。结构体的这种优越性,在大型程序的升级和维护时体现得非常明显。

【70.2   “造模”和“生成”和“调用”。】

       结构体的使用,有三道标准工序“造模”和“生成”和“调用”。塑胶外壳,必须先开模具(造模),然后再用模具印出外壳(生成),再把外壳应用于日常生活中(调用)。结构体也一样,先“造”结构体的“模”(造模),再根据这个“模”来“生成”一个结构体变量(生成),然后在某函数里使用此变量(调用)。例子如下:

struct StructMould   //“造模”
{
   unsigned charu8Data_A;   
   unsigned int   u16Data_B;
       unsigned longu32Data_C;
    };

struct StructMouldGtMould;//“生成”一个变量GtMould。

void main()
{
    GtMould.u8Data_A=1;      //依靠成员的“名字”来“调用”
    GtMould.u16Data_B=2;   //依靠成员的“名字”来“调用”
    GtMould.u32Data_C=3;   //依靠成员的“名字”来“调用”

    while(1)
    {

    }
}

       把上述程序转换成“普通数组”和“指针”的形式,给大家一个直观的对比,代码如下:


unsigned char Gu8MouldBuffer;//相当于结构体变量GtMould

unsigned char *pu8Data_A;
unsigned int*pu16Data_B;
unsigned long *pu32Data_C;

void main()
{
    pu8Data_A=(unsigned char *)&Gu8MouldBuffer;//依靠数组的下标来“调用”
    *pu8Data_A=1;

    pu16Data_B=(unsigned int *)&Gu8MouldBuffer;//依靠数组的下标来“调用”
    *pu16Data_B=2;

    pu32Data_C=(unsigned long *)&Gu8MouldBuffer; //依靠数组的下标来“调用”
    *pu32Data_C=3;

    while(1)
    {

    }
}

      分析:上述两种代码,目标都是把1,2,3这三个数字存放在一个数组里。第一种用结构体的方式,第二种用普通数组的方式。

【70.3   例程练习和分析。】

      现在编写一个练习的程序:

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

struct StructMould   //“造模”
{
   unsigned charu8Data_A;   
   unsigned int   u16Data_B;
       unsigned longu32Data_C;
};

struct StructMouldGtMould;//“生成”一个变量GtMould。

void main() //主函数
{
GtMould.u8Data_A=1;      //依靠成员的“名字”来“调用”
GtMould.u16Data_B=2;       //依靠成员的“名字”来“调用”
GtMould.u32Data_C=3;       //依靠成员的“名字”来“调用”

View(GtMould.u8Data_A);    //把结构体成员GtMould.u8Data_A发送到电脑端观察
View(GtMould.u16Data_B);   //把结构体成员GtMould.u16Data_B发送到电脑端观察
View(GtMould.u32Data_C);   //把结构体成员GtMould.u32Data_C发送到电脑端观察

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

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

开始...

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

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

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

分析:
      GtMould.u8Data_A为1。
      GtMould.u16Data_B为2。
      GtMould.u32Data_C为3。

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

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


jianhong_wu 发表于 2017-5-29 11:10:19

本帖最后由 jianhong_wu 于 2017-5-29 11:33 编辑

第七十一节: 结构体的内存和赋值。

【71.1   结构体的内存生效。】

       上一节讲到结构体有三道标准工序“造模”和“生成”和“调用”,那么,结构体在哪道工序的时候才会开始占用内存(或者说内存生效)?答案是在第二道工序“生成”(或者说定义)的时候才产生内存开销。第一道工序仅“造模”不“生成”是不会产生内存的。什么意思呢?请看下面的例子。

      第一种情况:仅“造模”不“生成”。

struct StructMould   //“造模”
{
   unsigned charu8Data_A;   
   unsigned charu8Data_B;   
    };

       分析:这种情况是没有内存开销的,尽管你已经写下了数行代码,但是C编译器在翻译此代码的时候,它会识别到你偷工减料仅仅“造模”而不“生成”新变量,此时C编译器会把你这段代码忽略而过。

       第二种情况:先“造模”再“生成”。

struct StructMould   //“造模”
{
   unsigned charu8Data_A;   
   unsigned charu8Data_B;   
};

struct StructMouldGtMould_1;//“生成”一个变量GtMould_1。占用2个字节内存
struct StructMouldGtMould_2;//“生成”一个变量GtMould_2。占用2个字节内存


       分析:这种情况才会占用内存。你“生成”变量越多,占用的内存就越大。像本例子,“生成”了两个变量GtMould_1和GtMould_2,一个变量占用2个字节,两个就一共占用了4个字节。结论:内存的占用是跟变量的“生成”有关。

【71.2   结构体的内存对齐。】

       什么是对齐?为了确保内存的地址能整除某个“对齐倍数”(比如4)。比如以4为“对齐倍数”,在地址0存放一个变量a,因为地址0能整除“对齐倍数”4,所以符合“地址对齐”,接着往下再存放第二个变量b,紧接着的地址1不能整除“对齐倍数”4,此时,为了内存对齐,本来打算把变量b放到地址1的,现在就要更改挪到地址4才符合“地址对齐”,这就是内存对齐的含义。“对齐倍数”是什么?“对齐倍数”就是单片机的位数除以8。比如8位单片机的“对齐倍数”是1(8除以8),16位单片机是2(16除以8),32位单片机是4(32除以8)。本教材所用的单片机是8位的51内核单片机,因此“对齐倍数”是1。1是可以被任何整数整除的,因此,8位单片机在结构体的使用上被内存对齐的“干扰”是最小的。


       为什么要对齐?单片机内部硬件层面一条指令处理的数据宽度是固定的,比如,因为一个字节是8位,所以,8位的单片机一次处理的数据宽度是1个字节(8除以8等于1),16位的单片机一次处理的数据宽度是2个字节(16位除以8位等于2),32位的单片机一次处理的数据宽度是4个字节(32位除以8位等于4),如果字节不对齐,本来单片机一个指令能处理的数据可能就要分解成2个指令甚至更多的指令,所以C编译器为了让单片机处于最佳状态,在某些情况就会涉及内存对齐,结构体就涉及到内存对齐。
结构体的内存对齐表现在哪里呢?请看下面两个例子:

       第一个例子:8位单片机。

struct StructMould_1   //“造模”
{
   unsigned charu8Data;   //一个unsigned char占用1个字节。
   unsigned longu32Data;    //一个unsigned long占用4个字节。
};

struct StructMould_1GtMould_1;//占用多少个字节内存呢?

       分析:GtMould_1这个变量占用多少个内存字节呢?假设GtMould_1的首地址是0,那么地址0就存放成员u8Data,u8Data占用1个字节,所以接下来的地址是1(0+1),问题来了,地址1能直接存放占用4个字节的成员u32Data吗?因为8位单片机的“对齐倍数”是1(8除以8),那么地址1显然是可以整除“对齐倍数”1的,因此,地址1是可以果断存储u32Data成员的。因此,GtMould_1占用的总字节数是5(1+4),也就是u8Data和u32Data两者所占字节数之和。

       第二个例子:32位单片机。

struct StructMould_1   //“造模”
{
   unsigned charu8Data;   //一个unsigned char占用1个字节。
   unsigned longu32Data;    //一个unsigned long占用4个字节。
};

struct StructMould_1GtMould_1;//占用多少个字节内存呢?

      分析:GtMould_1这个变量占用多少个内存字节呢?假设GtMould_1的首地址是0,那么地址0就存放成员u8Data,u8Data占用1个字节,所以接下来的地址是1(0+1),那么问题来了,地址1能直接存放占用4个字节的成员u32Data吗?不能。因为32位单片机的“对齐倍数”是4(32除以8),那么地址1显然是不可以整除“对齐倍数”4的,因此,就要把地址1更改挪到地址4这里才符合“地址对齐”,这样,就意味着多插入了3个“填充的字节”,因此,GtMould_1占用的总字节数是8(1+3+4),也就是“1个字节u8Data,3个填充字节,4个u32Data”三者所占字节数之和。那么问题又来了,如果把结构体内部成员u8Data和u32Data的位置顺序更改一下,内存容量会有所改变吗?位置顺序更改后如下。

struct StructMould_1   //“造模”
{
   unsigned longu32Data;    //一个unsigned long占用4个字节。
   unsigned charu8Data;   //一个unsigned char占用1个字节。
};

struct StructMould_1GtMould_1;//占用多少个字节内存呢?

       分析:更改u8Data和u32Data的位置顺序后,u32Data在前u8Data在后,GtMould_1这个变量占用多少个内存字节呢?假设GtMould_1的首地址是0,那么地址0就存放成员u32Data,u32Data占用4个字节,所以接下来的地址是4(0+4),那么问题来了,地址4能直接存放占用1个字节的成员u8Data吗?能。因为32位单片机的“对齐倍数”是4(32除以8),那么地址4显然是可以整除“对齐倍数”4的,因此,地址4是可以果断存储u8Data的。那么,是不是GtMould_1就占用5个字节呢?不是。因为结构体的内存对齐,还包括另外一条规定,那就是“一个结构体变量所占的内存总容量必须能整除该单片机的“对齐倍数”(单片机的位数除以8),如果不能,C编译器就会擅自在最后一个成员的后面插入若干个“填充字节”来满足这个规则”,根据这条规定,计算所得的总容量5是不能整除“对齐倍数”4的,必须再额外填充3个字节补足到8,才能整除“对齐倍数”4,因此,更改顺序后,GtMould_1还是占用8个字节(4+1+3),前4个字节是u32Data,中间1个字节是u8Data,后3个字节是“填充字节”。

       因为本教程采用的是8位的51内核单片机,因此,在上述这个例子中,GtMould_1所占的字节数是符合“第一个例子”的情况,也就是占用5个字节。内存对齐是遵守几条严格的规则的,我只列出其中最关键的两条给大家大致阅读一下,有一个印象即可,不强求死记硬背,只需知道“结构体因为存在内存对齐,所以实际内存容量是有可能大于内部各成员类型字节数相加之和,尤其是16位或者32位这类单片机”就可以了。

       第(1)条:结构体内部某个成员相对结构体首地址的偏移地址必须能整除该单片机的“对齐倍数”(单片机的位数除以8),如果不能,C编译器就会擅自在各成员之间插入若干个“填充字节”来满足这个规则。
       第(2)条:一个结构体变量所占的内存总容量必须能整除该单片机的“对齐倍数”(单片机的位数除以8),如果不能,C编译器就会擅自在最后一个成员的后面插入若干个“填充字节”来满足这个规则。

【71.3   如何获取某个结构体变量的内存容量?】

       结构体存在内存对齐的问题,就说明它的内存占用情况不会像普通数组那样一目了然,那么,我们编写程序的时候怎么知道某个结构体变量占用了多少个字节数?答案是:用sizeof宏函数。比如:

struct StructMould_1   
{
   unsigned longu32Data;
   unsigned charu8Data;
    };

struct StructMould_1GtMould_1;

unsigned long a; //此变量用来获取结构体变量GtMould_1所占用的字节总数
void main() //主函数
{
   a=sizeof(GtMould_1);//利用宏函数sizeof获取结构体变量所占用的字节总数
}

【71.4   结构体之间的赋值。】

      结构体之间的赋值有两种,第一种是成员之间“一对一”的赋值,第二种是整个结构体之间“面对面”的整体赋值。第一种成员赋值像普通变量赋值那样,没有那么多套路和忌讳,数据传递安全可靠。第二种整个结构体之间赋值在编程体验上带有“一键操作”的快感,但是要注意避开一些“雷区”,首先,整体赋值的前提是必须保证两个结构体变量都是同一个“结构体模板”造出来的变量,不同“模板”的结构体变量之间禁止“整体赋值”,其次,哪怕是“同一个模板”的结构体变量,也并不是所有的“同模板结构体”变量都能实现整个结构体之间的直接赋值,只有在结构体内部成员比较简单的情况下才适合“整体赋值”,如果结构体内部包含有“指针”或者“字符串”或者“其它结构体中的结构体”,这类情况就比较复杂,这时建议大家绕开有“雷区”的“整体赋值”而直接选用安全可靠的“成员赋值”。什么是“成员赋值”什么是“整体赋值”?请看下面两个例子。

      第一种:成员赋值。把结构体变量GtMould_2_A赋值给GtMould_2_B。

struct StructMould_2   //“造模”
{
   unsigned longu32Data;   
   unsigned charu8Data;
};

struct StructMould_2GtMould_2_A;//生成第1个结构体变量
struct StructMould_2GtMould_2_B   //生成第2个结构体变量

void main() //主函数
{
   //先给GtMould_2_A赋初值。
   GtMould_2_A.u32Data=1;
   GtMould_2_A.u8Data=2;

   //通过“成员赋值”,把结构体变量GtMould_2_A赋值给GtMould_2_B。
   GtMould_2_B.u32Data=GtMould_2_A.u32Data;   //成员之间“一对一”的赋值
   GtMould_2_B.u8Data=GtMould_2_A.u8Data;   //成员之间“一对一”的赋值
}

       第二种:整体赋值。把结构体变量GtMould_2_A赋值给GtMould_2_B。

struct StructMould_2   //“造模”
{
   unsigned longu32Data;   
   unsigned charu8Data;
};

struct StructMould_2GtMould_2_A;//生成第1个结构体变量
struct StructMould_2GtMould_2_B   //生成第2个结构体变量

void main() //主函数
{
   //先给GtMould_2_A赋初值。
   GtMould_2_A.u32Data=1;
   GtMould_2_A.u8Data=2;

   //通过“整体赋值”,把结构体变量GtMould_2_A赋值给GtMould_2_B。
   GtMould_2_B=GtMould_2_A;   //整体之间“一次性”的赋值
}

       上述例子中的整体赋值,是因为结构体内部的数据比较“简单”,没有包含“指针”或者“字符串”或者“其它结构体中的结构体”这类数据成员,如果包含这类成员,建议大家不要用整体赋值。比如遇到以下这类结构体就建议大家直接用安全可靠的“成员赋值”:

struct StructMould   //“造模”
{
   unsigned char u8String[]=”String”;//字符串
   unsigned char*pu8Data;   //指针
   struct StructOtherMould GtOtherMould;//结构体中的结构体
};

【71.5   例程练习和分析。】

       现在编写一个练习的程序:

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

struct StructMould_1   //“造模”
{
   unsigned longu32Data;    //一个unsigned long占用4个字节。
   unsigned charu8Data;   //一个unsigned char占用1个字节。
};

struct StructMould_2   //“造模”
{
   unsigned charu8Data;   
   unsigned longu32Data;   
};

struct StructMould_1GtMould_1;//占用多少个字节内存呢?

struct StructMould_2GtMould_2_A;
struct StructMould_2GtMould_2_B;   

unsigned long a; //此变量用来获取结构体变量GtMould_1所占用的字节总数

void main() //主函数
{
a=sizeof(GtMould_1);//利用宏函数sizeof获取结构体变量GtMould_1所占用的字节总数

//先给GtMould_2_A赋初值。
GtMould_2_A.u32Data=1;
GtMould_2_A.u8Data=2;

//通过“整体赋值”,把结构体变量GtMould_2_A赋值给GtMould_2_B。
GtMould_2_B=GtMould_2_A;   //整体之间“一次性”的赋值

View(a);    //把a发送到电脑端观察
View(GtMould_2_B.u32Data);   //把结构体成员GtMould_2_B.u32Data发送到电脑端观察
View(GtMould_2_B.u8Data);   //把结构体成员GtMould_2_B.u8Data发送到电脑端观察

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

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

开始...

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

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

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

分析:
      GtMould_1所占的字节数a为5。
      GtMould_2_B的结构体成员GtMould_2_B.u32Data为1。
      GtMould_2_B的结构体成员GtMould_2_B.u8Data为2。

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

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


jianhong_wu 发表于 2017-6-4 11:59:49

本帖最后由 jianhong_wu 于 2017-6-4 12:10 编辑

第七十二节: 结构体的指针。

【72.1   结构体指针的重要用途。】

       结构体指针有两个重要用途,一个是结构体数据的拆分和打包,另一个是作为结构体数据在涉及函数时的参数入口。
       什么是“结构体数据的拆分和打包”?结构体本质是一个数组,数组内可能包含了许多不同数据长度类型的成员,当我们直接操作某个具体的成员时,只改变某个成员的数值,不影响其它成员,这个就是“拆分”的角度。那么,什么是“打包”?当涉及整个结构体数据的存储或者传输(通信)给另外一个单片机时,这时候有两种选择,一种是一个成员一个成员的挨个处理,这种“拆分”的处理方式比较繁琐麻烦,另外一种就是把整个结构体当作一个以字节为单位的整体数组来处理,这种处理方式就是高速便捷的“打包”处理,但是关键的问题来了,我们把整个结构体数据以字节的方式“打包”传递给另外一个单片机,但是这个单片机接收到我们一组数据后,如何把这“一包”以字节为单位的数组转换成相同的结构体变量,以便在它的程序处理中也能以“拆分”的角度直接处理某个具体的成员变量,这时就涉及到结构体指针的作用。
       什么是“作为结构体数据在涉及函数时的参数入口”?结构体数据一般内部包含了很多成员,当要把这一包数据传递给某个函数内部时,这个函数要给结构体数据预留参数入口,这时,如果函数以结构体成员的角度来预留入口,那么有多少个成员就要预留多少个函数的参数入口,可阅读性非常差,操作起来也麻烦。但是,如果以指针的角度来预留入口,那么不管这个结构体内部包含多少个成员,只需要预留一个指针参数入口就够用了,这就是绝大多32单片机在写库函数时都采样结构体指针作为函数的参数入口的原因。
结构体指针这两个重要用途后续章节会深入讲解,本节的重点是先让大家学会结构体指针的基础知识,为后续章节做准备。

【72.2   结构体指针的基础。】

       操作结构体内部某个具体变量时,有两种方式,一种是成员调用的方式,另一种是指针调用的方式。C语言语法为了区分这两种方式,专门设计了两种不同的操作符号。成员调用方式采样小数点“.”的符号,指针调用方式采用箭头“->”的符号。例子如下:

struct StructMould_1   
{
   unsigned charu8Data_A;   
   unsigned longu32Data_B;   
};

struct StructMould_1   GtMould_1;//“生成”一个变量。   //占用5个字节。
struct StructMould_1*ptMould_1;//定义一个结构体指针。 //占用3个字节。

void main() //主函数
{
    GtMould_1.u8Data_A=5;    //“成员调用”的方式,用小数点符号“.”

    ptMould_1=&GtMould_1;   //ptMould_1指针与变量GtMould_1建立关联。
    ptMould_1->u8Data_A=ptMould_1->u8Data_A+5; //“指针调用”的方式,用箭头符号“->”


    while(1)
    {
    }
}


      分析:上述例子中,信息量很大,知识点有两个。
       第一个知识点:为什么结构体变量GtMould_1占用5个字节,而结构体指针*ptMould_1只占用3个字节?结构体变量GtMould_1所占的内存是由结构体成员内部的数量决定的,而结构体指针*ptMould_1是由C编译器根据芯片硬件寻址范围而决定的,在一个相同的C编译器系统中,所有类型的指针所占用的字节数都是一样的,比如在本教程中所用8位单片机的C51编译器系统中,unsigned char *,unsigned int *,unsigned long *,以及本节的struct StructMould_1 *,都是占用3个字节(题外话,我前面第60节中所说的“凡是32位以下的单片机的指针都是占用4个字节”是有误的,抱歉)。32位单片机的指针往往都是4个字节,而某些64位的PC机,指针可能是8个字节,这些内容大家只要有个大概的了解即可。
       第二个知识点:结构体成员GtMould_1.u8Data_A经过第一步的“成员调用”直接赋值5,紧接着经过“指针调用”的累加5操作,最后GtMould_1.u8Data_A的数值是10(5+5)。

【72.3   例程练习和分析。】

       现在编写一个练习的程序:

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

struct StructMould_1   
{
   unsigned charu8Data_A;   
   unsigned longu32Data_B;   
};

struct StructMould_1   GtMould_1;//“生成”一个变量。   //占用5个字节。
struct StructMould_1*ptMould_1;//定义一个结构体指针。 //占用3个字节。

void main() //主函数
{
   GtMould_1.u8Data_A=5;    //“成员调用”的方式,用小数点符号“.”

   ptMould_1=&GtMould_1;   //ptMould_1指针与变量GtMould_1建立关联。
   ptMould_1->u8Data_A=ptMould_1->u8Data_A+5; //“指针调用”的方式,用箭头符号“->”

View(sizeof(GtMould_1));    //在电脑端观察变量GtMould_1占用多少个字节。
View(sizeof(ptMould_1));    //在电脑端观察指针ptMould_1占用多少个字节。
View(GtMould_1.u8Data_A);   //在电脑端观察结构体成员GtMould_1.u8Data_A的最后数值。
   while(1)
    {
    }
}
/*---C语言学习区域的结束。-----------------------------------------------*/

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

开始...

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

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

第3个数
十进制:10
十六进制:A
二进制:1010

       分析:
       变量GtMould_1占用5个字节。
       指针ptMould_1占用3个字节。
       结构体成员GtMould_1.u8Data_A的最后数值是10。

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

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


jianhong_wu 发表于 2017-6-11 09:17:47

本帖最后由 jianhong_wu 于 2017-6-11 09:39 编辑

第七十三节: 结构体数据的传输存储和还原。

【73.1   结构体数据的传输存储和还原。】

       结构体本质是一个数组,数组内可能包含了许多不同数据长度类型的成员,当整个结构体数据需要存储或者传输(通信)给另外一个单片机时,这时候有两种选择,一种是一个成员一个成员的挨个处理,这种“以成员为单位”的处理方式比较繁琐麻烦,另外一种是把整个结构体变量当作一个“以字节为单位”的普通数组来处理,但是有两个关键的问题来了,第一个问题是如何把结构体“拆分”成“以字节为单位”来进行搬动数据,第二个问题是假如我们把整个结构体数据以“字节为单位”的方式“整体打包”传递给另外一个单片机,当这个接收方的单片机接收到我们这一组数据后,如何把这“一包”以字节为单位的数组再“还原”成相同的结构体变量,以便在程序处理中也能直接按“结构体的方式”来处理某个具体的成员。其实,这两个问题都涉及到“指针的强制转换”。具体讲解的例子,请直接阅读下面73.2段落的源代码例子和注释。

【73.2   例程练习和分析。】

      现在编写一个练习程序,把一个结构体变量“以字节的方式”存储到另外一个普通数组里,然后再把这个“以字节为单位”的普通数组“还原”成“结构体的方式”,以便直接操作内部某个具体的成员。

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

struct StructMould_1   
{
   unsigned charu8Data_A;   
   unsigned longu32Data_B;   
   unsigned int   u16Data_C;   
};

struct StructMould_1   GtMould_1;//“生成”一个变量。

unsigned char Gu8Buffer; //定义一个内存跟结构体变量大小一样的普通数组
unsigned char *pu8;   //定义一个把结构体变量“拆分”成“以字节为单位”的指针
struct StructMould_1   *ptStruct; //定义一个结构体指针,用于“还原”普通数组为“结构体”
unsigned int i;      //定义一个用于for循环的变量

void main() //主函数
{
   //先把该结构体变量内部具体成员分别以“成员的方式”初始化为5,6,7
GtMould_1.u8Data_A=5;   
GtMould_1.u32Data_B=6;
GtMould_1.u16Data_C=7;

pu8=(unsigned char *)&GtMould_1;    //把结构体变量强制转换成“以字节为单位”的指针
for(i=0;i<sizeof(GtMould_1);i++)
{
   Gu8Buffer=pu8;   //把结构体变量以字节的方式搬运并且存储到普通数组里。
}

ptStruct=(struct StructMould_1*)&Gu8Buffer;//再把普通数组强制“还原”成结构体指针
ptStruct->u8Data_A=ptStruct->u8Data_A+1;   //该变量从5自加1后变成6。
ptStruct->u32Data_B=ptStruct->u32Data_B+1; //该变量从6自加1后变成7。
ptStruct->u16Data_C=ptStruct->u16Data_C+1; //该变量从7自加1后变成8。

View(ptStruct->u8Data_A);//在电脑端观察结构体成员u8Data_A的数值。
View(ptStruct->u32Data_B); //在电脑端观察结构体成员u32Data_B的数值。
View(ptStruct->u16Data_C); //在电脑端观察结构体成员u16Data_C的数值。

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

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

开始...

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

第2个数
十进制:7
十六进制:7
二进制:111

第3个数
十进制:8
十六进制:8
二进制:1000

分析:
       结构体成员u8Data_A的数值是6。
       结构体成员u32Data_B的数值是7。
       结构体成员u16Data_C的数值是8。

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

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


jianhong_wu 发表于 2017-6-17 17:43:00

本帖最后由 jianhong_wu 于 2017-6-17 18:02 编辑

第七十四节: 结构体指针在函数接口处的频繁应用。

【74.1   重温“函数的接口参数”。】

       函数的接口参数主要起到标识的作用。比如:
       一个加法函数:
unsigned char add(unsinged char a,unsigned char b)
{
    return (a+b);
}

       这里的a和b就是接口参数,它的作用是告诉人们,你把两个加数分别代入a和b,返回的就是你要的加法运算结果。这里的接口参数就起到入口标识的作用。注意,这句话的关键词是“标识”而不是“入口”,因为函数的“入口”不是唯一的,而是无数条路径。为什么这么说?我们把上面的例子改一下,改成全局变量,例子如下:
       一个加法函数:
unsinged char a;//加数
unsigned char b; //加数
unsigned char c;//和
void add(void)
{
    c=a+b;
}

       上述例子中,尽管我用“两个”void(空的)关键词把原来加法函数的入口(接口参数)和出口(return返回)都堵得死死的,但是,全局变量是无法阻挡的,它进入一个函数的内部不受任何限制,也就是说,我们做项目的时候,如果把所有函数的接口参数和返回都改成void类型,所有的信息传递都改用全局变量,这样也是可以勉强把项目做完成的。但是,如果真的把所有函数的接口参数都改成void,全部靠全局变量来传递信息,那么最大的问题是函数多了之后,阅读非常不方面,你每看到一个被调用的函数,你不能马上猜出它大概跟哪些全局变量发生了关联,你必须一个一个的去查该函数的源代码才能理清楚,针对这个问题,C语言的设计者,给了函数非常丰富的接口参数,最理想的函数是:你把凡是与此函数相关的全局变量都经过接口参数的入口才进入到函数内部,尽量把接口参数的入口看作是函数的唯一合法入口(尽管不是唯一也不是必须),这样只要看函数的接口参数就知道这个函数跟哪些全局变量有关,函数的输入输出就非常清晰明了。但是问题又来了,如果有多少个全局变量就开多少个接口参数,接口参数就会变得非常多,接口参数多了,函数的门面就非常难看,无异于把本来应该“小而窄”的接口设在“宽而广”的平原上,还不如直接用原来那种全局变量强行进入呢。那么,要解决这个问题怎么办?本节的主角“结构体指针”可以解决这个问题。

【74.2   结构体指针在函数接口处的频繁应用。】

       当函数的接口参数非常多的时候,可以把N个相关的全局变量“打包”成一个结构体数据,碰到函数接口的时候,可以通过“结构体指针”以“包”为单位的方式进入,这样就可以让函数的接口参数看起来非常少,这种方法,是很多32位单片机的库函数一直在用的方法,它最重要的好处是简化入口的通道数量。你想想,32位单片机有那么多寄存器,如果没有这种以“结构体指针”为接口参数的方式,它的入口可能需要几十个接口参数,那岂不是非常麻烦?库函数设计的成败与否,本来就在于接口的设计合不合理,“结构体指针作为函数接口参数”在此场合就显得特别有价值,使用了这种方法,函数与全局变量之间,它们的关联脉络再也不用隐藏起来,并且可以很清晰的表达清楚。现在举一个例子,比如有一个函数,要实现把5个全局变量“自加1”的功能,分别使用两种接口参数来实现,例子如下:

       第一种方式:有多少个全局变量就开多少个接口参数。

//函数的声明
void Add_One( unsigned char *pu8Data_1,//第1个接口参数
unsigned char *pu8Data_2,//第2个接口参数
unsigned char *pu8Data_3,//第3个接口参数
unsigned char *pu8Data_4,//第4个接口参数
unsigned char *pu8Data_5);//第5个接口参数

//5个全局变量的定义
unsigned char a;
unsigned char b;
unsigned char c;
unsigned char d;
unsigned char e;

//函数的定义
void Add_One( unsigned char *pu8Data_1,//第1个接口参数
unsigned char *pu8Data_2,//第2个接口参数
unsigned char *pu8Data_3,//第3个接口参数
unsigned char *pu8Data_4,//第4个接口参数
unsigned char *pu8Data_5)//第5个接口参数
{
*pu8Data_1=(*pu8Data_1)+1;   //实现自加1的功能
*pu8Data_2=(*pu8Data_2)+1;
*pu8Data_3=(*pu8Data_3)+1;
*pu8Data_4=(*pu8Data_4)+1;
*pu8Data_5=(*pu8Data_5)+1;
}

void main()
{
    //5个全局变量都初始化为0
a=0;
    b=0;

    c=0;
    d=0;
e=0;

//函数的调用,实现5个变量都“自加1”的功能。加“&”表示“传址”的方式进入函数内部。
Add_One(&a,//第1个接口参数
&b,//第2个接口参数
&c,//第3个接口参数
&d,//第4个接口参数
&e); //第5个接口参数
}

      第二种方式:把N个全局变量打包成一个结构体,以“结构体指针”的方式进入函数内部。

    //函数的声明
void Add_One(struct StructMould *ptMould);//只有1个结构体指针,大大减小了接口参数。

//结构体的“造模”
struct StructMould
{
unsigned char a;
unsigned char b;
unsigned char c;
unsigned char d;
unsigned char e;
};

struct StructMould GtMould;//生成一个结构体变量,内部包含了5个全局变量a,b,c,d,e。

//函数的定义
void Add_One(struct StructMould *ptMould)//只有1个结构体指针,大大减小了接口参数。
{
    ptMould->a=ptMould->a+1;//实现“自加1”的功能。
    ptMould->b=ptMould->b+1;
    ptMould->c=ptMould->c+1;
    ptMould->d=ptMould->d+1;
    ptMould->e=ptMould->e+1;
}

void main()
{
      //5个全局变量的结构体成员都初始化为0
GtMould.a=0;
    GtMould.b=0;

    GtMould.c=0;
    GtMould.d=0;
GtMould.e=0;

//函数的调用,实现5个变量都“自加1”的功能。加“&”表示“传址”的方式进入函数内部。
Add_One(&GtMould);//只有1个结构体指针,大大减小了接口参数。
}

【74.3   例程练习和分析。】

      现在编写一个“以结构体指针为函数接口参数”的练习程序。

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

//函数的声明
void Add_One(struct StructMould *ptMould);//只有1个结构体指针,大大减小了接口参数。

//结构体的“造模”
struct StructMould
{
unsigned char a;
unsigned char b;
unsigned char c;
unsigned char d;
unsigned char e;
};

struct StructMould GtMould;//生成一个结构体变量,内部包含了5个全局变量a,b,c,d,e。

//函数的定义
void Add_One(struct StructMould *ptMould)//只有1个结构体指针,大大减小了接口参数。
{
    ptMould->a=ptMould->a+1;//实现“自加1”的功能。
    ptMould->b=ptMould->b+1;
    ptMould->c=ptMould->c+1;
    ptMould->d=ptMould->d+1;
    ptMould->e=ptMould->e+1;
}

void main() //主函数
{
      //5个全局变量的结构体成员都初始化为0
GtMould.a=0;
    GtMould.b=0;

    GtMould.c=0;
    GtMould.d=0;
GtMould.e=0;

//函数的调用,实现5个变量都“自加1”的功能。加“&”表示“传址”的方式进入函数内部。
Add_One(&GtMould);//只有1个结构体指针,大大减小了接口参数。

View(GtMould.a);//在电脑端观察结构体成员GtMould.a的数值。
   View(GtMould.b);//在电脑端观察结构体成员GtMould.b的数值。
View(GtMould.c);//在电脑端观察结构体成员GtMould.c的数值。
View(GtMould.d);//在电脑端观察结构体成员GtMould.d的数值。
View(GtMould.e);//在电脑端观察结构体成员GtMould.e的数值。

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

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

开始...

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

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

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

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

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

分析:
         结构体成员GtMould.a的数值是1。
         结构体成员GtMould.b的数值是1。
         结构体成员GtMould.c的数值是1。
         结构体成员GtMould.d的数值是1。
         结构体成员GtMould.e的数值是1。


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

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


jianhong_wu 发表于 2017-6-25 10:36:31

本帖最后由 jianhong_wu 于 2017-6-25 11:14 编辑

第七十五节: 指针的名义(例:一维指针操作二维数组)。

【75.1   指针的名义。】

       刚开始接触指针往往有这种感觉,指针的江湖很乱,什么“乱七八糟”的指针都能冒出来,空指针,指针的指针,函数的指针,各种名目繁多的指针,似乎都可以打着指针的名义让你招架不住,而随着我们功力的提升,会逐渐拨开云雾,发现指针的真谛不外乎三个,第一个是所有的指针所占用字节数都一样,第二个是所有指针的操作本质都是“取地址”,第三个是所有各种不同类型的指针之间的转换都可以用“小括号的类型强制转换”。

【75.2   一维指针操作二维数组。】

       C语言讲究门当户对,讲究类型匹配,什么类型的指针就操作什么类型的数据,否则C编译器在翻译代码的时候,会给予报错或者警告。如果想甩开因类型不匹配而导致的报错或者警告,就只能使用“小括号的类型强制转换”,这个方法在项目中应用很频繁,也很实用。一维指针想直接操作二维数组也是必须使用“小括号的类型强制转换”。实际项目中为什么会涉及“一维指针想直接操作二维数组”?二维数组更加像一个由行与列组合而成的表格,而且每行单元的内存地址是连续的,并且上下每行与每行之间的首尾单元的内存地址也是连续的,凡是内存地址连续的都是指针的菜。我曾遇到这样一种情况,要从一个二维表格里提取某一行数据用来显示,而这个显示函数是别人封装好的一个库函数,库函数对外的接口是一维指针,这样,如何把二维表格(二维数组)跟一维指针在接口上兼容起来,就是一个要面临的问题,这时有两种思路,一种是把二维数组的某一行数据先用原始的办法提取出来存放在一个中间变量的一维数组,然后再把这个一维数组代入到一维指针接口的库函数里,另一种思路是绕开中间变量,直接把二维数组的某一行的地址强制转换成一维指针的类型,利用“类型强制转换”绕开C编译器的报错或警告,实现二维数组跟一维指针“直通”,经过实验,这种方法果然可以,从此对指针的感悟就又上了一层,原来,指针的“取地址”是不仅仅局限于某个数组的首地址,它完全可以利用类型强制转换的小括号“()”与取地址符号“&”结合起来,让指针跟一维数组或者二维数组里面任何一个单元直接关联起来。请看下面两个例子,用一维指针提取二维数组里面某一行的数据,第一个例子是在程序处理中的类型强制转换的应用,第二个例子是在函数接口中的类型强制转换的应用。

【75.3   在程序处理中的类型转换。】


unsigned char table[]=//二维数组
{
{0x00,0x01,0x02},//二维数组的第0行数据
{0x10,0x11,0x12},//二维数组的第1行数据
{0x20,0x21,0x22},//二维数组的第2行数据
};

unsigned char *pGu8;    //一维指针
unsigned charGu8Buffer;    //一维数组,存放从二维数组里提取出来的某一行数据
unsigned chari; // for循环的变量
void main()
{
    pGu8=(unsigned char *)&table;//利用类型强制转换使得一维指针跟二维数组关联起来。
    for(i=0;i<3;i++)
{
    Gu8Buffer=pGu8;   //提取二维数组的第2行数据,存入到一个一维数组里。
}

    while(1)
{

}
}

【75.4   在函数接口中的类型转换。】

在函数接口中,也可以利用类型强制转换来实现函数接口的匹配问题,比如,下面这个写法也是合法的。


void GetRowData(unsigned char *pu8); //函数的声明

unsigned char table[]=//二维数组
{
{0x00,0x01,0x02},//二维数组的第0行数据
{0x10,0x11,0x12},//二维数组的第1行数据
{0x20,0x21,0x22},//二维数组的第2行数据
};

unsigned charGu8Buffer;    //一维数组,存放从二维数组里提取出来的某一行数据

void GetRowData(unsigned char *pu8)//一维指针的函数接口
{
unsigned chari; // for循环的变量
    for(i=0;i<3;i++)
{
    Gu8Buffer=pu8;   //提取二维数组的某行数据,存入到一个一维数组里。
}
}

void main()
{
GetRowData((unsigned char *)&table); //利用类型强制转换来兼容一维指针的函数接口

    while(1)
{

}
}

【75.5   注意指针或者数组越界的问题。】

       上述例子中,二维数组内部只有9个数据,如果指针操作的数据超过了这9个数据的地址范围,就会导致系统其它无辜的数据受到破坏,这个问题导致的后果是很严重的,这类指针或者数组越界的问题,大家平时做项目时必须留心注意。

【75.6   例程练习和分析。】

      现在编写一个练习程序。


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

void GetRowData(unsigned char *pu8); //函数的声明

unsigned char table[]=//二维数组
{
{0x00,0x01,0x02},//二维数组的第0行数据
{0x10,0x11,0x12},//二维数组的第1行数据
{0x20,0x21,0x22},//二维数组的第2行数据
};

unsigned charGu8Buffer;    //一维数组,存放从二维数组里提取出来的某一行数据

void GetRowData(unsigned char *pu8)//一维指针的函数接口
{
unsigned chari; // for循环的变量
    for(i=0;i<3;i++)
{
    Gu8Buffer=pu8;   //提取二维数组的某行数据,存入到一个一维数组里。
}
}

void main() //主函数
{
GetRowData((unsigned char *)&table); //利用类型强制转换来兼容一维指针的函数接口

View(Gu8Buffer);//在电脑端观察存放二维数组某行数据的一维数组的内容
    View(Gu8Buffer);//在电脑端观察存放二维数组某行数据的一维数组的内容
View(Gu8Buffer);//在电脑端观察存放二维数组某行数据的一维数组的内容
      while(1)
      {
      }
}
/*---C语言学习区域的结束。-----------------------------------------------*/


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

开始...

第1个数
十进制:32
十六进制:20
二进制:100000

第2个数
十进制:33
十六进制:21
二进制:100001

第3个数
十进制:34
十六进制:22
二进制:100010

分析:
         Gu8Buffer是十六进制的0x20,提取了二维数组第2行中的某数据。
         Gu8Buffer是十六进制的0x21,提取了二维数组第2行中的某数据。
         Gu8Buffer是十六进制的0x22,提取了二维数组第2行中的某数据。

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

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


jianhong_wu 发表于 2017-7-9 09:09:38

本帖最后由 jianhong_wu 于 2017-7-9 09:13 编辑

第七十六节: 二维数组的指针。

【76.1   二维数组指针的用途。】

       前面章节讲了一维指针操作二维数组,本质是通过“类型强制转换”实现的,这种应用局限于某些特定的场合,毕竟一维有1个下标,二维有2个下标,一维和二维在队形感上是有明显差别的,强行用一维指针操作二维数组会破坏了代码原有的队形感,大多数的情况,还是用二维指针操作二维数组。
二维指针主要应用在两个方面,一方面是N个二维数组的“中转站”应用,另一方面是函数接口的应用。比如,当某项目有N个二维数组表格时,要通过某个变量来切换处理某个特定的表格,以便实现“N选一”的功能,此时,二维指针在这N个二维数组之间就起到中转站的作用。又,当某个函数接口想输入或者输出一个二维数组时,就必然要用到二维指针作为函数的接口参数。

【76.2   二维指针的“中转站”应用。】

       举一个例子,有3个现有的二维数组,通过某个变量来选择切换,把某个二维数组的数据复制到指定的一个缓存数组中。

code unsigned char table_1=//第1个现有的二维数组
{
{0x00,0x01,0x02},
{0x10,0x11,0x12},
{0x20,0x21,0x22},
};

code unsigned char table_2=//第2个现有的二维数组
{
{0xA0,0xA1,0xA2},
{0xB0,0xB1,0xB2},
{0xC0,0xC1,0xC2},
};

code unsigned char table_3=//第3个现有的二维数组
{
{0xD0,0xD1,0xD2},
{0xE0,0xE1,0xE2},
{0xF0,0xF1,0xF2},
};

unsigned char SaveBuffer;//指定的一个缓存数组

unsigned char TableSec;//选择变量
const unsigned char (*pTable);//“中转站”的二维指针
unsigned char R,L;//复制数据时用到的for循环变量
void main()
{
    TableSec=2; //选择第2个现有的二维数组
switch(TableSec)//根据选择变量来切换选择某个现有的二维数组
{
   case 1://选择第1个现有二维数组
      pTable=table_1;//二维指针pTable在这里关联指定的数组,起到中转站的作用。
      break;
   case 2:   //选择第2个现有二维数组
      pTable=table_2;//二维指针pTable在这里关联指定的数组,起到中转站的作用。
      break;
   case 3:    //选择第3个现有二维数组
      pTable=table_2;//二维指针pTable在这里关联指定的数组,起到中转站的作用。
      break;
}

//通过二维指针pTable来复制数据到指定的缓存数组SaveBuffer
for(R=0;R<3;R++)//行循环
{
for(L=0;L<3;L++)//列循环
    {
      SaveBuffer=pTable; //这里能看到,二维指针维护了二维数组的队形感
}
}

    while(1)
{

}
}


【76.3   二维指针在“函数接口”中的应用。】

       把上述例子“复制过程”的代码封装成一个函数,实现的功能还是一样,有3个现有的二维数组,通过某个变量来选择切换,把某个二维数组的数据复制到指定的一个缓存数组中。

//函数声明
void CopyBuffer(const unsigned char (*pTable),unsigned char (*pSaveBuffer));

code unsigned char table_1=//第1个现有的二维数组
{
{0x00,0x01,0x02},
{0x10,0x11,0x12},
{0x20,0x21,0x22},
};

code unsigned char table_2=//第2个现有的二维数组
{
{0xA0,0xA1,0xA2},
{0xB0,0xB1,0xB2},
{0xC0,0xC1,0xC2},
};

code unsigned char table_3=//第3个现有的二维数组
{
{0xD0,0xD1,0xD2},
{0xE0,0xE1,0xE2},
{0xF0,0xF1,0xF2},
};

unsigned char SaveBuffer;//指定的一个缓存数组

unsigned char TableSec;//选择变量

//*pTable是输入接口带const修饰,*pSaveBuffer是输出结果的接口无const。
void CopyBuffer(const unsigned char (*pTable),unsigned char (*pSaveBuffer))
{
    unsigned char R,L;//复制数据时用到的for循环变量

for(R=0;R<3;R++)//行循环
{
for(L=0;L<3;L++)//列循环
    {
      pSaveBuffer=pTable; //这里能看到,二维指针维护了二维数组的队形感
}
}

}
void main()
{
    TableSec=2; //选择第2个现有的二维数组
switch(TableSec)//根据选择变量来切换选择某个现有的二维数组
{
   case 1://选择第1个现有二维数组
      CopyBuffer(table_1,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
      break;
   case 2:   //选择第2个现有二维数组
      CopyBuffer(table_2,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
      break;
   case 3:    //选择第3个现有二维数组
      CopyBuffer(table_3,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
      break;
}
    while(1)
{

}
}

【76.4   二维指针“类型强制转换”的书写格式。】

      unsigned char *pu8,unsigned int *pu16,unsigned int *pu32这些指针的书写定义都是很有规则感的,相比之下,二维指针的定义显得缺乏规则感,比如定义的二维指针变量unsigned char (*pTable),不规则在哪?就在于二维指针的变量pTable嵌入到了括号中去,跟符号“*”捆绑在一起,这时就会冒出一个问题,如果我要强制某个指针变量为二维指针怎么办?下面的例子已经给出了答案。
unsigned char table=//二维数组
{
{0xD0,0xD1,0xD2},
{0xE0,0xE1,0xE2},
{0xF0,0xF1,0xF2},
};

unsigned char (*pTable);

void main()
{
    pTable=(unsigned char (*))table;//这里,强制类型转换用unsigned char (*)
}

      总结:二维数组的强制类型转换用这种书写格式(unsigned char (*)),这里的N是代表实际项目中某数组的“列”数。

【76.5   例程练习和分析。】

      现在编写一个练习程序。

/*---C语言学习区域的开始。-----------------------------------------------*/
void CopyBuffer(const unsigned char (*pTable),unsigned char (*pSaveBuffer));

code unsigned char table_1=//第1个现有的二维数组
{
{0x00,0x01,0x02},
{0x10,0x11,0x12},
{0x20,0x21,0x22},
};

code unsigned char table_2=//第2个现有的二维数组
{
{0xA0,0xA1,0xA2},
{0xB0,0xB1,0xB2},
{0xC0,0xC1,0xC2},
};

code unsigned char table_3=//第3个现有的二维数组
{
{0xD0,0xD1,0xD2},
{0xE0,0xE1,0xE2},
{0xF0,0xF1,0xF2},
};

unsigned char SaveBuffer;//指定的一个缓存数组

unsigned char TableSec;//选择变量

//*pTable是输入接口带const修饰,*pSaveBuffer是输出结果的接口无const。
void CopyBuffer(const unsigned char (*pTable),unsigned char (*pSaveBuffer))
{
    unsigned char R,L;//复制数据时用到的for循环变量

for(R=0;R<3;R++)//行循环
{
for(L=0;L<3;L++)//列循环
    {
      pSaveBuffer=pTable; //这里能看到,二维指针维护了二维数组的队形感
}
}

}

void main() //主函数
{
   TableSec=2; //选择第2个现有的二维数组
switch(TableSec)//根据选择变量来切换选择某个现有的二维数组
{
   case 1://选择第1个现有二维数组
      CopyBuffer(table_1,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
      break;
   case 2:   //选择第2个现有二维数组
      CopyBuffer(table_2,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
      break;
   case 3:    //选择第3个现有二维数组
      CopyBuffer(table_3,SaveBuffer); //二维指针在这里分别体现了输入和输出接口作用
      break;
}

View(SaveBuffer);//在电脑端观察某个二维数组第0行数据第0个元素的内容
    View(SaveBuffer);//在电脑端观察某个二维数组第0行数据第1个元素的内容
View(SaveBuffer);//在电脑端观察某个二维数组第0行数据第2个元素的内容
      while(1)
      {
      }
}
/*---C语言学习区域的结束。-----------------------------------------------*/

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

开始...

第1个数
十进制:160
十六进制:A0
二进制:10100000

第2个数
十进制:161
十六进制:A1
二进制:10100001

第3个数
十进制:162
十六进制:A2
二进制:10100010

分析:
      SaveBuffer是十六进制的0xA0,提取了第2个二维数组的第0行第0个数据。
      SaveBuffer是十六进制的0xA1,提取了第2个二维数组的第0行第1个数据。
      SaveBuffer是十六进制的0xA2,提取了第2个二维数组的第0行第2个数据。

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

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


页: 1 2 3 [4] 5 6 7
查看完整版本: 从单片机基础到程序框架(连载)