1 引言考虑下面这段代码c#include stdio.h int main(void) { int arr[5] {10, 20, 30, 40, 50}; int *p arr; /* p 指向 arr[0] */ printf(arr[2] %d\n, arr[2]); /* 使用下标 */ printf(*(arr 2) %d\n, *(arr 2)); /* 使用指针运算 */ printf(p[2] %d\n, p[2]); /* 指针也可以使用下标 */ printf(*(p 2) %d\n, *(p 2)); /* 标准的指针运算 */ return 0; }四种方式都输出30。这说明数组和指针在访问元素时可以互换使用。但它们完全等同吗数组名是指针吗本章将回答这些问题。2 数组名的本质2.1 数组名不是指针变量数组名在大多数表达式中被当作指向数组首元素的指针但它不是指针变量而是一个地址常量。cint arr[5] {1, 2, 3, 4, 5}; int *p arr; /* 合法arr 被当作指向首元素的指针 */ arr p; /* 错误arr 是常量不能被赋值 */ arr; /* 错误arr 是常量不能自增 */关键区别指针是变量可以修改指向别处数组名是常量始终指向固定的地址数组的首地址2.2 两个例外情况数组名不是在任何情况下都等同于指针有两个重要的例外例外1对数组名使用 sizeofcint arr[5]; int *p arr; printf(sizeof(arr) %zu\n, sizeof(arr)); /* 20整个数组的大小 */ printf(sizeof(p) %zu\n, sizeof(p)); /* 8指针本身的大小 */例外2对数组名使用取地址 cint arr[5]; int *p arr; int (*parr)[5] arr; /* arr 是指向整个数组的指针 */ printf(arr %p\n, arr); /* 数组首元素地址 */ printf(arr %p\n, arr); /* 也是数组首地址但类型不同 */ printf(arr 1 %p\n, arr 1); /* 增加 4 字节一个 int */ printf(arr 1 %p\n, arr 1); /* 增加 20 字节整个数组 */arr的类型是int*指向第一个元素arr的类型是int (*)[5]指向整个长度为5的数组3 通过指针访问数组元素3.1 基本方式假设有cint arr[5] {10, 20, 30, 40, 50}; int *p arr; /* p 指向 arr[0] */访问数组元素的几种等价方式访问方式含义示例结果arr[i]数组下标arr[2] 30*(arr i)指针运算*(arr 2) 30p[i]指针下标p[2] 30*(p i)指针运算*(p 2) 303.2 指针移动访问c#include stdio.h int main(void) { int arr[5] {10, 20, 30, 40, 50}; int *p arr; /* 方式1通过移动指针遍历 */ for (int i 0; i 5; i) { printf(%d , *p); /* 输出当前指向的元素 */ p; /* 指向下一个元素 */ } printf(\n); /* 注意p 已经指向数组末尾之后需要重新指向开头 */ p arr; /* 方式2通过偏移量访问 */ for (int i 0; i 5; i) { printf(%d , *(p i)); /* p 本身不变通过偏移访问 */ } printf(\n); return 0; }3.3 指针加减的步长指针加减运算的步长由指向的类型决定cchar *cp; int *ip; double *dp; cp 1; /* 地址增加 1 字节 */ ip 1; /* 地址增加 4 字节假设 int 占4字节 */ dp 1; /* 地址增加 8 字节假设 double 占8字节 */这正是为什么*(p i)能正确访问第 i 个元素的原因——编译器会自动根据类型计算偏移量。4 下标运算的本质4.1 下标运算的等价性C语言标准规定a[i]等价于*(a i)。这是一个非常重要的等价关系它意味着下标运算是通过指针运算实现的a[i]只是*(a i)的语法糖cint arr[5] {10, 20, 30, 40, 50}; /* 下面三行完全等价 */ arr[2] 100; *(arr 2) 100; 2[arr] 100; /* 是的这也可以但千万别这么写 */4.2 为什么可以写成 2[arr]根据等价关系arr[2]→*(arr 2)2[arr]→*(2 arr)加法满足交换律所以*(arr 2)和*(2 arr)是一样的。因此2[arr]在语法上是合法的但这种写法极其不推荐会严重降低代码可读性。c/* 合法但反人类的写法 */ int arr[5] {10, 20, 30, 40, 50}; printf(%d\n, 2[arr]); /* 输出 30 */4.3 对指针使用下标因为p[i]等价于*(p i)所以指针也可以使用下标cint arr[5] {10, 20, 30, 40, 50}; int *p arr 2; /* p 指向 arr[2] */ printf(%d\n, p[0]); /* 30即 arr[2] */ printf(%d\n, p[-1]); /* 20即 arr[1] —— 负下标也是合法的 */ printf(%d\n, p[2]); /* 50即 arr[4] */重要指针可以使用负下标只要结果指针仍在数组范围内。这给了我们很大的灵活性但也要格外小心越界。5 数组作为函数参数5.1 数组参数的退化当数组作为函数参数时它退化为指针c#include stdio.h void func1(int arr[]) /* 这里的 arr 实际上是指针 */ { printf(在 func1 中sizeof(arr) %zu\n, sizeof(arr)); /* 8指针大小 */ } void func2(int *arr) /* 和上面完全等价 */ { printf(在 func2 中sizeof(arr) %zu\n, sizeof(arr)); /* 也是 8 */ } int main(void) { int arr[10] {0}; printf(在 main 中sizeof(arr) %zu\n, sizeof(arr)); /* 40 */ func1(arr); func2(arr); return 0; }重要结论在函数内部无法通过sizeof(arr)/sizeof(arr[0])获取数组长度必须额外传递一个参数表示数组长度5.2 函数参数的几种等价写法c/* 这三种写法在函数内部完全等价 */ void print_array(int arr[], int size) void print_array(int *arr, int size) void print_array(int arr[10], int size) /* 10 被忽略只是文档作用 */ { for (int i 0; i size; i) { printf(%d , arr[i]); /* 或 *(arr i) */ } }5.3 为什么要传递长度c#include stdio.h /* 错误的尝试试图在函数内计算长度 */ void bad_print(int arr[]) { int size sizeof(arr) / sizeof(arr[0]); /* size 8/4 2错误 */ for (int i 0; i size; i) { printf(%d , arr[i]); } } /* 正确的方式 */ void good_print(int arr[], int size) { for (int i 0; i size; i) { printf(%d , arr[i]); } } int main(void) { int arr[10] {1,2,3,4,5,6,7,8,9,10}; bad_print(arr); /* 只输出前两个元素 */ printf(\n); good_print(arr, 10); /* 正确输出全部 */ return 0; }6 指针数组 vs 数组指针这两个概念容易混淆需要区分清楚6.1 指针数组指针数组数组的每个元素都是指针。cint *arr[5]; /* 包含5个 int* 的数组 */cint a 10, b 20, c 30; int *arr[3] {a, b, c}; for (int i 0; i 3; i) { printf(%d , *arr[i]); /* 输出 10 20 30 */ }6.2 数组指针数组指针指向整个数组的指针。cint (*p)[5]; /* p 是一个指针指向包含5个int的数组 */cint arr[5] {1,2,3,4,5}; int (*p)[5] arr; /* p 指向整个数组 */ for (int i 0; i 5; i) { printf(%d , (*p)[i]); /* 通过指针访问数组元素 */ }6.3 区别总结写法名称含义int *p[5]指针数组一个数组包含5个int*元素int (*p)[5]数组指针一个指针指向包含5个int的数组7 综合示例7.1 数组逆序使用指针c#include stdio.h void reverse_array(int *start, int *end) { /* end 指向最后一个元素 */ while (start end) { int temp *start; *start *end; *end temp; start; end--; } } int main(void) { int arr[10] {1,2,3,4,5,6,7,8,9,10}; printf(原数组); for (int i 0; i 10; i) { printf(%d , arr[i]); } printf(\n); reverse_array(arr, arr 9); /* 传递首尾指针 */ printf(逆序后); for (int i 0; i 10; i) { printf(%d , arr[i]); } printf(\n); return 0; }7.2 数组的切片访问c#include stdio.h void print_slice(int *start, int *end) { printf([); while (start end) { printf(%d , *start); } printf(%d]\n, *end); } int main(void) { int arr[10] {1,2,3,4,5,6,7,8,9,10}; print_slice(arr, arr 4); /* 前5个元素 [1 2 3 4 5] */ print_slice(arr 3, arr 7); /* 中间5个元素 [4 5 6 7 8] */ print_slice(arr 5, arr 9); /* 后5个元素 [6 7 8 9 10] */ return 0; }7.3 字符串数组与指针c#include stdio.h int main(void) { /* 二维字符数组每个字符串存储在连续的内存中 */ char names1[][10] {Alice, Bob, Charlie}; /* 指针数组每个元素指向字符串常量 */ char *names2[] {Alice, Bob, Charlie}; /* 访问方式相同 */ for (int i 0; i 3; i) { printf(names1[%d] %s\n, i, names1[i]); printf(names2[%d] %s\n, i, names2[i]); } /* 重要区别能否修改 */ names1[0][0] a; /* 可以修改在栈上 */ /* names2[0][0] a; */ /* 危险字符串常量在只读区不能修改 */ return 0; }8 常见错误与注意事项8.1 混淆数组和指针的 sizeofcvoid func(int arr[]) { int size sizeof(arr) / sizeof(arr[0]); /* 错误arr 是指针 */ }8.2 数组名自增cint arr[5]; arr; /* 错误数组名是常量 */8.3 指针越界cint arr[5] {1,2,3,4,5}; int *p arr 5; /* 指向数组末尾之后允许 */ int x *p; /* 危险越界访问 */8.4 混淆指针数组和数组指针cint *p1[5]; /* 指针数组 */ int (*p2)[5]; /* 数组指针 */ p2 arr; /* 正确 */ p1 arr; /* 错误类型不匹配 */8.5 试图返回局部数组cint* get_array(void) { int arr[5] {1,2,3,4,5}; return arr; /* 危险arr 是局部变量 */ }9 本章小结本章系统介绍了指针与数组的关系1. 数组名的本质大多数情况下被当作指向首元素的指针但它是常量不能修改两个例外sizeof和运算符2. 访问元素的等价方式a[i]等价于*(a i)指针也可以用下标p[i]等价于*(p i)甚至i[a]也合法但千万别用3. 指针加减的步长由指向的类型决定p i增加i * sizeof(指向类型)字节4. 数组作为函数参数退化为指针必须额外传递长度三种写法等价int arr[]、int *arr、int arr[10]5. 容易混淆的概念指针数组int *p[5]数组元素是指针数组指针int (*p)[5]指向整个数组的指针6. 核心结论texta[i] *(a i) i[a] p[i] *(p i)理解这个等价关系就掌握了 C 语言中数组和指针的精髓。
【C语言程序设计】第29篇:指针与数组的关系
1 引言考虑下面这段代码c#include stdio.h int main(void) { int arr[5] {10, 20, 30, 40, 50}; int *p arr; /* p 指向 arr[0] */ printf(arr[2] %d\n, arr[2]); /* 使用下标 */ printf(*(arr 2) %d\n, *(arr 2)); /* 使用指针运算 */ printf(p[2] %d\n, p[2]); /* 指针也可以使用下标 */ printf(*(p 2) %d\n, *(p 2)); /* 标准的指针运算 */ return 0; }四种方式都输出30。这说明数组和指针在访问元素时可以互换使用。但它们完全等同吗数组名是指针吗本章将回答这些问题。2 数组名的本质2.1 数组名不是指针变量数组名在大多数表达式中被当作指向数组首元素的指针但它不是指针变量而是一个地址常量。cint arr[5] {1, 2, 3, 4, 5}; int *p arr; /* 合法arr 被当作指向首元素的指针 */ arr p; /* 错误arr 是常量不能被赋值 */ arr; /* 错误arr 是常量不能自增 */关键区别指针是变量可以修改指向别处数组名是常量始终指向固定的地址数组的首地址2.2 两个例外情况数组名不是在任何情况下都等同于指针有两个重要的例外例外1对数组名使用 sizeofcint arr[5]; int *p arr; printf(sizeof(arr) %zu\n, sizeof(arr)); /* 20整个数组的大小 */ printf(sizeof(p) %zu\n, sizeof(p)); /* 8指针本身的大小 */例外2对数组名使用取地址 cint arr[5]; int *p arr; int (*parr)[5] arr; /* arr 是指向整个数组的指针 */ printf(arr %p\n, arr); /* 数组首元素地址 */ printf(arr %p\n, arr); /* 也是数组首地址但类型不同 */ printf(arr 1 %p\n, arr 1); /* 增加 4 字节一个 int */ printf(arr 1 %p\n, arr 1); /* 增加 20 字节整个数组 */arr的类型是int*指向第一个元素arr的类型是int (*)[5]指向整个长度为5的数组3 通过指针访问数组元素3.1 基本方式假设有cint arr[5] {10, 20, 30, 40, 50}; int *p arr; /* p 指向 arr[0] */访问数组元素的几种等价方式访问方式含义示例结果arr[i]数组下标arr[2] 30*(arr i)指针运算*(arr 2) 30p[i]指针下标p[2] 30*(p i)指针运算*(p 2) 303.2 指针移动访问c#include stdio.h int main(void) { int arr[5] {10, 20, 30, 40, 50}; int *p arr; /* 方式1通过移动指针遍历 */ for (int i 0; i 5; i) { printf(%d , *p); /* 输出当前指向的元素 */ p; /* 指向下一个元素 */ } printf(\n); /* 注意p 已经指向数组末尾之后需要重新指向开头 */ p arr; /* 方式2通过偏移量访问 */ for (int i 0; i 5; i) { printf(%d , *(p i)); /* p 本身不变通过偏移访问 */ } printf(\n); return 0; }3.3 指针加减的步长指针加减运算的步长由指向的类型决定cchar *cp; int *ip; double *dp; cp 1; /* 地址增加 1 字节 */ ip 1; /* 地址增加 4 字节假设 int 占4字节 */ dp 1; /* 地址增加 8 字节假设 double 占8字节 */这正是为什么*(p i)能正确访问第 i 个元素的原因——编译器会自动根据类型计算偏移量。4 下标运算的本质4.1 下标运算的等价性C语言标准规定a[i]等价于*(a i)。这是一个非常重要的等价关系它意味着下标运算是通过指针运算实现的a[i]只是*(a i)的语法糖cint arr[5] {10, 20, 30, 40, 50}; /* 下面三行完全等价 */ arr[2] 100; *(arr 2) 100; 2[arr] 100; /* 是的这也可以但千万别这么写 */4.2 为什么可以写成 2[arr]根据等价关系arr[2]→*(arr 2)2[arr]→*(2 arr)加法满足交换律所以*(arr 2)和*(2 arr)是一样的。因此2[arr]在语法上是合法的但这种写法极其不推荐会严重降低代码可读性。c/* 合法但反人类的写法 */ int arr[5] {10, 20, 30, 40, 50}; printf(%d\n, 2[arr]); /* 输出 30 */4.3 对指针使用下标因为p[i]等价于*(p i)所以指针也可以使用下标cint arr[5] {10, 20, 30, 40, 50}; int *p arr 2; /* p 指向 arr[2] */ printf(%d\n, p[0]); /* 30即 arr[2] */ printf(%d\n, p[-1]); /* 20即 arr[1] —— 负下标也是合法的 */ printf(%d\n, p[2]); /* 50即 arr[4] */重要指针可以使用负下标只要结果指针仍在数组范围内。这给了我们很大的灵活性但也要格外小心越界。5 数组作为函数参数5.1 数组参数的退化当数组作为函数参数时它退化为指针c#include stdio.h void func1(int arr[]) /* 这里的 arr 实际上是指针 */ { printf(在 func1 中sizeof(arr) %zu\n, sizeof(arr)); /* 8指针大小 */ } void func2(int *arr) /* 和上面完全等价 */ { printf(在 func2 中sizeof(arr) %zu\n, sizeof(arr)); /* 也是 8 */ } int main(void) { int arr[10] {0}; printf(在 main 中sizeof(arr) %zu\n, sizeof(arr)); /* 40 */ func1(arr); func2(arr); return 0; }重要结论在函数内部无法通过sizeof(arr)/sizeof(arr[0])获取数组长度必须额外传递一个参数表示数组长度5.2 函数参数的几种等价写法c/* 这三种写法在函数内部完全等价 */ void print_array(int arr[], int size) void print_array(int *arr, int size) void print_array(int arr[10], int size) /* 10 被忽略只是文档作用 */ { for (int i 0; i size; i) { printf(%d , arr[i]); /* 或 *(arr i) */ } }5.3 为什么要传递长度c#include stdio.h /* 错误的尝试试图在函数内计算长度 */ void bad_print(int arr[]) { int size sizeof(arr) / sizeof(arr[0]); /* size 8/4 2错误 */ for (int i 0; i size; i) { printf(%d , arr[i]); } } /* 正确的方式 */ void good_print(int arr[], int size) { for (int i 0; i size; i) { printf(%d , arr[i]); } } int main(void) { int arr[10] {1,2,3,4,5,6,7,8,9,10}; bad_print(arr); /* 只输出前两个元素 */ printf(\n); good_print(arr, 10); /* 正确输出全部 */ return 0; }6 指针数组 vs 数组指针这两个概念容易混淆需要区分清楚6.1 指针数组指针数组数组的每个元素都是指针。cint *arr[5]; /* 包含5个 int* 的数组 */cint a 10, b 20, c 30; int *arr[3] {a, b, c}; for (int i 0; i 3; i) { printf(%d , *arr[i]); /* 输出 10 20 30 */ }6.2 数组指针数组指针指向整个数组的指针。cint (*p)[5]; /* p 是一个指针指向包含5个int的数组 */cint arr[5] {1,2,3,4,5}; int (*p)[5] arr; /* p 指向整个数组 */ for (int i 0; i 5; i) { printf(%d , (*p)[i]); /* 通过指针访问数组元素 */ }6.3 区别总结写法名称含义int *p[5]指针数组一个数组包含5个int*元素int (*p)[5]数组指针一个指针指向包含5个int的数组7 综合示例7.1 数组逆序使用指针c#include stdio.h void reverse_array(int *start, int *end) { /* end 指向最后一个元素 */ while (start end) { int temp *start; *start *end; *end temp; start; end--; } } int main(void) { int arr[10] {1,2,3,4,5,6,7,8,9,10}; printf(原数组); for (int i 0; i 10; i) { printf(%d , arr[i]); } printf(\n); reverse_array(arr, arr 9); /* 传递首尾指针 */ printf(逆序后); for (int i 0; i 10; i) { printf(%d , arr[i]); } printf(\n); return 0; }7.2 数组的切片访问c#include stdio.h void print_slice(int *start, int *end) { printf([); while (start end) { printf(%d , *start); } printf(%d]\n, *end); } int main(void) { int arr[10] {1,2,3,4,5,6,7,8,9,10}; print_slice(arr, arr 4); /* 前5个元素 [1 2 3 4 5] */ print_slice(arr 3, arr 7); /* 中间5个元素 [4 5 6 7 8] */ print_slice(arr 5, arr 9); /* 后5个元素 [6 7 8 9 10] */ return 0; }7.3 字符串数组与指针c#include stdio.h int main(void) { /* 二维字符数组每个字符串存储在连续的内存中 */ char names1[][10] {Alice, Bob, Charlie}; /* 指针数组每个元素指向字符串常量 */ char *names2[] {Alice, Bob, Charlie}; /* 访问方式相同 */ for (int i 0; i 3; i) { printf(names1[%d] %s\n, i, names1[i]); printf(names2[%d] %s\n, i, names2[i]); } /* 重要区别能否修改 */ names1[0][0] a; /* 可以修改在栈上 */ /* names2[0][0] a; */ /* 危险字符串常量在只读区不能修改 */ return 0; }8 常见错误与注意事项8.1 混淆数组和指针的 sizeofcvoid func(int arr[]) { int size sizeof(arr) / sizeof(arr[0]); /* 错误arr 是指针 */ }8.2 数组名自增cint arr[5]; arr; /* 错误数组名是常量 */8.3 指针越界cint arr[5] {1,2,3,4,5}; int *p arr 5; /* 指向数组末尾之后允许 */ int x *p; /* 危险越界访问 */8.4 混淆指针数组和数组指针cint *p1[5]; /* 指针数组 */ int (*p2)[5]; /* 数组指针 */ p2 arr; /* 正确 */ p1 arr; /* 错误类型不匹配 */8.5 试图返回局部数组cint* get_array(void) { int arr[5] {1,2,3,4,5}; return arr; /* 危险arr 是局部变量 */ }9 本章小结本章系统介绍了指针与数组的关系1. 数组名的本质大多数情况下被当作指向首元素的指针但它是常量不能修改两个例外sizeof和运算符2. 访问元素的等价方式a[i]等价于*(a i)指针也可以用下标p[i]等价于*(p i)甚至i[a]也合法但千万别用3. 指针加减的步长由指向的类型决定p i增加i * sizeof(指向类型)字节4. 数组作为函数参数退化为指针必须额外传递长度三种写法等价int arr[]、int *arr、int arr[10]5. 容易混淆的概念指针数组int *p[5]数组元素是指针数组指针int (*p)[5]指向整个数组的指针6. 核心结论texta[i] *(a i) i[a] p[i] *(p i)理解这个等价关系就掌握了 C 语言中数组和指针的精髓。