强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

C/C++ Linux 开发教程(GCC + CMake) / 06 — 指针基础

指针基础

1. 指针概念

指针是 C 语言的核心特性,也是最让初学者困惑的部分。简单来说,指针就是一个存储内存地址的变量

变量 a          指针 p
┌──────┐       ┌──────┐
│  42  │ <──── │ 0x7f │  p 存储的是 a 的地址
└──────┘       └──────┘
地址: 0x7f00   地址: 0x7f08

1.1 取地址 & 与解引用 *

#include <stdio.h>

int main(void)
{
    int a = 42;
    int *p = &a;    // & 取地址:p 指向 a

    printf("a 的值:       %d\n", a);       // 42
    printf("a 的地址:     %p\n", (void *)&a);
    printf("p 的值:       %p\n", (void *)p);  // 与 &a 相同
    printf("*p 解引用:    %d\n", *p);      // 42(通过地址访问 a 的值)

    // 通过指针修改值
    *p = 100;
    printf("修改后 a:     %d\n", a);       // 100

    return 0;
}
操作符名称说明
&取地址获取变量的内存地址
*解引用通过地址访问(读/写)该内存处的值

2. 指针变量声明

int *p;         // 指向 int 的指针
char *s;        // 指向 char 的指针(常用于字符串)
double *dp;     // 指向 double 的指针
int **pp;       // 指向 int 指针的指针(二级指针)
void *vp;       // 通用指针(可指向任何类型)

💡 提示: int *pint* p 都合法。推荐 int *p,因为 int *a, b 中只有 a 是指针,bint

int *a, b;     // a 是 int*,b 是 int
int *c, *d;    // c 和 d 都是 int*

3. 指针运算

3.1 指针加减

#include <stdio.h>

int main(void)
{
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;  // 指向 arr[0]

    printf("p = %p, *p = %d\n", (void *)p, *p);       // arr[0] = 10

    p++;  // 移动到下一个 int(地址 + sizeof(int))
    printf("p = %p, *p = %d\n", (void *)p, *p);       // arr[1] = 20

    p += 2;
    printf("p = %p, *p = %d\n", (void *)p, *p);       // arr[3] = 40

    p--;
    printf("p = %p, *p = %d\n", (void *)p, *p);       // arr[2] = 30

    return 0;
}

3.2 指针比较

#include <stdio.h>

int main(void)
{
    int arr[5] = {10, 20, 30, 40, 50};
    int *start = &arr[0];
    int *end = &arr[4];

    if (start < end) {
        printf("start 在 end 之前\n");
    }

    // 计算两个指针之间的距离(元素个数)
    ptrdiff_t diff = end - start;
    printf("距离: %td 个元素\n", diff);  // 4

    // 用指针遍历数组
    for (int *p = arr; p <= &arr[4]; p++) {
        printf("%d ", *p);
    }
    printf("\n");

    return 0;
}

4. 指针与数组

数组名在大多数上下文中会退化为指向首元素的指针:

#include <stdio.h>

void print_array(const int *arr, int n)
{
    for (int i = 0; i < n; i++) {
        // arr[i] 等价于 *(arr + i)
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void)
{
    int arr[] = {1, 2, 3, 4, 5};
    int n = sizeof(arr) / sizeof(arr[0]);

    // 传数组给函数时,退化为指针
    print_array(arr, n);

    // 下标访问 vs 指针访问
    printf("arr[2]   = %d\n", arr[2]);
    printf("*(arr+2) = %d\n", *(arr + 2));

    // 指针常量 vs 常量指针
    // arr 本身是"指针常量",不能重新赋值
    // arr = NULL;  // 编译错误!

    return 0;
}
表达式等价写法含义
arr[i]*(arr + i)第 i 个元素的值
&arr[i]arr + i第 i 个元素的地址
arr&arr[0]首元素的地址

5. const 指针

#include <stdio.h>

int main(void)
{
    int a = 10, b = 20;

    // 指向 const int 的指针:不能通过指针修改值,但可以改变指向
    const int *p1 = &a;
    // *p1 = 100;  // 错误!不能通过 p1 修改值
    p1 = &b;       // OK,可以改变指向

    // const 指针,指向 int:不能改变指向,但可以修改值
    int *const p2 = &a;
    *p2 = 100;     // OK,可以修改值
    // p2 = &b;    // 错误!不能改变指向

    // const 指针,指向 const int:都不能改
    const int *const p3 = &a;
    // *p3 = 100;  // 错误
    // p3 = &b;    // 错误

    printf("*p1 = %d, *p2 = %d, *p3 = %d\n", *p1, *p2, *p3);
    return 0;
}

const 指针速记法

从右向左读

声明读法含义
const int *pp is a pointer to const int不能通过 p 修改值
int *const pp is a const pointer to int不能改变 p 的指向
const int *const pp is a const pointer to const int两者都不能改

💡 提示: 使用 const 指针可以在函数中承诺不修改传入的数据,既是文档也是保护。


6. void 指针

void * 是通用指针,可以指向任何类型的数据:

#include <stdio.h>

void print_bytes(const void *data, size_t size)
{
    const unsigned char *bytes = (const unsigned char *)data;
    for (size_t i = 0; i < size; i++) {
        printf("%02X ", bytes[i]);
    }
    printf("\n");
}

int main(void)
{
    int x = 0x12345678;
    double d = 3.14;

    printf("int x 的字节: ");
    print_bytes(&x, sizeof(x));

    printf("double d 的字节: ");
    print_bytes(&d, sizeof(d));

    // void* 可以存储任意类型指针
    void *vp = &x;
    printf("通过 void* 读取: %d\n", *(int *)vp);  // 需要先转换

    return 0;
}

⚠️ 注意: void * 不能直接解引用,必须先转换为具体类型指针。


7. NULL 指针

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *p = NULL;  // 空指针,不指向任何有效内存

    // 解引用 NULL 是未定义行为(通常导致段错误)
    // printf("%d\n", *p);  // 危险!

    // 使用前必须检查
    p = malloc(sizeof(int));
    if (p == NULL) {
        fprintf(stderr, "内存分配失败\n");
        return 1;
    }

    *p = 42;
    printf("*p = %d\n", *p);
    free(p);
    p = NULL;  // 释放后置空,防止悬空指针

    return 0;
}

💡 提示: 在 C 中,NULL 通常是 (void *)00。C23 引入了 nullptr,语义更明确。


8. 指针常见错误

8.1 野指针(Uninitialized Pointer)

int *p;       // 未初始化,指向随机地址
*p = 42;      // 未定义行为!可能写入任意内存

// 正确做法:声明时初始化
int *p = NULL;

8.2 悬空指针(Dangling Pointer)

int *p = malloc(sizeof(int));
*p = 42;
free(p);      // 内存已释放
// *p = 100;  // 未定义行为!p 成为悬空指针

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

8.3 内存泄漏

void leaky_function(void)
{
    int *p = malloc(100 * sizeof(int));
    // 忘记 free(p)!
    return;  // 内存泄漏!
}

错误总结表

错误类型原因后果预防
野指针未初始化写入随机内存声明时置 NULL
悬空指针使用已释放的内存读写无效内存free 后置 NULL
内存泄漏分配后未释放内存耗尽确保每个 malloc 都有对应 free
越界访问指针偏移超出范围数据损坏注意边界检查

9. 指针作为函数参数

#include <stdio.h>

// 值传递 —— 无法修改原值
void swap_wrong(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
}

// 地址传递 —— 可以修改原值
void swap_right(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 输出参数
int divide(int a, int b, int *remainder)
{
    if (b == 0) return -1;
    *remainder = a % b;
    return a / b;
}

int main(void)
{
    int x = 10, y = 20;

    swap_wrong(x, y);
    printf("swap_wrong 后: x=%d, y=%d\n", x, y);  // 未变

    swap_right(&x, &y);
    printf("swap_right 后: x=%d, y=%d\n", x, y);  // 已交换

    int rem;
    int quot = divide(17, 5, &rem);
    printf("17 / 5 = %d 余 %d\n", quot, rem);

    return 0;
}
传递方式函数签名能否修改原值
值传递void f(int x)
指针传递void f(int *p)
const 指针void f(const int *p)❌ 只读

10. 实际场景:动态数组

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} IntArray;

IntArray *int_array_create(size_t capacity)
{
    IntArray *arr = malloc(sizeof(IntArray));
    if (!arr) return NULL;
    arr->data = malloc(capacity * sizeof(int));
    if (!arr->data) { free(arr); return NULL; }
    arr->size = 0;
    arr->capacity = capacity;
    return arr;
}

int int_array_push(IntArray *arr, int value)
{
    if (arr->size >= arr->capacity) {
        size_t new_cap = arr->capacity * 2;
        int *new_data = realloc(arr->data, new_cap * sizeof(int));
        if (!new_data) return -1;
        arr->data = new_data;
        arr->capacity = new_cap;
    }
    arr->data[arr->size++] = value;
    return 0;
}

void int_array_destroy(IntArray *arr)
{
    if (arr) {
        free(arr->data);
        free(arr);
    }
}

int main(void)
{
    IntArray *arr = int_array_create(4);
    if (!arr) {
        fprintf(stderr, "创建失败\n");
        return 1;
    }

    for (int i = 0; i < 20; i++) {
        int_array_push(arr, i * i);
    }

    for (size_t i = 0; i < arr->size; i++) {
        printf("arr[%zu] = %d\n", i, arr->data[i]);
    }
    printf("容量: %zu, 大小: %zu\n", arr->capacity, arr->size);

    int_array_destroy(arr);
    return 0;
}

11. 实际场景:函数返回多个值

#include <stdio.h>

typedef struct {
    double min;
    double max;
    double avg;
} Stats;

Stats compute_stats(const double *data, size_t n)
{
    Stats s = {0};
    if (n == 0) return s;

    s.min = data[0];
    s.max = data[0];
    double sum = 0;

    for (size_t i = 0; i < n; i++) {
        if (data[i] < s.min) s.min = data[i];
        if (data[i] > s.max) s.max = data[i];
        sum += data[i];
    }
    s.avg = sum / (double)n;
    return s;
}

int main(void)
{
    double data[] = {3.5, 1.2, 7.8, 2.1, 5.6};
    size_t n = sizeof(data) / sizeof(data[0]);

    Stats s = compute_stats(data, n);
    printf("min=%.2f max=%.2f avg=%.2f\n", s.min, s.max, s.avg);

    return 0;
}

12. 扩展阅读