1. 函数间传参

① 解决的问题:

普通变量传给函数时,函数只是创建了一个与原变量地址不同的临时变量。即使函数把临时变量改变,后续程序中的原变量不会变化.

② 指针的作用:

指针变量标记了原变量的地址,使工具函数能直接修改原变量.

#include <stdio.h>

// 1. 定义工具函数:一个能"顺着地址改数据"的函数
void addOne(int *p) { 
    *p = *p + 1;  // 找到地址 p 指向的内存,把里面的值 +1
}

// 2. 指挥部:程序从这里开始
int main() {
    int num = 10;      // 准备变量num,赋值为10

    addOne(&num);      // 把 num 的"地址"交给 addOne,请它帮忙把 num 的值 +1

    printf("%d", num); // 验证结果

    return 0;
}

2. 动态内存分配

① 程序创建内存空间的方式:静态分配 & 动态分配

静态分配:写代码时内存空间大小即被写定,即使未完全利用.

② 解决的问题:

Ⅰ 防止内存浪费;

Ⅱ 避免函数执行完,函数内部定义的普通变量(静态内存所存内容)被系统强制回收销毁导致后续程序报错.

③ 指针的作用:

根据用户的实际需求,向操作系统临时借一块指定大小的内存空间(这块空间叫"堆").

P.S. 想要多少借多少;想用多久用多久.

④ 动态内存分配的具体应用:

malloc:申请内存空间

void* malloc(size_t size);

传入需要的字节数. 因为返回的是无类型指针 void*,所以通常需要强转成对应类型的指针. 例如:

int num = 5;
// 申请能装下 5 个 int 的空间(5 * 4字节 = 20字节)
int *ptr = (int *)malloc(num * sizeof(int)); 

if (ptr == NULL) {
    // 关键步骤:如果系统内存满了,申请会失败返回 NULL
    printf("内存申请失败!\n");
    return 1;
}

free:释放内存空间

void free(void *ptr);

把记录着之前申请的内存地址的指针传回系统.

free(ptr);   // 把借来的内存还给系统
ptr = NULL;  // 良好习惯:归还后把指针置空,防止误操作"野指针"

free 只是把这块内存空间的状态改成了"空闲",里面的旧数据(之前存的数据)依然会残留在之前的内存空间,直到下一次有新数据写入把旧数据覆盖掉.

calloc(Clear Allocation):申请内存空间并清零

参数:分别传"元素个数"和"每个元素的大小". ② 自动清零:会自动把申请到的内存全部清零(填充为 0).

// 申请 5 个 `int` 空间,且里面每一个值都被初始化为 0
int *ptr = (int *)calloc(5, sizeof(int));
// 系统会自动相乘($5 \times 4 = 20$ 字节),在堆内存(Heap)里找一块连续的 20 字节 空间

malloc 的区别,malloc 执行后不会清零申请内存空间内的数据.

realloc:重新调整先前申请的内存空间大小

void* realloc(void *ptr, size_t new_size);
// 觉得刚才的 5 个空间不够了,想扩充到 10 个 int
int *new_ptr = (int *)realloc(ptr, 10 * sizeof(int));

if (new_ptr != NULL) {
    ptr = new_ptr; // 扩容成功,更新指针
}

作用:realloc 把"找新房子、搬家、退旧房子"一系列复杂的操作流程全部封包成了自动操作. 如果运气好后面有空间,它连家都不用搬,可以直接原地把墙拆了扩大房间.

3. 传递大型数据结构,提升运行效率

① 解决的问题:

// 普通传值方式
void print_employee(struct Employee emp) {
    printf("姓名:%s\n", emp.name);
}

int main() {
    struct Employee e1 = { ... }; // 假设 e1 已经填满了数据

    print_employee(e1); // 调用函数
}

如果这个函数被在一个 for 循环里调用了 1,000 次,CPU 就需要重复进行 1,000 次 14KB 的内存搬运(累计搬运 14MB 数据)。这不仅白白浪费了大量 CPU 算力和内存带宽,还会导致程序出现明显的卡顿.

② 指针传参的"零复制"的作用

// 指针传参方式(零复制)
void print_employee(const struct Employee *emp_ptr) {
    // -> 是指针专用的结构体成员访问符,等价于 (*emp_ptr).name
    printf("姓名:%s\n", emp_ptr->name); 
}

int main() {
    struct Employee e1 = { ... };

    print_employee(&e1); // 只把 e1 的内存地址(钥匙)传过去
}
  • 地址传输:无论 e1 里面包含多少兆(MB)的数据,指针变量 emp_ptr 固定只占用 8 个字节(64 位系统下,所有指针统一为 8 字节).
  • 零内存复制:CPU 只需要把这 8 字节的地址值复制一下给函数即可,完全不需要动 e1 内部 14KB 的庞大数据.
  • 直接访问:函数内部通过 emp_ptr 这个地址,可以直接读取 e1 中的内容.
  • 安全防护:避免指针传参后不小心在函数内部写错代码把原数据改掉,我们可以使用const关键字.
// `const` 确保了函数只有只读权限,绝对无法损坏外部的原数据
void print_employee(const struct Employee *emp_ptr) {
    // emp_ptr->sales_data[0] = 0; // 如果试图修改数据,编译器会直接报错并拒绝编译

    printf("姓名:%s\n", emp_ptr->name); // 安全地只读访问
}