在高级语言中,C是相当低级的,接近机器。这主要是因为它有显式指针。指针值是内存中数据的地址。该地址处数据的类型由指针本身的数据类型指定。一元运算符“*”获取指针指向的数据。这称为指针解引用。
C还允许指向函数的指针,但由于它们的工作方式存在一些差异,因此我们稍后再处理它们。
创建指针的最基本方法是使用取址操作符“&”。假设我们有以下变量可用:
int i;
double a[5];
现在,&i给出了变量i的地址——一个指向i位置的指针值,而&a[3]给出了a的3号元素的地址(它实际上是数组中的第四个元素,因为第一个元素的索引为0)
取址运算符是不寻常的,因为它在存储值的位置上操作,而不是在当前存储的值上操作(简单赋值的左侧参数也以同样的方式不同寻常)。您可以在任何左值上使用它,但位域或构造函数除外。
对于每个数据类型t,存在指向类型t的指针的类型。对于这些变量,
int i;
double a[5];
声明变量foo指向类型t的方法是
t *foo;
要记住这种语法,请考虑“如果使用“*”运算符取消对foo的引用,得到的东西是类型t。从而,foo指向类型t。”
因此,我们可以声明包含指向这三种类型的指针的变量,如下所示:
int *ptri; /* Pointer to int. */
double *ptrd; /* Pointer to double. */
double (*ptrda)[5]; /* Pointer to double[5]. */
int *ptri;的意思是,如果你取消引用,你会得到一个整数。double(*ptrda)[5]; 的意思是,如果你取消ptrda的引用,然后用小于5的下标访问它,你会得到一个double。括号表示你要先取消引用,然后再使用下标。
将最后一个与以下内容进行对比:
double *aptrd[5]; /* Array of five pointers to double. */
它声明了一个指针数组,而不是一个指针。
C中的每个类型都有一个指示符;通过从声明中删除变量名和分号来实现。以下是上一节中声明的指针类型的指示符:
int * /* Pointer to int. */
double * /* Pointer to double. */
double (*)[5] /* Pointer to double[5]. */
指针值的主要用途是使用一元“”运算符访问它指向的数据。例如,&i是i的地址处的值,即i。只要&i有效,这两个表达式是等价的。
类型为数据(不是函数)的指针解引用表达式是左值。
当我们将指针存储在某个地方并稍后使用时,指针变得非常有用。下面是一个简单的例子来说明这种做法:
{
int i;
int *ptr;
ptr = &i;
i = 5;
…
return *ptr; /* Returns 5, fetched from i. */
}
这显示了如何将变量ptr声明为int*(指向int的指针),将指针值存储到其中(指向i),然后使用它获取它指向的对象的值(i中的值)。
指针值可以为空,这意味着它不指向任何对象。获取空指针的最干净方法是写NULL,一个在stddef.h中定义的标准宏。也可以通过将0强制转换为所需的指针类型进行此操作,如(char*)0。(强制转换运算符执行显式类型转换;请参阅显式类型转换。)
可以将空指针存储在数据类型为指针类型的任何左值中:
char *foo;
foo = NULL;
这两个如果是连续的,可以组合成带有初始值设定项的声明,
char *foo = NULL;
您还可以显式地将NULL强制转换为所需的特定指针类型。这没有任何区别。
char *foo;
foo = (char *) NULL;
要测试指针是否为空,请将其与零或NULL进行比较,如下所示:
if (p != NULL)
/* p is not null. */
operate (p);
由于测试指针是否为空是基本且频繁的,所以除了C中的初学者之外,所有人都理解条件表达式,因而无需!=NULL:
if (p)
/* p is not null. */
operate (p);
尝试解空指针是错误的。在大多数平台上,它通常会产生SIGSEGV信号(参见信号)。
char *foo = NULL;
c = *foo; /* This causes a signal and terminates. */
同样,指针与目标数据类型对齐错误(在大多数类型的计算机上),或指向尚未在进程的地址空间中分配的内存部分。
信号终止程序,除非程序已安排处理信号。
然而,如果解引用被优化掉,则信号可能不会发生。在上面的示例中,如果您随后不使用c的值,GCC可能会优化掉*foo的代码。您可以使用volatile限定符阻止此类优化,如下所示:
volatile char *p;
volatile char c;
c = *p;
您可以使用它来测试p是否指向未分配的内存。首先设置信号处理程序,以便信号不会终止程序。
特殊类型void*,一个目标类型为void的指针,在C中经常使用。因此
void *numbered_slot_pointer (int);
声明一个函数numbered_slot_pointer,该函数接受一个整数参数并返回一个指针,但我们不说它指向什么类型的数据。
使用类型void*,可以四处传递指针并测试它是否为空。但是,取值引用会给出一个无法使用的void值。要取值指针,应首先将其转换为其他指针类型。
如果左操作数具有指针类型,则赋值会自动将void*转换为任何其他指针类型;例如,
{
int *p;
/* Converts return value to int *. */
p = numbered_slot_pointer (5);
…
}
为具有指针类型的参数传递void类型的参数也会转换。例如,假设函数hack被声明为需要类型float作为其参数,这将把空指针转换为该类型。
/* Declare hack that way.
We assume it is defined somewhere else. */
void hack (float *);
…
/* Now call hack. */
{
/* Converts return value of numbered_slot_pointer
to float * to pass it to hack. */
hack (numbered_slot_pointer (5));
…
}
您还可以使用显式转换强制转换为另一种指针类型,如下所示:
(int *) numbered_slot_pointer (5)
下面是一个示例,它在运行时决定要转换为哪种指针类型:
void
extract_int_or_double (void *ptr, bool its_an_int)
{
if (its_an_int)
handle_an_int (*(int *)ptr);
else
handle_a_double (*(double *)ptr);
}
表达式*(int*)ptr的意思是将ptr转换为int*类型,然后解引用。
如果两个指针值指向同一位置,或者都为空,则它们相等。您可以使用==和!=来测试这一点。下面是一个微不足道的例子:
{
int i;
int *p, *q;
p = &i;
q = &i;
if (p == q)
printf ("This will be printed.
");
if (p != q)
printf ("This won't be printed.
");
}
排序比较,如>和>=通过将指针转换为无符号整数对指针进行操作。C标准规定两个指针必须指向内存中的同一对象,但在GNU/Linux系统上,这些操作只是比较指针的数值。
要比较的指针值原则上应具有相同的类型,但在有限的情况下允许不同。首先,如果两个指针的目标类型几乎兼容(请参见兼容类型),则允许进行比较。
如果其中一个操作数是void*,另一个是另一种指针类型,则比较运算符将void*指针转换为另一种类型,以便进行比较。(在标准C中,如果另一种类型是函数指针类型,则是不允许的,但在GNU C中是有效的。)
比较运算符还允许将整数0与指针值进行比较。因此,通过将0转换为与其他操作数类型相同的空指针来工作。
将整数(正或负)添加到指针在C中是有效的。它假设指针指向数组中的某个元素,并在整数指定的任意数量的数组元素上前进或撤回指针。下面是一个示例,其中添加正一个整数会将指针推进到同一数组中的后面元素。
void
incrementing_pointers ()
{
int array[5] = { 45, 29, 104, -3, 123456 };
int elt0, elt1, elt4;
int *p = &array[0];
/* Now p points at element 0. Fetch it. */
elt0 = *p;
++p;
/* Now p points at element 1. Fetch it. */
elt1 = *p;
p += 3;
/* Now p points at element 4 (the last). Fetch it. */
elt4 = *p;
printf ("elt0 %d elt1 %d elt4 %d.
",
elt0, elt1, elt4);
/* Prints elt0 45 elt1 29 elt4 123456. */
}
下面是一个示例,添加负整数将后撤指针到同一数组中较早元素的指针。
void
decrementing_pointers ()
{
int array[5] = { 45, 29, 104, -3, 123456 };
int elt0, elt3, elt4;
int *p = &array[4];
/* Now p points at element 4 (the last). Fetch it. */
elt4 = *p;
--p;
/* Now p points at element 3. Fetch it. */
elt3 = *p;
p -= 3;
/* Now p points at element 0. Fetch it. */
elt0 = *p;
printf ("elt0 %d elt3 %d elt4 %d.
",
elt0, elt3, elt4);
/* Prints elt0 45 elt3 -3 elt4 123456. */
}
如果一个指针值是通过将一个整数添加到另一指针值而生成的,则它应该可以减去指针值并恢复该整数。在C语言中也是如此。
void
subtract_pointers ()
{
int array[5] = { 45, 29, 104, -3, 123456 };
int *p0, *p3, *p4;
int *p = &array[4];
/* Now p points at element 4 (the last). Save the value. */
p4 = p;
--p;
/* Now p points at element 3. Save the value. */
p3 = p;
p -= 3;
/* Now p points at element 0. Save the value. */
p0 = p;
printf ("%d, %d, %d, %d
",
p4 - p0, p0 - p0, p3 - p0, p0 - p3);
/* Prints 4, 0, 3, -3. */
}
加法操作不知道数组的位置。它所做的只是将整数(乘以对象大小)添加到指针的值。当初始指针和结果指向同一个数组时,结果是对的。
警告:只有专家才应该进行指针运算,包括指向不同内存对象的指针。
指针减法的定义与指针整数加法一致,因为(p3-p1)+p1等于p3,就像在普通代数中一样。
在标准C中,void*不允许加减,因为在这种情况下没有定义目标类型的大小。同样,函数类型的指针不允许使用它们。然而,这些操作在GNU C中工作,并且“目标类型的大小”被取为1。
引用数组元素的干净方法是array[index]。完成相同工作的另一种复杂方法是获取该元素的地址作为指针,然后将其解引用:(&array[0]+index)(或等价地(array+index))。这首先获取指向元素零的指针,然后将其递增以指向所需的元素,然后从那里获取值。
该指针算术结构是C中方括号的定义。a[b]定义为*(a+b)。该定义对称使用a和b,因此一个必须是指针,另一个必须为整数;哪个先来并不重要。
由于方括号索引是根据加法和去引用来定义的,这也是对称的。因此,您可以写3[array],它相当于array[3]。然而,写3[array]是愚蠢的,因为它没有任何优势,并且可能会混淆阅读代码的人。
定义*(a+b)需要一个指针,但array[3]使用的是数组值,这似乎是一个矛盾。为什么这是有效的?当数组本身用作表达式时(而不是在sizeof中),数组的名称表示指向数组第零个元素的指针。因此,array+3隐式地将数组转换为&array[0],结果是指向元素3的指针,相当于&array[3]。
由于方括号是根据这种加法定义的,所以array[3]首先将数组转换为指针。这就是为什么在该构造中直接使用数组是可行的。
指针算术的行为在理论上仅在指针值都指向内存中分配的一个对象内时才定义。但是加法和减法运算符不能判断指针值是否都在一个对象内。他们不知道对象的起点和终点。那幺他们真正做了什么呢?
将指针p添加一个整数i,将p视为内存地址,实际上它是一个整数,称之为pint。它将i视为p所指向的元素的数量。这些元素的大小加起来就是i*sizeof(p)。因此,作为整数的总和是pint+isizeof(*p)。该值被重新解释为指针,如p。
如果起始指针值p和结果不指向同一对象的各部分,则该操作不是正式合法的,C代码也不“应该”这样做。但无论如何,您都可以这样做,它准确地给出了上述过程所描述的结果。在某些特殊情况下,它可以做一些有用的事情,但非巫师应该避免。
下面是一个函数,通过显式执行该计算来偏移指针值,就好像它指向任何给定大小的对象一样:
#include
void *
ptr_add (void *p, int i, int objsize)
{
intptr_t p_address = (long) p;
intptr_t totalsize = i * objsize;
intptr_t new_address = p_address + totalsize;
return (void *) new_address;
}
“++”运算符将变量加1。我们已经看到它适用于整数,但它也适用于指针。例如,假设我们有一系列以零结尾的正整数,我们想把它们全部相加。
int
sum_array_till_0 (int *p)
{
int sum = 0;
for (;;)
{
/* Fetch the next integer. */
int next = *p++;
/* Exit the loop if it’s 0. */
if (next == 0)
break;
/* Add it into running total. */
sum += next;
}
return sum;
}
语句“break;”将在后面进一步解释。以这种方式使用时,它会立即退出周围的for语句。
p++解析为(p++),由于p是指针,向其加一将使其前进一个数据的宽度,在本例中为一个整数。因此,循环的每次迭代都从序列中提取下一个整数并将其放入下一个。
这个for循环没有初始化表达式,因为p和sum已经初始化,它没有结束测试,因为'break;'语句将退出它,并且不需要表达式来推进它,因为这是在循环中通过递增p和sum来完成的。因此,for后面的三个表达式留空。
此函数的另一种写法是保持参数值不变,并使用索引访问表中的整数。
int
sum_array_till_0_indexing (int *p)
{
int i;
int sum = 0;
for (i = 0; ; i++)
{
/* Fetch the next integer. */
int next = p[i];
/* Exit the loop if it’s 0. */
if (next == 0)
break;
/* Add it into running total. */
sum += next;
}
return sum;
}
在这个程序中,我们不是推进p,而是推进i并将其加到p上,无论哪种方式,它都使用相同的地址来获取下一个整数。
“--”运算符也适用于指针;它可以用于向后扫描数组,如下所示:
int
after_last_nonzero (int *p, int len)
{
/* Set up q to point just after the last array element. */
int *q = p + len;
while (q != p)
/* Step q back until it reaches a nonzero element. */
if (*--q != 0)
/* Return the index of the element after that nonzero. */
return q - p + 1;
return 0;
}
指针算法干净优雅,但它也是C语言中一个主要安全缺陷的原因。从理论上讲,仅在内存中作为一个单元分配的一个对象内调整指针是有效的。但是,如果您无意中调整指针跨越对象边界并进入其他对象,系统将无法检测此错误。
这样做的bug很容易破坏另一个对象的部分数据。例如,使用array[-1],您可以在数组开始之前读取或写入不存在的元素,这可能是其他数据的一部分。
将指针算术与指针类型之间的强制转换相结合,可能创建一个无法与其类型正确对其的指针。例如
int a[2];
char *pa = (char *)a;
int *p = (int *)(pa + 1);
警告:使用不正确对齐的指针是有风险的。除非确实需要,否则不要这样做。
在现代计算机上,地址只是一个数字。它占用的空间与整数的大小相同。在C中,您可以将指针转换为适当的整数类型,反之亦然,而不会丢失信息。适当的整数类型是uintptr_t(无符号类型)和intptr_ t(有符号类型)。两者都在stdint.h中定义。
例如,
#include
#include
void
print_pointer (void *ptr)
{
uintptr_t converted = (uintptr_t) ptr;
printf ("Pointer value is 0x%x
",
(unsigned int) converted);
}
printf模板(第一个参数)中的“%x”表示使用十六进制表示法表示此参数。使用uintptr_t更干净,因为十六进制打印将数字视为无符号,但实际上并不重要:printf看到的只是数字中的一系列位。
警告:将指针转换为整数是有风险的 - 除非确实有必要,否则不要这样做。
要打印指针的数值,请使用“%p”说明符。例如:
void
print_pointer (void *ptr)
{
printf ("Pointer value is %p
", ptr);
}
规范“%p”适用于任何指针类型。它打印“0x”,后跟十六进制地址,打印为适当的无符号整数类型。
留言与评论(共有 0 条评论) “” |