当前位置:网站首页>>电脑技术>>C、C++语言>>我的著作 双击自动滚屏
C语言速成(第五章 指针)

发表日期:2006年1月1日      已经有4597位读者读过此文

第五章  指针

    指针是一种不同于基本类型和构造类型的特殊数据类型,它在C语言里被大量使用,许多其他数据类型难以实现的复杂对象的操作都可以通过指针来完成。使用指针具有结构紧凑、实现速度快等优点。指针及其灵活方便的使用是C语言区别于其他语言的最有特色的内容之一。

第一节  指针与地址

    指针本身不是变量,不分配存储单元,它表示所指向对象在内存中的地址。换句话说,一个变量在内存空间中占用的地址称为该变量的指针。在计算机里,存储单元的地址就象街道上住宅的门牌号码一样,但不同的是连续的存储单元有连续的地址。计算机内存中每一个存储单元的地址是唯一的。在学习指针时,要特别注意区分存储单元的地址和存储单元中的内容这两个不同的概念。
    如果把对象的地址保持在一个变量中,那么,这个变量被称为指针变量。指针变量作为一种变量,是占用存储单元的。它保存数据的地址,而不是数据本身。在不会发生混淆的场合下,通常不加区分地把指针变量简称为指针。实际上应该把指针变量理解为类型为指针的变量。
    例如有六个字符型变量:

        char a=''''''''''''''''W'''''''''''''''';        /* 定义字符型变量 a 并赋初值 ''''''''''''''''W'''''''''''''''' */
        char b=''''''''''''''''O'''''''''''''''';        /* 定义字符型变量 b 并赋初值 ''''''''''''''''O'''''''''''''''' */
        char c=''''''''''''''''R'''''''''''''''';        /* 定义字符型变量 c 并赋初值 ''''''''''''''''R'''''''''''''''' */
        char d=''''''''''''''''L'''''''''''''''';        /* 定义字符型变量 d 并赋初值 ''''''''''''''''L'''''''''''''''' */
        char e=''''''''''''''''D'''''''''''''''';        /* 定义字符型变量 e 并赋初值 ''''''''''''''''D'''''''''''''''' */
        char f=''''''''''''''''\0'''''''''''''''';       /* 定义字符型变量 f 并赋初值 ''''''''''''''''\0'''''''''''''''' */

    我们假定这六个变量的值87、79、82、76、68、0已经分别放在计算机内存的第414、415、416、417、418、419存储单元中,那么可以用C语言中的取地址运算符“&”得到每个字符型变量的存储地址。若把这些取出来的地址分别放到六个变量中:

        pa=&a;             /* 取变量 a 的地址放入变量 pa 中 */
        pb=&b;             /* 取变量 b 的地址放入变量 pb 中 */
        pc=&c;             /* 取变量 c 的地址放入变量 pc 中 */
        pd=&d;             /* 取变量 d 的地址放入变量 pd 中 */
        pe=&e;             /* 取变量 e 的地址放入变量 pe 中 */
        pf=&f;             /* 取变量 f 的地址放入变量 pf 中 */

    这些变量就是指针变量,pa 是指向字符型变量 a 的指针,pb 是指向字符型变量 b 的指针……,它们的值分别为414─419。必须注意,单目的取地址运算符“&”只能放在变量前,而不能用它来取一个常量的地址。表达式 pa=&87; 是错误的。反之,如果已知一个合法的指针变量,则可通过C语言中的取值运算符“*”来访问该地址,取得对象的值,即这个存储单元的内容。如 *pa 这个表达式里的指针变量 pa 代表着一个对象的地址,而通过取值运算符“*”把放在这个地址中的值取出来。因此 *pa 的值为字符 W 的 ASCII 码值87。
    在使用取地址和取内容的单目运算符“&”和“*”时, 要注意与代表位操作逻辑与的“&”及四则运算的乘号“*”区分开来。
    假如把指针变量 pa、pb、pc、pd、pe、pf 代表的地址看作一栋大楼里的一个个房间,那么字符型变量 a、b、c、d、e、f 所代表的字符 W、O、R、L、D 和空字符 ''''''''''''''''\0'''''''''''''''' 就好比和房间相对应的住户。我们可以用“&”通过住户得到房间号码,也可以用“*”根据房间找到住户。
    指针所指向的对象可以是各种类型的变量,可以是 int、char、float 等基本类型变量,也可以是数组等构造类型的变量,因此,指针使用前必须进行定义。要使上述含有指针变量的语句能正确地执行,必须先对指针变量作如下定义:

        char *pa,*pb,*pc,*pd,*pe,*pf;       /* 定义指针变量 */

    在上面的表达式里,* 表示所定义的变量是指针变量,类型符 char 表示这些指针变量指向的对象是字符。同样地,可以用  int *k;  定义一个指向整数的指针变量 k,或者用 float *s;  定义一个指向浮点数的指针变量 s。注意,这指的是 s 指向的对象是浮点数,而不是 s 本身是浮点数。
    在C语言里不允许存在指向未确定类型对象的指针,但在 ANSI 标准的C语言版本里取消了这一限制,提供了 void 修饰符,凡定义为 void * 的指针指向对象的类型不定,只是在赋值时才转换成对象的类型。指向不同类型对象的指针不能混用,需要把定义为一种类型的指针变量指向另一种类型的对象时,必须通过强制转换,如:

        int a[6];                /* 定义整型数组 a */
        char *p;                 /* 定义指针变量 p */
        p=(char *)a;             /* 把 a 的值强制转换成字符型再赋给 p */

    要注意,在指针变量中只能存放地址值,不要试图把一个非地址值的数据赋给一个指针变量。如果必须把一个常量作为地址值赋给一个指针变量时,也必须通过强制转换,如:

        int *p;                  /* 定义一个指针变量 p */
        p=(int *)1000;           /* 把常量强制转换成整型指针类型赋给 p */

    指针变量定义时,也可以给出各种存储类,例如:

        register int *k;         /* 定义一个整型指针类型的寄存器变量 */

表示 k 将保持一个整形变量的地址,而这个地址用一个寄存器来存放。综上所述,可以将定义一个指针变量的一般形式归纳为:

        <存储类型> <数据类型>  * <指针变量名> ;


第二节 指针与数组的关系

    指针和数组间有极其密切的关系,C语言把数组名看成是指向数组第0个元素的常量指针,它保存的是数组的起始地址。例如,定义一个数组 k:

        int k[10];

又定义一个指向整数的指针:

        int *pk;

把数组第0个元素的地址赋给 pk:

        pk=&k[0];

则 pk 包含的就是数组 k 的首地址,*pk 为数组第0个元素 k[0] 的内容。由于数组名 k 被初始成指向数组起始项 k[0] 的指针,因此,上式又可写成:

        pk=k;

    如果改变 pk 所包含的地址,就可以使之指向数组的其他元素,这样就能通过指针的移动在不同的存储单元里进行赋值、检索等信息处理了。 如 pk+1 指向数组项 k[1],这时,pk+1 包含的是 k[1] 的地址,而 *(pk+1) 则为 k[1] 的内容。同样,数组第七个元素 k[7]的值可以表示为 *(pk+7)。在对数组第 i 个元素进行存取时,用数组的下标表达式 k[i] 和用指针位移量表示方法 *(pk+i) 是等价的。
    在第6.1节中,存放在计算机内存六个连续存储单元中的六个字符型变量 ''''''''''''''''W''''''''''''''''、''''''''''''''''O''''''''''''''''、''''''''''''''''R''''''''''''''''、''''''''''''''''L''''''''''''''''、''''''''''''''''D''''''''''''''''、''''''''''''''''\0'''''''''''''''' 可以用一个数组来表达:

        char a[]="WORLD";

这在第五章里已经阐明了,同样地,可用一个指针变量来表示指向这个数组的指针:

        char *pa;
        pa=a;

    这时候指针 pa 指向了数组 a 的第一个元素 W,*pa 的值为字符 W 的 ASCII 码87;*(pa+1) 的值为字符 O 的 ASCII 码79;  而 *(pa+2) 的值为字符 R 的 ASCII码73;……。
    在上述变换中,pa 本身的值并没有发生变化,它仍是指向数组第0个元素的地址值,仅仅是用 pa+1、pa+2、......来表示数组第一、第二、......个元素的地址。必须搞清楚,p+1 是表示 p+1 指向数组的下一个元素,决不能简单地理解成把 p 包含的地址值加1。对于指向不同类型数据的指针,其值加1所移动的字节数是不同的,这我们将在第四节中作进一步的介绍。
    还有一种地址的变换方法是使 pa 的值发生变化,也就是使指针“移动”,指向数组的其他元素。其方法是使 pa 加或减一个整数,使 pa 包含另一个地址,从而使 pa 指向的元素发生改变。例如,要让 pa 指向数组的下一个元素,可用:

        pa+=1;   或   pa++;

    这时 pa 包含的将不再是 a[0] 的地址,而是 a[1] 的地址了。同样地,*pa 的值也将不再是字符 W 的 ASCII 码87,而是 O 的 ASCII 码79了。当指针指向第1个元素时,也可以用 pa-=1; 或 pa--; 将指针移回指向数组的第0个元素,这时,*pa 的值又变成了87。当然,在移动指针时,必须十分注意不要把指针移到已定义的数组允许的范围之外。如上例中,当指针已经指向数组第0个元素时再进行 pa-- 或已指向最后一个字符时,再进行 pa++ 运算,就可能出现难以预料的错误。
    由于指针和数组之间存在这样密切的关系,我们在用C语言编程时,往往可以交替使用数组表示法和指针表示法。例如当我们定义了一个数组:

        char a[]="WORLD";
后,可以用 a[4] 或 *(a+4) 来表示数组第4个元素的内容。
    在函数参数传递时,如果把一个数组作为参数传递给一个函数,实质上传递的是存放该数组起始地址的指针,可以将函数形参定义成数组,也可以把它定义成指针。

    例6.1 把例5.4中的 copy 子函数改写成指针形式。

    #define SIZE 50                 /* 通过宏替换命令用 SIZE 代替常量50 */
    main()                          /* 主函数 */
    {
      char s1[SIZE],s2[SIZE];       /* 定义两个字符型数组 */
      printf("请输入一个字符串:");  /* 屏幕打印提示字符串 */
      scanf("%s",s1);               /* 键盘输入字符串放入 s1 */
      copy(s1,s2);                  /* 调用函数 copy 并传递参数 s1、s2 */
      printf("复制的字符串是:%s\n",s2); /* 屏幕打印字符串 s2 */
    }
    copy(char *a,char *b)           /* 定义函数 copy,并说明形参 */
    {
      while((*b++=*a++)!=''''''''''''''''\0'''''''''''''''');     /* 为复制字符串建的循环 */
    }

    在 *b++ 和 *a++ 中由于增量运算符“++”放在指针变量名 b 和 a 的后面,所以,表达式 *b++=*a++ 表示取出 a 指向元素的内容,赋给 b 指向的元素,然后把 b 和 a 指针均后移指向下一个元素,直到把 a 串尾结束符 ''''''''''''''''\0'''''''''''''''' 赋给 b 相应的元素,while 循环条件不再成立而退出循环,它等同于:

        while(1) {
          *b=*a;                  /* 把 a 一个元素的值赋给 b 的相应元素 */
          if(*b==''''''''''''''''\0'''''''''''''''')  break;    /* 如果复制的字符是空字符,退出循环 */
          b++;                    /* b 的指针后移一个字符位置 */
          a++;                    /* a 的指针后移一个字符位置 */
        }

    必须指出,虽然指针可以代替数组,但指针和数组之间是有区别的。指针是可以移动的,而数组名是一个指向数组第0个元素的常量指针,也就是说,它只是一个地址表达式,它所存放的地址是固定的,不能移向其他的对象。所以例6.1中的 s1、s2 不能改变其值,下列表达式都是不合法的:

        s1++;                     /* 试图移动一个数组名表示的常量指针 */
        s1+=2;                    /* 试图移动一个数组名表示的常量指针 */
        s1=s2;                    /* 试图改变一个数组的存放地址 */

    指针的应用大大增强了C语言的程序处理功能,有程序简洁、生成的代码紧凑、运行效率高等优点。但指针在很多情况下会使程序的可读性变差,不如数组表示方法那样直观、易读,而且,指针使用不当,可能造成一些难以检查的错误。因此,什么情况下使用指针表示方式,什么情况下使用数组下标表示方式要视实际情况而定。

第三节  指针的初始化

    表达式 int *p; 定义了一个指针变量,但它并没有指向一个确定的对象,在指针变量 p 中也没有包含确定的地址值,只能说变量 p 即将保持一个整数的地址,但是具体保持哪一个整数的地址尚未确定,可以称它为空指针。这样的指针变量在使用前必须进行初始化,通过初始化使它包含一个确定的地址。对于一个已经定义过的指针变量,可以赋初值,即把一个表示地址的整数或一个前面已经定义过的变量的地址作为指针变量的初值。也可以通过内存动态分配函数把一片存储区的起始地址赋给指针变量,这在本章第五节里将要讲到。指针的初始化主要是指对指针变量的初始化,而不是对指针指向对象的初始化,例如:

        static int b,*a=&b;           /* 把静态变量 b 的地址赋给指针变量 a */
        static int m[20],*n=m;        /* 把数组 m 的起始地址赋给指针变量 n */
        char *s="WORLD",*p=s;         /* 把字符串常量的首地址赋给 s,并赋给 p */

    在上面第一行表达式里,指针定义时带的初始化项表示把整形变量 b 的地址赋给 a,a 指向 b,它等价于下面两个语句:

        int b,*a;
        a=&b;

    同样,在第二行表达式里,在定义指针变量 n 的同时,也将整形数组第一个元素 m[0] 的地址赋给了它。
    字符指针的初始化可以用字符串直接量来完成,这和字符型数组的赋值方式很相象。在程序中字符串直接量占有一块固定的存储区域,这一区域是静态的。当字符串直接量出现在一个表达式里时,它被转换成字符指针,所以在第三行表达式里,s 指向字符串 "WORLD" 的第一个字符''''''''''''''''W'''''''''''''''',使得 s 包含''''''''''''''''W''''''''''''''''的存储地址,也就把字符串常量 WORLD 的首地址赋给了指针变量 s 。在定义指针变量 p 的同时,也对其进行了初始化,把 s 包含的地址值又赋给了 p ,使指针 p 也指向了字符''''''''''''''''W''''''''''''''''。但必须注意,除字符串指针外的其他指针变量的初始化则不能套用数组初始化的方法,因为指针初始化需要的是一个地址值。如下面的表达式就是不合法的:

        int *a={0,1,2,3,4,5};

    对于没有初始化的指针变量,在程序里不能用“*”运算符对其进行间接访问。对于已经初始化了的指针变量所指向的对象可以进行赋值和运算,如同对一个已定义的变量进行赋值一样,如:
 
        int *k,a;          /* 定义一个整型指针变量 k 和一个整形变量 a */
        k=&a;              /* 把变量 a 的地址赋给 k */
        *k=60;             /* 把常数60放入 k 指向的地址单元 */

假如把上面的程序改为:

        int *k;
        *k=60;

    程序编译时就会提示出错,因为不能对一个不确定的指针进行赋值。同样的道理,下面的程序也是错误的:

        char *string;
        scanf("%s",string);

第四节  指针变量的基本运算

    前面,我们已经接触到了一些指针变量的运算,指针变量的基本运算归纳起来有以下几种:

    一、取地址运算和取内容运算
    取地址运算符“&”和取内容运算符“*”在本章一开始就已经作了介绍。在C语言的实现过程中,对程序已定义的变量在计算机内存里的存储地址是由编译程序自动完成的。
    必须注意,单目运算符“&”只能放在变量前,不能用它来取一个常量或一个表达式的地址,也不能用它来取一个寄存器变量的地址,下列表述方式都是错误的:

        int p,pa,pb,a;
        register int b;
        p=&67;                      /* 试图取一个常量的地址 */
        pa=&(a+5);                  /* 试图取一个表达式的地址 */
        pb=&b;                      /* 试图取一个寄存器变量的地址 */

    二、指针变量值加或减一个整数
    在第二节里已经有了这方面的表述。指针变量与一个整数的加减是一种地址运算,其结果实际上是指针指向数组元素的改变。指针变量加1,则指针指向数组的下一个元素;指针变量减1,则指向数组的上一个元素。但由于应用C语言程序的计算机的不同和指针变量所指向的数组可以定义成不同类型的原因,指针所指向对象的长度不都相同,在将指针变量加、减一个整数的运算时,地址的实际变化值也就有所不同。变量加减1,地址究竟移动多少字节可由 sizeof(类型名) 计算出来。 如在一台16位计算机上,一个 char 类型变量占一个字节,指向它的指针变量加1,地址也加1;一个 int 变量占用2个字节,一个指向int类型对象的指针变量加1,地址值加2;一个指向 float 类型对象的指针加1,地址值加4;而一个指向 double 类型对象的指针加1,地址值则要加8。当然所有这些都不需要编程者操心,而由C语言的编译程序自动进行处理。在用C语言编程时,碰到要把指针移动一个元素位置,通常用单目运算符“++”和“--”,这样可以使程序更为简洁、紧凑。
    和指针变量相加或相减的数必需是整数,不允许将指针变量与 float 或 double类型的数相加减。对于C语言的编程员来说重要的是在编制程序时把指针的移动控制在许可的范围之内。

    三、两个指针变量相减
    两个指针变量相减只有在这两个指针指向同一个数组中元素的情况下方可进行。  假如 p1 和 p2 为指向同一数组中元素的指针,p1-p2 即为这两个指针指向元素之间元素的个数。除了指向同一数组元素的指针可以相减外,两个指针变量之间的加、乘、除运算和位操作都是非法的。下面就是一个指针相减的实例:

    例6.2  编制一个测量字符串长度的函数。

    strlen(char *s)                    /* 定义一个函数,形参说明为字符型指针 */
    {
      char *t;                         /* 定义一个指针变量 */
      t=s;                             /* 使指针 t 也指向字符串 s 的起始地址 */
      while(*t) t++;                   /* 移动指针 t 指向字符串尾 */
      return t-s;                      /* 返回字符串长度字节数 */
    }

    程序中 t=s; 把 s 的值赋给 t,使指针 t 也指向 s 指向的字符串首。然后通过一个循环移动指针 t,直到指向字符串尾的空字符''''''''''''''''\0'''''''''''''''',循环条件为假而退出循环。最后一句中的 t-s 即为两个指针相减,得到字符串的字节数,用 return 语句返回这个字节数。

    四、用关系运算符对两个指针变量进行比较
    C语言中的关系运算符“>”、“<”、“==”、“>=”、“<=”、“!=”都适用于对指针变量的比较。但相比较的两个指针变量必须指向同一数组中的元素,不允许对指向不同数组的元素的指针变量进行任何一种类型的比较。假定 p1、p2 是指向同一数组中元素的指针变量,p1 所指向的数组元素在 p2 指向的元素前时,p1<p2 为真,否则为假。当 p1、p2 指向数组的同一元素时,p1==p2 为真,否则为假。

第五节  内存动态分配

    能动态分配内存是指针的重要特点之一。在C语言的函数库中提供了一系列内存动态分配函数和释放已分配内存空间的函数,不同版本的C语言系统所提供的这类函数的函数名会有差异。最常用的有内存动态分配函数 malloc() 和释放内存函数 free()。在 Microsoft C 中它们被定义在包含文件 malloc.h 中 (Turbo C 中为 alloc.h),当我们使用指针来处理问题时,如果不出现数组下标的表达形式,就不必先定义一个数组,而只定义一个指针变量,然后用内存动态分配函数提供一片存储区域,并把这个存储区域的首地址放在该指针变量中。因为指针变量包含的地址确定了,所以在内存动态分配建立变量的过程中,指针变量也就初始化了。这时就能够在分配的有效的存储区域内移动指针并对存储单元进行赋值等信息处理了。这样用指针建立变量和分配内存空间而不出现数组名的方式在C语言编程时是用得非常多的。
    malloc(size) 请求分配连续的 size 个存储位置的区域,参数 size 为整形量。当请求得到满足时,函数 malloc 返回该存储区的首地址,如果请求不能满足,返回 NULL,其值为0,表示内存动态分配失败。必须明确,free 只能释放通过 malloc 等内存动态分配函数申请的空间,而不能用来释放变量空间。

    例6.3  定义一个字符型指针变量,给它分配6个字符位置的存储空间,并移动指针将 ''''''''''''''''W''''''''''''''''、''''''''''''''''O''''''''''''''''、''''''''''''''''R''''''''''''''''、''''''''''''''''L''''''''''''''''、''''''''''''''''D''''''''''''''''、''''''''''''''''\0'''''''''''''''' 填入这6个位置,在屏幕上打印这个字符串,最后释放已分配的内存空间。

    #include "malloc.h"              /* 通过预处理命令指定包含文件 */
    #define SIZE 6                   /* 通过宏替换命令用 SIZE 代替常数6 */
    main()                           /* 主函数 */
    {
      char *s,*p;                    /* 定义两个字符型指针变量 */
      s=malloc(SIZE);                /* 分配内存空间,并将存储区首地址赋给 s */
      p=s;                           /* 用指针 p 保存字符串的首地址 */
      *s++=''''''''''''''''W'''''''''''''''';                      /* 把''''''''''''''''W''''''''''''''''放入 s 处,指针后移一字符位置 */
      *s++=''''''''''''''''O'''''''''''''''';                      /* 把''''''''''''''''O''''''''''''''''放入 s 处,指针后移一字符位置 */
      *s++=''''''''''''''''R'''''''''''''''';                      /* 把''''''''''''''''R''''''''''''''''放入 s 处,指针后移一字符位置 */
      *s++=''''''''''''''''L'''''''''''''''';                      /* 把''''''''''''''''L''''''''''''''''放入 s 处,指针后移一字符位置 */
      *s++=''''''''''''''''D'''''''''''''''';                      /* 把''''''''''''''''D''''''''''''''''放入 s 处,指针后移一字符位置 */
      *s=''''''''''''''''\0'''''''''''''''';                       /* 把''''''''''''''''\0''''''''''''''''放入 s 处 */
      s=p;                           /* 指针 s 恢复指向字符串首 */
      printf("%s",s);                /* 在屏幕上打印字符串 */
      free(s);                       /* 释放 malloc 分配的存储空间 */
    }

    在上面的例子里,表达式 p=s 使指针变量 p 指向了 s 的起始地址,实际上是保存了字符串 s 的首地址。当把指定的字符赋给 s 已分配存储区各地址单元对应的元素后,由于 s 的指针已经由首地址移动到了存储区最后一个字符位置,因此在用 printf 打印 s 和用 free 函数释放内存空间前,必须将指针恢复到它原来的起始地址,这是通过表达式 s=p 来完成的。如果去掉这句表达式,printf 将什么也打印不出来,free 实际上也没有把前面分配的内存空间释放掉。因为 free 在释放占用的内存空间时,参数 s 必须指向要释放空间的起始地址,这一点必须经常注意到。

第六节  指针数组和多级指针

    元素都是指针变量的数组称为指针数组。下面的表达式定义的就是一个指针数组:

        char *p[10];

    这个指针数组被定义成有10个元素,而每一个元素又是一个指向字符串的指针。在上一章第三节中我们讲到过二维数组,指针数组通常可以用在使用二维数组的场合。定义一个指针数组的一般形式为:

        <存储类型> <数据类型>  * <指针数组名> [数组长度];

    例6.4  建立一个由6个元素为字符型指针变量的指针数组,从键盘输入6个字符串分别放入这个指针数组的成员中,最后在屏幕上打印这6个字符串,并在每个字符串前加上数组行号。

    #include "malloc.h"                /* 用预处理命令指定一个包含文件 */
    #define SIZE 6                     /* 通过宏替换命令用 SIZE 代替常数6 */
    #define LINE 30                    /* 通过宏替换命令用 LINE 代替常数30 */
    main()                             /* 主函数 */
    {
      char *a[SIZE];                   /* 定义一个指针数组 a */
      int i;                           /* 定义一个整形变量 i */
      for(i=0;i<SIZE;i++) {            /* 为分别输入六个字符串建的循环 */
        a[i]=malloc(LINE);             /* 给数组元素分配存储空间 */
        scanf("%s",a[i]);              /* 键盘输入一字符串放入一个数组元素中 */
      }
      for(i=0;i<SIZE;i++)              /* 为屏幕输出字符串建的循环 */
         printf("%d  %s\n",i,a[i]);    /* 屏幕打印数组行号和字符串 */
    }

    上例中,在第一个 for 循环里,给指针数组每一个成员分配一个存储区域,并在键盘输入一个字符串给这个成员。在第二个 for 循环里,用 printf 依次打印数组行号和字符串。
    指针数组在使用前也需要赋初值,下面是一个字符型指针数组初始化的例子:

        char *city[]={                 /* 定义一个字符型指针数组,并赋初值 */
                "Beijing",
                "Shanghai",
                "Hefei"
             };

    指针数组与多维数组密切相关,但两者又是有区别的。例如一个二维数组每各行的长度是一样的,如果要用一个二维数组容纳上面的三个字符串,就必须定义一个有3行每行至少9个字符元素的二维数组:

        char city[3][9];

    在这个数组中,city[2][8] 是合法的元素,由于在数组里不是每个字符串都有9个字符长,因而浪费了不少空间。但是,在用指针数组形式定义时,数组中各行的长度可以是不同的,在上例中指针数组的第0行长度为8个字符,第1行长度为9个字符,第2行只有6个字符,在程序里如果出现 city[2][8] 就成为非法的了。这就是说,指针数组可以是一个不规整的数组。在处理一些比较大又不规整的数组时,用指针数组可以做到比多维数组节省内存空间,而且它访问元素的速度相对也要快些。
    在用C语言编程时,经常碰到用指针数组作为 main 函数形参的情况,它的实参又叫做命令行参数,是在程序执行时由操作者输入执行文件名后依次输入的。输入命令行参数的一般格式为:

        <命令名>  <参数1>  <参数2> ...... <参数N>

    命令名及各参数间用空格或制表符分开。在从键盘输入命令行参数的情况下,main 函数一般可带有两个形参,这两个形参习惯上用 argc、argv 表示,如:

        main(argc,argv)                /* 主函数 */
        int argc;                      /* 形参说明 */
        char *argv[];                  /* 形参说明为一个指针数组 */
        {
           ........
        }

    整形变量 argc 的值是包括命令名在内从命令行输入参数的个数;指针数组 argv 保存命令名和命令行参数,argv[0] 指向命令名,argv[1] 指向第一个命令行参数,argv[2] 指向第二个命令行参数……。所有命令行参数均为字符串,只有最后一个为 NULL。由于 MS -DOS 的限制,包括程序名在内的所有命令行参数的总长度应小于或等于128个字符。命令行参数在操作系统和执行程序的命令之间建立了一种联系方式,使得该命令执行时可以按不同的命令行参数选择完成不同的功能。
    除了 argc 和 argv 外,一些C语言版本还允许 main 函数带有第三个形参,习惯上取名为 envp,它是一个指向建立程序运行环境的字符串数值表的字符串指针数组。envp 参数的使用是对 ANSI 标准的一个扩充,以支持从 XENIX 或 UNIX 系统移殖的代码。

    例 6.5  打印从键盘输入的执行文件名、命令行参数和环境参数。

    main(int argc,char *argv[],char *envp[])   /* 主函数 */
    {
       int i;                             /* 定义一个整形变量 */
       printf("命令行参数为:\n");         /* 打印一个字符串 */
       for(i=0;i<argc;i++) {              /* 为打印命令行参数设的循环 */
          printf("%s\n",argv[i]);         /* 打印一个命令行参数字符串 */
       }
       printf("环境字符串为:\n");         /* 打印一个字符串 */
       for(i=0;*envp[i];i++) {            /* 为打印环境参数设的循环 */
          printf("%s\n",envp[i]);         /* 打印一个环境参数字符串 */
       }
    }

    如果把这个程序命名为 DEMO.C,编译连接后生成可执行文件 DEMO.EXE。如果这个文件在磁盘子目录 TC 中,在自动批处理文件 AUTOEXEC.BAT 里设置了环境参数:

    PATH=C:\DOS

    运行这个文件时在键盘输入命令行参数 AAA 和 BBB,运行后将得到如下结果:

    C:>DEMO AAA BBB
    命令行参数为:
    C:\TC\DEMO.EXE
    AAA
    BBB
    环境字符串为:
    COMSPEC=C:\COMMAND.COM
    PATH=C:\DOS

    指针可以指向任何类型的对象,包括指向另外的指针。当指针指向的对象仍是指针时就构成了多级指针。在通常情况下,指针数组也可以用多级指针来代替。多级指针可使程序更为简洁,下面定义的就是一个多级指针:

        char **s;

在这个表达式里可以看到 s 的前面有两个“*”号,它相当于:

        char *(*s);

    例6.6  用多级指针改写例6.4。

    #include "malloc.h"                /* 用预处理命令指定一个包含文件 */
    #define SIZE 6                     /* 通过宏替换命令用 SIZE 代替常数6 */
    #define LINE 30                    /* 通过宏替换命令用 LINE 代替常数30 */
    main()                             /* 主函数 */
    {
      char **a;                        /* 定义一个二级指针变量 a */
      int i;                           /* 定义一个整形变量 i */
      a=malloc(SIZE*LINE*sizeof(char));/* 分配一块存储区,首地址赋给 a */
      for(i=0;i<SIZE;i++)              /* 为逐行输入字符串设的循环 */
         scanf("%s",*a++);             /* 键盘输入字符串 */
      a-=SIZE;                         /* 指针 a 恢复指向第一个字符串首地址 */
      for(i=0;i<SIZE;i++)              /* 为逐行打印字符串设的循环 */
         printf("%d  %s\n",i,*a++);    /* 逐行打印各字符串 */
    }     

    这里,多级指针 **a 代替了例6.4中的指针数组 *a[SIZE]。在例6.4里在定义指针数组时确定了共有6个元素,通过一个循环分别给每个元素分配内存空间。而在例6.6中则用表达式

        a=malloc(SIZE*LINE*sizeof(char));

给这个二级指针分配了容纳6个字符串的空间。在第一个 for 循环从键盘输入字符串后,最上层的指针 a 用表达式 a-=SIZE; 恢复到指向第一个字符串。第二个 for 循环依次打印各字符串,函数 printf 中的 *a 是包含字符串首地址的指针,而 **a 才代表字符串中的字符。在编程涉及多级指针运算时,一定要注意是那一层指针在变化。上例中最后的打印语句如果改成

        printf("%c\n",**a++);

便变成打印每一个字符串的第一个字符;如果改成
        printf("%c\n",*(*a)++);

则打印出来的是第一个字符串的前6个字符。
    同样,main 函数的指针数组形参也可以用一个二级指针来代替:

    例 6.7 用二级指针改写例6.5。

    main(int argc,char **argv,char **envp)   /* 主函数 */
    {
       register char **p;                    /* 设置一个二级指针 p */
       printf("命令行参数为\n");             /* 打印一个字符串 */
       for(p=argv;argc>0;argc--,p++) {       /* 为打印命令行参数设的循环 */
          printf("%s\n",*p);                 /* 打印命令行参数字符串 */
       }
       printf("环境字符串为:\n");            /* 打印一个字符串 */
       for(p=envp;*p;p++) {                  /* 为打印环境参数设的循环 */
          printf("%s\n",*p);                 /* 打印环境字符串 */
       }
    }

第七节  指针与函数

    指针和函数之间可以有下列几种关系:

    一、指针作为函数的参数
    在C语言中可以把指针作为函数的参数。在函数调用中使用指针作为参数有很大的优越性,因为传送一个指针给函数只是传送了一个变量的地址,这比传送一大批数据要快得多,在例6.1里我们已经用过了这种传送方式。当我们把一个基本类型的自动变量作为参数被函数调用时,被调用函数退出后,主调程序里这个变量值的改变并没有被保留下来,而用  return 语句又只能返回一个变量的值给调用者。但使用指针作为参数就不同了,被调用函数可以改变指针指向的各元素的值,当退出这个函数时主调程序里这些元素的值已经被改变了。因此,使用指针做参数,可以得到很多被改变了的值。实际上参数数据的传递仍是单向的,并非形参的值传回了实参,而是因为指针作为参数时实参和形参占用了同一段内存,因此,改变了的值被保留下来了。
    前面我们已经说过,指针和数组常常可以交替使用,在函数参数调用时也是这样,因为不论是数组还是指针作为参数都是进行指针的传递,所以可以用一个指针变量作为实参,而形参用数组形式,也可以在实参是数组时把形参定义成指针,如:

        main()                         /* 主函数 */
        {
          int a[5],*p=a;               /* 定义一个整形数组 a 和一个指针变量 p
                                          并将数组的首地址赋给 a */
          ......
          func(p);                     /* 调用函数 func,并把指针作为实参 */
        }
        func(t)                        /* 定义函数 func */
        int t[];                       /* 把形参说明为一个整形数组 */
        {
          ......
        }

或:

        main()                         /* 主函数 */
        {
          int a[5];                    /* 定义一个整形数组 */
          ......
          func(a);                     /* 调用函数 func,并把数组作为实参 */
        }
        func(p)                        /* 定义函数 func */
        int *p;                        /* 把形参说明为指针类型 */
        {
          ......
        }

不过要注意,不能将一个还没有确定指向对象的指针当作参数来传递。
   
    二、返回一个指针的函数
    函数可以返回一个指针,例如在程序中可以说明这样一个函数:

        int *func();

这个函数返回一个指向整数的指针。定义一个返回指针值的函数的一般形式为:

        <存储类型> <数据类型> * <函数名> (参数表)
        参数说明
        {
            局部数据描述
            执行语句
        }

    例6.8  使例6.1的字符串拷贝函数 copy 增加返回指向目标字符串的指针的功能。

    char *copy(char *a,char *b)         /* 定义一个函数,返回值为字符型指针 */
    {
      char *s=b;                        /* 定义一指针变量,指向字符串 b 之首 */
      while((*b++=*a++)!=''''''''''''''''\0'''''''''''''''');         /* 为复制字符串建的循环 */
      return s;                         /* 返回字符串首指针 */
    }

    程序中定义了一个指针变量 s,并使 s 包含字符串 b 的首地址,最后用 return 语句返回这个指针。要返回一个指针,在 return 语句中必须用地址表达式。

    三、函数的指针
    函数不是变量,不能用它来构成一个数组,也不能作为参数传递给另一个函数,但它在存储器中占有实际的位置。在C语言中,可以定义一个指向函数的指针。函数指针实际上是程序代码段的一个地址,即是指向函数可执行代码的起始地址,或称为该函数的入口地址,因而指针可以代替函数名。使用函数指针的目的在于访问函数或将函数作为参数传递给其他的函数,也可以构造一个每个元素都是指向函数的指针的指针数组。定义一个函数指针的一般形式为:

        <存储类型> <数据类型> (* <指针变量名>)( );

    例如可以定义一个指向函数的指针:

        int (*func)();

它的返回值是整形的。当它作为一个函数的形参时可进行如下的描述:

        defunc(func)                   /* 定义一个函数 defunc */
        int (*func)();                 /* 说明其形参为一个指向函数的指针 */
        {
          ........
          (*func)();                   /* 函数调用 */
          ........
        }

    func 是指向一个函数的指针,是一个地址值,而 *func 是函数,所以在函数体内,函数可用 (*func)() 调用。在程序其他地方调用 defunc 函数时,应用已说明的函数名作为实参,这是因为函数名本身就是该函数的入口地址,调用函数就是把指针指向函数的这个入口地址。例如:

        ........
        defunc(f);
        ........

    调用函数 defunc 时,将把函数 f 的地址传送给指针 func,defunc 可以在相应的地方执行 func 所指向的函数。上面介绍的指向函数的指针定义成:
 
        int (*func)();

表示 func 是指向一个函数的指针,它返回的是一个整数,要注意和前面讲过的返回一个指针的函数 int *func() 区分开来。 另外要注意,在函数指针上进行算术运算也是不允许的。如  func++ 和 func+=4 等表达式都是无意义的。

    例6.9  函数指针运行实例。

    main()                      /* 主函数 */
    {
      int f(),a=0,b=0;          /* 定义整形变量 a、b,说明函数 f 返回一个整数 */
      int (*func)();            /* 定义 func 是指向函数的指针,返回一个整数 */
      func=f;                   /* 使 func 指向函数 f 的入口地址 */
      a=(*func)();              /* 调用指针函数,返回值赋给变量 a */
      b=f();                    /* 调用函数 f,返回值赋给 b */
      printf("%d\n%d\n",a,b);   /* 屏幕打印返回值 */
    }
    int f()                     /* 定义函数 f */
    {
      printf("ABCDEF\n");       /* 在屏幕上打印一字符串 */
      return 1;                 /* 返回整形值1 */
    }

    该程序的运行结果为:

    ABCDEF
    ABCDEF
    1
    1

    在C语言中,函数名表示函数的入口地址,在给函数指针赋值时只需给出函数名就可以了。func=f; 把函数 f 的地址赋给了 func,我们可以看到 (*func)(); 运行指针函数和运行函数 f(); 结果都是在屏幕上打印了字符串 "ABCDEF",打印的返回值均为1。 在用函数指针变量调用函数时,(*func) 代替了函数名。




相关专题: 我的著作
专题信息:
  C语言速成(第三章 程序控制语句)(2006-1-1 13:51:28)[9443]
  C语言速成(第七章 联合、枚举和自定义数据类型)(2006-1-1 13:51:46)[10134]
  C语言速成(第六章 结构)(2006-1-1 13:52:05)[8850]
  C语言速成(第四章 数组)(2006-1-1 13:52:42)[4756]
  C语言速成(第二章 常量、变量、运算符与表达式)(2006-1-1 13:53:05)[7265]

相关信息:
 没有相关信息
  打印本页
设为首页 | 加入收藏 | 联系我们 | 管理入口
皖ICP备05018956号
Copyright © 2003 J.W.SHEN All Rights Reserved
后台管理系统 V1.0 制作