指针

维基教科书,自由的教学读本

概述[编辑]

信息工程中指针是一个用来指示一个内存地址的计算机语言变量中央处理器(CPU)中的寄存器(Register)。指针一般出现在比较近机器语言的语言,如汇编语言C语言面向对象语言Java一般避免用指针,而是引用。[1]指针一般指向一个函数或一个变量。在使用一个指针时,一个程序既可以直接使用这个指针所储存的内存地址,又可以使用这个地址里储存的变量或函数的值。

指针的实质[编辑]

指针就是一个整数[编辑]

计算机中的内存都是编址的,每个地址都有一个符号,就像家庭地址或者IP地址一样。指针,是一个无符号整数(unsigned int,因不致歧义,下简称“整数”),它是一个以当前系统寻址范围为取值范围的整数。声明指针和声明一个无符号整数实质上并无区别。

32位系统的寻址能力(地址空间)是4Gb(0~232-1)二进制表示长度为32比特,也就是4B。不难验证,在32位系统里,int类型也正好是4B(32-bit)长度,可以取遍上述范围。同理,64位系统取值范围为0~264-1,int类型长度为8B。

例证就是程序1得到的答案和程序2的答案一致。[2]

程序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);
}

指针和整数的区别[编辑]

既然指针的实质是一个整数,为何不用unsigned int直接声明,或者统一用int *声明,而要用不同的类型后面加上一个“*”表示呢?char *声明过的类型,一次访问1个sizeof(char)长度,double *声明过的类型,一次访问1个sizeof(double)长度。也正因此,程序2第6行加上“(char *)”是因为毕竟unsigned int和char *不是一回事,需要强制转换,否则会有个警告。

在汇编里,没有数据类型这一概念,整数类型和指针就是一回事了。不论是整数还是指针,执行自增的时候,都是将原值加一。如果上文声明char *pT;,汇编语言中pT自增(INC)之后值为1245049,可是C语言中pT++之后pT值为1245049。如果32位系统中,s上文声明int *pT;,汇编语言中pT自增之后值为1245049,可是C语言中pT++之后pT值为1245052。

为什么DOS下面的Turbo C,和Windows下的VC的int类型自增时的步进不一样长?因为DOS是16位的,Windows x86是32位的,int类型长度取决于操作系统的位长。可以预见,在Windows x64中编译,上文声明int *pT;,在执行pT++之后pT值为1245056。

指针的空间分配[编辑]

基本数据类型[编辑]

在程序编译或者运行时,系统[3]开辟了一张表。每遇到一次声明语句(包括变量的声明、函数的声明和传入参数的声明等等)都会开辟一个内存空间,并在表中增加一行纪录,记载一些对应关系。以32位操作系统下为例:

声明 序号 变量名 内存地址 访问长度
int nP; 1 nP 2000 4B 0xCCCCCCCC[4]
char myChar; 2 myChar 2002 1B 0xCC
int * myPointer; 3 myPointer 2003 4B 0xCCCCCCCC
char * myPointer2; 4 myPointer2 2005 4B 0xCCCCCCCC

高级数据类型[编辑]

那么,复杂的结构怎么分配空间呢?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++,答案不是自增,也不是160。因为数组声明时,pT是常量,不能加减。

指针连接的数据类型[编辑]

所以,我们就可以声明:

#typedef struct BiTree
{
    int value;
    struct BiTree *LeftChild;
    struct BiTree *RightChild;
} BTree;

用一个整数,代表一棵树的结点。把它赋给某个结点的LeftChild/RightChild值,就形成了上下级关系。只要无法找到一个路径,使得A->LC/RC->LC/RC...->LC/RC==A(A泛指某一结点),这就构成了一棵二叉树。反之就成了图。

使用指针的目的[编辑]

简化代码[编辑]

如果没有指针,我们很难用一个统一的模式去A的定位并修改一棵树的结点。例如:不用指针要修改A的左子树的左子树的右子结点,只有“A.LC.LC.RC=…”一种表达方式,不能通过赋值而简化。

参数传递[编辑]

C中函数调用是按值传递的,传入参数在子函数中只是一个初值相等的副本,无法对传入参数作任何改动。但实际编程中,经常要改动传入参数的值。这一点我们可以用一个小技巧,即传入参数的地址而不是原参数本身,当对传入参数(地址)取“*”运算时,就可以直接在内存中修改,从而改动原想作为传入参数的参数值。

#include <stdio.h>
void inc(int *val)
{
    (*val)++;
}
main()
{
    int a=3;
    inc(&a);
    printf("%d", a);
}

在执行inc(&a);时,系统在内存分配表里增加了一行“val@inc”,其地址为新地址,值为&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

然后,执行这么一段代码“int *p;”,假设开辟空间时p被分配3001H、3002H两个位置。则p为2003H,*p为3001H。

**p的值为多少?

**p=*(*(p))=*(*(2003H))=*(3000H)=0300H。

那么&&p、*(&p)和&(*p)又等于多少?

&&p=&(&(p))=&(3001H),此时出错了,3001H是个常数怎么可能有地址呢?

*&p=*(&(p))=*(3001H)=2003H,也就是*&p=p。

&*p=&(*(p))=&(3001H),读者可能以为&*p=p此时出错了,3001H是个常数怎么可能有地址呢?

指针和引用的声明注记[编辑]

我们再看看另类的*和&。这里有两个地方要注意:

(1)在程序声明变量的时候的*,只是表明“它是一个整数,这个整数指向某个内存地址,一次访问sizeof(type)长度”。这点不要和(*)操作符混淆;

(2)在C++程序声明变量的时候的&,只是表明“它是一个引用,这个引用声明时不开辟新空间,它在内存分配表加入新的一行,该行内存地址等于和调用时传入的对应参数内存地址”。这一点不要与“*”声明符和“&”操作符混淆。

指针的复杂形式[编辑]

双重指针(指向指针的指针)[编辑]

双重指针是指向指针的指针,它是一个整数,这个整数指向某个内存地址,该地址的值是一个整数,指向给另一个内存地址(通常异于前者,但不排除二者相等)。综合前述的BTree定义,对于一棵树,我们通常用它的根结点地址来表示这棵树。正所谓“擒贼先擒王”,找到了树的根,其每个结点都可以找到。

但是有时候我们需要对树进行删除结点,增加节点操作,往往考虑到删除根结点,增加的结点取代原来的根结点作为新根结点的情况。为了修改根结点这个“整数”,我们需要退一步,使用这个“整数”的内存地址,也就是指向这个“整数”的指针。在声明时,我们用2个*号,声明指向指针的指针。它的意思是“它是一个整数,这个整数指向某个内存地址,一次访问sizeof(unsigned int)长度,其指向的内存地址所存储的值是一个整数,那个整数值指向某个内存地址,一次访问sizeof(BTree)长度。”详见数据结构有关“树”的程序代码。

指针数组[编辑]

指针数组:就是一个整数数组,那个数组的各个元素都是整数,指向某个内存地址。

数组指针[编辑]

数组指针:数组名本身就是一个指针,指向数组的首地址。注意这是声明定长数组时,其数组名指向的数组首地址是常量。而声明数组并使某个指针指向其值指向某个数组的地址(不一定是首地址),指针取值可以改变。

指向函数的指针[编辑]

指向函数的指针:从二进制角度考虑,数组名是该数组数据段首地址,函数名就是该代码段的首地址,可以用“int *fun()”。在二进制层面,代码段和数据段什么区别?没什么区别。很多人都说C语言是一种面向过程的语言,因为它最多只有struct的定义,而没有class的概念。根据本段所述,我们可以认为C语言能成为面向对象的语言,只是表述比较麻烦而已。事实上很多开源程序都使用这种方式组织他们的代码。

#include <stdio.h>

void inc(int *val)
{
    (*val)++;
}

main()
{
    void (*fun)(int *);
    int a=3;
    fun=inc;
    (*fun)(&a);
    printf("%d", a);
}

参考[编辑]

注释[编辑]

  1. 实质上Java在传递对象的时候用的是按指针(这里认为指针和引用没有本质区别)传递,在传递基本类型(如int)时用的是按值(副本)传递。
  2. 不同机器可能需要调整一下pT的取值。
  3. 编译器决定了地址偏移量,操作系统决定了地址段的初始值和可访问长度。
  4. 声明后尚未赋值的变量,其值通常为两个1两个0相互交替的序列。

参考资料[编辑]

  • Wei Huang, 指针, 2008/10/26, (于2008年10月28日查阅)