指针

  • 指针是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 的支持和帮助!

《C语言程序设计》指针篇<一>的更多相关文章

  1. 快速上手系列-C语言之指针篇(一)

    快速上手系列-C语言之指针篇(一) 浊酒敬风尘 发布时间:18-06-2108:29 指针的灵活运用使得c语言更加强大,指针是C语言中十分重要的部分,可以说指针是C语言的灵魂.当然指针不是万能的,但没 ...

  2. 瘋子C语言笔记(指针篇)

    指针篇 1.基本指针变量 (1)定义 int i,j; int *pointer_1,*pointer_2; pointer_1 = &i; pointer_2 = &j; 等价于 i ...

  3. Java语言程序设计-助教篇

    1. 给第一次上课(软件工程)的老师与助教 现代软件工程讲义 0 课程概述 给学生:看里面的第0个作业要求 2. 助教心得 美国视界(1):第一流的本科课堂该是什么样?(看里面的助教部分) 助教工作看 ...

  4. 《C语言程序设计》指针篇<二>

    通过指针引用多维数组 如何理解二维数组元素的地址? 要知道,这本书用了整整两页的内容来讲解这方面的知识,从这里足以看出来理解通过指针来引用二维数组是一件比较麻烦的事情,但是我认为理解并不难. 什么是二 ...

  5. 0031 Java学习笔记-梁勇著《Java语言程序设计-基础篇 第十版》英语单词

    第01章 计算机.程序和Java概述 CPU(Central Processing Unit) * 中央处理器 Control Unit * 控制单元 arithmetic/logic unit /ə ...

  6. C语言重点——指针篇(一文让你完全搞懂指针)| 从内存理解指针 | 指针完全解析

    有干货.更有故事,微信搜索[编程指北]关注这个不一样的程序员,等你来撩~ 注:这篇文章好好看完一定会让你掌握好指针的本质 C语言最核心的知识就是指针,所以,这一篇的文章主题是「指针与内存模型」 说到指 ...

  7. C\C++语言重点——指针篇 | 为什么指针被誉为 C 语言灵魂?(一文让你完全搞懂指针)

    本篇文章来自小北学长的公众号,仅做学习使用,部分内容做了适当理解性修改和添加了博主的个人经历. 注:这篇文章好好看完一定会让你掌握好指针的本质! 看到标题有没有想到什么? 是的,这一篇的文章主题是「指 ...

  8. Java语言程序设计(基础篇)第一章

    第一章 计算机.程序和Java概述 1.1 引言 什么是程序设计呢? 程序设计就是创建(或者开发)软件,软件也称为程序. 1.2 什么是计算机 计算机是存储和处理数据的电子设备,计算机包括硬件(har ...

  9. C语言程序设计(十二) 结构体和共用体

    第十二章 结构体和共用体 当需要表示复杂对象时,仅使用几个基本数据类型显然是不够的 根本的解决方法是允许用户自定义数据类型 构造数据类型(复合数据类型)允许用户根据实际需要利用已有的基本数据类型来构造 ...

  10. Java语言程序设计(基础篇)第二章

    第二章 基本程序设计 2.2 编写简单的程序 1.变量名尽量选择描述性的名字(descriptive name). 2.实数(即带小数点的数字)在计算机中使用一种浮点的方法来表示.因此,实数也称为浮点 ...

随机推荐

  1. GridFS Example

    http://api.mongodb.com/python/current/examples/gridfs.html This example shows how to use gridfs to s ...

  2. java.security.NoSuchAlgorithmException: AES KeyGenerator not available

    异常信息 Caused by: Java.lang.IllegalStateException: Unable to acquire AES algorithm. This is required t ...

  3. 栈(stack)和堆(heap)

    栈(stack)和堆(heap), Java程序在运行时都要开辟空间,任何软件在运行时都要在内存中开辟空间,Java虚拟机运行时也是要开辟空间的.JVM运行时在内存中开辟一片内存区域,启动时在自己的内 ...

  4. canvas绘图,html5 k线图,股票行情图

    canvas绘图,html5 k线图,股票行情图 canvas跟其他标签一样,也可以通过css来定义样式.但这里需要注意的是:canvas的默认宽高为300px * 150px,在css中为canva ...

  5. java中避免乱码

    response.setContentType("text/html;charset=UTF-8"); 这个是在action中的 这个是在json中设置乱码的 contentTyp ...

  6. 前端框架VUE----表单输入绑定

    vue的核心:声明式的指令和数据的双向绑定. 那么声明式的指令,已经给大家介绍完了.接下来我们来研究一下什么是数据的双向绑定? 另外,大家一定要知道vue的设计模式:MVVM M是Model的简写,V ...

  7. Python3+PyQt5+PyCharm 桌面GUI开发环境搭建

    Python3+PyQt5+PyCharm 桌面GUI开发环境搭建 一.安装python PyQt5所支持的python版本是不低于3.5版本 python3.5以上的版本安装:https://www ...

  8. 做了 3 年企业级 SaaS,我收获的 10 点心得(转)

    关于中国企业级服务的总结不少,本土派和海外派都有出色的文章出来,VC 和创业者站在各自角度也有很多不错的总结.本文基于 Ping++ 近三年的创业历程而来,有弯路,有教训,有醒悟,也有心得.盛景 B2 ...

  9. UI自动化(一)html基础

    前端的三把利器 HTML:赤裸的一个人 CSS:华丽的衣服 JS/JavaScript:赋予这个人的行为,也就是动起来 DOM 就是将页面变成可操 HTML(超文本标记语言) html代码实际上就是一 ...

  10. jq svg 修改image的xmlns:xlink及图片的显隐

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...