4. 指针和数组
到目前为止,本章中关于指针使用的例子仅限于指针指向某一简单变量的情况。
实际上,指针可以指向任何左值。特别是,数组元素是左值,所以也有地址。
数组声明
double list[3];
预留了三个连续的内存单元,每个单元的大小都足以存放一个double类型的值。
假设double类型为8个字节长,该数组占用的内存如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/7b033c2ee970bde8c8b50bafddea2f41.png)
三个数组元素都有地址,这些地址可使用&运算符得到。
例如,表达式
&list[1]
的指针值为1008,因为元素list[1]
存储于该地址。
此外,下标值可以不是常数。例如,表达式
&list[i]
表示list
中第i
个元素的地址。
由于list
中第i
个元素的地址取决于变量i
的值,所以C语言编译器在编译程序时是无法计算这一地址的。
为了确定这一地址,编译器产生一段指令,取数组的基地址,加上适当的偏移量。
偏移量是将i
的值乘以每个数组元素的字节数而得到的。所以,计算list[i]
的地址可以由公式:
1000+i×8
得到。
4.1 指针运算
运算符+和-,在C语言中,也可以应用于指针。
对指针值应用加减的过程称为指针运算(pointer arithmetic)。
![](https://i-blog.csdnimg.cn/blog_migrate/a6a5e15e803a2cd79bd865d2170f5d70.png)
换句话说,如果在指针值上加上一个整数k,则结果就是下标为k的元素的地址。
为了解释指针运算规则,假设有一个函数包含如下所示的变量声明:
double list[3];
double *p;
在函数帧内给每个变量都分配了空间。对数组变量list
,编译器为它的三个元素都分配了足够的空间以便存放一个double型的数据。
对指针p
,编译器为其分配了足够空间来存放某个double类型的左值的地址。
如果帧从地址1000开始,内存分配如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/42442bdc9cf1d6502b85eec194a10b60.png)
使用以下赋值语句将值存储到每个数组元素中:
list[0] = 1.0;
list[1] = 1.1;
list[2] = 1.2;
初始化指针变量p
,使其指向数组的起始地址,执行以下赋值语句:
p = &list[0];
此时,内存单元中的存放情况,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/008984da1a31dd14609e1e07f8be5457.png)
如果程序包含表达式:
p+2
表达式求得的值将是指向list[2]
的新指针值。
所以,p
指向地址1000,p+2
指向数组中该元素后出现的第二个元素的地址,即地址1016。
需要注意的是,指针加法和传统加法是不同的,因为指针运算必须考虑到基本类型的大小。
在本例中,由于每个double型需要8个字节,所以指针值每增加1个单位,内部数值必须增加8。
重要的是认识到指针运算被定义为自动考虑基本类型的大小。
给定任何指针p
和整数k
,则表达式
p+k
意味着不管每个元素需要多少内存,结果总是数组中p
目前指向的地址后第k
个元素的指针。
算术运算符*、/和%对指针来说是没有意义的,不能和指针操作数一起使用。
而且,+和-的使用也是有限的。在C语言中,可以给一个指针加上或减去一个整数偏移量,但不能将两个指针相加。
其他唯一可用于指针的算术操作是将两个指针相减。在表达式
p1-p2
中,p1
和p2
是指针,用于返回p2
和p1
当前值之间的数组元素的个数。
例如,如下图所示,指针p1
指向list[2]
,p2
指向list[0]
,则表达式:
p1-p2
的值为2,因为当前两指针值之间有2个元素。
理解这个定义的另一种方法是将指针相减所得的值通过赋值语句赋给一个整型变量k
:
k=p1-p2;
它将满足以下关系式:
p1==p2+k
4.2 运算符++和–的新作用
重新看一下自增和自减运算符,++和–。这两个运算符都可以用两种方式写:
一种是将运算符写在操作数后,即
x++
另一种是将运算符写在操作数前,即
++x
第一种形式,即运算符在操作数之后,称为后缀(postfix)式,而第二种形式称为前缀(prefix)式。
如果所要做的只是执行单独++运算,就像把它作为一个单独的语句,或作为for循环中的自增运算符那样,前缀运算符和后缀运算符的效果是完全一样的。
只有在把这两个运算符作为更长的表达式的一部分时,区别才显现出来。
和所有的运算符一样,++运算符会返回一个值,但为何值取决于运算符相对于操作数的位置:
- x++ 先计算x的值再将它加1。返回给外部表达式的值是加1前的原始值。
- ++x 先将x的值加1再使用新值作为整个++操作所得的值。
- – 运算符也类似,但为减1而非加1。
假如执行以下程序:
main() {
int x, y;
x = 5;
y = x++;
printf("x=%d, y=%d\n", x, y);
}
结果为:
x=6, y=5
如果程序写为:
main() {
int x, y;
x = 5;
y = ++x;
printf("x=%d, y=%d\n", x, y);
}
则最后结果为:
x=6, y=6
语句
y=x++;
确实给x
的值增加了1,即它的值为6,但是赋给y
的值是在自增操作之前的值,所以y值为5。
++和–已成为习惯用语而被频繁使用。
举例来说,假设想要把一个数组arr
的前n
个元素置为0,最直接的方法是使用for语句:
for (i = 0; i < n; i++) arr[i] = 0;
但自增运算可以和选择操作结合使用:
for (i = 0; i < n;) arr[i++] = 0;
这个例子要求的是i++
的形式而非++i
的形式。
想要让i
自增,但同时也想选择数组中编号为0,1,2以及以下的元素。所以,在选择表达式中需要的是自增前的i
值。
常见错误:
使用运算符++和–时,注意必须避免写出有歧义的表达式。
一般规则是,使用这些运算符进行自增或自减的变量不应在同一个表达式中出现两次。
4.3 指针的自增和自减
解释C语言中最常用的一种结构,即表达式:
*p++
要确定该表达式意义,首先需要解决的问题就是操作的顺序是什么。
按照语言的优先级规则和结合性规则,该表达式可能会等同于
(*p)++
或者是
*(p++)
当这种情况发生时,C语言中的一元运算符按照从右至左的顺序进行运算。
所以,++运算优先于*运算,第二种解释是正确的。
后缀式++运算符让p
值先自增,随后再返回自增操作前的p
值。
由于p
是一个指针,在计算p+1
时,结果值应该指向数组中的下一个元素。
于是,表达式
*p++
表示下面的含义:
间接引用指针p
,使其当前指向的对象作为左值返回。
其负面影响是,p
值进行了自增,所以如果原左值是数组的一个元素的话,新的p
值指向数组中的下一个元素。
4.4 指针和数组的关系
C语言最不寻常的特征中,最有趣的是数组名作为指向该数组第一个元素的指针的同义词。
用例子解释如下:
声明
int intlist[5];
为一个含有5个整型数的数组分配了空间,如下图所示:
![](https://i-blog.csdnimg.cn/blog_migrate/6719810401b0f659b164b967f6b520e7.png)
分配的空间在计算机内存的某处。图中左边的地址是任意的。在这个图中,起始地址是2000。
intlist
代表一个数组,但也可以直接用作指针值。
当它作为指针使用时,intlist
定义为数组中第一个元素的地址。
对于任意数组arr
来说,下述等式在C语言中是永远成立的:
arr等同于&arr[0]
给定任何数组名,可以将它的地址直接赋给任何指针变量。
当一个数组从一个函数传递到另一个函数时通常可以用到这个等式。
典型地,被调用函数利用 排序 一节中Sort
原型所示的句法来声明这个数组:
void SortIntegerArray(int array[], int n);
如果想要用上面定义的intlist
数组调用Sort
,那么传递给Sort
的形式参数array
的值将会是intlist
中第一个元素的地址。
如果将原型写成
void SortIntegerArray(int *array, int n);
Sort
函数也会达到一样的效果。
在这种情况下, 第一个参数被声明为指针,但效果是相同的。
intlist
中第一个元素的地址赋值给了形式参数array
,并且通过指针运算进行处理。
作为一般的规则,声明参数时必须能体现出它们的用途。
如果想要将一个参数作为数组使用,并从中选择元素,那么应该将该参数声明为数组。
如果想要将一个参数作为指针使用,并且对其间接引用,那么应该将该参数声明为指针。
在C语言中,当变量不是在作为参数传递时声明,而是原始的声明时,数组和指针间最关键的区别就体现出来了。
声明
int array[5];
和声明
int *p;
间最根本的区别在于内存的分配。
第一个声明为数组分配了五个字的连续内存来存放数组元素。
第二个声明只分配了一个字的内存空间,其大小只能存放一个机器地址。
认识这一区别至关重要。如果声明一个数组,则需要有工作空间,若声明一个指针变量,那么变量在显式地初始化之前和任何内存空间都无关。
按照目前的理解水平,将指针作为数组使用的唯一途径是通过将数组的基地址赋给指针变量来初始化指针。
如果进行过上述声明后,写了代码
p=array;
那么指针变量p
和array
指向同样的地址,两者可以互换使用。
将指针指向一个已经存在的数组的地址的技术是有很大局限性的。
如果已经有一个数组名的话就可以直接使用了。把数组名赋给指针实际上并没有什么好处。
将指针作为数组使用的实际优势在于,可以把指针初始化为未声明的新内存,从而能在程序运行时建立新的数组。
参考
《C语言的科学和艺术》 —— 13 指针