Skip to content
约 0 字 · 预计阅读 0 分钟

指针详解

什么是指针?

指针是 C 语言最强大也最危险的特征。指针本质上是一个存储内存地址的变量。通过指针,可以直接操作内存,实现高效的内存管理和数据操作。

指针的本质

内存模型:
┌─────────────────────────────────────────────────────────────┐
│                    内存地址空间                              │
│                                                             │
│  地址:  0x1000    0x1004    0x1008    0x100C    0x1010     │
│        ┌────────┬────────┬────────┬────────┬────────┐      │
│        │   10   │ 0x1000 │   20   │   30   │   40   │      │
│        └────────┴────────┴────────┴────────┴────────┘      │
│            ↑         ↑                                      │
│            │         │                                      │
│          变量 a    指针 p                                   │
│          (int)    (int*)                                   │
│                                                             │
│  p 存储了 a 的地址,通过 p 可以访问 a 的值                   │
└─────────────────────────────────────────────────────────────┘

上述图示展示了指针与变量的内存关系。

指针与内存可视化

指针声明语法:

c
int *p;        // 声明一个指向 int 的指针
char *str;     // 声明一个指向 char 的指针
void *ptr;     // 声明一个通用指针(可指向任何类型)

上述代码展示了指针的声明方式。

两个核心运算符:

运算符名称说明
&取地址运算符获取变量的内存地址
*解引用运算符访问指针指向的值

指针基本操作

c
#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 │                       │
│  └────────┘               └────────┘                       │
└─────────────────────────────────────────────────────────────┘

上述图示展示了指针操作的内存变化。

指针运算

指针加减运算

c
#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 字节

上述图示展示了指针运算的偏移规则。

指针与数组

c
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整个数组大小指针大小
取地址得到数组指针得到指针变量的地址
c
int arr[5];
int *p = arr;

sizeof(arr);    // 20(5 * 4)
sizeof(p);      // 8(64 位系统指针大小)

arr = p;        // 错误!数组名是常量
p = arr;        // 正确

上述代码展示了数组名与指针的区别。

指针与函数

指针作为函数参数

c
/*
 * 交换两个变量的值
 * 通过指针实现引用传递效果
 */
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 改变                           │  │          │
└─────────────────────────────────────────────────────────────┘

上述图示展示了值传递与指针传递的区别。

指针作为返回值

c
/*
 * 在字符串中查找字符
 * 返回指向该字符的指针
 */
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;
}

上述代码展示了指针作为返回值的用法。

注意事项:

危险操作说明
返回局部变量地址函数返回后,局部变量被销毁
返回未初始化指针导致未定义行为
c
// 错误示例:返回局部变量地址
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;  // 调用者负责释放
}

上述代码展示了返回指针的正确与错误方式。

函数指针

函数指针基础

c
/*
 * 函数指针声明语法:
 * 返回类型 (*指针名)(参数类型列表)
 */

// 声明函数指针
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                       │
└─────────────────────────────────────────────────────────────┘

上述图示展示了函数指针的内存模型。

回调函数

c
#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;
}

上述代码展示了回调函数的使用方式。

函数指针数组

c
#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;
}

上述代码展示了函数指针数组的用法。

多级指针

二级指针

c
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                │
└─────────────────────────────────────────────────────────────┘

上述图示展示了二级指针的内存模型。

二级指针的应用

c
/*
 * 修改指针本身的值
 * 需要传递指针的地址(二级指针)
 */
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 指针组合

c
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 指针

通用指针

c
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 指针应用

c
/*
 * 通用内存复制函数
 * 使用 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 指针在通用函数中的应用。

指针与字符串

字符串指针

c
// 字符串字面量
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 指向这里                                              │
└─────────────────────────────────────────────────────────────┘

上述图示展示了字符串指针与字符数组的内存布局差异。

常见指针错误

野指针

c
int *p;         // 未初始化,野指针
*p = 10;        // 危险!访问未知内存

// 正确做法
int *p = NULL;  // 初始化为 NULL
if (p != NULL) {
    *p = 10;
}

上述代码展示了野指针问题及解决方案。

悬空指针

c
int *p = (int*)malloc(sizeof(int));
*p = 10;
free(p);        // 释放内存

*p = 20;        // 危险!悬空指针

// 正确做法
free(p);
p = NULL;       // 释放后置空

上述代码展示了悬空指针问题及解决方案。

内存越界

c
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

相关主题

基于 VitePress 构建