C 语言数组与指针的深入理解

来深入理解一下数组与指针吧~

C 语言函数指针的一些理解中已经梳理过了函数指针、指针数组以及二者结合的使用情形,现在继续深入理解一下数组与指针之间的差异与联系。

回顾

首先,看一段代码回顾一下数组与指针的常规使用:

test1
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>

int main() {
int arr[] = {1, 2, 3}, *pa = arr;
for(int i = 0; i < sizeof(arr) / sizeof(*arr); i++)
printf("%d\n", *pa++);
return 0;
}

类似的,还有下面这样的用法:

test2
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>

int main() {
int arr[] = {1, 2, 3}, *pa = arr;
for(int i = 0; i < sizeof(arr) / sizeof(*arr); i++)
printf("%d\n", pa[i]);
return 0;
}

以及,这样的用法:

test3
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>

int main() {
int arr[] = {1, 2, 3}, *pa = arr;
for(int i = 0; i < sizeof(arr) / sizeof(*arr); i++)
printf("%d\n", *(pa + i));
return 0;
}

以上这些用法,都是比较容易理解的指针在数组上的简单应用。
下面,再来仔细研究一下数组与指针的关系。

一维数组

在之前的文章里面,已经提到过了数组名与指针指的不是同一种东西,用来区分二者不同的工具是sizeof关键字,也就是下面这段代码:

test4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>

int main() {
int arr[] = {1, 2, 3}, *pa = arr;
printf("arr = %p, pa = %p\n", arr, pa);
printf("sizeof(arr) = %d\n", sizeof(arr));
printf("sizeof(pa) = %d\n", sizeof(pa));
return 0;
}
/*
out:
arr = 0061FF10, pa = 0061FF10
sizeof(arr) = 12
sizeof(pa) = 4
*/

显然,从上面的输出结果中可以知道arr这个数组名占了 12 个字节,也就是它总元素个数乘以单位元素的大小;同时,我们也知道了一个普通int *指针所占大小是 4。尽管它们值都是地址且相同,但二者是完全不一样的。

实际上,按照 C 语言的规定,数组名是一个常量,并且其值是一个地址,但并不能将数组名与指针等价。
在前面的代码中,使用了一个int *指针来访问数组的元素,实际上这个指针指向的是数组的元素。如果,需要有一个指针指向数组,那么需要按照如下的方式声明指针:

test5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

int main() {
int arr[3] = {1, 2, 3};
int (*parr)[3] = &arr;
printf("arr = %p\n", arr);
printf("parr = %p\n", parr);
return 0;
}
/*
out:
arr = 0061FF10
parr = 0061FF10
*/

这与之前讨论函数指针的用法时是一致的,也就是需要使用()将指针名与*括起来,从而告诉编译器,这是一个指针,而不是指针数组。
但此时需要注意的是,parr是一个数组指针,并不能像使用pa一样遍历数组。而pa之所以能遍历数组的原因是因为 C 语言中存在一个叫做偏移量的概念,在声明了pa是一个int *后,每次执行pa + 1,那么pa执行的地址就会移动 4 个字节,这就是偏移量(offset)。

二维数组

现在,再回头看二维数组。
我们先声明一个二维数组,并输出对应的地址:

test6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>

int main() {
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
printf("arr = %p\n", arr);
printf("*arr = %p\n", *arr);
printf("**arr = %d\n", **arr);
return 0;
}
/*
out:
arr = 0061FEFC
*arr = 0061FEFC
**arr = 1
*/

可以发现,arr*arr表示的值都是一样的地址,也即数组首元素的地址;并且arr这个数组名需要进行两次解引用操作,才可以表示数组的元素。
实际上,arr就是在 test5 中声明的数组指针,我们可以用下面的代码来验证:

test7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>

int main() {
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
printf("arr = %p\n", arr);
printf("arr + 1 = %p\n", arr + 1);
printf("**arr = %d\n", **arr);
printf("**(arr + 1) = %d\n", **(arr + 1));
return 0;
}
/*
out:
arr = 0061FEFC
arr + 1 = 0061FF08
**arr = 1
**(arr + 1) = 4
*/

在上面的代码中,借用了一维数组与指针的技巧,将arr指针移动了一个偏移量,得到了两个不同的地址。。而两个地址的差值:$0061FEFC_{16} - 0061FF08_{(16)} = C_{(16)} = 12_{(10)}$,这个 12 正好就是 3 个int的大小,所以**(arr + 1) = 4

现在,我们就可以写出下面的代码了:

test8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>

int main() {
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int (*parr)[3] = arr;
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++)
printf("%d ", parr[i][j]);
putchar('\n');
}
return 0;
}
/*
out:
1 2 3
4 5 6
7 8 9
*/

或者是:

test9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>

int main() {
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int (*parr)[3] = arr;
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++)
printf("%d ", *(*(parr + i) + j));
putchar('\n');
}
return 0;
}
/*
out:
1 2 3
4 5 6
7 8 9
*/

类似的用法,其实都是差不多的。

数组传参

接下来,我们在考虑如何在函数中传入数组的问题。
就一维数组而言,直接利用一个指针即可,比如:

test10
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>

void fun(int *a, int size) {
for(int i = 0; i < size; i++)
printf("%d ", *(a + i));
putchar('\n');
}

int main() {
int arr[3] = {1, 2, 3};
fun(arr, 3);
return 0;
}
/*
out:
1 2 3
*/

函数的声明也可以写成void fun(int a[], int size);,这样写其实更新醒目。
同样,二维数组也类似,但是需要指定第二维的大小,也就是:

test11
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

void fun(int (*arr)[3]) {
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++)
printf("%d ", *(*(arr + i) + j));
putchar('\n');
}
}

int main() {
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
fun(arr);
return 0;
}
/*
out:
1 2 3
4 5 6
7 8 9
*/

同样,函数的声明也可以写成void fun(int arr[][3]);

下面,我们来做一件有趣的事情,先看代码:

test12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>

void fun(int *arr) {
for(int i = 0; i < 9; i++)
printf("%d ", *(arr + i));
putchar('\n');
}

int main() {
int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
fun(arr);
return 0;
}
/*
out:
1 2 3 4 5 6 7 8 9
*/

在上面的代码中,我们把二维数组的首地址,当作一维数组的首地址传给函数,尽管编译有 warnings,但是程序运行的结果是符合预期的。为什么会出现这种现象呢?
实际上,二维数组在内存中的存储方式与一维数组别无二致,都是一个接一个顺序排列的,这也就意味着我们可以利用前面提到的“偏移量”这个概念来直接读取内存,达到我们的目的。
这就是 C 语言区别于 Java 这类语言的地方,它允许程序员自己操作内存,但前提是你要知道自己在干什么,会得到什么样的效果,是否符合预期。同时,上面这个例子,也告诉我们,程序中的 warnings,不可忽略,稍不注意就会产生错误。

字符数组

下面,再单独看看字符指针数组,先看下面的代码:

test13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>

int main() {
char *str1 = "How";
char str2[5] = "How";
printf("str1 = %s\n", str1);
printf("str2 = %s\n", str2);
str1 = "are";
// str2 = "are"; // error assignment
printf("str1 = %s\n", str1);
return 0;
}
/*
out:
str1 = How
str2 = How
str1 = are
*/

前面已经提到过数组名是一个常量,所以这里的str2就不能直接被赋值,但是str1仅仅只是一个指针,它是可以被赋值的。实际上,这个过程也很容易想明白,重新赋值str1无非是要让这个指针再指向另外一块内存即可,但重新赋值str2意味着要在内存中找一块一样大小的空间,并写入与原先相同的值,这是很复杂的事情。

尽管字符数组无法直接赋值,但字符指针数组是可以直接赋值的,比如:

test14
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>

int main() {
char *str[4] = {"I", "am", "a", "cat."};
for(int i = 0; i < 4; i++)
printf("%s ", str[i]);
putchar('\n');
str[3] = "mouse.";
for(int i = 0; i < 4; i++)
printf("%s ", str[i]);
putchar('\n');
return 0;
}
/*
out:
I am a cat.
I am a mouse.
*/

总结起来,也就是一句话:数组名无法直接被赋值,但指针可以直接被赋值。在进行赋值操作时,注意区分数组名和指针即可。

结尾

到这里,有关函数、指针和数组之间的基本理解,基本上已经说完了。其他与指针、函数和数组组合一起使用的东西本质上也就是多层概念的嵌套使用,如果基本概念扎实,仔细分析之后也能得出结果。比如下面两种:

test15
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>

int main() {
int *(*funcp[3])(int);
printf("sizeof(funcp) = %d\n", sizeof(funcp));
int (*(*funcp2)[3])(int);
printf("sizeof(funcp2) = %d\n", sizeof(funcp2));

exit(0);
}
/*
out:
sizeof(funcp) = 12
sizeof(funcp2) = 4
*/

根据sizeof关键字,我们可以知道,funcp是指向指针函数的函数指针数组,funcp2是指向函数指针数组的指针。不得不说,指针真的是个强大的东西。尽管如此,真正写代码的时候这样写,大概率会被打的很惨🤣~尽量保持简单、可读性高的代码才是比起炫技更应该值得考虑的事情。

OK,到此结束,后续想到了再补充~


Buy me a coffee ? :)
0%