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 的数组。
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
分析:
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. 一维数组传参
#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;
}
输出结果:
注意事项:
① 函数在内存中也是有对应的地址的。
② &函数名 和 函数名,两者是一样的意思,都可以当作函数的地址使用。
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 指针,用来指定用哪个自定义排序函数。如果读者感兴趣的,可以看一下。
四、指针与数组的笔试题
结论
结论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
分析:
例题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
分析:
例题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
分析:
例题4
#include <stdio.h>
int main()
{
char* arr[] = { "go","to","school" };
char** pa = arr; // arr 表示首元素的地址
pa++;
printf("%s\n", *pa);
return 0;
}
//输出结果:to