
指针常被称为C语言的灵魂,它既是最强大、最灵活的工具,也最容易引发错误,是区分C语言初学者与进阶者的关键。本指南旨在系统性地剖析指针的方方面面,涵盖核心概念、内存操作、数组关联、高级应用(如函数指针与回调)以及典型难点,力求帮助开发者透彻理解并熟练运用这一核心机制。
一、指针概念
1.1 什么是指针
通俗来讲,指针即指针变量,是用来存放内存地址的变量。可以这样理解:内存编号等于地址,地址就是指针。
你可以将指针想象成一把钥匙,能够打开内存中某个特定位置的“房间”。运用得当,它能高效操作数据;使用不当,则可能导致程序崩溃或难以排查的错误。
核心理解:
- 普通变量存储的是数据本身。
- 指针变量存储的是数据的地址(即数据的“门牌号”)。
1.2 指针的大小为何固定
指针变量的大小不取决于它指向的数据类型,而取决于操作系统位数(即地址总线的数量)。
- 32位系统:地址由32位二进制序列表示,需4字节存储,故指针大小为4字节。
- 64位系统:地址由64位二进制序列表示,需8字节存储,故指针大小为8字节。
重要结论:无论指针指向char、int还是double,其大小都是相同的,仅与系统位数相关。
#include <stdio.h>
int main()
{
printf("char*的大小:%zu字节\n", sizeof(char*));
printf("int*的大小:%zu字节\n", sizeof(int*));
printf("double*的大小:%zu字节\n", sizeof(double*));
// 32位系统输出4;64位系统输出8
return 0;
}

1.3 指针变量的定义与使用
通过&(取地址操作符)获取变量的内存起始地址,存入指针变量。
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a; // 定义整型指针pa,存储a的地址
printf("a的值:%d\n", a);
printf("a的地址:%p\n", &a);
printf("pa存储的地址:%p\n", pa); // 与&a相同
printf("通过pa访问的值:%d\n", *pa); // 解引用,输出10
*pa = 20; // 通过指针修改变量a的值
printf("修改后a的值:%d\n", a); // 输出20
return 0;
}
关键说明:
&a 取出的是变量a所占内存空间的起始地址。
pa 是指针变量,专门用于存放地址。
*pa 是解引用操作,通过地址访问或修改目标变量的值。
二、指针类型
指针拥有类型,如int*、char*、float*等,这决定了它能指向何种类型的变量。
int a = 10;
int* pa = &a; // 正确,类型匹配
float f = 3.14f;
float* pf = &f; // 正确,类型匹配
// int* p = &f; // 错误!类型不匹配,将导致未定义行为
2.1 区分指针类型的目的
指针类型主要有两大作用:
- 决定解引用时的访问范围:不同类型指针解引用时,操作的内存字节数不同。
- 决定指针算术运算的步长:指针进行
+1等操作时,实际移动的字节数由类型决定。
2.2 指针运算的步长
指针类型决定了其算术运算(如+1)的步长(即一次移动的字节数)。
#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int* pi = arr;
char* pc = (char*)arr;
printf("pi地址:%p\n", pi);
printf("pi+1地址:%p\n", pi+1); // 增加4字节(一个int大小)
printf("pc地址:%p\n", pc);
printf("pc+1地址:%p\n", pc+1); // 增加1字节(一个char大小)
return 0;
}
核心结论:指针类型决定了指针向前或向后移动一步所跨越的实际距离(字节数)。
2.3 指针类型的作用总结
- 解引用访问范围:
int*访问4字节,char*访问1字节。
- 运算步长:
int*指针+1移动4字节,double*指针+1移动8字节。
重要警告:切勿混用不同类型的指针(如用int*指向float数据),因为整型和浮点数在内存中的存储格式完全不同,解引用将得到无意义的数据。
三、野指针
野指针是指向位置未知(随机、不正确或未限定)的指针。使用野指针是危险行为,可能导致程序崩溃或数据损坏。
3.1 野指针的成因
-
指针未初始化
int* p; // 未初始化,值是随机的
*p = 10; // 危险!向未知地址写入数据
正确做法:int* p = NULL;
-
指针越界访问
int arr[5] = {0};
int* p = arr;
for(int i=0; i<=5; i++) // i<=5 导致越界
printf("%d", p[i]);
-
指针指向已释放的空间
int* test() {
int a = 10;
return &a; // 错误!返回局部变量地址,函数返回后a被销毁
}
int main() {
int* p = test();
*p = 20; // 危险!访问已释放内存
return 0;
}

3.2 如何规避野指针
- 初始化指针:定义时即初始化为
NULL。
- 小心指针越界:确保访问在有效内存范围内。
- 释放后置空:动态内存释放后,立即将指针置为
NULL。
- 避免返回局部变量地址:如需返回,可使用
static或动态内存分配。
- 使用前检查有效性:对可能为
NULL的指针进行判断。
四、指针运算
指针支持有限的算术运算,包括与整数加减、指针间相减及关系比较,这些在数组操作中极为有用。
4.1 指针 ± 整数
指针加上或减去一个整数,结果指向新的内存位置。移动字节数 = 整数 × 指针指向类型的大小。
#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
for(int i = 0; i < 5; i++)
{
printf("%d ", *(p + i)); // 等价于 p[i] 或 arr[i]
}
return 0;
}

4.2 指针 - 指针
两个指针相减,得到的是它们之间元素的个数(非字节数)。前提是它们必须指向同一块连续的内存空间(如同一个数组)。
#include <stdio.h>
#include <stddef.h>
// 模拟实现strlen
size_t my_strlen(const char* s)
{
const char* p = s;
while (*p != '\0')
p++;
return p - s; // 指针相减得到字符个数
}
int main()
{
char str[] = "hello";
printf("长度:%zu\n", my_strlen(str)); // 输出5
return 0;
}

4.3 指针的关系运算
指针可进行比较(<, <=, >, >=, ==, !=)。C标准规定,允许指向数组元素的指针与指向数组“最后一个元素之后”那个位置的指针比较,但不允许与“第一个元素之前”的位置比较。
五、指针和数组
数组名在多数情况下表示数组首元素的地址。
int arr[10];
printf("%p\n", arr); // 首元素地址
printf("%p\n", &arr[0]); // 与上行相同
两个例外(数组名代表整个数组):
sizeof(arr):计算整个数组的字节大小。
&arr:取出的是整个数组的地址(类型为数组指针)。
5.1 使用指针访问数组
int arr[] = {1,2,3,4,5};
int* p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
// 方式1:指针算术运算
for(int i=0; i<sz; i++)
printf("%d ", *(p + i));
// 方式2:移动指针本身
for(int i=0; i<sz; i++)
printf("%d ", *p++);
// 方式3:数组下标(本质是指针运算的语法糖)
for(int i=0; i<sz; i++)
printf("%d ", arr[i]);

关键理解:arr[i] 等价于 *(arr + i)。
六、二级指针
指针变量也是变量,其地址可以存放在另一个指针变量中,后者即为二级指针。
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a; // 一级指针,存a的地址
int** ppa = &pa; // 二级指针,存pa的地址
printf("a = %d\n", a); // 10
printf("*pa = %d\n", *pa); // 10
printf("**ppa = %d\n", **ppa);// 10
**ppa = 20; // 通过二级指针修改a的值
printf("修改后 a = %d\n", a); // 20
return 0;
}

解引用过程:*ppa 得到 pa,**ppa 即 *(*ppa) 得到 a。
七、指针数组
指针数组是数组,其元素为指针。
int* arr[5]; // arr是数组,包含5个元素,每个元素是int*类型
记忆技巧:看变量名与哪个运算符先结合。int* arr[5]中[]优先级高,arr是数组,故为指针数组。
示例:用指针数组模拟二维数组(注意:各行数据在内存中可能不连续)。
#include <stdio.h>
int main()
{
int data1[] = {1, 2, 3};
int data2[] = {4, 5, 6};
int* arr[2] = {data1, data2}; // 指针数组
for(int i=0; i<2; i++)
for(int j=0; j<3; j++)
printf("%d ", arr[i][j]); // 访问方式与二维数组类似
return 0;
}

八、字符指针
字符指针(char*)可指向单个字符,更常见的是指向字符串。
char ch = 'w';
char* pc = &ch; // 指向单个字符
*pc = 'a'; // 修改ch的值
const char* pstr = "hello"; // 指向常量字符串(只读)
printf("%s\n", pstr); // 输出 hello
// *pstr = 'H'; // 错误!常量字符串不可修改
8.1 常见误区
常量字符串在内存中仅有一份副本。当多个指针指向相同的字符串字面量时,它们实际指向同一块内存。
const char* p1 = "abcdef";
const char* p2 = "abcdef";
char arr1[] = "abcdef";
char arr2[] = "abcdef";
if(p1 == p2) printf("p1 == p2\n"); // 输出此句,地址相同
if(arr1 == arr2) printf("arr1 == arr2\n"); // 不输出,数组地址不同
原因:p1和p2指向常量区同一份"abcdef";arr1和arr2是各自在栈上独立分配的数组。

九、数组指针
数组指针是指针,指向一个数组。
int (*p)[10]; // p是指针,指向一个包含10个int元素的数组
记忆技巧:int (*p)[10]中()优先级高,p先与*结合,故为指针。
9.1 &数组名 vs 数组名
&数组名取出的是整个数组的地址,其类型是数组指针。虽然其值与首元素地址相同,但进行+1运算时,跳过的距离是整个数组的大小。
int arr[10];
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); // 增加40字节(整个数组)
9.2 数组指针的使用
数组指针主要用于二维数组的操作,因为二维数组可视为“一维数组的数组”。
#include <stdio.h>
void print(int (*p)[5], int row, int col) {
for(int i=0; i<row; i++) {
for(int j=0; j<col; j++) {
printf("%d ", *(*(p+i)+j)); // 等价于 p[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}};
print(arr, 3, 5); // arr是首元素地址,即第一行的地址
return 0;
}

理解:p指向二维数组的第一行(一个一维数组)。p+i指向第i行,*(p+i)得到第i行的数组名(即该行首元素地址),*(p+i)+j得到第i行第j列元素的地址。
十、数组与指针参数
函数传参时,数组和指针关系密切。理解数组在函数参数中的传递机制,对于掌握网络/系统编程中涉及缓冲区的操作至关重要。
10.1 一维数组传参
形参可写作数组形式或指针形式,两者等价。
void test1(int arr[]) { /* arr实际是指针 */ }
void test2(int arr[10]) { /* 大小10被忽略 */ }
void test3(int* p) { /* 推荐写法 */ }
int main() {
int arr[10] = {0};
test1(arr); // 传递数组名(首元素地址)
test2(arr);
test3(arr);
return 0;
}
10.2 二维数组传参
形参必须指明列数(编译器需知一行有多少元素以计算地址)。
void test1(int arr[][5]) { /* 行可省略 */ }
void test2(int arr[3][5]) { /* 行数可写,但被忽略 */ }
void test3(int (*p)[5]) { /* 数组指针形式,推荐 */ }
// void test(int arr[][]) { // 错误!列不能省略 }
10.3 一、二级指针传参
- 一级指针传参:形参用一级指针接收。
- 二级指针传参:实参可以是二级指针变量、一级指针的地址或指针数组名。
十一、函数指针
函数也有地址,指向函数的指针称为函数指针。
#include <stdio.h>
int Add(int x, int y) { return x+y; }
int main() {
int (*pf)(int, int) = Add; // 定义函数指针并初始化
int ret1 = (*pf)(2, 3); // 通过指针调用,推荐
int ret2 = pf(2, 3); // 简写方式,也可行
printf("%d, %d\n", ret1, ret2); // 输出 5, 5
return 0;
}

复杂声明解析:
(*(void (*)())0)(); – 将0强制转换为无参无返回值的函数指针,并调用0地址处的函数。
void (*signal(int, void(*)(int)))(int); – signal是一个函数,其参数为int和一个函数指针,返回值也是一个函数指针。可使用typedef简化。
十二、函数指针数组
函数指针数组是存放函数指针的数组,可用于实现“转移表”,例如简易计算器。
int (*pFuncArr[5])(int, int) = {NULL, Add, Sub, Mul, Div};
// 根据用户选择input调用
ret = (*pFuncArr[input])(x, y);
十三、回调函数
回调函数是通过函数指针调用的函数。将函数指针作为参数传递给另一函数,在后者内部通过该指针调用函数,即为回调。这是实现灵活程序设计的常用技巧,在算法/数据结构(如排序比较函数)中应用广泛。
13.1 使用qsort排序
C标准库qsort函数利用回调函数实现对任意类型数据的排序。
#include <stdio.h>
#include <stdlib.h>
int cmp_int(const void* e1, const void* e2) {
return *(int*)e1 - *(int*)e2; // 升序
}
int main() {
int arr[] = {9,8,7,6,5,4,3,2,1,0};
int sz = sizeof(arr)/sizeof(arr[0]);
qsort(arr, sz, sizeof(int), cmp_int); // cmp_int是回调函数
// 排序后arr为{0,1,2,3,4,5,6,7,8,9}
return 0;
}

对结构体数组排序:
struct Stu { char name[20]; int age; };
int cmp_by_age(const void* e1, const void* e2) {
return ((struct Stu*)e1)->age - ((struct Stu*)e2)->age;
}
int cmp_by_name(const void* e1, const void* e2) {
return strcmp(((struct Stu*)e1)->name, ((struct Stu*)e2)->name);
}
// 使用qsort(arr, sz, sizeof(struct Stu), cmp_by_age);
十四、难点解析与综合练习
本节通过典型题目加深对指针复杂用法的理解。
14.1 sizeof 与 strlen 辨析
sizeof 是操作符,计算对象或类型所占内存字节数,编译时求值。
strlen 是库函数,计算字符串长度(\0前的字符数),运行时求值。
char arr[] = "abcdef"; // sizeof(arr)=7 (包含\0), strlen(arr)=6
char arr2[] = {'a','b','c'}; // sizeof(arr2)=3, strlen(arr2)=随机值(无\0)
char *p = "abcdef"; // sizeof(p)=4/8 (指针大小), strlen(p)=6
14.2 典型题目分析
题目1:
int a[5] = {1,2,3,4,5};
int *ptr = (int*)(&a + 1);
printf("%d,%d\n", *(a+1), *(ptr-1)); // 输出:2,5
分析:&a是整个数组的地址,&a+1跳过整个数组。ptr-1向前移动一个int,指向元素5。
题目2(多级指针):
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char **cp[] = {c+3, c+2, c+1, c};
char ***cpp = cp;
printf("%s\n", **++cpp); // 输出:POINT
分析:cpp指向cp[0](值为c+3)。++cpp使其指向cp[1](值为c+2)。*cpp得c+2,**cpp得c[2]即"POINT"首地址。
总结
指针是C语言的核心与灵魂,掌握它意味着获得了直接操作内存的能力,能编写出高效灵活的代码。核心要点包括:
- 理解本质:指针是存储地址的变量,类型决定了解引用视角和运算步长。
- 警惕危险:始终防范野指针,遵循初始化、检查、释放后置空等安全准则。
- 厘清关系:深刻理解指针与数组、字符串、函数之间的紧密联系。
- 善用工具:灵活运用函数指针、回调机制(如
qsort)等高级特性。
学习指针没有捷径,需要结合大量的图示、代码实践和深度思考。从理解基本的内存模型开始,逐步挑战复杂的多级指针和类型转换,你终将驯服这一强大的工具,为深入系统编程、数据库/中间件乃至云原生等领域打下坚实基础。