<< Back to man.ChinaUnix.net

                  指针、结构、联合和枚举

    本节专门对第二节曾讲述过的指针作一详述。并介绍Turbo C新的数据类型:
结构、联合和枚举, 其中结构和联合是以前讲过的五种基本数据类型(整型、 浮
点型、字符型、指针型和无值型)的组合。 枚举是一个被命名为整型常数的集合。
最后对类型说明(typedef)和预处理指令作一阐述。

                    指         针(point)

    学习Turbo C语言, 如果你不能用指针编写有效、正确和灵活的程序,  可以
认为你没有学好C语言。指针、地址、数组及其相互关系是C语言中最有特色的部
分。规范地使用指针, 可以使程序达到简单明了, 因此, 我们不但要学会如何正
确地使用指针, 而且要学会在各种情况下正确地使用指针变量。

    1. 指针和地址
    1.1 指针基本概念及其指针变量的定义
    1.1.1 指针变量的定义
    我们知道变量在计算机内是占有一块存贮区域的, 变量的值就存放在这块区
域之中, 在计算机内部, 通过访问或修改这块区域的内容来访问或修改相应的变
量。Turbo C语言中, 对于变量的访问形式之一, 就是先求出变量的地址,  然后
再通过地址对它进行访问, 这就是这里所要论述的指针及其指针变量。
    所谓变量的指针, 实际上指变量的地址。变量的地址虽然在形式上好象类似
于整数, 但在概念上不同于以前介绍过的整数, 它属于一种新的数据类型, 即指
针类型。Turbo C中, 一般用"指针"来指明这样一个表达式&x的类型,  而用 "地
址"作为它的值, 也就是说, 若x为一整型变量, 则表达式&x的类型是指向整数的
指针, 而它的值是变量x的地址。同样, 若
     double d;
则&d的类型是指向以精度数d的指针, 而&d的值是双精度变量d的地址。所以, 指
针和地址是用来叙述一个对象的两个方面。虽然&x、&d的值分别是整型变量x 和
双精度变量d的地址, 但&x、&d的类型是不同的, 一个是指向整型变量x的指针,
而另一个则是指向双精度变量d的指针。在习惯上,  很多情况下指针和地址这两
个术语混用了。
    我们可以用下述方法来定义一个指针类型的变量。
     int *ip;
首先说明了它是一指针类型的变量, 注意在定义中不要漏写符号"*",  否则它为
一般的整型变量了。另外, 在定义中的int 表示该指针变量为指向整型数的指针
类型的变量, 有时也可称ip为指向整数的指针。ip是一个变量, 它专门存放整型
变量的地址。
    指针变量的一般定义为:
     类型标识符  *标识符;
    其中标识符是指针变量的名字, 标识符前加了"*"号,  表示该变量是指针变
量, 而最前面的"类型标识符"表示该指针变量所指向的变量的类型。一个指针变
量只能指向同一种类型的变量, 也就是讲, 我们不能定义一个指针变量, 既能指
向一整型变量又能指向双精度变量。
    指针变量在定义中允许带初始化项。如:
      int i, *ip=&i;
注意, 这里是用&i对ip初始化, 而不是对*ip初始化。和一般变量一样,  对于外
部或静态指针变量在定义中若不带初始化项, 指针变量被初始化为NULL, 它的值
为0。Turbo C中规定, 当指针值为零时, 指针不指向任何有效数据, 有时也称指
针为空指针。因此, 当调用一个要返回指针的函数(第五节中介绍)时, 常使用返
回值为NULL来指示函数调用中某些错误情况的发生。
    1.1.2 指针变量的引用
    既然在指针变量中只能存放地址, 因此, 在使用中不要将一个整数赋给一指
针变量。下面的赋值是不合法的:
     int *ip;
     ip=100;
假设
     int i=200, x;
     int *ip;
我们定义了两个整型变量i, x, 还定义了一个指向整型数的指针变量ip。i, x中
可存放整数, 而ip中只能存放整型变量的地址。我们可以把i的地址赋给ip:
     ip=&i;
此时指针变量ip指向整型变量i, 假设变量i的地址为1800, 这个赋值可形象理解
为下图所示的联系。
            ip               i
        ┏━━━┓       ┏━━━┓
        ┃ 1800 ╂──→ ┃ 200  ┃
        ┗━━━┛       ┗━━━┛
            图1. 给指针变量赋值
以后我们便可以通过指针变量ip间接访问变量i, 例如:
     x=*ip;
运算符*访问以ip为地址的存贮区域, 而ip中存放的是变量i的地址, 因此, *ip
访问的是地址为1800的存贮区域(因为是整数, 实际上是从1800开始的两个字节),
它就是i所占用的存贮区域, 所以上面的赋值表达式等价于
     x=i;
    另外, 指针变量和一般变量一样, 存放在它们之中的值是可以改变的, 也就
是说可以改变它们的指向, 假设
     int i, j, *p1, *p2;
     i='a';
     j='b';
     p1=&i;
     p2=&j;
则建立如下图所示的联系:
            p1               i
        ┏━━━┓       ┏━━━┓
        ┃      ╂──→ ┃ 'a'  ┃
        ┗━━━┛       ┗━━━┛
            p2               i
        ┏━━━┓       ┏━━━┓
        ┃      ╂──→ ┃ 'b'  ┃
        ┗━━━┛       ┗━━━┛
             图2. 赋值运算结果
这时赋值表达式:
     p2=p1
就使p2与p1指向同一对象i, 此时*p2就等价于i, 而不是j, 图2.就变成图3.所示:
            p1               i
        ┏━━━┓       ┏━━━┓
        ┃      ╂──→ ┃ 'a'  ┃
        ┗━━━┛  ┌→ ┗━━━┛
            p2      │       j
        ┏━━━┓  │   ┏━━━┓
        ┃      ╂─┘   ┃ 'b'  ┃
        ┗━━━┛       ┗━━━┛
            图3. p2=p1时的情形
如果执行如下表达式:
     *p2=*p1;
则表示把p1指向的内容赋给p2所指的区域, 此时图2.就变成图4.所示
            p1               i
        ┏━━━┓       ┏━━━┓
        ┃      ╂──→ ┃ 'a'  ┃
        ┗━━━┛       ┗━━━┛
            p2               j
        ┏━━━┓       ┏━━━┓
        ┃      ╂──→ ┃ 'a'  ┃
        ┗━━━┛       ┗━━━┛
            图4. *p2=*p1时的情形
    通过指针访问它所指向的一个变量是以间接访问的形式进行的, 所以比直接
访问一个变量要费时间, 而且不直观, 因为通过指针要访问哪一个变量, 取决于
指针的值(即指向), 例如"*p2=*p1;"实际上就是"j=i;", 前者不仅速度慢而且目
的不明。但由于指针是变量, 我们可以通过改变它们的指向, 以间接访问不同的
变量, 这给程序员带来灵活性, 也使程序代码编写得更为简洁和有效。
    指针变量可出现在表达式中, 设
     int x, y *px=&x;
指针变量px指向整数x, 则*px可出现在x能出现的任何地方。例如:
     y=*px+5;  /*表示把x的内容加5并赋给y*/
     y=++*px;  /*px的内容加上1之后赋给y  [++*px相当于++(px)]*/
     y=*px++;  /*相当于y=*px; px++*/

    1.2. 地址运算
    指针允许的运算方式有:
    (1). 指针在一定条件下, 可进行比较, 这里所说的一定条件,  是指两个指
针指向同一个对象才有意义, 例如两个指针变量p, q指向同一数组, 则<, >, >=,
<=, ==等关系运算符都能正常进行。若p==q为真, 则表示p, q指向数组的同一元
素; 若p<q为真, 则表示p所指向的数组元素在q所指向的数组元素之前(对于指向
数组元素的指针在下面将作详细讨论)。
    (2). 指针和整数可进行加、减运算。设p是指向某一数组元素的指针, 开始
时指向数组的第0号元素, 设n为一整数, 则
     p+n
就表示指向数组的第n号元素(下标为n的元素)。
    不论指针变量指向何种数据类型, 指针和整数进行加、减运算时, 编译程序
总根据所指对象的数据长度对n放大, 在一般微机上, char放大因子为1, int、
short放大因子为2, long和float放大因子为4, double放大因子为8。 对于下面
讲述到的结构或联合, 也仍然遵守这一原则。
    (3). 两个指针变量在一定条件下, 可进行减法运算。设p, q指向同一数组,
则p-q的绝对值表示p所指对象与q所指对象之间的元素个数。 其相减的结果遵守
对象类型的字节长度进行缩小的规则。

    2. 指针和数组
    指针和数组有着密切的关系, 任何能由数组下标完成的操作也都可用指针来
实现, 但程序中使用指针可使代码更紧凑、更灵活。

    2.1. 指向数组元素的指针
    我们定义一个整型数组和一个指向整型的指针变量:
          int a[10], *p;
和前面介绍过的方法相同, 可以使整型指针p指向数组中任何一个元素,  假定给
出赋值运算
          p=&a[0];
此时, p指向数组中的第0号元素, 即a[0], 指针变量p中包含了数组元素a[0] 的
地址, 由于数组元素在内存中是连续存放的, 因此,  我们就可以通过指针变量p
及其有关运算间接访问数组中的任何一个元素。
    Turbo C中, 数组名是数组的第0号元素的地址, 因此下面两个语句是等价的
     p=&a[0];
     p=a;
根据地址运算规则, a+1为a[1]的地址, a+i就为a[i]的地址。
    下面我们用指针给出数组元素的地址和内容的几种表示形式。
    (1). p+i和a+i均表示a[i]的地址, 或者讲, 它们均指向数组第i号元素, 即
指向a[i]。
    (2). *(p+i)和*(a+i)都表示p+i和a+i所指对象的内容, 即为a[i]。
    (3). 指向数组元素的指针, 也可以表示成数组的形式, 也就是说,  它允许
指针变量带下标, 如p[i]与*(p+i)等价。
    假若:      p=a+5;
则p[2]就相当于*(p+2), 由于p指向a[5], 所以p[2]就相当于a[7]。而p[-3]就相
当于*(p-3), 它表示a[2]。

    2.2. 指向二维数组的指针
    2.2.1. 二维数组元素的地址
    为了说明问题, 我们定义以下二维数组:
     int a[3][4]={{0,1,2,3}, {4,5,6,7}, {8,9,10,11}};
a为二维数组名, 此数组有3行4列, 共12个元素。但也可这样来理解, 数组a由三
个元素组成: a[0], a[1], a[2]。而它匀中每个元素又是一个一维数组, 且都含
有4个元素 (相当于4列),  例如, a[0]所代表的一维数组所包含的 4 个元素为
a[0][0], a[0][1], a[0][2], a[0][3]。如图5.所示:
        ┏━━━━┓    ┏━┳━┳━┳━┓
  a─→ ┃  a[0]  ┃─→┃0 ┃1 ┃2 ┃3 ┃
        ┣━━━━┫    ┣━╋━╋━╋━┫
        ┃  a[1]  ┃─→┃4 ┃5 ┃6 ┃7 ┃
        ┣━━━━┫    ┣━╋━╋━╋━┫
        ┃  a[2]  ┃─→┃8 ┃9 ┃10┃11┃
        ┗━━━━┛    ┗━┻━┻━┻━┛
                    图5.
    但从二维数组的角度来看, a代表二维数组的首地址,  当然也可看成是二维
数组第0行的首地址。a+1就代表第1行的首地址, a+2就代表第2行的首地址。 如
果此二维数组的首地址为1000, 由于第0行有4个整型元素, 所以a+1为1008, a+2
也就为1016。如图6.所示
                            a[3][4]
                   a    ┏━┳━┳━┳━┓
              (1000)─→┃0 ┃1 ┃2 ┃3 ┃
                   a+1  ┣━╋━╋━╋━┫
              (1008)─→┃4 ┃5 ┃6 ┃7 ┃
                   a+2  ┣━╋━╋━╋━┫
              (1016)─→┃8 ┃9 ┃10┃11┃
                        ┗━┻━┻━┻━┛
                              图6.
    既然我们把a[0], a[1], a[2]看成是一维数组名, 可以认为它们分别代表它
们所对应的数组的首地址, 也就是讲,  a[0]代表第 0 行中第 0 列元素的地址,
即&a[0][0], a[1]是第1行中第0列元素的地址, 即&a[1][0], 根据地址运算规则,
a[0]+1即代表第0行第1列元素的地址, 即&a[0][1], 一般而言, a[i]+j即代表第
i行第j列元素的地址, 即&a[i][j]。
    另外, 在二维数组中, 我们还可用指针的形式来表示各元素的地址。如前所
述, a[0]与*(a+0)等价, a[1]与*(a+1)等价, 因此a[i]+j就与*(a+i)+j等价, 它
表示数组元素a[i][j]的地址。
    因此, 二维数组元素a[i][j]可表示成*(a[i]+j)或*(*(a+i)+j),  它们都与
a[i][j]等价, 或者还可写成(*(a+i))[j]。
    另外, 要补充说明一下, 如果你编写一个程序输出打印a和*a,  你可发现它
们的值是相同的, 这是为什么呢? 我们可这样来理解: 首先, 为了说明问题, 我
们把二维数组人为地看成由三个数组元素a[0], a[1], a[2]组成, 将a[0], a[1],
a[2]看成是数组名它们又分别是由4个元素组成的一维数组。因此, a表示数组第
0行的地址, 而*a即为a[0], 它是数组名, 当然还是地址, 它就是数组第0 行第0
列元素的地址。

    2.2.2 指向一个由n个元素所组成的数组指针
    在Turbo C中, 可定义如下的指针变量:
      int (*p)[3];
    指针p为指向一个由3个元素所组成的整型数组指针。在定义中, 圆括号是不
能少的, 否则它是指针数组, 这将在后面介绍。这种数组的指针不同于前面介绍
的整型指针, 当整型指针指向一个整型数组的元素时, 进行指针(地址)加1运算,
表示指向数组的下一个元素, 此时地址值增加了2(因为放大因子为2), 而如上所
定义的指向一个由3个元素组成的数组指针, 进行地址加1运算时, 其地址值增加
了6(放大因子为2x3=6), 这种数组指针在Turbo C中用得较少,  但在处理二维数
组时, 还是很方便的。例如:
          int a[3][4], (*p)[4];
          p=a;
    开始时p指向二维数组第0行, 当进行p+1运算时, 根据地址运算规则,  此时
放大因子为4x2=8, 所以此时正好指向二维数组的第1行。和二维数组元素地址计
算的规则一样, *p+1指向a[0][1], *(p+i)+j则指向数组元素a[i][j]。
     例1
     int a[3] [4]={
     {1,3,5,7},
     {9,11,13,15},
     {17,19,21,23}
    };
    main()
    {
         int i,(*b)[4];
           b=a+1;                  /* b指向二维数组的第1行, 此时*b[0]或
                                     **b是a[1][0] */
         for(i=1;i<=4;b=b[0]+2,i++)/* 修改b的指向, 每次增加2 */
           printf("%d\t",*b[0]);
         printf("\n");
         for (i=0; i<2; i++) {
           b=a+i;                  /* 修改b的指向,  每次跳过二维数组的
                                      一行 */
           printf("%d\t",*(b[i]+1));
        }
         printf ("\n");
     }
    程序运行结果如下:
     9    13   17   21
     3    11   19

    3. 字符指针
    我们已经知道, 字符串常量是由双引号括起来的字符序列, 例如:
          "a string"
就是一个字符串常量, 该字符串中因为字符a后面还有一个空格字符, 所以它由8
个字符序列组成。在程序中如出现字符串常量C 编译程序就给字符串常量按排一
存贮区域, 这个区域是静态的, 在整个程序运行的过程中始终占用, 平时所讲的
字符串常量的长度是指该字符串的字符个数, 但在按排存贮区域时, C 编译程序
还自动给该字符串序列的末尾加上一个空字符'\0', 用来标志字符串的结束, 因
此一个字符串常量所占的存贮区域的字节数总比它的字符个数多一个字节。
    Turbo C中操作一个字符串常量的方法有:
    (1). 把字符串常量存放在一个字符数组之中, 例如:
          char s[]="a string";
数组s共有9个元素所组成, 其中s[8]中的内容是'\0'。实际上, 在字符数组定义
的过程中, 编译程序直接把字符串复写到数组中, 即对数组s初始化。
    (2). 用字符指针指向字符串, 然后通过字符指针来访问字符串存贮区域。
当字符串常量在表达式中出现时, 根据数组的类型转换规则, 它被转换成字符指
针。因此, 若我们定义了一字符指针cp:
     char *cp;
于是可用:
     cp="a string";
使cp指向字符串常量中的第0号字符a, 如图7.所示。
            cp
        ┏━━━┓     ┏━┳━┳━┳━┳━┳━┳━┳━┳━┓
        ┃    ─╂─→ ┃a ┃  ┃s ┃t ┃r ┃i ┃n ┃g ┃\0┃
        ┗━━━┛     ┗━┻━┻━┻━┻━┻━┻━┻━┻━┛
                              图7.
以后我们可通过cp来访问这一存贮区域, 如*cp或cp[0]就是字符a,  而cp[i]或
*(cp+i)就相当于字符串的第i号字符, 但企图通过指针来修改字符串常量的行为
是没有意义的。

    4. 指针数组
    因为指针是变量, 因此可设想用指向同一数据类型的指针来构成一个数组,
这就是指针数组。数组中的每个元素都是指针变量, 根据数组的定义, 指针数组
中每个元素都为指向同一数据类型的指针。指针数组的定义格式为:
     类型标识 *数组名[整型常量表达式];
    例如:
       int *a[10];
定义了一个指针数组, 数组中的每个元素都是指向整型量的指针, 该数组由10个
元素组成, 即a[0], a[1], a[2], ..., a[9], 它们均为指针变量。a为该指针数
组名, 和数组一样, a是常量, 不能对它进行增量运算。a为指针数组元素a[0]的
地址, a+i为a[i]的地址, *a就是a[0], *(a+i)就是a[i]。
    为什么要定义和使用指针数组呢? 主要是由于指针数组对处理字符串提供了
更大的方便和灵活, 使用二维数组对处理长度不等的正文效率低, 而指针数组由
于其中每个元素都为指针变量, 因此通过地址运算来操作正文行是十分方便的。
    指针数组和一般数组一样, 允许指针数组在定义时初始化, 但由于指针数组
的每个元素是指针变量, 它只能存放地址, 所以对指向字符串的指针数组在说明
赋初值时, 是把存放字符串的首地址赋给指针数组的对应元素, 例如下面是一个
书写函数month_name(n), 此函数返回一个指向包含第n月名字的字符指针( 关于
函数, 第6节将专门介绍)。
    例2: 打印1月至12月的月名:
     char *month_name(int n)
     {
          static char *name[]={
               "Illegal month",
               "January",
               "February",
               "March",
               "April",
               "May",
               "June",
               "July",
               "August",
               "September",
               "October",
               "November",
               "December"
          };
          return((n<1||n>12)?name[0]:name[n]);
     }
     main()
     {
          int i;
          for(i=0; i<13; i++)
               printf("%s\n", month_name(i));
 

                             返回目录
     }