聊到“函数”这两个字,我脑子里冒出来的不是教科书上那个“函数是描述输入与输出关系的映射”的定义,而是一连串真实的开发场景:JavaScript里那个让人又爱又恨的this、Python 里input()接收多个值时的报错、C 语言里scanf的const char*参数、Excel 里LEFT和VLOOKUP组合出来的诡异结果、甚至嵌入式板子上中断函数重复定义导致的编译失败。函数这种东西,几乎每一门语言都有,但每门语言对它的处理方式又都不一样,真正用起来全是细节。这篇东西不打算给你背一遍“函数定义”的教材,而是想把我这些年在实际项目里摸爬滚打总结出来的“函数相关知识”串一遍——从 JS 的函数对象、箭头函数、回调函数,到 Python 的嵌套函数、内置函数、再到 C/C++ 的底层系统调用、嵌入式中断函数、数据库和表格软件里的函数应用,最后还有机器学习里那些损失函数、激活函数。无论是刚入门的新手,还是在某个领域卡壳的老手,应该都能在里面找到一些能直接拿去用的东西。
1. 函数到底是什么:先解决“函数是对象吗”这个老问题
1.1 从数学函数到程序函数
我第一次写程序的时候,老师告诉我“函数就是一段封装好的代码,可以重复调用”,这个说法没错,但不够深。后来接触了数学里的函数,比如f(x) = x² + 1,才发现程序里的函数其实跟数学函数有本质区别:数学函数强调“映射关系”,输入一个值,输出一个确定的值;程序函数强调的是“行为封装”,它接收参数、执行一系列操作、然后返回结果,这个结果可以是确定的,也可以是不确定的,甚至它还可以不返回任何东西。
这个区别决定了我们看待函数的方式。写数学题的时候,f是一个静态的规则;写程序的时候,函数是活的,它有自己的生命周期、作用域、执行上下文,甚至在很多语言里函数本身就是一个值,可以被赋值、被传递、被返回。这就引出了编程领域最核心的一个概念——函数是一等公民。
1.2 JavaScript 里函数确实是对象
“js中函数是对象吗”这个问题被搜了很多次,答案是:是的,而且不只是“是对象”这么简单。在 JavaScript 里,函数是Function类型的对象,它有属性(比如name、length)、有方法(比如call、apply、bind),同时它又可以被调用。你可以像操作对象一样给函数挂属性:
function greet(name) { return 'Hello, ' + name; } greet.author = '小明'; console.log(greet.author); // 小明 console.log(greet.name); // greet console.log(greet.length); // 1,代表形参个数这种“函数即对象”的特性,让 JavaScript 可以玩出很多花活:函数可以作为另一个函数的参数(回调函数),也可以作为另一个函数的返回值(高阶函数)。理解不了这一点,后面看 React 函数组件、防抖节流、柯里化这些概念都会觉得云里雾里。很多人学 JS 学到一半卡住,往往就是卡在“函数到底是个啥”这个根上。
1.3 函数声明与函数表达式的区别
“函数声明”也是搜索词里的高频词,而且它在不同语言里的含义差别很大。在 C/C++ 里,函数声明通常指“在头文件里告诉编译器这个函数存在”:
// 声明 int add(int a, int b); // 定义 int add(int a, int b) { return a + b; }在 JavaScript 里,函数声明和函数表达式是两种写法,它们的执行时机完全不同:
// 函数声明:会被提升(hoisting),定义之前就能调用 console.log(add(2, 3)); // 5 function add(a, b) { return a + b; } // 函数表达式:不会被提升,必须在定义之后调用 console.log(sub(5, 2)); // TypeError: sub is not a function var sub = function(a, b) { return a - b; };这个细节经常在面试里出现,实际开发中也容易踩坑。比如代码里提前调用了后面才定义的事件处理函数,用函数声明就没事,用函数表达式就报错。我个人的习惯是:顶层逻辑用函数声明(代码结构清晰),需要动态创建函数时用函数表达式或箭头函数。
2. 函数的写法进化史:箭头函数、函数组件和嵌套函数
2.1 箭头函数的写法与 this 绑定
“箭头函数写法”是前端搜索的大热门。箭头函数() => {}最早是为了简化写法设计的,但它真正的精髓在于this的绑定规则。普通函数的this是在调用时决定的——谁调用了它,this就指向谁;箭头函数的this是在定义时决定的——它继承外层作用域的this。
const obj = { name: 'obj', normalFunc: function() { console.log(this.name); }, arrowFunc: () => { console.log(this.name); } }; obj.normalFunc(); // obj obj.arrowFunc(); // undefined,因为箭头函数的 this 指向定义时的外层(这里是全局)这个特性在写事件回调、定时器、异步请求的时候特别有用。以前用普通函数写:
btn.addEventListener('click', function() { const that = this; // 先把 this 存下来 setTimeout(function() { console.log(that); // 才能拿到按钮 }, 100); });现在直接:
btn.addEventListener('click', function() { setTimeout(() => { console.log(this); // 箭头函数继承外层 this,指向按钮 }, 100); });箭头函数还有几个细节需要注意:它没有自己的arguments对象,没有prototype,不能作为构造函数。所以需要动态this的场合,比如 Vue 的methods里需要绑定组件实例时,就别用箭头函数。
2.2 函数组件:为什么前端框架都在回归“函数”
“函数组件”这个词在 React 里已经被讲烂了,但它背后代表的一种思路很值得琢磨。早期 React 组件有两种写法:类组件和函数组件。类组件功能强,但写起来啰嗦,还要处理this绑定问题;函数组件简洁,但早期不能管理状态、不能使用生命周期。后来 Hooks 出现,函数组件可以管理useState、useEffect了,于是整个圈子迅速从类组件转向函数组件。
函数组件本质上就是一个“纯函数”的延伸——接收props,返回 UI 结构:
function Welcome(props) { return <h1>Hello, {props.name}</h1>; } // 箭头函数写法 const Welcome = ({ name }) => <h1>Hello, {name}</h1>;为什么函数组件能赢?因为“函数式思维”天然更贴近数据流:输入确定,输出就确定,没有隐藏的状态,没有“生命周期钩子”带来的心智负担。我个人从类组件切到函数组件之后,最大的感受是代码量少了接近一半,排查问题也更容易了,因为数据在组件内部的流动路径非常清晰。
2.3 Python 函数嵌套定义和嵌套调用的坑
“python函数嵌套定义和嵌套调用”也是高频搜索词。Python 允许在函数内部再定义函数,这是实现闭包(closure)的基础:
def outer(x): def inner(y): return x + y return inner add_five = outer(5) print(add_five(3)) # 8这里inner函数捕获了outer函数里的变量x,形成了闭包。这个技巧在装饰器、延迟计算、工厂函数里用得非常多。但嵌套函数有几个很容易踩的坑:
第一个坑:在内部函数里修改外部变量需要用nonlocal声明。
def counter(): count = 0 def add(): nonlocal count # 不加这个会报错:UnboundLocalError count += 1 return count return add c = counter() print(c()) # 1 print(c()) # 2第二个坑:闭包变量是延迟绑定的,循环里创建闭包容易出现“全是同一个值”的现象:
funcs = [] for i in range(3): def f(): return i funcs.append(f) print([func() for func in funcs]) # [2, 2, 2],不是 [0, 1, 2]解决办法是使用默认参数def f(i=i): return i,或者在 Python 3.8+ 里用海象运算符配合立即调用。这类问题遇到的频率比想象中高,写工厂函数、事件处理器时要特别注意。
3. 内置函数与纯函数:遇到问题先想“有没有现成的”
3.1 Python 内置函数里的冷知识:upper、abs、input、sqrt
“python中upper函数有什么用”这种问题看起来简单,但背后代表的是新手对内置函数库的不熟悉。upper()是字符串对象的方法,不是内置函数,它把字符串里的小写字母转成大写:
name = "hello world" print(name.upper()) # HELLO WORLD同样容易被忽略的还有abs()(绝对值)、len()(长度)、sum()(求和)、sorted()(排序)。但内置函数里真正容易踩坑的是input()。很多人想一次接收多个输入,直接写:
a, b = input("请输入两个数:") print(a, b)这行代码看着没毛病,但实际上如果输入"1 2",你得到的是a = "1",b = " ",完全不是预期的两个数。正确用法:
values = input("请输入两个数(用空格分隔):").split() a, b = map(float, values) # 先转换成浮点数 print(a, b)还有sqrt平方根函数,在 Python 里是math.sqrt或cmath.sqrt。前者只接受实数,负数的平方根会报ValueError;后者支持复数。记得有一次我在数值计算脚本里对一个负数取了根号,程序直接崩了,后来才发现要用cmath。
import math import cmath print(math.sqrt(4)) # 2.0 # print(math.sqrt(-4)) # ValueError: math domain error print(cmath.sqrt(-4)) # 2j3.2 纯函数:写函数最容易忽略却最重要的设计准则
“内置函数”搜的人很多,但我想借这个话题说说比“用什么内置函数”更重要的一件事:你的函数是不是纯函数。纯函数有两个特点:同样的输入永远得到同样的输出;函数执行过程中不产生副作用(不修改外部状态、不读写文件、不打印)。
为什么纯函数重要?因为它可测试、可缓存、可并行。React 函数组件受欢迎,本质上就是因为它是纯函数。写 Python 时也一样:
# 不纯:依赖全局变量 base = 10 def add_to_base(x): return x + base # 纯函数:输入输出明确 def add(x, y): return x + y这个理念虽然简单,但日常开发里真的能做到的人不多。我自己写函数时有个习惯:每写一个函数,先想三秒钟“这个函数能不能用单元测试测”,如果不能,就说明它可能不够“纯”,需要重构。
3.3 Excel 函数组合的坑:LEFT、VLOOKUP、REGEXEXTRACT
函数不只在编程语言里有,Excel 里的函数也是无数职场人天天打交道的东西。搜索词里“left函数和vlookup函数一起使用注意事项”特别典型。这两个函数单独用都很简单,但组合起来经常出问题。
LEFT(text, num_chars)是提取字符串左边指定数量的字符,返回的是文本类型。比如LEFT("ABC1234", 3)返回"ABC"——注意,这个结果看起来像文本,实际也是文本,哪怕里面全是数字,也是文本型数字。VLOOKUP(lookup_value, table_array, col_index_num, [range_lookup])查找时,如果第一列是数字,而你的查找值是文本型的“数字”,就会匹配不上。
一个真实场景:产品编号由字母和数字组成,你想提取中间的纯数字部分去VLOOKUP匹配数据库里的记录。直接用LEFT提取出来的是文本,数据库里的编号是数字,结果#N/A。解决办法是用VALUE()把文本转成数字,或者用--双重负号。
Excel 新版还推出了REGEXEXTRACT函数,这是正则表达式进入表格软件的标志:
=REGEXEXTRACT("订单号: ABC-123-XYZ", "[A-Z]+-\d+-[A-Z]+") ' 返回 ABC-123-XYZ这类函数在数据清洗中非常强大,以前要写 VBA 或 Python 才能干的事,现在一个公式搞定。不过要注意:REGEXEXTRACT目前只在支持动态数组的 Excel 365 版本里可用,老版本打不开文件会报错。
4. 回调函数与高阶函数:把函数当成数据来操作
4.1 回调函数:异步世界的灵魂
“回调函数”几乎是所有语言都绕不开的概念。回调的本质是:你定义一个函数,但不立刻调用它,而是把它交给另一个函数,让它在合适的时机去调用。就像你去餐厅订位,留了电话(回调函数),餐厅有位子了打电话通知你(调用回调函数)。
在 JavaScript 里最常见:
function fetchData(url, onSuccess, onError) { // 模拟异步请求 setTimeout(() => { if (url === 'https://api.example.com') { onSuccess({ data: 'hello' }); } else { onError(new Error('404')); } }, 1000); } fetchData( 'https://api.example.com', (data) => console.log('成功', data), (err) => console.log('失败', err) );回调函数的核心价值是解决异步执行顺序问题——主流程不用停下来等待,可以先干别的事,等结果出来再处理。但回调用多了就出现“回调地狱”,一层套一层,代码根本没法看。后来才出现了 Promise、async/await 这些方案,本质上都是在让回调的写法更舒服。
4.2 sort 函数用法:Java 和 JavaScript 都有讲究
sort函数也是搜索热词,因为排序太常用了,但默认排序几乎总是“不符合预期”。
JavaScript 的Array.sort()默认把元素转成字符串按字典序排,所以[10, 9, 100].sort()得到[10, 100, 9]。必须传比较函数:
const arr = [10, 9, 100]; arr.sort((a, b) => a - b); // [9, 10, 100]Java 的Arrays.sort和Collections.sort需要实现Comparator接口:
List<Integer> list = Arrays.asList(10, 9, 100); list.sort((a, b) -> a - b); // [9, 10, 100]这里有个特别容易出错的地方:比较函数的返回值设计是“负数表示 a 排在 b 前面,正数表示 b 排在 a 前面,0 表示相等”。JavaScript 里的a - b是升序,b - a是降序。但 Java 里如果要按多个字段排序,用Comparator.comparing(...).thenComparing(...)会更清晰。
我还踩过一个更隐蔽的坑:在 JavaScript 里用sort时,如果比较函数里做了字符串拼接过多的复杂操作,大数组排序会非常慢。所以给对象数组按某个数值字段排序时,尽量直接(a, b) => a.value - b.value,别在里面调函数做类型转换。
4.3 fun 函数的作用:术语背后的设计模式
“fun函数的作用”这个搜索词,大概率来源于某些教材或者伪代码。在很多编程语言里(比如 Kotlin),fun就是声明函数的关键字:
fun add(a: Int, b: Int): Int { return a + b }在 C/C++ 的教材里,有时会看到fun()这种命名,它就是function的缩写,没有特殊含义。真正值得研究的是“把函数当作参数传递”的设计模式。C 语言里用函数指针实现:
#include <stdio.h> int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int calc(int a, int b, int (*func)(int, int)) { return func(a, b); } int main() { printf("%d\n", calc(5, 3, add)); // 8 printf("%d\n", calc(5, 3, sub)); // 2 return 0; }这种模式在 C 语言的库函数(比如qsort)里很常见。理解了函数指针,再去理解 Python 里sorted(list, key=func)、JavaScript 里arr.map(func)就会非常顺——都是同一个思路:函数可以作为一个值传来传去。
5. 函数在底层:系统调用、零拷贝和函数劫持
5.1 select 函数:从 I/O 多路复用说起
“select函数”在编程领域有两个含义,一个是 SQL 查询语句,一个是我重点想讲的 I/O 多路复用函数。C 语言里select()是 Linux/Unix 下用于同时监控多个文件描述符(socket、文件、管道)状态的系统调用:
#include <sys/select.h> fd_set readfds; FD_ZERO(&readfds); FD_SET(sockfd, &readfds); struct timeval timeout; timeout.tv_sec = 5; timeout.tv_usec = 0; int ready = select(sockfd + 1, &readfds, NULL, NULL, &timeout); if (ready > 0 && FD_ISSET(sockfd, &readfds)) { // 有数据可读 }这个函数的优点是跨平台、简单,缺点是性能上限低——每次调用都要把整个 fd_set 从用户态拷贝到内核态,而且 fd 数量有上限。现在的网络编程新项目一般用epoll(Linux)或kqueue(macOS)替代,但很多老项目、嵌入式环境还在用select。我遇到过的一个真实案例是:一个服务同时监听几百个长连接,用select每次循环都全量遍历,CPU 飙到 80%,换成epoll之后降到个位数。
5.2 sendfile 和 splice:零拷贝到底省了什么
“利用sendfile和splice函数实现零拷贝文件传送”是又一个高频搜索词。要理解零拷贝,得先知道传统文件传输流程:硬盘数据 → 内核缓冲区 → 用户态缓冲区 → 内核 Socket 缓冲区 → 网卡。中间至少经历 4 次上下文切换、4 次数据拷贝。零拷贝的思路就是:数据不经过用户态,直接在内核态从文件搬运到网络。
sendfile的典型用法:
#include <sys/sendfile.h> int sendfile(int out_fd, int in_fd, off_t *offset, size_t count); // 把 in_fd 指向的文件内容直接发送到 out_fd(网络 socket)splice更底层,它在两个文件描述符之间移动数据,不需要数据落进用户空间:
#include <fcntl.h> int splice(int fd_in, loff_t *off_in, int fd_out, loff_t *off_out, size_t len, unsigned int flags);我实测过的场景是:一个文件代理服务,用普通read+write转发 1GB 文件耗时约 8 秒,改用sendfile后降到 3 秒,CPU 占用下降了三分之一。做高性能静态文件服务器的时候,这个优化属于性价比最高的那类。
5.3 LD_PRELOAD 劫持:编译一个假的函数会发生什么
“ld_preload 劫持:编译一个假的 ptrace 函数”是个很专业的搜索词。LD_PRELOAD是 Linux 动态链接器的一个环境变量,指定在程序启动时预先加载某个共享库。因为动态链接时符号解析有优先级,LD_PRELOAD里定义的符号会覆盖掉系统库的同名符号。
一个简单的“劫持”示例——写一个假的rand函数:
// fake_rand.c #include <stdio.h> #include <stdlib.h> int rand(void) { return 42; // 所有随机数都是 42 }编译成共享库,然后设置环境变量:
gcc -shared -fPIC fake_rand.c -o fake_rand.so LD_PRELOAD=./fake_rand.so ./your_program这看起来有点“黑魔法”,但它本质上是函数拦截技术,常用于调试、性能分析、兼容层实现。搜索词里说的 ptrace 函数也是一样的道理——通过替换ptrace函数,可以在不修改目标程序源码的情况下改变其行为。这个技术的边界是:它依赖动态链接,静态链接的程序劫持不了;有 LD_PRELOAD 权限限制的系统也加载不了。写这个不是让大家去搞对抗,而是理解动态链接的机制,调试一些诡异问题时会非常有用。
5.4 kernel_read 函数和内核视角的函数
kernel_read是 Linux 内核里的一个函数,用于从文件读取数据。用户态用read()系统调用,进入内核后最终会调用一系列内核函数,kernel_read就是其中之一:
#include <linux/fs.h> int kernel_read(struct file *file, loff_t offset, char *addr, size_t count, loff_t *pos);它常被用于内核模块中读取配置文件、读取固件等。学习驱动开发的人会遇到这个函数。这里想提醒的是:用户态的函数和内核态的函数是两个世界,用户态的函数错误最多导致进程 crash,内核态的函数错误可能导致整个系统崩溃。写内核代码时,函数指针、并发安全、内存分配都要格外小心。
6. 嵌入式与硬件场景:中断函数、打印函数和编译器的坑
6.1 CH32V 在 GCC 编译器下定义中断函数
“ch32v在gcc编译器下定义中断函数”这个搜索词说明现在用 RISC-V 芯片做开发的人越来越多了。CH32V 是沁恒的 RISC-V 内核 MCU,在 GCC 环境下定义中断函数并不像 Keil 的__interrupt那么简单,关键要处理中断向量表和函数命名。
在 RISC-V GCC 环境下,通常的做法是使用__attribute__((interrupt()))属性声明中断函数,并把函数名放到向量表的对应位置。比如用沁恒官方 SDK,定时器中断:
void TIM2_IRQHandler(void) __attribute__((interrupt("machine"))); void TIM2_IRQHandler(void) { if (TIM2->INTFR & TIM2_INT_UP) { TIM2->INTFR = ~TIM2_INT_UP; // 清中断标志 // 处理逻辑 } }如果你用的是 OpenOCD 和 GCC 直接操作,还需要检查链接脚本(.ld文件)里的中断向量表是否正确。我最初踩过的坑就是:函数定义了,中断也使能了,但就是进不了中断函数,后来发现是链接脚本里的向量表偏移和 Bootloader 不对齐,中断向量被引导到了错误位置。
另外,同一个函数名被定义两次会导致“重复定义”错误。“lvgl 打印函数 重复定义”这个问题特别典型——LVGL 移植到某个平台时,官方会提供弱符号_lv_log_print或要求用户自定义打印函数,如果项目里有两个文件都定义了printf重映射,就会冲突。解决办法是只保留一个输出实现,或者使用__attribute__((weak))弱符号机制。
6.2 C 语言函数的细节坑:sizeof、scanf、exit、flush
“sizeof函数需要头文件”这个搜索词很有意思,因为sizeof根本不是一个函数,它是一个运算符。之所以有人觉得需要头文件,是因为在某些编译器里sizeof的结果要配合size_t类型使用,而size_t定义在<stddef.h>、<stdio.h>或<stdlib.h>里,所以不包含这些头文件时可能报类型未定义。
scanf的签名是int scanf( const char *format [, argument]... );,它第一个参数是const char *,后面的参数必须是“指针”,因为scanf需要把读取到的值写回你的变量:
int num; scanf("%d", &num); // 必须加 &,取变量地址经常有新手漏掉&,程序不报错但不工作,特别难排查。exit(0)是stdlib.h里的函数,调用后会立即终止进程,它和return的区别是:return只退出当前函数,exit退出整个进程。flush函数则是刷新缓冲区的,C 语言里fflush(stdout)用于强制把输出缓冲区的内容写到终端。
6.3 CLion 无法跳转到函数定义处的原因排查
“clion无法跳转到函数定义处”是很多用 JetBrains 系 IDE 写 C/C++ 的人遇到的通病。CLion 的代码索引依赖 CMake 的编译信息,如果函数定义能编译但是跳转不了,通常是这几个原因:
- CMakeLists.txt 里没有把源文件加进项目。CLion 能打开单文件看,但索引不会覆盖所有文件,检查
add_executable或add_library是否包含了目标文件。 - 启用了系统头文件的高亮,但索引没加载。可以在
File | Reload CMake Project里重载一次。 - 文件用了软链接或用外部路径引用,CLion 的索引可能不追踪。解决办法是给文件加入项目路径,或者用
File | New CMake Project from Sources重新生成项目。 - 函数定义在宏或者模板展开里,静态索引根本识别不出来,只能搜索文本。
这类问题不算编程错误,但特别打击开发效率,养成“写代码前先把 CMakeLists 配好”的习惯能省很多时间。
6.4 invalidate 函数:UI 刷新的隐形关键
“invalidate函数”在 GUI 开发领域非常关键,Windows 编程里有InvalidateRect、Qt 里有update()、LVGL 里有lv_obj_invalidate。这个函数的作用是标记某个区域为“需要重绘”。它不会立刻重绘,而是把重绘请求放到事件队列里,等事件循环空闲时再处理。
我踩过的实际坑是:在嵌入式 GUI 上频繁调用invalidate导致刷新闪烁,后来通过只刷新变化区域、控制刷新频率解决了。理解这个函数的关键在于:UI 显示本质是“数据变化 → 触发失效 → 系统重绘”,如果你改了数据但忘了调用对应的 invalidate 函数,界面就保持旧样子;反过来,调用太频繁又浪费时间,影响帧率。这个度要自己根据业务来调。
7. 特定领域里的函数:从机器学习到信号处理
7.1 YOLO 损失函数和 Word2Vec 里的数学函数
“yolo损失函数”和“word2vec中的数学函数”搜索词告诉我们,算法工程师写代码时,“函数”往往不再是代码函数,而是数学意义上的函数。YOLO 系列目标检测算法里,损失函数是训练的核心:
Loss = 分类损失 + 定位损失 + 置信度损失分类损失通常用二元交叉熵(BCE),定位损失用 CIoU,置信度用 BCE。这里面每个子项都是一个个函数。理解损失函数的形状,能直接指导训练策略:比如定位损失的权重调高,模型会更多关注框的位置精度。调参不是玄学,本质上就是在调整这些函数之间的比例。
Word2Vec 里的“数学函数”则主要是两个:softmax(归一化指数函数)和sigmoid(S 形函数)。
softmax(x_i) = exp(x_i) / Σ exp(x_j) sigmoid(x) = 1 / (1 + exp(-x))这两个函数把任意实数映射到概率空间。Word2Vec 的 Skip-gram 模型用 softmax 计算中心词预测上下文词的概率,负采样则用 sigmoid 判断词对是否相关。我自己学这部分时最大的顿悟是:这些函数不是“模型参数”,而是“概率的翻译器”,它们让网络的输出变成可以训练的概率分布。
7.2 相位噪声的互相关函数:射频测量里的函数思维
“相位噪声”和“相位噪声的互相关函数”是射频测试领域很专业的内容。相位噪声是衡量振荡器短期频率稳定度的指标。在测量时,传统单通道方法测出来的相位噪声包含参考源本身的噪声,如果参考源不够好,测出来的就不是被测件真实性能。互相关方法用两套独立的参考源和相位检测器同时测量同一个信号,然后把两路结果做互相关运算——共同的信号成分被保留,独立的随机噪声被平均掉。
互相关函数的数学表达是:
R_xy(τ) = E[x(t) · y(t - τ)]射频领域说“互相关”就是把两路采样数据做相关运算,起到降噪作用。这个知识点给非射频工程师的启发是:用相关函数可以在强噪声背景下提取微弱信号,同样的思路也用于语音处理、地震信号分析等领域。
7.3 从海洋波浪拟合到期货指标:别让“未来函数”骗了你
“海洋水文波浪拟合函数”和“期货交易日内高低点波段源码指标无未来函数”两个搜索词放一起看特别有意思。前者是科学计算里的曲线拟合,通常用三角函数叠加拟合波面,比如用若干个不同频率、振幅、相位的正弦波叠加来近似实际波浪:
import numpy as np from scipy.optimize import curve_fit def fit_function(t, a1, f1, p1, a2, f2, p2): return (a1 * np.sin(2 * np.pi * f1 * t + p1) + a2 * np.sin(2 * np.pi * f2 * t + p2))后者“无未来函数”则是技术分析指标里的一个极其重要的概念:一个指标如果使用了当前 K 线之后的数据,它就是一个带未来函数的指标,用它复盘时信号完美,实盘时却会漂移和重绘。比如某些高低点探测公式,在高点还没走完时就已经把“未来”的高点标出来了,等行情继续走,信号又跟着变。这类指标在实盘里几乎必然亏钱,因为信号是“事后诸葛”。判断未来函数最简单的方法:把 K 线倒过来看,如果指标值仍然合理,那基本没有未来函数;如果倒序后图形完全变样,就要警惕了。
7.4 MySQL 函数大全与 JSON 查询:数据库里的函数修行
“mysql函数大全及举例”这个搜索词说明大家在数据库里也需要函数。MySQL 的函数分几大类:聚合函数(COUNT、SUM、AVG)、字符串函数(CONCAT、SUBSTRING)、日期函数(NOW()、DATE_FORMAT)、条件函数(IF、CASE WHEN),以及加密函数(MD5、AES_ENCRYPT)。
这里我想重点提一下“sql md5加密函数”的坑。MD5()在 MySQL 里是一个简单易用的哈希函数:
SELECT MD5('password123');但它有两个大问题:MD5 本身已经不安全,彩虹表满天飞;直接调用MD5()不给加盐,等于把用户的密码当明文存。现在做用户密码存储,至少要用SHA2(password, 256)加随机盐,或者直接交给后端的专门的密码哈希库处理。数据库函数方便归方便,安全边界要清醒。
“json查询函数”也是高频词。MySQL 5.7+ 支持 JSON 类型,提供了JSON_EXTRACT、JSON_SET、JSON_CONTAINS等函数:
SELECT JSON_EXTRACT('{"name": "张三", "age": 30}', '$.name'); -- 输出 "张三"这个函数在存储半结构化数据时非常有用,但要注意:JSON 函数处理起来比普通字段慢,不要在大表上直接用,能拆成普通字段就拆。
8. 函数报错现场:那些让人血压升高的常见问题
8.1 pip 无法识别:这不是函数问题是环境问题
“pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称”是 Windows PowerShell 下最经典的错误之一。这个提示的意思是系统在当前的环境变量 Path 里找不到 pip 这个可执行文件。很多人会误以为“pip 这个函数坏了”,其实不是。
解决办法通常是:
- 打开 Python 安装目录下的
Scripts文件夹,确认里面有没有pip.exe。 - 把这个路径加入系统环境变量
Path。 - 重新打开终端,执行
pip --version。
还有种情况是电脑上装了多个 Python 版本,pip 对应错版本了。这时候用python -m pip代替pip更稳妥:
python -m pip install requests这个命令明确告诉 Python:“你去调用自己的 pip 模块”,不会因为环境变量混乱而找错对象。
8.2 VMware 报错:部分链上无法执行所调用的函数
“vmware workstation 在部分链上无法执行所调用的函数。请打开父虚拟磁盘。”这个错误出现的原因是虚拟磁盘的快照链接坏了。VMware 的虚拟磁盘文件(.vmdk)可以基于父盘做增量,父盘被打断或者被移动位置后,子盘找不到父盘,就会报这个错。
常规处理思路:
- 先确认父盘是否被重命名或移动过,找回原来的路径。
- 用 VMware 官方工具
vmware-vdiskmanager对虚拟磁盘做一致性检查:
vmware-vdiskmanager -R "虚拟机路径\disk.vmdk"- 如果确认父盘损坏,可以把增量盘的内容“摊平”到新盘:
vmware-vdiskmanager -r "子盘.vmdk" -t 0 "新合并盘.vmdk"我自己遇到这个错误时最深刻的体会是:不要随便移动或删除 vmware 虚拟机的父快照文件,快照链就像链表,断了就不好续。
8.3 函数接口对不上:scanf、solvePnP 和 C++ 返回字符串
很多函数报错的本质是“接口没对上”。scanf的典型错误是不加&;OpenCV 的solvePnP则是一堆参数类型和坐标系的坑。solvePnP函数的签名是:
bool solvePnP(InputArray objectPoints, InputArray imagePoints, InputArray cameraMatrix, InputArray distCoeffs, OutputArray rvec, OutputArray tvec, bool useExtrinsicGuess = false, int flags = SOLVEPNP_ITERATIVE);objectPoints是世界坐标系下的三维点,imagePoints是图像上的二维像素点,顺序必须一一对应。特别容易踩的坑是:imagePoints里点的数据类型必须是float32或float64,如果传了整数类型,函数会报Assertion failed。还有相机内参矩阵cameraMatrix一定要基于标定得到,随手填一个内参会让你解出来的 rvec/tvec 完全不对。
C++ 函数返回字符串也是个经典话题。如果返回局部变量的指针,函数执行完内存就释放了,产生悬垂指针:
const char* bad() { char str[] = "hello"; return str; // 悬垂指针,行为未定义 }正确做法有三种:返回std::string(值语义,最推荐);返回char*但指向静态存储区;或者在堆上分配并让调用者释放。现代 C++ 直接用std::string就好:
std::string good() { return "hello"; }9. 关于函数的几个学习建议
聊了这么多函数的实际用法和坑,最后说几句我怎么学“函数”这个东西的心得。
函数相关的知识点散落在不同的语言和领域里,但它们的内核是相通的:入参、出参、副作用、生命周期、执行上下文。你在 JavaScript 里理解了“函数是一等公民”,学 Python 闭包时就更容易;你在 C 语言里理解了“函数指针”,看 Go 的func type、Rust 的fn、Rust 的闭包就会觉得似曾相识。真正的高手不是背住了每种语言的函数语法,而是掌握了一套“函数思维”的框架:先想输入输出,再想副作用,最后想调用时机和上下文。
我写代码这些年,一个特别深的体会是:函数不是拿来炫技的,它是拿来管理复杂度的。把一个大模块拆成若干个小函数,每个函数只干一件事,可读性、可测试性、可维护性都会直线上升。很多项目后期改 bug 特别痛苦,往往不是算法多难,而是函数写得又长又乱、副作用满天飞。所以,无论你正在学哪种语言,把“函数设计”当成一门正经手艺来练,绝对值得。
最后分享一个我常用的“函数自查清单”:写完之后问自己三个问题——这个函数的功能是否单一?它的名字是否让别人一眼看懂?如果输入完全一样的参数,它是否总能给出一致的结果?能把这三个问题回答好,写出来的函数基本就没什么大问题了。