嵌入式学习日志(八)

8   学习函数

1   函数核心知识

1.1  函数基础与设计价值

        1.  本质与入口:程序从 main 函数启动,函数是构建程序功能的基本单元,实现“从无到有”的功能拆分。

        2.  设计意义:降低耦合性(功能模块独立,关联少)、提升复用性(代码可重复调用) 。 void prtchar(); 是声明(告知编译器存在此函数,未定义实现),区别于函数定义(含具体逻辑)。

1.2函数定义规则

        1.  定义限制:函数不可嵌套定义(函数内部不能再定义新函数)。

        2.  完整结构:

返回值类型 函数名(形式参数列表)

{

    语句块(实现逻辑)

}

        1)返回值类型:可是基本类型( int / char 等)、指针,但不能是数组类型(数组名退化为指针传递,实际用指针替代)。

        2)形式参数(形参):定义函数时的参数占位符,调用时由实参传递值;形参名可自定义,需明确每个参数的类型(如 int a, int b ,不能简写 int a,b  ,语法需严谨)。

        3.  调用规则:

        1)调用前需确保“可见”:要么函数在调用处之前定义,要么通过 extern 声明(多文件场景)。

         2)实参、形参匹配:数量一致、类型兼容(支持隐式转换,如 double 转 int 会截断小数;但类型差异大时(如 指针 转 int )需显式强转,否则编译报错 );实参、形参名称可相同(作用域不同,互不影响 )。

示例(含调用逻辑):

// 函数定义(加法逻辑)

int add(int a, int b)

{

    int ret = a + b;

    return ret;

}

// 主调函数(main)

int main(void)

{

    int i = 10, j = 20;

    int sum = add(i, j); // 实参i/j传值给形参a/b

    printf("结果:%d\n", sum);

    printf("直接调用:%d\n", add(11, 22));

    return 0;

}

        4.  return 语句细节:

        1)立即终止函数,将结果“带回”调用处;若函数无返回值( void 类型),可省略 return (或写 return; 显式结束 )。

        2)返回值类型需与函数声明/定义的类型兼容:若函数定义时未明确返回值类型(旧语法兼容),默认按 int 处理;但现代 C 标准建议显式声明,避免歧义 。

        3)禁用场景:函数调用表达式(如 add(1,2)  )不能直接作为形参传递(需先计算值再传参 )。

        5.  特殊函数类型(void):

        1.  无返回值,常用于纯逻辑执行(如打印、修改全局状态 )。

示例:

void sayHi(void)

{

    printf("Hello!\n");

}

int main(void)

{

    sayHi(); // 调用无参函数,括号不能省略

    return 0;

}

1.3  参数传递机制

        1.  值传递:

        1)本质:形参是实参的“副本”(独立内存空间),函数内修改形参不影响实参 。

示例(验证值传递特性 ):

void modify(int n)

{

    n += 10; // 修改形参,实参不受影响

}

int main(void)

{

    int num = 5;

    modify(num);

    printf("实参值:%d\n", num); // 输出5

    return 0;

}

        2.  指针传参(地址传递):

        1)本质:传递变量的内存地址,函数内通过指针操作可直接修改实参的值(突破值传递的“只读限制” )。

示例(修改实参 ):

void modify(int *p)

{

    *p += 10; // 通过指针修改实参内存

}

int main(void)

{

    int num = 5;

    modify(&num); // 传递地址

    printf("实参值:%d\n", num); // 输出15

    return 0;

}

        3.  数组传参(退化为指针):

        1)一维数组:形参写为 数组名[] 或 指针 形式(本质是传递数组首元素地址 );因数组退化为指针,无法直接用 sizeof 获取原数组长度,需手动传长度参数 。

示例(修改数组元素 ):

void updateArray(int arr[], int len)

{

    for (int i = 0; i < len; i++)

        {

        arr[i] += 10; // 通过指针操作修改数组元素

        }

}

int main(void)

{

    int arr[] = {1,2,3,4,5};

    int len = sizeof(arr) / sizeof(arr[0]);

    updateArray(arr, len);

    // 遍历验证

    for (int i = 0; i < len; i++)

        {

                printf("%d ", arr[i]); // 输出11 12 13 14 15

        }

    return 0;

}

        2)字符数组(字符串):依赖 '\0' 判断结束,无需显式传长度;

示例:

void printStr(char str[])

{

    printf("字符串:%s\n", str); // 自动识别'\0'

}

int main(void)

{

    char str[] = "Hello";

    printStr(str); // 输出Hello

    return 0;

}

        3)二维数组:需指定列数(或第二维长度 ),形参写为 int arr[][N] ( N 为列数,不可省略 );函数内通过 sizeof(arr[0])/sizeof(arr[0][0]) 计算列数(基于首行数组 ),需手动传行数即元素个数   int rows = sizeof(a) / sizeof(a[0]); 。

示例(打印二维数组 ):

void print2DArray(int arr[][3], int rows)

{

    for (int i = 0; i < rows; i++)

        {

                for (int j = 0; j < 3; j++)

                {

                        printf("%d ", arr[i][j]);

                }

                printf("\n");

        }

}

int main(void)

{

    int arr[2][3] = {{1,2,3}, {4,5,6}};

    int rows = sizeof(a) / sizeof(a[0]);

    print2DArray(arr, 2);

    return 0;

}

        4)传参顺序与副作用:

        多参数传递时,实际入栈顺序是“从右到左”(编译器实现细节 );避免对同一变量多次操作(如 incr(++i, i++)  ),因顺序依赖编译器,结果不确定(代码不健壮 )。

1.4  函数递归

        1.  核心逻辑:函数直接/间接调用自身,需明确“递归条件”(避免无限递归 )。

        2.  优缺点:

        1)优点:简化复杂问题(如汉诺塔、分治场景 )。

        2)缺点:效率低(频繁入栈出栈)、占用栈空间大(可能导致栈溢出,程序崩溃 );递归深度有限,需合理设计终止条件 。

示例(累加求和 ):

int sum(int n)

{

    if (n == 1)

        { // 递归终止条件

                return 1;

         }

    return sum(n - 1) + n; // 递归调用自身

}

int main(void)

{

    printf("1-5累加和:%d\n", sum(5)); // 输出15

    return 0;

}

        3)经典案例(汉诺塔 ):

        规则:将 n 个盘子从 A 柱移到 C 柱,借助 B 柱,每次只能移一个,大盘不能压小盘。

递归逻辑:

1. 把 n-1 个盘子从 A → B (借助 C  )。

2. 把第 n 个盘子从 A → C  。

3. 把 n-1 个盘子从 B → C (借助 A  )。

代码实现:

// 打印移动步骤

void move(char from, char to)

{

    printf("%c -> %c\n", from, to);

}

void hanoi(int n, char A, char B, char C)

{

    if (n == 1)

        {

        move(A, C); // 终止条件:直接移动

        return;

         }

    hanoi(n - 1, A, C, B); // 1. 移n-1个到B

    move(A, C); // 2. 移第n个到C

    hanoi(n - 1, B, A, C); // 3. 移n-1个到C

}

int main(void)

{

    hanoi(3, 'A', 'B', 'C'); // 3个盘子的汉诺塔

    return 0;

}

移动步数规律: 2^n - 1 ( n 为盘子数量 )。

2   变量与内存管理

2.1  变量作用域与存储周期

        1.  分类核心:按作用域(哪里能访问)和存储周期(存在多久)划分,影响变量的“可见性”和“生存期” 。

        2.  局部变量:

        1)定义:在 {} 代码块、函数形参中声明的变量,作用域仅限当前代码块/函数(局部可见 )。

        2)存储:默认在栈区( auto 类型,空间自动分配/销毁 );若用 static 修饰(静态局部变量 ),则存储在静态区,生存期与程序一致(值会保留,下次调用延续 )

静态局部变量示例:

void count()

{

    static int num = 0; // 静态局部变量,存储在静态区

    num++;

    printf("调用次数:%d\n", num);

}

int main(void)

{

    count(); // 输出1

    count(); // 输出2(值保留)

    return 0;

}

        3.  全局变量:

        1)定义:在函数外声明的变量,作用域默认全局可见(整个程序均可访问 );存储在静态区,未初始化时自动赋 0 (数值类型)或空(指针/数组 )。

        2)最佳实践:尽量减少全局变量(增加耦合性,易引发 Bug ),优先用函数传参替代 。

        4.  可见性规则:

        1)标识符需先定义,后使用(编译器按顺序解析 )。

        2)同一作用域(如同一函数内)禁止同名变量(编译器无法区分 )。

        3)不同作用域(无包含关系):同名变量互不影响(作用域隔离 )。

        4)不同作用域(有包含关系,如函数内与全局 ):内层作用域的变量会“屏蔽”外层同名变量(就近原则,外层变量在此作用域不可见 )。

2.2  变量存储类别(内存分区)

        1.  栈区(stack):

        1)存储:局部变量、函数参数、返回地址等;自动分配/销毁(函数调用时入栈,返回时出栈 )。

        2)特点:空间小(几 MB )、速度快,遵循FILO(先进后出) 。

        2.  堆区(heap):

        1)存储:动态分配的内存(如 malloc / free 管理的空间 );需手动申请、释放,否则内存泄漏 。

        2)特点:空间大(按需分配 )、灵活,但操作复杂(需管理指针 )。

        3.  静态区(全局区):

        1)存储:全局变量、静态变量( static 修饰 );程序运行期间一直存在,程序结束后由系统回收 。

        4.  字符串常量区:

        1)存储:字符串常量(如 "Hello"  );只读(修改会触发未定义行为 ),程序结束后释放 。

        5.  代码区:

        1)存储:程序指令(函数逻辑、操作码等 );只读,由编译器加载到内存 。

2.3  多文件协作(extern 与 static 修饰)

        1.  extern 声明(跨文件共享):

        多文件编程时,若 main.c 需调用 func.c 的函数/变量,需通过 extern 声明“告知编译器存在” 。规范流程(头文件配合 ):

        1)func.c :定义函数/变量(实现逻辑 )。

// func.c

int add(int a, int b)

{

    return a + b;

}

        2)func.h :声明函数/变量(头文件只做声明,不写实现 ),并通过头文件保护(防止重复包含 )。

// func.h

#ifndef FUNC_H

#define FUNC_H

extern int add(int a, int b); // 函数声明

#endif

        3)main.c :通过 #include "func.h" 引入声明,直接调用 。

// main.c

#include "func.h"

#include

int main(void)

{

    printf("结果:%d\n", add(10, 20)); // 调用func.c的函数

    return 0;

}

编译命令(GCC 示例 ):

gcc -o app main.c func.c

./app # 运行程序

        2.  static 修饰(限制作用域):

        1)静态全局变量( static int g_var;  ):作用域仅限当前文件(其他文件无法访问 ),避免跨文件变量冲突 。

        2)静态函数( static int func();  ):作用域仅限当前文件,不对外暴露,增强代码封装性 。

你可能感兴趣的:(嵌入式学习日志(八))