目录
前言
前面呢~, 我们说到了C语言中的分支语句和循环语句, 了解到了程序的执行过程, 这一部分呢~, 我们就来说说C语言中的函数, 函数是C语言的灵魂(和指针一样),它不仅是代码复用的基本单元,更是构建复杂系统的基石。本文将带您全面探索C语言函数的精髓,揭示高效函数设计的奥秘~~。
函数
认识函数
什么是函数呢?, 我们可以想一想我们第一次听到函数这个名词是在什么时候呢? 我想应该是在我们初中的数学课上吧~~, y = kx + b, f(x) = kx + b, 我们可以把函数想想成一个工厂, 或者一个机器人, 我们将我们的原料给到这个工厂, 这个工厂生产出来我们想要的东西, 比如前面的f(x) = kx + b, 我们给到这个函数一个x他就会给我们一个y~, 但是我们又不需要知道函数内部干了什么?
再比如, 我们之前使用到的 printf() 函数, 我们在其中传入了 "Hello World!\n", 这个参数, 显示器就帮助我们打印了 "Hello World!\n" 字符串, 然我们却不知道他内部到底干了什么? 所以在学习语言的初期, 我们只需要知道函数的功能是什么? 我们如何去调用他就可以了.
C语言中的函数
在C语言中呢, 有库函数和自定义函数, 库函数就像我们之前使用的printf()函数, 以后会说到的memset(), memcpy() 函数一样, 自定义函数呢, 则需要我们自己的实现, 他类似于我们最开始写的main() 函数一般, 其实main()函数就是我们自己定义的函数, 程序在执行起来之后, 会有别的函数来调用main()函数.
C语言中的函数呢, 有函数的声明, 函数的定义, 还有最终函数的调用, 函数本身又有函数的返回值, 函数的名称, 以及函数的参数列表. 我们以一个求和的函数为例
函数的声明
函数的声明方式 : 函数的返回值类型 函数名(函数的参数列表) ;

这里要注意的是: 在函数的声明后面是有 ; 的
函数的定义
函数的声明方式 :函数的返回值类型 函数名(函数的参数列表) {
// 函数的具体实现 ...
}

函数的调用
main函数作为主调函数, sum作为被调函数

函数的嵌套访问
C语言中的函数是可以嵌套访问的, 就是一个函数的返回值作为另一个函数的参数, 比如:
sum()函数的返回值3, 作为printf()函数的参数.

这里要注意的是: 必须函数是有返回值, 才可以嵌套访问, 如果没有返回值, 是不能作为另一个函数的参数的. 并且C语言中, 函数可以嵌套访问, 但是不可以嵌套定义, 函数的定义必须是在函数的外面.

这样是不可以的. 但是可以在函数中声明函数, 比如:

函数的参数传递
C语言中函数的参数传递, 有这么几个, 值传递, 地址传递, 数组传递, 结构体(自定义类型)传递
前面的sum就是值传递, 如果参数值指针类型就是地址传递, 这里一个C语言中十分经典的函数, 交换两个数的值就必须使用地址传递
void swap(int *e1, int (e2)
{
int tmp = *e1;
*e1 = *e2;
*e2 = tmp;
}
这里是为什么呢? 在C语言中 形参是实参的一份临时拷贝, 所以修改形参是不会改变实参的, 本质是因为他们的地址空间不同~
数组在传递的过程中, 不会传递整个数组, 只会传递数组首元素的地址过来, 这样就快捷很多, 包括结构体传递, 我们最好也是使用指针传递, 因为我们不知道这个结构体是有多大, 如果想内置类型那样的值传递, 如果说结构体小了还行, 大了的话就很浪费性能了.
函数指针
指针变量其中有一个函数指针, 就是指向函数的指针, 函数名其本身也是一个地址.
// 普通声明
int (*operation)(int, int);
// typedef创建类型别名
typedef int (*ArithFunc)(int, int);
函数的递归
函数递归(Function Recursion)是编程中一种强大的技术,指一个函数在其定义中直接或间接地调用自身。它提供了一种优雅的方式来解决可以被分解为多个结构相同但规模更小的子问题的问题。
核心思想
自相似性: 问题可以分解为与自身结构相同、但规模更小的子问题。
基线条件: 存在一个或多个最简单、最小规模的情况(称为基线条件或递归终止条件),这些情况可以直接求解,不再需要递归调用。
递归步骤: 对于非基线条件的情况,函数调用自身来处理更小的子问题,并将这些子问题的结果组合起来得到当前问题的解。
函数递归, 是不能递归的太深的, 因为函数调用是有栈帧的开销的, 如果一致递归, 就会造成栈溢出(Stack Overflow), 海外还有著名的程序员网站就叫这个~, 类似于国内的优快云.
unsigned long fib_recursive(unsigned int n) {
if (n <= 1) return n;
return fib_recursive(n-1) + fib_recursive(n-2);
}
函数栈帧的创建与销毁(番外)
在调用函数的时候呢~, 主调函数要给被调函数创建并销毁函数栈帧, 这里主要想说明的是, 函数的调用是存在系统开销的, 但是当代的操作系统对函数调用的开销已经可以降到很低了, 但是我认为还是很有必要说一下这一部分的~
我们在VS2019 IDE中, 编写代码, 启动调试, 使用F11, 反汇编, 逐语句调试, 我们可以看到下状况(不会汇编没有关系, 这里主要是想让大家知道函数调用时候开销的), 我们能看到主调函数函数下一行代码的地址被压入函数栈帧中, 一些函数参数的传递, 以及函数返回时候返回到主调函数最开始压入的下一行代码的地址处.


栈溢出这也这里的原因, 因为要给函数的栈空间开辟一块不小的空间, 所以如果函数一直递归就会曹正溢出的问题
小结
在C语言中优秀的函数设计遵循核心原则:
原子性:单一函数完成单一任务
可预测性:相同输入产生相同输出
防御性:验证所有输入,处理所有错误
高效性:最小化拷贝,合理使用指针
文档化:注释说明前置条件、后置条件
/**
* 计算数组平均值 - 函数文档示例
* @param arr 整型数组
* @param size 数组大小
* @return 平均值(double)
* @pre size > 0
* @post 返回值为所有元素的算术平均
*/
double array_average(const int *arr, size_t size) {
assert(size > 0 && "空数组无平均值");
long sum = 0; // 防止大数溢出
for(size_t i=0; i<size; i++) {
sum += arr[i];
}
return (double)sum / size;
}
希望大家读完这个之后, 小萌新都能写出非常优雅的函数~~
2270

被折叠的 条评论
为什么被折叠?



