这一章在干嘛?
第 3 章你已经会写单值函数和脚本了,本章把「程序」这个词当真:一个像样的 MATLAB 程序 = 一个主脚本 + 一堆各司其职的函数。本章讲清楚函数的另外两种形态(多返回值、无返回值)、怎么把程序拆成模块、变量在脚本/函数/命令行之间到底谁看得见谁(作用域),以及程序出错后怎么定位和修复。学完你就能组织起几百行、多文件的完整程序,而不是把所有代码塞进一个脚本里。
6.1 更多类型的用户自定义函数
第 3 章写的函数都是「进几个参数、出一个返回值」。MATLAB 函数其实有三种形态:
- 返回一个值——第 3 章已学;
- 返回多个值——输出参数放在方括号里;
- 不返回任何值,只干活(比如打印、画图)。
不管哪种形态,函数定义的结构都一样:函数头(header,第一行)→ 说明注释(help 函数名 时显示的就是它)→ 函数体 → end。函数名必须与存放它的 .m 文件同名,否则调用时会找不到。
6.1.1 返回多个值的函数
对比 C:C 的函数只能有一个返回值,想「多带点东西回来」只能靠指针参数。MATLAB 直接在函数头用方括号列出多个输出参数:
function [area, circum] = areacirc(rad)
% areacirc 同时返回圆的面积和周长
% 格式:areacirc(radius)
area = pi * rad .* rad;
circum = 2 * pi * rad;
end调用时必须用同样数量的变量接住返回值,顺序与函数头一致:
>> [a, c] = areacirc(4)
a =
50.2655
c =
25.1327只写一个变量也能调,但只有第一个返回值被留下,第二个直接丢失——这是初学者常犯的错。想只取第二个,用波浪号占位跳过第一个:[~, c] = areacirc(4)。
多返回值函数天生支持向量化
areacirc里用的是.*,所以传进去一个半径向量也行,返回的就是面积向量和周长向量——[a, c] = areacirc(1:4)一次算四个圆。这就是第 5 章向量化思维在函数设计上的体现:写函数时优先让输入输出支持数组。
再看一个三返回值的例子:把总秒数拆成时、分、秒(7515 秒 = 2 小时 5 分 15 秒):
function [hours, minutes, secs] = breaktime(totseconds)
% breaktime 把总秒数拆成时、分、秒
hours = floor(totseconds / 3600); % 整除 3600 取小时
remsecs = rem(totseconds, 3600); % 余下的秒数(局部变量)
minutes = floor(remsecs / 60);
secs = rem(remsecs, 60);
end>> [h, m, s] = breaktime(7515)
h =
2
m =
5
s =
15算法和 C 里拆时分秒完全一样:floor 取商、rem 取余。remsecs 没有出现在输出参数表里,它只是函数内部使用的局部变量(6.4 节细说)。
6.1.2 不返回值的函数:只干活的函数
第二类函数只完成任务(典型如打印),函数头里没有输出参数、也没有等号:
function printem(a, b)
% printem 以句子格式打印两个数
fprintf('The first number is %.1f and the second is %.1f\n', a, b)
end>> printem(3.3, 2)
The first number is 3.3 and the second is 2.0因为它不返回任何东西,不能出现在赋值语句右边:
>> x = printem(3, 5) % 错误!
Error using printem
Too many output arguments.记住这个心智模型:无返回值函数的调用本身就是一条语句,和 C 里调用 printf 后加分号一个待遇——不能把它塞进表达式里。
这类函数产生的「外部可见效果」(打印、画图等)称为副作用(side effects)。
6.1.3 设计原则:算值的函数别顺手打印
「返回值」和「打印结果」是两种职责,好的做法是分开:函数只算并返回,打印留给调用者。看反例:
function calccircum1(radius) % 反例:只打印不返回
disp(2 * pi * radius)
end>> calccircum1(3.3)
20.7345
>> c = calccircum1(3.3) % 想存下来?没门
Error using calccircum1
Too many output arguments.值只在屏幕上一闪而过,后续计算完全用不上。改成返回值:
function circle_circum = calccircum2(radius) % 正解:返回
circle_circum = 2 * pi * radius;
end>> circumference = calccircum2(3.3);
>> height = 4;
>> surf_area = circumference * height % 返回值能继续参与计算
surf_area =
82.9380计算圆柱表面积时,直接拿返回值乘高就行。这条原则与 C 里「函数返回结果、调用方决定怎么用」完全同构。
6.1.4 传参:按值传递,参数可以为零
MATLAB 的传参方式是按值传递(call-by-value):调用时把实参的值复制给函数头的形参,函数内改形参不影响外部。这点和 C 传普通变量一致(MATLAB 没有指针,想改调用者的变量,只能把新值作为返回值传回去)。
参数数量也可以是零。无参数的函数,圆括号可有可无,调用时同样随意:
function printrand() % 无输入无输出
fprintf('The random # is %.2f\n', rand)
end>> printrand()
The random # is 0.946.2 MATLAB 程序的组织:模块化与子函数
6.2.1 模块化程序:主脚本 + 各司其职的函数
**模块化程序(modular program)**把问题拆成若干模块,每个模块实现为一个函数,负责调度它们的主脚本称为主程序(main program)。以「算圆面积」为例,算法三步——取输入、算面积、显示结果——就拆成一个主脚本加三个函数,共四个 .m 文件:
% calcandprintarea.m —— 主脚本
radius = readradius; % 1. 读半径
area = calcarea(radius); % 2. 算面积
printarea(radius, area) % 3. 打印(无返回值,独立成句)function radius = readradius % readradius.m
radius = input('Enter the radius: ');
end
function area = calcarea(rad) % calcarea.m
area = pi * rad .* rad;
end
function printarea(rad, area) % printarea.m
fprintf('For a circle with a radius of %.2f inches, \n', rad)
fprintf('the area is %.2f inches squared. \n', area)
end>> calcandprintarea
Enter the radius: 5.3
For a circle with a radius of 5.30 inches,
the area is 88.25 inches squared.写模块化程序时最容易乱的是「调用和定义对不上」。核对方法很简单,逐个函数检查两头:
radius = readradius;↔function radius = readradius——调用没传参,头里就没有输入参数;返回一个值,左边就用一个变量接。area = calcarea(radius);↔function area = calcarea(rad)——传 1 个,收 1 个。printarea(radius, area)↔function printarea(rad, area)——传 2 个,收 2 个;无返回值,调用独立成句。
注意形参名和实参名不需要相同(radius vs rad),因为传的是值不是变量本身。
6.2.2 子函数:多个函数住一个文件
到目前为止每个函数都独占一个 .m 文件。其实一个文件里可以放多个函数:第一个叫主函数(primary function),文件名与它同名;后面跟的叫子函数(subfunction),只有同一文件里的函数才能调用它——相当于 C 里加了 static 限定的文件内私有函数。适合放「只被主函数使用、外界不需要知道」的辅助逻辑:
% printrectarea.m —— 文件名 = 主函数名
function printrectarea(len, wid)
% 打印矩形面积;内部调用子函数算面积
area = calcrectarea(len, wid); % 调用下面的子函数
fprintf('For a rectangle with a length of %.2f\n', len)
fprintf('and a width of %.2f, the area is %.2f\n', wid, area)
end
function area = calcrectarea(len, wid) % 子函数:外界不可直接调用
area = len * wid;
end用 help 看子函数的注释要加 > 操作符:help printrectarea>calcrectarea。
文件里可以有多个函数 ≠ 文件名可以随便起
每个文件只暴露第一个(主)函数给外部调用;子函数是文件私有的。如果发现你在文件外面想调某个子函数,说明它不该是子函数——提升成独立文件吧。
6.3 应用实战:菜单驱动的模块化程序
把前面的东西拼起来,看一个完整的菜单驱动程序(menu-driven program):程序循环显示菜单,用户选什么就调用对应函数,选「退出」才结束。几乎所有交互式程序都是这个骨架。
示例程序探索常数 。数学背景:,指数函数可用麦克劳林级数近似:
程序结构:主脚本 + 4 个函数文件(eoption 显示菜单并校验选择、explaine 打印说明、limite 用极限近似 e、expfn 对比内置 exp(x) 与级数近似),其中 limite 和 expfn 各带一个子函数负责输入校验和级数求和。核心骨架如下:
% eapplication.m —— 主脚本
choice = eoption; % 显示菜单,取用户选择
while choice ~= 4 % 4 = 退出
switch choice
case 1
explaine; % 打印 e 的说明
case 2
limite; % 用极限近似 e
case 3
x = input('Please enter a value for x: ');
expfn(x); % 对比 exp(x) 与级数近似
end
choice = eoption; % 干完活,再显示一次菜单
endfunction choice = eoption % eoption.m
printchoices
choice = input('');
while ~any(choice == 1:4) % 校验:不在 1~4 内就重来
disp('Error - please choose one of the options.')
printchoices
choice = input('');
end
end
function printchoices % 子函数:打印菜单
fprintf('1) Explanation\n2) Limit\n3) Exponential function\n4) Exit program\n\n')
endfunction outval = appex(x, n) % expfn.m 中的级数子函数
outval = 1; % 第一项,累加器初始化
for i = 1:n
outval = outval + (x^i) / factorial(i);
end
end>> eapplication
Enter a positive integer for n: 10
An approximation of e with n = 10 is 2.59
Enter a positive integer for n: 100
An approximation of e with n = 100 is 2.70 % n 越大越接近 2.7183这个例子集合了本章全部要点:while 驱动菜单循环(第 5 章)、switch 分发任务、每个功能一个函数、所有用户输入都经过错误校验、辅助逻辑下沉为子函数。以后写交互式工具,照这个骨架套即可。
6.4 变量作用域与持久变量
6.4.1 谁看得见谁:基础工作区与局部变量
一个变量的作用域(scope)指它在哪个工作区(workspace)里有效。命令窗口创建的工作区叫基础工作区(base workspace)。规则两条:
- 函数里的变量是局部变量(local variable):只在函数执行期间存在,函数一结束就销毁。调用函数不会给基础工作区留下任何变量,命令窗口的变量函数也看不见(除非通过参数传进来)。
>> clear
>> disp(mysum([5 9 1]))
15
>> who % 空!mysum 里的 runsum、i 都没了- 脚本和命令窗口共用基础工作区:脚本里定义的变量会留在命令窗口的
who列表里;反过来,命令窗口已有的变量(比如vec)也能被脚本直接使用。
第二条看起来方便,实则是坑:脚本偷偷依赖工作区里的旧变量,换个环境一跑就报「变量未定义」。所以教材反复强调——别在命令窗口定义变量给脚本用,要用就作为参数传给函数。也因此很多程序员习惯在脚本开头写 clear,把工作区清干净再跑。
也正因为脚本会污染基础工作区,稍大的程序常把「主脚本」换成「主函数」:全函数化的程序不给基础工作区留任何东西,边界干净。(MATLAB 也有 global 全局变量,但公认是不良风格,知道有这回事即可,别用。)
6.4.2 持久变量:函数的「记忆」
局部变量函数一结束就销毁。如果想让某个变量跨调用存活——比如统计函数被调用了几次——用**持久变量(persistent variable)**声明:
function func1 % 普通变量版:每次都归零
count = 0;
count = count + 1;
fprintf('The value of count is %d\n', count)
end
function func2 % 持久变量版:记得上一次
persistent count % 声明(只分配空间,未初始化)
if isempty(count) % 第一次调用时才初始化
count = 0;
end
count = count + 1;
fprintf('The value of count is %d\n', count)
end>> func1
The value of count is 1
>> func1
The value of count is 1 % 每次都从 0 开始
>> func2
The value of count is 1
>> func2
The value of count is 2 % 记住了上一次!对 C 程序员来说这就是老朋友:persistent 等价于函数内的 static 变量。两点 MATLAB 特有之处:其一,MATLAB 中只有持久变量需要「先声明后使用」,普通变量随手赋值即可;其二,惯用 if isempty(count) 来判断「是不是第一次调用」,从而完成一次性初始化。想重置持久变量,用 clear func2(清除该函数即可,不必 clear functions 全清)。
常见坑:持久变量在脚本和命令窗口里都会累积
持久变量每次调用都更新——不管调用来自脚本还是命令窗口。调试时发现「计数怎么不从 1 开始了」,多半是之前已经调过几次,
clear 函数名重置即可。
6.5 调试技术
程序里的任何错误统称 bug(这个词来自 1940 年代一只飞进继电器的飞蛾——真事)。找错改错的过程叫调试(debugging)。第 5 章介绍的 checkcode 静态检查是第一道防线,这里看运行期的三大类错误:
语法错误(syntax errors):语言用法写错了,比如少引号、拼错关键字。MATLAB 能当场抓住并报错,编辑器还会画波浪线提示:
>> mystr = 'how are you;
Error: A MATLAB string constant is not terminated properly.运行时错误(run-time errors):语法对、跑起来才炸。典型如数组越界:
>> runtimeEx % vec 只有 3 个元素,循环却跑到 4
3
4
5
Attempted to access vec(4); index out of bounds because numel(vec)=3.
Error in runtimeEx (line 6)顺带一提:C 里除零是未定义行为,MATLAB 里除以零返回 Inf,不报错——所以「除数是否为零」要自己检查,别指望 MATLAB 替你炸出来。
逻辑错误(logical errors):程序能跑、不报错,但结果是错的——比如把英寸转厘米写成了除以 2.54。这是最难抓的一类,因为没有任何报错指路。
6.5.1 追踪与断点
逻辑错误的定位靠「看程序到底执行了哪些语句、变量的值到底是什么」。两个工具:
echo 追踪:echo 函数名 on 会让每条语句执行时都回显出来,看清走了哪个分支。下面这个函数想判断 x 是否在 3~6 之间,却对所有 x 都打印「In middle of range」:
function testifelse(x)
if 3 < x < 6 % 有 bug 的条件!
disp('In middle of range')
else
disp('Out of range')
end
end用 dbstop 在第 6 行设断点,进入调试模式(提示符变成 K>>),手动检查表达式:
>> dbstop testifelse 6
>> testifelse(-2)
K>> x
x = -2
K>> 3 < x
ans = 0
K>> 3 < x < 6
ans = 1 % 真相大白!
K>> dbcont问题找到了:3 < x < 6 先算 3 < x 得到 0 或 1,而 0 和 1 都小于 6,所以整个表达式永远为真。这是 C 程序员的经典陷阱——C 里这么写同样是 bug,只是 MATLAB 的「真值就是 0/1 数值」让它的行为更隐蔽。正确写法:x > 3 && x < 6。
调试命令三件套:dbstop 设断点、dbcont 继续执行(或 dbstep 单步)、dbquit 退出调试。也可以不用命令:编辑器中代码行左侧那条灰色窄条被称为断点小巷(breakpoint alley),点一下就能在该行立一个红色断点,再点取消。
6.5.2 函数桩:先搭骨架再填肉
写「主脚本 + 一堆函数」的大程序时,别妄想一次全写完再调试——出了错根本不知道藏在哪。正确姿势是用函数桩(function stubs):先写出所有函数头,函数体暂时放个「占位实现」(随便返回个固定值),让主脚本先跑通,然后一个函数一个函数地实现、调试:
function [x, y] = getvals % 桩:先返回假数据
x = 33; y = 11;
end
function z = calcz(x, y) % 桩:先随便算个和
z = x + y;
end主脚本此刻就能执行验证流程对不对,之后逐个把桩替换成真实现。这套「增量式开发」在 C 里一样适用,只是 MATLAB 的脚本环境让迭代更轻快。
6.6 实时脚本与代码单元
6.6.1 实时脚本(Live Script)
R2016a 起 MATLAB 推出了实时脚本(live script):一种 .mlx 格式的富文本文档,可以把文字说明、LaTeX 公式、代码、运行结果(包括图)按小节混排在同一个文档里,图就显示在生成它的代码旁边。

创建方式:New → Live Script;普通 .m 脚本也可以「另存为 Live Script」。运行时可整体 Run All,也可以点某小节左侧的竖条只跑那一节:

写完的实时脚本可以导出为 PDF/HTML 分享,也能另存回普通 .m 文件(只保留代码部分,用 type 命令查看时看到的正是代码段)。
6.6.2 代码单元:普通脚本也能分节
买不起新版 MATLAB?普通 .m 脚本也能实现「分节运行」——用 %% 开头的注释行把代码切成若干代码单元(code cells),%% 行的文字就是单元标题:
% sinncosCells.m:画出 sin(x) 和 cos(x)
%% 创建向量并绘图
clf
x = 0: 2*pi/40: 2*pi;
plot(x, sin(x), 'ro')
hold on
plot(x, cos(x), 'b+')
%% 添加图例、坐标标签和标题
legend('sin', 'cos')
xlabel('x'); ylabel('sin(x) or cos(x)')
title('sin and cos on one graph')在编辑器里点进某个单元,选「Run Section」只跑这一节(结果保留),或「Run and Advance」跑完跳到下一节。调参数迭代画图时特别顺手——改一节跑一节,不用整个脚本重跑。「Publish」功能还能把整个脚本连同图形发布成 HTML 文档,单元标题自动生成目录,很适合写实验报告。
本章通关标准:
- 能写出多返回值函数并用
[a, c] = f(x)正确接住全部返回值,能解释「只打印不返回」的函数为什么无法参与后续计算。- 能把一个任务拆成「主脚本 + 3 个函数」的模块化结构,并逐条核对函数调用与函数头的参数个数是否匹配;知道子函数是文件私有的辅助函数。
- 能说清脚本与函数在作用域上的区别(基础工作区 vs 局部变量),会用
persistent做调用计数,会用 dbstop 断点抓出3 < x < 6这类逻辑错误。
函数头是
function [outa, outb] = qq1(x, y, z),调用[a, b] = myfun(x, y, z)和[outa, outb] = qq1(x, z)为什么都错?第一个错在函数名:调用名
myfun与定义名qq1不一致。第二个错在参数个数:函数头要求 3 个输入参数,只传了 2 个。调用时「传几个、函数收几个」「返回几个、左边接几个」必须对齐(返回值可以少接,但多出的会丢)。 [!question]- 脚本和函数在变量作用域上的核心区别是什么?脚本与命令窗口共用基础工作区:脚本里建的变量会留在工作区,命令窗口已有的变量脚本也能直接用。函数则拥有独立的工作区,内部变量全是局部变量、函数结束即销毁,外部变量必须通过参数传入。所以工程上推荐「主函数 + 子函数」而不是「主脚本」,让程序不给基础工作区留垃圾。 [!question]-
persistent count之后为什么通常跟着if isempty(count)?
persistent只声明变量、分配空间,不初始化,此时它是空矩阵。第一次调用时isempty(count)为真,执行初始化;之后的调用中 count 已有值,跳过初始化。这样就实现了「只初始化一次、之后跨调用保留」的语义,等价于 C 里函数内 static 变量的一次性初始化。 [!question]-if 3 < x < 6为什么永远是真?怎么改?MATLAB 从左到右算:先算
3 < x,结果非 0 即 1;再拿这个 0 或 1 与 6 比较,恒成立。所以无论 x 是多少条件都为真。必须写成if x > 3 && x < 6。这和 C 是同一个坑,切记数学连不等式不能直接搬进代码。 [!question]- 主脚本要调用 5 个函数,但函数都还没写,怎么让程序先跑起来?用函数桩(function stubs):先给每个函数写好正确的函数头,函数体放临时占位实现(返回固定值或简单打印),保证主脚本可执行、流程可验证;然后逐个把桩替换成真实现,每替换一个就单独调试一个。一次性全写完再调试,出了错很难定位是哪个函数的问题。