第 14 节:指针(下)

90 阅读12分钟

5.2 二维数组与指针

5.2.1 使用数组名访问

设有一个二维数组 a 定义为:

int a[3][4] = {{12,  3,  4},
               {56,  7,  8},
               {9101112}};

二维数组 a,可视为三个一维数组:a[0]、a[1]、a[2];而每个一维数组又是一维数组,分别由 4 个元素组成。首先,理解如下的操作:

printf("%d\n",a[0][0]);  //二维数组中元素a[0][0]的值
printf("%p\n",&a[0][0]); //二维数组中元素a[0][0]的值对应的地址
printf("%p\n",a[0]);     //二维数组中a[0][0]的地址
printf("%p\n",a);        //二维数组中a[0]的地址
printf("%p\n",&a);       //二维数组a的地址

对应图示

举例:

表示形式含义地址
a二维数组名,指向一维数组a[0], 即0行起始地址2000
&a[0][0],a[0],*a0行0列元素地址2000
a[0][0],*(a[0]),**a0行0列元素a[0][0]的值1
&a[1],a+1指向索引为1行的起始地址2016
&a[1][0],a[1],*(a+1)1行0列元素a[1][0]的地址2016
a[1][0],*(a[1]),*(*(a+1))1行0列元素a[1][0]的值5
&a[1][2],a[1]+2,*(a+1)+21行2列元素a[1][2]的地址2024
a[1][2],*(a[1]+2),*(*(a+1)+2)1行2列元素a[1][2]的值是元素值,7

总结:

&a:二维数组a的地址
a: 二维数组中a[0]的地址
a[0]:二维数组中a[0][0]的地址

讨论:a[0][0]相关的
a[0][0]的地址:&a[0][0],a[0],*a,
a[0][0]的值: a[0][0],*(a[0]),**a,

讨论:a[1]相关的
a[1]的地址:&a[1],a + 1

讨论:a[1][0]相关的
a[1][0]的地址:&a[1][0],a[1],*(a+1)
a[1][0]的值:a[1][0],*a[1],*(*(a+1))

讨论:a[1][2]相关的
a[1][2]的地址:&a[1][2],a[1]+2,*(a+1)+2
a[1][2]的值:a[1][2],*(a[1]+2),*(*(a+1)+2)

注意:

如果 a 是二维数组,则 a[i]代表一个数组名, a[i]并不占内存单元,也不能存放a 数组元素值。它只是一个地址。所以:a、a+i、a[i]、(a+i)、(a+i)+j、a[i]+j 都是地址。

获取数组元素值的三种表示形式:

1) a[i][j] 下标法

2) *(a[i]+j) 用一维数组名

3) *(*(a+i)+j) 用二维数组名

5.2.2 使用指针变量访问

设 p 是指针变量,若p 指向数组首元素,即p = a[0];,那a[i][j]的指针如何表示?

先看一个代码:

int main() {
    int a[3][2] = {{1020},
                   {3040},
                   {5060}};

    int *p;
    p = &a[0][0];
    printf("%p\n", p);     //000000f2f49ff7b0
    printf("%p\n", p + 1); //000000f2f49ff7b4
    printf("%p\n", p + 2); //000000f2f49ff7b8

    int *q;
    q = a[0];
    printf("%p\n", q);      //000000f2f49ff7b0
    printf("%p\n", q + 1);  //000000f2f49ff7b4
    printf("%p\n", q + 2);  //000000f2f49ff7b8

    int *r;
    r = a;  //代码片段1
    printf("%p\n", r);      //000000f2f49ff7b0
    printf("%p\n", r + 1);  //000000f2f49ff7b4
    printf("%p\n", r + 2);  //000000f2f49ff7b8

    return 0;
}

进而:

  • p+j 将指向 a[0] 数组中的元素 a[0][j]

  • 对于二维数组a[M][N]来讲,由于 a[0]、a[1]、... 、a[M-1]等各行数组在内存中是依次连续存储,则对于 a 数组中的任一元素 a[i][j]

    • 地址表示:p+i*N+j
    • 值表示:*(p+i*N+j)p[i*N+j]

注意:上述代码中,代码片段1中的赋值操作会存在类型不匹配的情况,我们在5.6节中展开说明。

举例1:

int b[4][3] = {{102030},
               {405060},
               {708090},
               {100110120}};

int *p = b[0];

则:元素 b[1][2]对应的地址/指针、元素值为:

printf("b[1][2]对应的地址/指针为:%p\n",p+1*3+2);
printf("b[1][2]对应的值为:%d\n",*(p+1*3+2));
printf("b[1][2]对应的值为:%d\n",p[1*3+2]);

举例2:用指针访问二维数组,求二维数组元素的最大值。

#include <stdio.h>

#define ROWS 3
#define COLS 4

int main() {
    int a[ROWS][COLS] = {{10,  20,  30,  40},
                   {50,  60,  70,  80},
                   {12011010090}};

    //方式1:
//    int max = a[0][0];
//    for (int i = 0; i < ROWS; i++) {
//        for (int j = 0; j < COLS; j++) {
//            if (max < a[i][j]) {
//                max = a[i][j];
//            }
//        }
//    }

    //方式2:
    int *p = a[0];
    int max;
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            if (max < *(p + i * COLS + j)) {
                max = *(p + i * COLS + j);
            }
        }
    }
    printf("max=%d\n", max);

    //方式3:
    int *q, max1;
    for (q = a[0], max1 = *q; q < a[0] + ROWS * COLS; q++)
        if (max1 < *q)
            max1 = *q;
    printf("Max=%d\n", max1);


    return 0;
}

5.3 指针数组

5.3.1 数组指针 vs 指针数组

数组指针:

当指针变量里存放一个数组的首地址时,此指针变量称为指向数组的指针变量,简称数组指针

数组指针是指针?还是数组?

答案是:指针。

整型指针: int * pint; 能够指向整型数据的指针。

浮点型指针: float * pf; 能够指向浮点型数据的指针。

数组指针:能够指向数组的指针。

指针数组:

数组是用来存放一系列相同类型的数据,当然数组也可以用来存放指针,这种用来存放指针的数组被称为指针数组,它要求存放在数组中指针的数据类型必须一致

问题:指针数组是指针还是数组?

答案:是数组。是存放指针的数组。

5.3.2 指针数组的使用

格式:

数据类型 *指针数组名[大小];

举例1:

int *arr[5];

arr是一个数组,有5个元素,每个元素是一个整型指针,需要使用下标来区分。

举例2:

int main() {
    int a, b, c, d, e;
    a = 1;
    b = 2;
    c = 3;
    d = 4;
    e = 5;
    int *arr[] = {&a, &b, &c, &d, &e};//定义一个int类型的指针数组

    for(int i = 0;i < 5;i++){
        printf("%d ",*arr[i]);
    }

    return 0;
}

举例3:

#include <stdio.h>

int main() {
    int line1[] = {1234};         //声明数组,矩阵的第一行
    int line2[] = {5678};         //声明数组,矩阵的第二行
    int line3[] = {9101112};         //声明数组,矩阵的第三行
    int *p_line[3];              //声明整型指针数组
    p_line[0] = line1;           //初始化指针数组元素
    p_line[1] = line2;
    p_line[2] = line3;
    printf("矩阵:\n");
    for (int i = 0; i < 3; i++) {      //对指针数组元素循环

        for (int j = 0; j < 4; j++) {    //对矩阵每一列循环
            printf("%d\t", p_line[i][j]); //或改为printf("%d\t", *(p_line[i] + j));
        }
        printf("\n");
    }
    return 0;
}

运行结果:

5.4 字符数组 vs 字符指针变量

一个字符串,可以使用一维字符数组表示,也可以使用字符指针来表示。

  • 字符数组由若干个元素组成,每个元素放一个字符
  • 字符指针变量中存放的是地址(字符串/字符数组的首地址),绝不是将字符串放到字符指针变量中。

举例1:

char str[] = "hello tom";  //使用字符数组

char * pStr = "hello tom";  //使用字符指针

图示:

两种方式的对比:

对已声明好的字符数组,只能一一对各个元素赋值,不能用以下错误方法对字符数组赋值

char str[14];

str[0] = 'i'//正确

str = "hello Tom"//错误

对字符指针变量,采用如下方式赋值是可以的。

char * pStr = "hel";

pStr = "hello tom"//正确

图示:

一个字符数组,因为它有确定的内存地址,所以字符数组名是一个常量。而定义一个字符指针变量时,它在指向某个确定的字符串数据的情况下,也可以多次重新赋值

举例2:体会字符串字面量的不可变性

情况1:针对于整型数组、整型指针变量

int main() {
    int arr[10] = {12345678910};

    int *p = arr;

    printf("%d\n", p[1]); //2

    p[1] = 50;
    printf("%d\n", p[1]); //50
    printf("%d\n", arr[1]);  //50

    //
    int num = 30;
    p = &num;
    printf("%d\n",*p);    //30
    printf("%d\n",p[0]);  //30

    return 0;
}

情况2:针对于字符数组、字符指针变量

int main() {
    
    char arr[] = "hello";

    arr[1] = "m";     //运行时错误 
    printf("%s\n",arr);

    return 0;
}

因为字符串字面量存储在只读内存区域,是不可变的,不能修改其值。

拓展:

int main() {
    char *pStr = "hello";
    pStr = "hello tom"//正确

    pStr[1] = 'm';      //运行时错误
    printf("%s\n",pStr);

    return 0;
}

5.5 字符串数组的表示

字符串可以使用一维字符数组或字符指针变量等两种方式表示,那么字符串数组如何表示呢?

如果一个数组的每个成员都是一个字符串,则构成了字符串数组。字符串数组有两种表示方式:① 二维字符数组②字符指针数组

举例1:

方式1:使用二维字符数组

char fruit[][7]={"Apple","Orange","Grape","Pear","Peach"};  //上一章5.6节举例4
char weekdays[7][10] = {   //行数7也可以省略
 "Monday",
 "Tuesday",
 "Wednesday",
 "Thursday",
 "Friday",
 "Saturday",
 "Sunday"
};

字符串数组,一共包含7个字符串,所以第一维的长度是7。其中,最长的字符串的长度是10(含结尾的终止符 \0 ),所以第二维的长度统一设为10。

思考: 数组的第二维,长度统一定为10,有点浪费空间,因为大多数成员的长度都小于10。解决方法就是把数组的第二维,从字符数组改成字符指针。

方式2:使用字符指针数组

char* weekdays[7] = {  //7也可以省略
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
  "Sunday"
};

上面的字符串数组,其实是一个一维数组,成员就是7个字符指针,每个指针指向一个字符串(字符数组)。

类似的:定义表示颜色的指针数组 colors,存储“red”、“yellow”、“blue”、“white”、“black” 5 种颜色。

char *colors[5] = {"red""yellow""blue""white""black"};

遍历字符指针数组:

for (int i = 0; i < 7; i++) {
 printf("%s\n", weekdays[i]);
}

举例2:

请编写程序,定义一个字符指针数组,用来存储四大名著的书名, 并通过遍历该指针数组,显示字符串信息 , (即:定义一个指针数组,该数组的每个元素,指向的是一个字符串)

方式1:二维字符数组

int main() {

    char books[4][13] = {"三国演义""西游记""红楼梦""水浒传"};
    int len = 4;
    for(int i = 0;i < len;i++){
        printf("books[%d] : %s\n",i,books[i]);
    }

    return 0;
}

方式2:字符指针数组

int main() {
    //定义一个指针数组,该数组的每个元素,指向的是一个字符串
    char *books[] = {
            "三国演义",
            "西游记",
            "红楼梦",
            "水浒传",
    };

    //遍历
    int len = 4;
    for (int i = 0; i < len; i++) {
        printf("books[%d] : %s\n", i, books[i]);
    }

    return 0;
}

5.6 拓展:指向固定长度数组的指针变量

定义一个整型指针变量指向一维数组,一维数组的每个元素包含 m 个元素。

定义格式:

(*标识符)[一维数组元素个数];

例如:定义一个指针变量 p,它指向包含有 4 个元素的一维数组。

int (*p)[4];

说明:p先和*结合,说明p是一个指针变量,指向一个大小为4的整型数组。

注意:此时定义的是一个指针变量,并非是一个指针数组。(*p 必须放在括弧内,否则就变成了定义指针数组。)

由于 p 是指向有 4 个整型元素的一维数组的指针变量,因此,p+1 是将地址值加上 4*4,即指向下一个一维数组。

举例:

int a[3][4] = {{12,  3,  4},
               {56,  7,  8},
               {9101112}};
int (*q)[4];
q = a;

则:

q 为二维数组第 0 行首地址,与 a 相同;

q+1 为二维数组第 1 行首地址,与 a+1相同;

q+2 为二维数组第 2 行首地址,与 a+2相同;

*(q+i)为二维数组第 i 行第 0 列元素的地址,与*(a+i)相同;

*(q+i)+j 为二维数组第 i 行第 j 列元素的地址,与*(a+i)+j 相同;

*(*(q+i)+j) 为二维数组第 i 行第 j 列元素值,与*(*(a+i)+j)相同,即 a[i][j]

举例:

int main() {
    int arr[3][4] = {{12,  3,  4},
                     {56,  7,  8},
                     {9101112}};
    int (*q)[4];
    q = arr;

    printf("arr[0]的地址为:%p\n", arr);            //0000006460dffb40
    printf("arr[0]的地址为:%p\n", q);              //0000006460dffb40
    printf("arr[0][1]的地址为:%p\n", *arr + 1);    //0000006460dffb44
    printf("arr[0][1]的地址为:%p\n", *q + 1);      //0000006460dffb44
    printf("arr[0][1]的地址为:%p\n", arr[0] + 1);  //0000006460dffb44
    printf("arr[0][1]的地址为:%p\n", q[0] + 1);    //0000006460dffb44

    printf("arr[1]的地址为:%p\n", arr + 1);       //0000006460dffb50
    printf("arr[1]的地址为:%p\n", q + 1);         //0000006460dffb50

    printf("arr[1][0]的值为:%d\n", *(*(arr + 1))); //5
    printf("arr[1][0]的值为:%d\n", *(*(q + 1)));   //5
    printf("arr[1][1]的值为:%d ", *(*(q + 1) + 1));//6

    //遍历arr[0]中的几个元素
    for (int i = 0; i < 4; i++) {
        printf("%d ", *(*q + i)); //输出1 2 3 4
    }
    printf("\n");

    return 0;
}

【华南理工大学2018研】若有以下说明:

int w[3][4]={{0,1},{2,4},{5,8}};
int (*p)[4]=w;

则数值为4的表达式是( )。
A.w[1]+1
B.p++,
(p+1)
C.w[2][2]
D.p[1][1]

【答案】D

【解析】A中w[1]表示的是数值2,则表达式的值为3,错误;B中p++表示指向二维数组第二行w[1]的地址,而(p+1)代表的是第三行w[2][0]元素的地址,B错误;C中表示的是数值0,答案选D。