Bootstrap

C语言 指针与数组

引言

1. 指针与数组之间的联系

在 C语言中,虽然我们平时访问数组的时候是用 arr[ i ] 进行表示,但在底层解析的时候,其实是通过 *(arr + i) 这样的指针配合解引用的方式来做到的。这是指针与数组之间所能联系的核心所在。

int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
arr[i]; // *(arr + i);

观察上面两行代码,arr 作为数组名,表示首元素的地址,即 &arr[0].
由于数组的地址是由低到高连续存储的,所以知道了首元素的地址,就能够间接访问到数组的剩余元素了。

2. 指针与字符串之间的联系

由于在 C语言中,字符串通常由字符数组构造,所以字符串也可以通过 " 头部指针 " 来间接地拿到每个字符。这样一来,指针就像一条线一样,可以 " 顺藤摸瓜 " 。

#include <stdio.h>

int main() {

	char arr[] = "hello world";
	printf("%s\n", arr); // arr 表示首元素的地址
 
	return 0;
}

// 输出结果:hello world

一、指针与数组

1. 指针数组与数组指针

#include <stdio.h>

int main() {

	int a = 1, b = 2, c = 3, x = 4, y = 5;

	int arr[5] = { 1,2,3,4,5 }; 			// 整型数组
	int* parr[5] = { &a, &b, &c, &x, &y }; 	// 指针数组
	int (*parr2)[5] = &arr; 				// 数组指针

	return 0;
}

注意事项: [ ] 的优先级要高于 * 号

① 整型数组表示数组,[ ] 先与 arr 结合,所以 arr 本质上就是一个数组。
② 指针数组表示数组,[ ] 先与 parr 结合,所以 parr 本质上就是一个数组。
③ 数组指针表示指针,* 先与 parr2 结合,所以 parr2 本质上就是一个指针,它指向长度为 5 的数组。

1-1

2. 指针数组的用法

指针数组即存放指针的数组,或者说,指针数组是存放地址的数组。

程序清单:打印二维数组

#include <stdio.h>

int main() {

	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 2,3,4,5,6 };
	int arr3[] = { 3,4,5,6,7 };

	int* parr[] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++) {
		for (int j = 0; j < 5; j++) {
			printf("%d ", parr[i][j]);
		}
		printf("\n");
	}

	return 0;
}

// 输出结果:
// 1 2 3 4 5
// 2 3 4 5 6
// 3 4 5 6 7

分析:

1-2

3. 数组指针的用法

数组指针表示指向数组的指针。

程序清单:打印二维数组

#include <stdio.h>

void print(int (*arr)[5], int row, int column) {

	for (int i = 0; i < row; i++) {
		for (int j = 0; j < column; j++) {
			printf("%d ", *(*(arr+i) + j ));
		}
		printf("\n");
	}
}

int main() {

	int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7} };
	int row = sizeof(arr) / sizeof(arr[0]);
	int column = sizeof(arr[0]) / sizeof(arr[0][0]);

	print(arr, row, column);

	return 0;
}

// 输出结果:
// 1 2 3 4 5
// 2 3 4 5 6
// 3 4 5 6 7

1-3

二、数组参数、指针参数

1. 一维数组传参

#include <stdio.h>

void test(int arr[]) {} 	// √
void test(int* arr) {} 		// √

void test2(int* arr[]) {} 	// √
void test2(int** arr) {} 	// √


int main() {

	int arr1[10] = { 0 };
	int* arr2[10] = { 0 };	// 指针数组

	test(arr1); // 首元素的地址 &arr1[0]
	test2(arr2); // 首元素的地址 
}

注意事项:

① arr1 是一个一维整型数组,它存放着 10 个 int 类型的元素。arr1 表示首元素的地址,即 &arr1[0],所以形参在接收时,可以利用 int* 指针接收。

② arr2 是一个一维指针数组,它存放着 10 个 int* 类型的元素。arr2 表示首元素的地址 (首元素本身就是地址,所以传的参数就是地址的地址),所以形参在接收时,可以利用 int** 指针接收。

2. 二维数组传参

#include <stdio.h>

void test(int arr[3][5]) {} 	// √
void test(int arr[][5]) {} 		// √
void test(int arr[][]){} 		// X
void test(int (* arr)[5]){} 	// √

int main() {

	int arr[3][5] = { 0 };
	test(arr);

	return 0;
}

注意事项:

二维数组的数组名表示第一行数组的地址。在函数形参接收时,可以利用数组名直接接收 (但不能省略列数);也可以利用数组指针的方式来接收。

三、指针与函数 (了解)

1. 其实函数也有地址

程序清单:

#include <stdio.h>

int add(int a, int b) {

	return a + b;
}

int main() {

	add(10, 20);

	printf("%p\n", &add);
	printf("%p\n", add);

	return 0;
}

输出结果:

1-4

注意事项:

① 函数在内存中也是有对应的地址的。
② &函数名 和 函数名,两者是一样的意思,都可以当作函数的地址使用。

2. 函数指针

函数指针即一个函数地址,或者说指针指向一个函数。

#include <stdio.h>

int add(int a, int b) {
	return a + b;
}

int main() {
	
	int (*p) (int, int) = &add; // 1 (变量 p 是一个函数指针)
	int ret = (*p)(20, 30); 	// 2
	printf("%d\n", ret);

	return 0;
}

// 输出结果:50

注意事项:

① 注释1,我们可以说 p 指向 add 函数。第一个 int 表示 add 返回值为 int,(*p) 声明了这就是一个指针,(int, int) 表示 add 的形参类型。

② 注释2,(*p) 表示解引用 p,它的含义就等价于 add 函数。通过先解引用,再传参,就相当于使用了 add 函数。

*p <==> *(&add) <==> add	// 解引用

3. 函数指针数组

函数指针数组,顾名思义就是存放函数指针的数组,或者说存放函数地址的数组。

#include <stdio.h>

int add(int a, int b) {
	return a + b;
}

int sub(int a, int b) {
	return a - b;
}

int mult(int a, int b){
	return a * b;
}

int div(int a, int b) {
	return a / b;
}

int main() {

	int (* parr[4])(int, int) = { add, sub, mult, div }; // 函数指针数组

	for (int i = 0; i < 4; i++) {

		int ret =  parr[i](40, 20);		// 函数名直接充当函数的地址
		printf("%d ", ret);
	}

	return 0;
}

// 输出结果:
// 60  20  800  2

注意事项:

① 对于下面这行代码, parr 和 [ ] 先结合,所以 parr 本质上就是一个数组。

int (* parr[4])(int, int) = { add, sub, mult, div };

② 对于下面这行代码, 函数名直接充当函数的地址。

parr[1] 等价 add,add 等价 &add.
parr[2] 等价 sub,sub 等价 &sub.
parr[3] 等价 mult,mult 等价 &mult.
parr[4] 等价 div,div 等价 &div.

int ret =  parr[i](40, 20);

4. qsort 函数中的 compare 函数指针

下面是我之前写的 qsort 函数的博客,qsort 参数中有一个 compare 指针,用来指定用哪个自定义排序函数。如果读者感兴趣的,可以看一下。

qsort 函数博客

四、指针与数组的笔试题

结论

结论1:

对于一维数组来说,数组名就是首元素的地址;
对于二维数组来说,数组名就是第一行数组的地址。

但有两个例外:

① sizeof(数组名),此时数组名表示整个数组,计算的是整个数组占用内存的大小。
② &数组名,此时数组名表示整个数组,取出的是整个数组的地址。

结论2:

指针变量是用来存放地址的。所以,地址的存放需要多大空间,指针变量的大小就应该是多大。

① 32位 机器,支持 32位 虚拟地址空间,其产生的地址就是 32位,所以此时指针变量就需要 32位 的空间存储,即 4字节。
② 64位 机器,支持 64位 虚拟地址空间,其产生的地址就是 64位,所以此时指针变量就需要 64位 的空间存储,即 8字节。

结论3:

① sizeof 是一个操作符,它是用来计算变量 (类型) 所占内存空间大小的,计算单位是 " 字节 "。

② 字符串的结束标志是一个 ’ \0 ’ 的转义字符。在使用格式化输出时, ’ \0 ’ 的作用相当于告诉了编译器, ’ \0 ’ 是一个停止的标志。在使用 strlen 这个库函数计算字符串的长度时,也是一样的道理,它只计算 ’ \0 ’ 之前的长度。

例题1

#include <stdio.h>

int main() {

	int a[] = { 1,2,3,4 };

	printf("%d\n", sizeof(a)); // 求整个数组所占内存的大小 -> 16

	printf("%d\n", sizeof(a + 0)); // sizeof(&arr[0]) -> 4/8

	printf("%d\n", sizeof(*a)); // sizeof(arr[0]) -> 4

	printf("%d\n", sizeof(a + 1)); // sizeof(&arr[1]) -> 4/8

	printf("%d\n", sizeof(a[1])); // 4

	printf("%d\n", sizeof(&a)); // 拿到整个数组的地址,依然是地址 -> 4/8

	printf("%d\n", sizeof(*&a)); // sizeof(a) -> 16

	printf("%d\n", sizeof(&a + 1)); // 跳过整个数组,拿到的还是地址 -> 4/8

	printf("%d\n", sizeof(&a[0])); // 4/8

	printf("%d\n", sizeof(&a[0] + 1)); // sizeof(&arr[1]) 4/8

	return 0;
}

例题2

#include <stdio.h>

int main() {

	char arr[] = { 'a','b','c','d','e','f' }; // [ a b c d e f ]

	printf("%d\n", sizeof(arr)); // 求整个数组所占内存的大小 -> 6

	printf("%d\n", sizeof(arr + 0)); // sizeof(&arr[0]) -> 4/8

	printf("%d\n", sizeof(*arr)); // sizeof(arr[0]) -> 1

	printf("%d\n", sizeof(arr[1])); // 1

	printf("%d\n", sizeof(&arr)); // 拿到整个数组的地址,依然是地址 -> 4/8

	printf("%d\n", sizeof(&arr + 1)); // 跳过整个数组,拿到的还是地址 -> 4/8

	printf("%d\n", sizeof(&arr[0] + 1)); // sizeof(&arr[1]) -> 4/8

	printf("%d\n", strlen(arr)); // 这里 arr 表示首元素的地址,由于 '\0' 不知道在什么位置,所以 strlen 求得为随机值

	printf("%d\n", strlen(arr + 0)); // 随机值

	printf("%d\n", strlen(*arr)); // strlen(arr[0]) ->  strlen 需要接收的是一个指针变量,这里非法访问

	printf("%d\n", strlen(arr[1])); // 同理,非法访问

	printf("%d\n", strlen(&arr)); // 由于 '\0' 不知道在什么位置,随机值

	printf("%d\n", strlen(&arr + 1)); // 由于 '\0' 不知道在什么位置,随机值

	printf("%d\n", strlen(&arr[0] + 1)); // 由于 '\0' 不知道在什么位置,随机值

	return 0;
}

例题3

#include <stdio.h>

int main() {

	char arr[] = "abcdef"; // [ a b c d e f \0 ]

	printf("%d\n", sizeof(arr)); // 7

	printf("%d\n", sizeof(arr + 0)); // sizeof(&arr[0]) -> 4/8

	printf("%d\n", sizeof(*arr)); // sizeof(arr[0]) -> 1

	printf("%d\n", sizeof(arr[1])); // 1

	printf("%d\n", sizeof(&arr)); // 4/8

	printf("%d\n", sizeof(&arr + 1)); // 4/8

	printf("%d\n\n", sizeof(&arr[0] + 1)); // 4/8

	printf("%d\n", strlen(arr)); // 6

	printf("%d\n", strlen(arr + 0)); // 6

	printf("%d\n", strlen(*arr)); // 非法访问

	printf("%d\n", strlen(arr[1])); // 非法访问

	printf("%d\n", strlen(&arr)); // 6

	printf("%d\n", strlen(&arr + 1)); // 跳过整个数组,由于 '\0' 不知道在什么位置,随机值

	printf("%d\n", strlen(&arr[0] + 1)); // strlen(&arr[1]) -> 5

	return 0;
}

例题4

#include <stdio.h>

int main() {

	char* p = "abcdef"; // p 指向 字符 'a',[ a b c d e f \0 ]

	printf("%d\n", sizeof(p)); // 字符 'a' 的地址 -> 4/8

	printf("%d\n", sizeof(p + 1)); // 字符 'b' 的地址 -> 4/8

	printf("%d\n", sizeof(*p)); // sizeof('a') -> 1

	printf("%d\n", sizeof(p[0])); // p[0] <==> *(p+0),sizeof('a') -> 1

	printf("%d\n", sizeof(&p)); // 指针 p 的地址 -> 4/8

	printf("%d\n", sizeof(&p + 1)); // 跳过指针 p 的地址 -> 4/8

	printf("%d\n\n", sizeof(&p[0] + 1)); // 字符 'b' 的地址 -> 4/8

	printf("%d\n", strlen(p)); // 6

	printf("%d\n", strlen(p + 1)); // 从字符 'b' 往后数字符数 -> 5

	//printf("%d\n", strlen(*p)); //  非法访问

	//printf("%d\n", strlen(p[0])); // 非法访问

	printf("%d\n", strlen(&p)); // 随机值

	printf("%d\n", strlen(&p + 1)); // 随机值

	printf("%d\n", strlen(&p[0] + 1)); // 从字符 'b' 往后数字符数 -> 5

	return 0;
}

例题5

#include <stdio.h>

int main() {

	int a[3][4] = { 0 };

	printf("%d\n", sizeof(a)); // 求整个二维数组所占内存的大小 -> 12 * 4 = 48

	printf("%d\n", sizeof(a[0][0])); // 4

	printf("%d\n", sizeof(a[0])); // a[0] 单独放在了 sizeof 的内部,二维数组第一行元素所占内存大小 -> 4*4 = 16

	printf("%d\n", sizeof(a[0] + 1)); // a[0] 并没有单独放在 sizeof 的内部,所以 a[0] 在这里就作为第一行第一个元素的地址
	// 所以 a[0] + 1 在这里就作为第一行第二个元素的地址,sizeof(&arr[0][1]) -> 4/8 

	printf("%d\n", sizeof(*(a[0] + 1))); // sizeof(arr[0][1]) -> 4

	printf("%d\n", sizeof(a + 1)); // 二维数组第二行的地址 ->  4/8

	printf("%d\n", sizeof(*(a + 1))); // 对二维数组第二行的地址解引用 -> 第二行元素的所有元素所占内存的大小 -> 4*4 = 16

	printf("%d\n", sizeof(&a[0] + 1)); // 二维数组第二行的地址 -> 4/8

	printf("%d\n", sizeof(*(&a[0] + 1)));// 对二维数组第二行的地址解引用 -> 第二行元素的所有元素所占内存的大小 -> 4*4 = 16

	printf("%d\n", sizeof(*a)); // 对二维数组第一行的地址解引用 -> 第一行元素的所有元素所占内存的大小 -> 4*4 = 16

	printf("%d\n", sizeof(a[3])); // a[3] 单独放在了 sizeof 的内部,二维数组第四行元素所占内存大小 -> 4*4 = 16

	return 0;
}

五、指针笔试题

例题1

#include <stdio.h>

int main()
{
	int arr[5] = { 1, 2, 3, 4, 5 };
	
	int* ptr = (int*)(&arr + 1); // 将数组指针转换成一个整型指针
	// arr+1 表示跳过一个元素,&arr+1 表示跳过整个数组
	printf("%d, %d\n", *(arr + 1), *(ptr - 1)); 
	return 0;
}

// 输出结果:2, 5

分析:

1-5

例题2

#include <stdio.h>

int main()
{
	int arr[3][2] = { {2, 4}, {6, 8}, {3, 7} };
	int* p = arr[0]; // arr[0] 表示数组名,p指向二维数组的第一行的第一个元素

	printf("%d\n", p[1]); // p[1] -> *(p + 1) -> 第一行第二个元素
	return 0;
}

// 输出结果:4

分析:

1-6

例题3

#include <stdio.h>

int main()
{
	int arr[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

	int* ptr1 = (int*)(&arr + 1); // 跳过整个数组
	int* ptr2 = (int*)(*(arr + 1)); // arr为数组名,表示数组的第一行地址,所以 arr+1 为数组的第二行开头

	printf("%d, %d\n", *(ptr1 - 1), *(ptr2 - 1));
	return 0;
}

// 输出结果:10, 5

分析:

1-7

例题4

#include <stdio.h>

int main()
{
	char* arr[] = { "go","to","school" };
	char** pa = arr; // arr 表示首元素的地址
	
	pa++;
	printf("%s\n", *pa);
	return 0;
}

//输出结果:to

1-8

;