指针详解
什么是指针?
指针是 C 语言最强大也最危险的特征。指针本质上是一个存储内存地址的变量。通过指针,可以直接操作内存,实现高效的内存管理和数据操作。
指针的本质
内存模型:
┌─────────────────────────────────────────────────────────────┐
│ 内存地址空间 │
│ │
│ 地址: 0x1000 0x1004 0x1008 0x100C 0x1010 │
│ ┌────────┬────────┬────────┬────────┬────────┐ │
│ │ 10 │ 0x1000 │ 20 │ 30 │ 40 │ │
│ └────────┴────────┴────────┴────────┴────────┘ │
│ ↑ ↑ │
│ │ │ │
│ 变量 a 指针 p │
│ (int) (int*) │
│ │
│ p 存储了 a 的地址,通过 p 可以访问 a 的值 │
└─────────────────────────────────────────────────────────────┘上述图示展示了指针与变量的内存关系。
指针声明语法:
int *p; // 声明一个指向 int 的指针
char *str; // 声明一个指向 char 的指针
void *ptr; // 声明一个通用指针(可指向任何类型)上述代码展示了指针的声明方式。
两个核心运算符:
| 运算符 | 名称 | 说明 |
|---|---|---|
& | 取地址运算符 | 获取变量的内存地址 |
* | 解引用运算符 | 访问指针指向的值 |
指针基本操作
#include <stdio.h>
int main(void)
{
int a = 10; // 普通变量
int *p; // 指针变量
p = &a; // p 指向 a(p 存储 a 的地址)
printf("a 的值: %d\n", a); // 10
printf("a 的地址: %p\n", &a); // 0x7fff...
printf("p 的值: %p\n", p); // 0x7fff...(与 &a 相同)
printf("p 指向的值: %d\n", *p); // 10(通过指针访问 a)
printf("p 的地址: %p\n", &p); // 不同的地址
// 通过指针修改值
*p = 20;
printf("修改后 a 的值: %d\n", a); // 20
return 0;
}上述代码展示了指针的基本操作。
代码执行流程图解:
初始状态:
┌─────────────────────────────────────────────────────────────┐
│ 变量 a (int) 变量 p (int*) │
│ 地址: 0x1000 地址: 0x2000 │
│ ┌────────┐ ┌────────┐ │
│ │ 10 │ │ ??? │ │
│ └────────┘ └────────┘ │
└─────────────────────────────────────────────────────────────┘
执行 p = &a 后:
┌─────────────────────────────────────────────────────────────┐
│ 变量 a (int) 变量 p (int*) │
│ 地址: 0x1000 地址: 0x2000 │
│ ┌────────┐ ┌────────┐ │
│ │ 10 │◄──────────────│ 0x1000 │ │
│ └────────┘ └────────┘ │
└─────────────────────────────────────────────────────────────┘
执行 *p = 20 后:
┌─────────────────────────────────────────────────────────────┐
│ 变量 a (int) 变量 p (int*) │
│ 地址: 0x1000 地址: 0x2000 │
│ ┌────────┐ ┌────────┐ │
│ │ 20 │◄──────────────│ 0x1000 │ │
│ └────────┘ └────────┘ │
└─────────────────────────────────────────────────────────────┘上述图示展示了指针操作的内存变化。
指针运算
指针加减运算
#include <stdio.h>
int main(void)
{
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p 指向数组首元素
printf("p = %p, *p = %d\n", p, *p); // 0x1000, 10
p++; // p 向后移动一个 int 的大小(4 字节)
printf("p = %p, *p = %d\n", p, *p); // 0x1004, 20
p += 2; // p 向后移动两个 int 的大小(8 字节)
printf("p = %p, *p = %d\n", p, *p); // 0x100C, 40
p--; // p 向前移动一个 int 的大小(4 字节)
printf("p = %p, *p = %d\n", p, *p); // 0x1008, 30
return 0;
}上述代码展示了指针的加减运算。
指针运算规则:
指针加减运算:
p + n 实际偏移 = n * sizeof(指针指向的类型)
int *p;
p + 1 → 偏移 4 字节(假设 int 为 4 字节)
p + 2 → 偏移 8 字节
char *p;
p + 1 → 偏移 1 字节
p + 2 → 偏移 2 字节
double *p;
p + 1 → 偏移 8 字节
p + 2 → 偏移 16 字节上述图示展示了指针运算的偏移规则。
指针与数组
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
// 以下四种方式等价
arr[i] // 数组下标访问
*(arr + i) // 数组名 + 偏移
p[i] // 指针下标访问
*(p + i) // 指针 + 偏移
// 遍历数组
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i));
}
// 或者使用指针遍历
for (int *ptr = arr; ptr < arr + 5; ptr++) {
printf("%d ", *ptr);
}上述代码展示了指针与数组的关系。
数组名与指针的区别:
| 特性 | 数组名 | 指针 |
|---|---|---|
| 本质 | 常量指针 | 变量 |
| 可修改 | 不可修改 | 可修改 |
| sizeof | 整个数组大小 | 指针大小 |
| 取地址 | 得到数组指针 | 得到指针变量的地址 |
int arr[5];
int *p = arr;
sizeof(arr); // 20(5 * 4)
sizeof(p); // 8(64 位系统指针大小)
arr = p; // 错误!数组名是常量
p = arr; // 正确上述代码展示了数组名与指针的区别。
指针与函数
指针作为函数参数
/*
* 交换两个变量的值
* 通过指针实现引用传递效果
*/
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
int main(void)
{
int x = 10, y = 20;
printf("交换前: x = %d, y = %d\n", x, y); // 10, 20
swap(&x, &y); // 传递地址
printf("交换后: x = %d, y = %d\n", x, y); // 20, 10
return 0;
}上述代码展示了指针作为函数参数实现引用传递。
值传递 vs 指针传递:
值传递:
┌─────────────────────────────────────────────────────────────┐
│ main() swap() │
│ ┌────────┐ ┌────────┐ │
│ │ x = 10 │──复制──► │ a = 10 │ │
│ │ y = 20 │──复制──► │ b = 20 │ │
│ └────────┘ └────────┘ │
│ 交换后 x, y 不变 │
└─────────────────────────────────────────────────────────────┘
指针传递:
┌─────────────────────────────────────────────────────────────┐
│ main() swap() │
│ ┌────────┐ ┌────────┐ │
│ │ x = 10 │◄──────┐ │ a = &x │ │
│ │ y = 20 │◄──┐ │ │ b = &y │ │
│ └────────┘ │ │ └────────┘ │
│ │ └────────通过指针修改─────────┐ │
│ └────────通过指针修改─────────┐ │ │
│ 交换后 x, y 改变 │ │ │
└─────────────────────────────────────────────────────────────┘上述图示展示了值传递与指针传递的区别。
指针作为返回值
/*
* 在字符串中查找字符
* 返回指向该字符的指针
*/
char* find_char(char *str, char c)
{
while (*str != '\0') {
if (*str == c) {
return str; // 返回找到的位置
}
str++;
}
return NULL; // 未找到
}
int main(void)
{
char *s = "Hello, World!";
char *p = find_char(s, 'W');
if (p != NULL) {
printf("找到: %s\n", p); // World!
}
return 0;
}上述代码展示了指针作为返回值的用法。
注意事项:
| 危险操作 | 说明 |
|---|---|
| 返回局部变量地址 | 函数返回后,局部变量被销毁 |
| 返回未初始化指针 | 导致未定义行为 |
// 错误示例:返回局部变量地址
int* bad_function(void)
{
int local = 10;
return &local; // 危险!local 在函数返回后被销毁
}
// 正确做法:返回静态变量或动态分配的内存
int* good_function(void)
{
static int local = 10; // 静态变量,生命周期为程序全程
return &local;
}
int* another_good_function(void)
{
int *p = malloc(sizeof(int));
*p = 10;
return p; // 调用者负责释放
}上述代码展示了返回指针的正确与错误方式。
函数指针
函数指针基础
/*
* 函数指针声明语法:
* 返回类型 (*指针名)(参数类型列表)
*/
// 声明函数指针
int (*pfunc)(int, int);
// 指向具体函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
pfunc = add; // pfunc 指向 add 函数
int result = pfunc(10, 5); // 调用 add(10, 5),结果为 15
pfunc = sub; // pfunc 指向 sub 函数
result = pfunc(10, 5); // 调用 sub(10, 5),结果为 5上述代码展示了函数指针的基本用法。
函数指针内存模型:
代码段:
┌─────────────────────────────────────────────────────────────┐
│ add 函数代码 sub 函数代码 │
│ 地址: 0x0040 地址: 0x0080 │
│ ┌──────────┐ ┌──────────┐ │
│ │ add 指令 │ │ sub 指令 │ │
│ │ ... │ │ ... │ │
│ └──────────┘ └──────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌────┴────┐ ┌────┴────┐ │
│ │ pfunc │ │ pfunc │ │
│ │ = 0x0040│ │ = 0x0080│ │
│ └─────────┘ └─────────┘ │
│ pfunc 指向 add pfunc 指向 sub │
└─────────────────────────────────────────────────────────────┘上述图示展示了函数指针的内存模型。
回调函数
#include <stdlib.h>
/*
* 回调函数示例:通用排序函数
*
* 参数说明:
* @base: 数组起始地址
* @nmemb: 元素个数
* @size: 每个元素大小
* @compar: 比较函数指针
*/
void my_qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
// 比较函数:升序
int compare_asc(const void *a, const void *b)
{
return *(int*)a - *(int*)b;
}
// 比较函数:降序
int compare_desc(const void *a, const void *b)
{
return *(int*)b - *(int*)a;
}
int main(void)
{
int arr[] = {5, 2, 8, 1, 9};
int n = sizeof(arr) / sizeof(arr[0]);
// 升序排序
qsort(arr, n, sizeof(int), compare_asc);
// arr: {1, 2, 5, 8, 9}
// 降序排序
qsort(arr, n, sizeof(int), compare_desc);
// arr: {9, 8, 5, 2, 1}
return 0;
}上述代码展示了回调函数的使用方式。
函数指针数组
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return b != 0 ? a / b : 0; }
int main(void)
{
// 函数指针数组
int (*operations[])(int, int) = {add, sub, mul, div};
char *op_names[] = {"+", "-", "*", "/"};
int a = 10, b = 5;
for (int i = 0; i < 4; i++) {
printf("%d %s %d = %d\n",
a, op_names[i], b, operations[i](a, b));
}
// 输出:
// 10 + 5 = 15
// 10 - 5 = 5
// 10 * 5 = 50
// 10 / 5 = 2
return 0;
}上述代码展示了函数指针数组的用法。
多级指针
二级指针
int a = 10;
int *p = &a; // 一级指针,指向 a
int **pp = &p; // 二级指针,指向 p
printf("a = %d\n", a); // 10
printf("*p = %d\n", *p); // 10
printf("**pp = %d\n", **pp); // 10
// 通过二级指针修改一级指针指向
int b = 20;
*pp = &b; // p 现在指向 b
printf("*p = %d\n", *p); // 20上述代码展示了二级指针的用法。
二级指针内存模型:
┌─────────────────────────────────────────────────────────────┐
│ 变量 a (int) 变量 p (int*) 变量 pp (int**) │
│ 地址: 0x1000 地址: 0x2000 地址: 0x3000 │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ 10 │◄──────│ 0x1000 │◄──────│ 0x2000 │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
│ 访问方式: │
│ a → 直接访问变量 a │
│ *p → 通过 p 访问 a │
│ **pp → 通过 pp 访问 p,再通过 p 访问 a │
└─────────────────────────────────────────────────────────────┘上述图示展示了二级指针的内存模型。
二级指针的应用
/*
* 修改指针本身的值
* 需要传递指针的地址(二级指针)
*/
void allocate_memory(int **ptr, int size)
{
*ptr = (int*)malloc(size * sizeof(int));
if (*ptr != NULL) {
for (int i = 0; i < size; i++) {
(*ptr)[i] = i;
}
}
}
int main(void)
{
int *arr = NULL;
allocate_memory(&arr, 10); // 传递指针的地址
if (arr != NULL) {
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
free(arr);
}
return 0;
}上述代码展示了二级指针在动态内存分配中的应用。
const 与指针
const 指针组合
int a = 10;
int b = 20;
// 1. 指向常量的指针(指针可变,指向的值不可变)
const int *p1 = &a;
// *p1 = 20; // 错误!不能通过 p1 修改值
p1 = &b; // 正确!可以改变 p1 指向
// 2. 常量指针(指针不可变,指向的值可变)
int * const p2 = &a;
*p2 = 30; // 正确!可以修改值
// p2 = &b; // 错误!不能改变 p2 指向
// 3. 指向常量的常量指针(都不可变)
const int * const p3 = &a;
// *p3 = 40; // 错误!
// p3 = &b; // 错误!上述代码展示了 const 与指针的三种组合方式。
记忆技巧:
从右往左读:
const int *p; → p is a pointer to const int
→ p 是指向常量 int 的指针
→ 值不可变,指针可变
int * const p; → p is a const pointer to int
→ p 是指向 int 的常量指针
→ 指针不可变,值可变
const int * const p; → p is a const pointer to const int
→ p 是指向常量 int 的常量指针
→ 都不可变上述技巧帮助记忆 const 与指针的组合。
void 指针
通用指针
void *ptr; // 可以指向任何类型
int a = 10;
float b = 3.14f;
char c = 'A';
ptr = &a; // 指向 int
ptr = &b; // 指向 float
ptr = &c; // 指向 char
// 使用前必须强制转换
printf("%d\n", *(int*)ptr); // 需要转换为具体类型上述代码展示了 void 指针的用法。
void 指针应用
/*
* 通用内存复制函数
* 使用 void 指针接受任意类型参数
*/
void my_memcpy(void *dest, const void *src, size_t n)
{
char *d = (char*)dest;
const char *s = (const char*)src;
while (n--) {
*d++ = *s++;
}
}
int main(void)
{
int arr1[] = {1, 2, 3, 4, 5};
int arr2[5];
my_memcpy(arr2, arr1, sizeof(arr1));
return 0;
}上述代码展示了 void 指针在通用函数中的应用。
指针与字符串
字符串指针
// 字符串字面量
char *str1 = "Hello"; // 指向常量区,不可修改
// 字符数组
char str2[] = "Hello"; // 在栈上,可修改
str1[0] = 'h'; // 错误!可能崩溃
str2[0] = 'h'; // 正确!上述代码展示了字符串指针与字符数组的区别。
内存布局:
str1 = "Hello":
┌─────────────────────────────────────────────────────────────┐
│ 常量区(只读) │
│ ┌───┬───┬───┬───┬───┬───┐ │
│ │ H │ e │ l │ l │ o │\0 │ │
│ └───┴───┴───┴───┴───┴───┘ │
│ ▲ │
│ │ │
│ ┌────┴────┐ │
│ │ str1 │ 栈区 │
│ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
str2[] = "Hello":
┌─────────────────────────────────────────────────────────────┐
│ 栈区 │
│ ┌───┬───┬───┬───┬───┬───┐ │
│ │ H │ e │ l │ l │ o │\0 │ │
│ └───┴───┴───┴───┴───┴───┘ │
│ str2 指向这里 │
└─────────────────────────────────────────────────────────────┘上述图示展示了字符串指针与字符数组的内存布局差异。
常见指针错误
野指针
int *p; // 未初始化,野指针
*p = 10; // 危险!访问未知内存
// 正确做法
int *p = NULL; // 初始化为 NULL
if (p != NULL) {
*p = 10;
}上述代码展示了野指针问题及解决方案。
悬空指针
int *p = (int*)malloc(sizeof(int));
*p = 10;
free(p); // 释放内存
*p = 20; // 危险!悬空指针
// 正确做法
free(p);
p = NULL; // 释放后置空上述代码展示了悬空指针问题及解决方案。
内存越界
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
p[5] = 10; // 越界访问!
*(p + 10) = 20; // 越界访问!
// 正确做法:检查边界
for (int i = 0; i < 5; i++) {
p[i] = i;
}上述代码展示了内存越界问题。
总结
| 概念 | 说明 |
|---|---|
| 指针 | 存储内存地址的变量 |
& 和 * | 取地址和解引用运算符 |
| 指针运算 | 加减运算按类型大小偏移 |
| 函数指针 | 指向函数的指针,实现回调 |
| 多级指针 | 指向指针的指针 |
| const 指针 | 限制指针或指向值的修改 |
| void 指针 | 通用指针,使用前需转换 |
参考资料
[1] C Programming Language. Brian W. Kernighan, Dennis M. Ritchie
[2] Pointers on C. Kenneth Reek
[3] Expert C Programming. Peter van der Linden