当指针指向数组元素时,C语言允许对指针进行算数运算,即加法和减法,这种运算引出了一种对数组进行处理的替换方法,它可以使指针代替数组下标进行操作。用指针处理数组的主要原因之一是效率。
- 指针的算术运算
- 指针用于数组处理
- 用数组名作为指针
- 指针和多维数组
指针的算术运算
指针指向数组元素:
int a[10],*p;
p = &a[0];
由此我们可以通过p来访问a[0]。
*p = 5;
上述语句把5存到了a[0]中。
通过在p上执行指针算术运算(或地址算数运算)可以访问到数组a的其他元素。C语言支持3种格式的指针算术运算:
- 指针加上整数。
- 指针减去整数。
- 两个指针相减。
指针加上整数
指针减去整数
指针相减
指针比较
关系运算符
判断运算符
只有在两个指针向指向同一数组时,用关系运算符进行指针比较才有意义。比较的结果依赖于两个元素的相对位置。
指针用于数组处理
一般数组处理
指针的算术运算允许通过对指针变量进行重复自增来访问数组的元素。
数组求和:
#include <stdio.h>
#define N 10
int main(){
int a[N] = {1,2,3,4,5,6,7,8,9,10},sum,*p;
sum = 0;
for(p = &a[0]; p < &a[N]; p++){
sum += *p;
}
printf("%d",sum);
}
输出结果为:
特别说明一下for语句中的条件p < &a[N]
。在标准C中,即使元素a[N]不存在(数组的下标为0到N-1),但是对它使用取地址运算符是合法的。因为循环不会尝试检查a[N]的值,所以在上述方法下用a[N]是非常安全的。利用p等于&a[0],&a[1]…&a[N-1]可以执行循环体,但是当p等于&a[N]时,循环终止。
*运算符和++运算符的组合
不妨忽视这些不好理解的表达式,只应用可以简化循环的*p++
其他的应用就交给聪明人吧
运用*p++
重写求和:
#include <stdio.h>
#define N 10
int main(){
int a[N] = {1,2,3,4,5,6,7,8,9,10},sum,*p;
sum = 0;
p = &a[0];
while(p < &a[N]){
sum += *p++;//p++的值是p,因为使用后缀++,所以p只有在表达式计算出来后才可以自增
}
printf("%d",sum);
}
输出结果为:
*
运算符和--
运算符的混合方法类似于*
和++
的组合。首先观察用外部变量实现栈的代码示例:
考察数据结构栈(stack)
像数组一样,栈可以存储具有相同数据类型的多个数据项。然而,栈中数据项的操作是十分受限制的,操作包括:出栈、入栈,仅对栈顶元素操作,禁止测试或修改不在栈顶的数据项。通常称其特性为LIFO(last-in,first-out)。
C语言实现栈的一种方法是把元素存储在数组里,我们称这个数组为contents。命名为top的一个整型变量用来标记栈栈顶的位置。栈为空时,top值为0。为了往栈中压入数据项,可以把数据项简单存储在contents中标记为top的位置上,然后自增top。弹出数据项则要求自减top,然后用它作为contents的索引取回弹出的数据项。
基于上述这些概要,给出一段代码,为栈留出了变量并且提供一组函数来表示栈的操作。全部五个函数都需要访问top,而且其中2个函数还都需要访问contents,所以将把contents和tops设为外部变量。
不用指针版本:
#include <stdio.h>
#define STACK_SIZE 100
#define TRUE 1
#define FALSE 0
typedef int Bool;
int contents[STACK_SIZE];
int top = 0;
void stack_overflow();
void stack_underflow();
void make_empty(void);
Bool is_Empty(void);
Bool is_full(void);
void push(int);
int pop(void);
int main(){
for(int i = 0; i < 100; i++){
push(i);
}
push(100);
for(int i = 0; i < 100; i++){
printf(" %d ",pop());
}
pop();
return 0;
}
void make_empty(void){
top = 0;
}
Bool is_empty(void){
return top == 0;
}
Bool is_full(void){
return top == STACK_SIZE;
}
void push(int i){
if(is_full())
stack_overflow();
else
contents [top++] = i;
}
int pop(void){
if(is_empty())
stack_underflow();
else
return contents [--top];
}
void stack_overflow(void){
printf("栈溢出拒绝入栈");
}
void stack_underflow(void){
printf("空栈拒绝出栈");
}
输出结果为:
上述版本的栈依赖名为top的整型变量来跟踪contents数组的“栈顶”位置。
现在用指针变量来替换top,指针变量初始指向contents数组的第0个元素。
对栈顶的声明和函数做如下修改:
int *top = &contents[0];
void make_empty(void){
top = &contents[0];
}
Bool is_empty(void){
return top == &contents[0];
}
Bool is_full(void){
return top == &contents[STACK_SIZE];
}
void push(int i){
if(is_full())
stack_overflow();
else
*top++ = i;
}
int pop(void){
if(is_empty())
stack_underflow();
else
return *--top;
}
输出结果为:
用数组名作为指针
可以用数组的名字作为指向数组第一个元素的指针。这种映射简化了指针的算术运算,而且使得数组和指针都更加通用。
int a[10];
*a = 7;/*store 7 in a[0]*/
*(a + 1) = 12;/*store 12 in a[1]*/
由上述规则可以有如下简化循环
for(p = &a[0]; p < &a[N]; p++)
sum += *p;
_______________________________
for(p = a; p < a + N; p++)
sum += *p;
虽然可以把数组名用作指针,但是不能给数组名赋新的值。试图使数组名指向其他地方是错误的。
数列反向(改进版)
/* Reverse a series of numbers (pointer version) */
#include <stdio.h>
#define N 10
int main(){
int a[N] , *p;
printf("Enter %d numbers :" , N);
for (p = a; p < a + N ; p++)
scanf("%d", p);
printf("In reverse order: ") ;
for (p = a + N - 1; p >= a; p--)
printf("%d\n", *p);
return 0;
}
输出结果为:
数组型实际参数(改进版)
在传递给函数时,数组名始终作为指针。思考下面的函数,这个函数会返回 整型数组中最大的元素:
int find_largest(int a[], int n){
int i, max;
max = a[0];
for (i = 1; i < n; i++){
if(a[i] > max)
max = a[i];
}
return max;
}
假设调用find_largest函数如下:
largest = find_largest (b, N);
这个调用会导致把数组b的第一个元素赋值给a;数组本身并没有进行复制。
把数组型形式参数看作是指针的事实会产生许多重要的结果:
- 在给函数传递普通变量时,把变量的值进行复制;任何对相应的形式参数的改变不会影响到变量。反之,因为没有使数组本身进行复制,所以数组作为实际参数不会防止改变。例如,通过在数组的每个元素储存零的方法,下列函数可以修改数组:
void store_zeros(int a[], int n){
int i;
for (i = 0; i < n; i++)
a[i] = 0;
}
为了指明数组型形式参数不会改变,可以在它的声明中包含单词const:
int find_largest(const int a[],int n){
...
}
- 给函数传递数组所需的时间不依赖于数组的大小。因为没有对数组进行复制,所以传递大数组不会产生不利的结果。
- 如果想要指针,可以把数组型形式参数声明为指针。例如,可以按如下形式定义find_largest函数:
int find_latgest(int *a,int n){
...
}
声明a是指针就相当于声明它是数组。编译器处理这两类声明就好像他们是完全一样的。
注意
虽然声明形式参数是数组就如同声明它是指针一样,但是这种一样不适用于变量。声明
int a[10];
这会导致编译器为10个整数预留空间,但声明
int *a;
会导致编译器为指针变量分配空间。在稍后的例子中,a不是数组。试图用a作为数组可能会导致极糟的后果。例如赋值
*a = 0; /***WRONG***/
将在a指向的地方存储0。因为不知道a指向哪里,所以对程序的影响是无法预料的。
- 可以给形式参数为数组的函数传递数组的“片段”,所谓片段是连续元素的序列。假设希望用find_largest函数来定位数组b中某一部分的最大元素,比如数元素b[5],…,b[14]。调用find_largest函数时。讲传递b[5]的地址和数10,这说明需要用find_largest函数检查从b[5]开始的10个数组元素:
largest = find_largest(&b[5],10);
用指针作为数组名
可以用数组名作为指针,C语言也允许把指针好像数组名一样进行标记。例如:
int a[N], i, sum = 0, *p = a;
for (i = 0; i < N; i++)
sum += p[i];
编译器对待p[i]
就像对待*(p+i)
一样,这是指针算术运算非常正规的用法,在指针的高级应用将更详细了解这个用法。
指针和多维数组
处理多维数组的元素
多维数组的存储:
C语言始终按照行为主的顺序存储二维数组;
在用指针工作时可以利用这个又是。如果使得指针p指向二维数组中的第一个元素(即第0行第0列的元素),就可以通过重复自增p的方法访问到数组中的每一个元素。
二维数组所有元素初始化为0的示例:
假设数组具有如下声明:
int a[NUM_ROWS][NUM_COLS];
显而易见的方法时用嵌套的for循环:
int row,col;
for(row = 0; row < NUM_ROWS; row+)
for(col = 0; col < NUM_COLS; col++)
a[row][col] = 0;
但是,如果把a看成是整形的一维数组,那么就可以用单独一个循环来替换上述两个循环了:
int *p;
for (p = &a[0][0]; p <= &a[NUM_ROW-1][NUM_COLS-1]; p++)
*p = 0;
处理多维数组的行
**问题:**只在二维数组的一行内处理元素,该怎么办呢?
为了访问到i行的元素,最好初始化p指向的数组a中的i行的元素0:
int *p;
p = &a[i][0];
对于任意的二维数组a来说,既然表达式a[i]是指向i行第一个元素的指针,那么还可以简化写成
p = a[i];
对于把某一行设为0的程序,将地址的算术运算用于数组的下标变换简化得到
int a[NUM_ROWS][NUM_COLS], *p, i;
.../*赋值等其他过程*/
//指针指向第i行的每一个元素并赋值为0
for (p = a[i]; p < a[i] + NUM_COLS; p++)
*p = 0;
最初设计用来确定一维数组最大元素的函数,现在也可以用来确定二维数组中一行中的最大元素:
largest = find_largest(a[i],NUM_COLS);
用多维数组名作为指针
可以忽略数组维数而采用任意数组的名字作为指针。
C语言多维数组的结构是指针^N指向N维数组。
也就是说int *^N
指针指向N维数组
下面用二位数组举例:
int a[10],b[10][10];
虽然可以使用a作为指针指向元素a[0]
,但是不是说b
是指向b[0][0]
的指针,而是说b
是指向b[0]
的指针。C语言认为b
不是二维数组而是作为一维数组,且这个一维数组的每个元素又是一一维数组。在类型项中,a
可以用作是int *
型的指针,而b
用作指针时则具有int **
型的指针(指向整数指针的指针)。
largest = find_largest(b, 100);/*WRONG*/
这条语句不编译,因为b的类型为int **而find_largest函数期望的实际参数类型是int *。
正确的调用是:
largest = find_largest(b[0], 100);
b[0] 指向第0行的第0个元素,而且它的类型是int *,所以调用将正确地执行。