C语言研究性学习的路线(7)指针

 

C语言研究性学习的路线

现行的多数C语言教材有太多的误区,不仅不能给读者提供有效的学习线索,还常常“误导”读者,于是,“死记硬背”便成了学习C语言的唯一选择。本文以拙作《新编C语言程序设计教程》(清华大学出版社出版,配套视频zeq126.56.com)为基础,探讨了C语言的研究性学习。

C语言的知识点有:

1.         C语言与计算机的关系

2.         表达式的求值

3.         逻辑运算及选择结构

4.         算法及循环结构

5.         数组的作用及准确理解

6.         函数的作用及准确理解

7.         指针的作用及准确理解

8.         自定义数据类型及文件

这几部分相辅相成,构成了一个有机的整体。分析如下:

七、指针的作用及准确理解

  提起指针,大家谈虎色变,之所以如此,我认为最主要的原因是概念不清。学习指针需弄懂两个关键问题:指针是什么?指针有什么用?

(一)指针是什么?

  变量是内存中一块存储单元的标识,C语言中通过变量使用计算机中的内存。指针变量也不例外。定义一个指针变量就会有一块存储单元与之关联(当然指针变量也同样有生命期)。整型变量存储整数,浮点型变量存储小数,指针变量存储什么?

  指针变量存储地址,似乎地球人都知道,不过这种理解并不准确。地址是什么?地址是计算机中以字节为单位的最小存储单元的编号。把一个整数如23存入整型变量所标识的存储单元时,它将以二进制补码形式编码并占4个字节,必须将这4个字节作为一个整体才能正确地访问数据,由此可见与变量相关的存储单元通常有多个字节组成并有固定的编码格式,也就是说存储单元通常是有类型的。存储单元有多少个字节组成就有多少个相关的地址,但存储单元以首字节的地址作为其地址。谈到地址时它必定是某类型存储单元的地址,单纯一个地址是毫无用处的。使用一个存储单元时必须知道它的地址和类型,当然我们在程序中可以通过变量幸福地使用某存储单元而不必考虑太多。因此,地址是有类型的,地址的类型就是其相关存储单元的类型。综上所述,指针变量存储某类型的地址。

  某种类型的指针变量只能存储相应存储单元的地址。整型指针变量只能存储整型变量的地址,字符型指针变量只能存储字符型变量的地址。指针变量有多少种类型呢?存储单元有多少种类指针变量就有多少种类型!存储单元的类型有整型、浮点型、字符型、长度为1的数组、长度为2的数组、整型指针......无穷无尽,甚至还有函数类型。代码段同样在内存中存储,也有相应的“存储单元”,也有相应的地址和类型,因此可以用与之匹配的指针变量保存其存储单元的地址,这样的指针变量称为指向函数的指针变量。指向函数的指针变量把代码段“存储”了,呵呵,代码段也成了数据!指针变量的确神通广大!

(二)指针有什么用?

  通过指针变量可以得知某存储单元的地址和类型,我们也就能使用这个存储单元了。如有int i, *pi=&i;,则*pi与变量i通常可以互换使用,此时在程序中,既可以用变量i使用它所标识的存储单元,也可以用*pi使用这个存储单元。有了指针变量,我们就有了一种新的访问存储单元的途径。指针变量的意义就在于此!

  通过标识符直接访问存储单元既直观又方便,为何要再提供一种新的访问存储单元的途径呢?新的途径通过指针变量利用间接引用操作符虽然稍嫌复杂,但是它呈现了新的特点。指针变量的主要作用总结如下:

1.可以突破变量作用域的限制

  程序中通常把相关代码组织成函数,但函数的封闭性致使某些操作不能进行,如使用函数交换两个实参的值。当指针变量作为参数时,实参传递的是相关存储单元的地址,于是我们就可以利用形参直接访问相关存储单元,这样就突破了变量作用域的限制,能够在函数中使用“外部的存储单元”。

2.可以提高函数的执行效率

  形参相关的存储单元比较大时,如大数组,大结构体等,函数执行用实参给形参赋值时既浪费时间又浪费空间。如果把形参的类型换成相应的普通的指针类型,因为无论何种类型的普通指针变量均占4个字节,则赋值时只需4个字节,且在函数中通过指针变量同样可以使用相关数据,函数的执行效率无疑会大大提高。注意:为了防止函数中误用实参(如修改实参的值),可以用关键字const限定。

3.可以使用“未命名”存储单元

  内存中的堆存储空间在程序运行中通过库函数调用“手动“申请,没有变量直接与之相关,只能通过指针变量间接引用的方式使用。

4.利用void型指针变量可以提高程序的抽象程度

  如下面的一个交换函数,它借助指针可以交换两个整型或浮点型变量的值,甚至是两个数组变量所有元素的值。

void swap(void *px, void *py, unsigned size)

{

   char temp, *pa, *pb;

   pa = (char *)px;

   pb = (char *)py;

   if(pa != pb)

      while (size--)

      {

         temp = *pa;

         *pa++ = *pb;

        *pb++ = temp;

      }

}

  又如对“任意类型”的数组进行排序的stdlib.h中的库函数qsort。其用法参见例9-31和例9-32。

(三)数组变量是指针吗?

  数组是变量吗?定义一个数组就是定义了多个变量并以数组名加下标的方式作为变量名,但数组名本身是变量吗?

举例讨论。已知int a[3]={1,2,3};则数组a的内存状态如图9-7所示。

   图9-7 一维数组a的内存状态

  数组a没有专属于自己的存储单元,仅是数组元素的代表。C语言中变量用于标识一个存储单元,数组名标识存储单元了吗?

  从图9-7可知,如果把数组元素标识的存储单元算作数组a的存储单元,则数组a标识了存储单元,是一个变量。数组a的存储单元的长度为(4×3)12个字节(sizeof(a)的值为12)。数组变量a存储了什么数据?数组变量a的存储单元仅是名义上的,不能通过a使用它们,也就是说数组变量a中什么数据也没有存储。这样一来数组变量a还能算作变量吗?

  C语言规定数组变量的值为数组首元素的地址。虽然数组变量的值仅是规定并非在某存储单元中存储,但是,按照此规定数组变量a有值(&a[0]),当把这个值看成在数组a的名义上的存储单元中存储时,数组变量a就可以算作是一个变量了。数组变量a的状态如图1所示。

     图1数组变量a的状态

  数组变量a虽然称为变量,但是它和普通变量还是有区别的。它的存储单元是名义上的,不能通过a使用它们,如a=&a[2];等语句都是非法的(由此可见数组变量a的内容在程序中不可能改变)。虽然数组变量a没有存储数据,但是,可以使用它的内容(规定的),也就是说数组变量a可以位于赋值操作符的右边。

  综上所述,数组变量a只是一个虚拟的变量,并非真实存在的变量。

  数组变量是指针变量吗?

  虚拟的数组变量存储的是某存储单元的地址,应该算作指针变量。当数组作为函数的参数时会退化为相应的指针变量。数组与什么样的指针变量相对应呢?以上面的数组a为例,其存储内容为其首元素地址,即整型变量a[0]的地址,对于普通的指针变量,只有整型指针变量才存储整型变量的地址,因此,与数组变量a相对应的普通指针变量为整型指针变量。

  如有int *pi; pi=&a[0];,则算作指针变量的数组a与相对应的普通指针变量pi有何不同?

  首先,数组变量a是虚拟的,而普通指针变量pi有自己的存储单元,可以在程序中通过变量pi自由地使用这块存储单元。

  其次,更为重要的,虽然数组变量a和普通指针变量pi的值均为整型存储单元的地址,但是它们本身的类型却完全不同。由图1可知,数组变量a的长度为12个字节,其自身存储单元的类型为有3个元素的整型数组(int[3]),而普通指针变量pi的长度为4个字节,其自身存储单元的类型为整型指针变量(int *)。如果再定义指针变量用于存储数组变量a和指针变量pi自身存储单元的地址,则指向数组变量a的指针变量应定义为int (*pa)[2];,而指向指针变量pi的指针变量应定义为int *(*ppi);。当pa=&a;ppi=π时,*pa与a等价,*ppi与pi等价。相关测试程序如下:

#include

void main()

{

    int a[3]={1, 2, 3};

    int (*pa)[3];

    int i;

    int *pi, **ppi;

    pa = &a;

    pi = &a[0];

    ppi = π

   

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

        printf("%3d", (*pa)[i]);//*pa与a可互换

    printf("\n");

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

        printf("%3d", (*ppi)[i]);//*ppi与pi可互换 

}

  程序的输出结果为:

  1  2  3

  1  2  3

  综上所述,虚拟的数组变量是特殊的指针变量,与相对应的普通指针变量相比其自身存储单元的类型非常特殊。

(四)指针学习中的难点

1.野指针

  当指针变量存储的不是合法的存储单元的(所谓合法的存储单元即程序所拥有的存储单元)地址时,就称该指针变量为野指针。

  出现野指针的常见情况有两种。

  一是指针变量没有初始化就通过间接引用操作符使用指针变量指向的存储单元。如int *pi; *pi=5;,此与int i; printf(“%d”, i*5);的情况类似。

  二是指针变量指向的存储单元由于生命期等原因不再属于程序所拥有。

  通过存储其它存储单元的地址,指针变量给我们提供了一种使用存储单元新的途径,因此,指针变量p必须在指向合法的存储单元之后才能用*p的形式访问相关存储单元。

  把暂时不用的指针变量设置成空指针,可以有效地避免野指针的出现。

  程序中的指针变量只能有两种状态:一是指向合法的存储单元;二是空指针。

2.指针变量的加法运算

  指针变量加法运算的规则非常简单,表达式的值为其指向的存储单元的下一个同类存储单元的地址。指针变量加法运算通常只有与数组相关时才有实际意义,然而数组变量类型的复杂性使得计算指针变量的加法非常困难。如有int a[3][2];,则如何计算并理解表达式a+1,a+2,*a+1,*(a+1)+1十分不易。

  解决这个难点的关键在于正确理解二维数组变量,弄清楚变量a,a[0],a[0][0]相关存储单元自身的类型(如&a)和它们存储的数据的类型(如*a),以及三者之间的关系。

  当然也可利用公式(a[i]=*(a+i))通过转化的方法理解相关表达式。

3.void型指针变量给程序带来的通用性。

  其它类型的指针变量可以直接赋值给void型指针变量,这就为设计通用的程序带来了可能。仔细分析例9-31和例9-32,体会如何利用通用的qsort库函数为不同类型的数组排序。

(五)章节详析

9.1指针类型

重点:

1.通过分析整型指针变量i实际的内存状态,理解“存储单元的地址仅是其首字节的地址,仅凭地址不知道类型无法使用相关存储单元”。

2.理解变量的左值与右值的概念。

3.分析指针变量的特殊之处。

4.理解指针变量定义的语法。什么情况下称一个指针变量指向一个变量?

5.给指针变量赋值为何要用强制类型转化的方法?通过赋值再次体会变量的左值与右值的概念。

难点:

1.分析何种情况下变量呈现左值或右值。(通常当变量单独位于赋值操作符的左边时,变量呈现左值,当变量参与运算时变量多数情况下呈现右值。)

2.为何不能用整数给指针变量赋值?

9.2指针操作符和空指针

重点:

1.  掌握取地址操作符&的用法。(只能用于变量,返回变量所标识存储单元的地址。)

2.  掌握间接引用操作符*的用法。(只能用于地址,返回地址所标识存储单元的内容。)

3.  通过修改例9-2中j=*i;的错误,再次分析间接引用操作符*的用法。

4.  证明在一般情况下如果指针变量pi指向变量i,则*pi与i可以互换。

5.  理解例9-4中每条语句的作用。

6.  通过分析例9-5中的程序,理解野指针的概念,并讨论如何避免野指针带来的问题。

难点:

1.  理解指针变量的作用。(通过*pi访问其指向变量所标识的存储单元。)

2.  为何会出现野指针?(常见情况为指针变量没有被赋值。指针变量没有指向存储单元,就试图使用*pi访问相关存储单元。)

3.  如何避免出现野指针问题。(使程序中的指针变量始终处于两种状态,要么指向合法的存储单元;要么为空指针。然后在使用过程中检测指针变量是否为空指针即可。)

9.3指针与函数

重点:

1.  指针作为函数参数时实参应为何值?

2.  通过指针类型的形参使用存储单元,为何可以影响其它函数中变量的值?

3.  比较利用指针类型和变量访问同一存储单元的异同。(变量受作用域限制。只要获得存储单元的地址,就可利用指针变量使用相关存储单元,因此,通过地址传递,利用指针类型拓展了存储单元可以使用的范围。)

4.  分析指针类型给函数带来的影响。(函数的封闭性受到了一定影响,但由于指针类型作为函数参数用户可见,因此它没有全局变量那么大的破坏性。指针类型对函数的影响还在于函数的多个返回值可由作为函数参数的指针类型带回而不必由全局变量“秘密地”带回。

5.  通过例9-8分析函数返回值类型为指针时常见的错误。

难点:

1.  指针类型的形参为何可以改变“实参”的值。

2.  比较指针类型和全局变量对函数的影响。(可见性不同)

3.  比较例9-8和例9-9,分析为何程序中能利用指针变量访问非法的存储单元(非程序所有的)并有时还能获得期望的结果?

9.4指针变量支持的运算

重点:

1.指针变量加法运算的规则。(结果为同类型的指针变量;值为相邻的同类型存储单元的地址,即以存储单元为单位进行运算。)

2.通过例9-10,体会指向一维数组的首元素之后,指针变量再进行加法运算的实际意义。

3.简单了解表达式的左值性和右值性。

难点:

1.  不能为运算而运算,一定要清楚指针变量进行运算的实际意义,否则容易出现野指针。

2.  理解类似pi+1=3的表达式为何出错。

9.5指针与数组

重点:

1.  C语言规定:数组变量的右值为数组首元素的地址。

2.  下标操作符[]可用间接引用操作符*改写,即a[i]与*(a+i)等价。

3.  数组变量为何称为虚拟的变量?

4.  数组变量为何称为特殊的指针变量?

5.  如有数组变量a,则如何理解表达式
sizeof(a),sizeof(&a),sizeof(a+1),sizeof(*a)?

6.  通过例9-13,对于一维整型数组变量a,分析表达式*a+1,*(a+1),pi=a,a=3的含义。当整型指针变量pi指向a[0]后,分析表达式pi++,*pi++,*++pi,pi[2]与数组a的关系。

7.  通过例9-14,分析指针变量在scanf函数中的用法。

8.  数组类型作为形参时,为何可以省略其长度?(因为此时数组已退化成普通的指针类型)

9.  如何理解二维数组变量?(定义一个二维数组,实际上定义了多少个变量,分别是什么类型的变量?)

10.定义一个二维整型数组a,分析表达式a,a+1,a+2,*a,*a+1,*(a+1)+1。

11.通过例9-19,分析如何用函数输出二维数组。(二维数组作形参时退化成何种类型的指针变量?)

12.通过图9-10理解字符串常量。

13.通过图9-11理解字符型指针数组。

14.通过图9-12分析一维字符型指针数组a、a[0]及字符串常量三者之间的关系。

15.一维字符型指针数组作为形参时会退化成何种类型的指针变量?

难点:

1.  理解虚拟的数组变量是特殊的指针变量这一结论。

2.  准确理解与数组有关的复杂表达式。

3.  用实例说明对于数组变量a,a[i]可改写为*(a+i)。

4.  分析数组变量与其相对应的普通指针变量的异同。

5.  分析字符串常量、字符串数组及普通的字符型指针变量的异同。

6.  字符型指针数组与何种类型的指针变量相对应?

7.  虽然用一个指向指针的指针变量、一个指针变量也可以构成如图9-12所示的(数组名、数组首元素)关系,但通过例9-21可知,两者在使用中还是有不同之处。

9.6main函数和命令行参数

重点:

1.  main函数的第二种标准形式中形参分别是什么类型?有什么作用?

2.  什么是命令行参数?

3.  VC6.0中如何设置命令行参数?

难点:

1.  在main函数中正确地使用命令行参数。

2.  编写有无命令行参数均可执行的程序。

9.7指向函数的指针变量

重点:

1.  如何理解函数名?

2.  指向函数的指针变量有什么特点?(可以指向形参的个数类型和返回值类型相同的所有函数)

3.  指向函数的指针变量有什么作用?

4.  通过例9-25体会如何定义功能为求f(x)的定积分的函数。

难点:

1.利用指向函数的指针变量定义“抽象的”函数。

9.8使用堆空间

重点:

1.  存放数据的内存空间通常分为几个区?它们各有什么特点?

2.  库函数malloc返回的堆空间中的内存块有什么特点?(连续的,无类型)

3.  如何使用库函数malloc返回的内存块?

4.  库函数free在使用上有什么特点?(只需把要释放的内存块的首地址传给它即可。)

5.  通过例9-26体会如何定义“动态的”数组。(长度根据需要在程序运行期间才确定的数组。)

6.  分析例9-27中函数存在的问题。

难点:

1.  正确地利用库函数malloc和free使用堆空间。

2.  如何防止内存泄露。

9.9典型例题

例9-28熟悉与&a[i][j]和a[i][j]等价的几种形式。

例9-29分别利用指向二维数组的指针、指向一维数组的指针、指向整型的指针输出三维数组中的元素。体会三维数组的特点。

例9-30报数出圈。体会指针变量的加法运算。

例9-31库函数qsort的用法。理解该函数的作用。

例9-32利用库函数qsort对数组中的几个字符串常量排序,通过与例9-31的对比,加深对void型指针变量的理解,体会qsort函数的灵活用法。

 

 

你可能感兴趣的:(C语言研究性学习的路线,语言,c,存储,出版,算法,测试)