英文原版 P527
理解指针和数组的关系,对于掌握C语言是非常关键的,因为这会让你深入理解C语言是如何设计的,以及帮助你理解现有的程序。
本章的主要内容有:
- 指针的算术运算;
- 指针和数组名的关系:数组名是一个指向数组首个元素的指针;
- 指针和可变长度数组的关系;
12.1节 指针的算术运算
指针算术运算的适用范围:
- 对指针进行算术运算仅限指向数组元素的指针。
- 对两个指针做减法仅限两个指针都指向同一个数组。
C语言支持3类指针算术运算:
- 指针加上整数
- 指针减去整数
- 指针减去指针
注意:
- 对没有指向数组元素的指针执行算术运算的结果是没有定义的。
- 除非两个指针都指向同一个数组,否则对两个指针进行相间的结果是没有定义的。
12.1.1节 指针加上整数
指针 p
加上整数 j
的值是一个指针,该指针指向 p
指向的数组元素后面的第 j
个元素。
假设指针 p
指向 a[i]
, 则 p+j
就是一个指向 a[i+j]
的指针。
例1 指针加上整数
int a[10], *p, *q, i;
p = &a[2];
q = p + 3;
p += 6;
12.1.2节 从指针减去整数
如果指针 p
指向 a[i]
,则 p-j
就是一个指针,该指针指向 a[i-j]
。
例2 指针减去整数
int a[10], *p, *q, i;
p = &a[8];
q = p -3;
p -= 6;
12.1.3节 指针减去指针
从指针中减去指针的值是数组元素间的距离。
如果指针 p
指向 a[i]
,指针 q
指向 a[j]
,则 p-q
的值就是 i-j
。
例3 指针减去指针
int a[10], *p, *q, i;
p = &a[5];
q = &a[1];
i = p - q;
j = q - p;
12.1.4小节 指针比较
可以使用关系运算符(<
、<=
、>
、>=
)和判等运算符(=
、!=
)来比较指针。
只有当两个指针都指向同一个数组的元素,才可以对两个指针使用关系运算符。
指针比较运算的结果取决于两个元素在数组中的相对位置。
例4 指针比较
int i,j,*p,*q;
p = &a[5];
q = &a[1];
则p<=q
的值是0,p>=q
的值是1。
12.1.5小节 指向复合字面量的指针
C99支持指针指向由复合字面量创建的数组的元素。
使用复合字面量省去了声明一个数组变量,并使指针指向数组的首个元素的麻烦。
例5 指向复合字面量的指针
不使用复合字面时的指向数组的指针初始化
int a[] = {3, 0 ,3, 4, 1};
int *p = &a[0];
使用复合字面量后的指针初始化
int *p = (int[]){3,0,3,4,1};//指针p是一个指向5个整数3、0、3、4、1的数组的首个元素的指针
12.2节 使用指针来处理数组
指针算术运算支持通过递增指针变量来访问数组元素。
例1 递增指针变量来访问数组元素
#define N 10
...
int a[N], sum, *p;
sum = 0;
for (p=&a[0];p<&a[N];p++) {
sum += *p;
}
12.2.1节 组合使用间接寻址运算符*
和自增1运算符++
常见的组合使用有:
-
*(p++)
:表达式的值是自增前的*p;自增的是p
; -
(*p)++
:表达式的值是*p
;自增的是*p
; -
*(++p)
:表达式的值是*(p+1)
;自增的是p
; -
++(*p)
:表达式的值是(*p)+1
;自增的是*p
例2 往数组元素里存储一个值,然后前进到下一个元素
版本1 下标写法
a[i++] = j;
版本2 指针写法
*p++ = j;
例3 使用*p++
来处理循环
版本1
for (p=&a[0]; p<a[N]; p++){
sum += *p;
}
版本二
p = &a[0];
while(p<a[N]){
sum += *p++;
}
12.2.2节 栈示例
#include <stdbool.h>
#define STACK_SIZE 100
//全局变量
int contents[STACK_SIZE];
int *top_ptr = &contents[0];
void make_empty(vid)
{
top_ptr = &contents[0];
}
bool is_empty(void)
{
return top_ptr == &contents[0];
}
bool is_full(void)
{
return top_ptr == &contents[N-];
}
void push(int i)
{
if (is_full())
{
stack_overflow();
}
else
{
*top_ptr++ = i;
}
}
int pop(void)
{
if (is_empty())
{
stack_overflow();
}
else
{
return *(--top_ptr);
}
}
12.3节 数组名作为指针
指针和数组关联的方式有:
- 指针算术运算;
- 数组名可被用作指向数组首元素的指针;
重新认识数组
- 数组名是指向数组首个元素的指针;
- 数组下标表示等价于对指针进行算术运算;
- 数组名可以作为实参;
- 指针可作为数组名;
例1 数组名作为指针
int a[10];
//a+i等价于&a[i];
//*(a+i)等价于a[i];
*a = 7;
*(a+1) = 12;
例2 数组名作为指针来改写循环
版本1:指针算术运算
for (p=&a[0];p<&a[N];p++) {
sum += *p;
}
版本2:数组名
for (p=a;p<(a+N);p++) {
sum += *p;
}
12.3.1程序示例:逆序输出
指针版本:reverse3.c
#include <stdio.h>
#define N 10
int main(void) {
int a[N], *p;
printf("Enter %d numbers: ", N);
for (p=a; p<(a+N); p++) {
scanf("%d", p);
}
printf("In reverse order:\n");
for (p=(a+N-1); p>=a; p--){
printf(" %d\n", *p);
}
printf("\n");
return 0;
}
12.3.2节 重新理解数组实参
当把数组名传递给一个函数时,该实参通常被看做是一个指针。
例1 找一个整数数组中的最大值
函数定义
int find_largest(int a[], int n)
{
int i, max;
max = a[0];
for(i=0; i<N; i++){
if(a[i]>max){
max = a[i];
}
}
return max;
}
函数调用:
//该函数调用是将指向数组b的首元素的指针拷贝给a,没有对数组b进行拷贝。
largest = find_largest(b, N);
认识到数组实参被当做指针,会得到哪些重要的结果?
- 把普通变量传递给函数,拷贝的是该变量的值,函数内对相应的形参的修改不会影响该变量的值。相反,数组名作为实参,传递的是指向该数组首个元素的指针,在函数内对形参的修改会改变该数组的内容。
- 把数组传递给函数花费的时间跟数组的长度无关,因为不会拷贝数组本身。
- 数组形参可被声明为指针类型;
- 可以给有数组形参的函数传递数组的片段:由连续的数组元素组成的序列。
例2 将数组名作为实参传递给函数可能会修改该数组的内容
void store_zeros(int a[], int n){
int i;
for (i=0;i<n;i++){
a[i] = 0;
}
}
如何实现数组形式参数不会被修改?
使用关键字const
,编译器会检查在函数体内是否有对数组a的元素的赋值。
void store_zeros(const int a[], int n){
int i;
for (i=0;i<n;i++){
a[i] = 0;
}
}
例3 声明数组形参为指针
int find_largest(int *a, int n)
{
int i, max;
max = a[0];
for(i=0; i<N; i++){
if(a[i]>max){
max = a[i];
}
}
return max;
}
注意:
虽然将形参声明为数组类型等价于声明一个指针变量,但是对于形参声明为非数组类型的情形不成立。
例4 对比声明数组类型和非数组类型
数组声明
//编译器会为变量a分配10个整数大小的存储单元
int a[10];
普通变量声明
//编译器为变量a分配的是一个指针大小的存储单元
int *a;
*a = 0;//由于不知道a指向的是哪里,所以对程序的影响是不确定的。
例5 传递数组片段给有数组参数的函数
//找出从b[5]开始的10个数里的最大数
largest = find_largest(&b[5], 10);
12.3.3节 使用指针作为数组名
C语言允许将指针看做数组名,并支持对指针进行下标操作
例6 指针作为数组名
#define N 10
...
int a[N], i, sum = 0, *p = a;
...
for (i=0; i<N; i++){
//编译器会把p[i]当做*(p+i)
sum += p[i];
}
12.3.4 注意事项
- 数组名可被用作指针,但不能给数组名分配新值,或者直接对数组名做算术运算。
错误示例1 对数组名进行算术运算
//假设a是一个数组名
while(*a != 0){
a++;//这样会使得整个数组的首地址发生改变,影响该数组原有的结构
}
正确示例1 将数组名拷贝给一个指针变量,操作该指针变量:
p = a;
while(*p != 0){
p++;
}
12.4节 指针和多维数组
C语言是如何存储多维数组的?
C语言是以行主序的方式存储多维数组的,即先存储第0行的元素,接着存储第1行的元素,以此类推。
12.4.1 使用指针来处理多维数组元素
例1 指针和下标的等价性
下标版本
int a[NUM_ROWS][NUM_COLS];
int row, col;
...
for (row = 0; row < NUM_ROWS; row++){
for(col = 0; col < NUM_COLS;col++){
a[row][col] = 0;
}
}
指针版本
int a[NUM_ROWS][NUM_COLS];
int *p;
...
for (p = &a[0][0]; p < = &a[NUM_ROWS-1][NUM_COLS-1]; p++){
*p = 0;
}
12.4.2 使用指针来处理多维数组的行
等价关系:
对于任何数组
a
来说,a[i]
等价于*(a+i)
。
类似地,&a[i][0]
等价于&(*(a[i]+0))
。
理解多维数组:
-
a[i]
:表示一个指针,该指针指向第i
行的首个元素。 -
p = &a[i][0]
及p=a[i]
:都表示指针p
指向数组的第i
行的第0个元素。 - 可将
a[i]
作为实参传递给处理一维数组的函数,比如函数find_largest
和store_zeros
等。
例2 清空多维数组的一行
int a[NUM_ROWS][NUM_COLS];
int *p, i;
...
for (p = a[i]; p < a[i] + NUM_COLS; p++){
*p = 0;
}
例3 求数组a
的第i
行元素的最大值
largest = find_largest(a[i], NUM_COLS);
12.4.3 使用指针来处理多维数组的列
例4 清空多维数组的一列
//声明p是一个指向长度为NUM_COLS的int类型数组;
int a[NUM_ROWS][NUM_COLS];
int (*p)[NUM_COLS], i;
...
//p++表示前进到下一行的开始
for (p = &a[0]; p < &a[ NUM_ROWS]; p++){
(*p)[i] = 0;
}
注意:
-
*p[NUM_COLS]
:表示一个长度为NUM_COLS的指针数组; -
(*p)[NUM_COLS]
:表示一个指向长度为NUM_COLS的数组的指针; -
*(p[i])
:表示a[i][0]的值; -
(*p)[i]
:表示某一行的第[i]个元素;
12.4.4 使用多维数组名作为指针
多维数组名是一个指向多维数组a的第0行的指针。
例5 多维数组名当做指针
//a不是指向a[0][0]的指针;
//a是一个指针,该指针指向第0行
//a的类型是int (*)[NUM_COLS],一个指向长度为NUM_COLS的int类型数组的指针。
int a[NUM_ROWS][NUM_COLS];
int (*p)[NUM_COLS], i;
...
//p++表示前进到下一行的开始
for (p = a; p < a + NUM_ROWS; p++){
(*p)[i] = 0;
}
例6 求两维数组的最大值
// 让函数把多维数组看做一维数组
largest = find_largest(a[0], NUM_ROWS*NUM_COLS);
12.5 指针和多维数组
C99允许指针指向可变长度数组的元素。
对于可改变类型有哪些限制?
可改变类型的变量必须在函数体的内部或者函数原型里。
例1 一维可变长度数组
//使用普通的指针变量来指向1维可变长度数组的元素
void f(int n)
{
int a[n], *p;
p = a;
}
例2 两维可变长度数组
void f(int m, int n)
{
//这里的p的类型取决于n
int a[m][n], (*p)[n];
p = a;
}
例3 将可变长度两维数组的第i
列清空
int a[m][n];
int (*p)[n];
...
for (p=a;p<a+m;p++) {
(*p)[i] = 0;
}
网友评论