C

C 知识量:16 - 74 - 317

10.7 指针和多维数组><

深入理解指针- 10.7.1 -

多维数组涉及更加复杂的指针,下面通过一个示例来更深入的理解指针的含义。

假设有以下一个二维数组:

int box[3][2];

根据数组和指针的关系,可以推断出以下几点:

  • 因为数组名代表一个指针,因此,数组名box指向该数组首元素的地址。而该数组是一个二维数组,所以,该数组首元素是一个内含两个int值的数组,由此推断出:数组名box指向这个内含两个int值的数组的地址,也就是说,数组名box的值是这个内含两个int值的数组的地址。

  • 因为box[0]就是box数组的首元素,而&box[0]就是box数组首元素的地址,所以,box的值就是&box[0]的值。

  • box[0]本身也是一个内含两个int值的数组,因此,box[0]的值就是其首元素的地址,也就是说,box[0]的值就是&box[0][0]的值。

  • 解引用一个指针(即在指针前使用*运算符)或在数组名后面使用带下标的[]运算符,可以得到引用对象代表的值。因此,*(box[0])的值就等于*(&box[0][0])的值,即等于box[0][0]的值;*box的值就等于*(&box[0])的值,即等于box[0]的值,而由上一条可知:box[0]的值就是&box[0][0]的值。由此推断出:*box的值就等于&box[0][0]的值,因此,**box的值就是*&box[0][0]的值,即box[0][0]的值。

  • box指向一个占用两个int大小对象的地址;box[0]指向一个占用一个int大小对象的地址。由于它们都始于同一个地址,所以,box与box[0]的值相同。

  • 给指针加1,其值会增加对应类型大小的数值。由于box与box[0]指向对象的大小不同,所以,即使始于同一个地址,但box+1与box[0]+1的值却不相同。

以下是一个多维数组的示例:

#include <stdio.h>

int main(void) {
    int box[3][2] = {
        {1, 2},
        {3, 4},
        {5, 6}
    };
    printf("   box=%p,    box+1=%p\n", box, box + 1);
    printf("box[0]=%p, box[0]+1=%p\n", box[0], box[0] + 1);
    printf("  *box=%p,   *box+1=%p\n", *box, *box + 1);
    printf("box[0][0]=%d\n", box[0][0]);
    printf("  *box[0]=%d\n", *box[0]);
    printf("    **box=%d\n", **box);
    printf("box[2][1]=%d\n", box[2][1]);
    printf("*(*(box+2)+1)=%d\n", *(*(box + 2) + 1));
    system("pause");
    return 0;
}

运行的结果为:

   box=0061FF18,    box+1=0061FF20
box[0]=0061FF18, box[0]+1=0061FF1C
  *box=0061FF18,   *box+1=0061FF1C
box[0][0]=1
  *box[0]=1
    **box=1
box[2][1]=6
*(*(box+2)+1)=6

由以上结果可以清楚的看出:

  • box与box[0]指向的初始地址都是0061FF18,但是它们指向对象的大小却不同,这导致box+1后移动了8个字节(2个int类型),而box[0]+1后移动了4个字节(1个int类型)。

  • *box==box[0]

  • **box==*box[0]==box[0][0]

  • box[2][1]==*(*(box+2)+1),即使用数组表示法和使用指针表示法的效果是一样的,这也进一步表明C语言中的数组与指针的关系。

指向多维数组的指针- 10.7.2 -

如果要创建一个指向多维数组的指针,需要使用类似以下形式的语句:

int (*p)[2];

以上代码声明了一个指针p,它指向一个数组,该数组内含两个int类型的值。之所以使用圆括号是因为[]的优先级高于*。如果不这样做,将代码修改为:

int * p[2];

那么,由于[]的优先级更高,将优先与p结合,p就成为了一个内含两个元素的数组;然后*又表示p数组内含的元素均是一个指针;最后int表示p数组中的指针都是指向int类型值的。整个表达式的含义就变成了:声明了一个内含两个指向int类型值的指针的数组p。

以下是一个指向二维数组的指针的示例:

#include <stdio.h>

int main(void) {
    int box[3][2] = {
        {1, 2},
        {3, 4},
        {5, 6}
    };
    int (*p)[2];  //指向二维数组的指针p
    p = box;
    printf("   p=%p,    p+1=%p\n", p, p + 1);
    printf("p[0]=%p, p[0]+1=%p\n", p[0], p[0] + 1);
    printf("  *p=%p,   *p+1=%p\n", *p, *p + 1);
    printf("p[0][0]=%d\n", p[0][0]);
    printf("  *p[0]=%d\n", *p[0]);
    printf("    **p=%d\n", **p);
    printf("p[2][1]=%d\n", p[2][1]);
    printf("*(*(p+2)+1)=%d\n", *(*(p + 2) + 1));
    system("pause");
    return 0;
}

运行结果为:

   p=0061FF14,    p+1=0061FF1C
p[0]=0061FF14, p[0]+1=0061FF18
  *p=0061FF14,   *p+1=0061FF18
p[0][0]=1
  *p[0]=1
    **p=1
p[2][1]=6
*(*(p+2)+1)=6

以上示例与上一节的示例很相似,但是通过指向二维数组的指针来引用和打印数组内容。虽然p是指针,不是数组名,但是也可以使用p[2][1]这样的写法。总之,可以使用数组表示法或指针表示法来表示一个数组元素,box[2][1]与p[2][1]代表的含义是相同的。

指针的兼容性- 10.7.3 -

指针之间的赋值比数值类型之间的赋值要严格。例如:不用类型转换就可以将int类型的值赋给double类型的变量,但是两种不同类型的指针不能这样做。以下是一些简单的示范:

int n = 100;
double m;
int * p1 = &n;    //允许,类型一致。
double * p2 = &m; //允许,类型一致。
m = n;            //允许,隐式类型转换。
p2 = p1;          //不允许,指针类型不同,编译会出错。

涉及数组的更复杂的指针示范如下:

int box23[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};
int box32[3][2] = {
    {1, 2},
    {3, 4},
    {5, 6}
};
int * p1;
int ** p2;
int (*p3)[3];
    
p1 = &box23[0][0];  //允许,都是指向int的指针。
p1 = box32[0];      //允许,都是指向int的指针。
p1 = box23;         //不允许,p1指向int;box23指向一个内含3个int元素的数组。
p3 = box23;         //允许,都指向一个内含3个int元素的数组。
p3 = box32;         /*不允许,p3指向一个内含3个int元素的数组;
                    box32指向一个内含2个int元素的数组。*/
p2 = &p1;           //允许,都是指向int的指针的指针。
*p2 = box32[0];     //允许,都是指向int的指针。
p2 = box32;         /*不允许,p2是指向int的指针的指针;
                    box32是指向一个内含2个int元素的数组(指针)的指针。*/

除了以上赋值规则外,把const指针赋给非const指针是不安全的,因为这样就可以使用新的指针改变const指针指向的数据。但是反过来,把非const指针赋给const指针是安全的,不过也有前提,那就是只进行一级解引用。而如果进行两级解引用,那么这样的赋值也不安全。例如:

const int **pp;
int *p;
const int n = 100;
pp = &p;  //允许,非const赋给const,但pp的const限定符失效,因为可以使用p修改pp指向内容。
*pp = &n; //允许,都是const,但是导致p指向n,pp指向内容被修改了。
*p = 200; //允许,但是改变了n的值,n的const限定符也失效了。

函数和多维数组- 10.7.4 -

二维数组可以视为一个包含行和列的表格,在处理二维数组的函数中,要正确处理行和列的信息,就需要借助设置正确的函数形参来实现。例如对于box[2][3]来说,可以这样声明函数的形参:

void somefunction(int (*p)[3]);

如果p只用于表示一个形参时,也可以这样声明:

void somefunction(int p[][3]);

空的方括号表明p是一个指针。以下是一个使用函数处理二维数组的完整的示例:

#include <stdio.h>
#define ROWS 2
#define COLS 3
void sum_r(int box[][COLS], int rows); //声明计算行元素的和的函数
void sum_c(int [][COLS], int);         //声明计算列元素的和的函数
int sum_both(int(*p)[COLS], int rows); //声明计算所有元素和的函数

int main(void) {
    int box[ROWS][COLS] = {
        {1, 2, 3},
        {4, 5, 6}
    };
    sum_r(box, ROWS);
    sum_c(box, ROWS);
    printf("Sum of all elements = %d\n", sum_both(box, ROWS));
    system("pause");
    return 0;
}
//定义计算行元素的和的函数
void sum_r(int box[][COLS], int rows) {
    int r;
    int c;
    int tot;
    for (r = 0; r < rows; r++) {
        tot = 0;
        for (c = 0; c < COLS; c++)
            tot += box[r][c];
        printf("row %d: sum = %d\n", r, tot);
    }
}
//定义计算列元素的和的函数
void sum_c(int box[][COLS], int rows) {
    int r;
    int c;
    int tot;
    for (c = 0; c < COLS; c++) {
        tot = 0;
        for (r = 0; r < rows; r++)
            tot += box[r][c];
        printf("col %d: sum = %d\n", c, tot);
    }
}
//定义计算所有元素和的函数
int sum_both(int box[][COLS], int rows) {
    int r;
    int c;
    int tot = 0;
    for (r = 0; r < rows; r++)
        for (c = 0; c < COLS; c++)
            tot += box[r][c];
    return tot;
}

运行结果为:

row 0: sum = 6
row 1: sum = 15
col 0: sum = 5
col 1: sum = 7
col 2: sum = 9
Sum of all elements = 21

需要注意,下面声明多维数组的方式是错误的:

int sum(int box[][], int rows);

编译器会把数组表示法转换成指针表示法,对于box[1],编译器会转换成box+1。但是转换的过程中,编译器需要知道box所指向对象的大小,box[][3]中的“3”就是表明box指向一个内含3个int类型值的数组,box+1需要在box指向地址上加上12个字节(一个int类型占4个字节,4*3=12),而如果第2个[]内是空的,编译器就不知道指针指向对象的大小,也就不能正确完成转换。

通常,声明一个指向N维数组的指针时,只能省略最左边方括号中的值,例如声明一个5维数组:

int sum(int box[][2][3][4][5], int rows);

声明时,第1对方括号只用于表明box是一个指针,其他方括号用于描述指针指向对象的类型。如果在第1对方括号内填写了数值,当然也是可以的,只不过编译器会忽略它。