51单片机基础剖析(基于C语言)

来源:互联网 发布:淘宝外卖和饿了么合并 编辑:程序博客网 时间:2024/05/17 01:05

一、51单片机内存剖析

    在编写应用程序时,定义一个变量,一个数组,或是说一个固定表格,到底存储在什么地方;当定义变量大小超过MCU的内存范围时怎么办;如何控制变量定义不超过存储范围;

以及如何定义变量才能使得变量访问速度最快,写出的程序运行效率最高。以下将一一解答。

1.六类存储类型  code  data  idata  xdata   pdata  bdata

    code:程序存储器,也即只读存储器,用来保存常量或是程序,采用16位地址线编码,可以是在片内,或是片外,大小被限制在64KB。

    作用:定义常量,如八段数码表或是编程使用的常,在定义时加上code或明确指明定义的常量保存到code memory(只读。)比如:

    char  code  table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90};

    此关键字的使用方法等同于const。

    data:数据存储区,只能用于声明变量,不能用来声明函数,该区域位于片内,采用8位地址线编码,具有最快的存储速度,但是数量被限制在128byte或更少。

    使用方法:unsigned char data fast_variable=0;

    Idata:数据存储区,只能用于声明变量,不能用来声明函数。该区域位于片内,采用8位地址线编码,内存大小被限制在256byte或更少。该区域的低地址区与data区地址一致,高地址区域是52系列在51系列基础上扩展的并与特殊功能寄存器具有相同地址编码的区域。即:data memory是idata memory的一个子集。       

    xdata:只能用于声明变量,不能用来声明函数,该区域位于MCU外部,采用16位地址线进行编码,存储大小被限制在64KB以内。如:unsigned char xdata count=0;

    pdata:只能用于声明变量,不能用来声明函数,该区域位于MCU外部,采用8位地址线进行编码。存储大小限制在256byte,是xdata memory的低256byte。为其子集。如:unsigned char pdata count=0;

    bdata:只能用于声明变量,不能用来声明函数。该区域位于8051内部位数据地址。定义的量保存在内部位地址空间,可用位指令直接读写。使用方法:unsigned char bdata varab=0。

注:一般情况下,定义字符型变量时,在缺省unsigned的情况下,默认为无符号。但是本人在Keil uV3中遇到并非如此的案例。在缺省的情况下默认为有符号。要注意一下,或许不同的编译器规则不同。所以我们在写程序的时候,还是最好把unsigned signed加上。

2.函数的参数和局部变量的存储模式

    C51 编译器允许采用三种存储器模式:SMALL,COMPACT 和LARGE。一个函数的存储器模式确定了函数的参数的局部变量在内存中的地址空间。处于SMALL模式下的函数参数和局部变量位于8051单片机内部RAM中,处于COMPACT和LARGE模式下的函数参数和局部变量则使用单片机外部RAM。在定义一个函数时可以明确指定该函数的存储器模式。方法是在形参表列的后面加上一存储模式。

   示例如下:

        #pragma large   //此预编译必须放在所有头文前面

        int    func0(char  x,y) small;

        char  func1(int  x) large;

        int    func2(char x);

    注:上面例子在第一行用了一个预编译命令#pragma,它的意思是告诉c51编译器在对程序进行编译时,按该预编译命令后面给出的编译控制指令LARGE进行编译,即本例程序编译时的默认存储模式为LARGE。随后定义了三个函数,第一个定义为SMALL存储模式,第二个函数定义为LARGE第三个函数未指定,在用C51进行编译时,只有最后一个函数按LARGE存储器模式处理,其它则分别按它们各自指定的存储器模式处理。

本例说明,C51编译器允许采用所谓的存储器混合模式,即允许在一个程序中将一些函数使用一种存储模式,而其它一些则按另一种存储器模式,采用存储器混合模式编程,可以充分利用8051系列单片机中有限的存储器空间,同时还可以加快程序的执行速度。

3.绝对地址访问(头文件为:absacc.h(相当重要))

    #define CBYTE ((unsigned char volatile code  *) 0)

    #define DBYTE ((unsigned char volatile data   *) 0)

    #define PBYTE ((unsigned char volatile pdata  *) 0)

#define XBYTE ((unsigned char volatile xdata  *) 0)

功能:CBYTE寻址CODE区

          DBYTE寻址DATA区

          PBYTE寻址XDATA(低256)区

          XBYTE寻址XDATA区

   例:如下指令在对外部存储器区域访问地址0x1000

       xvar=XBYTE[0x1000];

       XBYTE[0x1000]=20;

 

    #define CWORD ((unsigned int volatile code  *) 0)

    #define DWORD ((unsigned int volatile data   *) 0)

    #define PWORD ((unsigned int volatile pdata  *) 0)

    #define XWORD ((unsigned int volatile xdata  *) 0)

    功能:与前面的一个宏相似,只是它们指定的数据类型为unsigned int。

通过灵活运用不同的数据类型,所有的8051地址空间都是可以进行访问。例如:

DWORD[0x0004]=0x12F8;// 即内部数据存储器中(0x08)=0x12; (0x09)=0xF8

    注:用以上八个函数,可以完成对单片机内部任意ROM和RAM进行访问,非常方便。还有一种方法,那就是用指钟,后面会对C51的指针有详细的介绍。

4.寄存器变量(register)

为了提高程序的执行效率,C语言允许将一些频率最高的那些变量,定义为能够直接使用硬件寄存器的所谓的寄存器变量。定义一个变量时,在变量类型名前冠以“register” 即将该变量定义成为了寄存器变量。寄存器变量可以认为是一自动变量的一种。有效作用范围也自动变量相同。由于计算机寄存器中寄存器是有限的。不能将所有变量都定义成为寄存器变量,通常在程序中定义寄存器变量时,只是给编译器一个建议,该变量是否真正成为寄存器变量,要由编译器根据实际情况来确定。另一方面,C51编译器能够识别程序中使用频率最高的变量,在可能的情况下,即使程序中并未将该变量定义为寄存器变量,编译器也会自动将其作为寄存器变量处理。被定义的变量是否真正能成为寄存器变量,最终是由编译器决定的。

5.内存访问的实现

    (1)指钟

    指钟本身是一个变量,其中存放的内容是变量的地址,也即特定的数据。8051的地址是16位的,所以指针变量本身占用两个存储单元。指针的说明与变量的说明类似,仅在指针名前加上“*”即可。

  如:   int   *int_point;     //声明一个整型指针

       char  *char_point;   //声明一个字符型指针

  利用指针可以间接存取变量。实现这一点要用到两个特殊运算符

          &  取变量地址

          *   取指针指向单元的数据

示例一:

       int   a=15,b;

       int  *int_point;     //定义一个指向整型变量的指针

       int_point=&a;       //int_point指向 a

       *int_point=5;        //给int_point指向的变量a 赋值5 等同于a=5;

示例二:

       char  i,table[6],*char_point;

       char_point=table;

       for(i=0;i<6;i++)

       {

           char_point=i;

           char_point++;

       }

    注:指针可以进行运算,它可以与整数进行加减运算(移动指针)。但要注意,移动指针后,其地址的增减量是随指针类型而异的,如,浮点指针进行自增后,其内部将在原有的基础上加4,而字符指针当进生自增的时候,其内容将加1。原因是浮点数,占4个内存单元,而字符占一个字节。

    宏晶科技最新一代STC12C5A360S2系列,每一个单片机出厂时都有全球唯一身份证号码(ID号),用户可以在单片机上电后读取内部RAM单元F1H~F7H的数值,来获取此单片机的唯一身份证号码。使用MOV  @Ri指令来读取。下面介绍C51获取方法:

        char  id[7]={0};

        char  i;

        char idata  *point;

        for(i=0;i<7;i++)

        {

              id[i]=*point;

              point++;

        }

(此处只是对指针做一个小的介绍,达到访问内部任何空间的方式,后述有对指针使用的详细介绍)

    (2)对SFR,RAM ,ROM的直接存取

    C51提供了一组可以直接对其操作的扩展函数

    若源程序中,用#include包含头文件,io51.h 后,就可以在扩展函数中使用特殊功能寄存器的地址名,以增强程序的可读性:

    注 此方法对SFR,RAM,ROM的直接存取不建议使用.因为,淡io51.h这个头文件在KEIL中无法打开,可用指针,或是采用absacc.h头文件,

    (3) PWM与PCA

    STC12系列有两路PWM/PCA

    PWM:(Pulse Width Modulation)脉宽调制,是一种使用程序来控制波形占空比,周期,相位波形的技术。

     PCA:(Programmable Counter Array)可编程计数阵列,它比通常的定时/计数器的定时能力强,需要CPU的干预少。其优势一是软件简单,二是精度大有提高。

 

*6.动态内存分配的实现

    在单片机的实际开发中,很多情况下我么需要开辟一块内存,但是具体开辟多大,也就是内存的字节数我们还无法确定,比如可能要等到上位机的指令发送下来才能确定,这个时候我们就得动态分配内存。注意,单片机内部存储资源是极其有限的,不允许开发人员开辟出一块很大的存储区来备用。在VC 6.0环境下很容易用malloc()来得到一块RAM,但是由于单片机内部没有操作系统(如何在51上跑uC/OS-II我以后会写出来),所以在51上实现动态内存分配就是个难点也是一个重点问题。下面给出代码,详细分析大家可以参考求是科技编的《8051系列单片机C程序设计完全手册》这本书。

#include <REG52.H>                                              

#include <stdlib.h>    //init_mempool()、malloc()、free()函数所在的头文件

…… 

void main (void)

{

       char *ptr1;

       init_mempool (0x1000,0x500); //内存池初始化,0x1000为起始地址,0x500为内存大小

       ptr1=malloc(30);  /*动态为指针变量分配长度为30字节的存储空间*/

    ……

       //此处为你的代码

    ……

       free(ptr1) ;  //注意,动态内存用完之后务必要释放,否则程序将会出错

    while (1);

}

 

二、变量类型及其作用域剖析

变量可分为   1.局部变量;2.全局变量(按变量的有效作用范围划分)

    1.局部变量

    是指函数内部(包括main函数)定义的变量,仅在定义它的那个函数范围内有效,不同函数可使用相同的局部变量名,函数的形式参数也属于局部变量,在一个函数的内部复合语句中也可以定义局部变量,该局部变量只在该复合语合中有效。

    2.全局变量

    是指函数外部定义的变量,以称外部变量。可为多个函数共同使用,其有效作用范围是从它定义开始到整个程序文件结束。如果全局变量,定义在一个程序文件的开始处,则在整个程序文件范围都可以使用它,如果一个全局变量不是在程序文件的开始处定义,但又希望在它定义之前的函数中引用该变量,这时应在引用该变量的函数中用关键字extern将其声明为“外部变量”。另个,如果在一个程序模块文件中引用另一个程序模块文件中定义的变量时,也必须用extern进行说明。

    外部变量的说明与外部变量的定义是不同的,外部变量定义只能有一次,定义的位置在所有函数之外,而同一个程序文件中(不是指模块文件)的外部变量声明可以有多次,声明的置在需要引用该变量的函数之内,外部变量的声明的作用只是声明该变量是一个已经在外部定义过了的变量而已。

    如在同一个程序文件中,全局变量与局部变量同名,则在局部变量的有效作用范围之内,全局变量不起作用,也就是说,局部变量的优先级比全局变量高。

    在编写C语言程序时,不是特别必要的地方一般不要使用全局变量,而应当尽可能的使用局部变量。因为局部变量只在使用它的时候,才为其分配内存单元,而全局变量在整个程序的执行过程中都要占用内存单元,且当全局变量使用过多时,会降低程序的可读性。

 

   变量的存储种类

   (1).自动变量(auto)

   定义变量时,在变量类型名前加上  “auto” ,自动变量是C语言中使用最为广泛的一类变量,在函数体内部或是复合语句内部定义的变量,如果省略了存储种类说明,则该变量默认为自动变量。

   例如:

         {               等价于      {

            char  x;                      auto  char x;

            int   y;                       auto  int   y;

               ……                         ……

         }                            }

    

    注:自动变量的作用范围在定义它的函数体或是复合语句内部,只有在定义它的函数内被调用,或是定义它的复合语句被执行时,编译器才会为其分配内存空间,开始其生存期。当函数调用结束返回,或复合语句执行结束,自动变量所占用的内存空间就被释放,变量的值当然也就不复存在,其生存期结束。当函数再次调用,或是复合语句被再次执行时,编译器又会为其内部的自动变量重新分配内存空间。但不会保留上一次运行的值。而必须被重新分配。因此自动变量始终是相对于函数或复合语句的局部变量。

    (2).外部变量(extern)

    用说明符“extern”定义的变量称为外部变量。按缺省规则,凡是在所有函数之前,在函数外部定义的变量都是外部变量,定义时可以不写extern说明符,但是一个函数体内说明一个已在该函数体外或别的程序模块文件中定义过的外部变量时,刚必须要使用extern说明符。外部变量定义后,它就被分配了固定的内存空间。外部变量的生存期为程序的整个执行时间。 外部变量的存储不会随函数或复合语句执行完毕而释放,因此外部变量属于全局变量。

    C语言允许将大型程序分解为若干个独立的程序模块文件,各个模块可分别进行编译,然后再将它们连接在一起,如果某个变量需要在所有程序模块文件中使用,只要在一个程序模块文件中将该变量定义成全局变量,而在其它程序模块文件中用extern声明该变量是已被定义过的外部变量就可以了。

    函数是可以相互调用的,定义函数时,如果冠以关键字extern 即将其明确定义为一个外部函数。例如  extern  int  func2(char a,b) 。如果在定义函数时省略关键字extern,则隐含为外部函数。如果在调用一个在本程序模块文件以外的其它模块文件所定义的函数,则必须要用关键字extern说明被调用的函数是一个外部函数。对于具有外部函数相互调用的多模块程序,可用C51编译器分别对各个模块文件进行编译,最后再用L51连接定位器将它们连接成为一个完整的程序。如下为一个多模块程序:

 

程序模块1,文件名为file1.c

#include<stdio.h>

int x=5;

void main()

 {

   extern void fun1(  );

   extern viod fun2(int  y);

   fun1( );

   fun1( );

   fun1( );

   printf( “\n%d  %d\n”,x,fun2(x));

}

 

程序模块2,文件名为file2.c

#include<stdio.h>

extern  int x;

void fun1( )

{

    static  int  a=5;    //静态变量只在第一次调用函数时赋值,退出函数时

                       //会保留上次的值,下次调用不再重新赋值。

    int b=5;

    printf(“%d  %d  %d  |”,a,b,x);

    a-=2;

    b-=2

    x-=2;

    printf(“%d  %d  %d  |”,a,b,x);

}

int fun2(int y)

{

  return(35*x*y);

}

程序执行如果如下:

       5    5    5  |   3    3    3

       3    5    3  |   1    3    1

       1    5    1  |   -1    3    1

       -1    35

    注:C语言不允许在一个函数内嵌套定义另一个函数。为了能够访问不同文件中各个函数的变量,除了可以采用参数传递的方法外,还可以采用外部变量的方法,上面的例子就说了这一点。不过,尽管使用外部变量在不同函数之间传递数据有时比使用函数参数传递更为方便,不过当外部变量过多时,会增加程序的调试排错的困难。使得程序不便于维护。别外不通过参数传递直接在函数中改变全局变量的值,有时还会发生一些意想不到的副作用。因些最好还是使用函数参数来传递数据。

(3).寄存器变量(register)

    为了提高程序的执行效率,C语言允许将一些频率最高的那些变量,定义为能够直接使用硬件寄存器的所谓的寄存器变量。定义一个变量时,在变量类型名前冠以“register” 即将该变量定义成为了寄存器变量。寄存器变量可以认为是一自动变量的一种。有效作用范围也自动变量相同。由于计算机寄存器中寄存器是有限的。不能将所有变量都定义成为寄存器变量,通常在程序中定义寄存器变量时,只是给编译器一个建议,该变量是否真正成为寄存器变量,要由编译器根据实际情况来确定。另一方面,C51编译器能够识别程序中使用频率最高的变量,在可能的情况下,即使程序中并未将该变量定义为寄存器变量,编译器也会自动将其作为寄存器变量处理。被定义的变量是否真正能成为寄存器变量,最终是由编译器决定的。

(4).静态变量(static)

    使用存储种类说明符“static”定义的变量为静态变量,在上面模块2程序文件中使用了一个静态变量:static  int a =5 ;由于这个变量是在函数fun1( )内部定义,因此称为内部静态变量或局部静态变量。局部静态变量始终都是存在的,但只有在定义它的函数内部进行访问,退出函数之后,变量的值仍然保持,但不能进行访问。

还有一种全局静态变量,它是在函数外部被定义的。作用范围从它的定义点开始,一直到程序结束,当一个C语言程序由若干个模块文件所组成时,全局静态变量始终存在,但它只能在被定义的模块文件中访问,其数据值可为该模块文件内的所有函数共享,退出该文件后,虽然变量的值仍然保持着,但不能被其它模块文件访问。在一个较大的程序中,这就方便了多人设计时,各自写的程序模块不会被别的模块文件所引用。全局静态变量和单纯的全局变量,在编译时就已经为期分配了固定的内存空间,只是他们的作用范围不同而已。局部静态变量是一种在两次函数调用之间仍能保持其值的局部变量。如下,局部变量的使用——计算度输出1~5的阶乘值。

#include<stdio.h>

int  fac( int  n)

{

    static  int  f=1;

    f=f*n;

    return(f);

}

main( )

{

    int  i;

    for(i=1;i<=5;i++)

    printf(“%d!=%d\n”,i,fac(i));

}

程序执行结果

   1!=1

   2!=2

   3!=6

   4!=24

   5!=120

    注:在这个程序中一共调用了5次计算阶乘的函数fac(i),每次调用后输出一个阶乘值i!,同时保留了这个i!值,以便下次再乘(i+1).由此可见,如果要保留函数上一次调用结束时的值,或是在初始化之后变量只被引用而不改变其值,则这时使用局部静态变量;较为方便,以免在每调用时都要重新进行赋值,但是,使用局部静态变量需要占用较多的内存空间,而且降低了程序的可读性,因此并不建议多用局部静态变量。

    静态函数:

    对于函数也可以定义成为具为静态存储种类的属性,定义函数时在函数名前冠以关键字static即将其定义为一个静态函数。例如static  int  func1(char x, y)函数是外部型的,使用静态函数可以使该函数只局限于当前定义它的模块文件中。其它模块文件是不能调用它的。换名话说,就是在其它模块文件中可以定义与静态函数完全同名的另一个函数。不会因为程序中存在相同的函数名而发生函数调用时的混乱。 这一点对于进行模块化程序设计是很有用的。

 

三、中断浅谈

0

外中断0

1

定时器0

2

外中断1

3

定时器1

4

串行口

 

 

 

 

 

 

定义中断函数如下

void  timer1()  interrupt 3  

{

   ……

   ……

}

强烈建议:如上所述,定义中断函数时不要加using n选项。除非你对你的程序以及单片机的工作过程非常熟悉,否则会带来不必要的麻烦。具体原因由于篇幅的限制暂不讨论。

C51中断程序编写要求:

1.中断函数不能进行参数传递,否则,将导致编译出错

2.中断中,不能包含任何参数声明,否则,将导致编译出错。

3.中断函数没有返回值,如果企图定义一个返回值将得到不正确的结果,因些建议在定义中断函数的时将其定义为void 类型,明确说明没有返回值。

4.任何情况下都不能直接调用中断函数,否则会主生编译出错。

5.如果中断函数中用到了浮点运算,必须保存浮点寄存器的状态。当没有其它的程序执行浮点运算时(即只有中断中用到浮点运算),可以不用保存。

6.如果中断函数中调用了其它函数,则被调用的函数所使用的寄存器组必须与中断函数相同,用户必须保证按要求使用相同的寄存器组,否则会产生不正确的结果,这一点必须引起足够的注意,如果定义中断函数时没有使用using选项,则由编译器选择一个寄存器组作绝对寄存器访问。另外,不断的产生不可预测,中断函数对其它函数的调用可能形成递规调用,需要时,可将被中断调用的其它函数定义为再入函数。

 

浅析函数的递规调用与再入函数:

函数的递规调用: 在调用一个函数的过程中双直接或间接的调用该函数本身;    

再入函数:一种可以在函数体内直接或间接调用其自身的一种函数。

     C51编译器采用一个扩展关键字reentrant 作为定义函数时的选项,需要将一个函数定义为再入函数时,只要在函数名后加上关键字reentrant即可。空不空格以及空几格都无所谓。

再入函数剖析:

    再入函数可被递归调用,无论何时,包括中断服务函数在内的任何函数都可调用再入函数。与非再入函数的参数传递和局部就是的存储分配方法不同,C51编译器为每个再入函数都生成一个模拟栈。模拟栈所在的存储器空间根据再入函数的存储模式的不同,可以分配到DATA,PDATA 或XDATA。

对再入函数有如下规定:

1.再入函数不能传送bit类型的参数。也不能定义一个局部位变量,再入函数不能包括位操作以及8051系列单片机的可位寻址区。

2.与PL/ M51兼容的函数,不能具有reentrant属性,也不能调用再入函数。

3.编译时,在存储器模式的基础上,为再入函数在内部或外部存储中建立一个模拟堆栈区,称为再入栈,再入函数的局部变量及参数被放在再入栈中,从而使得再入函数可以进行递规调用。再非再入函数的局部变量被放在再入栈之外的暂存区内,如果对非再入函数进行递规调用,则上次调用时使用的局部变量数据将被覆盖。

4.在同一个程序中可以定义和使用不同存储器模式的再入函数,任意模式的再入函数不能调用不同模式的再入函数,但可以任意调用非再入函数。

5.在参数的传递上,实际参数,可以传递给间接调用的再入函数,无再入属性的间接调用函数不能包含调用参数。但是可以使用定义的全局变量来进行参数传递。

 

四、C51指针深度剖析(非常重要,嵌入式系统开发人员必须要掌握的内容)

注意:由于篇幅所限,本人暂时不打算讨论抽象指针的内容。但是你必须上网或去图书馆找找关于抽象指针的资料好好看看,抽象指针很有用的。

    指针是C语言中的一个重要概念,使用也十分普遍,正确使用指针类型数据可以有效的表示复杂的数据结构,直接处理内存地址,而且可以更为有效的使用数组。

    在C语言中,为了能够实现直接对内存单元的操作,引入了指针类型的数据,指针类型数据是专门用来确定其它数据类型的地址的,因此一个变量的地址就被称为该变量的指针如: 一个整形变量i  存放在内存单元40H中,则该内存单元地址40H就是变量i  的指针。如果有一个变量专门用来存放另一个变量的地址,则称之为“指针变量”

     变量指针与指针变量

    变量的指针:  是指某个变量的地址,而一个指针变量里面存放的是另一个变量在内存中的地址。拥有这个地址的变量则称为该指针变量所指向的变量。 所以每个变量都有它自己的指针(地址),而每一个指针变量都是指向另一个变量的。C语言中用符号“*”来表示“指向”,如下:

     i=50;

     *ip=50;

    如果指针ip这个指针变量指向i那么,两个赋值表达或同义,第二个表达式可以解释为“给指针变量ip所指向的变量赋值50”。

 

(1).指针变量的定义

指针变量的定义与一般变量的定义类似,其一般形式如下:

数据类型  [存储器类型]  * 标识符;

标识符,   是所定义的指针变量名

数据类型,  说明了该指针变量所指向的变量类型

存储器类型,是可选的,它是C51编译器的一种扩展,如果带有此选项,指针被定义为基于存储器的指针,无此选项时,被定义为一般指针,这两种指针的区别在于它们的存储字节不同。

    一般指针:占用三个字节,第一个字节存放该指针存储器类型的编码,第二和第三个字节分别存放该指针的高位和低位地址的偏移量

存储器类型

IDATA

XDATA

PDATA

DATA

CODE

编码值

   1

   2

  3

  4

  5

        

  

 

基于存储器指针:则该指针长度可为一个字节,也可为两字节

一个字节:  (存储器类型 idata data pdata)

两个字节:   (存储器类型为code xdata)

注:在定义指针变量时最好指定其为基于存储器的指针,这个生成的汇编代码长精       练一些,而且也节省空间(读者可自行到C51中写一个程序,查看其反汇编程序)但在一些函数调用的参数中指针需要采用一般指针,为此C51编译器允许这两种指针相互转换,转换规则如下:

一般指针转换成基于存储器指针,采取截断,基于存储器类型指针转换成一般指针采用扩展的。

(2).指针变量的引用

    指针变量是含有一个数据对象地址的特殊变量,指针变量中只能存放地址与指针变量有关的两个运算符:

     &   取地址运算符

     *    间接访问运算符

    &a为取变量a的地址,*P为指针变量P所指向的变量。如下:

        int  i , x, y;

        int  *pi,*px,*py;

        pi=&i;      //将变量i的地址赋给指针变量pi,也即pi指向i

        px=&x;

        py=&py;

        *pi=0;        //等价于i=0

        *px+=6;      //等价于 i+=6

        (*py)++;      //等价于 i++

   注:指向同类数据的指针之间可以相互赋值。如pi=px;

(3).指针变量作为函数的参数

    函数的参数不仅可以是整型,字符型等数据,还可以是指针类型,指针变量作为函数的参数的作用是将一个变量的地址传到另一个函数中去,地址传递是双向的,即主调用函数不仅可以向被调用函数传递参数,而且还可以从被调用函数返回其结果。下面通过一个简单的示例来进行说明。

#include<stdio.h>

swap(int  *pi,int  *pj)

{

    int  temp;

    temp=*pi;  

    *pi=*pj;       //把指针变量pj所指向的变量的值送给pi所指向的变量

    *pj=temp;

}

main( )

{

    int a,b;

    int *pa, *pb;

    a=9;

    b=7;

    pa=&a;

    pb=&b;

    if(a<b) swap(pa,pb);

    printf(“\n max=%d,min=%d \n”,a,b);

}

    上程序上定义了一个swap(  )函数,两个形参为指针变量,在调用函数时,所用的实参也是指针变量,在调用开始,实参变量将它的值传递给形参变量,采取的仍然是“值传递”方式,但这时传递的是指针的值(地址),传递后,形参pi的值为&a,pj的值为&b,即指针变量*pi 和*pa都指向了a, *pj和*pb指向了b。接着使*pj与*pi的值互换,从而达到了实现了a,和b值的互换。虽然函数返回时,pi  pj被释放而不存在,但main函数中a 与b的值已经交换。

(4).数组的指针

    在C语言中,指针与数组有着十分密切的关系,任何能够用数组实现的运算都可以通过指针来完成,例如定义一个具有十个元素的整形数据可以写成:

 int  a[10];

    数组名a表示元素a[0]的地址,而*a 则表示a所代表地址中的内容,即a[0].

    如果定义一个指向整形变量的指针pa并赋以数组a中的第一个元素a[0]的地址;

        int  *pa;

        pa=&a[0];   //也可写成pa=a;

    则可通过指针pa来操作数组a了,即可用*pa代表a[0];*(pa+i)代表a[i],也可以上pa[0];pa[1];pa[2]……pa[9]的形式

(5).字符数组的指针  

    用指针来描述一个字符数组是十分方便的,字符串是以字符数组的形式给出的,并且每个字符数组都是以转义字符‘\0’作为字符串的结束标志。因此在判断一个字符数组是否结束时,通常不采用计数的方法,而是以是否读到转义字符‘\0’来判别。利用这个特点,可以很方便的用指针处理字符数组。

示例如下:

 #include<stdio.h>

 main()

 {

    char  *s1;

    char  xdata  *s2;

    char code str[]={“how are you?”};

    s1=str;

    s2=0x1000;

    while((*s2=*s1)!=’\0’)

    {

        s2++;

        s1++;

    }

    s1=str;

    s2=0x1000;

    printf(“%s  \n,%s\n”,s1,s2);

}

    注:任何一个数组及其数组元素都可以用一个指针及其偏移值来表示,但要注意的是,指针是一个变量,因此像上例中的赋值 运算s1=str, s2=0x1000都是合法的。而数组名是一个常量,不能像变量那样进行运算,即数组的地址是不能改变的。如上面程序中的语句

char code str[]={“how are you?”};

是将字符串“how are you?”置到数组str中作为初值,而语句s1=str则是将数组str的首地址,即指向数组str的指针赋给指针变量s1,如果对数组进行如下的操作:

   str=s1;

   str++;

  都是错误的。

(6).指针的地址计算

指针的地址的计算包括以下几个方面:

1 赋初值

指针变量的初值可以是NULL(零),也可以是变量,数组,结构及函数等的地址,例如

   int   a[10],b[10];

   float  fbuf[100];

   char *cptr1=NULL;

   char *cptr2=&ch;

   int *iptrl=&a[5];

   int *iptr2=&b;

   float *flptr1=fbuf;

2 指针与整数的加减

    指针可以与一个整数或整数表达式进行加减运算,从而获得该指针当前所指位置前面或后面某个数据的地址。假设p为一个指针变量,n为一个整数,则p+n表示离开指针p当前位置的后面第n个数据的地址。

3 指针与指针相减

    指针与指针相减的结果为一个整数值,但它并不是地址,而是表示两个指针之间的距离或元素的个数,注意,这两个指针必须是指向同一类型的数据。

4 指针与指针的比较

    指向同一类型数据的两个指针可以进行比较运算,从面获得两指针所指地址大小的关系,此外,在计算指针地址的同时,还可以进行间接取值运算,不过在这种情况下,间接取值的地址应该是地址计算后的结果,并且还必须注意运算符的优先级和结合规则。如下设p1是一个指针

    a= *p1++;

   *与++优先级相同,所以上述赋值操作过程是首先将指针p1 所指向的内容赋值给变量a, 然后p1再指向下一个数据,表明是地址增加而不是p1所指向的变量内容增加。

    a=* - -p1;

    与上例相同,此处是先p1减一,指向前面一个数据,然后再把p1此时所指向的内容赋给变得a

    a=(*p2)++;

    此处,由于使用了括号,使得结合次序发生了变化,因此首先是将p2所指的内容赋值给变量a,然后再把p2所指向的变量加1,表明是p1所指变量的大小加一,面不是p1指向下一个元素。

 (7).函数型指针

    函数不是变量,但它在此内存中仍然需要占据一定的存储空间,如果将函数的入口地址赋给一个指针,该指针就是函数型指针,由于函数型指针指向的是函数的入口地址,因此可用指向函数的指针代替函数来调用该函数。利用函数指针,可以将函数作为参数传递给另一个函数,此处还可以将函数型指针放在一个指针数组中,则该指针的数组中每一个元素都是指向某个函数的指针。

  函数型指针定义形式:

  数据类型  (*标识符)();

   标识符为所定义的函数型指针变量名,数据类型说明了该指针指向函数的返回值类型。例如:

   Int ( *func1)(  );

     注:函数型指针变量是专门用来存放函数入口地址的,在程序中把哪个函数的地址赋给它,它就指向那个函数,在程序中可以对一个函数型指针多次赋值,该指针可以先后指向不同的函数。后面括号中不要加形参表。

   函数型指针赋值形式

   函数型指针变量名=函数名

   注,赋值时不带形参表,如有一个max(x,y), 可做执行如下操作

     func1=max;

   引入了函数型指针后,对函数的调用就可以采用如下两种方法。

           A:  z=max(x,y);

           B:  z=( *func1)(x,y);

   注意  若采用函数型指针来调用函数,必须预先对该函数指针进行赋值,使之指向所需调用的函数。

   函数型指针通常用来将一个函数的地址作为参数传递到另一个函数中去,这种方法对于要调用的函数不是某个固定函数的场合特别适用。

    作如下示例让你更加明白

    #include<stdio.h>

    int  max( int x,int y)

    {

        if(x>y)

           return(x);

        else

           return(y);

    }

    int  min(int x,int y)

    {

        if(x<y)

           return(x);

        else

           return(y);

}

int add(int x,int y)

{

  return(x+y);

}

int process(int  x,int  y, int( * f)( )  )

{

  int result=f(x,y );

  printf(“%d\n”,result);

}

main()

{

   int a,b;

   printf(“Please input a and b:\n”);

   scanf(“%d %d”,&a,&b);

   printf(“max=”);

   process(a,b,max);

   printf(“min=”);

   process(a,b,min);

   printf(“sum=”);

   process(a,b,add);

}

    本例中三个函数max(),min(),add(),在第一次调用process( )函数时,除了将a b作为实参传递给了形参,还将函数名max作为实参将其入口地址传递给了process(  )函数中的形参——指向函数的指针变量*f。process()中的函数调用语句,result=f(x,y)就相当于result=max(x,y),第二次调用时用了min作为实参,第三次用了add.从而实现每次调用process( )函数时完成了不同的功能。

(8).返回指针型数据的函数

    在函数的调用过程结束时,被调用的函数可以带一个整形,字符等到类型的数据,也可以带一个指针型数据。即地址。这种返回指针型数据的函数又称为指针函数。

    注意区别指针函数与函数指针。

    指针函数定义如下:

    数据类型   * 函数名(参数表);

    其中数据类型说明了所定义的指针函数返回的指针所指向的数据类型。

    int   *x(a,b);

    就定义了一个指针函数*x调用它以后可以得到一个指向整型数据的指针。注意*x 两则没有括号。这与函数指针是完全不同的,并且定义函数指针时,后面的括号是不加形参表列的。也很容易混淆。下面分别定义一个指针函数和函数指针。

    函数指针:    int  (*x)(   );

    指针函数:    int   *x(char  a,b)

    如下示例,指针函数的应用

       #include<stdio.h>

       main()

       {

            Float T[3][4]  = {{60.1,70.3,80.5,90.7},

                           {30.0,40.1,50.2,60.3},

                           {90.0,80.5,70.4,60.6}};

            float  * search(float  (*pointer)[4],int n);

            float  *p;

            int  i, m;

            printf(“please enter the number of chanal:”);

            scanf(“%d”,&m);

            printf(“\n The temperature of chanal %d  are: \n”,m);

            p=search(T,m);

            for(i=0;i<4;i++)

            printf(“%5.1f”,*(p+i));

      }

         float  *scarch(float  (*pointer)[4],int n)

         {

             float  *pt;

             pt=*(pointer+n);

             return(pt);

         }

    上程序中,红色标出来的那一行是定义了一个指针型函数,它的形参pointer是指向包含4个元素的一维数组的指针变量。于是pointer+i 就是指向二维数组T的第i行,而*(pointer+i)则指向第i行的第一个元素。pt是一个指针变量。调用search( )后,返回了一个指向第m行的首地址,

(9).指针数组

    由于指针本身也是一个变量,因此C语言允许定义指针数组,指针数组适合用来指向若干个字符串,使得字符串的处理更加方便。指针数组的定义方法与普通数组完全相同,一般格式如下:

   数据类型  * 数组名[数组长度];

   例如:

       int  *x[2];

       char  *sptr[5];

    

    指针数据在使用之前往往需要先赋初值,方法与一般数组赋初值类似,。使用指针数组最典型的场合就是通过对字符数组赋初值而实现各维长度不一致的多维数组的定义

#include<stdio.h>

main( )

{

    int  i;

    char  code *season[4]={“spring”,”summer”,”fall”,”winter”};

    for(i=0;i<4;i++)

       printf(“\n%c--------%s”,*season[i],season[i]);

}

程序执行结果:

  s--------spring

  s--------summer

  f--------fall

  w--------winter

     在这个例子中,在code区定义了指向char型数据的4个指针,其初值分别为“spring”,”summer”,”fall”和”winter,这样可以使这四个数组保存在一段地址连续的地址空间里(此可以通过程序验证),如果采用二维数组,那么会造成内存空间的浪费。因为二维数组的长度必须一致,且要等于最大的一列长度。

    下面写一个(经典)示例程序(不做解释)

        #include<stdio.h>

        char  code daytab[2][13]=

        {

             {0,31,28,31,30,31,30,31,31,30,31,30,31},

             {0,31,29,31,30,31,30,31,31,30,31,30,31},

        };

         char  *mname(int  n)

         {

              char  code *mn[]=

              {

                  “llligal month”,”january”,”February”,

                  “March”,”April”,”May”,”June”,

                  “July”,”August”,”September”,

                  “October”,”Novenmber”,”December”   

              };

              return((n<1||n>12)?mn[0];mn[n]);

          }

          monthday( int y,int yd)

          {

             int  i,leap;

             leap=y%4==0&&y%100!=0||y%400==0;

             for(i=1;yd>daytab[leap][i];i++)

              yd-=day[leap][i];

              printf(“%s,%d\n”,mname(i),yd);

           }

main( )

{

   int  year,yearday;

   printf(“input year and yearday: \n”);

   scanf(“%d,%d”,&year,&yearday);

   monthday(year,yearday);

}

(10).指针型指针

指针型指针所指向的是另一个指针变量的地址,故有时也称为多级别指针。

      定义指针型指针的一般形式: 

      数据类型   **标识符

      标识符:    定义的指针型指针变量。

      数据类型:  说明一个被指针型指针所指向的指针变量所指向的变量数据类型。

  #include<stdio.h>

                  main(  )

                  {

                    int  x,*p,**q;

                    x=10;

                    p=&x;

                    q=&p;

                    printf(“  %d\n  ”,x);        //直接取值

                    printf(“  %d\n  ”, *p);      //单重间接取址

                    printf(“  %d\n  ”, **q);     //多重间接取址

}

三行均是打印出10.

    一个指针型指针是一种间接取值的形式,而且这种间接取值的方式还可以进一步延伸,故可以将这种多重间接取值的形式看成一个打针链

使用指针型指针的例子

#include<stdio.h>

main(  )

{

    char    i;

    char  **j;

    char   *season[4]={ “spring”,”summer”,”fall”,”winter”} ;

    for(i=0;i<4;++i)

    {

        j=season+i;

        printf(“\n%c--------%s”,*season[i] ,  *j);  

    }

}

原创粉丝点击