指针是C语言中至关重要的概念,同时也是许多初学者的学习难点。它通常需要反复学习和实践才能被真正理解。本文将系统性地梳理指针相关的核心知识点,从基础概念到复杂应用,为你构建清晰的指针知识体系。
复杂类型说明
理解复杂类型是掌握指针的关键。类型声明中会出现各种运算符,它们和普通表达式一样拥有优先级。分析的关键原则是:从变量名开始,根据运算符的优先级逐步结合分析。
下面我们从简单到复杂,逐一解析:
int p;
这是一个普通的整型变量。
int *p;
首先从 p 开始,与 * 结合,说明 p 是一个指针。再与 int 结合,说明指针指向 int 型数据。因此,p 是一个指向整型数据的指针。
int p[3];
首先从 p 开始,与 [] 结合(其优先级比 * 高),说明 p 是一个数组。再与 int 结合,说明数组元素是整型的。因此,p 是一个由整型数据组成的数组。
int *p[3];
首先从 p 开始,与 [] 结合,说明 p 是一个数组。再与 * 结合,说明数组的元素是指针类型。最后与 int 结合,说明指针指向整型数据。因此,p 是一个由指向整型的指针组成的数组。
int (*p)[3];
首先从 p 开始,与 * 结合,说明 p 是一个指针。再与 [3] 结合(括号用于改变优先级),说明指针指向一个包含3个元素的数组。最后与 int 结合,说明数组元素是整型的。因此,p 是一个指向整型数组的指针。
int **p;
首先从 p 开始,与 * 结合,说明 p 是一个指针。再与 * 结合,说明该指针指向另一个指针。最后与 int 结合,说明二级指针最终指向整型数据。
int p(int);
从 p 开始,与 () 结合,说明 p 是一个函数,括号内的 int 说明它接受一个整型参数。最外层的 int 说明函数的返回值是整型。
int (*p)(int);
从 p 开始,与 * 结合,说明 p 是一个指针。再与 (int) 结合,说明指针指向一个接受一个整型参数的函数。最外层的 int 说明该函数的返回值是整型。因此,p 是一个指向特定函数的指针。
理解以上这些基本类型,对于解析更复杂的声明将大有裨益。不过在实际编程中,应尽量避免使用过于复杂的类型,以保证代码的可读性。
分析指针的方法
指针是一个特殊的变量,其存储的数值被解释为内存中的一个地址。要彻底理解一个指针,需要从以下四个方面入手:指针的类型、指针所指向的类型、指针的值(即所指向的内存地址)、指针本身所占用的内存区。
先声明几个指针作为例子:
(1)int *ptr;
(2)char *ptr;
(3)int **ptr;
(4)int (*ptr)[3];
(5)int *(*ptr)[4];
1、指针的类型
从语法上看,将指针声明语句中的指针名字去掉,剩下的部分就是指针本身的类型。
(1)int *ptr; 指针的类型是 int*
(2)char *ptr; 指针的类型是 char*
(3)int **ptr; 指针的类型是 int**
(4)int (*ptr)[3]; 指针的类型是 int(*)[3]
(5)int *(*ptr)[4]; 指针的类型是 int*(*)[4]
2、指针所指向的类型
当你通过指针访问内存时,指针所指向的类型决定了编译器如何解释那片内存中的数据。*将指针声明语句中的指针名字和它左边的 `` 去掉,剩下的就是指针所指向的类型**。
(1)int *ptr; 指向的类型是 int
(2)char *ptr; 指向的类型是 char
(3)int **ptr; 指向的类型是 int*
(4)int (*ptr)[3]; 指向的类型是 int()[3] (一个整型数组)
(5)int *(*ptr)[4]; 指向的类型是 int*()[4] (一个指针数组)
3、指针的值
指针的值就是其存储的地址数值。在32位程序中,所有类型的指针值都是一个32位整数。
指针所指向的内存区,就是从该地址开始、长度为 sizeof(指针所指向的类型) 的一片连续区域。
4、指针本身所占据的内存区
指针变量自身也占用内存空间。使用 sizeof(指针的类型) 即可得到其大小。在32位平台下,任何类型的指针通常都占据4个字节。
指针的算术运算
指针可以进行加减整数运算,但其意义与普通数值运算不同,它是以所指向类型的大小为单位进行地址偏移的。
例如,在32位环境下,int 占4字节,char 占1字节。对 int 指针加1,地址值实际增加4;对 char 指针加1,地址值增加1。
注意:并非所有整数运算都适用于指针,例如两个指针不能相乘。
示例程序
#include <stdio.h>
int main(void)
{
int a = 10, *pa = &a;
float b = 6.6, *pb = &b;
char c = 'a', *pc = &c;
double d = 2.14e9, *pd = &d;
//最初的值
printf("pa0=%d, pb0=%d, pc0=%d, pd0=%d\n", pa, pb, pc, pd);
//加法运算
pa += 2;
pb += 2;
pc += 2;
pd += 2;
printf("pa1=%d, pb1=%d, pc1=%d, pd1=%d\n", pa, pb, pc, pd);
//减法运算
pa -= 1;
pb -= 1;
pc -= 1;
pd -= 1;
printf("pa2=%d, pb2=%d, pc2=%d, pd2=%d\n", pa, pb, pc, pd);
return 0;
}
运行结果为:
pa0=6422268, pb0=6422264, pc0=6422263, pd0=6422248
pa1=6422276, pb1=6422272, pc1=6422265, pd1=6422264
pa2=6422272, pb2=6422268, pc2=6422264, pd2=6422256
解析:
以 pa 为例,pa0 到 pa1:pa0 + 2 * sizeof(int) = pa1;pa1 到 pa2:pa1 - 1 * sizeof(int) = pa2。因为 pa 是 int 型指针,加减运算以4字节为单位偏移。

如上图所示,pa1 在 pa0 之后8字节处,pa2 在 pa1 之前4字节处。同时,这个例子也表明连续定义的变量在内存中的存储地址不一定是紧挨着的。
数组和指针的联系
数组与指针关系密切,常见的结合形式有三种:数组指针、指针数组和二维数组指针。
1、数组指针
指向数组的指针。例如:
int arr[] = {0,1,2,3,4};
int *p = arr; // 等价于 int *p = &arr[0];
此时,p、arr、&arr[0] 都指向数组首元素的地址。
如果指针 p 指向数组开头,则 p+i 为第 i 个元素的地址 &arr[i],*(p+i) 为第 i 个元素的值 arr[i]。
示例:
#include <stdio.h>
int main(void)
{
int arr[] = {0, 1, 2, 3, 4};
int *p = &arr[3]; // 也可以写作 int *p = arr + 3;
printf("%d, %d, %d, %d, %d\n",
*(p-3), *(p-2), *(p-1), *(p), *(p+1) );
return 0;
}
运行结果:0, 1, 2, 3, 4
2、指针数组
数组中的每个元素都是一个指针。例如:
int a=1,b=2,c=3;
int *arr[3] = {&a, &b, &c};
示例程序:
#include <stdio.h>
int main(void)
{
int a = 1, b = 2, c = 3;
//定义一个指针数组
int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *parr[]
//定义一个指向指针数组的指针
int **parr = arr;
printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
return 0;
}
第一个 printf 中,arr[i] 是指针,*arr[i] 取得其指向的数据。
第二个 printf 中,parr+i 是指向第 i 个指针元素的地址,*(parr+i) 是该指针元素的值,**(parr+i) 是最终的数据。
指针数组也常用于处理字符串:
#include <stdio.h>
int main(void)
{
char *str[3] =
{
"hello C",
"hello C++",
"hello Java"
};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}
运行结果:
hello C
hello C++
hello Java
3、二维数组指针
指向二维数组的指针。例如:
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a;
a[3][4] 是一个3行4列的二维数组,元素在内存中连续存储。p 是一个指向包含4个整型元素的一维数组的指针。
可以得出以下等价关系:
a+i == p+i
*(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)
指针与数组的区别
数组与指针在多数情况下可以等价使用,例如:
int array[10]={0,1,2,3,4,5,6,7,8,9}, value;
value=array[0]; // 等价于 value=*array;
value=array[3]; // 等价于 value=*(array+3);
但在以下三种情况下,它们并不等价:
区别一:可变性
数组名是一个常量指针,其指向不可改变;而指向数组的普通指针变量可以改变。
#include <stdio.h>
int main(void)
{
int a[5] = {0, 1, 2, 3, 4}, *p = a;
char i;
// 方式一:使用指针遍历(允许)
for ( i = 0; i < 5; i++ )
{
printf("a[%d] = %d\n", i, *p++); // p++ 改变指针指向
}
// 方式二:使用数组名自增遍历(错误!)
// for ( i = 0; i < 5; i++ )
// {
// printf("a[%d] = %d\n", i, *a++); // a++ 非法,a是常量
// }
// 方式三:使用数组名加偏移遍历(正确)
for ( i = 0; i < 5; i++ )
{
printf("a[%d] = %d\n", i, *(a+i)); // a 的指向未变
}
return 0;
}
区别二:字符串的可修改性
用字符数组定义的字符串,其字符可以修改;用字符指针指向的字符串字面量,其字符通常不可修改(存储在常量区)。
// 方式一:字符数组(可修改)
char str1[] = "happy";
str1[3] = 'q'; // 允许
// 方式二:字符指针指向字面量(通常不可修改)
char *str2 = "happy";
// str2[3] = 'q'; // 可能导致运行时错误
区别三:求数组长度
使用数组名配合 sizeof 可以求得数组总长度;使用指针则只能得到指针本身的大小。
#include <stdio.h>
int main(void)
{
int a[] = {0, 1, 2, 3, 4}, *p = a;
char len = 0;
// 方式一:借用数组名
printf("方式一:len=%d\n", sizeof(a)/sizeof(int)); // 输出 5
// 方式二:借用指针
printf("方式二:len=%d\n", sizeof(p)/sizeof(int)); // 输出 1 (或2,取决于平台)
return 0;
}
编译器不知道 p 指向的是一个数组,sizeof(p) 得到的是指针变量本身的大小。
指针函数与函数指针
两者的顺序不同,意义截然不同。
1、指针函数
本质是函数,其返回值是一个指针。
声明:int *pfun(int, int);
由于 () 优先级高于 *,因此 pfun 首先是一个函数,其返回值类型为 int *。
示例程序:
#include <stdio.h>
//指针函数声明
int *pfun(int *arr, int n);
int main(void)
{
int array[] = {0, 1, 2, 3, 4};
int len = sizeof(array)/sizeof(array[0]);
int *p;
int i;
//指针函数的调用
p = pfun(array, len);
for (i = 0; i < len; i++)
{
printf("array[%d] = %d\n", i, *(p+i));
}
return 0;
}
//指针函数定义
int *pfun(int *arr, int n)
{
int *p = arr;
return p;
}

2、函数指针
本质是一个指针变量,它指向一个函数。每个函数在编译后都有一个入口地址,函数指针就保存这个地址。
声明与使用:
/* 声明一个函数指针 */
int (*fptr) (int, int);
/* 函数指针指向函数func,两种写法等价 */
fptr = func; // 或 fptr = &func;
/* 通过函数指针调用函数,两种方式等价 */
res = (*fptr)(1, 2); // 或 res = fptr(1, 2);
函数名 func 和取地址 &func 都代表函数的入口地址。
示例程序:
#include <stdio.h>
int add(int a, int b);
int main(void)
{
int (*fptr)(int, int); //定义一个函数指针
int res;
fptr = add; //函数指针指向函数add
/* 通过函数指针调用函数 */
res = (*fptr)(1,2); //等价于res = fptr(1,2);
printf("a + b = %d\n", res);
return 0;
}
int add(int a, int b)
{
return a + b;
}

函数指针在嵌入式开发中应用广泛,主要用于实现回调函数和作为函数参数,以增加程序的灵活性和模块化。
希望通过这篇系统的梳理,能帮助你更好地理解C语言中指针这一核心概念。掌握这些基础知识,是进行更高级的内存管理和系统编程的关键。如果在学习过程中有更多疑问,欢迎到云栈社区与更多开发者交流探讨。