《C语言程序设计》指针篇<一>

来源:互联网 发布:中信信托知乎 编辑:程序博客网 时间:2024/05/16 18:25

指针

  • 指针是C语言的精华,同时也是其中的难点和重点,我在近日对这一部分内容进行了重新的研读,把其中的一些例子自己重新编写和理解了一遍。此篇博客的内容即是我自己对此书例子的一些理解和总结。

一.大问题:指针是什么?

我的理解:
变量的本质即内存,指针即访问变量的地址。利用指针来 <间接访问> 变量。
定义一个指针,p是指针变量名,系统自动为其分配内存,存放的是其指向的变量(内存)的地址。
例如:

1> int a=4;2> int *p;3> p=&a;

上述程序定义一个变量a,系统自动为其分配内存,那么这个内存的名称就是a,其存放的值是4。
再定义一个整形指针p,系统也为其分配内存(根据指针的类型分配不同大小的内存单元,例如此处指针为int类型,计算机为其分配4个字节),该内存里存放的是指向名称为a的内存的地址

  • 变量的本质是什么?-变量名只是一个代号,变量的本质就是内存。
  • 指针保存的是什么?-指针保存的是内存地址。

这样来看,会不会对指针理解更加清楚一点呢?

草图:
待解决

二.指针变量

  • 使用指针变量
  • 定义指针变量
  • 引用指针变量
  • 指针变量作为函数参数

1.int *p1,*p2; p1-p2是什么?

#include<stdio.h>int main(){    int a[4]={3,4};        int *p1=&a[0],*p2=&a[1];    printf("%d %d\n",p1,p2);    printf("%d ",p2-p1);/*p2-p1=地址之差/数组元素的长度    就是p1所指向的元素与p2所指向的元素差之间几个元素*/    return 0;}

我的理解:
p1指向数组a的首元素a[0],p2指向数组a的a[1],p1-p2代表的是a[0]与a[1]之间相隔几个元素。
输出结果:

计算机自动处理为p2-p1=地址之差/数组元素的长度.而不是简单的地址之差=4.

2.输入三个整数按从大到小的顺序输出.(指针变量作为函数参数)

  • 错误解法:
#include<stdio.h>void swap(int *p1,int *p2,int *p3){    int *p;    if(*p1<*p2)    {        p=p1;        p1=p2;        p2=p;    }    if(*p1<*p3)    {        p=p1;        p1=p3;        p3=p;    }    if(*p2<*p3)    {        p=p2;        p2=p3;        p3=p;    }}int main(){    int a,b,c;    scanf("%d %d %d",&a,&b,&c);    printf("%d %d %d\n",a,b,c);        int *point_1=&a,*point_2=&b,*point_3=&c;        swap(point_1,point_2,point_3);    printf("%d %d %d\n",*point_1,*point_2,*point_3);    return 0;}

输入样例:7 8 9
输出结果:

  • 正确解法:
/*输入三个整数按从大到小的顺序输出*/#include<stdio.h>void swap(int *p1,int *p2,int *p3){    int p;    if(*p1<*p2)    {        p=*p1;        *p1=*p2;        *p2=p;    }    if(*p1<*p3)    {        p=*p1;        *p1=*p3;        *p3=p;    }    if(*p2<*p3)    {        p=*p2;        *p2=*p3;        *p3=p;    }}int main(){    int a,b,c;    scanf("%d %d %d",&a,&b,&c);    printf("%d %d %d\n",a,b,c);        int *point_1=&a,*point_2=&b,*point_3=&c;        swap(point_1,point_2,point_3);    printf("%d %d %d\n",a,b,c);    return 0;}

输入样例:7 8 9
输出结果:

我的理解:

  • 函数值是单向传递。swap函数中的指针变量p1,p2,p3交换地址,这样的变化并不会传递回原函数使原函数的指针变量的值发生改变。原函数中指针变量point_1/2/3所存储的内存地址并没有改变。
  • 错误样例的错误在于:
    原函数传递三个变量的地址到swap函数,企图通过swap交换形参p1,p2,p3这些指针变量所存储的地址,来交换实参point_1/2/3地址。然而忽略了函数调用时形参和实参是采用单向传递的值传递方式。在函数中改变了名称为p1,p2,p3的内存单元所存储的内存地址,但是主函数中名称为point_1/2/3的内存单元所存储的内存地址并没有改变。
  • 这样的一个细节问题导致了程序的错误。时刻注重细节是成为一名程序员的基本素养。
  • 使用指针来处理的优点:能够改变多个值。而普通的函数只能有一个返回值。

3.输入两个数 按从大到小的顺序输出

#include<stdio.h>void avoid(int *p1,int *p2)/*传递的是地址,p1,p2储存的是a,b的地址*/{    int p;    if(*p1<*p2)    {        p=*p1;        *p1=*p2;        *p2=p;/*实质就是交换a,b*/    }}int main(){    int *p1,*p2;    int a,b;    scanf("%d %d",&a,&b);    p1=&a;    p2=&b;    if(a<b)    avoid(p1,p2);    printf("%d %d",a,b);        return 0;}

我的理解:
void avoid(int *p1,int *p2)函数接收内存名为a和内存名为b的内存的地址。通过交换指针(函数接收的地址)所指向的内存所存储的值来达到排序的目的。

输入样例:3 4
输出结果:

三.通过指针引用数组

  • 数组元素的指针
  • 指针的运算
  • 通过指针引用数组元素
  • 数组名作为函数参数

  • 通过指针引用多维数组

探究a+i(p+i)与&a[i]的关系 例一

#include<stdio.h>int main(){    int a[10]={1,2,3,4,5,6,7,8,9,0};    int *p=a;    int i;    for(i=0;i<=9;i++)    {        printf("%d ",*(p+i));/* *(p+i)与a[i]等价 */    }    return 0;} 

输出结果:

我的理解:

  • 数组名a即是&a[0](该数组首元素的地址),将a[0]的地址赋值给指针变量p,并利用指针输出该数组的各元素。
  • for循环中的printf("%d ",*(p+i));语句里的*(p+i)a[i]无条件等价。
  • 原因:前面的语句,p的值是a数组首元素的地址,而对于(p+i),计算机系统处理成&a[0]+i*数组元素的长度,也就是说(p+i)是数组元素a[i]的地址(即&a[i])。那么*(p+i)就相当于是a[i]

基于这一原理,我们来看下一个例子

探究a+i(p+i)与&a[i]的关系 例二

#include<stdio.h>int main(){    int a[15];    int i;    int *p;    for(i=0;i<10;i++)        scanf("%d",&a[i]);    for(p=a;p<a+10/*地址小于&a[10]*/;p++)    /*a+10等价于&a[10]*/    {        printf("%d ",*p);    }    return 0;} 

输入样例:1 2 3 4 5 6 7 8 9 0
输出结果:

我的理解:

  • 在理解完例一之后,来看这个例子:a数组有十个元素,输入这十个元素,然后要求利用指针输出这十个元素。
  • 与例一不同的是,这里的for循环有些不一样: for(p=a;p<a+10;p++); 首先进行赋值操作,把a数组首元素的地址赋值给p,然后输出*p的值(p指向该数组元素),再执行p++(该操作的意义是:p指向该数组的下一元素),循环结束的条件是p<a+10,即当p指向a[9]时不再进行自增操作。
  • 本例利用p指针的自增来按序输出不同的数组元素,++自增运算符是C语言的一大特色。
    有关于p=a下文会有提及。

探究a+i(p+i)与&a[i]的关系 例三

#include<stdio.h>int main(){    int a[15];    int *p=a;    int i;    for(i=1;i<=5;i++)    scanf("%d",p+i);/*输入地址p+i相当于&a[i]*/        for(i=1;i<=5;i++)    printf("%d ",*(p+i));        return 0;}
  • a数组里有五个元素,输入这五个元素,并利用指针按序输出。
    有了上述两例的铺垫,这个例子现在理解起来是不是比较容易呢?

有关函数参数的应用 例四

题目:有一个含有n个元素的数组,第一行输入n,第二行输入这个数组的元素,编写一个程序使该数组的元素按相反顺序存放并输出。

  • 代码1:
#include<stdio.h>void inv(int a[],int n)/* void inv(int *p,int n) */{    int t,i,j;    for(i=1,j=n;i<=j;i++,j--)    {        t=a[i];        a[i]=a[j];        a[j]=t;    }}int main(){    int n,a[105];    int i,j;    int *p=a;    scanf("%d",&n);    for(i=1;i<=n;i++)    scanf("%d",p+i);        inv(a,n);        for(i=1;i<=n;i++)    printf("%d ",a[i]);        return 0;}

我的理解(代码1):形参和实参都用数组名

  • 首先,在主函数里定义指针p并将数组a首元素的地址(数组名 int *p=a)赋值给它,接着这个例子特别的地方在于:将数组名作为函数实参传递给inv函数。通过前面的例子我们可以知道:数组名相当于是数组首元素的地址,也就是说这里的函数实参相当于是数组首元素的地址。
  • 其次,在我们定义的函数inv中,void inv(int a[],int n); 这里我们的函数形参也用了数组名,它与 void inv(int *p,int n);等价,因为这里的形参数组名相当于指针变量,用来接收传递自主函数的地址。我比较喜欢这样的做法,这样的好处一是易于我们初学者理解,二是不像普通的int,float之类的自定义函数只能有一个返回的return值,它能够对整个数组元素进行操作。这里我个人并不是很准确的把它称为:“通过指针,inv函数接收了一个数组”。
  • 除了上述的有关于函数实参形参的问题,这段程序还有一个大家刚刚接触过的一个注意点:scanf("%d",p+i);中的 p+i
  • 代码2:
#include<stdio.h>int main(){    void inv(int *p,int n);    int n,a[105],i,j;    int *p=a;    scanf("%d",&n);    for(i=0;i<n;i++)    scanf("%d",p+i);        inv(p,n);            /* p=a */                for(p=a;p<a+n;p++) /* p<=p+n||p<=a+n? Error */    printf("%d ",*p);            return 0;}void inv(int *p,int n){    int *i,*j,t=0;        for(i=p,j=p+n-1;i<=j;i++,j--)    {        t=*i;        *i=*j;        *j=t;    }}

我的理解(代码2):形参和实参都用指针变量

  • 与代码1有所不同的是,代码2所定义的inv函数实参和形参都使用了指针来传递地址(数组首元素的地址),这也验证了我的理解(代码1)的说法:函数形参用数组名与函数形参使用指针变量效果和目的是一样的,都是接受来自主函数的地址。不过代码2不足的地方也是在这个地方,由于在函数inv里面没有声明a数组(或者说,没有把a数组“传递过来”),无法像代码1一样直接对a数组的元素进行操作,只能通过函数inv定义的指针p(其值为数组a的首元素地址)来进行操作。

  • 代码1和代码2提供了两种情况,如果有一个实参数组,想在函数中改变此数组中元素的值,实参和形参对应的情况如下:
    (1)形参和实参均使用数组名。如代码1。
    (2)形参和实参均使用指针变量。如代码2。
    (3)形参使用数组名,实参使用指针变量。
    (4)形参使用指针变量,实参使用数组名。
  • 不管是哪一种情况,函数实参和形参传递的都是数组a首元素的地址,在本篇文章的开头我有提到,指针与内存是息息相关的。此时,我们注意到代码1这里的形参void inv(int a[],int n);用到了数组名a,但是由于数组名a相当于指针变量,计算机系统并没有真正给它分配一个数组的空间。可以这样理解:

inv函数在调用期间,形参数组和实参数组共同使用 同一段内存 ,那么对于代码1,在函数中的操作就是 直接 对主函数中数组a的各个元素的操作。

  • 代码3:
#include<stdio.h>void swap(int *p1,int *p2){    int t;/*为何此处不能用*t?*/    t=*p1;    *p1=*p2;    *p2=t;}int main(){    int a[105],t,n,i,j;    int *p=a;    scanf("%d",&n);    for(i=1;i<=n;i++)    scanf("%d",p+i);    for(i=1,j=n;i<=j;i++,j--)    {        swap(&a[i],&a[j]);    }        for(i=1;i<=n;i++)    printf("%d ",a[i]);        return 0;}

我的理解(代码3):有点不同

  • 关于代码3,这是我拿到题目最初的思路和做法,其实如果要快速解题的话,大可不必利用指针这么麻烦,在主函数里面利用for循环即可快速解题。
  • 但是我为什么要贴出这一段代码呢? 原因在于这段代码的注释,这个地方也是很令我困扰。
  • 理论上,定义一个整形指针p,p指向的是一个未知的整形内存,也可以起到上面代码swap函数中中间变量t的作用:
void swap(int *p1,int *p2){    int *p;    *p=*p1;    *p1=*p2;    *p2=*p;}
  • 输入样例:4 1 2 3 4
  • 输出结果:
  • 系统报错。这让我感到意外,按照上述内容来看,我的想法应该是正确的。但是我忽略了这样做的危险性。
  • 上面我定义的指针准确的名称叫做野指针,其缺省值(未修改前的初始值)是随机的,如果刚刚开始定义的指针没有明确的初始化或者被设置成null指针,它的指向可能是不合法的。如果定义一个野指针,基于其未知的危险性,计算机系统会报错。
  • 正确样例:
  • 代码3(修改):
#include<stdio.h>void swap(int *p1,int *p2){    int a=4;        int *t=&a;        *t=*p1;    *p1=*p2;    *p2=*t;}int main(){    int a[105],t,n,i,j;    int *p=a;    scanf("%d",&n);    for(i=1;i<=n;i++)    scanf("%d",p+i);    for(i=1,j=n;i<=j;i++,j--)    {        swap(&a[i],&a[j]);    }        for(i=1;i<=n;i++)    printf("%d ",a[i]);        return 0;}

感谢大家的阅读,不足之处还望提出和指正!关于多维数组和指针数组等内容,详见我的下一篇博客。

感谢 @thousfeet 的支持和帮助!

0 0
原创粉丝点击