指针介绍和常见用法
1. 指针是什么
指针是一个变量,它存储的是内存地址。
1 | int a = 10; |
&a:取变量a的地址。int *p:声明一个指向int的指针。*p:解引用,访问p所指向的内存,也就是a。p:本身是一个变量,存储地址。
内存示意:
1 | 变量 a: 地址 0x1000,值 10 |
所以:
1 | printf("%d\n", *p); // 10 |
指针类型很重要:int *、char *、double * 不只是“地址”,还决定了:
- 解引用时读取多少字节;
- 指针加减时移动多少字节。
例如:
1 | char *cp; // cp + 1 通常移动 1 字节 |
指针大小通常与平台有关:32 位系统常见 4 字节,64 位系统常见 8 字节。但函数指针大小可能不同。
2. 基本语法
1 | int a = 10; |
注意:
%p用来打印地址,参数最好转换为void *。- 未初始化的指针是野指针,不能解引用。
1 | int *p; // 危险,p 指向未知位置 |
安全写法:
1 | int *p = NULL; |
空指针不能解引用:
1 | if (p != NULL) { |
3. 指针运算
指针可以进行有限算术运算:
1 | int arr[5] = {10, 20, 30, 40, 50}; |
指针加减整数时,移动的是 sizeof(*p) 个字节。
两个指针可以相减,前提是它们指向同一个数组或同一块连续内存:
1 | int *p1 = &arr[1]; |
指针也可以比较,但一般只在同一数组内比较有意义。
不能把两个指针相加:
1 | int *p3 = p1 + p2; // 错误 |
4. 指针与数组
数组名在大多数表达式中会退化为指向首元素的指针。
1 | int arr[5] = {1, 2, 3, 4, 5}; |
以下写法等价:
1 | arr[i] |
遍历数组:
1 | int arr[5] = {1, 2, 3, 4, 5}; |
也可以用指针自身移动:
1 | for (int *p = arr; p < arr + n; p++) { |
注意:数组名和指针并不完全等价:
1 | int arr[5]; |
&arr 的类型是 int (*)[5],即指向整个数组的指针:
1 | int (*pa)[5] = &arr; |
5. 指针与字符串
字符串字面量本质是以 '\0' 结尾的字符数组。
1 | const char *s = "hello"; |
建议用 const char *,因为字符串字面量通常不可修改,修改它是未定义行为。
如果要修改,用字符数组:
1 | char buf[] = "hello"; |
常见字符串遍历:
1 | size_t my_strlen(const char *s) { |
6. 指针作为函数参数
C 语言是值传递。如果函数要修改外部变量,需要传指针。
1 | void swap(int *x, int *y) { |
数组作为函数参数时,会退化为指针:
1 | void print_array(int *arr, int n) { |
下面两种写法等价:
1 | void f(int arr[]); |
函数内 sizeof(arr) 得到的是指针大小,不是数组大小。
函数也可以通过指针返回多个结果:
1 | void divmod(int a, int b, int *q, int *r) { |
7. 返回指针
函数可以返回指针,但不能返回局部变量的地址。
错误示例:
1 | int *bad(void) { |
正确方式:
- 返回静态变量地址;
- 返回全局变量地址;
- 返回动态分配内存;
- 返回调用者传入的指针。
1 | int *good(void) { |
动态分配:
1 | int *create_int(int value) { |
调用者负责释放:
1 | int *p = create_int(42); |
8. 函数指针
函数指针指向函数,而不是数据。
1 | int add(int a, int b) { |
常见用途:回调函数。
1 |
|
用 typedef 简化:
1 | typedef int (*BinOp)(int, int); |
注意区分:
1 | int (*fp)(int, int); // 函数指针 |
9. 指针数组与数组指针
指针数组:数组元素是指针。
1 | int a = 1, b = 2; |
字符串数组常见:
1 | const char *names[] = {"Alice", "Bob", "Cindy"}; |
数组指针:指针指向一个数组。
1 | int arr[5] = {1, 2, 3, 4, 5}; |
二维数组常用数组指针:
1 | int matrix[3][4]; |
10. 多级指针
二级指针保存指针的地址。
1 | int a = 10; |
常见用途:在函数内部修改指针本身。
1 | void alloc_int(int **out, int value) { |
11. const 与指针
const 和指针组合容易混淆:
1 | int a = 1, b = 2; |
示例:
1 | p1 = &b; // OK |
读法:const 修饰它左边的内容;如果左边没有,就修饰右边。
12. void* 与动态内存
void * 是通用指针,可以保存任意对象指针。
1 | int a = 10; |
void * 不能直接解引用:
1 | // printf("%d\n", *v); // 错误 |
动态内存函数返回 void *:
1 | int *p = malloc(10 * sizeof *p); |
calloc 会初始化为 0:
1 | int *p = calloc(10, sizeof *p); |
realloc 调整大小:
1 | int *tmp = realloc(p, 20 * sizeof *p); |
不要直接写:
1 | p = realloc(p, new_size); // 失败时可能丢失原指针 |
释放内存:
1 | free(p); |
规则:
malloc/calloc/realloc分配的内存用free释放。- 只能释放一次。
- 不能释放栈变量或非动态内存。
- 释放后不要再访问。
- 忘记释放会造成内存泄漏。
13. 结构体指针
结构体指针用 -> 访问成员。
1 | typedef struct Node { |
n->value 等价于 (*n).value。
链表常见操作:
1 | Node *head = NULL; |
14. 常见用法总结
作为输出参数:函数通过指针修改外部变量。
1
void get_value(int *out);
遍历数组:用指针访问连续内存。
1
2
3for (int *p = arr; p < arr + n; p++) {
printf("%d ", *p);
}字符串处理:遍历、查找、复制。
1
2
3while (*s) {
s++;
}动态内存:运行时创建数组、结构体、链表。
1
int *p = malloc(n * sizeof *p);
数据结构:链表、树、图等。
1
struct Node *next;
回调函数:函数指针作为参数。
1
qsort(arr, n, sizeof arr[0], cmp);
返回多个值:通过指针参数。
1
void divmod(int a, int b, int *q, int *r);
泛型编程:
void *+ 函数指针。1
2void *data;
int (*cmp)(const void *, const void *);访问硬件或内存映射寄存器:嵌入式常见。
1
2volatile unsigned int *reg = (volatile unsigned int *)0x40000000;
*reg = 1;多级指针:修改指针本身。
1
void alloc(int **p);
15. 常见陷阱
未初始化指针
1
2int *p;
*p = 10; // 未定义行为空指针解引用
1
2int *p = NULL;
*p = 10; // 崩溃或未定义行为悬空指针
1
2
3int *p = malloc(sizeof *p);
free(p);
*p = 10; // 错误,p 已悬空返回局部变量地址
1
2
3
4int *bad(void) {
int x = 10;
return &x; // 错误
}数组越界
1
2int arr[3];
arr[3] = 10; // 越界混淆数组和指针
1
2
3void f(int arr[]) {
printf("%zu\n", sizeof(arr)); // 指针大小,不是数组大小
}修改字符串字面量
1
2char *s = "hello";
s[0] = 'H'; // 未定义行为内存泄漏
1
2int *p = malloc(sizeof *p);
// 忘记 free(p)重复释放
1
2free(p);
free(p); // 错误指针类型不匹配
1
2int a = 10;
double *p = (double *)&a; // 可能错误
16. 建议
指针声明后尽量初始化:
1
int *p = NULL;
free后置空:1
2free(p);
p = NULL;动态分配后检查:
1
2int *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 等工具检查内存问题。
一句话总结:指针保存地址;指针类型决定访问方式和步长;使用前必须保证它指向有效内存;谁分配,谁释放。