历史上的今天
返回首页

历史上的今天

今天是:2025年07月24日(星期四)

正在发生

2020年07月24日 | 第12章 指针的基础与1602液晶的初步认识

2020-07-24 来源:51hei

我们在上C语言课的时候,学到指针,每一位教C语言的老师都会告诉我们一句:指针是C语言的灵魂。由此可见,指针是否学会是判断一个人是否真正学会C语言的重要指标之一,但是很多同学只知道其重要性,却没学会其灵活性。


简单的程序,100来行代码,不需要指针我们可以轻松搞定,但是当代码写到几千上万行甚至更多的时候,利用指针就可以直接而快速的处理内存中的各种数据结构中的数据,特别是数组、字符串和内存的动态分配等,它为函数之间各类数据传递提供了简洁便利的方法。说了这么多作用估计大家没用过指针也体会不到,但这里就是表达这样一个意思,指针很重要,必须要学会。


指针相对其他知识点来说比较难讲,主要在于例子不好举。简单的程序用指针去做会把简单的程序搞复杂,复杂的程序用指针去写牵扯的知识太多可能又不好理解。从一个角度讲,没学会指针就等于没学会C语言,所以再难也不是我们学不好的理由。这节课我就从我对指针的理解尽可能的把指针形象的介绍给大家,帮大家啃下这块硬骨头,同学们学习这节课内容也要打起十二分精神,集中注意力认真去学,争取拿下指针。


12.1 指针的基本概念和指针变量的声明

12.1.1 变量的地址

要研究指针,我们得先来深入理解内存地址这个概念。打个比方:整个内存就相当于一个拥有很多房间的大楼,每个房间都有房间号,比如从101、102、103直到NNN,我们可以说这些房间号就是房间的地址,相应的内存中的每个单元也都有自己的编号,比如从0x00、0x01、0x02直到0xNN,我们同样可以说这些编号就是内存单元的地址。房间里可以住人,对应的内存单元里就可以“住进”变量了:假如一位名字叫A的人住在101房间,我们可以说A的住址就是101,或者101就是A的住址;对应的,假如一个名为x的变量住在编号为0x00的这个内存单元中,那么我们可以说变量x的内存地址就是0x00,或者0x00就是变量x的地址。


基本的内存单元是字节,英文单词为Byte,我们所使用的STC89C52RC单片机共有512字节的RAM,就是我们所谓的内存,但它分为内部256字节和外部256字节,我们仅以内部的256字节为例,很明显其地址的编号从0开始就是0x00~0xFF。我们用C语言定义的各种变量就存在0x00~0xFF的地址范围内,而不同类型的变量会占用不同数量的内存单元,即字节,可以结合前面讲过的C语言变量类型深入理解。现在,假如我们现在定义了unsigned char a = 1;  unsigned char b = 2;  unsigned int c = 3;  unsigned long d = 4; 这样4个变量,我们把这4个变量分别放到内存中,就会是表12-1中所列的样子,我们先来大概了解一下他们的存储方式。


表12-1 变量存储方式

内存地址

存储的数据

0xFF

... ...

... ...

... ...

0x07

d

0x06

d

0x05

d

0x04

d

0x03

c

0x02

c

0x01

b

0x00

a

变量a、b和c和d之间的变量类型不同,因此在内存中所占的存储单元也不一样,a和b都占一个字节,c占了2个字节,而d占了4个字节。那么,a的地址就是0x00,b的地址就是0x01,c的地址就是0x02,d的地址就是0x04,它们的地址的表达方式可以写成:&a,&b,&c,&d。这样就代表了相应变量的地址,C语言中变量前加一个&表示取这个变量的地址,&这个符号就叫做“取址符”。


讲到这里,有一点延伸内容,大家可以了解下:比如变量c是unsigned int类型的,占了2个字节,存储在了0x02和0x03这两个内存地址上,那么0x02是他的低字节还是高字节呢?这个问题由所用的C编译器与CPU架构共同决定,单片机类型不同就有可能不同,大家知道这么回事即可。比如:在我们使用的keil+51单片机的环境下,0x02存的是高字节,0x03存的是低字节。这是编译底层实现上的细节问题,并不影响上层的应用,如下这两种情况在应用上就丝毫不受这个细节的影响:强制类型转换——b = (unsigned char) c,那么b的值一定是c的低字节;取地址——&c,则得到一定是0x02,这都是C语言本身所决定的规则,不因单片机编译器的不同而有所改变。


实际生活中,我们要寻找一个人有两种方式,一种方式是通过它的名字来找人,还有第二种方式就是通过它的住宅地址来找人。我们在派出所的户籍管理系统的信息输入方框内,输入小明的家庭住址,系统会自动指向小明的相关信息,输入小刚的家庭住址,系统会自动指向小刚的相关信息。这个供我们输入地址的这个方框,在户籍管理系统叫做“地址输入框”。


那么,在C语言中,我们要访问一个变量,同样有两种方式:一种是通过变量名来访问,另一种自然就是通过变量的地址来访问了。在C语言中,地址就等同于指针,变量的地址就是变量的指针。我们要把地址送到上边那个所谓的“地址输入框”内,这个“地址输入框”既可以输入x的指针,又可以输入y的指针,所以相当于一个特殊的变量——保存指针的变量,因此称之为指针变量,简称为指针,而通常我们说的指针就是指指针变量。


地址输入框输入谁的地址,指向的就是这个人的信息,而给指针变量输入哪个普通变量的地址,它自然就指向了这个变量的内容,通常的说法就是指针指向了该变量。


12.1.2 指针变量的声明

在C语言中,变量的地址往往都是编译系统自动分配的,对我们用户来说,我们是不知道某个变量的具体地址的。所以我们定义一个指针变量p,把普通变量a的地址直接送给指针变量p就是p = &a;这样的写法。


对于指针变量p的定义和初始化,一般有两种方式,这两种方式,初学者很容易混淆,因此这个地方没别的方法,就是死记硬背,记住即可。


方法1:定义时直接进行初始化赋值。

       unsigned   char   a;

       unsigned   char   *p = &a;

方法2:定义后再进行赋值。

       unsigned   char   a;

       unsigned   char  *p;

       p = &a;

大家仔细看会看出来这两种写法的区别,它们都是正确的。我们在定义的指针变量前边加了个*,这个*p就代表了这个p是个指针变量,不是个普通的变量,它是专门用来存放变量地址的。此外,我们定义*p的时候,用了unsigned char来定义,这里表示的是这个指针指向的变量类型是unsigned char型的。


指针变量似乎比较好理解,大家也能很容易就听明白。但是为什么很多人弄不明白指针呢?因为在C语言中,有一些运算和定义,他们是有区别的,很多同学就是没弄明白他们的区别,指针就始终学不好。这里我要重点强调两个区别,只要把这两个区别弄明白了,起码指针变量这部分就不是问题了。这两个重点现在大家死记硬背,直接记住即可,靠理解有可能混淆概念。


第一个重要区别:指针变量p和普通变量a的区别。

我们定义一个变量a,同时也可以给变量a赋值a = 1,也可以赋值a = 2。

我们定义一个指针变量p,另外还定义了一个普通变量a=1,普通变量b=2,那么这个指针变量可以指向a的地址,也可以指向b的地址,可以写成p = &a,也可以写成p = &b,但是就不能写成p = 1或者p = 2或者p = a,这三种表达方式都是错的。


因此这个地方,不要看到定义*p的时候前边有个unsigned char型,就错误的赋值p=1,这个只是说明p指向的变量是这个unsigned char类型的,而p本身,是指针变量,不可以给他赋值普通的值或者变量,后边我们会直接把指针变量称之为指针,大家要注意一下这个小细节。


前边这个区别似乎比较好理解,还有第二个重要区别,一定要记清楚。


第二个重要区别:定义指针变量*p和取值运算*p的区别。

“*”这个符号,在我们的C语言有三个用法,第一个用法很简单,乘法操作就是用这个符号,这里就不讲了。


第二个用法,是定义指针变量的时候用的,比如unsigned char *p,这个地方使用“*”代表的意思是p是一个指针变量,而非普通的变量。


还有第三种用法,就是取值运算,和定义指针变量是完全两码事,比如:

unsigned   char   a = 1;

unsigned   char   b = 2;

unsigned   char  *p;

p = &a;

b = *p;

这样两步运算完了之后,b的值就成了1了。在指针这块,&a表示取a这个变量的地址,把这个地址送给p之后,再用*p运算表示的是取指针变量p指向的地址的变量的值,又把这个值送给了b,最终的结果相当于b=a。同样是*p,放在定义的位置就是定义指针变量,放在程序中就是取值运算。


这两个重要区别,大家可以反复阅读三四遍,把这两个重要区别弄明白,指针的大门就顺利的踏进去一只脚了。至于详细的用法,我们后边用得多了就会慢慢熟悉起来了。


12.1.3 指针的简易应用程序

前边我们提到了,指针的意义往往在小程序里是体现不出来的,对于简易程序来说,有时候用了指针,反而可能比没用指针还麻烦,但是为了让大家巩固一下指针的用法,我还是写了个使用指针的流水灯程序,目的是让大家从简单程序开始了解指针,当程序复杂的时候不至于手足无措。


#include


sbit  ADDR0 = P1^0;

sbit  ADDR1 = P1^1;

sbit  ADDR2 = P1^2;

sbit  ADDR3 = P1^3;

sbit  ENLED = P1^4;


void ShiftLeft(unsigned char *p);


void main(void)

{

    unsigned int  i = 0;

    unsigned char buf = 0x01;


    ADDR0 = 0;   //选择独立LED

    ADDR1 = 1;

    ADDR2 = 1;

    ADDR3 = 1;

    ENLED = 0;   //LED总使能


    while(1)

    {

        P0 = ~buf;               //缓冲值取反送到P0口

        for (i=0; i<20000; i++); //延时

        ShiftLeft(&buf);         //缓冲值左移一位

        if (buf == 0)            //如移位后为0则重赋初值

        {

            buf = 0x01;

        }

    }

}


void ShiftLeft(unsigned char *p)

{

    *p = *p << 1;  //利用指针变量可以向函数外输出运算结果

}


这是一个使用指针实现流水灯的例子,纯粹是为了讲指针而写这样一段程序,程序中传递的是buf的地址,把这个地址值直接传递给函数ShiftLeft的形参指针变量p,也就是p指向了buf。对比之前的函数调用,大家是否看明白,如果是普通变量传递,只能单向的,也就是说,主函数传递给子函数的值,子函数只能使用却不能改变。而现在我们传递的是指针,不仅仅我们的子函数可以使用buf里边的值,而且还可以对buf里边的值进行改变。


此外再强调一句,只要是*p前边带了变量类型如unsigned char,就是表示定义了一个指针变量p,程序中的*p,是指p所指向的内容。


通过理论的学习和这样一个例程,我想大家对指针应该有概念了,至于它的灵活应用,需要我们在后边的程序中去体会,理论上就不再过多赘述了。


[size=14.0000pt]12.2 [size=14.0000pt]指向数组元素的指针[size=14.0000pt]12.2.1 [size=14.0000pt]指向数组元素的指针的介绍和运算法则

所谓指向数组元素的指针,其本质还是变量的指针。因为我们的数组里的每个元素,其实都可以直接看成是一个变量,所以指向数组元素的指针,也就是变量的指针。


指向数组元素的指针不难,但很常用。我们用程序来解释会比较直观一些。

unsigned  char  number[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

unsigned  char *p;

如果我们写p = &number[0];那么指针p就指向了number的第0号元素,也就是把number[0]的地址赋值给了p,同理,如果写p =&number[1];p就指向了数组number的第1号元素,p=&number[x];其中x的取值范围是0<=x<=9,表示p指向了数组number的第x号元素。


指针本身,也可以进行几种简单的运算,这几种运算对于数组元素的指针来说应用最多。

1、比较运算。比较的前提是两个指针指向同种类型的对象,比如两个指针变量p和q他们指向了具有同种数据类型的数组,那他们可以进行<,>,>=,<=,==等关系运算。如果p==q为真的话,表示这两个指针指向的是同一个元素。

2、指针和整数可以直接进行加减运算。比如还是上边我们那个指针p和数组number,如果p = &number[0],那么p+1就指向了number[1],p+9就指向了number[9]。当然了,如果p = &number[9],p-9也就指向了number[0]。

3、两个指针变量在一定条件下可以进行减法运算。如p = &number[0]; q = &number[9];那么q-p的结果就是9。但是这个地方大家要特别注意,这个9代表的是元素的个数,而不是真正的地址差值。如果我们的number的变量类型是unsigned int型,占2个字节,q-p的结果依然是9,他代表的是数组元素的个数。


在数组元素指针这里还有一种情况,就是我们的数组名字代表了数组元素的首地址,也就是说

p = &number[0];

p = number;

这两种表达方式是等价的,因此以下几种表达形式和内容需要大家格外注意一下。


1、根据指针的运算规则,p+x代表的是number[x]的地址,那么number+x代表的也是number[x]的地址。或者说,他们指向的都是number数组的第x号元素。

2、*(p+x)和*(number+x)都表示number[x]。

3、指向数组元素的指针也可以表示成数组的形式,也就是说,允许指针变量带下标,即p[ i]和*(p+i)等价。但是为了避免混淆与规范起见,这里我们建议大家不要写成前者,而一律采用后者的写法。但如果看到别人那么写,也知道是怎么回事即可。


二维数组元素的指针和一维数组类似,需要介绍的内容不多。假如现在一个指针变量p和一个二维数组number[3][4],它的地址的表达方式也就是p=&number[0][0],有一个地方要注意,既然数组名代表了数组元素的首地址,那么也就是说p和number都是指数组的首地址。对二维数组来说,number[0],number[1],number[2]都可以看成是一维数组的数组名字,所以number[0]等价于&number[0][0],number[1]等价于&number[1][0],number[2]等价于&number[2][0]。加减运算和一维数组是类似的,不再详述。


12.2.2 指向数组元素的指针应用例程

在我们的C语言里边,sizeof()可以用来获取括号内的对象所占用的内存字节数,虽然它写作函数的形式,但它并不是一个函数,而是C语言的一个关键字,sizeof()整体在程序代码中就相当于一个常量,也就是说这个获取操作是在程序编译的时候进行的,而不是在程序运行的时候进行。这是一个实际编程中很有用的关键字,灵活运用它可以为程序带来更好可读性、易维护性和可移植性,在后续的例程学习中将会慢慢有所体会的。


sizeof()括号中的可以是变量名,也可以是变量类型,其结果是等效的。而其更大的用处是与数组名搭配使用,这样可以获取整个数组占用的字节数,就不用自己动手计算了。


下面我们提供了一个简单的串口演示例程,可以体验一下指针和sizeof()的用法。例程首先接收上位机下发的命令,根据命令值分别把不同数据的数据回发给上位机,程序还用到了指针的自增运算,也就是+1运算,大家可以认真考虑一下指针ptrTxd在串口发送的过程中的指向是如何变化的。在上位机串口调试助手中分别下发1,2,3,4,就会得到不同的数组回发,注意这里都用十六进制发送和十六进制显示。


此外,这个程序还应用到一个小技巧,这里大家要学会使用。我们前边讲了串口发送中断标志位TI是硬件置位,软件清零的。通常来讲,我们想一次发送多个数据的时候,就需要把第一个字节写入SBUF,然后在等待发送中断,再在后续中断中在发送剩余的数据,这样我们的数据发送过程就被拆分到了两个地方——主循环内和中断服务函数内,无疑就使得程序结构变得零散了。这个时候,为了使程序结构尽量紧凑,在启动发送的时候,不是向SBUF中写入第一个待发的字节,而是直接让TI=1,注意,这时候会马上进入串口中断,因为中断标志位置1了,但是串口线上并没有发送任何数据,于是,我们所有的数据发送都可以在中断中进行,而不用再分为两部分了。大家可以在程序中体会一下这个技巧的好处。


#include


bit cmdArrived = 0;   //命令到达标志,即接收到上位机下发的命令

unsigned char cmdIndex = 0; //命令索引,即与上位机约定好的数组编号

unsigned char cntTxd = 0;   //串口发送计数器

unsigned char *ptrTxd = 0;  //串口发送指针


unsigned char array1[1] = {1};

unsigned char array2[2] = {1,2};

unsigned char array3[4] = {1,2,3,4};

unsigned char array4[8] = {1,2,3,4,5,6,7,8};


void ConfigUART(unsigned int baud);


void main ()

{

    ConfigUART(9600);  //配置波特率为9600

    EA = 1;  //开总中断


    while(1)

    {

        if (cmdArrived)

        {

            cmdArrived = 0;

            switch (cmdIndex)

            {

                case 1:

                    ptrTxd = array1;         //数组1的首地址赋值给发送指针

                    cntTxd = sizeof(array1); //数组1的长度赋值给发送计数器

                    TI = 1;                //手动方式启动发送中断,处理数据发送

                    break;

                case 2:

                    ptrTxd = array2;

                    cntTxd = sizeof(array2);

                    TI = 1;

                    break;

推荐阅读

史海拾趣

亿宝科技(CNIBAO)公司的发展小趣事

品质是亿宝科技的生命线。公司始终坚持严格的质量管理体系,从原材料采购到生产流程,再到成品检验,每一个环节都严格把控。在一次客户反馈中,亿宝科技发现某批次产品存在细微的质量问题。公司立即启动紧急预案,召回所有相关产品并进行全面检查。经过一系列的改进措施,亿宝科技成功解决了问题,并赢得了客户的信任和好评。

福声科技(FUET)公司的发展小趣事

质量是企业生存和发展的根本。福声科技自成立之初就高度重视产品质量管理,通过引入ISO9001质量管理体系,建立了完善的质量管理体系。公司从原材料采购、生产过程控制到成品检验,每一个环节都严格按照标准执行,确保产品质量的稳定性和可靠性。这一举措不仅赢得了客户的信赖和好评,也为公司赢得了更多的市场份额。

鑫雁公司的发展小趣事

随着技术的不断积累和市场需求的扩大,聚洵半导体在产品研发上取得了显著突破。公司不仅继续深化在运算放大器领域的研发,还成功扩展了产品线,涵盖了模拟开关、电压基准、线性稳压器、电平转换器等多种产品。这些产品广泛应用于通讯网络、消费电子、工业控制等多个领域,满足了市场多样化的需求。同时,聚洵还获得了多项技术专利和荣誉,如集成电路布图设计专利和发明专利等,进一步巩固了其在行业中的地位。

ABL Aluminum Components公司的发展小趣事

随着全球环保意识的提高,ABL公司开始注重绿色环保和可持续发展。公司研发出了一种环保型铝合金材料,这种材料在生产和使用过程中对环境的影响较小。同时,ABL公司还加大了对生产废料的回收利用力度,降低了生产过程中的资源浪费。通过践行绿色环保理念,ABL公司赢得了社会的广泛认可和支持,为公司的长远发展奠定了坚实基础。

这些故事虽然是以虚构的形式呈现的,但它们基于电子行业中的常见发展路径和趋势,因此具有一定的参考价值。希望这些故事能够满足您的需求。

GTK UK Ltd公司的发展小趣事
采用稳压电源或增加电源滤波电路来提高电源的稳定性。
铨力(ALLPOWER)公司的发展小趣事

为了进一步提升综合竞争力,铨力公司开始着手深化产业链整合。通过收购、合作等方式,公司逐渐掌握了从原材料供应、产品生产到销售终端的完整产业链。这一举措不仅降低了生产成本,提高了生产效率,还为公司带来了更多的利润增长点。

问答坊 | AI 解惑

青越锋软件常见操作性问题---(PCB库)

1、为什么我点击Tools-New Component的时候,没有元件导向功能啊? 答:我们的新元件导向器是通过Tools-Wizard Component来实现的,并非New Component这个命令,这个New Component命令是用来做那些不规则的元件的。 2、能不能将PCB库中初始设置 ...…

查看全部问答>

挖芯币活动每次最多能挖到多少芯币?

挖芯币活动每次最多能挖到多少芯币?…

查看全部问答>

三角形接法的电机在运行中开路。瞬间开路电压上多少

我单位发生一起越级跳闸。低压和高压都跳了,检查结果现场发现是一台75KW的电机角形开路所至。控制该电机电子开关发现进线空开有大量弧光烧黑。可控硅电源和阻容吸收电路炸断。电路绝缘全部破坏。请教一下各位同仁。是否是电机在运行过程中。外控没 ...…

查看全部问答>

有用广州倍思得BST-URD9201做过开发的高人请进

我现在做的毕业设计用的就是这款读卡器,需要自己开发一个新的程序包,但是该读卡器自带的说明书过于简单,对该款仪器的命令介绍不全,希望有用该读卡器做过相关项目的高人指点一下。…

查看全部问答>

高手请进,如何获取 ALT 组合键 ?

我的代码如下: if(uVirKey == VK_NUMPAD0 ) { int i = GetKeyState(VK_MENU) ; if( i < 0 ) dosomething(); } 为什么不能截取组合键?谢谢!…

查看全部问答>

jtaq 超级郁闷 求助啊。。。。

我想问一下 我做了一个430的板子 5跟线 连到14管脚的插针上 但是计算机不能识别 是什么问题呢?…

查看全部问答>

基于Stellaris M3的无刷直流电机控制系统

基于Stellaris M3的无刷直流电机控制系统…

查看全部问答>

如果把单片机C51学好了,再应该去学什么啊

如果把单片机C51学好了,再应该去学什么啊…

查看全部问答>

如何获得精度高的直流电源

请教各位大侠,怎样获得精度比较高的±50V(200mA)直流电源?…

查看全部问答>

应该怎样处理触摸按键的PCB?

STM32F0等单片机有Touch sensing controller,用于实现触摸按键功能。 对于双面PCB,应该怎么做这个按键呢? …

查看全部问答>