皮锐头像
关注

C语言内容知识总结(6.指针与内存管理)

一、指针基础

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,编译报错才知道 -> 优先级高于 *.

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 本身不能改

野指针:指向不可知位置的指针。成因:

  1. 未初始化:int *p; *p = 10;

  2. 越界访问:数组越界后指针指向未知区域

  3. 指向已释放空间: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概念

数组名是首元素地址,但有两个例外:

  1. sizeof(arr) 计算整个数组大小

  2. &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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--