值和类型
下面有一个例子,它显示了内存中的5个字的内容。
下面是这些变量的声明:
int a = 112, b = -1;
float c = 3.14;
int *d = &a;
float *e = &c;
- c所声明的存储类型是浮点值,但上图中c却是一个整数。因为该变量包含了一序列内容为0或1的位,它们可以被解释为整数,也可以被解释为浮点数,这取决于它们被使用的方式。
- 不能简单地通过检查一个值的位来判断它的类型。
指针变量的内容
指针的初始化是用&操作符完成的,它用于产生操作数的内存地址。
在上图定义的5个字中,d和e的内容是地址而不是整型或浮点型数值。事实上,d的内容与a的存储地址一致,而e的内容与c的存储地址一致。
间接访问操作符
通过一个指针访问它所指向的地址的过程称为间接访问(indirection)或解引用指针(dereferencing the pointer)。这个用于执行间接访问的操作符是单目操作符*。
举个例子:
表达式 | 右值 | 类型 |
---|---|---|
a | 112 | int |
b | -1 | int |
c | 3.14 | float |
d | 100 | int * |
e | 108 | float * |
*d | 112 | int |
*e | 3.14 | float |
- 其中,d的值是100。当我们对d使用间接访问操作符时,它表示访问内存位置100并察看那里的值,因此,*d的右值是112——位置100的内容,它的左值是位置100本身。
未初始化和非法的指针
下面这段代码段说明了一个极为常见的错误:
int *a;
...
*a = 12;
- 这个声明创建了一个名叫a的指针变量,后面那条赋值语句把12存储在a所指向的内存位置。
- 但是究竟a指向哪里呢?我们声明了这个变量,但从未对它进行初始化,所以我们没有办法预测12这个值将存储于什么地方,声明一个指向整型的指针都不会“创建”用于存储整型值的内存空间。
NULL指针
标准定义了NULL指针,它作为一个特殊的指针变量,表示不指向任何东西。要使一个指针变量为NULL,你可以给它赋一个零值。 为了测试一个指针变量是否为NULL,你可以将它与零值进行比较。 之所以选择零这个值是因为一种源代码约定。
- 由于NULL指针并未指向任何东西,因此,对一个NULL指针进行解引用操作是非法的。在对指针进行解引用操作之前,你首先必须确保它并非NULL指针。
指针、间接访问和左值
指针变量可以作为左值,并不是因为它们是指针,而是因为它们是变量。对指针变量进行间接访问表示我们应该访问指针所指向的位置。间接访问指定了一个特定的内存位置,这样我们可以把间接访问表达式的结果作为左值使用。在下面这几条语句中,
int a;
int *d = &a;
*d = 10 - *d;
d = 10 - *d;
- 第1条语句包含了两个间接访问操作。右边的间接访问作为右值使用,所以它的值是d所指向的位置所存储的值(a的值)。左边的间接访问作为左值使用,所以d所指向的位置(a)把赋值赋右侧的表达式的计算结果作为它的新值。
- 第2条语句是非法的,因为它表示把一个整型数量(10 - *d)存储于一个指针变量中。
指针、间接访问和变量
*&a = 25; //把值25赋值给变量a
- 首先,&操作符产生变量a的地址,它是一个指针常量(注意,使用这个指针常量并不需要知道它的实际值)。接着,*操作符访问其操作数所表示的地址。在这个表达式中,操作数是a的地址,所以值25就存储于a中。
指针常量
*100 = 25;
- 这条语句是非法的,因为字面值100的类型是整型,而间接访问操作只能作用于指针类型表达式。如果你确实想把25存储于位置100,你必须使用强制类型转换。
(int *)100 = 25;
指针的指针
考虑下面这样的声明:
int a = 12;
int *b = &a;
c = &b;
- 变量b是一个“指向整型的指针”,所以任何指向b的类型必须是指向“指向整型的指针”的指针,更通俗地说,是一个指针的指针。它是合法的,和其他变量一样,占据内存中某个特定的位置,所以用&操作符取得它的地址是合法的。
它将进行如下的声明:
int **c;
- 表示表达式**c的类型是int。
实例
程序1
程序1计算一个字符串的长度。
/*
** 计算一个字符串的长度
*/
#include <stdlib.h>
size_t
strlen( char *string )
{
int length = 0;
/*
** 依次访问字符串的内容,计数字符数,知道遇见NUL终止符。
*/
while( *string++ != '\0')
length += 1;
return length;
}
- 在指针到达字符串末尾的NUL字节之前,while语句中
*string++
表达式的值一直为真。它同时增加指针的值,用于下一次测试。这个表达式甚至可以正确地处理空字符串。
程序2
程序2和程序3增加了一层间接访问。它们在一些字符串中搜索某个特定的字符值,但我们使用指针数组来表示这些字符串,如下图所示。函数的参数是strings和value,strings是一个指向指针数组的指针,value是我们所查找的字符值。注意指针数组以一个NULL指针结束。 函数将检查这个值以判断循环何时结束。
while( ( string = *strings++ ) != NULL ){
- 上面这行表达式完成了三项任务:(1) 它把strings当前所指向的指针复制到变量string中。(2) 它增加strings的值,使它指向下一个值。(3) 它测试string是否为NULL。当string指向当前字符串中作为终止标志的NUL字节时,内层的while循环就终止。
/*
** 给定一个指向以NULL结尾的指针列表的指针,在列表中的字符串中查找一个特定的字符。
*/
#include <stdio.h>
#define TRUE 1
#define FALSE 0
int
find_char( char **strings, char value )
{
char *string; // 我们当前正在查找的字符串
// 对于列表中的每个字符串...
while( ( string = *strings++ ) != NULL ){
// 观察字符串中的每个字符,看看它是不是我们需要查找的那个。
while( *string != '\0' ){
if( *string++ == value )
return TRUE;
}
}
return FALSE;
}
- 如果string尚未到达其结尾的NUL字节,就执行下面这条语句
if( *string++ == value )
- 它测试当前的字符是否与需要查找的字符匹配,然后增加指针的值,使它指向下一个字符。
程序3
程序3实现与程序2相同的功能,但它不需要对指向每个字符串的指针作一份拷贝。但是,由于存在副作用,这个程序将破坏这个指针数组。这个副作用使该函数不如前面那个版本有用,因为它只适用于字符串只需要查找一次的情况。
/*
** 给定一个指向以NULL结尾的指针列表的指针,在列表中的字符串中查找一个特定的字符。这个函数将破坏这些指针,所以它只适用于这组字符串只使用一次的情况。
*/
#include <stdio.h>
#include <assert.h>
#define TRUE 1
#define FALSE 0
int
find_char( char **strings, char value )
{
// 若string == NULL,则终止程序
assert( strings != NULL );
// 对于列表中的每个字符串...
while( *strings != NULL ){
// 观察字符串中每个字符,看看它是否是我们查找的那个。
while( **strings != '\0' ){
if( *(*strings)++ == value )
return TRUE;
}
strings++;
}
return FALSE;
}
- 程序3中存在两个有趣的表达式。第1个是
**strings
。第1个间接访问操作访问指针数组中的当前指针,第2个间接访问操作随该指针访问字符串中的当前字符。内层的while语句测试这个字符的值并观察是否到达了字符串的末尾。 - 第2个有趣的表达式是
*(*strings)++
。括号是需要的,这样才能使表达式以正确的顺序进行求值。第1个间接访问操作访问列表中的当前指针。增值操作把该指针所指向的那个位置的值加1,但第2个间接访问操作作用于原先那个值的拷贝上。这个表达式的直接作用是对当前字符串中的当前字符进行测试,看看是否到达了字符串的末尾。作为副作用,指向当前字符串字符的指针值将增加1。
指针运算
指针加上一个整数的结果是另一个指针。问题是,它指向哪里?如果你将一个字符指针加1,运算结果产生的指针指向内存中的下一个字符。 float占据的内存空间不止1个字节,如果你将一个指向float的指针加1,将会发生什么呢?它会不会指向该float值内部的某个字节呢?
幸运的是,答案是否定的。当一个指针和一个整数量执行算数运算时,整数在执行加法运算前始终会根据合适的大小进行调整。 这个“合适的大小”就是指指针所指向类型的大小,“调整”就是把整数值和“合适的大小”相乘。
算数运算
C的指针算数运算只限于两种形式。
第1种形式是:
指针 ± 整数
- 标准定义这种形式只能用于指向数组中某个元素的指针,并且这类表达式的结果类型也是指针。
- 数组中的元素存储于连续的内存位置中,后面元素的地址大于前面元素的地址。因此,对一个指针加1使它指向数组中的下一个元素,相反减1则使它指向数组中的上一个元素。
- 对指针执行加法或减法运算之后如果结果指针所指的位置在数字第1个元素的前面或在数组最后一个元素的后面,那么其效果就是未定义的。
这里有一个例子,循环把数组中所有的元素都初始化为零。
#define N_VALUES 5
flaot values[N_VALUES];
float *vp;
for( vp = &value[0]; vp < &value[N_VALUES]; )
*vp++ = 0;
- 这个例子中的指针最后所指向的是数组最后一个元素后面的那个内存位置。指针可能可以合法地获得这个值,但对它执行间接访问时将可能意外地访问原先存储于这个位置的变量。因此,一般不允许对指向这个位置的指针执行间接访问操作。
第2种类型的指针运算具有如下形式:
指针 - 指针 - 只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。
- 两个指针相减的结果的类型是ptrdiff_t,它是一种有符号整数类型。减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。 例如,如果p1指向array[i]而p2指向array[j],那么p2-p1的值就是j-i的值。
关系运算
对指针执行关系运算也是有限制的。用下列关系操作符对两个指针值进行比较是可能的:
< <= > >=
不过前提是它们都指向同一个数组中的元素。
- 根据你所使用的操作符,比较表达式将告诉你哪个指针指向数组中更前或更后的元素。
- 你可以在两个任意的指针间执行相等或不相等测试,因为这类比较的结果和编译器选择在何处存储数据并无关系——指针要么指向同一个地址,要么指向不同的地址。