打印
[51单片机]

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

[复制链接]
楼主: jianhong_wu
手机看帖
扫描二维码
随时随地手机跟帖
261
为LZ点赞。

使用特权

评论回复
262
yddjz| | 2017-5-3 00:02 | 只看该作者

使用特权

评论回复
263
jianhong_wu|  楼主 | 2017-5-7 10:39 | 只看该作者
第六十八节: 为函数接口指针“定向”的const关键词。
第六十八节_pdf文件.pdf (70.79 KB)
【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[4]={2,6,8,4};
unsigned long Gu32PinJunZhi;  

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

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

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


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

开始...

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


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

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

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

使用特权

评论回复
264
348565004| | 2017-5-8 01:34 | 只看该作者
谢谢分享!

使用特权

评论回复
265
jianhong_wu|  楼主 | 2017-5-14 10:30 | 只看该作者
第六十九节: 宏函数sizeof()。
第六十九节_pdf文件.pdf (74.22 KB)
【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];    //常量。占用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[2]={1,2}; //占用2个字节
unsigned char b[3]={3,4,5}; //占用3个字节
unsigned char c[4]={6,7,8,9}; //占用4个字节
unsigned char  HeBing[9];//合并a,b,c在一起的数组。这里的9是人工计算a,b,c容量累加所得。
unsigned char i; //循环变量i

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

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

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


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

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

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

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

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

【69.3   例程练习和分析。】

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

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

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

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

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

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

for(i=0;i<sizeof(HeBing);i++)  //利用宏sizeof计算出HeBing数组所占用的字节数
    {
    View(HeBing[i]);   //把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[0]为1。
        HeBing[1]为2。
        HeBing[2]为3。
        HeBing[3]为4。
        HeBing[4]为5。
        HeBing[5]为6。
        HeBing[6]为7。
        HeBing[7]为8。
        HeBing[8]为9。

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

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

使用特权

评论回复
266
jianhong_wu|  楼主 | 2017-5-21 10:27 | 只看该作者
第七十节: “万能数组”的结构体。
第七十节_pdf文件.pdf (76.58 KB)
【70.1   结构体与数组。】

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

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

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

struct StructMould     //“造模”
{
   unsigned char  u8Data_A;   
   unsigned int   u16Data_B;  
       unsigned long  u32Data_C;  
    };

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

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

    while(1)
    {

    }
}


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


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

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

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

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

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

    while(1)
    {

    }
}

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

【70.3   例程练习和分析。】

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

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

struct StructMould     //“造模”
{
   unsigned char  u8Data_A;   
   unsigned int   u16Data_B;  
       unsigned long  u32Data_C;  
};

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

使用特权

评论回复
267
hunancjz| | 2017-5-21 11:34 | 只看该作者
向楼主学习,我也是06年毕业,现在还在一家小公司做基础的开发,差距啊

使用特权

评论回复
268
dcxq13| | 2017-5-23 08:46 | 只看该作者
楼主写的非常详细,可以说是我迄今看过讲的最基础,最实在的单片机入门教程,期待楼主继续更新

使用特权

评论回复
269
528618581| | 2017-5-23 09:12 | 只看该作者
谢谢坚鸿大神,无私奉献

使用特权

评论回复
270
jianhong_wu|  楼主 | 2017-5-29 11:35 | 只看该作者
第七十一节: 结构体的内存和赋值。
第七十一节_pdf文件.pdf (102.98 KB)
【71.1   结构体的内存生效。】

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

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

struct StructMould     //“造模”
{
   unsigned char  u8Data_A;   
   unsigned char  u8Data_B;   
    };


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

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

struct StructMould     //“造模”
{
   unsigned char  u8Data_A;   
   unsigned char  u8Data_B;   
};

struct StructMould  GtMould_1;  //“生成”一个变量GtMould_1。占用2个字节内存
struct StructMould  GtMould_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 char  u8Data;     //一个unsigned char占用1个字节。
   unsigned long  u32Data;    //一个unsigned long占用4个字节。
};

struct StructMould_1  GtMould_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 char  u8Data;     //一个unsigned char占用1个字节。
   unsigned long  u32Data;    //一个unsigned long占用4个字节。
};

struct StructMould_1  GtMould_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 long  u32Data;    //一个unsigned long占用4个字节。
   unsigned char  u8Data;     //一个unsigned char占用1个字节。
};

struct StructMould_1  GtMould_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 long  u32Data;  
   unsigned char  u8Data;  
    };

struct StructMould_1  GtMould_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 long  u32Data;   
   unsigned char  u8Data;  
};

struct StructMould_2  GtMould_2_A;  //生成第1个结构体变量
struct StructMould_2  GtMould_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 long  u32Data;   
   unsigned char  u8Data;  
};

struct StructMould_2  GtMould_2_A;  //生成第1个结构体变量
struct StructMould_2  GtMould_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 long  u32Data;    //一个unsigned long占用4个字节。
   unsigned char  u8Data;     //一个unsigned char占用1个字节。
};

struct StructMould_2     //“造模”
{
   unsigned char  u8Data;     
   unsigned long  u32Data;   
};

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

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

使用特权

评论回复
271
小河有点冏| | 2017-5-31 18:08 | 只看该作者
顶顶顶,写的真好

使用特权

评论回复
272
jianhong_wu|  楼主 | 2017-6-11 09:39 | 只看该作者
第七十二节: 结构体的指针。
第七十二节_pdf文件.pdf (79.71 KB)
【72.1   结构体指针的重要用途。】

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

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

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

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


使用特权

评论回复
273
jianhong_wu|  楼主 | 2017-6-11 09:42 | 只看该作者
第七十三节: 结构体数据的传输存储和还原。
第七十三节_pdf文件.pdf (68.5 KB)
【73.1   结构体数据的传输存储和还原。】

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

【73.2   例程练习和分析。】

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

struct StructMould_1   
{
   unsigned char  u8Data_A;     
   unsigned long  u32Data_B;   
   unsigned int   u16Data_C;   
};

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

unsigned char Gu8Buffer[sizeof(GtMould_1)]; //定义一个内存跟结构体变量大小一样的普通数组
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[i]=pu8[i];   //把结构体变量以字节的方式搬运并且存储到普通数组里。
}

ptStruct=(struct StructMould_1  *)&Gu8Buffer[0];  //再把普通数组强制“还原”成结构体指针
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学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
274
arima| | 2017-6-11 20:28 | 只看该作者
**就是胜利,一直在关注。。。。

使用特权

评论回复
275
82493210| | 2017-6-16 11:15 | 只看该作者
好东西呀,对基础不好的人真的是有意义,

使用特权

评论回复
276
jianhong_wu|  楼主 | 2017-6-17 18:07 | 只看该作者
第七十四节: 结构体指针在函数接口处的频繁应用。
第七十四节_pdf文件.pdf (88.09 KB)
【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学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。


使用特权

评论回复
277
吴新明| | 2017-6-19 05:30 | 只看该作者
666

使用特权

评论回复
278
YLSJWGX| | 2017-6-19 19:31 | 只看该作者
坚鸿晚上好!请问你分享的51单片机技术贴配套的开发板用哪款?(朱兆祺51单片机开发板在淘宝上目前找不到),我是门外汉,谢谢你的爱心回复!

使用特权

评论回复
279
jianhong_wu|  楼主 | 2017-6-25 10:33 | 只看该作者
YLSJWGX 发表于 2017-6-19 19:31
坚鸿晚上好!请问你分享的51单片机技术贴配套的开发板用哪款?(朱兆祺51单片机开发板在淘宝上目前找不到) ...

对不起。我现在不卖学习板了。这个学习板目前是不存在了。除非以后有其他感兴趣的网友自发生产销售。

使用特权

评论回复
280
jianhong_wu|  楼主 | 2017-6-25 11:12 | 只看该作者
第七十五节: 指针的名义(例:一维指针操作二维数组)。
第七十五节_pdf文件.pdf (86.13 KB)
【75.1   指针的名义。】

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

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

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

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

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

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

    while(1)
{

}
}



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

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

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

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

unsigned char  Gu8Buffer[3];    //一维数组,存放从二维数组里提取出来的某一行数据

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

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

    while(1)
{

}
}


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

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

【75.6   例程练习和分析。】

        现在编写一个练习程序。
/*---C语言学习区域的开始。-----------------------------------------------*/

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

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

unsigned char  Gu8Buffer[3];    //一维数组,存放从二维数组里提取出来的某一行数据

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

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

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



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

开始...

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

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

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


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

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

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

使用特权

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

本版积分规则