第12章 指针和数组

作者: 橡树人 | 来源:发表于2020-03-05 02:28 被阅读0次

英文原版 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. 数组名可被用作指针,但不能给数组名分配新值,或者直接对数组名做算术运算。

错误示例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_largeststore_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;
}

相关文章

  • C语言指针相关

    一、指针数组 和 数组指针的区别:1:指针数组,还是数组,数组每个元素是指针。2:数组指针,是指针,指向的是数组。...

  • JNI基础 -- C++基础知识(指针数组)

    数组和指针,数组指针,指针数组 数组 声明一个数组 这个比较简单,不做过多介绍 数组指针 数组指针是一个指针,指向...

  • 数组指针和指针数组

    1.数组名 数组首元素的地址和数组地址是两个不同的概念 数组名代表数组首元素的地址,它是个常量. 变量本质是内存空...

  • 数组指针和指针数组

    1.数组指针(又称行指针) 二维数组赋给一指针时: 2.指针数组 二维数组赋给一指针数组: 小结:数组指针只是个指...

  • 数组指针和指针数组

    指针*p不仅仅是地址(数据访问的位置),还包括所指向类型,即p+1移动的步长(如何访问)。数组指针,char (*...

  • 数组指针和指针数组

    输出结果:Example 数组指针:我我是是大大好人好人Example 指针数组: 我是大好人

  • 数组指针和指针数组

    https://www.cnblogs.com/mq0036/p/3382732.html 一维数组名和二维数组名...

  • 关于二维数组及数组指针和指针数组的深度思考(涉及指针)

    转载请注明在纠结指针数组和数组指针时无意发现的小细节。总算搞清数组指针和指针数组。 数组指针定义 int (*p)...

  • 指针

    指针 数组指针和指针数组 函数指针和指针函数 指针作为参数 指针多用于处理值传递,减少值复制耗费的内存

  • C语言特性(指针数组和指向指针变量的指针)

    指针数组和指向指针变量的指针 指针数组与main()函数形参 声明指针数组与声明普通数组的语法类似,其语法格式如下...

网友评论

    本文标题:第12章 指针和数组

    本文链接:https://www.haomeiwen.com/subject/rjinlhtx.html