计算机的内存是由数以亿万计的位(bit)组成,每一个位可以容纳值0、1值。由于一个位所能表示的值的范围太有限,所以单独的位用处不大。,通常许多为合成一组作为一个单位,这样就可以存储范围较大的值。
这些位置的每一个都被称为字节(byte),每个字节都包含了存储一个字符所需要的位数。在现在的许多机器上每一个字节包含8个位,可以存储无符号值0-255,或者有符号值-128--127,。
之后为了存储更大的值,我们把两个或更多的字节合在一起作为一个更大的内存单位。例如,机器以字为单位存储整数,每一个字一般有2至4个字节组成。下面这个图所表示的内存位置以4个字节来表示。
由于它们包含了更多的位,每一个字可以容纳无符号整数的范围是从0-4294967295(或者是2的32次方-1),或者是有符号的范围(-2147483648至2147483647(或者说是负2的31次方至2的31次方-1))。
这里有一个例子,上边的数字是内存地址,下边的是对应的内容:
如果你记得这几个数的地址你就可以根据地址来用这些值,但是要记住地址太难了,地址有很多,很复杂,所以不切实际。所以可以通过名字而不是地址来访问内存的位置。如下图所示:
有一点很重要,名字与内存位置之间的关联并不是硬件提供的,它是由编译器实现的。所以这些变量给了我们一种更方便的方法记住地址————硬件仍然通过地址访问内存位置。
现在让我们来看一下存储于这些位置的值。
int a = 112,b = -1;
float c = 3.14;
int *d = &a;
float *e = &c;
在这个声明中,a和b确实是存储整型变量的值,一个是112,一个是-1,但是c声明的浮点型变量。但是在图中却是一个整数。那么到底是一个什么样的数呢?答案是该变量包含了一系列内容为0或1 的位,他们可以解释为整数,也可以被解释为浮点数,这取决于他们被使用的方式,如果使用的是整型算术指令,这个值就解释为整数,如果使用的浮点数类型的指令,就解释为浮点数。
这个事实引出了一个重要的结论:不能简单的通过检查一个值的位来判断他的类型。
接下来看刚才的指针声明。他们被声明为指针,并且用其他变量的地址予以初始化。
d和e的内容是指定的地址。区分变量d的地址(112)和他的内容(100)是非常重要的。要意识到,100这个数值用于标识其他位置。
a的值是112;b的值是-1;c的值是3.14;d的值是100;e的值是108;
下面这个代码段说明了一个极为常见的错误:
int *a;
...
*a = 12;
这个声明创建了一个名叫a的指针变量,后面那条赋值语句把12存储在a所指向的内存位置。
声明这个变量但是并没有对他进行初始化,所以我们没有办法预测12这个值将存储于什么地方。如果变量是静态的,他会被初始化为0;如果是自动的,他根本不会被初始化。无论是那种情况,声明一个指向整型的指针都不会创建用于存储整型值的内存空间。
所以,如果程序执行这个赋值操作,1)a的初始值会是一个非法地址,这样的赋值语句将会出错,从而终止程序。它提示程序试图访问一个并未分配给程序的内存位置。2)一个更为严重的错误是,这个指针包含一个合法的地址,那么位于那个地址的值就会被修改。所以,在你对指针进行间接访问之前,必须非常小心,确保他们已经被初始化。
标准定义了NULL指针,作为一个特殊的指针变量,不指向任何东西。要使一个指针变量为NULL,你可以给他赋一个零值。为了测试一个指针变量是否为NULL,你可以将它与零值进行比较。之所以选择零值,是因为一种源代码约定。
让我们来分析另外一个表达式。假设变量a存储于位置100,那么下面这条语句:
*100 = 25;
这个语句看上去像是把25赋值给a,因为a是位置100所存储的变量。但是这是错的,因为字面值100的类型是整数,而间接访问操作只能作用于指针类型表达式。如果你确实想把25存储于位置100,你必须使用强制类型转换:
*(int *)100 = 25;
强制类型转换把值100从整型转换为指向整型的指针。这样对他进行访问就是合法的。
假定进行了如下声明:
int a = 12;
int *b = &a;
int **c = &b;
a是常量,b是指向整型变量的指针,c是指向“指向整型变量的指针”的指针,就是指向指针的指针。
*操作符具有从右向左的结合性,所以这个表达式相当于*(*c),我们必须从里向外求值。
现在让我们观察各种不同的指针表达式,并看一下当他们分别作为左值和右值时是如何进行求值的。
首先看一些声明:
char ch = 'a';
char *cp = &ch;
他们的关系如图所示:
图中还显示了ch后面的内存位置,由于我们不知道它的初始值,所以用一个问号代替。
当ch作为右值使用时,表达式的值表示内容a;当它作为左值使用时,它是这个内存的地址而不是该地址包含的值,所以他的图示有所不同:
ch作为右值:
ch作为左值:(它的值并未显示,因为他不重要)
下面我们对几种表达式求值进行分析:
(1)
作为右值,这个表达式是求变量ch的地址,这个值和cp的内容一样,都是指向ch 的地址,但是与cp没有什么关系。第二个问题是,他不能作为左值使用,因为这个表达式没有标识任何机器内存的特定位置,所以他不是一个合法的左值。
(2)
在这个表达式中,cp作为右值的内容就是ch的地址,作为左值就是指针变量cp的地址。
(3)
这个表达式中,作为右值表示指针变量cp的地址,结果是指向“指向字符指针”的指针;作为左值这个值的存储位置没有清楚定义,所以非法。
(4)
这个表达式中,*cp作为右值得到的是指针指向的地址中的内容;作为左值得到的是指向另一个的地址。
(5)
这个图中,表达式作为右值,因为*运算符的优先级高于+,所以先进行*cp求解,得到虚线中的值a,然后执行加1操作,得到下一个值b。表达式作为左值,这个表达式的最终存储位置没有清晰定义,所以他不是一个合法的左值。优先级的表格证实+的结果不能作为左值。
(6)
这个表达式先进行括号内的运算,作为右值时,cp + 1 为图中虚线椭圆,表示ch后面的内存位置中的内容;作为左值时,就是表示这个位置的本身地址。在这里,指针加法运算的结果是一个右值,因为他的存储位置并未清晰定义。如果没有间接访问操作,这个表达式将不是一个合法的值,然而有了间接访问之后,指针访问了一个特定的位置,这样表达式*(cp + 1)就可以作为左值使用。
(7)
在这个表达式中,cp为指向ch的指针,这个表达式是增值后指针的一份拷贝,前缀++先增加了操作数,然后再返回这个结果,增加了指针变量cp的值,指向了ch后一个单位的内存位置。作为左值,存储位置没有清楚定义,所以不能作为合法左值。
(8)
这个表达式,作为左值,先是返回cp的一份拷贝然后再增加cp的值,也就是图中实线椭圆,这样这个表达式的值就是cp原来的值的一份拷贝;作为左值,作为左值,存储位置没有清楚定义,所以不能作为合法左值。但是如果我们在表达式中增加了间接访问操作符,他们就可以成为合法的左值,如下图所示。
(9)
在这里,间接访问操作符作用于增值后的指针的拷贝上,所以它的右值是ch后面那个内存地址的值,而他的左值就是那个位置本身。
(10)
这个表达式中,在右值中,先是cp++,返回cp的拷贝然后进行增值,返回拷贝之后进行解引用,那么指的就是cp指针指向的内容;在左值中,指向ch的内存位置,也就是地址。
(11)
在这个表达式中,由于这两个操作符的结合性都是从右向左,所以首先执行的间接访问操作。然后,cp所指向的位置的值增加1,也就是a增加1成为b,表达式的结果是这个增值后的值的一份拷贝;左值时,没有一个确定的内存位置,所以不能用作左值。
(12)
这个表达式中,显示求括号内的表达式,右值时,先是求*cp得到a,然后返回值a,最后进行加1操作;
(13)
这个表达式中,这里边的操作符的结合性是从右向左的,,所以首先执行的是++cp,然后返回cp指向的内存地址的拷贝增值,也就是后一个内存位置,然后解引用,得到后一个位置的内容,然后对后一个位置的内容进行增值操作。
(14)
在这个表达式中,由于符号的优先级问题,先执行最后的++操作,然后得到cp指针的拷贝,然后在进行增值操作,然后进行拷贝的解引用得到指针指向的值a,然后进行增值操作得到b。
实例(1):
/*
计算一个字符串的长度
*/
#include
size_t strlen ( char *string )
{
int length = 0;
while ( *string++ != '\0' ){
length ++;
}
return length;
}
实例(2):
下面的两个函数增加了一层间接访问,他们在一些字符串中搜索某个特定的字符值,我们使用指针数组来表示这些字符串,如图所示:
函数的参数是strings和value,strings是一个指向指针数组的指针,value使我们要搜索的值,注意指针数组以一个NULL指针结束。函数将检查这个值以判断循环何时结束。
/*
给定一个指向以NULL结尾的指针列表的指针,在列表中的字符串中查找一个特定的字符。
*/
#include
#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;
}
之前的博客文章中都有介绍这里就不赘述了。
C的指针算术运算只限于两种形式。
第一种形式是:指针 加减 整数
第二种形式是:指针 减 指针
只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。
两个指针相减的结果的类型是ptrdiff_t, 它是一种有符号整数类型,减法运算的值是两个指针在内存中的距离(以数组元素的长度为单位,而不是以字节为单位),因为减法运算的结果将除以数组元素类型的长度。