【C语言】第四课 指针与内存管理

1 指针的本质:地址与解引用

1.1 什么是指针?

指针是一个变量,其存储的值是另一个变量的内存地址。你可以将内存想象成一个巨大的公寓楼,每个字节是一个房间,每个房间都有唯一的门牌号(地址)。指针就是记录着这些门牌号的便签。

  • 声明指针数据类型 *指针变量名;

    c 复制代码
    int *p;      // 指向整型的指针
    char *ch;    // 指向字符型的指针
    float *fp;   // 指向浮点型的指针

    * 表示这是一个指针变量,数据类型 说明了指针所指向的内存区域中存储的数据类型。

  • 初始化指针 :使用 &(取地址操作符)获取变量的地址。

    c 复制代码
    int a = 10;
    int *p = &a;  // p 指向变量a的地址

    未初始化的指针是"野指针",指向随机内存,非常危险。良好的习惯是定义时立即初始化 ,若暂无明确指向,可初始化为 NULL(或 0)。

    c 复制代码
    int *safe_ptr = NULL; // 安全的初始化
  • 解引用指针 :使用 *(解引用操作符)访问或修改指针所指向地址的值。

    c 复制代码
    printf("a = %d\n", a);   // 输出: a = 10
    printf("*p = %d\n", *p); // 输出: *p = 10 (通过p访问a的值)
    
    *p = 20; // 通过指针p修改其指向地址(即变量a)的值
    printf("a is now %d\n", a); // 输出: a is now 20

    解引用本质上是一次内存访问 。对 *p 的操作就是对 p 所存地址处数据的操作。

1.2 指针的大小

指针变量的大小是固定的,取决于系统的寻址能力,与它指向的数据类型无关:

  • 32位系统:通常为 4字节
  • 64位系统:通常为 8字节
    你可以用 sizeof 操作符验证:
c 复制代码
printf("Size of int pointer: %zu\n", sizeof(int*));    // 输出: 8 (在64位系统)
printf("Size of char pointer: %zu\n", sizeof(char*));  // 输出: 8
printf("Size of double pointer: %zu\n", sizeof(double*)); // 输出: 8

2 指针的算术运算

指针的算术运算(加减)不是简单的整数加减,而是以所指向数据类型的大小为单位进行移动。

c 复制代码
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // p 指向数组首元素(arr[0])的地址

printf("*p = %d\n", *p);     // 输出: 10
printf("Address: %p\n", p);

p++; // 向后移动一个int单位(通常是4字节),指向arr[1]
printf("After p++:\n");
printf("*p = %d\n", *p);     // 输出: 20
printf("Address: %p\n", p); // 地址值比之前增加了4

p += 2; // 向后移动两个int单位,指向arr[3]
printf("After p += 2:\n");
printf("*p = %d\n", *p);     // 输出: 40

减法运算可以计算两个指针之间的距离(元素个数):

c 复制代码
int *p1 = &arr[0];
int *p2 = &arr[3];
ptrdiff_t diff = p2 - p1; // 计算两个指针之间相差的元素个数
printf("p2 - p1 = %td\n", diff); // 输出: 3 (相差3个元素)

注意 :指针减法的两个指针必须指向同一块连续内存空间(如同一个数组),否则行为未定义。

3 指针与数组

在C语言中,数组名在大多数情况下是一个指向数组首元素的常量指针

c 复制代码
int arr[5] = {1, 2, 3, 4, 5};

// 以下访问方式是等价的:
printf("arr[0] = %d\n", arr[0]);
printf("*arr = %d\n", *arr); // 对数组名解引用访问第一个元素

// 通过指针算术访问数组元素
printf("arr[1] = %d\n", *(arr + 1)); 
printf("arr[2] = %d\n", *(arr + 2));

// 定义一个指针遍历数组
int *ptr = arr;
for (int i = 0; i < 5; i++) {
    printf("Element %d: %d\n", i, *(ptr + i));
    // 或 printf("Element %d: %d\n", i, ptr[i]); // 指针也可以使用下标!
}

重要区别 :数组名是指针常量,其值(指向的地址)不可改变。而指针变量可以重新赋值。

c 复制代码
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

// p = p + 1;  // 合法,p现在指向arr[1]
// arr = arr + 1; // 非法!编译错误!数组名是常量,不能修改。

4 指针与函数

4.1 指针作为函数参数(模拟"传引用")

C语言函数参数传递默认是传值调用 ,即函数获得的是实参值的副本。修改副本不会影响原始实参。若希望函数内部修改外部变量的值,需要传递变量的指针

c 复制代码
// 一个交换两个变量值的函数
void swap(int *x, int *y) { // 接收指针作为参数
    int temp = *x; // 解引用x,获取其指向的值
    *x = *y;       // 将y指向的值赋给x指向的内存
    *y = temp;     // 将temp的值赋给y指向的内存
}

int main() {
    int a = 10, b = 20;
    printf("Before swap: a = %d, b = %d\n", a, b);
    swap(&a, &b); // 传递变量a和b的地址
    printf("After swap: a = %d, b = %d\n", a, b);
    return 0;
}

输出:

复制代码
Before swap: a = 10, b = 20
After swap: a = 20, b = 10

工作原理 :函数 swap 接收的是 ab 的地址(指针),通过解引用操作 *x*y,直接操作 main 函数栈帧中 ab 所在的内存单元,从而真正交换它们的值。

4.2 数组作为函数参数

当数组作为函数参数传递时,它会退化为指向其首元素的指针 。因此,函数内部无法通过 sizeof 获取原始数组的长度。

c 复制代码
void printArray(int arr[], int size) { // int arr[] 等价于 int *arr
    // 在函数内部,sizeof(arr) 将是指针的大小(8或4),而不是整个数组的大小!
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]); // 虽然arr是指针,但仍可使用下标语法
    }
    printf("\n");
}

int main() {
    int myArray[] = {1, 2, 3, 4, 5};
    int length = sizeof(myArray) / sizeof(myArray[0]); // 正确计算数组长度
    printArray(myArray, length); // 传递数组名和实际长度
    return 0;
}

5 动态内存分配:堆(Heap)

5.1 堆与栈的区别

理解的区别至关重要。

特性 栈 (Stack) 堆 (Heap)
管理方式 编译器自动分配和释放 程序员手动分配 (malloc, calloc) 和释放 (free)
生命周期 函数执行期间,函数返回后自动销毁 从分配开始直到显式释放为止
大小限制 较小(例如几MB),操作系统依赖 很大,仅受系统可用虚拟内存大小限制
分配速度 非常快 相对较慢,涉及更复杂的管理
碎片化 可能产生碎片
灵活性 大小和生命周期在编译时确定 大小和生命周期在运行时动态决定

5.2 动态内存分配函数

C语言使用 malloc, calloc, realloc 在堆上分配内存,使用 free 释放内存。

  • malloc:分配指定字节数的未初始化内存。

    c 复制代码
    // 分配可存储10个int的内存空间
    int *arr = (int *)malloc(10 * sizeof(int));
    if (arr == NULL) {
        // 分配失败必须检查!NULL可能意味着内存不足
        fprintf(stderr, "Memory allocation failed!\n");
        exit(1);
    }
    // 使用分配的内存...
  • calloc :分配指定数量和大小的内存,并初始化为0

    c 复制代码
    // 分配10个int,并全部初始化为0
    int *arr_zero = (int *)calloc(10, sizeof(int));
  • realloc:调整已分配内存块的大小(可能移动位置)。

    c 复制代码
    // 将之前分配的内存扩大到20个int
    int *new_arr = (int *)realloc(arr, 20 * sizeof(int));
    if (new_arr == NULL) {
        // 处理失败,注意:原来的arr指针依然有效,需要单独释放
        free(arr);
        fprintf(stderr, "Memory reallocation failed!\n");
        exit(1);
    } else {
        arr = new_arr; // 让arr指向新的内存块
    }
  • free:释放之前动态分配的内存。

    c 复制代码
    free(arr); // 释放arr指向的内存
    arr = NULL; // 良好实践:释放后立即将指针置为NULL,防止悬垂指针

    谁分配,谁释放 :确保每个 malloc, calloc, realloc 都有对应的 free

5.3 常见动态内存错误(漏洞根源)

  1. 内存泄漏 (Memory Leak):分配的内存没有被释放,导致程序持续占用内存直至耗尽。

    c 复制代码
    void leak() {
        int *ptr = (int *)malloc(100 * sizeof(int));
        // ... 使用ptr ...
        // 忘记 free(ptr); 函数返回后,再也无法访问或释放那100个int的内存!
    }
  2. Use-After-Free :释放内存后,再次使用该指针访问已释放的内存。这是一个严重的安全漏洞,攻击者可能利用此漏洞执行恶意代码。

    c 复制代码
    int *ptr = (int *)malloc(sizeof(int));
    *ptr = 42;
    free(ptr); // 内存被释放,交还给系统
    // ptr现在是一个"悬垂指针"(Dangling Pointer)
    *ptr = 10; // 危险!未定义行为:可能崩溃,也可能 silently corrupt data。
  3. Double Free :对同一块动态内存多次调用 free。这会导致内存管理数据结构损坏,可能引发程序崩溃。

    c 复制代码
    int *ptr = (int *)malloc(sizeof(int));
    free(ptr);
    // ...
    free(ptr); // 错误!同一内存释放两次。
相关推荐
励志不掉头发的内向程序员3 小时前
C++进阶——继承 (1)
开发语言·c++·学习
菩提树下的凡夫3 小时前
瑞芯微RV1126目标识别算法Yolov8的部署应用
java·算法·yolo
多打代码4 小时前
2025.09.05 用队列实现栈 & 有效的括号 & 删除字符串中的所有相邻重复项
python·算法
黑客思维者5 小时前
《我是如何用C语言写工控系统的漏洞和Bug》连载(1)内容大纲
c语言·bug·工控漏洞
中国胖子风清扬5 小时前
Rust 序列化技术全解析:从基础到实战
开发语言·c++·spring boot·vscode·后端·中间件·rust
我就是全世界5 小时前
【存储选型终极指南】RustFS vs MinIO:5大维度深度对决,95%技术团队的选择秘密!
开发语言·分布式·rust·存储
j_xxx404_5 小时前
数据结构:栈和队列力扣算法题
c语言·数据结构·算法·leetcode·链表
南莺莺5 小时前
假设一个算术表达式中包含圆括号、方括号和花括号3种类型的括号,编写一个算法来判别,表达式中的括号是否配对,以字符“\0“作为算术表达式的结束符
c语言·数据结构·算法·