C 语言指针详解

//C 语言指针详解//
一.指针:
在信息工程中指针是一个用来指示一个内存地址的计算机语言的变量或中央处理器(CPU)中寄存器(Register)。指针一

般出现在比较近机器语言的语言,如汇编语言或C 语言。
面向对象的语言如Java 一般避免用指针。指针一般指向一个函数或一个变量。在使用一个指针时,一个程序既可以直

接使用这个指针所储存的内存地址,又可以使用这个地址里储存的变量或函数的值。

二.指针与C 语言:
c 语言之所以强大,以及其自由性,很大部分体现在其灵活的指针运用上。因此,说指针是c 语言的灵魂,一点都不

为过。同时,这种说法也让很多人产生误解,似乎只有c 语言的指针才能算指针。basic 不支持指针,在此不论。其

实,pascal 语言本身也是支持指针的。从最初的pascal 发展至今的object pascal,可以说在指针运用上,丝毫不会

逊色于c 语言的指针。

三.内存分配表:
计算机中的内存都是编址的,就像你家的地址一样。在程序编译或者运行的时候,系统(可以不关心具体是什么,可

能是编译器,也可能是操作系统)开辟了一张表。每遇到一次声明语句(包括函数的传入参数的声明)都会开辟一个

内存空间,并在表中增加一行纪录。
记载着一些对应关系。

Declaration | ID Name Address Length
int nP; | 1 nP 2000 2B
char myChar; | 2 myChar 2002 1B
int *myPointer; | 3 myPointer 2003 2B
char *myPointer2; | 4 myPointer2 2005 2B

四.是一个整数:
指针,是一个无符号整数(unsigned int),它是一个以当前系统寻址范围为取值范围的整数。32 位系统下寻址能力

(地址空间)是4G-byte(0~2^32-1)二进制表示长度为32bit(也就是4B)。int 类型也正好如此取值。

例证(一)就是程序1 得到的答案和程序2 的答案一致。(不同机器可能需要调整一下pT 的
取值。)
程序1
#include <stdio.h>
main()
{
char *pT;
char t='h';
pT=&t;
putchar(*pT);
}
程序2
#include <stdio.h>
main()
{
char *pT;
char t='h';
pT=(char *)1245048;
putchar(*pT);
}
加上(char *)是因为毕竟int 和char *不是一回事,需要强制转换,否则会有个警告。因为char *声明过的类型,一

次访问1 个sizeof(char)长度,double *声明过的类型,一次访问1个sizeof(double)长度。

在汇编里int 类型和指针就是一回事了。因为不论是整数还是指针,执行自增的时候,都是其值加一。如果上文声明

char *pT;,汇编语言中pT 自增之后值为1245049,可是C 语言中pT++之后pT 值为1245049。如果32 位系统中, s 上

文声明int *pT;,汇编语言中pT增之后值为1245049,可是C 语言中pT++之后pT 值为1245052。为什么DOS 下面Turbo

C,和Windows 下VC 的int 类型不一样长。因为DOS 是16 位的,Windows 是32位的,可以预见,在64 位Windows 中

编译,上文声明int *pT;,pT++之后pT 值为1245056。

例证(二)
那么,复杂的结构怎么分配空间呢?C 语言的结构体(汇编语言对应为Record 类型)
按顺序分配空间
int a[20];
typedef struct st
{
double val;
char c;
struct st *next;
} pst;
pst pT[10];
在32 位系统下,内存里面做如下分配(单位:H,16 进制);
变量2000 2001 2002 2003 2004 2005 2006… 204C 204D 204E 204F
地址a[0] a[1]… a[19]
变量2050 2051…2057 2058 2059 205A 205B 205C 205D 205E 205F
地址pst.val pst.c pst.next 无效无效无效
这就说明了为什么sizeof(pst)=16 而不是8。编译器把结构体的大小规定为结构体成员大小最大的那个类型的整数倍

。至于pT 的存储,可以依例推得。总长为160,此不赘述。
有个问题,如果执行pT++,答案是什么?是自增16,还是160?别忘了,pT 是常量,不能加减。所以,我们就可以声

明:
typedef struct BinTree
{
int value;
struct BinTree *LeftChild;
struct BinTree *RightChild;
} BTree;
用一个整数,代表一棵树的结点。把它赋给某个结点的LeftChild/RightChild 值,就形成了上下级关系。只要无法找

到一个路径,使得A->LC/RC->LC/RC...->LC/RC==A,这就构成了一棵二叉树。反之就成了图。

五.C 按值传递
概述
C 中函数调用是按值传递的,传入参数在子函数中只是一个初值相等的副本,无法对传入参数作任何改动。但实际编

程中,经常要改动传入参数的值。这一点我们可以用传入参数的地址而不是原参数本身,当对传入参数(地址)取(*

)运算时,就可以直接在内存中修改,从而改动原想作为传入参数的参数值。编程参数值
#include <stdio.h>
void inc(int *val)
{ (*val)++; }
main()
{
int a=3;
inc(&a);
printf("%d" , a);
}
在执行inc(&a);时,系统在内存分配表里增加了一行“inc 中的val”,其地址为新地址,值为&a。操作*val,即是在

操作a 了。
六.*和&运算:
为了(*p)操作是这样一种运算,返回p 的值作为地址的那个空间的取值。(&p)则是这样一种运算,返回当时声明p

时开辟的地址。显然可以用赋值语句对内存地址赋值。我们假设有这么两段内存地址空间,他们取值如下:(单位:H

,16 进制)
地址0000 ... 2000 2001 2002 2003 2004 ... 3000 3001 3002 3003 ...
取值... 01 30 00 00 30 00 03 20 9A
假设有这么一段代码:(假设开辟空间时p 被分配给了3001H、3002H 两个位置)
int *p;
p=2003;
*p=3000H
**p 的值为多少?
**p=*(*(p))=*(*(2003H))=*(3000H)=0300H。
那么&&p、*(&p)和&(*p)又等于多少?
&&p=&(&(p))=&(3001H) , 此时出错了, 3001H 是个常数怎么可能有地址呢?
*&p=*(&(p))=*(3001H)=2003H,也就是*&p=p。
&*p=&(*(p))=&(3000H),读者可能以为&*p=p 此时出错了,3000H 是个常数怎么可能
有地址呢?

七.另类*和&:
两个地方要注意:
在程序声明变量的时候的*,只是表明“它是一个整数,这个整数指向某个内存地址,一次访问sizeof(type)长度”。

这点不要和(*)操作符混淆;在C++程序声明变量的时候的&,只是表明“它是一个引用,这个引用声明时不开辟新空间

,它在内存分配表加入新的一行,该行内存地址等于和调用时传入的对应参数内存地址”。这点不要和(*)声明符,

(&)操作符混淆。清华大学软件学院免试研究生面试就有问过这个问题。

八.双重指针
双重指针又是怎么一回事儿呢?综合2 的BTree 定义,和3 的说法。对于一棵树,我们通常用它的根节点地址来表示

这棵树。这就是“擒贼先擒王”。找到了树的根,其每个节点都可以找到。但是有时候我们需要对树进行删除节点,

增加节点操作,往往考虑到删除根节点,增加的节点取代原来的根节点作为新根节点的情况。为了修改根节点这个“

整数”,我们需要退一步,使用这个“整数”的内存地址,也就是指向这个“整数”的指针。在声明时,我们
用2 个*号,声明指向指针的指针。它的意思是“它是一个整数,这个整数指向某个内存地址,一次访问sizeof(int)

长度,其值是一个整数,那个整数值指向某个内存地址,一次访问sizeof(BTree)长度。”。详见<数据结构>有关“树

”的程序代码。

九.指针数组、数组指针和指向函数的指针:
【指针数组】就是一个由指针组成的数组,那个数组的各个元素都是指针,指向某个内存地址。eg:
char *p[10];//p 是一个指针数组。
【数组指针】数组名本身就是一个指针,指向数组的首地址。注意这是一个常数。eg:
char (*p)[10]//p 是一个数组指针。
【函数指针】本身是一个指针,指向一个函数入口地址,通过该指针可调用其指向的函数,使用函数指针可实现回调

函数。eg:
#include <stdio.h>
void inc(int *val) { (*val)++; }
main()
{
void (*fun)(int *);
int a=3;
fun=inc;//fun 是一个函数指针
(*fun)(&a);
printf("%d" , a);
}
【指针函数】本身是一个函数,其返回值是一个指针。eg:
void * fun(void);// fun 是一个指针函数

十.指针的作用:
指针可以用来有效地表示复杂的数据结构,可以用于函数参数传递并达到更加灵活使用函数的目的.使C 语言程序的设

计具有灵活、实用、高效的特点。

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。要搞清一个指针需要搞清指针的四方面的

内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区。让我

们分别说明。先声明几个指针放着做例子:
例一:
(1)int*ptr;
(2)char*ptr;
(3)int**ptr;
(4)int(*ptr)[3];
(5)int*(*ptr)[4];
指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有

的类型。让我们看看例一中各个指针的类型:
(1)int*ptr;//指针的类型是int*
(2)char*ptr;//指针的类型是char*
(3)int**ptr;//指针的类型是int**
(4)int(*ptr)[3];//指针的类型是int(*)[3]
(5)int*(*ptr)[4];//指针的类型是int*(*)[4]
怎么样?找出指针的类型的方法是不是很简单?指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看

待。

从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例

如:
(1)int*ptr;//指针所指向的类型是int
(2)char*ptr;//指针所指向的的类型是char
(3)int**ptr;//指针所指向的的类型是int*
(4)int(*ptr)[3];//指针所指向的的类型是int()[3]
(5)int*(*ptr)[4];//指针所指向的的类型是int*()[4]
在指针的算术运算中,指针所指向的类型有很大的作用。

指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C 越来越熟悉时,你会发现,把与指针搅和

在一起的"类型"这个概念分成"指针的类型"和"指针所指向的类型"两个概念,是精通指针的关键点之一。我看了不少

书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,所以看起书来前后矛盾,越看越糊涂。

指针的值,或者叫指针所指向的内存区或地址
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32 位程序里,所有类

型的指针的值都是一个32 位整数,因为32 位程序里内存地址全都是32 位长。指针所指向的内存区就是从指针的值所

代表的那个内存地址开始,长度为si zeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相
当于说该指针指向了以XX 为首地址的一片内存区域;我们说一个指针指向了某块内存区
域,就相当于说该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概

念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是
无意义的。以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?
该指针指向了哪里?

指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32 位平台里,指针本身占据了4 个

字节的长度。指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。指针的算术运算指针可以加

上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的。例如:
例二:
1、chara[20];
2、int*ptr=a;
...
...
3、ptr++;
在上例中,指针ptr 的类型是int*,它指向的类型是int,它被初始化为指向整形变量a。接下来的第3 句中,指针ptr

被加了1,编译器是这样处理的:它把指针ptr 的值加上了sizeof(int),在32 位程序中,是被加上了4。由于地址是

用字节做单位的,故ptr 所指向的地址由原来的变量a 的地址向高地址方向增加了4 个字节。由于char 类型的长度是

一个字节,所以,原来ptr 是指向数组a 的第0 号单元开始的四个字节,此时指向了数组a 中从第4 号单元开始的四

个字节。我们可以用一个指针和一个循环来遍历一个数组,看例子:

例三:
intarray[20];
int*ptr=array;
...
//此处略去为整型数组赋值的代码。
...
for(i=0;i <20;i++)
{
(*ptr)++;
ptr++;
}
这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr 加1,所以每次循环都能访问数组的下一个单元

。再看例子:
例四:
1、chara[20];
2、int*ptr=a;
...
...
3、ptr+=5;
在这个例子中,ptr 被加上了5,编译器是这样处理的:将指针ptr 的值加上5 乘sizeof(int),在32 位程序中就是加

上了5 乘4=20。由于地址的单位是字节,故现在的ptr 所指向的地址比起加5 后的ptr 所指向的地址来说,向高地址

方向移动了20 个字节。在这个例子中,没加5 前的ptr 指向数组a 的第0 号单元开始的四个字节,加5 后,ptr 已经

指向了数组a 的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵

活性。如果上例中,ptr 是被减去5,那么处理过程大同小异,只不过ptr 的值是被减去5 乘sizeof(int),新的ptr

指向的地址将比原来的ptr 所指向的地址向低地址方向移动了20 个字节。

总结一下,一个指针ptrold 加上一个整数n 后,结果是一个新的指针ptrnew,ptrnew 的类型和ptrold 的类型相同,

ptrnew 所指向的类型和ptrold 所指向的类型也相同。ptrnew 的值将比ptrold 的值增加了n 乘sizeof(ptrold 所指

向的类型)个字节。就是说,ptrnew 所指向的内存区将比ptrold 所指向的内存区向高地址方向移动了n 乘sizeof

(ptrold 所指向的类型)个字节。

一个指针ptrold 减去一个整数n 后,结果是一个新的指针ptrnew,ptrnew 的类型和ptrold的类型相同,ptrnew 所指

向的类型和ptrold 所指向的类型也相同。ptrnew 的值将比ptrold 的值减少了n 乘sizeof(ptrold 所指向的类型)个

字节,就是说,ptrnew 所指向的内存区将比ptrold所指向的内存区向低地址方向移动了n 乘sizeof(ptrold 所指向的

类型)个字节。

运算符&和*
这里&是取地址运算符,*是...书上叫做"间接运算符"。

&a 的运算结果是一个指针,指针的类型是a 的类型加个*,指针所指向的类型是a 的类
型,指针所指向的地址嘛,那就是a 的地址。
*p 的运算结果就五花八门了。总之*p 的结果是p 所指向的东西,这个东西有这些特点:它的类型是p 指向的类型,

它所占用的地址是p 所指向的地址。
例五:
3/29
第 6/14页
inta=12;
intb;
int*p;
int**ptr;
p=&a; //&a 的结果是一个指针,类型是int*,指向的类型是int,指向的地址是a 的地址。
*p=24;
//*p 的结果,在这里它的类型是int,它所占用的地址是p 所指向的地址,显然,*p 就是变
量a。
ptr=&p;
//&p 的结果是个指针,该指针的类型是p 的类型加个*,在这里是int **。该指针所指向的类型是p 的类型,这里是

int*。该指针所指向的地址就是指针p 自己的地址。
*ptr=&b;
//*ptr 是个指针,&b 的结果也是个指针,且这两个指针的类型和所指向的类型是一样的,所以用&b 来给*ptr 赋值

就是毫无问题的了。
**ptr=34;
//*ptr 的结果是ptr 所指向的东西,在这里是一个指针,对这个指针再做一次*运算,结果就是一个int 类型的变量


指针表达式
一个表达式的最后结果如果是一个指针,那么这个表达式就叫指针表式。下面是一些指针表达式的例子:
例六:
inta,b;
intarray[10];
int*pa;
pa=&a;//&a 是一个指针表达式。
int**ptr=&pa;//&pa 也是一个指针表达式。
*ptr=&b;//*ptr 和&b 都是指针表达式。
pa=array;
pa++;//这也是指针表达式。
例七:
char*arr[20];
char**parr=arr;//如果把arr 看作指针的话,arr 也是指针表达式
char*str;
str=*parr;//*parr 是指针表达式
str=*(parr+1);//*(parr+1)是指针表达式
str=*(parr+2);//*(parr+2)是指针表达式
由于指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型

,指针指向的内存区,指针自身占据的内存。好了,当一个指针表达式的结果指针已经明确地具有了指针自身占据的

内存的话,这个指针表达式就是一个左值,否则就不是一个左值。在例七中,&a 不是一个左值,因为它还没有占据明

确的内存。*ptr 是一个左值,因为
*ptr 这个指针已经占据了内存,其实*ptr 就是指针pa,既然pa 已经在内存中有了自己的位
置,那么*ptr 当然也有了自己的位置。

数组和指针的关系
数组的数组名其实可以看作一个指针。看下例:
例八:
intarray[10]={0,1,2,3,4,5,6,7,8,9},value;
...
...
value=array[0];//也可写成:value=*array;
value=array[3];//也可写成:value=*(array+3);
value=array[4];//也可写成:value=*(array+4);
上例中,一般而言数组名array 代表数组本身,类型是int[10],但如果把array 看做指针的话,它指向数组的第0 个

单元,类型是int*,所指向的类型是数组单元的类型即int。因此*array 等于0 就一点也不奇怪了。同理,array+3

是一个指向数组第3 个单元的指针,所以
*(array+3)等于3。其它依此类推。
例九:
char*str[3]={
"Hello,thisisasample! ",
"Hi,goodmorning. ",
"Helloworld "
};
chars[80];
strcpy(s,str[0]);//也可写成strcpy(s,*str);
strcpy(s,str[1]);//也可写成strcpy(s,*(str+1));
strcpy(s,str[2]);//也可写成strcpy(s,*(str+2));
上例中,str 是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str

当作一个指针的话,它指向数组的第0 号单元,它的类型是char**,它指向的类型是char*。
*str 也是一个指针,它的类型是char*,它所指向的类型是char,它指向的地址是字符串
"Hello,thisisasample! "的第一个字符的地址,即'H '的地址。str+1 也是一个指针,它指向数组的第1 号单元,它

的类型是char**,它指向的类型是char*。
*(str+1) 也是一个指针, 它的类型是char* , 它所指向的类型是char , 它指向
"Hi,goodmorning. "的第一个字符'H ',等等。
下面总结一下数组的数组名的问题。声明了一个数组TYPEarray[n],则数组名称array就有了两重含义:第一,它代表

整个数组,它的类型是TYPE[n];第二,它是一个指针,该指针的类型是TYPE*,该指针指向的类型是TYPE,也就是数

组单元的类型,该指针指的内存区就是数组第0 号单元,该指针自己占有单独的内存区,注意它和数组第0 号单元占
据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。
在不同的表达式中s数组名array 可以扮演不同的角色。
在表达式sizeof(array)中,数组名array 代表数组本身,故这时sizeof 函数测出的是整个
数组的大小。在表达式*array 中,array 扮演的是指针,因此这个表达式的结果就是数组第0 号单元的值。
sizeof(*array)测出的是数组单元的大小。表达式array+n(其中n=0,1,2,....。)中,array 扮演的是指针,故

array+n 的结果是

一个指针,它的类型是TYPE*,它指向的类型是TYPE,它指向数组第n 号单元。故
sizeof(array+n)测出的是指针类型的大小。
例十
intarray[10];
int(*ptr)[10];
ptr=&array;:
上例中ptr 是一个指针,它的类型是int(*)[10],他指向的类型是int[10] ,我们用整个数组的首地址来初始化它。

在语句ptr=&array 中,array 代表数组本身。本节中提到了函数sizeof(),那么我来问一问,sizeof(指针名称)测出

的究竟是指针自身类型的大小呢还是指针所指向的类型的大小?答案是前者。例如:
int(*ptr)[10];
则在32 位程序中,有:
sizeof(int(*)[10])==4
sizeof(int[10])==40
sizeof(ptr)==4
实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么类型的大小。指针和结构类型的关系
可以声明一个指向结构类型对象的指针。
例十一:
structMyStruct
{
inta;
intb;
intc;
}
MyStructss={20,30,40};
//声明了结构对象ss,并把ss 的三个成员初始化为20,30 和40。
MyStruct*ptr=&ss;
//声明了一个指向结构对象ss 的指针。它的类型是MyStruct*,它指向的类型是MyStruct。
int*pstr=(int*)&ss;
//声明了一个指向结构对象ss 的指针。但是它的类型和它指向的类型和ptr 是不同的。
请问怎样通过指针ptr 来访问ss 的三个成员变量?
答案:
ptr-> a;
ptr-> b;
ptr-> c;
又请问怎样通过指针pstr 来访问ss 的三个成员变量?
答案:
*pstr;//访问了ss 的成员a。
*(pstr+1);//访问了ss 的成员b。
*(pstr+2)//访问了ss 的成员c。
虽然我在我的MSVC++6.0 上调式过上述代码,但是要知道,这样使用pstr 来访问结构成员是不正规的,为了说明为什

么不正规,让我们看看怎样通过指针来访问数组的各个单元:
例十二:

intarray[3]={35,56,37};
int*pa=array;
通过指针pa 访问数组array 的三个单元的方法是:
*pa;//访问了第0 号单元
*(pa+1);//访问了第1 号单元
*(pa+2);//访问了第2 号单元
从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。所有的C/C++编译器在排列数组的单元时,总是把

各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种编译环境

下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干个"填充字节",这就导致各

个成员之间可能会有若干个字节的空隙。
所以,在例十二中,即使*pstr 访问到了结构对象ss 的第一个成员变量a,也不能保证*(pstr+1)就一定能访问到结构

成员b。因为成员a 和成员b 之间可能会有若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证

明了指针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节,嘿,这倒是个不错的方法。过

指针访问结构成员的正确方法应该是象例十二中使用指针ptr 的方法。指针和函数的关系可以把一个指针声明成为一

个指向函数的指针。intfun1(char*,int);
int(*pfun1)(char*,int);
pfun1=fun1;
....
....
inta=(*pfun1)( "abcdefg ",7);//通过函数指针调用函数。
可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为实参。
例十三:
intfun(char*);
inta;
charstr[]= "abcdefghijklmn ";
a=fun(str);
...
...
intfun(char*s)
{
intnum=0;
for(inti=0;i{
num+=*s;s++;
}
returnnum;
}
这个例子中的函数fun 统计一个字符串中各个字符的ASCII 码值之和。前面说了,数组的名字也是一个指针。在函数

调用中,当把str 作为实参传递给形参s 后,实际是把str 的值传递给了s,s 所指向的地址就和str 所指向的地址一

致,但是str 和s 各自占用各自的存储空间。在函数体内对s 进行自加1 运算,并不意味着同时对str 进行了自加1

运算。指针类型转换

当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式。在我们前

面所举的例子中,绝大多数情况下,指针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指

向的类型是一样的。
例十四:
1、floatf=12.3;
2、float*fptr=&f;
3、int*p;
在上面的例子中,假如我们想让指针p 指向实数f,应该怎么搞?是用下面的语句吗?
p=&f;
不对。因为指针p 的类型是int*,它指向的类型是int。表达式&f 的结果是一个指针,指针的类型是float*,它指向的

类型是float。两者不一致,直接赋值的方法是不行的。至少在我的MSVC++6.0 上,对指针的赋值语句要求赋值号两边

的类型一致,所指向的类型也一致,其它的编译器上我没试过,大家可以试试。为了实现我们的目的,需要进行"强制

类型转换
":
p=(int*)&f;
如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP*TYPE, 那么语法格
式是:
(TYPE*)p;
这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE*,它指向的类型是TYPE,它指向的地址就是原指针指

向的地址。而原来的指针p 的一切属性都没有被修改。一个函数如果使用了指针作为形参,那么在函数调用语句的实

参和形参的结合过程中,也会发生指针类型的转换。
例十五:
voidfun(char*);
inta=125,b;
fun((char*)&a);
...
...
voidfun(char*s)
{
charc;
c=*(s+3);*(s+3)=*(s+0);*(s+0)=c;
c=*(s+2);*(s+2)=*(s+1);*(s+1)=c;
}
注意这是一个32 位程序,故int 类型占了四个字节,char 类型占一个字节。函数fun 的作用是把一个整数的四个字

节的顺序来个颠倒。注意到了吗?在函数调用语句中,实参&a的结果是一个指针,它的类型是int*,它指向的类型是

int。形参这个指针的类型是char*,它指向的类型是char。这样,在实参和形参的结合过程中,我们必须进行一次从

int*类型到char*类型的转换。结合这个例子,我们可以这样来想象编译器进行转换的过程:编译器先构造一个临时指

针char*temp, 然后执行temp=(char*)&a,最后再把temp 的值传递给s。所以最后的结果是:s 的类型是char*,它指

向的类型是char,它指向的地址就是a 的首地址。我们已经知道,指针的值就是指针指向的地址,在32 位程序中,指

针的值其实是一个32 位整数。那可不可以把一个整数当作指针的值直接赋给指针呢?就象下面的语句:
Unsigned int a;
3/29
第 11/14页
TYPE*ptr;//TYPE 是int,char 或结构类型等等类型。
...
...
a=20345686;
ptr=20345686;//我们的目的是要使指针ptr 指向地址20345686(十进制

ptr=a;//我们的目的是要使指针ptr 指向地址20345686(十进制)编译一下吧。结果发现后面两条语句全是错的。那

么我们的目的就不能达到了吗?不,还有办法:
unsignedinta;
TYPE*ptr;//TYPE 是int,char 或结构类型等等类型。
...
...
a=某个数,这个数必须代表一个合法的地址;
ptr=(TYPE*)a;//呵呵,这就可以了。
严格说来这里的(TYPE*)和指针类型转换中的(TYPE*)还不一样。这里的(TYPE*)的意思是把无符号整数a 的值当作一个

地址来看待。上面强调了a 的值必须代表一个合法的地址,否则的话,在你使用ptr 的时候,就会出现非法操作错误


想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完全可以。下面的例子演示了把一个指针的

值当作一个整数取出来,然后再把这个整数当作一个地址赋给一个指针:
例十六:
inta=123,b;
int*ptr=&a;
char*str;
b=(int)ptr;//把指针ptr 的值当作一个整数取出来。
str=(char*)b;//把这个整数的值当作一个地址赋给指针str。
现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以把一个整数值当作
地址赋给一个指针。
指针的安全问题
看下面的例子:
例十七:
chars= 'a ';
int*ptr;
ptr=(int*)&s;
*ptr=1298;
指针ptr 是一个int*类型的指针,它指向的类型是int。它指向的地址就是s 的首地址。在32 位程序中,s 占一个字

节,int 类型占四个字节。最后一条语句不但改变了s 所占的一个字节,还把和s 相临的高地址方向的三个字节也改

变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常

重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这

会造成崩溃性的错误。让我们再来看一例:
例十八:
3/29
第 12/14页
1、chara;
2、int*ptr=&a;
...
...
3、ptr++;
4、*ptr=115;
该例子完全可以通过编译,并能执行。但是看到没有?第3 句对指针ptr 进行自加1 运算后,ptr 指向了和整形变量a

相邻的高地址方向的一块存储区。这块存储区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是

一条代码。而第4 句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使用指针时,程序员心里必须非常

清楚:我的指针究竟指向了哪里。在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则也会造

成类似的错误。在指针的强制类型转换:ptr1=(TYPE*)ptr2 中,如果sizeof(ptr2 的类型)大sizeof(ptr1的类型)那

么在使用指针ptr1 来访问ptr2 所指向的存储区时是安全的。如果sizeof(ptr2 的类型)小于sizeof(ptr1 的类型),

那么在使用指针ptr1 来访问ptr2 所指向的存储区时是不安全的。至于为什么,读者结合例十七来想一想,应该会明

白的。

指针右左法则
C 语言所有复杂的指针声明,都是由各种声明嵌套构成的。如何解读复杂指针声明呢?右左法则是一个既著名又常用

的方法。不过,右左法则其实并不是C 标准里面的内容,它是从C标准的声明规定中归纳出来的方法。C 标准的声明规

则,是用来解决如何创建声明的,而右左法则是用来解决如何辩识一个声明的,两者可以说是相反的。右左法则的英

文原文是这样说的:
The right-left rule: Start reading the declaration from the innermost
parentheses, go right, and then go left. When you encounter
parentheses, the direction should be reversed. Once everything in the
parentheses has been parsed, jump out of it. Continue till the
whole declaration has been parsed.

这段英文的翻译如下:
右左法则:首先从最里面的圆括号看起,然后往右看,再往左看。每当遇到圆括号时,就应该掉转阅读方向。一旦解

析完圆括号里面所有的东西,就跳出圆括号。重复这个过程直到整个声明解析完毕。笔者要对这个法则进行一个小小

的修正,应该是从未定义的标识符开始阅读,而不是从括号读起,之所以是未定义的标识符,是因为一个声明里面可

能有多个标识符,但未定义的标识符只会有一个。现在通过一些例子来讨论右左法则的应用,先从最简单的开始,逐

步加深:
int (*func)(int *p);
首先找到那个未定义的标识符,就是func,它的外面有一对圆括号,而且左边是一个*号,这说明func 是一个指针,

然后跳出这个圆括号,先看右边,也是一个圆括号,这说明(*func)是一个函数,而func 是一个指向这类函数的指针

,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是int。
int (*func)(int *p, int (*f)(int*));

func 被一对括号包含,且左边有一个*号,说明func 是一个指针,跳出括号,右边也有个括号,那么func 是一个指

向函数的指针,这类函数具有int *和int (*)(int*)这样的形参,返回值为int 类型。再来看一看func 的形参int

(*f)(int*),类似前面的解释,f 也是一个函数指针,指向的函数具有int*类型的形参,返回值为int。
int (*func[5])(int *p);
fnc 右边是一个[]运算符,说明func 是一个具有5 个元素的数组,func 的左边有一个*,
说明func 的元素是指针,要注意这里的*不是修饰func 的,而是修饰func[5]的,原因是[]运算符优先级比*高,func

先跟[]结合,因此*修饰的是func[5]。跳出这个括号,看右边,也是一对圆括号,说明func 数组的元素是函数类型的

指针,它所指向的函数具有int*类型的形参,返回值类型为int。
int (*(*func)[5])(int *p);

func 被一个圆括号包含,左边又有一个*,那么func 是一个指针,跳出括号,右边是一个[]运算符号,说明func 是

一个指向数组的指针,现在往左看,左边有一个*号,说明这个数组的元素是指针,再跳出括号,右边又有一个括号,

说明这个数组的元素是指向函数的指针。总结一下,就是:func 是一个指向数组的指针,这个数组的元素是函数指针

,这些指针指向具有int*形参,返回值为int 类型的函数。
int (*(*func)(int *p))[5];

func 是一个函数指针,这类函数具有int*类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有5 个

int 元素的数组。要注意有些复杂指针声明是非法的,例如:
int func(void) [5];
func 是一个返回值为具有5 个int 元素的数组的函数。但C 语言的函数返回值不能为数组,这是因为如果允许函数返

回值为数组,那么接收这个数组的内容的东西,也必须是一个数组,但C 语言的数组名是一个右值,它不能作为左值

来接收另一个数组,因此函数返回值不能为数组。
int func[5](void);
func 是一个具有5 个元素的数组,这个数组的元素都是函数。这也是非法的,因为数组的元素除了类型必须一样外,

每个元素所占用的内存空间也必须相同,显然函数是无法达到这个要求的,即使函数的类型一样,但函数所占用的空

间通常是不相同的。作为练习,下面列几个复杂指针声明给读者自己来解析,答案放在第十章里。
int (*(*func)[5][6])[7][8];
int (*(*(*func)(int *))[5])(int *);
int (*(*func[7][8][9])(int*))[5];
实际当中,需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害。应该用

typedef 来对声明逐层分解,增强可读性,例如对于声明:
int (*(*func)(int *p))[5];
可以这样分解:
typedef int (*PARA)[5];
typedef PARA (*func)(int *);

//完结///
老向执笔/
/2011年4月23

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值