深入浅出C指针

http://bbs.9ria.com/blog-164422-18039.html

初学者在学习C语言时,通常会遇到两个瓶颈,一个是“递归”,一个是“指针”。大学老师在讲述这两个知识点时通常都是照本宣科,而没有站在一个初学者的角 度来审视问题,更没有剖析其内部机理。本人在此将发表一系列技术文章,希望能将C语言中“指针”这一概念讲述清楚,希望初学者能从中收益。在此笔者也极力 推荐Kenneth A.Reek写的《Pointers On C》这本书。
 1.内存和地址
初学者面对内存一词时总是有一种既陌生又熟悉的感觉。首先,在日常生活中大家总是会讨论某某设备内存有多大,是不是该加一个内存条等等。但是,内存究竟是 什么?在此,笔者并不想深入探讨内存的本质,以及内存在不同操作系统上的结构有什么区别等等这一系列问题。这里只希望初学者知道以下三个问题即可:
 (1)内存是计算机在运行过程中存储数据的地方
 (2)内存被分割成了“无数”个小区域,每个区域的大小在不同的环境下有所不同,可能是1个字节,2个字节,或者多个字节。
 (3)每个小区域都有一个独一无二的标识,即我们后面所说的地址(指针)。
 (4)每个小区中都有包含一个值,可以是整形,浮点型,字符型等等。
我们可以形象的用下图表示内存的结构:
    100           104           108           112             116
     |                   |                 |                 |                   |
------------------------------------------------------------------------
   (542)         (3.14)         (‘A’)     (12323)        (-12)
如图所示,上方表示内存的地址,下方括号中的内容表示该地址下内存中的值。我们想访问这些内存中的值,只需要知道内存地址即可。但是,我们怎么知道我们要 访问的内容其内存地址是多少?的确,要记住这些内存地址几乎是不可能的,所以,编译器可以让我们用变量的形式访问他们,于是这张图可以变成如下所示:
 total_count      Pi            m_ch         money           number
     |                      |                  |                 |                       |
------------------------------------------------------------------------
   (542)         (3.14)         (‘A’)     (12323)        (-12)
我们将内存地址变成了我们更容易记住的变量,这样我们在编写程序的时候就可以方便的访问内存中的数据。但是,请记住,这只是编译器帮助我们进行了优化,而 真正编译后的机器码则是通过真正的内存地址来访问内存中的数据,即寻址,关于计算机的寻址过程,有兴趣的读者可以参考计算机组成原理或者汇编语言等书。


2.值和类型
我们首先来看一下下面这几个表达式:
int  total_count = 542;
float Pi = 3.14;
int *p1 = &total_count;
char *p2 = &m_ch;
前两个表达式很好理解,我们申明了一个整形和一个浮点型变量,并分别赋值为542和3.14。那后面两个表达式是什么意思呢?
我们姑且可以简单记住:在申请变量的表达式中,如果类型的后面出现了*号,那么这个变量就叫做指针,或者指针变量。指针变量分为很多种类型,例如整形指针,浮点型指针,字符型指针等等。
好了,我们知道了指针的概念,那么,指针到底是什么?
指针也是变量,即指针变量,它和其他的变量在本质上是没有区别的。但是指针变量只能保存一种值,就是地址。
也许你会问了,既然指针保存的是地址,那么为什么要将指针分为那么多的种类,整形值要有整形指针,字符型值要有字符型指针?为什么不能用一种指针就把所有的地址都包含了呢?难道不同类型变量的地址也不同吗?
这个问题,本人没有查阅过官方的解释,首先可以肯定,内存地址在理论上是没有任何区别的,无论是用来保存什么类型变量的内存,其本质都是01Bit构成的 区域,内存地址当然也不会有任何区别,从纯技术技术的角度上讲,笔者认为编译器完全可以建立一种制度,用统一的指针类型保存不同变量的地址,这完全不会影 响程序的运行。但是编译器没有这样做的原因,笔者分析主要是出于安全性的考虑。当程序员有意或无意的将两种不同类型的指针所指向的内存内容进行赋值时,如 果编译器事先不能做出检查,那么也许会在程序运行过程中出现异常,例如产生非常严重的缓冲区溢出错误。


3.指针的间接访问符
好了,我们将问题回到这几个表达式上来。现在的问题是p1和p2这两个变量中保存的是什么。拿p1为例,也许你可以这样理解,因为p1是指针变量,所以他 的值应该和他指向的内存中的值一样,所以p1 = 542. 这样理解看似非常符合逻辑,但却是一个大的错误。虽然p1很特殊,但是指针变量也是变量,它不会聪明到自动去完成一个非常复杂的自动间接访问操作。p1的 值实际是100,即变量total_count的地址。但是读者请注意,100并不是传统意义上整形100,例如 int *p1 = 100;这是一个错误的表达式,因为不能直接将整形值赋值给指针变量,作如下变化即可 int *p1 = (int *)100; 
也就是说,如果我们输出p1的值,打印出来将会是一串莫名其妙的数字。当然,我们通常对这些莫名其妙的地址不感兴趣,我们更感兴趣的是这些地址背后影藏这怎样的信息。所以我们可以这样来访问int count = *p1;
等等...读者读到这里也许有些糊涂了,
int *p1 = &total_count; int count = *p1; 这么多的*号和&号也许读者有些搞不清了。我们来理清一下思路。
在int *p1 = &total_count;中:
 *p1只是一个标识,代表p1是一个指针,以后访问这个指针时直接p1即可,访问得到的值是一个地址编码。
&total_count代表一个地址,在任意一个变量前(包括指针)加上&符号,都将代表这个变量所在内存的地址。&p1则代 表指针所在内存的地址,即指针的指针(稍后详细介绍)其实这么有什么可疑问的,因为指针本身也是变量,所以和其他变量本质上没有任何区别。
在 int count = *p1;中,p1和前面说的一样,访问它的值是一个地址,而*号与之前的*号有些不同,之前的*号只是一个标识,表示当前申请的变量是一个指针,而这里 的*号我们给它一个新的名字,叫指针的间接访问符。听起来有些别扭,简单地理解就是,在一个指针变量前加上*号则可以访问该指针所所表示内存的实际值。
这里可以教初学者一个小技巧,当利理解一个和指针有关的语句是,“指针”和“地址”这两个词可以互换,初学者姑且可以认为“指针”就是“地址”,“地址”就是“指针”,这样理解不确切,但却很实用。
好了,下面让我们看一个更复杂的例子:
int *p1 = &total_count;
int count2 = *(*(&p1));
请读者在10秒钟之内告诉我count2的值是多少。哈哈,好吧,让我们来一点点分析。
从括号最里面开始:
p1是一个指针,他的值是total_count的地址;
&p1是一个地址,即指针p1的地址,我们用刚才的小技巧来看看,即地址p1地址,即指针p1的指针。
*(&p1)是一个值,这个值是&p1表示的地址所在内存的值实际上就是p1的值,&对p1进行了一次引用操作,*对其再解引用,实际上没有变化。此时*(&p1)的值为total_count的地址。
*(*(&p1))为total_count的值,也就是542.
到此,该表达式的分析结束,不过在实际编程中,没有人会用如此复杂的表达式进行编码。


4.指针的指针
指针的指针,即地址的地址。第一个地址是狭义上的地址,该地址实际上已经被“值”化,第二个地址是我们传统意义上的地址。这句话理解起来有些困难,我们先来看看下面的表达式;
int *p1 = &total_count;
int **p2 = &p1;
第一个表达式的意义我们已经清楚了,我们申请了一个指针变量,或者说是地址变量,它保存的是total_count的地址。
第二个表达式我们来一步步分析,p1是一个指针,即一个地址,&为地址符,加起来就是地址的地址,或者说指针的指针。而int **p2 我们可以这样理解 int *X,我们定义了一个指针变量X ,X为*p2,即X也是一个指针变量,加起来的意思就是我们申请了一个p2指针变量,这个指针变量指向的也是一个指针变量。
同理: int ***p3 = &p2;
       int ****p4 = &p3;
      ......都是成立的表达式,不过在实际编码中,遇到的最多情况是指针和指针的指针这两种情况。

posted @ 2013-08-16 16:53  若 ♂ 只如初见  阅读(276)  评论(0编辑  收藏  举报