Wiki LogoWiki - The Power of Many

Week 05: 指针基础

掌握指针概念、地址运算、指针与数组的关系、指针算术与常见陷阱.

1. 指针的本质

1.1 什么是指针

指针是存储内存地址的变量:

内存布局:
地址        内容
0x1000:     10      <- 变量 a
0x1008:     0x1000  <- 指针 p (存储 a 的地址)
int a = 10;
int *p = &a;  // p 存储 a 的地址

printf("a 的值: %d\n", a);       // 10
printf("a 的地址: %p\n", &a);    // 0x7ffc...
printf("p 的值: %p\n", p);       // 0x7ffc... (等于 &a)
printf("*p 的值: %d\n", *p);     // 10 (解引用)

1.2 声明语法

int *p;      // p 是指向 int 的指针
int* p;      // 同上 (风格不同)
int * p;     // 同上

// 多变量声明的陷阱
int* p, q;   // p 是指针, q 是 int!
int *p, *q;  // p 和 q 都是指针

1.3 取地址运算符 (&)

int x = 10;
int *p = &x;  // & 获取 x 的地址

// 不能取右值的地址
// int *q = &10;       // 错误
// int *r = &(x + 1);  // 错误

1.4 解引用运算符 (*)

int x = 10;
int *p = &x;

*p = 20;  // 通过指针修改 x 的值
printf("x = %d\n", x);  // 20

int y = *p;  // 读取 p 指向的值
printf("y = %d\n", y);  // 20

1.5 指针的大小

指针大小取决于平台, 不是所指类型:

printf("sizeof(int *) = %zu\n", sizeof(int *));      // 8 (64位)
printf("sizeof(char *) = %zu\n", sizeof(char *));    // 8
printf("sizeof(double *) = %zu\n", sizeof(double *)); // 8
printf("sizeof(void *) = %zu\n", sizeof(void *));    // 8

32 位系统: 4 字节
64 位系统: 8 字节


2. 空指针与野指针

2.1 空指针 (NULL)

int *p = NULL;  // 显式初始化为空

// 检查空指针
if (p == NULL) {
    printf("p 是空指针\n");
}

if (!p) {
    printf("p 是空指针\n");  // 等价写法
}

// 解引用空指针是未定义行为
// *p = 10;  // 段错误!

2.2 NULL 的定义

// 通常定义为
#define NULL ((void *)0)

// 或者 (C99)
#define NULL 0

// C23 引入 nullptr

2.3 野指针

野指针指向无效内存:

// 情况 1: 未初始化
int *p;  // p 包含垃圾值
// *p = 10;  // 危险!

// 情况 2: 指向已释放的内存
int *q = malloc(sizeof(int));
free(q);
// *q = 10;  // 危险! (悬空指针)

// 情况 3: 指向已超出作用域的变量
int *get_ptr(void) {
    int x = 10;
    return &x;  // 警告: 返回局部变量地址
}
int *r = get_ptr();
// *r 可能是垃圾值

2.4 最佳实践

// 声明时初始化
int *p = NULL;

// 释放后置空
free(ptr);
ptr = NULL;

// 使用前检查
if (ptr != NULL) {
    *ptr = 10;
}

3. 指针与数组

3.1 数组名是指针常量

int arr[5] = {1, 2, 3, 4, 5};

// arr 是指向第一个元素的指针
printf("arr = %p\n", arr);       // 数组首地址
printf("&arr[0] = %p\n", &arr[0]); // 第一个元素地址 (相同)

// arr 不可修改
// arr = NULL;  // 错误: arr 不是变量

3.2 通过指针访问数组

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

// 索引访问
printf("arr[2] = %d\n", arr[2]);  // 3

// 指针访问
printf("*(p + 2) = %d\n", *(p + 2));  // 3

// arr[i] 等价于 *(arr + i)
// 甚至可以写成 i[arr] (arr + i 和 i + arr 都合法)

3.3 数组与指针的区别

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

// 区别 1: sizeof
printf("sizeof(arr) = %zu\n", sizeof(arr));  // 20 (5 * 4)
printf("sizeof(p) = %zu\n", sizeof(p));      // 8 (指针大小)

// 区别 2: 赋值
// arr = p;  // 错误: 数组名不可赋值
p = arr;    // OK

// 区别 3: 取地址
printf("&arr = %p\n", &arr);    // 整个数组的地址
printf("&p = %p\n", &p);        // 指针变量的地址

// &arr 类型是 int (*)[5], 不是 int **

4. 指针算术

4.1 指针加减整数

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;

// 加法: 移动 n 个元素
printf("*p = %d\n", *p);         // 10
printf("*(p + 1) = %d\n", *(p + 1)); // 20
printf("*(p + 2) = %d\n", *(p + 2)); // 30

// 实际移动的字节数 = n * sizeof(类型)
// p + 1 实际是 p + 1 * sizeof(int) = p + 4

4.2 指针自增自减

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;

printf("*p++ = %d\n", *p++);  // 10, 然后 p 移动到 arr[1]
printf("*p = %d\n", *p);      // 20

printf("*++p = %d\n", *++p);  // 30, p 先移动到 arr[2]

4.3 指针差

int arr[5] = {10, 20, 30, 40, 50};
int *p = &arr[1];
int *q = &arr[4];

// 指针差返回元素个数, 不是字节数
ptrdiff_t diff = q - p;
printf("q - p = %td\n", diff);  // 3

4.4 指针比较

int arr[5] = {10, 20, 30, 40, 50};
int *p = &arr[1];
int *q = &arr[3];

// 只有指向同一数组的指针才能比较
if (p < q) {
    printf("p 在 q 之前\n");
}

// 可以检查是否在数组范围内
int *end = arr + 5;  // 指向数组末尾之后 (合法但不能解引用)
for (int *ptr = arr; ptr < end; ptr++) {
    printf("%d ", *ptr);
}

5. void 指针

5.1 void* 的特点

// void* 可以指向任何类型
int x = 10;
double y = 3.14;
char c = 'A';

void *p;
p = &x;  // OK
p = &y;  // OK
p = &c;  // OK

// 使用前必须转换
int *ip = (int *)p;
printf("%d\n", *ip);

// 不能直接解引用
// *p = 10;  // 错误

// 不能进行指针算术 (大小未知)
// p++;  // 某些编译器允许 (作为扩展), 但不推荐

5.2 void* 的典型用途

// malloc 返回 void*
void *malloc(size_t size);

int *p = (int *)malloc(10 * sizeof(int));

// 通用函数参数
void print_bytes(const void *ptr, size_t size) {
    const unsigned char *p = ptr;
    for (size_t i = 0; i < size; i++) {
        printf("%02x ", p[i]);
    }
    printf("\n");
}

int x = 0x12345678;
print_bytes(&x, sizeof(x));  // 78 56 34 12 (小端序)

6. 多级指针

6.1 二级指针

int x = 10;
int *p = &x;      // p 指向 x
int **pp = &p;    // pp 指向 p

printf("x = %d\n", x);       // 10
printf("*p = %d\n", *p);     // 10
printf("**pp = %d\n", **pp); // 10

// 修改
**pp = 20;
printf("x = %d\n", x);  // 20

*pp = NULL;  // 修改 p 的值
printf("p = %p\n", p);  // (nil)

6.2 二级指针的用途

修改指针本身:

void alloc_int(int **pp) {
    *pp = malloc(sizeof(int));
    **pp = 42;
}

int main(void) {
    int *p = NULL;
    alloc_int(&p);  // 传递指针的地址
    printf("*p = %d\n", *p);  // 42
    free(p);
    return 0;
}

指针数组:

char *strings[] = {"Hello", "World", "C"};
char **pp = strings;

printf("%s\n", pp[0]);   // Hello
printf("%s\n", *(pp + 1)); // World

7. 指针与 const

7.1 const 的位置

// 1. 指向 const 数据的指针 (数据不可变)
const int *p1;
int const *p2;  // 等价

int x = 10;
p1 = &x;
// *p1 = 20;  // 错误: 不能通过 p1 修改
p1++;         // OK: 指针可以移动

// 2. const 指针 (指针不可变)
int y = 20;
int *const p3 = &y;
*p3 = 30;     // OK: 可以修改数据
// p3++;      // 错误: 指针不可移动

// 3. 两者都 const
const int *const p4 = &x;
// *p4 = 40;  // 错误
// p4++;      // 错误

7.2 记忆技巧

从右向左读:

const int *p;   // p is a pointer to const int
int *const p;   // p is a const pointer to int
const int *const p;  // p is a const pointer to const int

8. 常见陷阱

8.1 解引用空指针

int *p = NULL;
*p = 10;  // 段错误!

8.2 使用未初始化的指针

int *p;   // 野指针
*p = 10;  // 未定义行为!

8.3 返回局部变量的地址

int *bad(void) {
    int x = 10;
    return &x;  // 警告!
}

8.4 指针算术越界

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr + 10;  // 越界
*p = 100;           // 未定义行为!

8.5 混淆数组和指针

// 外部声明
extern int arr[];    // OK
extern int *arr;     // 错误 (如果 arr 是数组)

// 原因: 数组和指针的内存布局不同

9. 练习

9.1 交换指针

编写函数交换两个指针的指向.

9.2 数组反转

使用指针实现数组原地反转.

9.3 字符串复制

实现 my_strcpy 函数.


10. 思考题

  1. 为什么数组名不能赋值?
  2. arr&arr 有什么区别?
  3. 为什么 void* 不能解引用?
  4. 二级指针有什么用?
  5. const int *pint *const p 有什么区别?

11. 本周小结

  • 指针: 存储地址的变量.
  • **& 和 ***: 取地址与解引用.
  • 空指针与野指针: NULL, 未初始化, 悬空.
  • 指针与数组: 数组名是指针常量.
  • 指针算术: 移动元素个数, 不是字节.
  • void*: 通用指针, 需转换.
  • 多级指针: 指向指针的指针.
  • const 指针: 修饰位置决定含义.

指针是 C 语言的灵魂. 理解指针, 就理解了 C 语言对内存的直接控制.

On this page