Keil Cx51 V7.0单片机高级语言编程与μVision2应用实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第4章 数组与指针

在第2章中介绍了C语言的基本数据类型,如整型、字符型、浮点型等,除此之外C语言还提供一种构造类型的数据。构造类型数据是由基本类型数据按一定规则组合而成的,又称为导出类型数据。C语言中的构造类型数据有数组类型、结构类型以及联合类型等。本章介绍数组类型的数据,结构和联合类型数据将在第5章介绍。由于数组和指针有着十分密切的联系,因此本章还将介绍在C语言中用途极为广泛的指针类型数据。

4.1 数组的定义与引用

数组是一组有序数据的集合,数组中的每一个数据都属于同一种数据类型。数组中的各个元素可以用数组名和下标来唯一地确定。一维数组只有一个下标,多维数组有两个以上的下标。在C语言中数组必须先定义,然后才能使用。一维数组的定义形式如下:

数据类型  数组名[常量表达式];

其中,“数据类型”说明了数组中各个元素的类型。

“数组名”是整个数组的标识符,它的定名方法与变量的定名方法一样。

“常量表达式”说明了该数组的长度,即该数组中的元素个数。常量表达式必须用方括号“[ ]”括起来,而且其中不能含有变量。下面是几个定义一维数组的例子:

    char  x[5];          /* 定义字符型数组x,它具有5个元素 */
    int   y[10];         /* 定义整型数组y,它具有10个元素 */
    float z[15];         /* 定义浮点型数组z,它具有15个元素 */

定义多维数组时,只要在数组名后面增加相应于维数的常量表达式即可。对于二维数组的定义形式为:

数据类型  数组名[常量表达式1][常量表达式2];

例如,要定义一个10×10的整数矩阵A,可以采用如下的定义方法:

int A[10][10];

需要指出的是,C语言中数组的下标是从0开始的,因此对于数组char x[5]来说,其中的5个元素是x[0]~x[4],不存在元素x[5],这一点在引用数组元素时是应当加以注意的。C语言规定在引用数值数组时,只能逐个引用数组中的各个元素而不能一次引用整个数组;但如果是字符数组则可以一次引用整个数组。

例4.1:用数组计算并输出Fibonacci数列的前20项。

Fibonacci数列在数学和计算机算法研究中是十分有用的。Fibonacci数列是这样的一组数:第一个数字为0,第二个数字为1,之后每个数字都是前两个数字之和。

#include<stdio.h>
main() {
  int f[20], i;
  f[0]=0;
  f[1]=1;
  for (i=2;i<20; i++)
      f[i]=f[i-2]+f[i-1];
    for(i=0; i<20; i++)
      {
      if(i%5==0)
        printf("\n");
      printf("%10d",f[i]);
      }
  while(1);
}

程序执行结果:

    0    1    1    2    3
    5    8    13   21   34
    55   89   144   233   377
    610   987   1597  2584  4181

4.2 字符数组

用来存放字符数据的数组称为字符数组,它是C语言中常用的一种数组。字符数组中的每个元素都是一个字符,因此可用字符数组来存放不同长度的字符串。字符数组的定义方法与一般数组相同,下面是两个定义字符数组的例子:

char menu[20];
char string[50];

在C语言中字符串是作为字符数组来处理的。一个一维的字符数组可以存放一个字符串,这个字符串的长度应小于或等于字符数组的长度。为了测定字符串的实际长度,C语言规定以“\0”作为字符串结束标志,对字符串常量也自动加一个“\0”作为结束符。因此字符数组char menu[20]可存储一个长度≤19的不同长度的字符串。

在访问字符数组时,遇到“\0”就表示字符串结束,因此在定义字符数组时,应使数组长度大于它允许存放的最大字符串的长度。另外,符号“\0”是一个表示ASCII码值为0的字符,它不是一个可显示字符,而是一个“空操作符”,在这里仅仅起一个结束标志的作用。

对于字符数组的访问可以通过数组中的元素逐个进行访问,也可以对整个数组进行访问。

例4.2:对字符数组进行输入和输出。

#include<stdio.h>
main() {
  char c[10];
  scanf("%s",c);
printf("%s\n",c);
while(1);
}

程序中用“%s”格式控制输入输出字符串,这里的输入输出操作是对整个字符数组进行的,因此输入项必须是数组名c,而不能用数组元素名c[i]。在μVision2环境下对例4.2程序编译连接通过后进行仿真调试,启动程序全速运行,将光标移到串行窗口,从键盘输入HELLO并回车,系统会自动在输入的字符串后面加一个结束符“\0”,然后输出HELLO,如果输入的字符数大于10,则只取前10个字符作为有效字符输出。

前面介绍了数组的定义方法,可以在内存中开辟一个相应于数组元素个数的存储空间,数组中各个元素的赋值是在程序运行过程中进行的。如果希望在定义数组的同时给数组中各个元素赋以初值,可以采用如下方法:

数据类型  [存储器类型] 数组名[常量表达式]={常量表达式表};

其中,“数据类型”指出数组元素的数据类型。

“存储器类型”是可选项,指出定义数组所在的存储器空间。

“常量表达式表”中给出各个数组元素的初值。

需要注意的是,在定义数组的同时对数组元素赋初值时,初值的个数必须小于或等于数组中元素的个数(即数组长度),否则在程序编译时作为出错处理。赋初值时可以不指定数组的长度,编译器会根据初值的个数自动计算出该数组的长度。因此数组名后面的“常量表达式”为可选项,省略该选项时数组的长度由实际初值的个数决定。例如:

unsigned char a[5]={0x11,0x22,0x33,0x44,0x55}
unsigned char xdata a[]={0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99}

对于多维数组可以采用同样的方法来赋值,例如,可用下面的方法在定义一个二维数组的同时赋以初值:

int MAX[4][3]=
    {{1,4,7}, {2,5,8}, {3,6,9}, {0, 0, 0}};

例4.3:用冒泡法对一组数据进行排序。

#include <reg51.h>
#include <stdio.h>
void main()  {
    unsigned char xdata a[]=
    {0x3f,0x44,0x32,0x54,0x66,0x56,0x99,0x88,0x77,0x11,0x34};
     unsigned char i,j,t;
     printf("the unsorted numbers : \n");
     for (i=0;i<=9;i++)
      printf("%bx  ",a[i]);
      printf("\n");
     for (j=0;j<=8;j++)
      for (i=0;i<=9-j;i++)
        if (a[i]>a[i+1])  {t=a[i];a[i]=a[i+1];a[i+1]=t;}
    printf("the sorted numbers : \n");
    for (i=0;i<=10;i++)  printf("%bx  ",a[i]);
    while(1);
}

程序执行结果:

    the unsorted numbers :
    0x3f,0x44,0x32,0x54,0x66,0x56,0x99,0x88,0x77,0x11,0x34
    the sorted numbers :
    0x11,0x32,0x34,0x3f,0x44,0x54,0x56,0x66,0x77,0x88,0x99

给字符数组赋初值对于在程序存储器ROM中制作一些常数表格特别有用,例如,可以采用如下方法在ROM中制作一张共阴极LED的显示字符段码表:

char code SEG[11]= {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};

利用字符数组可以很方便地实现LED段码的查表显示。图4.1是利用8031单片机串行口实现的动态LED扫描显示接口电路,在串行口上扩展一片移位寄存器74LS164作为共阴极7段LED的段码数据口,8031的P1.0~P1.3作为LED显示器的位扫描信号,串行口工作于移位寄存器方式(方式0)。执行下面的程序后可在LED上显示出“8031”这几个数字。

例4.4:利用字符数组实现LED数字显示。

#include <reg51.h>
char code seg[]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f};
void dsp(char digt, j)  {
    unsigned char tmp;
    SBUF=seg[digt];
    P1=j;
    tmp=0x7f;
    while(tmp--);
}
void main()  {
    SCON=0x00;
    while(1) {
      dsp(0x08, 1);
      dsp(0x00, 2);
      dsp(0x03, 4);
      dsp(0x01, 8);
    }
}

图4.1 用串行口实现的LED动态显示电路

4.3 数组作为函数的参数

除了可以用变量作为函数的参数之外,还可以用数组名作为函数的参数。一个数组的数组名表示该数组的首地址。用一个数组名作为函数的参数时,在进行函数调用的过程中参数传递方式采用的是地址传递。将实际参数数组的首地址传递给被调函数中的形式参数数组,这样一来两个数组就占用同一段内存单元。如图4.2所示,若数组a的起始地址为0x1000,则数组b的起始地址也是0x1000,显然数组a和b占用同一段内存单元。

图4.2 两个数组占用同一段内存单元

用数组名作为函数的参数,应该在主调函数和被调函数中分别进行数组定义,而不能只在一方定义数组。而且在两个函数中定义的数组类型必须一致,如果类型不一致将导致编译出错。实参数组和形参数组的长度可以一致也可以不一致,编译器对形参数组的长度不作检查,只是将实参数组的首地址传递给形参数组。如果希望形参数组能得到实参数组的全部元素,则应使两个数组的长度一致。定义形参数组时可以不指定长度,只在数组名后面跟一个空的方括号[]。这时为了在被调函数中处理数组元素的需要,应另外设置一个参数来传递数组元素的个数。

例4.5:用数组作为函数的参数,计算两个不同长度的数组中所有元素的平均值。

#include<stdio.h>
float average(array, n)
int n;
float array[];
 {
  int i;
  float aver, sum=array[0];
  for (i=1; i<n; i++)
  sum=sum+array[i];
  aver=sum/n;
  return(aver);
}
main() {
  float pot_1[5]={99.9,88.8,77.7,66.6,0};
  float pot_2[10]={11.1,22.2,33.3,44.4,55.5,99.9,88.8,77.7,66.6,0};
  printf("the average of A is  %6.2f\n", average(pot_1,5));
  printf("the average of B is  %6.2f\n", average(pot_2,10));
  while(1);
}

程序执行结果:

    the average of A is 66.60
    the average of B is 49.95

在这个程序中定义了一个求平均值的函数average(),它有两个形式参数array和n。array是一个长度不定的float类型的数组,n是int类型的变量。在主函数main()中定义了两个长度确定的float类型的数组pot_1[5]和pot_2[10]。通过嵌套函数调用实现求数组元素平均值的运算并输出。可以看到,两次调用average()函数时数组的长度是不同的,在调用时通过一个实际参数(5或10)将数组的长度传递给形式参数n,从而使average()函数能处理数组pot_1和pot_2中的所有元素。

用数组名作为函数的参数,参数的传递过程采用的是地址传递。地址传递方式具有双向传递的性质,即形式参数的变化将导致实际参数也发生变化,这种性质在程序设计中有时是很有用的。下面的例子就是利用这一性质来对数组a中的10个整数按从小到大的顺序排序。

例4.6:采用选择法对数组元素进行排序。

#include<stdio.h>
void sort (array, n)
int array[];
int n;
  {
    int i, j, k, t;
    for (i=0; i<n-1; i++)
      {
      k=i;
      for (j=i+1; j<n; j++)
      if (array[j]<array[k])  k=j;
      t=array[k]; array[k]=array[i]; array[i]=t;
      }
  }
main()  {
  int a[10], i;
  printf("please enter the array\n");
  for (i=0; i<10; i++)
  scanf("%d", &a[i]);
  sort(a, 10);
  printf("the sorted array is: \n");
  for (i=0; i<10; i++)
  printf("%d  ", a[i]);
  printf("\n");
  while(1);
  }

程序执行结果:

    please enter the array
    9
    7
    5
    3
    1
    8
    6
    4
    2
    0
    the sorted array is:
    0  1  2  3  4  5  6  7  8  9

在这个例子中,主程序在执行函数调用语句sort(a, 10)的前后,数组a中各个元素的值是不同的。执行函数调用语句之前,数组a中的元素是无序的,执行函数调用语句之后,数组a中的元素已经按大小排序。这是因为在执行被调用函数时,形式参数数组已经进行了排序,而形式参数数组的改变会使实际参数数组随之改变,从而使主函数中的数组a也进行了排序。

对于多维数组作为函数的参数与一维数组的情形类似。可以用多维数组名作为函数的实际参数和形式参数,在被调用函数中对形式参数说明时可以指定每一维的长度,也可以省略数组第一维的长度说明,但是绝不能省略第二维以及其他高维的长度说明。因为从实际参数传送过来的是数组的起始地址,在内存中数组是按行存放的,而并不区分数组的行和列。如果在形式参数中不说明列数,编译器就无法确定该数组有几行几列。

例4.7:求一个3×4矩阵中的最大元素。

#include<stdio.h>
max(int array[][4])  {
  int i, j, max;
  max=array[0][0];
  for (i=0; i<3; i++)
    for (j=0; j<4; j++)
      if (array[i][j]>max)  max=array[i][j];
  return(max);
}
main()  {
  int a[3][4]={{1,3,5,7},{2,4,6,8},{15,17,34,12}};
  printf("max value is  %d\n", max(a));
  while(1);
  }

程序执行结果:

    max  value  is 34

4.4 指针

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

4.4.1 指针与地址

众所周知,一个程序的指令、常量和变量等都要存放在机器的内存单元中,而机器的内存是按字节来划分存储单元的。给内存中每个字节都赋予一个编号,这就是存储单元的地址。各个存储单元中所存放的数据,称为该存储单元的内容。计算机在执行任何一个程序时都要涉及许多的寻址操作。所谓寻址,就是按照内存单元的地址来访问该存储单元中的内容,即按地址来读或写该单元中的数据。由于通过地址可以找到所需要的存储单元,因此可以说地址是指向存储单元的。

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

变量的指针和指针变量是两个不同的概念。变量的指针就是该变量的地址,而一个指针变量里面存放的内容是另一个变量在内存中的地址,拥有这个地址的变量则称为该指针变量所指向的变量。每一个变量都有它自己的指针(即地址),而每一个指针变量都是指向另一个变量的。

为了表示指针变量和它所指向的变量之间的关系,C语言中用符号“*”来表示“指向”。例如,整型变量i的地址40H存放在指针变量ip中,则可用*ip来表示指针变量ip所指向的变量,即*ip也表示变量i,下面两个赋值语句:

i=0x50;
*ip=0x50;

都是给同一个变量赋值0x50。图4.3形象地说明了指针变量ip和它所指向的变量i之间的关系。

图4.3 指针变量和它所指向的变量

从图4.3可以看到,对于同一个变量i,可以通过变量名i来访问它,也可以通过指向它的指针变量ip,用*ip来访问它。前者称为直接访问,后者称为间接访问。符号“*”称为指针运算符,它只能与指针变量一起联用,运算的结果是得到该指针变量所指向的变量的值。

在2.2.5节中介绍了一个取地址运算符“&”,它可以与一个变量联用,其作用是求取该变量的地址。通过运算符“&”可以将一个变量的地址赋值给一个指针变量。例如:赋值语句ip=&I;它的作用是取得变量i的地址并赋给指针变量ip。通过这种赋值后即可以说指针变量ip指向了变量i。不要将符号“&”和“*”弄混淆,&i是取变量i的地址,*ip是取指针变量ip所指向的变量的值。

4.4.2 指针变量的定义

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

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

其中“标识符”是所定义的指针变量名。

“数据类型”说明了该指针变量所指向的变量的类型。

“存储器类型1”和“存储器类型2”是可选项,它是Keil Cx51编译器的一种扩展,如果带有“存储器类型1”选项,指针被定义为基于存储器的指针,无此选项时,被定义为一般指针。这两种指针的区别在于它们的存储字节不同。一般指针在内存中占用3个字节,第一个字节存放该指针存储器类型的编码(由编译时编译模式的默认值确定),第二个和第三个字节分别存放该指针的高位和低位地址偏移量。存储器类型的编码值如下:

“存储器类型2”选项用于指定指针本身的存储器空间。

一般指针可用于存取任何变量而不必考虑变量在8051单片机存储器空间的位置,许多C51库函数采用了一般指针,函数可以利用一般指针来存取位于任何存储器空间的数据。下面是一个一般指针定义的例子,同时给出了汇编语言代码,请注意汇编语言代码中指针第一个字节是存储器类型编码值,第二、三字节是地址偏移量。

例4.8:定义一般指针。

3stmt level   source
  1         char *c_ptr;                /* char ptr */
  2         int *i_ptr;                 /* int ptr */
  3         long *l_ptr;                /* long ptr */
  4
  5         void main (void)
  6         {
  7   1     char data dj;               /* data vars */
  8   1     int data dk;
  9   1     long data dl;
  10   1
  11   1      char xdata xj;            /* xdata vars */
  12   1      int xdata xk;
  13   1      long xdata xl;
  14   1
  15   1      char code cj = 9;          /* code vars */
  16   1      int code ck = 357;
  17   1      long code cl = 123456789;
  18   1
  19   1
  20   1      c_ptr = &dj;               /* data ptrs */
  21   1      i_ptr = &dk;
  22   1      l_ptr = &dl;
  23   1
  24   1      c_ptr = &xj;               /* xdata ptrs */
  25   1      i_ptr = &xk;
  26   1      l_ptr = &xl;
  27   1
  28   1      c_ptr = &cj;               /* code ptrs */
  29   1      i_ptr = &ck;
  30   1      l_ptr = &cl;
  31   1     }
  32
ASSEMBLY LISTING OF GENERATED OBJECT CODE
              ;FUNCTION main (BEGIN)
                                          ;SOURCE LINE # 5
                                          ;SOURCE LINE # 6
                                          ;SOURCE LINE # 20
0000750000     R    MOV    c_ptr,#00H
0003750000     R    MOV    c_ptr+01H,#HIGH dj
0006750000     R    MOV    c_ptr+02H,#LOW dj
                                          ;SOURCE LINE # 21
0009750000     R    MOV    i_ptr,#00H
000C 750000     R    MOV    i_ptr+01H,#HIGH dk
000F 750000     R    MOV    i_ptr+02H,#LOW dk
                                          ;SOURCE LINE # 22
0012750000     R    MOV    l_ptr,#00H
0015750000     R    MOV    l_ptr+01H,#HIGH dl
0018750000     R    MOV    l_ptr+02H,#LOW dl
                                          ;SOURCE LINE # 24
001B 750001     R    MOV    c_ptr,#01H
001E 750000     R    MOV    c_ptr+01H,#HIGH xj
0021750000     R    MOV    c_ptr+02H,#LOW xj
                                          ;SOURCE LINE # 25
0024750001     R    MOV    i_ptr,#01H
0027750000     R    MOV    i_ptr+01H,#HIGH xk
002A 750000     R    MOV    i_ptr+02H,#LOW xk
                                          ;SOURCE LINE # 26
002D 750001     R    MOV    l_ptr,#01H
0030750000     R    MOV    l_ptr+01H,#HIGH xl
0033750000     R    MOV    l_ptr+02H,#LOW xl
                                          ;SOURCE LINE # 28
00367500FF     R    MOV    c_ptr,#0FFH
0039750000     R    MOV    c_ptr+01H,#HIGH cj
003C 750000     R    MOV    c_ptr+02H,#LOW cj
                                        ;SOURCE LINE # 29
003F 7500FF     R    MOV    i_ptr,#0FFH
0042750000     R    MOV    i_ptr+01H,#HIGH ck
0045750000     R    MOV    i_ptr+02H,#LOW ck
                                        ;SOURCE LINE # 30
00487500FF     R    MOV    l_ptr,#0FFH
004B 750000     R    MOV    l_ptr+01H,#HIGH cl
004E 750000     R    MOV    l_ptr+02H,#LOW cl
                                        ;SOURCE LINE # 31
0051 22               RET
            ;FUNCTION main (END)

上例中一般指针c_ptr、i_ptr、l_ptr全部位于8051单片机的片内数据存储器中,如果在定义一般指针时带有“存储器类型2”选项,则可指定一般指针本身的存储器空间位置,例如:

char * xdata strptr;            /* 位于xdata空间的一般指针 */
int * data numptr;              /* 位于data空间的一般指针   */
long * idata varptr;            /* 位于idata空间的一般指针 */

由于一般指针所指对象的存储器空间位置只有在运行期间才能确定,编译器在编译期间无法优化存储方式,必须生成一般代码以保证能对任意空间的对象进行存取,因此一般指针所产生的代码运行速度较慢,如果希望加快运行速度则应采用基于存储器的指针。基于存储器的指针所指对象具有明确的存储器空间,长度可为1个字节(存储器类型为IDATA、DATA、PDATA)或2个字节(存储器类型为CODE、XDATA),例如:

char data * str;                /* 指向DATA空间char型数据的指针 */
int xdata *numtab;              /* 指向XDATA空间int型数据的指针 */
long code *powtab;              /* 指向CODE空间long型数据的指针 */

与一般指针类似,若定义时带有“存储器类型2”选项,则可指定基于存储器的指针本身的存储器空间位置,例如:

char data * xdata str;
int xdata * data numtab;
long code * idata powtab;

基于存储器的指针长度比一般指针短,可以节省存储器空间,运行速度快,但它所指对象具有确定的存储器空间,缺乏灵活性。下面是一个基于存储器的指针定义例子,其汇编代码长度明显小于一般指针。

例4.9:基于存储器的指针定义。

stmt level   source
  1         char data *c_ptr;               /* memory-specific char ptr */
  2         int xdata *i_ptr;               /* memory-specific int ptr */
  3         long code *l_ptr;               /* memory-specific long ptr */
  4
  5         long code powers_of_ten []=
  6         {
  7          1L,
  8          10L,
  9          100L,
 10          1000L,
 11          10000L,
 12          100000L,
 13          1000000L,
 14          10000000L,
 15          100000000L,
 16         };
 17
 18          void main (void) {
 19   1       char data strbuf [10];
 20   1       int xdata ringbuf [1000];
 21   1
 22   1       c_ptr = &strbuf [0];
 23   1       i_ptr = &ringbuf [0];
 24   1       l_ptr = &powers_of_ten [0];
 25   1       }
 26
ASSEMBLY LISTING OF GENERATED OBJECT CODE
              ;FUNCTION main (BEGIN)
                                    ;SOURCE LINE # 18
                                    ;SOURCE LINE # 22
0000750000     R    MOV    c_ptr,#LOW strbuf
                                    ;SOURCE LINE # 23
0003750000     R    MOV    i_ptr,#HIGH ringbuf
0006750000     R    MOV    i_ptr+01H,#LOW ringbuf
                                    ;SOURCE LINE # 24
0009750000     R    MOV    l_ptr,#HIGH powers_of_ten
000C 750000     R    MOV    l_ptr+01H,#LOW powers_of_ten
                                    ;SOURCE LINE # 25
000F 22              RET
          ;FUNCTION main (END)

在一些函数调用中进行参数传递时需要采用一般指针,例如,Cx51的库函数printf()、sprintf()、gets()等便是如此。当传递的参数是基于存储器的指针时,若不特别指明,Keil Cx51编译器会自动将其转换为一般指针,如果被调用函数的参数应该为某种较短长度指针,则会产生程序出错,为避免此类错误,应采用#include预处理器命令将函数的说明文件包含到C语言源程序中去。

一般指针与基于存储器的指针转换规则如下(一般指针用GP表示)。

GP→xdata:使用GP的偏移部分(2字节)。

GP→code:同上。

GP→idata:使用GP偏移部分的低字节,高字节不用。

GP→data:同上。

GP→pdata:同上。

xdata→GP:一般指针的存储器类型编码被设定为0x01,使用xdata *的双字节偏移量。

code→GP:一般指针的存储器类型编码被设定为0xFF,使用code *的双字节偏移量。

idata→GP:一般指针的存储器类型编码被设定为0x00,指针的一字节偏移量被转换为unsigned int类型。

data→GP:同上。

pdata→GP:一般指针的存储器类型编码被设定为0xFE,指针的一字节偏移量被转换为unsigned int类型。

例4.10:指针转换及其所生成的代码。

stmt level   source
  1         int *p1;        /* 一般指针,3字节 */
  2         int xdata *p2;  /* xdata指针,2字节 */
  3         int idata *p3;  /* idata指针,1字节 */
  4         int code *p4;   /* code指针,2字节 */
  5
  6         void pconvert (void) {
  7   1       p1 = p2;    /* xdata指针转换为一般指针 */
  8   1       p1 = p3;    /* idata指针转换为一般指针 */
  9   1       p1 = p4;    /* code指针转换为一般指针 */
  10   1
  11   1       p4 = p1;    /* 一般指针转换为code指针 */
  12   1       p3 = p1;    /* 一般指针转换为idata指针 */
  13   1       p2 = p1;    /* 一般指针转换为xdata指针 */
  14   1
  15   1       p2 = p3;    /* idata指针转换为xdata指针 (警告错误) */
  16   1       p3 = p4;    /* code指针转换为idata指针 (警告错误) */
  17   1      }
  18
ASSEMBLY LISTING OF GENERATED OBJECT CODE
            ;FUNCTION pconvert (BEGIN)
                                      ;SOURCE LINE # 6
                                      ;SOURCE LINE # 7
0000750001     R    MOV    p1,#01H
0003850000     R    MOV    p1+01H,p2
0006850000     R    MOV    p1+02H,p2+01H
                                      ;SOURCE LINE # 8
0009750000     R    MOV    p1,#00H
000C 750000     R    MOV    p1+01H,#00H
000F 850000     R    MOV    p1+02H,p3
                                      ;SOURCE LINE # 9
0012 AA00       R    MOV    R2,p4
0014 A900       R    MOV    R1,p4+01H
0016 7BFF            MOV    R3,#0FFH
0018 8B00       R    MOV    p1,R3
001A 8A00       R    MOV    p1+01H,R2
001C 8900       R    MOV    p1+02H,R1
                                      ;SOURCE LINE # 11
001E AE02            MOV    R6,AR2
0020 AF01            MOV    R7,AR1
0022 8E00       R    MOV    p4,R6
0024 8F00       R    MOV    p4+01H,R7
                                      ;SOURCE LINE # 12
0026 AF01            MOV    R7,AR1
0028 8F00       R    MOV    p3,R7
                                      ;SOURCE LINE # 13
002A AE02            MOV    R6,AR2
002C 8E00       R    MOV    p2,R6
002E 8F00       R    MOV    p2+01H,R7
                                      ;SOURCE LINE # 15
0030750000     R    MOV    p2,#00H
0033 8F00       R    MOV    p2+01H,R7
                                      ;SOURCE LINE # 16
0035850000     R    MOV    p3,p4+01H
                                      ;SOURCE LINE # 17
0038 22              RET
            ;FUNCTION pconvert (END)

4.4.3 指针变量的引用

指针变量是含有一个数据对象地址的特殊变量,指针变量中只能存放地址。与指针变量有关的运算符有两个,它们是取地址运算符&和间接访问运算符*。例如:&a为取变量a的地址,* p为指针变量p所指向的变量。

指针变量经过定义之后可以像其他基本类型变量一样引用。例如:

● 变量定义

    int i, x, y, *pi, *px, *py;

● 指针赋值

    pi=&i;   /* 将变量i的地址赋给指针变量pi,使pi指向i */
    px=&x;   /* px指向x */
    py=&y;   /* py指向y */

● 指针变量引用

    *pi=0;   /* 等价于i=0; */
    *pi+=1;  /* 等价于i+=1; */
    (*pi)++; /* 等价于i++; */

指向相同类型数据的指针之间可以相互赋值。例如:

px=py;

原来指针px指向x,py指向y,经上述赋值之后,px和py都指向y。

例4.11:输入两个整数x和y,经比较后按大小顺序输出。

#include<stdio.h>
main()  {
  int x, y;
  int *p, *p1, *p2;
  printf("Input x and y: \n");
  scanf("%d  %d", &x, &y);
  p1=&x; p2=&y;
  if(x<y)  {
    p=p1; p1=p2; p2=p;
  }
  printf("max=%d, min=%d, \n", *p1, *p2);
  while(1);
}

程序执行结果:

Input x and y:
  4  8  回车
max=8, min=4

这个程序中定义了三个指针变量*p、*p1和*p2,它们都指向整型变量。经过赋值之后,p1指向x,p2指向y。然后比较变量x和y的大小,若x<y,则将p1和p2交换,使p1指向y,p2指向x;若x>y则不交换。最后的结果必然使指针p1指向较大的数,p2指向较小的数,按顺序输出*p1和*p2的值,就可得到正确的结果。值得注意的是在程序执行过程中,变量x和y的值并未交换,所交换的只是它们的指针。由于指针p、p1和p2都是指向int类型数据的指针,故可以相互赋值,实现指针p1和p2的交换。

4.4.4 指针变量作为函数的参数

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

例4.12:利用指针变量作为函数的参数实现两个元素的相互交换。

#include <stdio.h>
swap(int * pi, int * pj)  {
  int temp;
  temp = * pi;
  * pi = * pj;
  * pj = temp;
}
main()  {
  int a,b;
  int *pa, *pb;
  printf("Please input a and b: \n");
  scanf("%d  %d", &a, &b);
  pa = &a;
  pb = &b;
  if(a<b)  swap(pa, pb);
  printf("\n max=%d, min=%d \n", a, b);
  while(1);
}

程序执行结果:

    Please input a and b:
    1234  5678  回车
    max=5678, min=1234

程序中自定义函数swap()的功能是交换两个变量a和b的值,swap()函数的两个形式参数p1和p2是指针变量。程序开始执行时,先输入a和b的值,然后将a和b的地址分别赋给指针变量pa和pb,使pa指向a,pb指向b。接着执行if语句,如果a<b则调用swap()函数。

注意函数调用时的实参pa和pb也是指针变量,在调用开始时,实参变量将它的值传递给形参变量,采取的仍然是“值传递”方式,但这时传递的是指针的值(即地址)。参数传递完成后,形参pi的值为&a,pj的值为&b,即指针变量*pi和*pa都指向了变量a,*pj和*pb都指向了变量b。接着执行swap()函数体,使*pi和*pj的值互换,从而实现了a和b的值的互换。函数返回时,虽然pi和pj被释放而不再存在,但main()函数中a和b的值已经被交换,因此最后能输出正确的结果。

由此可见以指针变量作为函数的参数,被调用函数在执行过程中使指针变量所指向的变量值发生变化,函数调用结束后,这些变量值的变化将被保留下来,从而可以实现“在被调用函数中改变变量的值,在主调用函数中使用这些被改变了的值”。

如果希望通过函数调用得到n个要改变的值,可以在主调用函数中设置n个变量,再用n个指针变量来指向它们,然后将指针变量作为实参将这n个变量的地址传递给被调用函数中的形参,通过形参指针变量来改变这n个变量的值,被改变了值将被保留下来,最后在主调用函数中就可以使用这些被改变了的值。

4.5 数组的指针

4.5.1 用指针引用数组元素

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

int  a[10];

数组a中各个元素分别为a[0],a[1],…,a[9]。数组名a表示元素a[0]的地址,而*a则表示a所代表的地址中的内容,即a[0]。

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

int *pa;
pa=&a[0];

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

例4.13:使用指针与数组的例子——计算质数。

#include <stdio.h>
#define  MAX  20
main()  {
  int  i, j, n, p, r, primes[MAX];
  int  *pntw, *pntr;
  long q;
  pntw = primes;
  n = 2;
  *pntw++ = 2; *pntw++ = 3;
  i = 5;
  do  {
    pntr = primes;
    do  {
        p = *pntr++;
        q = i / p;
        r = i - q * p;
      } while( r && i < q*q );
      if( r ) {
        *pntw++ = i;
        n++;
      }
      i += 2;
    } while( n < MAX );
    j = 0;
    pntr = primes;
    for( i=0; i<MAX; ++i )  {
      printf("%4d",*pntr++);
      if( ++j == 10 ) {
         printf("\n");
         j = 0;
       }
    }
    while(1);
  }

程序执行结果:

    2   3   5   7   11  13  17  19  23  29
    31  37  41  43  47  53  59  61  67  71

4.5.2 字符数组指针

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

例4.14:利用指针将一个字符数组中的字符串复制到另一个字符数组中去。

#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);
  while(1);
}

程序执行结果:

    How are you ?
  How are you ?

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

char code str[]={"How are you ? "};

是将字符串“How are you ?”置到数组str[]中作为初值,而语句:

s1=str;

则是将数组str[]的首地址,即指向数组str的指针赋值给指针变量s1。如果对数组名进行如下的操作:

str=s1;
str++;

则都是错误的。

4.5.3 指针的地址计算

指针的地址计算包括以下几个方面的内容。

(1)赋初值

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

int a[10], b[10];
float fbuf[100];
char *cptr1=NULL;
char *cptr2=&ch;
int *iptr1=&a[5];
int *iptr2=b;
float *fptr1=fbuf;

(2)指针与整数的加减

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

(3)指针与指针相减

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

(4)指针与指针的比较

指向同一类型数据的两个指针可以进行比较运算,从而获得两指针所指地址的大小关系。

此外,在计算指针地址的同时,还可进行间接取值运算。不过在这种情况下,间接取值的地址应该是地址计算后的结果,并且还必须注意运算符的优先级和结合规则。设p1、p2都是指针,对于

a=*p1++;

由于运算符*和++具有相同的优先级而指针运算具有右结合性,按右结合规则,有++、*的运算次序,而运算符++在p1的后面,因此上述赋值运算的过程是首先将指针p1所指的内容赋值给变量a,然后p1再指向下一个数据,表明是地址增加而不是内容增加。对于

a=*--p1;

与上例相同,按右结合规则有--、*的运算次序,而运算符--在p1的前面,因此首先将p1减去1,即指向前面一个数据,然后再把p1此时所指的内容赋值给变量a。对于

a=(*p2)++;

由于使用括号()使结合次序变为*、++,因此首先将p2所指的内容赋值给变量a,然后再把p2所指的内容加1,表明是内容增加而不是地址增加。

例4.15:指针运算的例子。

#include <stdio.h>
main()  {
    char data *p1, *p2, *p3, *px;
    char x[]={1,2,3,4};
    char px1, px2, px3;
    px=p1=p2=p3=x;
    px1=*p1++;
    p2=p1+2;
    px2=*--p2;
    px3=(*p3)++;
    printf("The start address of ARRAY x[]is %P\n",px);
    printf("px1 = %bd, p1 = %P\n",px1, p1);
    printf("px2 = %bd, p2 = %P\n",px2, p2);
    printf("px3 = %bd, p3 = %P\n",px3, p3);
    while(1);
}

程序执行结果:

    The start address of ARRAY x[]is  D:002C
  px1=1, p1=D:002D
  px2=3, p2=D:002E
  px3=1, p3=D:002C

例4.16:两指针相减——计算字符串长度的函数。

#include <stdio.h>
main()  {
    char *s = "abcdef";
    int  strlen(char *s);
    printf("\n length of '%s' = %d\n",s,strlen(s));
    while(1);
}
int strlen(char *s)  {
    char *p = s;
    while( *p != '\0' ) p++;
    return( p - s );
}

程序执行结果:

  lenth of 'abcdef' = 6

需要指出的是,指针的运算是很有限的,它只能进行如上所述的四种运算操作,除此之外所有其他的指针运算都是非法的。特别强调一点,不允许对两个指针进行加、乘、除、移位或屏蔽运算,也不允许用float类型数据与指针作加减运算。

4.6 函数型指针

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

定义一个函数型指针的一般形式为:

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

其中,“标识符”就是所定义的函数型指针变量名。

“数据类型”说明了该指针所指向的函数返回值的类型。例如:

int (* func1)();

定义了一个函数型指针变量func1,它所指向的函数返回值为整型数据。函数型指针变量是专门用来存放函数入口地址的,在程序中把哪个函数的地址赋给它,它就指向那个函数。在程序中可以对一个函数型指针多次赋值,该指针可以先后指向不同的函数。

给函数型指针赋值的一般形式为:

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

如果有一个函数max(x,y),则可用如下的赋值语句将该函数的地址赋给函数型指针func1,使func1指向函数max:

func1=max;

引入了函数型指针之后,对于函数的调用可以采用两种方法。例如,程序中要求将函数max(x,y)的值赋给变量z,可采用如下方法:

z=max(x,y); 或z=(* func1)(x, y);

用这两种方法实现函数调用的结果是完全一样的。但需要注意的是,若采用函数型指针来调用函数,必须先对该函数指针进行赋值,使之指向所需调用的函数。

函数型指针通常用来将一个函数的地址作为参数传递到另一个函数中去。这种方法对于要调用某个非固定函数的场合特别适用。下面通过一个例子来说明函数型指针的这种应用。

例4.17:函数型指针作为函数的参数。

设置一个函数process(),每次调用它时完成不同的功能。输入两个整型数a和b,第一次调用时找出a和b中较大者,第二次调用时找出较小者,第三次调用时求出a与b的和。

#include <stdio.h>
int max(int x, int y)  {
  int z;
  if (x>y) z=x;
  else z=y;
  return(z);
}
int min(int x, int y)   {
  int z;
  if (x<y) z=x;
  else z=y;
  return(z);
}
int add(int x, int y)   {
  int z;
  z=x+y;
  return(z);
}
int process(int x, int y,int (*f)())  {
  int result;
  result=f();
  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);
  while(1);
}

程序执行结果:

    Please input a and b:
    1234  5678 回车
    max=5678
    min=1234
    sum=6912

本例中的三个函数max()、min()和add()分别用来实现求较大数、求较小数和求和的功能。在主函数main()中第一次调用process()函数时,除了将a和b作为实参将两个数传递给process()的形参x、y之外,还将函数名max作为实参将其入口地址传递给process()函数中的形参——指向函数的指针变量*f。这样一来,process()函数中的函数调用语句result=f();就相当于result=max(x, y);因此,执行process()函数即可求出a与b中的较大者。第二次调用process()函数时改用函数名min作为实参,第三次调用process()函数时改用函数名add作为实参,从而实现每次调用process()函数时完成不同的功能。

4.7 返回指针型数据的函数

上一节中介绍了函数型指针的概念,在使用过程中要注意函数型指针与返回指针型数据的函数的区别。在函数的调用过程结束时,被调用的函数可以带回一个整型数据、字符型数据、实型数据等,也可以带回一个指针型数据,即地址。这种返回指针型数据的函数又称为指针函数,它的一般定义形式为:

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

其中,数据类型说明了所定义的指针函数在返回时带回的指针所指向的数据类型。例如:

int * x(a, b);

定义了一个指针函数* x,调用它以后可以得到一个指向整型数据的指针,即地址。请读者注意,在指针函数* x的两侧没有括号(),这是与函数型指针完全不同的,也是容易搞混的,实际使用时一定要注意。

例4.18:指针函数应用。

内存中存有巡回检测3个通道的温度值(每个通道有4个点),要求用户在输入通道号以后,能立即输出该通道所有点的温度值。

#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.2,70.4,60.6}};
  float * search(float (* pointer)[4], int n);
  float * p;
  int i, m;
  printf("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));
  while(1);
}
float * search (float (* pointer)[4], int n)
 {
  float *pt;
  pt= * (pointer+n);
  return(pt);
 }

程序执行结果:

    Enter the number of chanal: 2   回车
    The temperature of chanal are:
    90.0  80.2  70.4  60.6

程序中将巡回检测得到的某个通道各点的温度值存放在一个二维数组T[3][4]中,通道号作为数组的行,各点温度值作为数组的列。在输入通道号时要注意,通道号是从0算起的。函数search被定义为指针型函数,它的形式参数pointer是指向包含4个元素的一维数组的指针变量。pointer+1指向二维数组T的第0行,而*(pointer+1)则指向第0行第0列元素。

pt是一个指针变量,它指向实型变量(而不是指向一维数组)。main()函数调用search()函数,将T数组的首地址传递给pointer(注意T是指向数组行的指针,而不是指向数组列元素的指针)。m是要查找的通道号。调用search()函数后,得到一个地址(指向第m个通道第0点温度值),并将这个地址赋给变量p。*(p+i)表示该通道第i点的温度值,从而可将该通道4个点的温度值输出来。读者可参考图4.4来加深对这个例子的理解。

图4.4 数组与指针的关系

4.8 指针数组与指针型指针

4.8.1 指针数组

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

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

例如:

int * x[2];         /* 指向整型数据的2个指针 */
char * sptr[5];     /* 指向字符型数据的5个指针 */

指针数组在使用之前往往需要先赋初值,方法与一般数组赋初值类似。使用指针数组最典型的场合是通过对字符数组赋初值而实现各维长度不一致的多维数组的定义,下面通过两个例子来进一步说明指针数组赋初值的问题。

例4.19:使用指针数组的例子。

#include<stdio.h>
main()  {
  int i;
  char code *season[4]=
    {
    "spring", "summer", "fall", "winter"
    };
    for(i=0; i<4; ++i)
        printf("\n%c---%s", *second[i], second[i]);
    while(1);
  }

程序执行结果:

    s---spring
    s---summer
    f---fall
    w---winter

这个例子中在code区定义了指向char型数据的4个指针,其初值分别为“spring”、“summer”、“fall”和“winter”。它们可用图4.5直观地表示。

图4.5 指针数组与所赋初值的关系

从图4.5中可以看到各个指针数组的长度可以不同,实际的存储空间分配情况如图4.6所示,它们是各行首尾相接的连续区域,这样可以更为有效地利用内存空间。如果不使用指针数组而采用二维字符数组,则该二维数组各列的长度必须相等,并且要等于最大一列的长度,这样会造成内存空间的浪费。

图4.6 指针数组的初值在内存中的存放格式

例4.20:指针数组的应用。

日期转换程序,输入年份和天数,输出这一天所在的月份及该月天数。

#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[]=
  {
    "illegal 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-=daytab[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);
  while(1);
 }

程序执行结果:

    Input year and yearday:
    1996, 60  回车
    February, 29

这个程序由三个函数组成:主函数main()、求月份名函数mname()和求月天数函数monthday()。在主函数main()中输入年份year和天数yearday,然后调用求月天数函数将该年的这一天转换为该年的某月某日。

求月天数函数monthday()中定义了一张每月的天数表。由于二月的天数因闰年和平年而不同,因此将天数表设计为二维数组,用变量leap来判别输入的年份是否为闰年,闰年leap为1,平年leap为0。for语句的作用是控制当输入天数大于每月的天数时,用输入的天数减去该月的天数,并使月数i+1,再进入下一个循环,直到天数不大于第i月的天数时,退出循环。此时的i值即为所求的月数,而天数则为该月的天数。例如,在程序执行中,输入1996,60,经过上述转换得到i=2,yd=29,即1996年的第60天为2月29日。

求月份名函数mname()的作用是将由函数monthday() 所求得的月数能用相应的月份名来表示。即要求对于给出的月份数n,能返回一个指向n月名字符串的指针。函数中定义了一个指针数组mn并给它赋了初值,mn中共有13个元素,它们都是指针,指向字符类型数据,其初值分别是字符串“illegal month”、“January”、…、“December”的首地址。函数monthday()调用mname(i)后,返回一个指向第i月的月名字符串指针mn[i],然后用格式“%s”输出,就能得到该指针所指向的月名字符串。

如果一个指针数组中的元素都是函数指针,则称之为函数指针数组。利用函数指针数组可以很方便地实现散转处理。在设计一个单片机应用系统时,键盘管理程序是整个系统监控程序的一个重要组成部分。为了实现各个不同按键的功能,通常是根据不同的键值进行散转。下面是一个键值散转处理的例子,根据从键盘输入不同的数字键进行不同的处理。

例4.21:键值散转处理。

#include <stdio.h>
void Key_0()   {
  /* "0" 键处理 */
}
void Key_1()   {
  /* "1" 键处理 */
}
void Key_2()   {
  /* "2" 键处理 */
}
/* k3, k4, ... 其他键处理 */
/* 函数指针数组定义 */
code void (code * KeyProcTab[])()=  {Key_0, Key_1, Key_2/*k2,...,k9 */ };
void main()  {
    unsigned char key,num;
    while(1){
      scanf ("%c", &key);         /* 等待按键 */
      num=key-0x30;               /* 计算键值 */
      (*KeyProcTab[num])();       /* 按键值散转 */
}
}

4.8.2 指针型指针

在掌握了指针数组的基础上,再来介绍一种指针型的指针,这种指针所指向的是另一个指针变量的地址,故有时也称之为多级指针。从前面的图4.5中可以看到,指针数组season中的每一个元素都是一个指针型数据。既然season是一个数组,那么它的每一个元素都有相应的地址,而数组名season则代表了该指针数组的首地址。因此我们可以定义一个指向指针数组中元素的指针变量,这就是指向指针型数据的指针变量,即指针型指针变量。

定义一个指针型指针变量的一般形式为:

数据类型  ** 标识符

其中,“标识符”就是所定义的指针型指针变量名,而“数据类型”则说明一个被指针型指针所指向的指针变量所指向的变量数据类型。

例4.22:指针型指针变量的应用。

#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);
  while(1);
 }

程序执行结果:

    10
    10
    10

本例程序中定义了一个指向整型数据的指针变量p,又定义了一个指向整型数据的指针型指针q。经过赋值之后,指针p指向整型变量x,而指针型指针q则指向指针变量p。换句话说,在q中存放的是p的地址,在p中存放的是x的地址,在x中存放的才是整型数据10。若要将这个整型数据的值输出,可以通过变量名x直接输出:

printf("%d\n:, x);

这种方法称为直接取值。也可以先通过指针变量p找到变量x的地址,然后再从这个地址中将整型数据的值输出:

printf("%d\n:, * p);

这种方法称为单重间接取值。还可以通过指针型指针变量q先找到指针p的地址,再通过指针p找到x的地址,最后从x的地址中将整型数据的值输出:

printf("%d\n:, ** q);

这种方法称为多重间接取值。从程序的执行结果可以看到,这三种方法得到的结果是完全一样的。由此可知,一个指针型指针是一种间接取值的形式,而且这种间接取值的方式还可以进一步延伸,故可以将这种多重间接取值的形式看成为一个指针链。

指针型指针常用来作为指向指针数组的指针变量。例如:

int * x[5];
int ** y;

其中,第一条语句定义了一个指向整型数据的指针数组x[5],它由5个元素组成,各个元素x[0],x[1],…,x[4]均为指针变量,它们都指向整型数据;第二条语句定义了一个指针型指针y。如果经过如下赋值:

y=x;

则y就成了指向指针数组x的多级指针了。

例4.23:使用指针型指针的例子。

#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);
    }
    while(1);
 }

程序执行结果:

    s---spring
    s---summer
    f---fall
    w---winter

这个程序的执行结果与例4.19程序是完全一样的。在这个程序中先定义了一个指针数组season,并给它赋以字符串初值。在指针数组season中有4个元素,它们分别指向4个字符串的首地址。程序中还定义了一个指针型指针j,并进行了赋值:

j=season+i;

当i为0时,*j为season[0]的值,即第一个字符串的首地址,用字符串格式“%s”输出*j就可以得到第一个字符串,以后循环i为1、2、3时,j分别为j[1]、j[2]和j[3],用“%s”输出即可依次得到各个字符串。

4.8.3 抽象型指针

ANSI新标准增加了一种“void *”指针类型,这是一种抽象型指针,即可以定义一个指针变量,但不指定该指针是指向哪一种类型数据的。对于这种抽象型指针在给另一个指针变量赋值时,需要进行强制类型转换,使之适合于被赋值的变量的类型。例如:

char *p1;
void *p2;
p1=(char *)p2;

当然也可以用(void *)p1将p1的地址转换成void *类型之后赋值给p2:

p2=(void *)p1;

函数也可以定义为void *类型,例如:

void * fun(x, y)

表示函数fun返回的是一个地址,它指向“空类型”。

抽象型指针可以用来在每个存储区内访问任意绝对地址,或者用来产生绝对调用。下面是一个使用Keil Cx51编译器对抽象型指针形成汇编码的例子,读者可以从这个例子中进一步了解如何正确使用这种抽象类型的指针。

例4.24:抽象指针的使用。

stmt level   source
  1         char xdata * px;
  2          char idata * pi;
  3          char code * pc;
  4        char c;
  5        int i;
  6     void main(void){
  7   1   pc=(void *) main;
  8   1   pi=(char idata *)&i;
  9   1   pi=(char idata *)px;
  10   1   pc=(char code *)0x7788;
  11   1
  12   1   i=((int (code *)(void))0xff00)(); /* LCALL 0FF00H */
  13   1   c=*((char code *)0x8000);         /* char [code[0x8000]]*/
  14   1   c+=*((char xdata *)0xff00);       /* char [xdata[0xFF00]]*/
  15   1   c+=*((char idata *)240);          /* char [idata[0xF0]]*/
  16   1   c+=*((char pdata *)240);          /* char [pdata[0xF0]]*/
  17   1
  18   1   i=*((int code *)0x1200);          /* int from code[0x1200]*/
  19   1   px=*((char xdata *xdata *)0x4000);/* x-ptr from xdata[0x4000]*/
  20   1   px=((char xdata *xdata *)0x4000)[0];/* same as before */
  21   1   px=((char xdata*xdata*)0x4000)[1];/*x-ptr from xdata[0x4002]*/
  22   1   }
  23
  24
ASSEMBLY LISTING OF GENERATED OBJECT CODE
            ;FUNCTION main (BEGIN)
                                      ;SOURCE LINE # 6
                                      ;SOURCE LINE # 7
0000750000     R    MOV    pc,#HIGH main
0003750000     R    MOV    pc+01H,#LOW main
                                      ;SOURCE LINE # 8
0006750000     R    MOV    pi,#LOW i
                                      ;SOURCE LINE # 9
0009850000     R    MOV    pi,px+01H
                                      ;SOURCE LINE # 10
000C 750077    R    MOV    pc,#077H
000F 750088    R    MOV    pc+01H,#088H
                                      ;SOURCE LINE # 12
0012 12FF00          LCALL   0FF00H
0015 8E00      R    MOV    i,R6
0017 8F00      R    MOV    i+01H,R7
                                      ;SOURCE LINE # 13
0019908000          MOV    DPTR,#08000H
001C E4              CLR    A
001D 93              MOVC   A,@A+DPTR
001E F500      R    MOV    c,A
                                      ;SOURCE LINE # 14
0020 90FF00          MOV    DPTR,#0FF00H
0023 E0              MOVX   A,@DPTR
00242500       R    ADD    A,c
0026 F500      R    MOV    c,A
                                      ;SOURCE LINE # 15
0028 78F0            MOV    R0,#0F0H
002A E6              MOV    A,@R0
002B 2500      R    ADD    A,c
002D F500      R    MOV    c,A
                                      ;SOURCE LINE # 16
002F E2              MOVX   A,@R0
00302500       R    ADD    A,c
0032 F500      R    MOV    c,A
                                      ;SOURCE LINE # 18
0034901200          MOV    DPTR,#01200H
0037 E4              CLR    A
0038 93              MOVC   A,@A+DPTR
0039 F500      R    MOV    i,A
003B 7401            MOV    A,#01H
003D 93              MOVC   A,@A+DPTR
003E F500      R    MOV    i+01H,A
                                      ;SOURCE LINE # 19
0040904000          MOV    DPTR,#04000H
0043 E0              MOVX   A,@DPTR
0044 FE              MOV    R6,A
0045 A3              INC    DPTR
0046 E0              MOVX   A,@DPTR
0047 8E00      R    MOV    px,R6
0049 F500      R    MOV    px+01H,A
                                      ;SOURCE LINE # 20
004B 8E00      R    MOV    px,R6
004D F500      R    MOV    px+01H,A
                                      ;SOURCE LINE # 21
004F A3              INC    DPTR
0050 E0              MOVX   A,@DPTR
0051 FE              MOV    R6,A
0052 A3              INC    DPTR
0053 E0              MOVX   A,@DPTR
0054 8E00      R    MOV    px,R6
0056 F500      R    MOV    px+01H,A
                                      ;SOURCE LINE # 22
0058 22              RET
            ;FUNCTION main (END)