C语言进阶—函数(static,递归,回调,指针,内联,变参,结构体尺寸)
目录
一 static函数
1. static变量
1.静态局部变量
2.静态全局变量
2. static函数
二 递归函数
三 指针函数&函数指针
1. 指针函数
2. 函数指针
四 回调函数
五 内联函数
1. 核心特性表
2. 优缺点分析表
3. 用场景建议
3.1 推荐使用场景
3.2 应避免场景
六 变参函数
1. 核心特性表
2. 优缺点对比表
七 结构体尺寸
1. 内存对齐规则表
2. 结构体布局优化表
一 static函数
功能分析底层内存分析
类型 特点 使用场景 优点 缺点 内存特点 局部变量 生命周期延长至程序结束,作用域保持局部 计数器、状态保持、缓存重用 保持状态,避免全局污染 线程不安全 存储在静态存储区 全局变量 限制作用域仅在当前文件 文件内共享数据 避免命名冲突 增加内存占用 存储在静态存储区 函数 限制函数作用域仅在当前文件 模块化开发、隐藏实现细节 提高封装性 无法跨文件调用 代码段存储
内存区域 存储内容 生命周期 访问特点 栈 自动变量 函数执行期间 自动分配/释放 堆 动态分配内存 直到free() 手动管理 静态区 static/全局变量 程序整个生命周期 编译时分配 代码段 函数/static函数 程序整个生命周期 只读
1. static变量
1.静态局部变量
1. 初始化在程序启动时执行一次
2. 存储在静态存储区(.data段)
3. 作用域仍为函数内局部
4. 线程不安全(多线程需加锁)
void func() {static int count = 0;
}
2.静态全局变量
其他文件无法通过extern访问
// file1.c
static int secret = 42; // 其他文件无法通过extern访问// file2.c
extern int secret; // 链接错误:undefined reference
2. static函数
关键字static
1.修饰变量为静态变量,修饰函数为静态函数
static 函数:只能本文件使用,其他文件不可访问,static要加在返回类型前
普通函数:普通函数默认都是可以跨文件可见的,也就是比如A.c 中有一个swap()函数,那么在b.c也可以访问得到
2.static可以限定变量或者函数为静态存储,static限定的变量或函数不会和同意程序中的其他文件名相冲突
static 变量,一开始就会有内存,不会随函数的出入栈,分配空间
优点:
1.静态函数会被自动分配在一个一直使用的存储区,知道程序结束内存消失,避免调用函数时压栈出栈 即全局
2.其文件可以定义相同的函数,不会冲突
3.静态函数不能被其他文件调用,对本代码可用
#include <stdio.h>static void function1()
{printf("niho");
}int main()
{function1();return 0;
}
二 递归函数
递归函数: 即函数调用自己
递归解决的问题:
1.阶乘
n!=(n-1)!*n=(n-2)!*(n-1)*n.......asm
0 的阶乘就是0
2.幂运算
同底数的幂的乘法
3.字符反转
123-321
递归的使用:
1.必须要有跳出条件,否则无限循环
#include <stdio.h>
#include <stdlib.h>int factorial(int n);
int main(int argc, char const *argv[])
{printf("输入一个不小于0的数值");int b = 0;scanf("%d", &b);int num = factorial(b);printf("%d的递归结构为:num=%d", b, num);return 0;
}
int factorial(int n)
{if (n < 0){printf("n不能小于0");exit(EXIT_FAILURE);}else if (n == 0){return 1;}else{return factorial(n - 1) * n;}
}
三 指针函数&函数指针
概念对比表
特性 指针函数(返回指针的函数) 函数指针(指向函数的指针) 定义方式 int* func(int a)
int (*func_ptr)(int)
本质 函数的返回类型是指针 变量类型为函数指针 内存存储 函数代码存储在代码段 指针变量存储在栈/静态区,指向代码段地址 主要用途 返回动态内存、数组首地址、静态变量地址 实现回调函数、策略模式、动态函数调用 优点 1. 灵活返回数据结构
2. 支持动态内存管理1. 提高代码灵活性
2. 实现多态行为缺点 1. 可能返回无效指针
2. 需手动管理内存1. 语法复杂
2. 类型安全需自行保证典型应用场景 1. 工厂模式创建对象
2. 返回数组首地址1. 事件处理器
2. 排序算法比较函数内存结构分析
元素 存储区域 生命周期 访问方式 指针函数 代码段 程序运行期间 函数调用 函数指针变量 栈/静态区 取决于作用域 指针解引用 动态分配内存 堆 直到free() 通过指针访 使用建议
指针函数:
优先返回动态分配内存(需配套提供释放函数)
避免返回局部变量地址
可返回静态变量地址(但需注意线程安全)
函数指针:
使用typedef定义易读的类型别名
始终检查指针是否为NULL后再调用
适用于插件架构、策略模式等场景
1. 指针函数
指针函数 :一个返回指针的函数,就被称为指针函数
定义全局变量,多个函数需要用到某一个变量的指针,就不能返回局部变量的指针,栈空间用完是销毁的
#include <stdio.h>
int i=9527;//指针函数
int* get_point(int a); int main(int argc, char const *argv[])
{ printf("main &i=%p\n",&i);//调用int *p=get_point(666);printf(" p=%p\n",p);return 0;
}//指针函数
int* get_point(int a){printf("get_point.....&a=%p,&i=%p\n",&a,&i);//全局变量的指针return &i;
}
2. 函数指针
函数指针:函数指针是一个变量,他存储了一个函数的地址(函数也是要放到内存内,也有地址)。
指向一个函数的指针,称为函数指针
声明函数指针语言:
int (*p)(int n);
指向函数的返回值和参数列表必须一致
#include <stdio.h>int my_function(int, double);
int you_function(int, double);int my_function(int i, double n)
{printf("my_function:%d,and%lf\n", i, n);
}
int you_function(int i, double n)
{printf("you_function:%d,and%lf\n", i, n);
}
int main(int argc, char const *argv[])
{// 1.普通调用函数my_function(1, 1.1);// 2.调用指针函数调用you的值,与函数地址int (*you)(int, double);you = you_function;int you1 = you(2, 2.2);printf("指针调用的值为%d\n", you);printf("指针调用函数地址为%p\n", you);printf("指针调用函数地址为%p\n", you_function); // 此处的函数名和数组名一样,可以当作内存地址。// 3.指针函数的返回值类类型,必须与参数列表一致return 0;
}
四 回调函数
核心概念表
特性 描述 底层原理 定义方式 通过函数指针传递函数作为参数 函数指针存储代码段地址 内存存储 函数代码存储在代码段,指针变量存储在栈/静态区 通过指针间接寻址执行 调用时机 由被调用函数在特定条件/事件触发时执行 通过函数指针跳转执行 参数传递 支持普通参数和函数指针参数 遵循C函数调用约定(参数压栈) 生命周期 回调函数需在调用时保持有效 需避免使用已释放的栈内存指针 特性对比表
维度 普通函数调用 回调函数调用 调用控制权 直接由调用者控制 由被调用方在特定时机触发 耦合度 高(直接依赖具体实现) 低(仅依赖接口) 灵活性 固定实现 运行时动态绑定 典型应用 顺序执行逻辑 事件处理、算法策略、异步通知 内存开销 无额外开销 需存储函数指针(4/8字节) 调试难度 简单(线性执行) 复杂(执行路径不可见)
#include <stdio.h>
#include <string.h>
/*
回调函数:会点函数是一种特殊函数,他作为参数传递给另一个函数,并在被调用函数执行完毕后被调用回调:旨被传入到另一个函数异步编程:指代码执行时不会阻塞整个程序运行的方式;事件驱动:指程序的执行是由外部时间触发而不是顺序执行的方式;c语言实现回调函数的方法:回调函数可以通过指针来实现*/// 回调函数,接受数值,做出回应。
void callback_function(int num);// 发送数值,请求回应。
void weixin_function(char *str, void (*p)(int));// 回调函数,接受数值,做出回应
void callback_function(int num)
{if (num > 10){printf("回应收到,做的不错\n");}else{printf("回应收到,做的很坏了\n");}
}
// 发送数值,请求回应,然后被main执行。void (*p)(int)即为函数指针
void weixin_function(char *str, void (*p)(int))
{int length = strlen(str);printf("数值已经发送,请求回应中......\n");// 事件驱动:指程序的执行是由外部事件触发而不是顺序执行的方式;// 并在被调用函数执行完毕后被调用p(length);
}
int main(int argc, char const *argv[])
{weixin_function("aaaaaaaaaa", callback_function);return 0;
}
五 内联函数
1. 核心特性表
特性 内联函数(inline) 普通函数 定义方式 inline 返回类型 函数名(参数)
返回类型 函数名(参数)
编译行为 代码直接嵌入调用处 生成独立函数体 执行效率 高(无调用开销) 低(有调用/返回开销) 代码体积 可能增大(多位置展开) 较小(单一副本) 调试难度 较难(无明确调用栈) 容易(完整调用栈) 适用场景 简单频繁调用的小函数 复杂或递归函数 内存特点 代码段多位置复制 代码段单一存储 跨文件可见性 需在头文件定义 通过声明可见 2. 优缺点分析表
优点 缺点 1. 消除函数调用开销 1. 增加代码体积(代码膨胀) 2. 避免栈操作(push/pop) 2. 调试困难(无调用信息) 3. 支持编译器优化 3. 不适合复杂函数 4. 减少指令缓存未命中 4. 可能被编译器忽略 3. 用场景建议
3.1 推荐使用场景
频繁调用的小函数(如数学运算)
硬件寄存器操作(如嵌入式开发)
关键性能路径代码(如游戏引擎)
替代宏函数(类型安全的宏)
3.2 应避免场景
递归函数
包含循环的大函数
虚函数/多态函数
导出库函数
#include <stdio.h>/*
内联函数函数频繁被调用,不断出入栈,会导致栈空间不断被消耗,慢语法:关键字inline必须和函数定义放在一起才能使用成为内联函数,仅将inline放在函数声名为何不所有的函数都使用内联?内联是以代码“膨胀”(赋值代码内容),仅仅省略了函数调用的开销从而提高代码执行效率如果执行函数体内代码的时间相比于函数调用的开下较大,那么效率收货会很少不适用于内联1.如果函数体内的代码较长,使用内联将导致内存消耗代价较高2.如果函数体内出现循环,那么执行函数体内的代码时间比函数调用的开销大
一个好的编译器会将根据函数的定义体,自动地取消不值得的内联(这进一步说了inline不应该出现在函数的声明中);*/// 内联函数在声名的时候,必须要加入static关键字
static inline void in_func();
static inline void in_func()
{printf("内联函数被调用,每一次调用,都要拷贝函数体的内容,消耗空间\n");
}int main(int argc, char const *argv[])
{in_func();// 内联函数调用,拷贝函数体,上面调用和下面的效果等同printf("内联函数被调用,每一次调用,都要拷贝函数体的内容,消耗空间\n");return 0;
}
六 变参函数
1. 核心特性表
地址高地址
特性 变参函数(Variable Arguments) 普通函数 定义方式 使用省略号 ...
声明参数列表固定参数列表 参数数量 调用时参数数量可变 参数数量固定 参数类型 支持不同类型参数混合 参数类型固定 内存访问 通过指针遍历栈内存 编译器自动处理参数访问 类型安全 无(需手动校验类型) 有(编译器检查) 适用场景 格式化输出、日志系统、数学运算 常规函数调用 内存特点 参数连续存储在调用栈 参数按调用约定存储 实现复杂度 高(需手动处理参数) 低(编译器自动处理)
+-----------------+
| 返回地址 |
| 参数n | <-- va_arg(args, type)访问位置
| ... |
| 参数2 |
| 参数1 |
| count参数 | <-- va_start后的args初始指向位置
+-----------------+
低地址2. 优缺点对比表
优点 缺点 1. 参数数量灵活 1. 无类型安全检查 2. 支持混合类型参数 2. 需要手动管理参数 3. 实现通用接口 3. 调试困难(无参数信息) 4. 兼容C标准库函数 4. 参数读取顺序敏感
#include <stdio.h>
#include <stdarg.h> //可变参数/*
可变参数:指的是参数的个数以及类型可以根据实际应用有所变化的c语言参数的入栈的顺序是从右往左进行的。
*/// 求传入的参数之和
double sum(int a, ...);
double sum(int a, ...)
{// 声名一个va_list list;// 2.读取参数列表,并给参数列表赋值a是 last最后一个读取到的va_start(list, a);// 3.取出参数double sum = 0;sum = sum + va_arg(list, int);sum = sum + va_arg(list, int);for (int i = 0; i < a - 2; i++){sum = sum + va_arg(list, double);}// 销毁释放listva_end(list);return sum;
}
int main(int argc, char const *argv[])
{printf("ddd\n");printf("%d\n", 4444);printf("%d,%lf\n", 4444, 2.22);/*参数列表的进入顺序是从右往左void fun(int a,)*/// 传入两个参数printf("%lf\n", sum(3, 2, 2, 2.2, 2.2, 2.2));return 0;
}
七 结构体尺寸
1. 内存对齐规则表
数据类型 典型尺寸(字节) 对齐要求(字节) 常见平台 char 1 1 所有 short 2 2 x86, ARM int 4 4 32/64位系统 float 4 4 多数平台 double 8 8 x86-64, ARM64 指针 4/8 4/8 32位/64位系统 结构体 不定 最大成员对齐值 依赖成员类型 2. 结构体布局优化表
结构体的大小收到存储变量地址对齐规则和编译器优化策略影响:
策略 优点 缺点 降序排列成员(大→小) 最小化填充空间 可能影响代码可读性 使用位域 精确控制位级存储 增加访问复杂度 #pragma pack(1) 完全紧凑布局 降低访问性能 人工填充 明确控制内存布局 需要手动计算 联合体嵌套 共享内存空间节省尺寸 需要严格管理使用状态
1.偏移量从0开始
2.整体结构体按照最大成员的对齐要求来对齐(通常是基本类型成员)
3.每个成员按照其自身类型的对齐规则对齐(通常是其类型大小)
4.结构体的整体大小必须是最大对齐数的整数倍(对齐规则)
5.编译器为了对齐,可能在成员之间插入填充字节(padiing偏移、补齐)
6.结构体末尾可能也会补(padiing偏移、补齐)
#include <stdio.h>
#include <stddef.h> //查看偏移量/*
结构体的大小收到存储变量地址对齐规则和编译器优化策略影响:1.偏移量从0开始2.整体结构体按照最大成员的对齐要求来对齐(通常是基本类型成员)3.每个成员按照其自身类型的对齐规则对齐(通常是其类型大小)4.结构体的整体大小必须是最大对齐数的整数倍(对齐规则)5.编译器为了对齐,可能在成员之间插入填充字节(padiing偏移、补齐)6.结构体末尾可能也会补(padiing偏移、补齐)*/
// 1.无嵌套的结构体
struct student
{int id; // 4字节long age; // 8字节int ranking; // 4字节char gender; // 1字节
};// 2.含有数组的结构体大小
struct student2
{// 如果下一个成员是数组,则不填充额外的字节int id; // 4字节char name[5]; // 5字节char address[5]; // 5字节long age; // 8字节int ranking; // 4字节char gender; // 1字节
};// 3.嵌套结构体的大小
struct student3
{int id; // 4字节struct{long a; // 8字节char c; // 1字节} my;int age; // 4字节int ranking; // 4字节char gender; // 1字节
};int main(int argc, char const *argv[])
{printf("struct student 结构体大小:%lu 字节\n", sizeof(struct student)); // 24printf("struct student id的偏移量=%lu\n", offsetof(struct student, id)); // 0printf("struct student age的偏移量=%lu\n", offsetof(struct student, age)); // 8printf("struct student ranking的偏移量=%lu\n", offsetof(struct student, ranking)); // 16printf("struct student gender的偏移量=%lu\n", offsetof(struct student, gender)); // 20printf("struct student2 结构体大小:%lu 字节\n", sizeof(struct student2)); // 32printf("struct student2 id的偏移量=%lu\n", offsetof(struct student2, id)); // 0printf("struct student2 name的偏移量=%lu\n", offsetof(struct student2, name)); // 4printf("struct student2 address的偏移量=%lu\n", offsetof(struct student2, address)); // 9printf("struct student2 age的偏移量=%lu\n", offsetof(struct student2, age)); // 16printf("struct student2 ranking的偏移量=%lu\n", offsetof(struct student2, ranking)); // 24printf("struct student2 gender的偏移量=%lu\n", offsetof(struct student2, gender)); // 28printf("struct student3 结构体大小:%lu 字节\n", sizeof(struct student3)); // 40printf("struct student3 id的偏移量=%lu\n", offsetof(struct student3, id)); // 0printf("struct student3 my的偏移量=%lu\n", offsetof(struct student3, my)); // 8printf("struct student3 age的偏移量=%lu\n", offsetof(struct student3, age)); // 24printf("struct student3 ranking的偏移量=%lu\n", offsetof(struct student3, ranking)); // 28printf("struct student3 gender的偏移量=%lu\n", offsetof(struct student3, gender)); // 32return 0;
}
2. 总结
即,想要求结构体的尺寸,首先要知道其最大字节的type,依据这个,来算出整个结构体的字节大小,以64位处理器来算,不满8的字节,系统自己填充为8,然后继续下个类型,数组除外,数组按照顺序进行排列。