一、指针和数组名可互换的场景(核心:数组名被隐式转换为指针)
在绝大多数表达式场景中,数组名会被编译器隐式转换为指向数组第一个元素的指针(这个转换也叫“数组退化为指针”),此时二者的用法完全等价。
1. 访问数组元素([]下标访问)
数组名用下标访问,本质是*(数组名 + 偏移量);指针也支持同样的语法,二者完全互换。
#include <stdio.h> int main() { int arr[5] = {1,2,3,4,5}; int *p = arr; // 数组名arr隐式转为指向arr[0]的指针 // 以下4种写法完全等价,输出均为3 printf("%d\n", arr[2]); // 数组名+下标 printf("%d\n", *(arr+2)); // 数组名转指针+指针运算 printf("%d\n", p[2]); // 指针+下标 printf("%d\n", *(p+2)); // 指针+指针运算 return 0; }2. 函数参数传递
当数组作为函数参数时,编译器会自动将其转换为指向第一个元素的指针(即使写int arr[],实际也是int *arr),此时形参的指针和实参的数组名完全互换。
// 形参写int arr[] 或 int *arr 效果完全一样 void print_arr(int arr[], int len) { for(int i=0; i<len; i++) { printf("%d ", *(arr+i)); // 等价于arr[i] } } int main() { int arr[5] = {1,2,3,4,5}; int *p = arr; print_arr(arr, 5); // 传数组名 print_arr(p, 5); // 传指针,效果完全相同 return 0; }3. 取地址后访问(&)
数组名取地址(&arr)是指向整个数组的指针,指针取地址(&p)是指向指针变量的指针,但如果仅用于访问元素,仍可互换(需注意类型匹配)。
int arr[5] = {1,2,3,4,5}; int *p = arr; // 访问arr[1],以下写法等价 printf("%d\n", (*&arr)[1]); // &arr是数组指针,解引用后回到数组名 printf("%d\n", (*&p)[1]); // &p是指针的指针,解引用后回到指针p二、指针和数组名不可互换的场景(核心:数组名是“常量”,指针是“变量”)
数组名的本质是数组首元素地址的常量(不能被修改),而指针是存放地址的变量(可自由修改),这一本质区别导致以下场景绝对不能混用。
1. 试图修改数组名(赋值操作)
数组名是常量,不能被赋值;指针是变量,可随意赋值指向不同地址。
int arr[5] = {1,2,3,4,5}; int *p = arr; p++; // 合法:指针变量可以自增,指向arr[1] // arr++; // 非法!编译报错:数组名是常量,不能修改(lvalue required) p = &arr[2]; // 合法:指针指向arr[2] // arr = &arr[2]; // 非法!数组名不能被赋值2. 使用sizeof运算符
sizeof(数组名)计算的是整个数组的字节大小;sizeof(指针)计算的是指针变量本身的字节大小(与平台有关,32位系统4字节,64位8字节),二者结果完全不同。
int arr[5] = {1,2,3,4,5}; int *p = arr; // 关键区别! printf("%zu\n", sizeof(arr)); // 输出20(5个int,每个4字节:5*4=20) printf("%zu\n", sizeof(p)); // 输出8(64位系统)或4(32位系统) // 即使指针指向整个数组,结果仍不同 int (*arr_ptr)[5] = &arr; // 指向整个数组的指针 printf("%zu\n", sizeof(arr_ptr)); // 还是8/4,不是203. 取地址运算的类型和偏移
&arr是指向整个数组的指针(类型为int (*)[5]),&p是指向指针变量的指针(类型为int **),二者的偏移规则完全不同,混用会导致地址错误。
int arr[5] = {1,2,3,4,5}; int *p = arr; // &arr是数组指针(int (*)[5]),+1偏移整个数组的大小(20字节) printf("%p\n", &arr); // 假设输出0x7ffeefbff560 printf("%p\n", &arr + 1); // 输出0x7ffeefbff574(0x560 + 20 = 0x574) // &p是指针的指针(int **),+1偏移指针变量的大小(8字节) printf("%p\n", &p); // 假设输出0x7ffeefbff558 printf("%p\n", &p + 1); // 输出0x7ffeefbff560(0x558 + 8 = 0x560) // 若强行混用,会访问错误地址 int (*ap)[5] = &p; // 类型不匹配,编译警告,运行时访问错误4. 字符串常量与字符数组(特殊场景)
字符数组名是可修改的(数组元素可改),而指向字符串常量的指针是只读的,混用会导致运行时错误。
// 字符数组:内存在栈上,可修改 char str_arr[] = "hello"; str_arr[0] = 'H'; // 合法,输出Hello // 字符串指针:指向只读数据区(.rodata),不可修改 char *str_ptr = "hello"; // str_ptr[0] = 'H'; // 非法!编译可能通过,但运行时崩溃(段错误)三、底层原理总结
特性 | 数组名 | 指针 |
本质 | 数组首元素地址的常量 | 存放地址的变量 |
是否可修改 | 不可(如++、赋值) | 可(如++、指向新地址) |
sizeof运算 | 整个数组的字节大小 | 指针变量本身的字节大小 |
取地址(&)类型 | 指向整个数组的指针 | 指向指针变量的指针 |
总结
- 可互换场景:仅当数组名被隐式转换为指针时(如下标访问、函数参数传递),指针和数组名的用法完全等价;
- 不可互换场景:涉及修改操作(如++、赋值)、
sizeof运算、取地址后的偏移、字符串常量修改时,二者本质不同,绝对不能混用; - 核心记忆点:数组名是“常量指针”(地址不可改),指针是“变量指针”(地址可改),这是二者所有区别的根源。