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); // 201.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 *)); // 832 位系统: 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 引入 nullptr2.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 + 44.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); // 34.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)); // World7. 指针与 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 int8. 常见陷阱
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. 思考题
- 为什么数组名不能赋值?
arr和&arr有什么区别?- 为什么
void*不能解引用? - 二级指针有什么用?
const int *p和int *const p有什么区别?
11. 本周小结
- 指针: 存储地址的变量.
- **& 和 ***: 取地址与解引用.
- 空指针与野指针: NULL, 未初始化, 悬空.
- 指针与数组: 数组名是指针常量.
- 指针算术: 移动元素个数, 不是字节.
- void*: 通用指针, 需转换.
- 多级指针: 指向指针的指针.
- const 指针: 修饰位置决定含义.
指针是 C 语言的灵魂. 理解指针, 就理解了 C 语言对内存的直接控制.