C语言重难点全解析:指针到内存四区

C 语言重难点系统总结笔记

一、指针全解析

1. 一级指针(基础指针)
  • 定义 :指向普通变量的指针(存储变量的内存地址 ,不是值!)。
    语法:数据类型 *指针名;
    例:int *p; char *str;

  • 💡 通俗理解
    把变量a想象成一个放着物品的房间&a房间的门牌号 ,指针p就是一张写着门牌号的纸条

    • p = &a:把a的门牌号抄到p上。
    • *p:根据p上的门牌号,找到房间a拿出/修改里面的物品
  • 核心操作 (加了我之前搞混的点注释):

    c 复制代码
    int a = 10;
    int *p = &a; // p存的是a的地址(门牌号),不是10!
    *p = 20; // *p是"根据地址找房间",修改a的值,此时a=20
    printf("a的值:%d,p存的地址:%p", a, p); // 输出:a的值:20,p存的地址:0x...
  • 作用:间接访问变量、传递变量地址给函数(实现"引用传递"效果)。

2. 二级指针(指向指针的指针)
  • 定义 :存储一级指针地址 的指针(指针的指针,像"套娃")。
    语法:数据类型 **二级指针名;
    例:int **pp;

  • 💡 通俗理解
    一级指针p是"写着房间门牌号的纸条",二级指针pp是"放着这张纸条的文件夹 "。

    • pp = &p:文件夹里放着纸条p
    • *pp:打开文件夹,拿出纸条p(得到p的内容,即a的地址)。
    • **pp:根据纸条p找房间a,拿出物品(得到a的值)。
  • 核心逻辑

    c 复制代码
    int a = 10;
    int *p = &a; // 一级指针p指向a
    int **pp = &p; // 二级指针pp指向p
    
    // 通过二级指针修改a的值:**pp → *p → a
    **pp = 30; 
    printf("a的值:%d", a); // 输出:30
  • ⚠️ 为什么需要二级指针?
    比如在函数里想修改一级指针的指向 (不是修改指针指向的值),就需要二级指针。
    举个简单例子:

    c 复制代码
    // 错误示例:不用二级指针,函数里修改p没用
    void changePtr(int *p) {
        int b = 20;
        p = &b; // 这里的p是局部变量,外面的p不会变
    }
    
    // 正确示例:用二级指针,修改外面p的指向
    void changePtr(int **pp) {
        int b = 20;
        *pp = &b; // *pp是外面的p,让p指向b
    }
    
    int main() {
        int a = 10;
        int *p = &a;
        changePtr(&p); // 传p的地址(二级指针)
        printf("*p = %d", *p); // 输出:20(p现在指向b了)
        return 0;
    }
  • 作用:在函数中修改一级指针的指向(如链表头节点的修改,后面会讲)。

3. 数组指针(指向数组的指针)
  • 定义 :专门指向整个数组的指针,不是指向数组元素

  • 语法:数据类型 (*指针名)[数组长度];
    例:int (*arr_p)[5]; // 指向包含5个int元素的数组

  • 💡 易混区分技巧

    • 数组指针:(*p)[5] → 先看括号里的*p,说明p指针,指向一个数组。
    • 指针数组:*p[5] → 先看p[5],说明p数组,里面存了5个指针。
  • 使用示例

    c 复制代码
    int arr[5] = {1,2,3,4,5};
    int (*arr_p)[5] = &arr; // 注意:取整个数组的地址用&arr,不是arr
    // 虽然arr和&arr的地址值一样,但意义不同:
    // arr是数组首元素地址(arr+1跳过1个int),&arr是整个数组地址(&arr+1跳过5个int)
    
    // 访问数组元素:(*arr_p)[索引]
    printf("arr[0] = %d", (*arr_p)[0]); // 输出:1
    printf("数组首地址:%p,arr_p指向的地址:%p", arr, arr_p); // 地址相同
4. 函数指针(指向函数的指针)
  • 定义 :存储函数入口地址 的指针,可通过指针调用函数。
    语法:返回值类型 (*指针名)(参数类型1, 参数类型2, ...);
    例:int (*func_p)(int, int); // 指向参数为2个int、返回int的函数

  • 💡 先看中间的(*func_p),说明是指针;再看两边:左边int是返回值,右边(int, int)是参数类型 → 合起来就是"指向返回int、参数2个int的函数的指针"。

  • 使用示例

    c 复制代码
    // 普通函数
    int add(int a, int b) { return a + b; }
    
    int main() {
        int (*func_p)(int, int) = add; // 函数指针func_p指向add(函数名就是地址,可省略&)
        
        // 通过函数指针调用函数
        int result = func_p(3, 4); // 等价于 (*func_p)(3,4),两种写法都可以
        printf("3+4=%d", result); // 输出:7
        return 0;
    }
  • 作用 :实现函数回调(如排序函数qsort中自定义比较规则)。

二、数组与指针的区别

对比维度 数组(如int arr[5] 指针(如int *p
内存分配 编译时分配连续内存空间,大小固定(5×4=20字节)。 编译时仅分配指针变量本身的空间 (通常8字节),指向的内存需手动分配(如malloc)。
可修改性 数组名是常量地址 ,不能被重新赋值! ❌ 错误示例:arr = p;(报错) 指针是变量,可随时指向不同地址! ✅ 正确示例:p = arr; p = &a;
sizeof 计算 返回数组总字节数 ! 例:sizeof(arr) = 20 返回指针变量本身的字节数 ! 例:sizeof(p) = 8(64位系统)。
退化(Decay) 数组名作为函数参数时,会退化为指向首元素的指针(丢失长度信息)! 💡 比如:void func(int arr[]) { sizeof(arr) = 8; }(这里arr是指针,不是数组了) 本身就是指针,无退化问题。

三、结构体与联合体

1. 结构体(struct)
  • 定义 :将不同类型的数据组合成一个整体,各成员独立分配内存 (像"一套房子里的不同房间")。
    语法:

    c 复制代码
    struct 结构体名 {
        数据类型 成员1;
        数据类型 成员2;
        ...
    };
  • 💡 内存分配图示(内存对齐)
    结构体遵循"内存对齐"原则(成员地址是自身大小的整数倍,总大小是最大成员大小的整数倍),比如:

    c 复制代码
    struct Student {
        char name[10]; // 10字节(占地址0-9)
        int age; // 4字节(对齐到地址12,补2字节空着,占12-15)
        float score; // 4字节(占16-19,总大小补到24,因为最大成员是4,24是4的倍数)
    };
    printf("struct大小:%d", sizeof(struct Student)); // 输出:24

    (画图理解:name[10] → 空2字节 → age → score → 空4字节,总共24)

  • 核心作用:自定义复杂数据类型(如链表节点、学生信息)。

2. 联合体(union)
  • 定义 :与结构体类似,但所有成员共享同一块内存空间 (像"一个房间,不同时间放不同东西"),大小等于最大成员的大小。
    语法:

    c 复制代码
    union 联合体名 {
        数据类型 成员1;
        数据类型 成员2;
        ...
    };
  • 💡 内存分配图示(共享内存)
    所有成员从同一地址开始存储,修改一个成员会覆盖其他成员。比如:

    c 复制代码
    union Data {
        int i; // 4字节
        float f; // 4字节
        char str[20]; // 20字节(最大,所以union大小是20)
    };
    union Data data;
    data.i = 10; // 把i设为10,内存里存的是10的二进制
    printf("data.f = %f", data.f); // 输出乱码(把i的二进制当成float解析了)
    printf("union大小:%d", sizeof(data)); // 输出:20(最大成员str[20]的大小)

    (画图理解:i、f、str都从地址0开始,用i时这块内存存int,用f时存float,互相覆盖)

  • 核心作用:节省内存(同一时刻只使用一个成员)、数据类型转换(通过不同成员解析同一块内存)。

四、内存四区模型(程序运行时的内存分配)

内存区域 作用 生命周期 存储内容示例
栈区(Stack) 由编译器自动分配释放,存储函数参数、局部变量。 💡 生长方向:从高地址向低地址(像"往桶里放东西,从顶端(高地址)往下放")。 随函数调用创建,函数结束释放。 int a=10;(局部变量)、函数形参int x
堆区(Heap) 由程序员手动分配释放(malloc/calloc/realloc分配,free释放)。 💡 生长方向:从低地址向高地址(像"在空地上盖房子,从低地址开始盖")。 手动分配后,直到free才释放。 int *p = (int*)malloc(4);(p指向堆内存)。
数据区(静态区) 存储全局变量、静态变量(static修饰)、常量字符串。 程序启动时创建,程序结束释放。 int g_a=20;(全局变量)、static int s_b=30;(静态局部变量)。
代码区 存储程序的二进制可执行代码(函数体指令)。 程序启动时加载,只读。 main函数、add函数的机器指令。
  • ⚠️ 常见错误
    • 栈区变量越界访问(如数组下标越界)。
    • 堆区内存泄漏(忘记free)或重复释放。
    • 返回局部变量地址(函数结束后栈区内存释放,地址无效!比如:int* func() { int a=10; return &a; } 错误)。

五、字符串核心函数(自定义实现+底层逻辑)

1. 拷贝函数:my_strcpy
  • 功能char *my_strcpy(char *dest, const char *src)
    src指向的字符串(以\0结尾)复制到dest指向的数组。

  • 💡 核心逻辑 :逐个复制字符,直到src遇到\0必须手动给dest\0

  • ⚠️ 不加\0的错误示例

    c 复制代码
    // 错误版:没加'\0'
    char* my_strcpy_wrong(char* dest, const char* src) {
        char* ret = dest;
        while (*src != '\0') {
            *dest++ = *src++;
        }
        // 这里没加'\0'
        return ret;
    }
    
    int main() {
        char dest[10] = "xxxxxx"; // 初始是"xxxxxx"
        char src[] = "hello";
        my_strcpy_wrong(dest, src);
        printf("dest = %s", dest); // 输出:helloxxx(因为没'\0',会把后面的xxx也打出来)
        return 0;
    }
  • 正确代码实现 (防重叠优化版):

    c 复制代码
    char *my_strcpy(char *dest, const char *src) {
        if (dest == NULL || src == NULL) return NULL; // 判空
        char *ret = dest; // 保存起始地址
        // 循环复制(包含src的'\0')
        while ((*dest++ = *src++) != '\0'); 
        return ret;
    }
  • 注意 :目标数组dest必须有足够空间,否则会导致缓冲区溢出。

2. 比较函数:my_strcmp
  • 功能int my_strcmp(const char *str1, const char *str2)
    按ASCII值比较str1str2,返回差值。

  • 核心逻辑 :逐个比较字符,直到不同或遇到\0

  • 代码实现

    c 复制代码
    int my_strcmp(const char *str1, const char *str2) {
        if (str1 == NULL || str2 == NULL) return (int)(str1 - str2); // 判空
        while (*str1 != '\0' && *str2 != '\0' && *str1 == *str2) {
            str1++;
            str2++;
        }
        return (int)(*str1 - *str2); // 返回ASCII差值
    }
  • 返回值示例
    my_strcmp("abc", "abd") → 返回'c'-'d' = -1(str1 < str2)。
    my_strcmp("abc", "abc") → 返回0(相等)。

3. 拼接函数:my_strcat
  • 功能char *my_strcat(char *dest, const char *src)
    src字符串拼接到dest字符串的末尾(覆盖dest原有的\0,并在拼接后加\0)。

  • 💡 核心逻辑图示

    1. 先移到dest的末尾(找到原来的\0)。
    2. src的字符逐个复制到dest末尾(覆盖原来的\0)。
    3. 最后加新的\0
  • 代码实现

    c 复制代码
    char *my_strcat(char *dest, const char *src) {
        if (dest == NULL || src == NULL) return NULL; // 判空
        char *ret = dest;
        // 步骤1:移到dest末尾(找'\0')
        while (*dest != '\0') {
            dest++;
        }
        // 步骤2:拼接src(包含'\0')
        while ((*dest++ = *src++) != '\0');
        return ret;
    }
  • 注意dest必须有足够空间容纳拼接后的字符串。

六、结构体实现链表节点

1. 链表节点的结构体定义
  • 核心成员数据域(存储数据) + 指针域(指向下一节点)。

    c 复制代码
    struct Node {
        int data; // 数据域(可替换为任意类型,如struct Student)
        struct Node *next; // 指针域(指向同类型节点,像"链条"一样连起来)
    };
  • 💡 链表结构图示
    node1(data=10, next→node2) → node2(data=20, next→node3) → node3(data=30, next→NULL)

2. 链表的基本操作(代码示例)
(1)创建节点
c 复制代码
#include <stdlib.h> // 包含malloc
struct Node* createNode(int data) {
    struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
    newNode->data = data;
    newNode->next = NULL; // 新节点默认指向NULL(链表尾巴)
    return newNode;
}
(2)连接节点形成链表
c 复制代码
int main() {
    // 创建3个节点
    struct Node* node1 = createNode(10);
    struct Node* node2 = createNode(20);
    struct Node* node3 = createNode(30);

    // 连接节点:node1 → node2 → node3 → NULL
    node1->next = node2;
    node2->next = node3;

    // 遍历链表
    struct Node* p = node1; // p为遍历指针,从node1开始
    while (p != NULL) {
        printf("%d ", p->data); // 输出:10 20 30
        p = p->next; // 指针后移,指向下一个节点
    }
    return 0;
}
(3)通过二级指针修改头节点(如头插法)
  • ⚠️ 不用二级指针的错误示例

    c 复制代码
    // 错误版:不用二级指针,函数里修改head没用
    void insertAtHead_wrong(struct Node* head, int data) {
        struct Node* newNode = createNode(data);
        newNode->next = head; // 新节点指向原头节点
        head = newNode; // 这里的head是局部变量,外面的head不会变
    }
    
    int main() {
        struct Node* head = NULL;
        insertAtHead_wrong(head, 10);
        printf("head = %p", head); // 输出:NULL(外面的head没变化)
        return 0;
    }
  • ✅ 正确版:用二级指针

    c 复制代码
    // 头插法:在链表头部插入新节点(需用二级指针修改头节点指向)
    void insertAtHead(struct Node** head, int data) {
        struct Node* newNode = createNode(data);
        newNode->next = *head; // 新节点指向原头节点(*head是外面的head)
        *head = newNode; // 更新头节点为新节点(修改外面的head)
    }
    
    // 使用示例:
    int main() {
        struct Node* head = NULL; // 初始链表为空
        insertAtHead(&head, 30); // 链表:30 → NULL(传head的地址,二级指针)
        insertAtHead(&head, 20); // 链表:20 → 30 → NULL
        insertAtHead(&head, 10); // 链表:10 → 20 → 30 → NULL
    
        // 遍历验证
        struct Node* p = head;
        while (p != NULL) {
            printf("%d ", p->data); // 输出:10 20 30
            p = p->next;
        }
        return 0;
    }

七、总结与学习建议

  1. 指针:从一级指针开始,多打印地址和值观察;重点用"括号法"区分数组指针与指针数组,用"套娃/文件夹"理解二级指针。
  2. 结构体/联合体:通过画图理解"独立分配"与"共享内存"的区别;结构体是链表、树等数据结构的基础。
  3. 内存四区:记住栈"高→低"、堆"低→高"的生长方向,理解变量的生命周期,避免野指针、内存泄漏。
  4. 字符串函数 :手动实现并测试,一定要记住\0的重要性 ;实际开发中优先使用系统函数(如strncpy防溢出)。
  5. 链表:多画图理解节点连接逻辑,掌握头插、尾插、遍历等基本操作,体会二级指针在修改头节点时的作用。
相关推荐
南宫萧幕3 小时前
HEV 智能能量管理实战:从 MPC/PPO 理论解析到 Python-Simulink 联合仿真闭环全流程
开发语言·python·算法·matlab·控制
码农的神经元3 小时前
Python 实现县域变电站智能巡检与抢修调度:地图、路径规划与恢复策略
开发语言·python
我命由我123453 小时前
Java 开发 - CountDownLatch 不需要手动关闭
android·java·开发语言·jvm·kotlin·android studio·android-studio
谭欣辰3 小时前
详细讲解 C++ 状压 DP
开发语言·c++·动态规划
chaofan9803 小时前
GPT-5.5 全压力测试:为什么 API 聚合调度是解决“首字延迟”的技术关键?
开发语言·人工智能·python·gpt·自动化·api
William_wL_3 小时前
【C++】stack和queue的使用和实现(附加deque的简单介绍)
开发语言·c++
hhb_6184 小时前
D架构底层调度与性能优化实践指南
开发语言
老花眼猫4 小时前
三角函数绘制椭圆和椭圆旋转
c语言·经验分享·青少年编程·课程设计
秋94 小时前
Java AI编程工具全景解析:功能、收费与工单系统实战指南
java·开发语言·ai编程