1. 指针是什么

指针是一个变量,它存储的是内存地址。

1
2
int a = 10;
int *p = &a; // p 保存 a 的地址
  • &a:取变量 a 的地址。
  • int *p:声明一个指向 int 的指针。
  • *p:解引用,访问 p 所指向的内存,也就是 a。
  • p:本身是一个变量,存储地址。

内存示意:

1
2
变量 a: 地址 0x1000,值 10
指针 p: 地址 0x2000,值 0x1000

所以:

1
2
3
printf("%d\n", *p); // 10
*p = 20;
printf("%d\n", a); // 20

指针类型很重要:int *、char *、double * 不只是“地址”,还决定了:

  1. 解引用时读取多少字节;
  2. 指针加减时移动多少字节。

例如:

1
2
3
char *cp;    // cp + 1 通常移动 1 字节
int *ip; // ip + 1 通常移动 4 字节
double *dp; // dp + 1 通常移动 8 字节

指针大小通常与平台有关:32 位系统常见 4 字节,64 位系统常见 8 字节。但函数指针大小可能不同。


2. 基本语法

1
2
3
4
5
6
7
8
9
10
int a = 10;
int *p = &a;

printf("a = %d\n", a);
printf("&a = %p\n", (void *)&a);
printf("p = %p\n", (void *)p);
printf("*p = %d\n", *p);

*p = 100;
printf("a = %d\n", a); // 100

注意:

  • %p 用来打印地址,参数最好转换为 void *。
  • 未初始化的指针是野指针,不能解引用。
1
2
int *p;   // 危险,p 指向未知位置
*p = 10; // 未定义行为

安全写法:

1
int *p = NULL;

空指针不能解引用:

1
2
3
if (p != NULL) {
*p = 10;
}

3. 指针运算

指针可以进行有限算术运算:

1
2
3
4
5
6
7
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // 指向 arr[0]

printf("%d\n", *p); // 10
printf("%d\n", *(p+1)); // 20
p++; // 指向 arr[1]
printf("%d\n", *p); // 20

指针加减整数时,移动的是 sizeof(*p) 个字节。

两个指针可以相减,前提是它们指向同一个数组或同一块连续内存:

1
2
3
int *p1 = &arr[1];
int *p2 = &arr[4];
ptrdiff_t n = p2 - p1; // 3

指针也可以比较,但一般只在同一数组内比较有意义。

不能把两个指针相加:

1
int *p3 = p1 + p2; // 错误

4. 指针与数组

数组名在大多数表达式中会退化为指向首元素的指针。

1
2
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 等价于 &arr[0]

以下写法等价:

1
2
3
4
arr[i]
*(arr + i)
*(p + i)
p[i]

遍历数组:

1
2
3
4
5
6
7
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
int n = sizeof(arr) / sizeof(arr[0]);

for (int i = 0; i < n; i++) {
printf("%d ", *(p + i));
}

也可以用指针自身移动:

1
2
3
for (int *p = arr; p < arr + n; p++) {
printf("%d ", *p);
}

注意:数组名和指针并不完全等价:

1
2
3
4
5
int arr[5];
int *p = arr;

printf("%zu\n", sizeof(arr)); // 整个数组大小,通常是 20
printf("%zu\n", sizeof(p)); // 指针大小,通常是 8

&arr 的类型是 int (*)[5],即指向整个数组的指针:

1
2
int (*pa)[5] = &arr;
printf("%zu\n", sizeof(*pa)); // 20

5. 指针与字符串

字符串字面量本质是以 '\0' 结尾的字符数组。

1
const char *s = "hello";

建议用 const char *,因为字符串字面量通常不可修改,修改它是未定义行为。

如果要修改,用字符数组:

1
2
3
char buf[] = "hello";
buf[0] = 'H';
printf("%s\n", buf); // Hello

常见字符串遍历:

1
2
3
4
5
6
7
size_t my_strlen(const char *s) {
const char *p = s;
while (*p != '\0') {
p++;
}
return (size_t)(p - s);
}

6. 指针作为函数参数

C 语言是值传递。如果函数要修改外部变量,需要传指针。

1
2
3
4
5
6
7
8
9
10
11
void swap(int *x, int *y) {
int t = *x;
*x = *y;
*y = t;
}

int main(void) {
int a = 1, b = 2;
swap(&a, &b);
printf("%d %d\n", a, b); // 2 1
}

数组作为函数参数时,会退化为指针:

1
2
3
4
5
void print_array(int *arr, int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
}

下面两种写法等价:

1
2
void f(int arr[]);
void f(int *arr);

函数内 sizeof(arr) 得到的是指针大小,不是数组大小。

函数也可以通过指针返回多个结果:

1
2
3
4
void divmod(int a, int b, int *q, int *r) {
*q = a / b;
*r = a % b;
}

7. 返回指针

函数可以返回指针,但不能返回局部变量的地址。

错误示例:

1
2
3
4
int *bad(void) {
int x = 10;
return &x; // 错误:x 在函数结束后销毁
}

正确方式:

  1. 返回静态变量地址;
  2. 返回全局变量地址;
  3. 返回动态分配内存;
  4. 返回调用者传入的指针。
1
2
3
4
int *good(void) {
static int x = 10;
return &x;
}

动态分配:

1
2
3
4
5
6
7
8
int *create_int(int value) {
int *p = malloc(sizeof *p);
if (p == NULL) {
return NULL;
}
*p = value;
return p;
}

调用者负责释放:

1
2
3
4
5
int *p = create_int(42);
if (p) {
printf("%d\n", *p);
free(p);
}

8. 函数指针

函数指针指向函数,而不是数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int add(int a, int b) {
return a + b;
}

int sub(int a, int b) {
return a - b;
}

int main(void) {
int (*op)(int, int) = add;
printf("%d\n", op(5, 3)); // 8

op = sub;
printf("%d\n", op(5, 3)); // 2

return 0;
}

常见用途:回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>

int cmp_int(const void *a, const void *b) {
int x = *(const int *)a;
int y = *(const int *)b;
return (x > y) - (x < y);
}

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

qsort(arr, n, sizeof(arr[0]), cmp_int);

for (size_t i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}

用 typedef 简化:

1
2
3
typedef int (*BinOp)(int, int);

BinOp op = add;

注意区分:

1
2
int (*fp)(int, int); // 函数指针
int *f(int, int); // 返回 int* 的函数

9. 指针数组与数组指针

指针数组:数组元素是指针。

1
2
3
4
int a = 1, b = 2;
int *arr[2] = {&a, &b};

printf("%d\n", *arr[0]); // 1

字符串数组常见:

1
const char *names[] = {"Alice", "Bob", "Cindy"};

数组指针:指针指向一个数组。

1
2
3
4
int arr[5] = {1, 2, 3, 4, 5};
int (*p)[5] = &arr;

printf("%d\n", (*p)[0]); // 1

二维数组常用数组指针:

1
2
3
4
int matrix[3][4];

int (*row)[4] = matrix; // row 指向一行
printf("%d\n", row[1][2]); // matrix[1][2]

10. 多级指针

二级指针保存指针的地址。

1
2
3
4
5
6
7
int a = 10;
int *p = &a;
int **pp = &p;

printf("%d\n", **pp); // 10
**pp = 20;
printf("%d\n", a); // 20

常见用途:在函数内部修改指针本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void alloc_int(int **out, int value) {
*out = malloc(sizeof **out);
if (*out != NULL) {
**out = value;
}
}

int main(void) {
int *p = NULL;
alloc_int(&p, 42);
if (p) {
printf("%d\n", *p);
free(p);
}
return 0;
}

11. const 与指针

const 和指针组合容易混淆:

1
2
3
4
5
int a = 1, b = 2;

const int *p1 = &a; // 指向常量的指针:不能通过 p1 改 a,但 p1 可改指向
int * const p2 = &a; // 常量指针:p2 不能改指向,但可通过 p2 改 a
const int * const p3 = &a; // 既不能改指向,也不能通过 p3 改值

示例:

1
2
3
4
5
6
7
8
p1 = &b;    // OK
// *p1 = 3; // 错误

*p2 = 3; // OK
// p2 = &b; // 错误

// *p3 = 3; // 错误
// p3 = &b; // 错误

读法:const 修饰它左边的内容;如果左边没有,就修饰右边。


12. void* 与动态内存

void * 是通用指针,可以保存任意对象指针。

1
2
3
4
int a = 10;
void *v = &a;
int *p = v; // C 中可以隐式转换
printf("%d\n", *p);

void * 不能直接解引用:

1
// printf("%d\n", *v); // 错误

动态内存函数返回 void *:

1
2
3
4
5
6
7
8
9
10
11
int *p = malloc(10 * sizeof *p);
if (p == NULL) {
// 处理分配失败
}

for (int i = 0; i < 10; i++) {
p[i] = i;
}

free(p);
p = NULL;

calloc 会初始化为 0:

1
int *p = calloc(10, sizeof *p);

realloc 调整大小:

1
2
3
4
int *tmp = realloc(p, 20 * sizeof *p);
if (tmp != NULL) {
p = tmp;
}

不要直接写:

1
p = realloc(p, new_size); // 失败时可能丢失原指针

释放内存:

1
2
free(p);
p = NULL;

规则:

  • malloc/calloc/realloc 分配的内存用 free 释放。
  • 只能释放一次。
  • 不能释放栈变量或非动态内存。
  • 释放后不要再访问。
  • 忘记释放会造成内存泄漏。

13. 结构体指针

结构体指针用 -> 访问成员。

1
2
3
4
5
6
7
8
9
10
typedef struct Node {
int value;
struct Node *next;
} Node;

Node *n = malloc(sizeof *n);
if (n != NULL) {
n->value = 42;
n->next = NULL;
}

n->value 等价于 (*n).value。

链表常见操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Node *head = NULL;

Node *n = malloc(sizeof *n);
if (n != NULL) {
n->value = 10;
n->next = head;
head = n;
}

// 释放链表
while (head != NULL) {
Node *tmp = head;
head = head->next;
free(tmp);
}

14. 常见用法总结

  1. 作为输出参数:函数通过指针修改外部变量。

    1
    void get_value(int *out);
  2. 遍历数组:用指针访问连续内存。

    1
    2
    3
    for (int *p = arr; p < arr + n; p++) {
    printf("%d ", *p);
    }
  3. 字符串处理:遍历、查找、复制。

    1
    2
    3
    while (*s) {
    s++;
    }
  4. 动态内存:运行时创建数组、结构体、链表。

    1
    int *p = malloc(n * sizeof *p);
  5. 数据结构:链表、树、图等。

    1
    struct Node *next;
  6. 回调函数:函数指针作为参数。

    1
    qsort(arr, n, sizeof arr[0], cmp);
  7. 返回多个值:通过指针参数。

    1
    void divmod(int a, int b, int *q, int *r);
  8. 泛型编程:void * + 函数指针。

    1
    2
    void *data;
    int (*cmp)(const void *, const void *);
  9. 访问硬件或内存映射寄存器:嵌入式常见。

    1
    2
    volatile unsigned int *reg = (volatile unsigned int *)0x40000000;
    *reg = 1;
  10. 多级指针:修改指针本身。

    1
    void alloc(int **p);

15. 常见陷阱

  1. 未初始化指针

    1
    2
    int *p;
    *p = 10; // 未定义行为
  2. 空指针解引用

    1
    2
    int *p = NULL;
    *p = 10; // 崩溃或未定义行为
  3. 悬空指针

    1
    2
    3
    int *p = malloc(sizeof *p);
    free(p);
    *p = 10; // 错误,p 已悬空
  4. 返回局部变量地址

    1
    2
    3
    4
    int *bad(void) {
    int x = 10;
    return &x; // 错误
    }
  5. 数组越界

    1
    2
    int arr[3];
    arr[3] = 10; // 越界
  6. 混淆数组和指针

    1
    2
    3
    void f(int arr[]) {
    printf("%zu\n", sizeof(arr)); // 指针大小,不是数组大小
    }
  7. 修改字符串字面量

    1
    2
    char *s = "hello";
    s[0] = 'H'; // 未定义行为
  8. 内存泄漏

    1
    2
    int *p = malloc(sizeof *p);
    // 忘记 free(p)
  9. 重复释放

    1
    2
    free(p);
    free(p); // 错误
  10. 指针类型不匹配

    1
    2
    int a = 10;
    double *p = (double *)&a; // 可能错误

16. 建议

  • 指针声明后尽量初始化:

    1
    int *p = NULL;
  • free 后置空:

    1
    2
    free(p);
    p = NULL;
  • 动态分配后检查:

    1
    2
    int *p = malloc(n * sizeof *p);
    if (p == NULL) { /* 处理 */ }
  • 使用 sizeof *p 而不是写死类型大小:

    1
    int *p = malloc(n * sizeof *p);
  • 用 const 表达“不修改”意图:

    1
    void print(const char *s);
  • 画内存图理解指针关系。

  • 用 valgrind、AddressSanitizer 等工具检查内存问题。


一句话总结:指针保存地址;指针类型决定访问方式和步长;使用前必须保证它指向有效内存;谁分配,谁释放。