The Wayback Machine - https://web.archive.org/web/20221028210102/https://baike.sogou.com/kexue/d10128.htm

指针(计算机编程)

编辑
指针a指向与变量b关联的内存地址。在该图中,无论是指针还是非指针,计算体系结构都使用相同的地址空间和数据基元;事实未必如此。

在计算机科学中,指针是一种编程语言对象,它存储位于计算机内存中的另一个值的内存地址。指针引用内存中的一个位置,获得存储在该位置的值称为指针解引用。作为类比,一本书索引中的页码可以被认为是指向相应页面的指针;通过翻转到具有给定页码的页面并读取在该页面上找到的文本,可以取消这种指针的引用。指针变量的实际格式和内容取决于底层计算机体系结构。

使用指针可以显著提高重复操作的性能,如遍历可迭代的数据结构,例如字符串、查找表、控制表和树结构。特别是,复制和取消引用指针通常比复制和访问指针指向的数据在时间和空间上要便宜得多。

指针还用于保存过程编程中被调用子程序的入口点地址,以及动态链接库(DLL)的运行时链接地址。在面向对象编程中,指向函数的指针用于绑定方法,通常使用所谓的虚拟方法表。

指针是对更抽象的引用数据类型的简单、更具体的实现。一些语言,尤其是低级语言支持某种类型的指针,尽管有些语言对它们的使用有更多的限制。虽然“指针”一般用来指引用,但它更适合于接口明确允许指针作为内存地址(通过指针算术)进行操作的数据结构,而不是不允许这样做的magic cookie或capability。因为指针允许对内存地址进行受保护和不受保护的访问,所以使用它们存在风险,尤其是在后一种情况下。原始指针通常以类似整数的格式存储;但是,试图取消引用或“查找”这样一个值不是有效内存地址的指针会导致程序崩溃。为了缓解这个潜在的问题,作为类型安全问题,指针被认为是由它们所指向的数据类型参数化的独立类型,即使底层表示是整数。也可以采取其他措施(例如验证和边界检查),以验证指针变量包含的值既是有效的存储器地址,又在处理器能够寻址的数值范围内。

1 历史编辑

哈罗德·劳森被认为是1964年指针的发明[1] 。2000年,劳森因发明指针变量并将这一概念引入PL/I而获得IEEE 颁发的计算机先锋奖,从而首次提供了以通用高级语言灵活处理链表的能力。[2] 他关于这些概念的开创性论文发表在1967年6月号的《CACM》杂志上,题为《损益清单处理》。根据牛津英语词典,在系统开发公司的一份技术备忘录中,单词指针首次以堆栈指针的形式出现在印刷品中。

2 形式描述编辑

在计算机科学中,指针是一种参考。

数据基元(或仅仅是基元)是可以使用一次存储器访问从计算机存储器读取或写入计算机存储器的任何数据(例如,字节和字都是基元)。

数据集合(或仅仅是集合)是一组在存储器中逻辑上连续的基元,它们被共同视为一个数据(例如,集合可以是3个逻辑上连续的字节,其值表示空间中一个点的3个坐标)。当一个集合完全由相同类型的基元组成时,该集合可以被称为数组;从某种意义上说,多字节字基元是字节数组,一些程序以这种方式使用字。

在这些定义的上下文中,字节是最小的基元;每个存储器地址指定不同的字节。数据初始字节的存储地址被认为是整个数据的存储地址(或基本存储地址)。

内存指针(或只是指针)是一个基元,其值旨在用作内存地址;指针指向一个存储器地址。也可以说,当指针的值是数据的存储地址时,指针指向数据。

更一般地说,指针是一种引用,指针引用存储在存储器某处的数据;获取数据就是解引用。指针与其他类型的引用的区别在于指针的值被解释为内存地址,这是一个相当低级的概念。

引用起到间接作用:指针的值决定了在计算中使用哪个内存地址(即哪个数据)。因为间接是算法的一个基本方面,指针在编程语言中通常被表示为一种基本的数据类型;在静态(或强)类型编程语言中,指针的类型决定指针指向的数据类型。

3 在数据结构中使用编辑

当设置数据结构(如列表、队列和树)时,需要有指针来帮助管理该结构的实现和控制。指针的典型例子是开始指针、结束指针和堆栈指针。这些指针可以是绝对的(虚拟内存中的实际物理地址或虚拟地址),也可以是相对的(相对于绝对起始地址(“基地址”)的偏移量,该偏移量通常使用比完整地址少的位,但通常需要一个额外的算术运算来解析)。

相对地址是手动内存分段的一种形式,并且共享内存分段的许多优点和缺点。包含16位无符号整数的两字节偏移量可用于为多达64千字节的数据结构提供相对寻址。如果指向的地址被迫在半字、字或双字边界上对齐(但是,在添加到基址之前,需要额外的“左移”逐位操作,即座机1、2或3位,以便将偏移调整2、4或8倍),则可以轻松地扩展到128K、256K或512K。然而,一般来说,这样的方案是很麻烦的,为了程序员的方便,绝对地址(在此基础上,平面地址空间)是首选的。

一个字节的偏移量,例如字符的十六进制ASCII值(例如,“29”)可以用来指向数组(例如,“01”)中的可选整数值(或索引)。这样,字符可以非常有效地从“原始数据”转换成可用的顺序索引,然后转换成绝对地址,而无需查找表。

4 在控制表中使用编辑

用于控制程序流的控制表通常广泛使用指针。指针通常嵌入在表条目中,例如,基于同一表条目中定义的某些条件,可以用来保存要执行的子程序的入口点。然而,指针可以简单地索引到其他独立但相关联的表,这些表包括实际地址的数组或地址本身(取决于可用的编程语言结构)。它们也可以用来指向更早的表条目(如在循环处理中),或者向前跳过一些表条目(如在switch中或从循环中“提前”退出)。对于后一个目的,“指针”可以简单地表示表的入口号本身,并且可以通过简单的算术转换成实际地址。

5 建筑根源编辑

指针是大多数现代体系结构提供的寻址能力之上的一个非常抽象的概念。在最简单的方案中,地址或数字索引被分配给系统中的每个存储器单元,其中该单元通常是字节或字(取决于体系结构是字节可寻址的还是字可寻址的),有效地将所有存储器转换成非常大的数组。然后,系统还将提供检索存储在给定地址的存储单元中的值的操作(通常利用机器的通用寄存器)。

在通常情况下,指针足够大,可以容纳比系统内存单元更多的地址。这引入了这样的可能性,即程序可能试图访问与没有存储单元相对应的地址,或者是因为没有安装足够的存储器(即超出可用存储器的范围),或者是因为体系结构不支持这样的地址。第一种情况在英特尔x86体系结构等特定平台中可能被称为段错误(segfault)。第二种情况在AMD64的当前实现中是可能的,其中指针是64位长,地址仅扩展到48位。指针必须符合某些规则(规范地址),因此如果非规范指针被解引用,处理器将引发一般保护故障。

另一方面,有些系统的内存单元比地址多。在这种情况下,用更复杂的方案(如内存分段或分页)来在不同的时间使用内存的不同部分。x86体系结构的最终版本支持多达36位的物理内存地址,这些地址通过PAE分页机制映射到32位线性地址空间。因此,一次只能访问可能的总存储器的1/16。同一计算机系列中的另一个例子是80286处理器的16位保护模式,虽然它仅支持16 MB的物理内存,但可以访问高达1 GB的虚拟内存,但是16位地址和段寄存器的组合使得在一个数据结构中访问超过64 KB的内存变得很麻烦。

为了提供一致的接口,一些体系结构提供存储器映射的输入/输出,这允许一些地址引用存储器单元,而另一些地址引用计算机中其他设备的设备寄存器。还有一些类似的概念,如文件偏移量、数组索引和远程对象引用,它们的作用与其他类型对象的地址相同。

6 使用编辑

指针在诸如PL/I、C、C++、Pascal、FreeBASIC之类的语言中得到直接支持,而在大多数汇编语言中则是隐式支持的。它们主要用于构建引用,而引用又是构建几乎所有数据结构的基础,也是在程序的不同部分之间传递数据的基础。

在严重依赖列表的函数式编程语言中,数据引用是通过使用cons和相应的元素car和cdr之类的基本构造来抽象管理的,这些元素可以被认为是指向cons-cell的第一和第二组件的专用指针。这就产生了函数式编程的一些惯用“味道”。通过在这样的构造列表中构造数据,这些语言促进了构建和处理数据的递归方式——例如,递归地访问列表的头和尾元素;例如“乘坐cdr的cdr的车”。相比之下,基于在存储器地址阵列的某种近似中的指针解引用的存储器管理有助于将变量视为可以强制分配数据的槽。

在处理数组时,关键的查找操作通常包括一个称为地址计算的阶段,该阶段包括构造指向数组中所需数据元素的指针。在其他数据结构(例如链表)中,指针被用作引用来显式地将结构的一部分与另一部分联系起来。

指针用于通过引用传递参数。如果程序员希望函数对参数的修改操作对函数的调用方可见,这是非常有用的。这对于从函数中返回多个值也很有用。

指针也可以用来分配和解除分配内存中的动态变量和数组。由于变量在达到其目的后通常会变得多余,保留它是浪费内存,因此当不再需要它时,最好解除分配(使用原始指针引用)。否则可能会导致内存泄漏(在可用空闲内存逐渐减少的情况下,或者在严重的情况下,由于大量冗余内存块的累积,可用空闲内存会迅速减少)。

6.1 c指针

定义指针的基本语法是:[3]

int *ptr;

这将ptr声明为以下类型对象的标识符:

  • 指向int类型对象的指针

这通常被更简洁地表述为“ptr是指向int的指针”。

因为C语言没有为自动存储持续时间的对象指定隐式初始化,[4] 所以应该经常注意确保ptr指向的地址是有效的;这就是为什么有时建议将指针显式初始化为空指针值的原因,该值传统上在C语言中用标准化的宏空值来指定:

[5]

int *ptr = NULL;

在C语言中解引用空指针会产生未定义行为,[6] 这可能是灾难性的。然而,大多数实现只是暂停问题程序的执行,通常带有分段错误。

然而,不必要地初始化指针可能会妨碍程序分析,从而隐藏错误。

无论如何,一旦一个指针被声明,下一个逻辑步骤就是指向某个东西:

int a = 5;
int *ptr = NULL;

ptr = &a;

这将a的地址值分配给ptr。例如,如果a存储在0x8130的存储器位置,则分配后ptr值将为0x8130。要解引用,需要再对指针使用星号:

*ptr = 8;

这意味着获取ptr(0x 8130)的内容,在内存中“定位”该地址,并将其值设置为8。如果稍后再次访问,其新值将为8。

如果直接检查内存,这个例子可能会更清楚。假设a位于存储器中的地址0x8130,ptr位于0x8134;还假设这是一台32位机器,因此int为32位宽。以下是执行以下代码片段后内存中的内容:

int a = 5;
int *ptr = NULL;

地址 内容
0x8130 0x00000005
0x8134 0x00000000

(这里显示的空指针是0x00000000。)通过将a的地址分配给ptr:

 ptr = &a;

产生以下内存值:

地址 内容
0x8130 0x00000005
0x8134 0x00008130

然后通过编码对ptr解引用:

 *ptr = 8;

计算机将获取ptr(0x 8130)的内容,“定位”该地址,并将8分配给该位置,产生以下内存:

地址 内容
0x8130 0x00000008
0x8134 0x00008130

显然,访问a将产生值8,因为前面的指令通过指针ptr修改了a的内容。

6.2 c数组

在C语言中,数组索引是根据指针算法正式定义的;也就是说,语言规范要求数组等同于*(数组+ i)。[7] 因此,在C语言中,数组可以被认为是指向连续内存区域的指针(没有间隙),访问数组的语法与用来解引用指针的语法相同。例如,数组array可以通过以下方式声明和使用:

int array[5];      /* Declares 5 contiguous integers */
int *ptr = array;  /* Arrays can be used as pointers */
ptr[0] = 1;        /* Pointers can be indexed with array syntax */
*(array + 1) = 2;  /* Arrays can be dereferenced with pointer syntax */
*(1 + array) = 2;  /* Pointer addition is commutative */
2[array] = 4;      /* Subscript operator is commutative */

这将分配一个由五个整数组成的块,并命名块数组,该数组充当指向该块的指针。指针的另一个常见用途是指向malloc中动态分配的内存,该内存返回的连续内存块不小于可用作数组的请求大小。

虽然数组和指针上的大多数运算符是等价的,但是sizeof运算符的结果是不同的。在本例中,sizeof(array)将计算为5 *sizeof(int) (数组的大小),而sizeof(ptr)将计算为sizeof(int*),即指针本身的大小。

数组的默认值可以声明如下:

int array[5] = {2, 4, 3, 1, 5};

如果数组位于32位小端机器上从地址0x1000开始的内存中,则内存将包含以下内容(值为十六进制,如地址):

0 1 2 3
1000 2 0 0 0
1004 4 0 0 0
1008 3 0 0 0
100C 1 0 0 0
1010 5 0 0 0

这里表示五个整数:2、4、3、1和5。这五个整数各占32位(4字节),最低有效字节先存储(这是一种小字节序的中央处理器体系结构),并从地址0x1000开始连续存储。

带指针的C语言的语法是:

  • array 表示 0x1000;
  • array+ 1表示0x 1004:“+1”表示增加1 int的大小,即4字节;
  • *array意味着解引用array的内容。将内容视为内存地址(0x1000),查找该位置的值(0x 0002);
  • array[i]是指数组中从0开始的元素号i,它被转换为*(数组+ i)。

最后一个例子是如何访问数组的内容。分解为:

  • array + i是数组第(i)个元素的存储位置,从i=0开始;
  • *(array + i)获取该内存地址并解引用它来访问该值。

6.3 C 链表

下面是c语言中链表的一个示例定义

/* the empty linked list is represented by NULL
 * or some other sentinel value */
#define EMPTY_LIST  NULL

struct link {
    void         data;  /* data of this link */
    struct link *next;  /* next link; EMPTY_LIST if there is none */
};

这个指针递归定义本质上与Haskell编程语言中的引用递归定义相同:

 data Link a = Nil
             | Cons a (Link a)

Nil是空列表,而Cons a(Link a)是类型a的Cons单元,它的另一个链接也是类型a。

然而,带有引用的定义是有类型检查的,并且不使用潜在的混淆信号值。因此,C语言中的数据结构通常通过包装函数来处理,包装函数会仔细检查正确性。

6.4 使用指针按地址传递

指针可以用来通过变量的地址传递变量,允许变量的值被改变。例如,参考下面的代码:

/* a copy of the int n can be changed within the function without affecting the calling code */
void passByValue(int n) {
    n = 12;
}

/* a pointer m is passed instead. No copy of the value pointed to by m is created */
void passByAddress(int *m) {
    *m = 14;
}

int main(void) {
    int x = 3;

    /* pass a copy of x's value as the argument */
    passByValue(x);
    // the value was changed inside the function, but x is still 3 from here on

    /* pass x's address as the argument */
    passByAddress(&x);
    // x was actually changed by the function and is now equal to 14 here

    return 0;
}

6.5 动态存储分配

在某些程序中,所需的内存取决于用户可能输入的内容。在这种情况下,程序员需要动态分配内存。这是通过在堆上而不是在堆栈上分配内存来实现的,在堆栈中通常存储变量。(变量也可以存储在中央处理器寄存器中,但那是另一回事)动态内存分配只能通过指针进行,不能给出名称(像普通变量一样)。

指针用于存储和管理动态分配的内存块的地址。这些块用于存储数据对象或对象数组。大多数结构化和面向对象的语言都提供了一个内存区域,称为堆或自由存储区,从中可以动态分配对象。

下面的示例代码说明了如何动态分配和引用结构对象。标准的C库提供了从堆中分配内存块的函数malloc()。它将要分配的对象的大小作为参数,并返回一个指向新分配的适合存储该对象的内存块的指针,或者如果分配失败,则返回一个空指针。

/* Parts inventory item */
struct Item {
    int         id;     /* Part number */
    char *      name;   /* Part name   */
    float       cost;   /* Cost        */
};

/* Allocate and initialize a new Item object */
struct Item * make_item(const char *name) {
    struct Item * item;

    /* Allocate a block of memory for a new Item object */
    item = (struct Item *)malloc(sizeof(struct Item));
    if (item == NULL)
        return NULL;

    /* Initialize the members of the new Item */
    memset(item, 0, sizeof(struct Item));
    item->id =   -1;
    item->name = NULL;
    item->cost = 0.0;

    /* Save a copy of the name in the new Item */
    item->name = (char *)malloc(strlen(name) + 1);
    if (item->name == NULL) {
        free(item);
        return NULL;
    }
    strcpy(item->name, name);

    /* Return the newly created Item object */
    return item;
}

下面的代码说明了内存对象是如何被动态地解除分配的,即将其返回到堆或空闲存储区。标准的C库提供了函数free(),用于解除分配先前分配的内存块并将其返回堆。

/* Deallocate an Item object */
void destroy_item(struct Item *item) {
    /* Check for a null object pointer */
    if (item == NULL)
        return;

    /* Deallocate the name string saved within the Item */
    if (item->name != NULL) {
        free(item->name);
        item->name = NULL;
    }

    /* Deallocate the Item object itself */
    free(item);
}

6.6 内存映射硬件

在某些计算架构上,指针可以用来直接操作内存或内存映射设备。

在对微控制器编程时,给指针分配地址是一个非常有用的工具。下面是一个简单的例子,声明一个int类型的指针,并将它初始化为十六进制地址,在这个例子中是常数0x7FFF:

int *hardware_address = (int *)0x7FFF;

在80年代中期,使用基本输入输出系统访问个人电脑的视频功能很慢。显示密集型应用程序通常通过将十六进制常数0xB8000转换为指向80个无符号16位int值数组的指针来直接访问CGA视频内存。每个值由低位字节的ASCII码和高位字节的颜色码组成。因此,要将第5行第2列的字母“A”用亮白色写在蓝色上,需要编写如下代码:

#define VID ((unsigned short (*)[80])0xB8000)

void foo(void) {
    VID[4][1] = 0x1F00 | 'A';
}

7 类型化指针和转换编辑

在许多语言中,指针有额外的限制,即它们指向的对象具有特定的类型。例如,可以声明指针指向整数;然后,该语言将试图防止程序员将它指向非整数的对象,例如浮点数,从而消除一些错误。例如,在C语言中

int *money;
char *bags;

money将是一个整数指针,而bags将是一个char指针。下面将在GCC下产生编译器警告“来自不兼容指针类型的赋值”

bags = money;

因为money和bags是用不同的类型申报的。为了消除编译器警告,必须明确表示您确实希望通过类型转换来进行赋值

bags = (char *)money;

它表示将整数货币指针转换为char指针并分配给bags。

2005年的一个C标准草案要求将一个从一种类型派生的指针转换成另一种类型应该保持两种类型的对齐正确性(6.3.2.3指针,par。7):[8]

char *external_buffer = "abcdef";
int *internal_data;

internal_data = (int *)external_buffer;  // UNDEFINED BEHAVIOUR if "the resulting pointer
                                         // is not correctly aligned"

在允许指针运算的语言中,指针运算会考虑类型的大小。例如,对指针整数加法会产生另一个指针,运算后的指针指向的地址比原指针大该类型的大小的数倍。这使我们能够容易地计算给定类型数组元素的地址,如上面的C数组示例所示。当一种类型的指针被转换成另一种不同大小的指针时,程序员应该期望指针算法的计算会有所不同。例如,在C语言中,如果money数组从0x2000开始,而sizeof(int)是4字节,而sizeof(char)是1字节,那么money+ 1将指向0x2004,而bags+’1’将指向0x2001。铸造的其他风险包括当“宽”数据写入“窄”位置时数据丢失(例如,bags= 65537;),移位值时出现意外结果,以及比较问题,特别是有符号值和无符号值之间的比较。

虽然通常不可能在编译时确定哪些转换是安全的,但是一些语言存储了运行时类型信息,这些信息可以用来确认这些危险的转换在运行时是有效的。其他语言只接受安全转换的保守近似,或者根本不接受。

8 让指针更安全编辑

由于指针允许程序试图访问未定义的对象,指针可能是各种编程错误的根源。然而,指针的用处是如此之大,以至于没有指针就很难执行编程任务。因此,许多语言都创建了一些结构,这些结构旨在提供指针的一些有用特性,而不存在一些陷阱(有时也称为指针危害)。在这种情况下,与智能指针或其他变体相比,直接寻址内存的指针(如本文所用)被称为原始指针。

指针的一个主要问题是,只要它们可以直接作为数字来操作,就可以指向未使用的地址或用于其他目的的数据。许多语言,包括大多数函数式编程语言和最近的命令式语言(如Java),都用更不透明的引用类型来代替指针,通常简称为引用,它只能用来引用对象,而不能作为数字来操作,从而防止了这种类型的错误。数组索引是作为特例处理的。

没有分配任何地址的指针称为通配符指针。任何使用这种未初始化指针的尝试都会导致意外的行为,要么是因为初始值不是有效地址,要么是因为使用它可能会损坏程序的其他部分。结果通常是分段错误、存储冲突或野生分支(如果用作函数指针或分支地址)。

在具有显式内存分配的系统中,可以通过解除分配指向的内存区域来创建悬挂指针。这种类型的指针是危险而微妙的,因为解除分配的内存区域可能包含与解除分配之前相同的数据,但是可能会被重新分配并被不相关的代码覆盖,而这些代码对于早期的代码是未知的。带有垃圾收集的语言可以防止这种类型的错误,因为当作用域中没有更多引用时,会自动执行解除分配。

有些语言,如C++,支持智能指针,智能指针使用一种简单形式的引用计数来帮助跟踪动态内存的分配,并充当引用。在没有引用循环的情况下,对象通过一系列智能指针间接引用自己,这消除了悬挂指针和内存泄漏的可能性。Delphi字符串本身支持引用计数。

Rust编程语言引入了借用检查器、指针生存期和基于空指针可选类型的优化,以消除指针错误,而无需求助于垃圾收集器。

9 空指针编辑

空指针有一个保留值,用于指示指针没有引用有效对象。空指针通常用于表示未知长度列表的结尾或未能执行某些操作等情况;空指针的这种使用可以与可空类型和选项类型中的无值进行比较。

10 自动相对指针编辑

自动相对指针是值被解释为指针自身地址的偏移量的指针;因此,如果数据结构具有指向数据结构本身的某个部分的自动相对指针成员,则数据结构可以在存储器中重新定位,而不必更新自动相对指针的值。[9]

引用的专有词也使用术语“相对指针”来表示同样的意思。然而,该术语的含义已被用于其他方面:

  • 指从结构地址的偏移,而不是从指针本身的地址的偏移;
  • 指包含其自己地址的指针,这对于在存储器的任何任意区域中重建相互指向的数据结构集合是有用的。[10]

11 基于指针的指针编辑

基于指针的指针是一个指针,它的值与另一个指针的值有偏移。这可用于存储和加载数据块,将数据块开头的地址分配给基指针。[11]

12 多重间接编辑

在某些语言中,一个指针可以引用另一个指针,需要多次解引用操作才能到达原始值。虽然每一级间接都可能增加性能成本,但有时为了给复杂的数据结构提供正确的行为,这是必要的。例如,在C语言中,通常根据包含指向列表下一个元素的指针的元素来定义链表:

struct element {
    struct element *next;
    int            value;
};

struct element *head = NULL;

这个实现使用指向列表中第一个元素的指针作为整个列表的代表。如果一个新值被添加到列表的开头,head必须被改变以指向新元素。由于C参数总是通过值传递,使用双重间接可以正确实现插入,并且具有消除特殊情况代码来处理列表前面插入内容的理想副作用:

// Given a sorted list at *head, insert the element item at the first
// location where all earlier elements have lesser or equal value.
void insert(struct element **head, struct element *item) {
    struct element **p;  // p points to a pointer to an element
    for (p = head; *p != NULL; p = &(*p)->next) {
        if (item->value <= (*p)->value)
            break;
    }
    item->next = *p;
    *p = item;
}

// Caller does this:
insert(&head, item);

在这种情况下,如果item的值小于head的值,调用方的head将被正确地更新为新的head的地址。

一个基本的例子是指向C(和C++)中主函数的argv参数,它在原型中以char **argv给出——这是因为变量argv本身是指向字符串数组(数组数组数组)的指针,所以*argv是指向第0个字符串的指针(按照惯例是程序的名称),而**argv是第0个字符串的第0个字符。

13 函数指针编辑

在某些语言中,指针可以引用可执行代码,即它可以指向函数、方法或过程。函数指针将存储要调用的函数的地址。虽然这个工具可以用来动态调用函数,但它通常是病毒和其他恶意软件编写人员最喜欢的技术。

int sum(int n1, int n2) {   // Function with two integer parameters returning an integer value
    return n1 + n2;
}

int main(void) {
    int a, b, x, y;
    int (*fp)(int, int);    // Function pointer which can point to a function like sum
    fp = &sum;              // fp now points to function sum
    x = (*fp)(a, b);        // Calls function sum with arguments a and b
    y = sum(a, b);          // Calls function sum with arguments a and b
}

14 悬空指针编辑

悬空指针是指不指向有效对象的指针,因此可能导致程序崩溃或行为异常。在Pascal或C编程语言中,没有特别初始化的指针可能指向内存中不可预测的地址。

下面的示例代码显示了一个悬空指针:

int func(void) {
    char *p1 = malloc(sizeof(char)); /* (undefined) value of some place on the heap */
    char *p2;       /* dangling (uninitialized) pointer */
    *p1 = 'a';      /* This is OK, assuming malloc() has not returned NULL. */
    *p2 = 'b';      /* This invokes undefined behavior */
}

这里,p2可以指向内存中的任何地方,所以执行赋值* p2 = 'b'可能损坏未知内存区域或触发分段错误。

15 反向指针编辑

在双链表或树形结构中,保持在元素上的反向指针指向引用当前元素的项。这些对导航和操作很有用,但代价是占用了更多的内存。

16 野生分支编辑

当指针被用作程序入口点的地址或一个函数的开始,该程序或函数不返回任何内容,也没有初始化或损坏,如果对该地址进行调用或跳转,则称发生了“野生分支”。结果通常是不可预测的,错误可能以几种不同的方式出现,这取决于指针是否是“有效”地址,以及在该地址是否(巧合地)存在有效指令(操作码)。野生分支的检测可能是最困难和最令人沮丧的调试工作之一,因为许多证据可能已经被预先销毁,或者通过在分支位置执行一个或多个不适当的指令而被销毁。如果可用的话,指令集模拟器通常不仅可以在生效之前检测到一个野生分支,还可以提供其历史的完整或部分跟踪。

17 使用数组索引的模拟编辑

可以使用(通常是一维)数组的索引来模拟指针行为。

主要是对于不明确支持指针但支持数组的语言,数组可以被认为是整个内存范围(在特定数组的范围内),对它的任何索引都可以被认为等同于汇编语言中的通用目标寄存器(它指向单个字节,但其实际值相对于数组的开头,而不是其在内存中的绝对地址)。假设数组是一个连续的16兆字节字符数据结构,单个字节(或数组中的一串连续字节)可以直接寻址和操作,使用数组的名称和一个31位无符号整数作为模拟指针(这与上面显示的C数组例子非常相似)。指针算法可以通过增加或减少索引来模拟,与真正的指针算法相比,增加的开销最小。

理论上,使用上述技术和合适的指令集模拟器,甚至可以用另一种完全不支持指针的语言(例如,Java / JavaScript)来模拟任何机器代码或任何处理器/语言的中间代码(字节代码)。为了实现这一点,二进制代码最初可以加载到数组的连续字节中,以便模拟器完全在同一数组包含的内存中“读取”、解释和操作。如有必要,为了完全避免缓冲区溢出问题,通常可以对编译器进行边界检查(否则,在模拟器中手动编码)。

18 各种编程语言对指针的支持编辑

18.1 Ada

Ada是一种强类型语言,其中所有指针都是类型化的,并且只允许安全类型转换。默认情况下,所有指针都被初始化为空,任何通过空指针访问数据的尝试都会引发异常。Ada中的指针称为访问类型。Ada 83不允许对访问类型进行算术运算(尽管许多编译器供应商将其作为非标准特性提供),但是Ada 95支持通过System.Storage_Elements包对访问类型进行“安全”算术运算。

18.2 基础

几个旧版本的用于Windows平台的BASIC支持STRPTR()返回一个字符串的地址,支持VARPTR()返回一个变量的地址。Visual Basic 5还支持OBJPTR()返回对象接口的地址,并支持address of运算符返回函数的地址。所有这些类型都是整数,但是它们的值等同于指针类型持有的值。

然而,新的BASIC变体,如FreeBASIC或BlitzMax,有详尽的指针实现。在FreeBASIC中,Any指针上的算术(相当于C的void*)都被视为Any指针是一个字节宽度。Any指针不能解引用,就像在c语言中一样。另外,在Any和任何其他类型的指针之间进行转换不会生成任何警告。

dim as integer f = 257
dim as any ptr g = @f
dim as integer ptr i = g
assert(*i = 257)
assert( (g + 4) = (@f + 1) )

18.3 C和C++

在C和C++中,指针是存储地址的变量,可以为空。每个指针都有它所指向的类型,但是可以在指针类型之间自由转换(但不能在函数指针和对象指针之间转换)。一种称为“空指针”的特殊指针类型允许指向任何(非函数)对象,但是受到不能直接解引用(应该强制转换)这一事实的限制。地址本身通常可以通过在足够大的整数类型之间转换指针来直接操作,尽管结果是由实现定义的,并且可能确实导致未定义行为;虽然早期的C标准没有保证足够大的整数类型,C99指定了< stdint.h >中定义的uintptr_t typedef名称,但是实现不需要提供它。

C++完全支持C指针和C类型转换。它还支持一组新的类型转换运算符,以帮助在编译时捕获一些意外的危险转换。自C++11以来,C++标准库还提供了智能指针(唯一指针、共享指针和弱指针),在某些情况下可以作为原始C指针的更安全的替代。C++还支持另一种形式的引用,与指针完全不同,简单地称为引用或引用类型。

指针算术,即通过算术运算(以及大小比较)修改指针目标地址的能力,受到语言标准的限制,只能保持在单个数组对象的边界内(或者就在它之后),否则将调用未定义行为。指针的加减会使指针移动其数据类型大小的倍数。例如,向指向4字节整数值的指针添加1将使指针的指向字节地址增加4。这样做的效果是递增指针以指向连续整数数组中的下一个元素——这通常是预期的结果。指针算法不能在空指针上执行,因为空类型没有大小,因此不能添加指向的地址,尽管gcc和其他编译器会将空*作为非标准扩展来执行字节算法,将其视为char *。

指针算法为程序员提供了一种处理不同类型的单一方法:增加和减少所需的元素数量,而不是以字节为单位的实际偏移量。(带字符*指针的指针算法使用字节偏移量,因为(字符)的大小定义为1。)特别地,C定义明确声明,作为数组a的第n个元素的语法a等价于*(a + n),即由+ n指向的元素的内容。这意味着n等价于a,并且可以同样好地写入例如a或3来访问数组a的第四个元素

指针算法虽然强大,但可能是计算机漏洞的来源。这往往会混淆程序员新手,迫使他们进入不同的环境:一个表达式可以是普通的算术表达式,也可以是指针算术表达式,有时很容易把一个错当成另一个。对此,许多现代高级计算机语言(例如Java)不允许使用地址直接访问内存。此外,安全的C语言变体Cyclone通过指针解决了许多问题。

ANSI C和C++支持空指针(void或void *)作为通用指针类型。指向void的指针可以存储任何对象(而不是函数)的地址,并且在C语言中,在赋值时隐式转换为任何其他对象指针类型,但是如果解引用,它必须显式转换。K&R C使用char*作为“不可知类型指针”的目标类型(在ANSI C之前)。

int x = 4;
void* p1 = &x;
int* p2 = p1;       // void* implicitly converted to int*: valid C, but not C++
int a = *p2;
int b = *(int*)p1;  // when dereferencing inline, there is no implicit conversion

C++不允许void*隐式转换为其他指针类型,即使在赋值时也是如此。这是一个避免粗心甚至意外转换的设计决策,尽管大多数编译器在遇到其他转换时只输出警告,而不是错误。

int x = 4;
void* p1 = &x;
int* p2 = p1;                     // this fails in C++: there is no implicit conversion from void*
int* p3 = (int*)p1;               // C-style cast
int* p4 = static_cast<int*>(p1);  // C++ cast

在C++中,没有void&(对void的引用)来补充void*(指向void的指针),因为引用的行为就像它们指向的变量的别名,永远不会有类型为void的变量。

指针声明语法概述

这些指针声明涵盖了指针声明的大多数变体。当然,有三重指针是可能的,但是三重指针背后的主要原理已经存在于双指针中。

char cff [5][5];    /* array of arrays of chars */
char *cfp [5];      /* array of pointers to chars */
char **cpp;         /* pointer to pointer to char ("double pointer") */
char (*cpf) [5];    /* pointer to array(s) of chars */
char *cpF();        /* function which returns a pointer to char(s) */
char (*CFp)();      /* pointer to a function which returns a char */
char (*cfpF())[5]; /* function which returns pointer to an array of chars */
char (*cpFf[5])();  /* an array of pointers to functions which return a char */

()和[]的优先级高于*。 [12]

18.4 C#

在C#编程语言中,指针仅在某些条件下受支持:任何包含指针的代码块都必须用unsafe关键字标记。这种块通常需要更高的安全权限才能运行。语法与C++中的语法基本相同,指向的地址可以是托管内存,也可以是非托管内存。但是,指向托管内存的指针(指向托管对象的任何指针)必须使用fixed关键字声明,这可以防止垃圾收集器在指针处于作用域内时移动指向的对象作为内存管理的一部分,从而保持指针地址有效。

这方面的一个例外是使用IntPtr结构,这是一个相当于int*的安全托管结构,不需要不安全的代码。当使用System.Runtime.InteropServices中的方法时,通常会返回此类型。例如:

// Get 16 bytes of memory from the process's unmanaged memory
IntPtr pointer = System.Runtime.InteropServices.Marshal.AllocHGlobal(16);

// Do something with the allocated memory

// Free the allocated memory
System.Runtime.InteropServices.Marshal.FreeHGlobal(pointer);

.NET框架包括System.Runtime.InteropServices和System中的许多类和方法 (如Marshal类),它们将.NET类型(例如,System.String)与非托管类型和指针(如LPWSTR和void*)相互转换,以允许与非托管代码通信。大多数这样的方法具有与非托管代码相同的安全权限要求,因为它们会对内存中的任意位置产生影响。

18.5 COBOL

COBOL编程语言支持指向变量的指针。在程序的LIANCE SECTION中声明的原始类型或group(record)数据对象本质上是基于指针的,程序中分配的唯一内存是数据项地址的空间(通常是单个内存字)。在程序源代码中,这些数据项的使用就像任何其他WORKSHONG-STORAGE变量一样,但是它们的内容是通过它们的LINKAGE指针间接隐式访问的。

每个指向数据对象的内存空间通常使用外部调用语句或通过嵌入式扩展语言结构(如执行CICS或执行SQL语句)动态分配。

COBOL的扩展版本还提供了用USAGE IS POINTER子句声明的指针变量。使用SET和SET ADDRESS语句来建立和修改这些指针变量的值。

COBOL的一些扩展版本还提供了PROCEDURE-POINTER变量,这些变量能够存储可执行代码的地址。

18.6 PL/I

PL/I语言为指向所有数据类型的指针(包括指向结构的指针)、递归、多任务处理、字符串处理和广泛的内置函数提供了全面的支持。与当时的编程语言相比,PL/I是一个很大的进步。PL/I指针是非类型化的,因此指针解引用或赋值不需要转换。指针的声明语法是DECLARE xxx POINTER,它声明一个名为“xxx”的指针。指针与BASED变量一起使用。BASED变量可以用默认定位变量声明(DECLARE xxx BASED,PPP);或不使用(DECLARE xxx BASED;),其中xxx是基变量,可以是元素变量、结构或数组,ppp是默认指针)。这样变量可以在没有显式指针引用的情况下被寻址(XXX = 1),或者可以明确引用默认定位变量(ppp)或任何其他指针(qqq-> XXX = 1;)来寻址。

指针算法不是PL/I标准的一部分,但是许多编译器允许ptr = ptr±expression形式的表达式。IBM PL/I还具有内置函数PTRADD来执行该算法。指针算法总是以字节为单位执行。

IBM企业PL/I编译器有一种新形式的类型化指针,称为句柄。

18.7 D

D语言是C和C++的衍生物,完全支持C指针和C类型转换。

18.8 Eiffel

埃菲尔面向对象语言使用没有指针算法的值和引用语义。尽管如此,还是提供了指针类。它们提供指针算法、类型转换、显式内存管理、与非埃菲尔软件的接口以及其他功能。

18.9 Fortran

Fortran-90引入了强类型指针功能。Fortran指针不仅仅包含一个简单的内存地址。它们还封装了数组维度、步长(例如,支持任意数组部分)和其他元数据的下限和上限。关联运算符,= >用于将指针关联到具有TARGET属性的变量。Fortran-90 ALLOCATE语句也可用于将指针与内存块相关联。例如,以下代码可用于定义和创建链接列表结构:

type real_list_t
  real :: sample_data(100)
  type (real_list_t), pointer :: next => null ()
end type

type (real_list_t), target :: my_real_list
type (real_list_t), pointer :: real_list_temp

real_list_temp => my_real_list
do
  read (1,iostat=ioerr) real_list_temp%sample_data
  if (ioerr /= 0) exit
  allocate (real_list_temp%next)
  real_list_temp => real_list_temp%next
end do

Fortran-2003增加了对过程指针的支持。此外,作为互操作性特性的一部分,Fortran-2003支持将C风格指针转换成Fortran指针并返回这个指针。

18.10 Go

Go有指针。它的声明语法等同于C语言,但是写法相反,以类型结束。与C不同,Go有垃圾收集功能,并且不支持指针算法。像C++中那样的引用类型是不存在的。有些内置类型,如映射和通道,被装箱(即,在内部它们是指向可变结构的指针),并使用make函数初始化。在指针和非指针之间统一语法的方法中,箭头(->)运算符已被删除:指针上的点运算符指的是解引用对象的字段或方法。然而,这仅适用于1级间接。

18.11 Java

与C、C++或Pascal不同,在Java中没有指针的显式表示。相反,使用引用来实现更复杂的数据结构,如对象和数组。该语言不提供任何显式指针操作运算符。然而,代码仍然可能试图解引用一个空引用(空指针),这将导致引发运行时异常。未引用内存对象占用的空间在运行时通过垃圾收集自动恢复。[13]

18.12 Modula-2

指针的实现与Pascal非常相似,过程调用中的VAR参数也是如此。Modula-2的类型比Pascal更强,从类型系统中退出的方法更少。Modula-2的一些变体(如Modula-3)包括垃圾收集。

18.13 Oberon

和Modula-2一样,指针也是可用的。逃避类型系统的方法仍然很少,因此Oberon及其变体在指针方面比Modula-2或其变体更安全。和Modula-3一样,垃圾收集是语言规范的一部分。

18.14 Pascal

与许多以指针为特征的语言不同,标准的ISO Pascal只允许指针引用动态创建的匿名变量,而不允许它们引用标准的静态或局部变量。[14] 它没有指针算法。指针还必须具有关联的类型,并且指向一种类型的指针与指向另一种类型的指针不兼容(例如,指向字符的指针与指向整数的指针不兼容)。这有助于消除其他指针实现固有的类型安全问题,尤其是那些用于PL/I或C的指针实现。它还消除了悬挂指针引起的一些风险,但是通过使用dispose标准过程(其具有与在C中找到的自由库函数相同的效果)动态释放引用空间的能力意味着悬挂指针的风险没有完全消除。[15]

然而,在一些商业和开源的Pascal(或派生语言)编译器实现中——比如Free Pascal,[16]Turbo Pascal或Embarcadero Delphi中的Object Pascal——指针被允许引用标准静态或局部变量,并且可以从一种指针类型转换到另一种指针类型。此外,指针算法是不受限制的:从指针中添加或减去,可以在任一方向上移动该字节数,但是使用Inc或Dec标准过程,指针会移动声明指向的数据类型的大小。名称指针下还提供了一个非类型化指针,它与其他指针类型兼容。

18.15 Perl

Perl编程语言支持打包和解包函数形式的指针,尽管很少使用。这些仅用于与编译后的操作系统库的简单交互。在所有其他情况下,Perl使用引用,这些引用是类型化的,不允许任何形式的指针算法。它们用于构建复杂的数据结构。[17]

参考文献

  • [1]

    ^Reilly, Edwin D. (2003). Milestones in Computer Science and Information Technology. ISBN 9781573565219. Retrieved 2018-04-13..

  • [2]

    ^"IEEE Computer Society awards list". Awards.computer.org. Retrieved 2018-04-13..

  • [3]

    ^ISO/IEC 9899, clause 6.7.5.1, paragraph 1..

  • [4]

    ^ISO/IEC 9899, clause 6.7.8, paragraph 10..

  • [5]

    ^ISO/IEC 9899, clause 7.17, paragraph 3: NULL... which expands to an implementation-defined null pointer constant....

  • [6]

    ^ISO/IEC 9899, clause 6.5.3.2, paragraph 4, footnote 87: If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined... Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer....

  • [7]

    ^Plauger, P J; Brodie, Jim (1992). ANSI and ISO Standard C Programmer's Reference. Redmond, WA: Microsoft Press. pp. 108, 51. ISBN 978-1-55615-359-4. An array type does not contain additional holes because all other types pack tightly when composed into arrays [at page 51].

  • [8]

    ^WG14 N1124, C – Approved standards: ISO/IEC 9899 – Programming languages – C, 2005-05-06..

  • [9]

    ^us patent 6625718, Steiner, Robert C. (Broomfield, CO), "Pointers that are relative to their own present locations", issued 2003-09-23, assigned to Avaya Technology Corp. (Basking Ridge, NJ).

  • [10]

    ^us patent 6115721, Nagy, Michael (Tampa, FL), "System and method for database save and restore using self-pointers", issued 2000-09-05, assigned to IBM (Armonk, NY).

  • [11]

    ^"Based Pointers". Msdn.microsoft.com. Retrieved 2018-04-13..

  • [12]

    ^Ulf Bilting, Jan Skansholm, "Vägen till C" (the Road to C), third edition, page 169, ISBN 91-44-01468-6.

  • [13]

    ^Nick Parlante, [1], Stanford Computer Science Education Library, pp. 9–10 (2000)..

  • [14]

    ^ISO 7185 Pascal Standard (unofficial copy), section 6.4.4 Pointer-types and subsequent..

  • [15]

    ^J. Welsh, W. J. Sneeringer, and C. A. R. Hoare, "Ambiguities and Insecurities in Pascal," Software Practice and Experience 7, pp. 685–696 (1977).

  • [16]

    ^Free Pascal Language Reference guide, section 3.4 Pointers.

  • [17]

    ^Contact details. "// Making References (Perl References and nested data structures)". Perldoc.perl.org. Retrieved 2018-04-13..

阅读 1.2w
版本记录
  • 暂无