这一章在干嘛?
第 3、6 章你已经会写「参数个数固定」的普通函数了。这一章把函数玩出花:参数个数可变(varargin/varargout)、函数里套函数(嵌套函数)、一行函数不用建文件(匿名函数)、函数当参数传(函数句柄 + fzero/integral 等实战)、以及函数调用自己(递归)。其中「函数句柄」是 MATLAB 最有特色的概念之一 —— 如果你写过 C 的函数指针,会觉得似曾相识。学完这章,你才算真正摸到 MATLAB「函数是一等公民」的门槛。
10.1 可变数量的参数:varargin 与 varargout
10.1 可变数量的参数:varargin 与 varargout
以前写的函数参数个数都是死的,比如 areacirc(rad) 一进一出。但你看 MATLAB 内置函数:plot 想传几组数据就传几组,max 要么返回最大值要么连位置一起返回 —— 它们怎么做到的?答案是两个内置元胞数组:
varargin(variable input arguments):装多余输入参数的元胞数组;varargout:装额外输出参数的元胞数组。
为什么必须是元胞数组?因为参数可能是任意类型,只有 cell 能混装。配套两个侦探工具:nargin 返回实际收到的输入参数个数,nargout 返回调用方期望的输出参数个数。
10.1.1 可变输入参数
求圆面积的函数:默认半径单位是英尺;若传了第二个参数 'i' 则按英寸算(1 英尺 = 12 英寸)。写法一是全部参数塞进 varargin:
% areafori.m
function area = areafori(varargin)
% areafori(radius) 或 areafori(radius, 'i')
n = nargin; % 实际传入的参数个数
radius = varargin{1}; % 注意:元胞数组用花括号取元素!
if n == 2
unit = varargin{2};
if unit == 'i'
radius = radius * 12;
end
end
area = pi * radius.^2;
end>> areafori(3)
ans =
28.2743
>> areafori(1,'i')
ans =
452.3893写法二更推荐:确定会传的参数写成普通形参,只有不定部分留给 varargin:
% areafori2.m
function area = areafori2(radius, varargin)
if nargin == 2
if varargin{1} == 'i'
radius = radius * 12;
end
end
area = pi * radius.^2;
end常见坑:nargin 不是「varargin 里的个数」
nargin返回的是全部输入参数的个数。在areafori2(radius, varargin)里传了(3,'i')时,nargin是 2,而varargin里只有 1 个元素(varargin{1}是'i')。两者对不上号是新手最常见的逻辑错误来源。
10.1.2 可变输出参数
输出端同理。下面的 typesize 总是返回一个类型字符(标量 's' / 向量 'v' / 矩阵 'm'),是向量就额外返回长度,是矩阵就额外返回行数和列数:
% typesize.m
function [arrtype, varargout] = typesize(inputval)
[r, c] = size(inputval);
if r==1 && c==1
arrtype = 's';
elseif r==1 || c==1
arrtype = 'v';
varargout{1} = length(inputval);
else
arrtype = 'm';
varargout{1} = r;
varargout{2} = c;
end
end>> [t, len] = typesize(4:6)
t =
v
len =
3
>> [t, r, c] = typesize([4:6;3:5])
t =
m
r =
2
c =
3问题来了:调用者必须自己知道该接几个变量,接多了直接报错。更优雅的做法是在函数内部用 nargout 探测调用方要几个:
% mysize.m —— 仿内置 size:默认返回行、列;若调用方接 3 个变量,再返回元素总数
function [row, col, varargout] = mysize(mat)
[row, col] = size(mat);
if nargout == 3
varargout{1} = row * col;
end
end
>> [r, c] = mysize(eye(3)) % nargout==2,只算行列
>> [r, c, n] = mysize(eye(3)) % nargout==3,多给一个元素总数
n =
9和 C 语言对照
C 的
printf用...可变参数 +va_list解析,思路一样但全靠约定和手工;MATLAB 的 varargin 直接把参数打包成元胞数组,取用更安全。nargout则是 C 里根本没有的能力 —— C 函数无从知道调用方拿不拿返回值,而 MATLAB 可以据此「按需输出」,这是设计接口函数的大杀器。
再来一个把 nargout 用足的完整例子:输入摄氏温度,调用方接 1 个变量就只转华氏度,接 2 个就顺带转开尔文(,):
% converttemp.m
function [degreesF, varargout] = converttemp(degreesC)
degreesF = 9/5 * degreesC + 32;
if nargout == 2
varargout{1} = degreesC + 273.15;
end
end
>> df = converttemp(17)
df =
62.6000
>> [df, dk] = converttemp(17)
df =
62.6000
dk =
290.1500注意细节:摄氏转华氏无条件执行(第一个输出永远存在),开尔文只在 nargout == 2 时才算。反过来,如果调用者期望 2 个输出而函数没给满,MATLAB 会直接报错 —— 所以「必给的参数写进函数头,额外输出交给 varargout + nargout」这条分工线要划清楚。另外配套还有 narginchk/nargoutchk 两个函数,用来校验参数个数是否在合法范围内,写正式接口时值得一用。
10.2 嵌套函数
和循环可以嵌套一样,函数体里也可以再定义函数:外层叫外部函数(outer function),里面的叫内部函数(inner function)。规则只有两条硬性要求:每个函数都必须有 end(和嵌套循环一样层层闭合)。
% nestedvolume.m —— 长方体体积 = 底面积 × 高
function outvol = nestedvolume(len, wid, ht)
outvol = base * ht; % 调用嵌套函数
function outbase = base
% 直接使用外层的 len、wid,无需传参!
outbase = len * wid;
end
end
>> v = nestedvolume(3,5,7)
v =
105最值得注意的就是:base 没有形参,却直接用了外层的 len 和 wid。这是嵌套函数的核心特性 —— 变量作用域属于最外层函数:外层定义的变量,内层直接可见(相当于隐式共享,省去了传参)。
反过来有几条边界要注意:
- 输出参数的作用域只在内层函数里。
outbase在base内有效,外层想打印它是不行的。 - 内层函数定义的变量若外层没用到,作用域就只在内层。
- 嵌套函数在后面 GUI 编程章节会大量出现(回调函数共享界面数据非常方便)。
顺带一提:子函数(subfunction)
同一个 .m 文件里还可以放多个平级的函数 —— 第一个叫主函数,后面的叫子函数,只有本文件内可以调用。子函数之间不共享变量、必须老老实实传参;嵌套函数才享受「免传参共享作用域」的特权,两者别混淆。看一个对比(圆柱计算,主函数调子函数算底面积):
% cylcalcs.m —— 一个文件两个平级函数
function [area, vol] = cylcalcs(rad, ht)
area = basearea(rad); % 调子函数:必须显式传 rad
vol = area * ht;
end
function a = basearea(r) % 子函数:文件外不可见
a = pi * r.^2;
end如果把 basearea 写成嵌套函数(挪进 cylcalcs 函数体内部),rad 就可以免传直接用。选择标准:辅助逻辑不需要共享外层变量时用子函数(依赖关系一目了然),需要频繁共享中间变量(典型如 GUI 回调)时用嵌套函数。
10.3 匿名函数与函数句柄
匿名函数(anonymous function)是不用建 .m 文件的一行式函数。语法:
句柄变量 = @(参数列表) 函数体@ 是函数句柄运算符(function handle operator),返回的「句柄」本质上是一个指向函数的引用值 —— 用 C 的话说就是「函数指针变量」:
>> cirarea = @(radius) pi * radius.^2; % .^ 让它支持向量输入
>> cirarea(4)
ans =
50.2655
>> cirarea(1:4) % 向量化,一次算四个半径
ans =
3.1416 12.5664 28.2743 50.2655
>> class(cirarea)
ans =
function_handle几个关键行为:
- 没有参数也必须带括号。定义写
@(),调用写prtran()。只敲句柄名只会把函数定义显示出来(相当于解引用看内容),不会执行:
>> prtran = @() fprintf('%.2f\n',rand);
>> prtran()
0.95- 句柄是普通变量,可以
save进 MAT 文件反复使用 —— 把常用的换算函数攒成一个tempconverters.mat,每次开 MATLABload一下就是你的个人工具箱:
>> cirarea = @(radius) pi * radius.^2;
>> save anonfns cirarea
>> clear
>> load anonfns
>> cirarea
cirarea =
@(radius)pi*radius.^2还有一个匿名函数的高级用法值得一提:定义时「冻结」当前变量的值。句柄记下的不只是表达式,还有定义那一刻它引用到的变量值:
>> k = 3;
>> addk = @(x) x + k; % k 的当前值 3 被捕获
>> addk(5)
ans =
8
>> k = 100; % 之后再改 k
>> addk(5) % 匿名函数里的 k 还是 3!
ans =
8这既是有用的特性(做「参数化模板」),也是隐蔽的坑(指望改外部变量来改函数行为是不行的)。需要动态改参数,就把它作为匿名函数的显式参数传进去。
什么时候用匿名函数?
教材的风格准则:函数体只是一条简单表达式时,就用匿名函数。它省掉一个文件、定义即使用,特别适合做被传入其他函数的「一次性小函数」(下一节就见分晓)。逻辑超过一行、需要分支循环的,老老实实写普通函数。
10.4 函数句柄的用法:函数函数与实战
@ 不仅能造匿名函数,还能给已有函数取句柄(内置函数和自定义函数都行):
>> facth = @factorial; % 给内置阶乘函数取句柄
>> facth(5)
ans =
120只用来代替函数名调用没什么意思。句柄真正的价值是 —— 把函数当参数传给另一个函数。这类「接收函数为输入」的函数叫函数函数(function functions)。
10.4.1 把函数传进函数
下面这个函数把传入的函数在 x = 1:.25:6 上求值并画图:
% fnfnexamp.m
function fnfnexamp(funh)
x = 1:.25:6;
y = funh(x); % 像调用普通函数一样调用句柄
plot(x,y,'ko')
xlabel('x'); ylabel('fn(x)')
title(func2str(funh)) % func2str:句柄 → 字符串,用作标题
end注意传法:直接传函数名字会报错,必须传句柄:
>> fnfnexamp(sin) % ✗ 报错:Not enough input arguments.
>> fnfnexamp(@sin) % ✓ 传入内置函数的句柄
>> fnfnexamp(cirarea) % ✓ 句柄已在变量里,直接传变量名(别再加 @)配套的一对转换函数值得记住:func2str 把句柄转成字符串(常用在标题里),str2func 把字符串转回句柄(适合让用户输入函数名再执行)。

10.4.2 内置函数函数实战
MATLAB 自带一大票函数函数,最常用的几个:
>> fplot(@sin, [-pi, pi]) % 自动采样画曲线,无需手造 x/y 向量
>> feval(@sin, 3.2) % feval:执行句柄,等价于 sin(3.2)
ans =
-0.0584
>> fzero(@cos, 4) % fzero:在 4 附近找零点
ans =
4.7124匿名函数同样可以传进去(句柄已在变量里,不再加 @):
>> f = @(x) 3*x.^2 - 2*x + 5;
>> fplot(f, [-6, 6]) % 一行画出二次函数
>> fzero(f, 1) % 还能找它的根
>> integral(f, 0, 1) % 数值积分也吃句柄![fplot(@sin,[-pi,pi]) 绘制的连续正弦曲线,无需手工构造数据点](../../images/matlab/ch10_02.jpg)
这一招在工程计算里无处不在:方程求根(fzero)、数值积分(integral/quad)、常微分方程求解(ode45)、甚至给 sort 指定排序键,全都是「传一个句柄进去」。掌握句柄,就解锁了 MATLAB 的整个数值算法库。
举一个把「匿名函数 + 函数函数」串起来的完整例子 —— 求几何级数 的和。 已知时根本不用循环,一条匿名函数向量化解决:
>> geom = @(r, n) sum(r.^(0:n)); % 向量化的等比求和
>> geom(2, 4) % 1+2+4+8+16
ans =
31还可以不传 n、每次随机生成(对应教材 Practice 10.1 的 varargin 版本)。而更实际的应用是:把一句物理公式做成匿名函数,直接交给 fzero 求根、交给 integral 积分 —— 比如声音在空气中的速度 (T 为兰氏温度):
>> vsound = @(T) 49.02 * sqrt(T);
>> fplot(vsound, [400, 1000]) % 看曲线走向
>> integral(vsound, 492, 671) % 直接数值积分,无需写任何文件
ans =
4.9789e+03此外还有按元胞数组逐元素执行函数的 cellfun(如 cellfun(@length, cs) 求每个元胞的长度),以及矩阵化的 bsxfun,都是函数函数家族的成员,用到时 doc 一下即可。
10.4.3 计时
给代码计时推荐 timeit(比 tic/toc 更稳健,会多次运行取平均):
>> fh = @() prod(1:10000000);
>> timeit(fh)
ans =
0.0308函数跑得太快会触发警告说结果不可信 —— 测量对象太短,计时本身的开销就淹没了它。tic/toc 的用法是成对夹住被测代码:tic 起表,toc 读秒,适合测一段脚本的总耗时;而 timeit 吃的是函数句柄,自动重复多次取统计意义上更稳的单次耗时。测算法性能时优先 timeit,测脚本整体耗时用 tic/toc 更省事。
本章函数类型速查
| 名称 | 定义方式 | 存放位置 | 何时用 |
|---|---|---|---|
| 普通函数 | function ... end | 独立 .m 文件 | 逻辑复杂、需复用 |
| 子函数 | 同文件内第二个 function | 同一 .m 文件 | 只给主函数用的辅助逻辑 |
| 嵌套函数 | 外层函数体内定义 | 外层函数体内 | 需共享外层变量(GUI 回调) |
| 匿名函数 | @(args) 表达式 | 变量(可存 MAT 文件) | 一行式小函数、做回调 |
10.5 递归函数
递归(recursion)就是函数调用自己。任何递归定义必须有两条腿:
- 一般情况(general case):问题化简为更小的同类问题;
- 基本情况(base case):直接给出答案,递归到此为止。
以阶乘为例,(一般情况),(基本情况)。展开 :
3! = 3 × 2! 挂起,等 2!
2! = 2 × 1! 挂起,等 1!
1! = 1 基本情况,终于能算出数了
2! = 2 × 1 = 2 回程,倒序逐层结算
3! = 3 × 2 = 6
% fact.m —— 递归求 n!
function facn = fact(n)
if n == 1
facn = 1; % 基本情况:递归的刹车
else
facn = n * fact(n - 1); % 一般情况:调用自己
end
end
>> fact(5)
ans =
120
>> factorial(5) % 与内置函数结果一致
ans =
120执行过程:fact(3) 要等 fact(2),fact(2) 要等 fact(1),fact(1) 直接返回 1 之后,挂起的式子倒序逐层结算 —— 和 C 里递归调用栈的行为一模一样(每层调用有自己的局部 n 和 facn)。
第二个例子更有意思:一个不返回值、只负责打印的递归函数,把句子里的单词倒序输出。思路是 strtok 切下第一个词,先递归处理剩下的部分,递归回来之后再打印当前词 —— 于是最后的词最先打印:
% prtwords.m —— 递归倒序打印句子中的单词
function prtwords(sent)
[word, rest] = strtok(sent);
if ~isempty(rest)
prtwords(rest); % 先处理"剩下的句子"(下潜)
end
disp(word) % 递归回来再打印(回程时打印 → 天然倒序)
end
>> prtwords('what does this do')
do
this
does
what这里的基本情况是 rest 为空 —— 句子切到头了,递归停止。
递归更经典的应用是斐波那契数列(Fibonacci):、,之后每项是前两项之和。它的递归定义有两个基本情况、一个一般情况:
% fib.m —— 递归求第 n 个斐波那契数
function f = fib(n)
if n == 0 || n == 1
f = n; % 两个基本情况
else
f = fib(n-1) + fib(n-2); % 一般情况:分裂成两次自调用
end
end
>> arrayfun(@fib, 0:9) % 顺便复习:句柄配合内置函数函数
ans =
0 1 1 2 3 5 8 13 21 34和阶乘不同,斐波那契每次分裂出两次自调用,计算量随 n 指数增长 —— fib(40) 就要卡好几秒,而一个 for 循环瞬间出结果。这是「能迭代就别递归」最直观的反面教材,但它把递归结构表达得清清楚楚,是理解分治和树形结构的敲门砖。
常见坑:没有基本情况,或到不了基本情况
忘写基本情况、或传入的参数让基本情况永远不可达,就是无限递归(infinite recursion)。C 里表现为栈溢出崩溃,MATLAB 则会在递归深度超限时报错。写递归函数时,第一行就先问自己:「什么情况下它不再调用自己?」
能用循环就别用递归
坦白说,阶乘这种教科书例子用 for 循环(或内置
factorial)更快更省内存 —— 递归每层都要压栈。教材明确说:可能时用迭代代替递归。递归的真正价值在那些天然自引用的问题上:树的遍历、分形、快速排序、汉诺塔等。这章只是给你装上这个思维工具。
本章通关标准:
- 能写出「必传参数 + varargin」形式的函数头,并用
nargin做分支;能解释为什么nargin数的是全部参数而不是 varargin 里的元素。- 能用
@创建匿名函数、把@sin或匿名函数句柄传给fplot/fzero/integral,说清「传函数名报错、传句柄才行」的原因。- 能手写递归阶乘,并准确指出一般情况、基本情况各是哪一行、执行顺序为何是「下去挂着、回来倒序结算」。
1.
varargin{1}为什么要用花括号?用圆括号会怎样?varargin 是元胞数组,圆括号返回的是元胞本身(一个 1×1 cell),不能直接当数值用;花括号才取出元胞里装的内容。这是第 8 章 cell 语法在这里的直接应用:
varargin{1}取值,varargin(1)取盒子。
2. 调用
fnfnexamp(sin)为什么报错,fnfnexamp(@sin)就可以?
sin不带@时,MATLAB 会试图立即调用 sin 函数,但没有给它输入参数,于是报 “Not enough input arguments”。@sin只是取得句柄(指向函数的引用),不执行函数。这个句柄传进 fnfnexamp 后,才在y = funh(x)处被真正调用。已存句柄的变量(如cirarea)传入时不再加@。
3. func2str 和 str2func 各用在什么场景?
func2str(句柄)转成字符串,最常见于把函数名放进图标题;str2func('sin')把字符串转回句柄,用于「用户/文件里给的是函数名字符串,代码要执行它」的场景。二者互为逆操作。
4. 嵌套函数里,内层为什么不用传参就能用外层的 len 和 wid?输出参数 outbase 为什么外层却用不了?
嵌套函数的变量作用域覆盖最外层函数:外层定义的变量对内层直接可见,所以
base免传参。但输出参数不是变量,outbase的作用域仅限base函数内部,外层拿不到它 —— 外层要的是base的返回值(outvol = base * ht里的函数调用返回值),而不是那个变量。
5. 递归版阶乘和循环版阶乘该用哪个?为什么教材还是教递归?
实际工程用循环版或内置
factorial:递归每层压栈、有函数调用开销,且深度过大有溢出风险。但递归是描述自引用结构(树、分形、回溯)最自然的语言,这些场景用循环写会非常痛苦。阶乘只是让你在最简单的题面上看清「一般情况 + 基本情况 + 倒序结算」这套机制。
6. nargout == 3 中的 3 是函数头里输出参数的个数吗?
不是。
nargout返回的是本次调用时赋值语句左边接了几个变量,与函数头声明了几个输出无关。mysize头里声明了 3 个输出,但调用[r,c] = mysize(eye(3))时nargout是 2 —— 函数据此可以「按需输出」。