日期:2014-05-16  浏览次数:20630 次

LINUX-C成长之路(七):数组与指针

谈到C语言编程,数组和指针是很多人的心头大石,总觉得它们是重点难点,重点是没错的,但绝不是什么难点,要说C语言的难点,客观地讲应该是带参宏,而数组和指针,概念浅显易懂,操作简洁方便,根本不是很多初学者想象的那么困难,所以一开始就要有充分的信心,其次,恰恰是因为它们的“方便”,导致如果一不小心会用错,所以数组和指针,尤其是指针,与其说它难,还不是说他容易用错,我们在使用的时候要格外小心。


指针和数组,都涉及一个核心概念,就是地址,因此,我们从内存的地址开始给大家理清问题。


内存是一个个的存储单元,每一个存储单元称之为一个字节(byte),一个字节有8位(即8bit)。我们存储数据的基本单位是字节,在32位的CPU架构下,最大能支持4G的内存,也就是1024 * 1024 * 1024 个字节,这些字节统统都要一个编号,用来方便访问它们,就像一幢大厦里面有很多房间,每个房间都有个门牌号,比如101,102,103,和201,202等等,不同房间的功能差别巨大,比如101是个会议室,102是个储物室,103可能是个厕所,功能千差万别,但是门牌号是一样的。


完全一样的道理,内存单元的每一个字节都有编号,这个编号就是该字节的“地址”,比如0x00000001, 0x 0000FFFF, 这里之所以没有用101和102,只不过是因为内存地址太多了,三位数不足以表达,实际上我们需要一个32位的二进制数来表达,或者8位的十六进制数来表达。反正,每一个字节都有一个地址。


好了,至此,我们明白了内存单元至少会有两个属性,一个是这个内存单元里面装的内容,比如一个整数,或者一个浮点数,或者一个字符,或者一个结构体,甚至是一段代码都可以,另一个属性是这块内存单元的地址,也就是门牌号。当这块内存单元包含很多字节的时候,我们拿最小的地址,也就是基地址作为整块内存的地址,也称为起始地址。


比如 int a = 100, 这个变量 a 就是一块内存,内存里面放的内容是 100, 而这块内存的地址是 &a 

再来 void f ( void ) { printf("helloworld"); } , 这个函数 f( ) 是一块内存,内存里面放的内容是一个打印 helloworld 的代码,而这块内存的地址是 &f


明白了内存单元的地址这个概念之后,要理解数组和指针,就很简单了。首先来谈谈数组。


在 C语言中我们是这样定义数组的: int  a [10] ; 

在上面的这个定义中,a 就是一个数组,是一个具有10个整型元素的数组,关键在于:这10个整型变量是一个挨着一个,紧密地排列在一起的,它们连成一片,我们将这整块内存起个名字,叫做 a。显然,由于每个整型变量的大小是 4 个字节,所以 整个数组的大小就是 4 * 10 = 40个字节。我们在来考虑 a 这个变量,这个变量的类型是 int [ 10] , 亦即 a 是一个具有10个整型元素的数组,那么它的值呢? 它的值就是 这块内存的基地址,也就是 第一个元素的地址。


下面是重点,不管你以前是如何理解数组的,请抛弃你头脑中所有模棱两可的概念,重新站在编译器的角度(是的,编译器的角度,不是我的角度)理解数组的定义:


当C编译器看到这样的定义语句:int a[10] 的时候,它会将这条语句拆分中两部分来看待,第一部分是 a[10] ,除此之外统统称为第二部分,在这里第二部分就是 int 

第一部分: a[10 ] ,这里确凿无误地告诉编译器,请你给我一块连续的内存,而且这块内存要包含10个元素在里面。 说完之后你是不是觉得少了一点什么呢? 对了,你还没说这10个元素是什么呢?? 你要10个粽子还是要10根葱啊? 得说明白,这就是第二部分的事情了。

第二部分:int ,这里确凿无误地告诉编译器,刚才那10个元素,既不是粽子也不是小葱,而是10个整型变量。 ok,一切明白,我们要的就是一块连续的内存,里面装有10个整型变量,我们将这样的内存称为数组,准确地讲,这是一个具有10个元素的整型一维数组。


问个问题,刚才我们的10个元素是 int ,那能不能是 float呢? 能不能是 char 呢? 能不能是结构体呢? 


答案是肯定的。下面再来从易到难再看两个例子:

int b[3][10]

有人看到以上定义可能会大叫:这是个二维数组! 是的,我们通常都会那么称呼它,但是现在咱们站在编译器的角度,编译器它可不认识什么二维数组,在它的法眼里,世界上只有一维数组,它实际上是这么看的: int (b[3]) [10];   


第一部分:b[3] ,确凿无疑地告诉编译器,请你给我一块连续的内存,而且这块内存要包含3个元素在里面。这3个元素是什么呢? 

第二部分:int [10] ,确凿无疑地告诉编译器,刚才那3个元素,既不是粽子也不是小葱,而是3个 int [10] 。 OK,一切明白,我们要的就是一块连续的内存,里面装有3个int [10] 变量,二 int [10] 是什么家伙呢? int [10] 就是上面说了半天的那个 int a[10], 准确地讲,这是一个包含了3个【具有10个整型变量的一维数组】的一维数组,这样说比较拗口,所以我们人为地发明了一个单词:二维数组。


再来一个例子: 

char *c[10];

因为方括号 [ ] 的优先级比星号高,因此这个定义语句要这么看: char * (c[10]) ;  编译器拿到这样的语句,毫无疑问地也会 分成两部分来分析:第一部分 c[10] ,因此这是一个具有10个元素的数组,那么这10个元素又是啥呢? 答案就是 第二部分: char * ,也就是说,这是一个存放了10个 char * 的数组,称之为 char 型指针数组,也就是专门用来存放 char * 的数组。


好了,数组先到此打住,再来看指针的定义,你会发现编译器原来是有一套既定的统一的规则的。


比如 int *p;

这个定义无比简单,就是定义了一个整型指针p,同样地不管你以前是怎么理解指针的,现在请你跟编译器站在一起,从它的角度来看看什么是指针,没错,我们又要将这个定义分成两部分了:


第一部分: *p   ,确凿无疑地告诉编译器,请你给我分配一块内存 p, 这块内存用来干嘛呢?因为p 的前面有个 星号,所以 p  既不是用来装猪饲料的,也不是用来装鸡蛋的,而是用来存放地址的! 前面已经说过,每一个字节都有一个编号,这个编号就是一个32位的二进制数,我们称之为该字节的地址,现在的这个 p,就是专门用来装地址的。既然是用来装地址的,那么要多大的变量才能装得下这个地址呢? 答案是在32位的系统里面需要4个字节,因为只有4个字节才能足以表达从 0b00000000 00000000 00000000 00000000 到 0b11111111 1111111 11111111 11111111 这样的内存单元地址。容易发现,每一个地址都是32位的一个二进制数,也就是需要4个字节来存放这个门牌号。


第二部分:int , 上面第一部分已经确凿无疑地知道了p是一个用来装地址的变量了,关键是那个地址所对应的内存是什么呢? 这个问题有第二部分来回答,int,说明 p 将来存放的地址所对应的内存是一个 int,换句话讲,p 是一个专门用来存放 int 型量的地址的,我们亲切地将 p 称为 int 型指针。


假如现在就有一个 int  型变量:  int  w = 100; 那么我们很自然地就可以将 w 的地址存放在 p 里面: p = &w ;这样,我们就说 p 指向了 w,如图:


现在明白了吧,所谓的指针,只不过就是用来装一个地址的内存而已,又因为我们可以将很多不同的量的地址交给指针来存储,所以又分为不同类型的指针,比如专门用来存放整型数据的地址的指针 int *p,我们把它称为整形指针,专门用来存放字符型数据的地址的指针 char  *q ,我们把它称为字符指针,专门用来存放某一种函数的地址的指针

int (*k)( ch