C语言自学之指针理解

目的: 通过以下学习,希望能理解指针的概念,理解指针和数组的关系,理解指针的定义,掌握指针的用法。

1. 简述

  用C语言写的代码基本上都用到指针,掌握好指针的概念对学好C有很大帮助。 为了方便理解我们可以把指针称作某一块内存的名字(指针的值是某块内存的地址),通常计算机的内存会被分成许多小块,而每块都可以有一个名字,而实际上它每一块内存(一般一字节为一块)是有个编号的。为了更为直观,我们可以把全部的内存比作一个大柜子, 这个柜子里面都是抽屉,每一个抽屉都有一个序号(内存地址编号),而抽屉名称(指针)则是我们为某个序号(内存编号)起的外号。




说明:
(1)上图中,内存地址的编号的绰号除了可以是指针变量外,还可以是其它变量,如int。如果地址的编号的绰号是指针变量,那么在这个地址里住着的就必须是另外一个内存地址编号,如果是一个int,里面住着的就是int数据。区别这些就需要我们去理解变量概念。
          变量的要素: <1> 变量名  <2>变量名在内存的位置  <3>变量的值  <4>变量的值的位置  <5>普通变量和指针变量

      <1> 变量名是变量的称呼,如人名。

      <2> 变量名就是内存位置的外号,内存中其实是不存在这个名称的,主要是通过编译器把变量名和一块内存空间的编号关联起来,如上图,可以关系0X00000002为变量pi
      <3>变量的值就是变量名所代表的内存空间里存放的数据,例如0X00000002里存放着数据"xyz",那么变量pi的值就等于"xyz"。

      <4>普通变量可以直接取值,变量名和变量值之间没有间隔,例如声明 int i = 10, 假定i代表的是0X00000002编号, 要取值时,直接拉开0X00000002抽屉就可以取到i的数值10,因为10就是存放在0X00000002这个内存空间里的数据。指针变量名和变量值之间存在着间隔,指针代表的内存空间里的值并不是最终的我们所期待的值,真正的值需要使用指针操作符(*)简接取值。

      <5>为什么要设计指针这种简接操作的变量呢?其中涉及到内存空间高可用、数据复制效率、程序运行效率等方面的问题,假如我们分别为0X00000002和0X00000001这两个空间复制0X00000000的数据,0X00000002使用指针方法,只需要把0X00000000这几个数复制到0X00000002里面的空间;0X00000001用普通方法,是要把0X00000000里面的所有数据搬过来,如果0X00000000里数据非常大,两种方法的工作量和效率从中可见。
  
(2)当我们声明int * pi时, 系统会把一个内存编号的外号记作pi, 我们假设是上图中的0X00000002,然后我们再在0X00000002里装入另一个地址,这就完成了int * pi声明的作用,我们怎样打0X00000002装入另外一个地址呢? 很简单,直接赋地址值就可以了,假定我们知道了0X00000001存储的是int数值(假定是100),我们就可以将0X00000001赋值给pi,赋值之后,0X00000002里面的数据是0X00000001,可以理解为0X00000002这号抽屉里面装了0X00000001。 我们要对pi取值时,就用 * pi,这时候取出的值不是 0X00000002里装的值(0X00000001),而是0X00000002里面的值所指向的值,*pi == 100, *取值中间是隔了一层的,因此我们说*是间接操作符。

(3)另外,当声明int * pi时, 是没有对pi赋值的, 我们继续假定0X00000002的外号是pi,当int * pi时,并没有在0X00000002里面放入任何东西,这种情况下0X00000002里面可能是空的,也可能存在着未知的东西,因此我们不能随意给*pi赋值,如果这样做了就等于给一个没有确定的地址写数据,这样的危害可大可小,小则没事,大则程序崩溃。例如,0X00000002在没有赋值的前提下,它的数据可能是0X00000001(当然这有无数可能),而0X00000001里面装着程序的重要数据,我们在不期待的情况下给0X00000002赋值,就会修改了0X00000001的数据。

(4)指针的另外一个操作符 & ,这个却是个翻译操作符,给定一个变量,"&变量"能将变量翻译成地址,就如你有绰号时,用"&绰号"可以得出真名。这个&的运行原理,是根据编译的一个表去对号入座的,每一个符号在内存里都有一席之地,编译时会对这些做好记录,当你要使用&,就会对照这个记录给出关联的席位(地址)。
 

2. 实验 
   
#include <stdio.h>

int main(void)
{
    int i = 10;

    int * pi;

    printf("i take the place of  %p\n", &i);

    printf("pi take the place of  %p\n", &pi);

    printf("i data is %d\n", i);

    printf("pi data is %p\n", pi);

    pi = &i;

    printf("pi take the place of %p\n", &pi);

    printf("pi data is %p\n", pi);

    printf("pi mean %d\n", *pi);

    return 0;

}    

 编译后运行结果如下:

i take the place of  0x7ffd94da2c4c      i是0x7ffd94da2c4c的外号,代表的是0x7ffd94da2c4c,如人名代表的是人
pi take the place of  0x7ffd94da2c40     pi是0x7ffd94da2c40的外号
i data is 10                             存储在i里面,0x7ffd94da2c4c这个内存地址空间里的是数字 10
pi data is 0x7ffd94da2d30                没有赋值前,pi里面的数据是0x7ffd94da2d30,即0x7ffd94da2c40空间内是0x7ffd94da2d30
pi take the place of 0x7ffd94da2c40      通过pi=&i赋值后,pi所代表的空间编号没有发生变化,发生变化的是里面的数据
pi data is 0x7ffd94da2c4c                pi里面装的是0x7ffd94da2c4c,它是一个里面装有int数值10的数据地址
pi mean 10                               通过间接运算符*取pi的值,得到的正是我们期待的值。
     
3. 进阶

    通过以上的学习我们基本上明白了int * p,  和 &p 是什么意思了, 下面我们来进一步区分 const char *p,  char * const p, 注意:const char  和 char const 其实是一样的,const都是对char进行修饰。

    (1) const char *p 表示  char is const,  根据上面的抽屉比喻, p是一个抽屉名, char则为抽屉里的抽屉的东西(指针是间接,所以要隔一个抽屉), 而const则限定了抽屉里只能为不变的东西,何为不变呢? 假定抽屉装了五个苹果,如果用const去限定了,则不能增加和减少这数量,更不能把苹果换成雪梨。虽然char不能改变,但我们却是可以改变p的值, 假如p里面装的是1号抽屉,我们可以改装2号抽屉, 如: <1>const char * p; char g[] = "GG"; p = g;  <2>char m[] = "MM";  <3>p = m  这样p就由GG变成了MM。  这就是const char *p的理解。


    (2)我们很容易理解 char * const p, 意思就是 p 的指向(p里面装的值)不能变更了,如p里面装着1号抽屉,那不成改装2号、3号、4号等等抽屉了,而至于1号抽屉里装什么东西,这却是可以变更的,只要是装着char类型的东西就可以,例如装着2个苹果可以加装3个、4个、5个······ 但是不能将苹果改为装雪梨。


    (3) 依此类推,我们可以掌握 const char * const p等声明的含义。

4. 不得不说的指针与数组

     (1)什么是数组呢? 数组就是内存里的一段固定的连续的区域,固定说的是位置已定,用抽屉去解释就是编号连在一起的抽屉群,从上而下(相对来说,可从左到右、从下到上)相叠在一块的N个抽屉,且里面装都是同一类型物品,我们称它们为组。

     (2)我们知道指针是某一块内存的别名,而数组又是内存的N个块的组合,两者都和内存地址有关,这就撵上了关系啦。我们可以把指针指到数组里,如果指针里装着的刚好是数组的开头元素的内存地址,我们就可以把这个指针和这个数组对等起来,而实际上我们也是这样定义,指针变量可以等于数组的名字, 如char a[5];  char * p;  然后 p = a , 不过,如果a = p反过来赋值却是不可以的, 因为数组的位置已经固定,是个常量,而指针是变量,我们不能够把变量赋值给常量,即是说可以变化的东西不能赋给不变的,比如"面包=包粉",是不可以的,因为面包做不了面粉,但"面粉=面包"却是允许的,这就是常量和变量的区别。我们不要奇怪变量p可以p++, 常量a却不能a++,因为++是个增量操作符,是对它的左值或者右值进行修改操作的符号,只适用于变量

      (3)数组的名字是数组所在的内存空间的地址名的外号,因此数组的第一个元素的地址和它的名字是所代表的地址一样,如: 由 int i[4] 得出 i = &i[0],我们再声明一个指针 int * p,  将i赋值p,指针p就可以顶着数组i的名头办事。

    (4)我们通过声明int * p , 然后将int[n]数组的地址赋给 p,这是因为两者类型相同。当我们遇到的是二维数组或者更多维数组时,指针的声明就必须要与数组的类型、长度都相同,(这里的长度是指数据占有的内存空间),比如我们要将int i[2][4]的地址赋给一个指针,首先我们要理解i[2][4]含义——是一个有两个元素的数组,每个元素都包含了4个int值,即是说这个数组的长度是4个int。所以我们声明指向这个数组的指针的长度必须是4个int, 声明如下: int (* p) [4], 为什么要用括号将(*p)括着?是因为[]的优先级高于*,如果没有括号(),p就会先和[4]结合,这就变成了声明的是一个指针数组,两者区别是: (*p)[4]只个指针变量, 而*p[4]有4个指针变量。 (*p)[4]内存只需要保留一个空间来存放包含有4个int值的内存地址,就是说只需要保留一个抽屉位,而*p[4]是需要预留4个抽屉的。
     (5)一维数线可用线性表示,二维就是一个平面,三维则是立体,至于四维、五维这些多维度的数组就很难想象和理解了,所以非不必要千万不要使用超过三维的数组。

5. 空指针和内存泄漏

    (1) 空指针是指:"指针的指向还没有确定"。明确的空指针会直接把指针设为NULL,但现实中会出现很多不明确的声明。如:char *p ; 编译后内存只会将一个内存地址映射为p,而这个p里面究竟装着什么东西,就无法确定,这需要你做下一步的指向工作,如: char e[] = "我思故我在”;  char *p ;  p = e ; 这样p就有了明确的指向。在未确定p里面是什么前,我们千万不要往p里写东西,如:char * p; 然后直接 *p="XX",这是错误的,不过这样声明却是可以的: char * p = "XX"。

    (2) 内存泄漏是指:"某个区域的内存空间被遗忘了"。这个遗忘有如“几十个中抽屉中的一个藏着一百元,但却无法找到”般悲哀。当然这100元你可能在以后的某个时间里找了出来,但在当前你想要用时,却没得用,这是种痛苦。内存的泄漏也一样,一段已给分配给你的内存却忘记使用,白白的浪费,它只能在程序退出后被系统回收去。导致内存泄漏的原因的前提是自已想管理内存,如通过malloc申请得到了一段内存空间,为了记忆,你必须给这空间起个名字,比如叫 swap, swap跟你一段时间后,你觉得不够用了,于是又申请了一段空间,你习惯性把新分到的空间又命名为swap,这就造成前一次申请的空间丢失,因为所有需要写入或修改的数据都写入这个新swap中,旧的swap在程序后面的日子被闲置和遗忘。像电话号码,你把某人的号码忘记了,又或者某人的号码被他人用了,导致你再也找不到某人。

   (3)内存泄漏的危害远远不是忘记这么简单,它会令到程序崩溃,更为可怕的是你永远不知道究竟什么原因导致崩溃,因为内存泄漏的排查是一项很艰辛的工作,编译器是不会直接告诉你这是内存泄漏。

6. 指针运算

   (1)指针是可以进行加减运算的,和指针进行运算的必须是整数,而且指针加减后所得的结果是和指针指向类型有关联的,如int * p,p+1的结果是p加上int所占的字节,这个字节数可能是4、8、16,不同体系有不同定义,反正指针的加减等于指针和指针所指类型的字节和整数的乘积的加减。
   (2)指针变量可以进行自增减运算,如int * p ; p++, p--,--p,++p等是可以进行的。
   (3)指向同一数组内元素的指针是可以进行差值运算的,如: int i[20]; int * p1, * p2; p1=&i[1]; p2=&i[5]; p2 - p1是可以运算的。
    (4) 具有相同类型的指针值,可以用关系运算符进行比较。

7. 练习

   声明一个数组:int i[4][2] = {{2,4},{6,8},{1,3},{5,7}};
   分析:
   1.  i 是什么?
   2.  i + 2 ?
   3.  *(i+2) ?
   4.  *(i+2)+1 ?
   5.  *(*(i+2)+1) ? 
   
   答:  1. i 是二维数组i[4][2]的第一个大小为2个int元素的地址
              
             2.    i+2 是二维数组i[4][2]的第三个大小为2个int元素的地址    

             3.    *(i+2)   是(i+2)地址里的值,也是一个地址,它是数组第三个元素里的第一个int值的地址,即是&i[2][1]

             4.    *(i+2) + 1  是数组里第三个元素的第二个int值的地址,即是&i[2][2]

             5.    *(*(i+2)+1)  是数组里第三个元素的每二个int值的值,即是i[2][1] 的值。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值