上一篇我们初识指针,知道了数组名就是首元素地址,arr[i] 就是 *(arr+i)。但如果你就此认为“数组和指针就是一回事”,那可就埋下隐患了——它们有微妙而重要的差别,而且围绕它们还衍生出几个让无数初学者抓狂的概念:指针数组、数组指针、二级指针。
今天我们就来彻底理清这些关系。顺便,你会真正理解 C 语言字符串的本质,以及 main 函数那两个神秘参数 argc 和 argv 到底是怎么回事。
一、数组名 ≠ 指针:三条铁律
数组名在绝大多数表达式中会被自动转换成指向首元素的指针,但这条规则有三个重要的例外。记住这三个例外,你就能分清什么时候数组是数组,什么时候它会变成指针。
例外 1:sizeof(数组名) 得到整个数组的大小
int arr[10];
printf("%zu\n", sizeof(arr)); // 40(假设 int 4 字节)
printf("%zu\n", sizeof(&arr[0])); // 8(指针大小,64位系统)
如果 arr 真是一个指针,sizeof(arr) 应该是指针大小。但它不是,它返回整个数组的字节数。这是区分数组和指针最直接的方法。
例外 2:&数组名 得到指向整个数组的指针
int arr[5];
int *p1 = arr; // 指向 int 的指针,类型 int*
int (*p2)[5] = &arr; // 指向“5个int组成的数组”的指针,类型 int(*)[5]
p2 就是所谓的数组指针,它的类型不是 int*,而是 int(*)[5]。p2+1 会跳过整个数组(20 字节),而不是一个 int。这个区别我们稍后详细展开。
例外 3:用字符串字面量初始化字符数组时
char str[] = "hello"; // 在栈上创建一个数组,内容是 "hello" 的副本
char *ptr = "hello"; // ptr 指向存储在只读数据区的字符串字面量
这是两种完全不同的东西:前者是可修改的字符数组,后者是指向只读字符串的指针。这也是字符串的经典陷阱,我们马上会讲。
二、指针数组 vs 数组指针:别被名字绕晕
这是 C 语言最经典的混淆点。记住一条规则:从右往左读,[] 的优先级高于 *,括号可以改变结合顺序。
指针数组:int *p[10]
读法:p 是一个数组,有 10 个元素,每个元素是 int*(指向 int 的指针)。
int a = 1, b = 2, c = 3;
int *ptr_array[3] = {&a, &b, &c}; // 一个存了三个指针的数组
for (int i = 0; i < 3; i++) {
printf("%d ", *ptr_array[i]); // 输出 1 2 3
}
指针数组最常见的应用,就是存储多个字符串:
char *fruits[] = {"apple", "banana", "cherry"};
// fruits[0] 是一个 char*,指向字符串 "apple" 的首字符
这比用二维 char 数组更灵活——每个字符串的长度可以不同,不用浪费空间。
数组指针:int (*p)[10]
读法:p 是一个指针,它指向一个“有 10 个 int 的数组”。
int arr[10];
int (*p)[10] = &arr; // p 指向 arr 整个数组
// 访问元素:
(*p)[3] = 100; // 等价于 arr[3] = 100;
为什么需要括号?因为 [] 优先级高于 *。没有括号的 int *p[10] 是“装指针的数组”,有括号的 int (*p)[10] 是“指数组的针”。
数组指针最常用于处理二维数组。当你把二维数组传给函数时,形参也可以写成数组指针:
void print_matrix(int (*mat)[4], int rows) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < 4; j++) {
printf("%d ", mat[i][j]);
}
printf("\n");
}
}
int main(void) {
int matrix[3][4] = {
{1,2,3,4}, {5,6,7,8}, {9,10,11,12}
};
print_matrix(matrix, 3);
return 0;
}
matrix 作为参数传递时退化为指向首元素(一行)的指针,那一行是 4 个 int 的数组,所以类型恰好是 int(*)[4]。这就是为什么二维数组形参必须写列数——编译器需要知道每一行多长。
快速对照表
| 写法 | 本质 | 解释 |
|---|---|---|
int *p[10] | 数组 | 10 个 int* 组成的数组 |
int (*p)[10] | 指针 | 指向“10个int数组”的指针 |
int *p(int) | 函数 | 函数名为 p,返回 int*(今天不讲) |
int (*p)(int) | 指针 | 指向“接收int返回int”的函数的指针(以后讲) |
记忆口诀:最后一步看符号——最后解析为数组就是数组,最后解析为指针就是指针。
三、字符串与指针:字符数组 vs 字符指针
在 C 语言里,字符串并不是一种独立的类型。它就是以 '\0' 结尾的字符数组。但对字符串的存储方式有两种截然不同的形式,这个区别一定要分清楚。
形式一:字符数组(可修改)
char str[] = "hello";
str[0] = 'H'; // 合法!数组内容可以修改
printf("%s\n", str); // Hello
str 是一个数组,在栈上分配了 6 个字节(h e l l o \0)。"hello" 的每个字符被复制到了这个数组里。你是这个内存的主人,可以随意改。
形式二:字符指针(指向只读字符串字面量)
char *ptr = "hello";
ptr[0] = 'H'; // 危险!未定义行为,通常会崩溃
这里 ptr 是一个指针,指向字符串字面量 "hello"。这个字面量存储在只读数据区(.rodata),操作系统不允许你修改它。试图修改要么崩溃(段错误),要么什么都没发生(但这是未定义行为,千万不要依赖)。
正确做法:如果你声明了一个指向字符串字面量的指针,并且以后不会修改它,用 const 保护起来:
const char *ptr = "hello"; // ptr[0] = 'H' 直接编译报错
形式三:用指针遍历字符串
不管字符串是以数组还是字面量形式存在,你都可以用指针来遍历它:
char str[] = "hello";
char *p = str;
while (*p != '\0') {
printf("%c ", *p);
p++;
}
// 输出 h e l l o
标准库的 strlen 本质上就是这么干的:
size_t my_strlen(const char *s) {
const char *p = s;
while (*p) p++; // *p 为 '\0' 时退出
return p - s; // 两个指针相减得到元素个数
}
四、命令行参数:argc 与 argv
也许你一直好奇 main 函数的另一种写法:
int main(int argc, char *argv[]) {
// ...
}
现在我们可以理解它了。
argc(argument count):命令行参数的个数,包含程序名本身。argv(argument vector):一个指针数组,每个元素是一个char*,指向一个参数字符串。
char *argv[] 等价于 char **argv(数组作为形参退化为指针,指针数组退化为二级指针)。我们先直观理解,二级指针下一节细说。
假如你的程序叫 echo,运行命令:
./echo hello world 123
那么:
argc= 4argv[0]="./echo"argv[1]="hello"argv[2]="world"argv[3]="123"argv[4]=NULL(标准保证最后一个指针是 NULL)
一个简单程序打印所有参数:
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("参数个数:%d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
argv 的图形化理解:
argv -> [0] -> "./echo\0"
[1] -> "hello\0"
[2] -> "world\0"
[3] -> "123\0"
[4] -> NULL
argv 本身是一个 char**,它指向一个由 char* 指针组成的数组,每个 char* 又指向一个实际的字符串。
五、二级指针初识
如果指针是一个变量,存着另一个变量的地址,那二级指针就是一个变量,存着另一个指针的地址。
int a = 10;
int *p = &a; // p 指向 a
int **pp = &p; // pp 指向 p
printf("%d\n", **pp); // **pp = *(*pp) = *p = a = 10
**pp 就是 a。二级指针最常见的用途:
- 在函数里修改一级指针本身(比如动态分配内存并传出)
- 操作指针数组(比如
argv就是char**)
一个函数示例:修改传入的指针,让它指向新分配的内存(后面动态内存会讲,这里先感性认知):
void allocate(int **ptr) {
*ptr = malloc(sizeof(int)); // 修改 ptr 指向的那个指针
**ptr = 100;
}
int main(void) {
int *p = NULL;
allocate(&p); // 传 p 的地址
printf("%d\n", *p); // 100
free(p);
return 0;
}
在二维动态数组的分配中,二级指针也是核心角色,这个我们在动态内存部分会专门展开。
六、如何读懂复杂声明:右左法则
当声明混有指针、数组、函数时,比如 int *(*p[10])(int),怎么读懂?可以按“右左法则”:
- 从变量名开始。
- 先往右看,遇到
[ ]或( )就读出来。 - 再往左看,遇到
*就读“指针”。 - 遇到括号就跳进去,重复上述过程。
以 char *argv[] 为例:
argv是……先右看,[]说明是数组。- 再左看,
*说明是指针 →argv是一个指针数组。 - 再左看,
char说明每个元素指向字符。
以 int (*p)[10] 为例:
p遇到括号,先处理括号内。左看*,p是指针。- 括号外右看
[10],指向有 10 个元素的数组。 - 左看
int,数组元素是 int。 - 结论:
p是指向有 10 个 int 的数组的指针。
这个法则很实用,以后遇到复杂的声明多试几次就熟了。
七、常见错误与陷阱
1. 把指针数组和数组指针搞反
int (*ptr)[5]; // 指针,指向5个int的数组
int *ptr[5]; // 数组,有5个int*
2. 试图修改字符串字面量
char *s = "hello";
s[0] = 'H'; // 未定义行为,崩溃或无效
用 const char * 或字符数组。
3. 对函数参数中的数组用 sizeof
void func(int arr[]) {
printf("%zu\n", sizeof(arr)); // 总是指针大小,不是数组大小
}
4. 二级指针解引用层级混乱
int a = 5;
int *p = &a;
int **pp = &p;
printf("%d\n", *pp); // 输出的是 p 的值(即 a 的地址),不是 a
*pp 是 p,**pp 才是 a。
八、小结
今天我们把指针和数组的关系理清了。核心记住:
- 数组名在大多数情况下退化为指针,但
sizeof、&数组名、字符串字面量初始化是例外。 - 指针数组(
int *p[10])和数组指针(int (*p)[10])是完全不同的东西,读声明的技巧是看优先级和结合方向。 - 字符串可以用字符数组(可修改)或字符指针(指向字面量,不可修改)来存储。
main的argv是指针数组/二级指针的典型应用。- 二级指针是指向指针的指针,用于间接修改指针本身。
现在你对指针已经有了基本的感觉。但指针最灵活也最危险的用途,还在后面——动态内存分配。下一篇我们就进入 malloc、free、堆与栈的广阔天地,让程序能够按需索取内存,而不再受编译时数组大小的束缚。
课后小练习
- 分别声明一个指针数组和一个数组指针,并写出它们的类型含义。用
sizeof验证它们的大小(数组指针是指针大小,指针数组是多个指针大小)。 - 用
argv实现一个简单的echo程序,要求将命令行参数逆序输出。 - 分析这段代码并修正错误:
char *msg = "hello"; msg[0] = 'H'; printf("%s\n", msg); - 写一个函数
void reverse_string(char *str),用指针操作(不用下标)实现字符串的原地反转。 - (挑战)解释以下声明的含义,并尝试各写一个简单用例:
int *arr1[10]; int (*arr2)[10]; int **arr3;
我们下期见!
112

被折叠的 条评论
为什么被折叠?



