目录第三阶段函数1声明与定义2参数传递例1例2例33数组退化不传入函数时传入函数时为什么 sizeof() 失效数组长度丢失多传一个size为什么用 size_t4函数作用域与生命周期①作用域Scope②生命周期5static①static 的作用②为什么static能做到③static有三个用途1:static修饰局部变量2static修饰全局变量3static修饰函数6return返回值1无返回值2单返回值3多返回值第三阶段函数1声明与定义这玩意没学但是用熟练了...2参数传递一句话先总结我们在学什么C语言只有一种传参方式—按值传递Pass by Value// main三个例子一起跑 int main() { // ---------- 例1传值 ---------- int a1 10; func1(a1); printf(例1 — 传值 a1 %d (还是 10没改成 100)\n, a1); // ---------- 例2传地址 ---------- int a2 10; func2(a2); printf(例2 — 传地址 a2 %d (变成了 100)\n, a2); // ---------- 例3二级指针 ---------- int a3 10; int *ptr a3; // ptr 指向 a3 printf(例3 — 调用前 ptr %p (指向 a3)\n, (void *)ptr); func3(ptr); // 传 ptr 自己的地址 printf(例3 — 调用后 ptr %p (变成了 NULL)\n, (void *)ptr); return 0; }void func1(int x) { x 100; // 改副本原件不受影响 } void func2(int *p) { *p 100; // 通过地址间接修改改的是原件 } void func3(int **pp) { *pp NULL; // 解引用一次摸到的是 ptr 本身 }例1// 例1传普通变量 /* 目标func1 想把 a 改成 100改得了吗 步骤① main 栈帧变量 a值 10 地址 变量 值 0x1000 → a 10 步骤② func1(a) 被调用 → 把 a 的值10拷贝一份塞给参数 x 地址 变量 值 0x1000 → a 10 ← main 地盘 0x2000 → x 10 ← func1 地盘副本 步骤③ x 100 → 改的是 0x2000 位置的副本 0x2000 → x 100 ← 副本变了 步骤④ func1 返回栈帧回收x 消失 0x1000 → a 10 ← 原件纹丝不动 步骤⑤ printf 输出 10 */例2// 例2传地址指针 /* 目标func2 想把 a 改成 100这次能行吗 步骤① main 栈帧变量 a值 10 地址 变量 值 0x1000 → a 10 步骤② func2(a) 被调用 → 把 a 的地址0x1000拷贝一份塞给参数 p 地址 变量 值 0x1000 → a 10 ← main 地盘 0x2000 → p 0x1000 ← func2 地盘p 存的是 a 的地址 步骤③ *p 100 → 解引用顺着 p 存的地址0x1000摸过去改了那个位置的值 0x1000 → a 100 ← 原件被改了 0x2000 → p 0x1000 ← p 自己没变也没必要变 步骤④ func2 返回p 消失但 a 已经被改成 100 了 步骤⑤ printf 输出 100 关键p 本身是副本但 *p 不是——*p 就是 a 本人的别名 */例3// 例3进阶二级指针 /* 场景你有一个指针 ptr 指向 a现在想写一个函数把 ptr 本身改成 NULL 回顾例1规则想改 int传 int *即 int 的地址 推广一下 想改 int *传 int **即 int * 的地址 就是这个规律想改什么类型就传那个类型的地址加一颗 * 步骤① main 栈帧 地址 变量 值 0x1000 → a 10 0x2000 → ptr 0x1000 ← ptr 指向 a 步骤② func3(ptr) 被调用 → 把 ptr 自己的地址0x2000拷贝一份给 pp 地址 变量 值 0x1000 → a 10 0x2000 → ptr 0x1000 0x3000 → pp 0x2000 ← pp 存的是 ptr 的地址 步骤③ *pp NULL → 顺着 pp 存的地址0x2000摸过去把那个位置改成 NULL 0x2000 → ptr NULL ← ptr 被改了 步骤④ func3 返回pp 消失ptr 已经是 NULL */3数组退化数组一旦传给函数就已经退化成指针函数无法知道数组到底有多长。数组表达式在大多数表达式中会自动转换decay为指向首元素的指针。数组与函数数组在定义处是完整的数组一旦作为函数参数传递就会自动退化为指向首元素的指针因此函数只能知道“数据从哪里开始”不知道“数据有多少”所以必须额外传一个size参数。不传入函数时定义一个数组int arr[5] {1, 2, 3, 4, 5};它在内存中长这样假设int为 4 字节整个数组占用5 × 4 20 字节int arr[5];printf(%zu\n, sizeof(arr)); 输出得到20 这里sizeof 正常计算数组大小知道它的长度。传入函数时void func(int arr[]) { printf(%zu\n, sizeof(arr)); }调用func(arr); 数组名被编译器自动改成void func(int *arr)实际上是func(arr[0]);传进去了第一个元素地址.这样设计效率高int arr[10000]; func(arr);如果真的复制数组 10000 × 440000字节每一次调用都将复制40KB效率极低为什么 sizeof() 失效void func(int arr[]) { printf(%zu\n, sizeof(arr)); }编译器看到的是void func(int *arr)sizeof(arr)实际上是sizeof(int *)数组长度已经丢失了只能知道数组元素的长度int 就是4 字节数组长度丢失多传一个sizevoid process(int *arr, size_t n) { for(size_t i 0; i n; i) { printf(%d\n, arr[i]); } }这就是 C 语言处理数组的标准方式。为什么用 size_t初学者写void process(int *arr, int len);更规范的是void process(int *arr, size_t len);因此长度参数也用size_t最自然、最安全。4函数作用域与生命周期①作用域Scopevoid func() { int a; }只有func里面才能用变量a。②生命周期它能存在多久叫做生命周期Lifetime进入fanc函数编译器开始创建a随后执行完毕进行销毁。5static①static 的作用void test() { int a 0; a; printf(%d\n,a); }调用测试三次test(); 都是 1 1 1因为函数每次进入都会创建a然后a随后输出a1然后销毁a。改用static修饰变量void test() { static int a 0; a; printf(%d\n,a); }第一次调用函数创建aa0a得a随后输出a。第二次调用函数不再创建a跳过static int a 0 执行a输出a2第三次调用函数不再创建a跳过static int a 0 执行a输出a3②为什么static能做到因为普通变量存放在Stack中函数结束即可销毁。而static变量放在Data/BSS中程序启动时开始创建只有等到程序结束时才会删除虽然写在函数里面但是它不是函数的临时变量。③static有三个用途1:static修饰局部变量void test() { static int count; }作用保存状态状态机常用状态机不是掉电不丢失要实现掉电不丢失要外扩flash或者其他硬件支持。好处1跨调用保持状态不用污染全局// ❌ 用全局变量实现 int call_count 0; // 谁都能改哪天被别的函数误写了都不知道 int count_calls(void) { call_count; return call_count; } // ✅ 用 static 局部变量 int count_calls(void) { static int n 0; // 只有这个函数能碰它 n; return n; }全局变量是工程里的慢性毒药——规模一上来你永远不知道哪个文件的哪个函数偷偷改了它。static局部变量把状态锁在函数内部封装性拉满。2避免栈上的大数组爆栈void process(void) { // ❌ 每次调用都在栈上分配 1MB → 递归/频繁调用直接 stack overflow // char buffer[1024 * 1024]; // ✅ 只分配一次在数据段不占栈空间 static char buffer[1024 * 1024]; // ... }嵌入式开发里栈通常很小几 KB 到几十 KB这个技巧非常实用3单例和懒初始化const char* get_config(void) { static char config[256] {0}; static int loaded 0; if (!loaded) { read_config_from_flash(config); // 只读一次 loaded 1; } return config; }高级C设计模式了这段代码对于刚学static的人来说确实有点超纲。其实它想表达的是一个非常经典的思想懒初始化Lazy Initialization需要的时候才初始化而且只初始化一次。2static修饰全局变量全局变量int a 10 整个工程都可以访问和改变该变量的值。static int a10;只属于当前.c文件static告诉链接器这个变量的名字不要暴露出去。3static修饰函数普通函数void test() { }其他文件调用时extern void test();可以调用和使用。static void test() { }经过static修饰后只有当前.c文件可以调用。2和3共同的好处1防止命名冲突最核心的好处// lcd.c static void init_gpio(void) { /* 初始化 LCD 的 GPIO */ } static int backlight_level 80; // wifi.c static void init_gpio(void) { /* 初始化 WiFi 模块的 GPIO */ } static int backlight_level 50; // WiFi 模块的信号背光概念两个文件各有一个init_gpio和backlight_level加了static后它们各过各的链接器不会报multiple definition。大工程几十上百个.c文件没有static的话命名就是噩梦——取函数名比写代码还累。2模块化 封装——C 语言里的 private// ring_buffer.c — 环形缓冲区模块 static uint8_t buf[256]; // 外面对 buffer 做了什么一无所知 static int read_idx 0; // 实现细节不需要暴露 static int write_idx 0; static int calc_free(void) { // 内部辅助函数 return (read_idx - write_idx - 1) 255; } // 公开接口——只有这几个函数在 .h 里声明 void rb_init(void) { read_idx write_idx 0; } int rb_write(uint8_t b) { if (calc_free() 0) return -1; buf[write_idx] b; write_idx (write_idx 1) 255; return 0; } int rb_read(uint8_t *b) { if (read_idx write_idx) return -1; *b buf[read_idx]; read_idx (read_idx 1) 255; return 0; }你给外部使用者的是一个干净的.h接口内部实现全部static隐藏。这就是面向对象里 private member 的思想在 C 里的落地3编译器优化更激进编译器知道static函数只在本文件内被调用、static全局变量只在本文件内被访问于是自动内联——一个小的static函数如果只被调用一两次编译器可能直接把函数体嵌进去省掉 call/ret 开销死代码消除——如果static函数没被任何地方调用直接删掉不占 ROM寄存器分配更自由——不需要按 ABI 规约传递参数对嵌入式来说就是更快的执行 更小的固件体积。不过空转函数做延时需要加个volatile修饰不然被编译器杀了。4重构安全你把一个static函数改了签名、改了行为影响范围 100% 在本文件内不会不小心炸了另一个模块。这在多人协作时是保命的东西——你改你的lcd.c别人改他的motor.c互不干扰6return返回值1无返回值void LED_ON(void) { GPIO_SetBits(GPIOA, GPIO_Pin_1); }它什么都不返回2单返回值int add(int a, int b) { return a b; }核心作用终止函数执行和传递结果数据。函数内部只能有一个return被执行。一旦执行了函数就结束了后面的代码永远不会运行。3多返回值结构体typedef struct { int x; int y; } Point; Point GetPoint(void) { Point p {10, 20}; return p; } Point p GetPoint(); printf(%d %d\n, p.x, p.y);指针传参void GetMaxMin(int a, int b, int *max, int *min) { if(a b) { *max a; *min b; } else { *max b; *min a; } } int max; int min; GetMaxMin(3, 8, max, min); printf(%d %d\n, max, min); 得到 8 3实现一个函数返回多个结果。