我认为指针是C中的一种运作机制,它可以根据一个memory address来访问一个存储单元。CPU以byte为单位,为每个存储单元顺序编号,这个编号就被称为memory address。无论以任何方式来read/write memory都是基于address来实现的。 



变量名: 

int x; 

上面是一条最简单的"变量声明语句",声明x为int type。在这之后程序员可以根据x来访问它所代表的存储单元: 

x=12; 

上面的语句向x所代表的内存单元中赋入值12。变量名应该被理解为一个符号地址,它与一个memory address对应,会被编译器翻译成地址的。所以上面的语句大致可以这样来理解: 

0011aa02=12; 

把数值12保存到特定地址所代表的存储单元中,当然上面这个地址是我随便写的。 

大家可能都知道变量名是标识符的一种,在命名的时候必须按照标识符的规则来取名字。在很多编程语言中都规定标识符不能以0开头,但是在大多数的书中都没对这一点做解释。我个人在反复思索之后得出了一个结论,也许是错的。呵呵,那在下就在这里谈论一下吧。 

int result; 

int 0result; 

上面是2条声明,其中第二条语句中给定的变量名是以数值0开头的,这明显违反了构造规则。我想大部分编译器是会声称错误信息以提示程序员的。 

编译器在翻译标识符时,需要首先对标识符进行"解码"。我们知道在编辑source code的时候一切都被看作"字符",而不同的编译器将会根据不同的"字符集"来表示字符。 

当然我们可以在ascii表中找到一个数值,然后知道代表的是什么字符,当然也可以根据字符找到它的ascii码。在C中字符与整型的差别在大多数时候被忽视。一个char就是一个int,该char的value是它在字符集中的编码。 

在上面的高谈阔论之后,我们来看一个image: 


字符'0'的ascii码是48,我们可以确定第一个字符是一个数字。但是如何确定后面的所有数值序列是多个字符的ascii码还是一个字符的ascii码呢? 



指针变量是否是一个普通的变量: 

int a; 

int *a; 

上面还是2条声明语句,第一个声明了一个普通变量,第二个声明了一个指针变量。忽略掉memory address的概念,我们认为变量名是存储单元最主要的标识,并且在大多数时候它是存储单元唯一的标识。 

我们可以根据a来读或写它的内容: 

a=1; 

printf("%d",a); 

第一条语句把1保存到a所代表的存储单元中,而第二条语句输出a的内容。在这里大家发现了,a在不同的位置代表的含义不同,在第一条语句中它代表数值1,也就是a所标识存储单元的内容。而在第二条语句中它代表它的值。所以: 

printf("%d",a); 与 

printf("%d",1); 的效果是一样的。 

编译器的设计人员引入了"左值"和"右值"的概念来描述变量名的不同特性的关系。比如看下面的语句: 

a=b; 

a和b同样都是一个普通变量的名字,但是它们在上面的语句中各自代表的含义却是不一样的,a代表存储单元的地址,与一个memory address对应着,而b代表的则是它的内容,也就是一个value。这样的不同,在一定意义上说增加了C语言的灵活性。我们通常都会这样来理解,虽然它并不能完整的表达本质含义: 

a=b; //把b赋给a 

其实,在大多数时候你不需要关心左值和右值,当然,你必须要知道它们之间的区别。 



指针变量在大多数方面和普通的变量并没有任何区别,我认为它们的区别只有一处:可以对指针型变量进行"间接访问"运算,其结果就是执行了一次所谓的"指针"。就是根据一个变量的内容找到另一个变量。 

虽然很多程序员说:指针变量的内容是一个内存地址,而普通变量的内容是一个给定类型的值。 

但是,除非编译器可以根据变量的内容判断出它是否代表一个合法范围内的地址,并且以此来认定一个指针变量和普通变量的区别,否则这一说法纯属多余。 



如果: 

int a; 

int *p; 

如果a与p是函数体内的局部变量,它们将不会被初始化,所以它们的内容是其它程序使用过的data。 

|???| <-这个是a,当然你可以printf()来看到它的内容 

|???| <-这个是p,它的内容是不确定的,所以最好别对他执行*运算 



如果我们: 

p=&a; 

假如: 

|???| <-1001 

看上去1001是a的地址,执行了上面的语句,所以: 

|1001| <-p之前的内容被1001覆盖 

如果我们执行: 

printf("%d",*p); 

其效果和 

printf("%d",a); 是一样的。 

这就是pointer的间接访问机制了 


有一个pointer它指向另一个pointer: 
这样的指针被称为"多级指针",多级指针的value是另一个指针的address,这样我们可以多层次来访问最终的存储单元。 
比如: 
int a; 
int *p; 
int **pi; 
上面的多条语句,a是一个整型变量,而p则是一个指向整型变量的指针,那么pi是什么呢?pi的值可以是一个指向整型变量指针的address。 
int a=12; 
上面的语句给a变量赋值为12,但是如果: 
p=&a; 
我们就可以printf("%d",*p);,当然如果我们使用多级指针: 
pi=&p;我们就可以printf("%d",**pi); 
在上面的多次赋值以后一下的表达式结果是true: 
a==12 
a==*p 
a==**pi 
&a==p 
&a==*pi 
&p==pi 
对一个多级指针进行*运算可以得到一个指针的值,如果我们根据它的级数依次求值可以得到最终给定type的变量。其实多级指针并不复杂的。 

array and pointer有关系吗: 
在c中array这种特殊的type是怎么实现的呢?array中的all elem是顺序存储的,elem之间的关系只不过是依次的递增关系而已,除此之外没有任何关系了。 
int array[length]; 
在c中可以以上面的语法声明一个最简单的array,array中的elem type是int而这个array最多可以save length个elem。 
问题在于数组名它代表的是什么,数组名有时候是数组中第一个元素的首地址,但其实它是一个指针常量,因为之前提到过变量对应一个address,数组名也对应一个address,并且我们无法改变数组名的值,它并不是一个"左值". 
经过上面的阐述以后我们可以这样来完整的描述数组名到底是个什么东西: 
int *pointer=arrayname; 
上面的声明语句,声明了pointer是一个指向整型的指针变量,并用一个数组的名字为它初始化。上面的语句和下面的语句效果一样: 
int *pointer=1001a20b; 
我们把数组名用一个内存地址替换了,其实数组的名字就和普通变量一样,arrayname对应一个memory address。这个地址是数组的首地址,也就是下标为0元素的第一个字节的地址。 

下标 and 间接访问的区别: 
访问数组元素最常用也是最简单的方法就是通过下标来访问元素,比如这样做: 
array[下标]; 
在c中下标是从0开始到最大长度-1。我们应该如何来看待下标引用呢? 
array,我们可以把它理解为一个指针常量,它本身代表一个address,但是却无法被改写。而后面的下标是一个数值,这个数值大于等于0小于等于max-1。 
如果我们把下标看成在array合法范围内的偏移,这样就很好得到一个完整的公式来描述下标引用的实现过程: 
*(array+下标) 
用上面的方式来理解数组元素的表示是非常正确的。 
其实下标引用方式就是上面方式的简写形式而已,当然还是下标方式比较容易理解。 

数组名是首地址,然后在+上在数组内的偏移值就得到了数组元素的地址,然后我们对这个address进行*运算就得到了它的内容。这里大家要知道,其实编译器就是用: 
*(arrayname+index) 
的方式来解释: 
array[index]的。所以,大家想想,如果我们这样来引用下标,是否可以? 
index[array]; 
其实,是可以的,只要让index和arrayname相加,然后*运算就可以得到elem的,但是这并不建议使用,不容易理解。 

多维数组是如何实现的: 
在以前我把二维数组理解为一个方方正正的矩形,并且拒绝指针的方式来表示多维数组,这样导致了我无法理解大于2维数组的构造形式。 
当然,现在我已经完全接受了指针的方式来描述所有维的数组的构造方式。 
如果理解了2维数组,以后理解大于2维的数组将不会难,只不过要进行复杂的地址计算而已。 
它的元素是由pointer组成的,比如一个数组声明如下: 
int array[2][2]; 
以上声明了一个二维数组,它由2行2列的元素组成。 
int *array[2]; 
上面的数组的每个元素是一个int type pointer,这样我们该如何来访问它所指向的单元呢? 
**(array+下标) 
这样是不是可以呢。 
数组的第一维,你可以把他们看成一个独立的一维数组,然后我们在用第二个[]来引用这个数组的元素,所以: 
array[i][j] 就等于: 
*(*(array+i)+j) 下面我们来给这个稍微有点复杂表达式依次解释一下: 
array是这个数组的名字,我们在前面的段落说到了数组名字就是一个指针常量,它是数组第一个元素的首地址。 
所以如果我们array+i就得到了array中第i个元素的地址,然后对它进行*来间接访问,我们得到了这个元素的值,它的值应该被看成一个address。其实它是一个array name就是一个顺序内存的首地址,然后我们在如之前的表达式那样*(array+i)+j这样我们就得到了这个新数组的元素的地址,现在只需要在对这个元素进行*运算就可以得到它所代表的值了*(*(array+i)+j) 

声明 是否是它看上去的那样: 
在可执行代码之前,我们通常都要预先声明某些变量,然后在这之后要使用这些变量。下面是一个最简单的声明语法: 
datatype name=12; 
datatype是一个类型,也就是C中的普通类型,可以是:int long char short float... name是我们给这个变量取的一个名字,当然是要按照标识符的命名规则。=12 是一个赋值,也就是在声明这个变量之后把12保存到这个变量中。我们通常称“初始化变量”,也就是在声明的同时给一个初始值。 
然后我们在这之后就可以来使用这个变量了,可以向它保存一个新值: 
name=99; 
或者,我们可以输出它的值: 
printf("format",name); 
上面的format暂时表示C语言中的格式化符号。当然name也可以作为表达式的一部分来参加一些运算: 
a+name-b; 
我们通常是怎么来看到“声明”的呢? 
int a=12; 
初学者可能会这样来理解这个声明语句:声明变量a是整型,并且初始化为12。 
当然我用的描述语言比较逻辑性一点,大多数初学者的思维和语言更加的通俗! 简单! 
其实,C语言和其它大多数语言不同,C在被设计的时候就要求很高,很多细节上的实现比较清晰,比较合理。所以你可以看到一些C语言的书籍,他们的名字具有哲理性: 
C陷阱与缺陷 
C专家编程 
C和指针 
但是,在其它编程语言中大致也只能看到: 
XX语言程序设计 
XX语言案例经选 
XX语言读书笔记 
对于,C中的声明语句,我们应该怎么来看待它呢? 
C语言中的声明可以描述变量在使用时的实现过程,也就是说,我们可以根据自己的情况,声明一个自己的变量。根据它在使用时的过程来声明一个特殊的变量。我们应该把声明看成一个表达式,并且对这个表达式进行求值计算,然后把每一步骤的求值联系起来,就得到了这个变量在使用过程中的实现细节了: 
int (*a)[2]; 
上面是一条声明语句,我们把这个表达式分解如下: 
int //数据类型符 
()  //圆括号 
*  //间接运算符 
a  //标识符 
[] //下标引用符 
2//数组的长度 

这里我告诉大家,a是一个指向数组的指针,但是我们在使用这个指针的时候是怎样的呢? 
|1001| <- 1001是一个memory address,该地址是一个数组的首地址 
  (a) 
上面这个存储单元中的内容是1001,并且在内存地址1001处是一个数组的首地址。上面这个存储单元用一个变量名来标识,这个变量名是a。 
然后我们该怎么通过这个a来使用一个数组呢? 
|1001| <- a 
对上面这个单元执行*运算的结果就得到了: 
| ? | ? | <- 这个是一个数组的存储结构,数组中的元素是不确定的。 
(1001) 
1001是这个数组第一个字节的地址,也就是这个数组的首地址。我们已经知道对a进行*操作得到的是a所指向的单元的地址。但是这个地址是一个首地址,那我们又该怎么得到后续的元素呢,答案就是下标。 
1001+index 
1001这个入口地址在加上数组的下标,这个下标代表数组内的偏移,这样就得到元素的地址了,然后在执行*操作就得到这个元素的值: 
a[1]; 
等同与: 
*(1001+1) 
根据上面的a的使用过程的描述,下面我们在来看看之前的声明语句应该怎样来理解呢: 
int (*a)[2]; 
别忘记了把一个声明看成一个表达式,并且对其求值。我们要先找到其中的标识符a,其实在C中[]的优先级要比*优先级高,这就是加圆括号的原因了,我们要先得到a所指向的地址。 
(*a)  //获得a的内容所指向的地址,似乎在一个变量名前加*就让这个变量变成了"左值"。也就是说现在(*a) 被转换成1001这样一个地址: 
|1001| <- a 
a标识了这个存储单元,然后我们执行*a,似乎就得到了: 
| ? | ? | 
(1001) 
也就是用1001这个左值来替代原来的a. 
当我们得到了1001 这个数组首地址之后该做什么了呢? 
1001[index] 
也就是开始访问数组的下标了。在这个步骤执行完以后原来的声明就只剩下了: 
int 
那就是说:经过以上求值步骤以后,这个表达式最终返回一个int,就是一个int type的value。 

C语言对type的认识大家要知道,其实指针和数组的类型就是不同的,在实现的时候过程也不一样。所以我们说:类型决定了变量是如何使用的,所以有的时候看上去差不多,但是类型其实是不一样的,使用的细节也不一样。