一、指针基础
1.1概念
内存与地址:内存被划分为一个个单元,每个单元1字节,每个单元有一个编号,这个编号就是地址。地址 = 指针。
取地址(&)和解引用(*):
-
&a:取变量 a 的地址 -
*p:通过地址 p 访问该地址上存储的数据
指针变量的定义:
int a = 10;
int *p = &a; // p 是指针,指向 int 类型
指针大小:32位系统4字节,64位系统8字节。所有指针类型大小相同。
空指针 NULL:
int *p = NULL; // 不指向任何有效内存,解引用会崩溃
指针类型的意义:
-
决定解引用权限:
int*访问4字节,char*访问1字节 -
决定步长:
int*+1跳4字节,char*+1跳1字节
1.2通用代码示例
typedef struct {
int *data; // 指向动态数组的指针
int size;
int capacity;
} SeqList;
void init(SeqList *list, int capacity) {
list->data = (int*)malloc(capacity * sizeof(int));
list->size = 0;
list->capacity = capacity;
}
void set(SeqList *list, int index, int value) {
if (index >= 0 && index < list->size) {
list->data[index] = value; // data[index] 等价于 *(data + index)
}
}
1.3踩坑记录
1 写顺序表时,init 里 (*list)->data 忘记加括号,写成 *list->data,编译报错才知道 -> 优先级高于 *.
2 malloc 后经常忘记 free,后来养成习惯:写完 malloc 立刻写对应的 free 框架
3.刚学指针时,经常 int *p; *p = 10; 直接解引用未初始化的指针,程序崩溃
二、指针的约束与调试
1.1概念
const 与指针:
-
const int *p:指向的内容不可改(左定值) -
int *const p:指针本身不可改(右定向) -
const int *const p:内容和指针都不可改
int a = 10, b = 20;
const int *p1 = &a;
// *p1 = 30; // 错误,不能通过 p1 改 a
p1 = &b; // 可以,p1 本身可以改
int *const p2 = &a;
*p2 = 30; // 可以,通过 p2 改 a
// p2 = &b; // 错误,p2 本身不能改
野指针:指向不可知位置的指针。成因:
-
未初始化:
int *p; *p = 10; -
越界访问:数组越界后指针指向未知区域
-
指向已释放空间:free 后没置 NULL,继续使用
规避方法:
-
定义时初始化为 NULL
-
不越界访问
-
free 后立即置 NULL
-
不返回局部变量的地址
assert 断言:调试时检查指针有效性
#include <assert.h>
void print(Node *node) {
assert(node != NULL); // 如果 node 是 NULL,程序终止并报错
printf("%d", node->data);
}
1.2通用代码示例(链表 destroy 中的野指针规避)
void destroy(Node **head) {
Node *p = *head;
while (p) {
Node *tmp = p->next;
free(p);
p = tmp;
}
*head = NULL; // free 后置 NULL,避免悬空指针
}
1.3踩坑记录
1 用 assert 检查参数,但 Release 模式下 assert 消失,后来知道生产代码要用 if 判断.
2.忘记引用assert.h头文件
3.写链表时,我选择了不带头节点的实现。空链表时 head == NULL,插入删除都要单独处理这个情况。 刚开始写的时候,经常忘记判断空链表,直接 head->next 导致崩溃。 后来想通了:带头节点可以统一代码逻辑,但不带头节点更节省内存。我选不带头节点是为了理解边界情况。
三、 指针与数组
1.1概念
数组名是首元素地址,但有两个例外:
-
sizeof(arr)计算整个数组大小 -
&arr取整个数组的地址
通过指针访问数组:arr[i] 等价于 *(arr + i)
int arr[5] = {1,2,3,4,5};
int *p = arr; // p 指向 arr[0]
arr[2] = 10; // 等价于 *(arr + 2) = 10
p[2] = 10; // 等价,p 是指针变量
// arr = p; // 数组名不能赋值
指针运算:
-
p + n:移动 n 个元素(地址增加n * sizeof(类型)) -
p - q:两个指针之间的元素个数(需指向同一数组) -
指针可以比较大小(地址高低)
一维数组传参的本质:数组名退化为指针
void func(int arr[]) { // 编译器当作 int *arr
int size = sizeof(arr); // 得到指针大小,不是数组大小
}
二维数组传参的本质:传递第一行的地址,用数组指针接收
void func(int (*arr)[4], int rows) { // arr 是数组指针,指向有4个int的数组
// arr[0][0] 访问第一行第一列
}
int main() {
int matrix[3][4];
func(matrix, 3); // 二维数组名退化为第一行的地址
}
1.2通用代码示例(顺序表遍历)
void print(SeqList *list) {
int *p = list->data;
int *end = list->data + list->size; // 尾后指针
while (p < end) {
printf("%d ", *p);
p++; // 指针移动
}
}
// 等价写法
for (int i = 0; i < list->size; i++) {
printf("%d ", list->data[i]); // data[i] 等价于 *(data + i)
}
1.3踩坑记录
顺序表遍历时,我想用 while (p < end) 来判断边界。
错误写法:int *end = list->data + list->size + 1; // 多算了 1
正确写法:int *end = list->data + list->size; // 指向最后一个元素的下一个位置 end 是“尾后指针”,不是最后一个元素的地址。这个坑让我理解了 C 语言“左闭右开”的设计。
四、 指针的进阶类型
1.1概念
二级指针:指向指针的指针
int a = 10;
int *p = &a;
int **pp = &p; // pp 是二级指针
**pp = 20; // 等价于 *p = 20,等价于 a = 20
指针数组:存放指针的数组
int a = 1, b = 2, c = 3;
int *arr[3] = {&a, &b, &c}; // 每个元素都是 int*
printf("%d", *arr[0]); // 输出 1
数组指针:指向数组的指针
int arr[5] = {1,2,3,4,5};
int (*p)[5] = &arr; // p 指向整个数组
// p+1 跳过一个数组(5个int),不是跳过一个int
用指针数组模拟二维数组:
int row1[3] = {1,2,3};
int row2[3] = {4,5,6};
int *matrix[2] = {row1, row2}; // 指针数组,每个元素指向一行
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", matrix[i][j]); // 访问方式像二维数组
}
}
1.2通用代码示例(链表 destroy 中的二级指针)
// 错误:传一级指针
void destroy_wrong(Node *head) {
Node *p = head;
while (p) {
Node *tmp = p->next;
free(p);
p = tmp;
}
head = NULL; // 只改了形参,外面的 head 还是野指针
}
// 正确:传二级指针
void destroy(Node **head) {
Node *p = *head;
while (p) {
Node *tmp = p->next;
free(p);
p = tmp;
}
*head = NULL; // 修改外面的 head
}
// 调用
destroy(&list);
1.3踩坑记录
1 写init时忘记用二级指针,发现初始化后头指针还是空的,后来才知道传指针的地址才能改变指针。
2
//int *p[5] 和 int (*p)[5] 我老搞混。后来用 sizeof 验证:
int *arr[5]; // 指针数组,arr 是数组,大小 = 5 * 8 = 40 字节
int (*ptr)[5]; // 数组指针,ptr 是指针,大小 = 8 字节
int a[5];
ptr = &a; // 数组指针指向整个数组
// arr = &a; // 错误,arr 是数组名不能赋值
//跑一次代码,看到大小不同,就记住了。
五、 指针与函数
1.1概念
字符指针与字符串常量:
char *p = "hello"; // p 指向常量区的字符串
// p[0] = 'H'; // 错误!字符串常量不可修改
char arr[] = "hello"; // 栈上的副本,可修改
arr[0] = 'H'; // 可以
函数指针:函数名是函数的地址
int add(int a, int b) {
return a + b;
}
int (*p)(int, int) = add; // p 是函数指针
int result = p(3, 5); // 通过指针调用,等价于 add(3,5)
typedef 简化函数指针:
typedef int (*FuncPtr)(int, int); // 定义类型别名
FuncPtr p = add; // 使用
函数指针数组(转移表):
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int (*calc[])(int, int) = {add, sub, mul};
int result = calc[0](3, 5); // 调用 add,结果 8
回调函数:通过函数指针调用的函数
// qsort 使用回调
int cmp(const void *a, const void *b) {
return *(int*)a - *(int*)b;
}
qsort(arr, n, sizeof(int), cmp); // cmp 是回调函数
*void 泛型指针**:可接收任意类型地址,但不能直接解引用
void *p = malloc(100); // malloc 返回 void*
int *q = (int*)p; // 使用时强制转换
1.2通用代码示例
函数指针在顺序表查找中的应用:
// 查找第一个满足条件的元素
int find_if(SeqList *list, int (*predicate)(int)) {
for (int i = 0; i < list->size; i++) {
if (predicate(list->data[i])) {
return i;
}
}
return -1;
}
// 使用
int is_even(int x) { return x % 2 == 0; }
int idx = find_if(&list, is_even);
1.3踩坑记录
1 用 qsort 时,比较函数参数是 const void*,忘记强制转换 (int*) 就直接解引用,编译报错
六、 指针与动态内存
1.1概念
指针是动态内存分配的基础。malloc/calloc/realloc 返回的地址必须用指针保存,free 后指针要置 NULL。
| 函数 | 作用 | 特点 |
|---|---|---|
malloc(size) | 分配 size 字节 | 内容未初始化 |
calloc(n, size) | 分配 n 个 size 字节 | 内容清零 |
realloc(ptr, new_size) | 扩容 | 可能移动内存,失败返回 NULL |
free(ptr) | 释放内存 | 释放后指针要置 NULL |
1.2通用代码示例
顺序表扩容(realloc 正确用法)
// 正确:用临时变量接收
int new_capacity = list->capacity * 2;
int *new_data = (int*)realloc(list->data, new_capacity * sizeof(int));
if (new_data == NULL) {
// 扩容失败,原数据还在
return;
}
list->data = new_data;
list->capacity = new_capacity;
// 错误:直接赋值,失败时原指针丢失
list->data = (int*)realloc(list->data, new_capacity * sizeof(int));
链表节点创建和销毁
// 创建节点
Node* create_node(int val) {
Node *node = (Node*)malloc(sizeof(Node));
if (node == NULL) {
return NULL; // 分配失败
}
node->data = val;
node->next = NULL;
return node;
}
// 销毁链表
void destroy(Node **head) {
Node *p = *head;
while (p) {
Node *tmp = p->next;
free(p);
p = tmp;
}
*head = NULL; // free 后置 NULL
}
队列扩容(循环队列拉直)
// 扩容时,把环形数据“拉直”放到新数组开头
int *new_data = (int*)malloc(new_capacity * sizeof(int));
for (int i = 0; i < size; i++) {
new_data[i] = old_data[(front + i) % old_capacity];
}
free(old_data);
data = new_data;
front = 0;
rear = size;
1.3踩坑记录
1 队列扩容时,先改了 capacity 再 malloc,如果 malloc 失败,capacity 已经是新值,状态不一致
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2502_94558334/article/details/159351911



