指针 (计算机科学)

计算机科学术语

计算机科学中,指标(英语:Pointer),是在许多程式语言中用来存储内存地址变量。指针变量的直接指向(points to)存在该地址的对象的值。所指向的可以是计算机内存中的另一个值,或者在某些情况下,是内存映射计算机硬件的值。

名为 a 的指针,指向一个内存地址,当中的值为 b。要注意的是,在这个示意状况中使用的计算结构,对指针及非指针,都使用相同的内存地址以及表示法,但是在真实状况中,不同的计算结构可能有不同做法。

历史

在1964年,哈罗德·劳森发明了最早的指标。他在PL/I中实作出了这个概念,其他高级程式语言也很快跟进,使用了这个想法。指标(pointer)这个名称首次出现在系统发展公司(System Development Corporation,SDC)的技术文件,当中使用了堆叠指标(stack pointer)这个名词。

概论

在计算机科学中,指标是一种最简单形式的参照(reference)。

指标有两种含义,一是作为资料类型,二是作为实体。前者如字元指标、浮点数指标等等;后者如指标物件、指标变数等。

指标作为资料类型,可以从一个函式类型、一个物件类型或一个不完备类型中导出。从中导出的资料类型被称之为被引用型态(referenced type)。指标类型描述了一类的物件,物件值为对被引用类型的实体的引用。[1]

C++标准中规定,“指针”概念不适用于成员指针(不包含指向静态成员的指标)。[2]C++标准规定,指标分为两类:[3]

  • object pointer type:指向void或物件类型,表示对象在随机存取存储器中的字元地址或空指标
  • function pointer type:指代一个函式

指标参考(reference)了记忆体中一个位址。通过被称为指标反参考(dereferencing)的动作,可以取出在那个位址中储存的。保存在指标指向的位址中的,可能代表另一个变数结构物件函数。但是从指标值是无法得知它所参照的记忆体中储存了什么资料型别的资讯。可以打个比方,假设将电脑记忆体当成一本书,那么一张记录了某个页码加上行号的便利贴,可以被当成是一个指向特定页面的指标;根据便利贴上面的页码与行号,翻到那个页面,把那个页面的那一行文字读出来,就相当于是对这个指标进行反参考的动作。可做一类比以增强对指标的理解:整数(integral)也是一类资料类型及其物件或变数,可定义具体的资料类型如短整数(short)、长整数(long)、超长整数(long long)、无符号整数(unsigned)等等;也可以用于称呼整数值、整数物件、整数变数等。又如,一个浮点数指标(float *),可称作指向了一个浮点数类型的物件。

高阶语言中,指标有效的取代了在低阶语言(如组合语言机器码)直接使用记忆体地址。但它可能只适用于合法位址之中。因为指标更贴近硬体,编译器能够很容易的将指标翻译为机械码,这使指标操作时的负担较少,因此能够提高程式的执行速度。

使用指标能够简化许多资料结构的实作,例如在走访字串,查取表格,控制表格及树状结构上。对指标进行复制,之后再解参照指标以取出资料,无论在时间或空间上,都比直接复制及存取资料本身来的经济快速。指标表示法较为直觉,使程式的表达更为简洁,同时也能够提供动态机制来建立新的节点。

程序式程式设计(procedural programming)中,指标也被用来保存系统呼叫流程,以及动态连结资料库(DLL)的进入点位址。在物件导向程式设计中,使用函数指标(Function pointer)来绑定方法(method),常见于虚拟方法表(Virtual method table)中。

但是指标本身也存在一些可被滥用之处,在存取某个资料结构时,可能会超出可用范围,使软体或作业系统出现异常,严重时可造成当机。利用指标去存取或修改非合法可取用的资料,也可能造成安全性问题。为此,C与C++语言规定指标类型为强类型,即指标值不仅是一个内存地址,同时它的资料类型说明了存在这个地址可以安全访问的地址的范围,例如,float*可以访问4个字元的记忆体空间,double*可以访问8个字元的记忆体空间。

许多程式语言中都支援某种形式的指标,最著名的是C语言,但是有些程式语言对指标的运用采取比较严格的限制。因为指标的机制比较简单,其功能可以被集中重新实作成更抽象化的参照(reference)资料形别,如Java一般避免用指标,改为使用参照[4]

指针的实现

C99与C++11标准分别明确规定了把一个指针值转换自(from)/成(to)整形是允许的,但整型的大小至少不低于std::intptr_t[5]

在C语言的多数实现中,指针值是一个以当前系统寻址范围为取值范围的整数。

32位系统的寻址能力(地址空间)是4GB[6](0~232-1),以二进制表示时长度为32位元,每格储存空间是1 Byte。不难验证,在32位系统的大多数实现里,int类型也正好是32-bit长度,可以取遍上述范围。

同理,64位系统取值范围为0~264-1,int类型长度为64-bit。

使用指标的目的

简化程式码

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

参数传递

C中函式调用是按值传递的,传入参数在子函式中只是一个初值相等的副本,无法对传入参数作任何改动。但实际程式设计中,经常要改动传入参数的值。在C语言中一般透过传入参数的位址而不是原参数本身来实现。当对传入参数(地址)取“*”运算时,就可以直接在记忆体中修改,从而改动原想作为传入参数的参数值。

传指标
#include <stdio.h>

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

int main(){
    int a=3;
    inc(&a);
    printf("%d\n", a);
    return 0;
}

// Output:
// 4

在执行inc(&a);时,操作*val,即是在操作a了。

传值

以下例子中,main()内的变数从来没有改变,改变的只是sw()内的变数。

#include <iostream>
using namespace std;

void sw(int x, int y) {
	int Temp;
	Temp = x;
	x = y;
	y = Temp;
}

int main() {
	int a=1;
	int b=2;
	cout <<  a << b << endl;
	sw(a,b);
	cout <<  a << b << endl;
	return 0;
}

// Output:
// 12
// 12


sw()执行完毕后,其内容会自动删除。

a b x y
1 2 - -
1 2 1 2
1 2 2 1
1 2 - -

指针的运算和声明

取地址和解引用运算

解引用(dereference)运算(*p)返回保存在内存地址为p的内存空间中的值。取地址(&p)运算则返回操作数p的内存地址。[7]显然可以用赋值语句对内存地址赋值。

假设一段内存地址空间解引用如下:(十六进制)

地址 0000 2000 2001 2002 2003 2004 3000 3001 3002 3003
解引用 ???? 01 30 00 30 00 00 20 03 9A

然后,执行代码“int *p;”,假设初始化时p被分配3001H、3002H两个地址。则p为2003H,*p为3000H。[8]

**p&&p*(&p)&(*p)的值分别为:

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

&&p=&(&(p))=&(3001H),出错,3001H是常数,无地址可言。

*&p=*(&(p))=*(3001H)=2003H,也即*&p=p

&*p=&(*(p))=&(3000H),出错,3000H是常数,无地址可言。

指针的复杂形式

双重指针(指向指针的指针)

双重指针是指向指针的指针,它是一个指针,这个指针指向某个内存地址,该地址的值是一个指针,指向给另一个内存地址(通常异于前者,但不排除二者相等)。

本质上,指针值就是内存地址。但为了防范指针值被滥用(如内存访问时越界),可以规定指针类型为强类型,即指针值及保存在该内存地址的对象的类型。双重指针不过是这种强类型的一个应用:该地址空间长度为一个指针长度(4或8字节),对象类型为另一种指针。

指针数组

指针数组:就是一个数组,数组的各个元素都是指针值。

数组指针

数组名出现在表达式中时,绝大多数情况(除了数组名作为sizeof的操作数或者作为取地址&元素符的操作数)会被隐式转换为指向数组的首个元素的指针右值

当数组名作为取地址&运算符的操作数,则表达式的值为指向整个数组的指针右值。

例子:

char s[]="hello";

int main() {
  char (*p1)[6]=&s; //OK!
  char (*p2)[6]=s; //compile error: cannot convert 'char*' to 'char (*)[6]'   
  char *p3=&s;//compile error: cannot convert 'char (*)[6]' to 'char*' 
}

根据上述C语言标准中的规定,表达式 &s 的值的类型是char (*)[6],即指向整个数组的指针;而表达式 s 则被隐式转换为指向数组首元素的指针值,即 char* 类型。同理,表达式 s[4] ,等效于表达式 *(s+4)。

指向函数的指针

指向函数的指针:不同于指向数据类型的指针,函数指针指向一段可执行的代码的首地址,这段代码仍然占用了一块内存空间。很多人都说C语言是一种面向过程的语言[来源请求],因为它最多只有结构体的定义,而没有的概念。根据本段所述,可以认为C语言能成为面向对象的语言,只是表述比较麻烦而已。[9]事实上很多开源程序都使用这种方式组织他们的代码。

#include <stdio.h>

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

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

指针运算符的重载

指标的进化与取代

由于指标太活跃,因此导致它几乎能不受限制的在各种内存地址间活动,所以一旦有任何重复、重叠、溢位的情形发生时,电脑便直接当机,这成为指标功能上的最大缺憾。因此在新的网路程式语言的开发上,新的语言如JavaC#等语种已经取消了指标的无限制使用形式。 C#允许指标的有限功能的使用,指标和运算指标在一个操作的环境中是存在潜在的非安全性的,因为他们的使用可以避开对象的一些严格访问规则。C#中使用指标的代码段或者方法的地址要用unsafe关键字进行标记,这样,这些代码的使用者就会知道这个代码相比其他的代码而言是不具有安全性的。编译器需要unsafe关键字时将使用此代码的程序转换成是允许被编译的。一般来说,不安全代码的使用可能是为了非托管的API(应用程序编程介面)的更好互用,或者是为了(存在内在不安全性的)系统调用,也有可能是出于提高性能等方面的原因。而Java中不允许指标或者算术指标的使用。

参考

  1. ^ C99语言标准的6.2.5 Types中规定:A pointer type may be derived from a function type, an object type, or an incomplete type, called the referenced type. A pointer type describes an object whose value provides a reference to an entity of the referenced type. A pointer type derived from the referenced type T is sometimes called ‘‘pointer to T’’. The construction of a pointer type from a referenced type is called ‘‘pointer type derivation’’.
  2. ^ C++11标准3.9.2 Compound types中的规定:Except for pointers to static members, text referring to “pointers” does not apply to pointers to members
  3. ^ C++11标准3.9.2第3段
  4. ^ 实际上Java在传递物件的时候用的是指标(这里认为指标和参照没有本质区别)传递,在传递基本型态(如int)时用的是按值(副本)传递。
  5. ^ C++11标准3.7.4.3
  6. ^ 这和32位操作系统最大支持内存大小没有关系,所谓32位操作系统只支持4GB的说法是不对的,Windows 2003数据中心版的32位版本页面存档备份,存于互联网档案馆)就支持最大64GB的内存。操作系统支持的内存数还取决于其存储访问的组织形式,以及操作系统的使用许可。
  7. ^ C99标准6.5.3.2.3中规定:The unary & operator returns the address of its operand.
  8. ^ 这里的字节序为小尾序,Little-Endian,低位在低地址,Intelx86系列CPU适用。MotorolaPowerPC系列则采用大尾序,Big-Endian,高位在低地址,则上述p为0320H,*p需进一步查内存0320H处的存储值。
  9. ^ 实际上,可以用C语言完成COM模块,这完全模拟了C++语言的类、对象、虚表、虚函数等结构。