一、函数文件
我们在许多时候希望将特定的算法写成函数的形式,以提高程序的可重用性和程序设计的效率。 函数文件定义了输出参数和输入参数的对应关系,以方便外部调用。事实上,MATLAB 提供的标准函数都是由函数文件定义的。
1. 函数文件的基本结构
函数文件由 function 语句引导,其基本结构如下:
function 输出形参表= 函数名( 输入形参表)
注释说明部分
函数体语句
其中,以 function 开头的一行为引导行,表示定义一个函数。函数名的命名规则与变量名相同。 在函数定义时,输入输出参数没有分配存储空间,所以称为形式参数,简称形参。当有多个形参时,形参之间用逗号分隔,组成形参表。当输出形参多于一个时,则应该用方括号括起来,构成一个输出矩阵。 说明: (1) 关于函数文件名。函数文件名通常由函数名再加上扩展名 .m 组成,不过函数文件名与函数名也可以不相同。当两者不同时,MATLAB 将忽略函数名,调用时使用函数文件名。为理解和记忆的方便,一般建议函数文件名和函数名统一。 (2) 关于注释说明部分。注释说明包括如下 3 部分内容。 ① 紧随函数文件引导行之后以 % 开头的第一注释行。这一行一般包括大写的函数文件名和函数功能简要描述,供 lookfor 关键词查询和 help 在线帮助用。 ② 第一注释行及之后连续的注释行。通常包括函数输入输出参数的含义及调用格式说明等信息,构成全部在线帮助文本。 ③ 与在线帮助文本相隔一空行的注释行。包括函数文件编写和修改的信息,如作者、修改日期、版本等内容,用于软件档案管理。 (3) 关于 return 语句。如果在函数文件中插入了 return 语句,则执行到该语句就结束函数的执行,程序流程转至调用该函数的位置。通常,在函数文件中也可不使用 return 语句,这时在被调用函数执行完成后自动返回。 例如,我们写函数文件, 求半径为 r 的圆的面积和周长。 函数文件如下:
function [ s, p] = fcircle ( r)
% CIRCLE calculate the area and perimeter of a circle of radii r
% r 圆半径
% s 圆面积
% p 圆周长
% 2023 年5 月15 日编
s= pi* r* r;
p= 2 * pi* r;
将以上函数文件以文件名 fcircle.m 存盘,然后在 MATLAB 命令行窗口调用该函数。
>> [ s, p] = fcircle ( 10 )
s =
314.1593
p =
62.8319
采用 help 命令或者 lookfor 命令可以显示出注释说明部分的内容,其功能和一般 MATLAB 函数的帮助信息是一致的。 利用 help 命令可查询 fcircle 函数的注释说明。
>> help fcircle
CIRCLE calculate the area and perimeter of a circle of radii r
r 圆半径
s 圆面积
p 圆周长
2023 年5 月15 日编
再用 lookfor 命令在 MATLAB 的搜索路径中寻找并列出所有第一注释行包括指定关键词的文件。
>> lookfor perimeter
fcircle - CIRCLE calculate the area and perimeter of a circle of radii r
2. 函数调用
2.1 函数调用的格式
函数文件建立好后,就可以调用该函数了,调用格式如下:
[ 输出实参表] = 函数名( 输入实参表)
在调用函数时,函数输入输出参数称为实际参数,简称实参。要注意的是,函数调用时各实参出现的顺序、个数,应与函数定义时形参的顺序、个数-致,否则会出错。函数调用时,先将实参传递给相应的形参,从而实现参数传递,然后再执行函数的功能。 例如,我们利用函数文件, 实现直角坐标
(
x
,
y
)
(x, y)
( x , y ) 与极坐标
(
ρ
,
θ
)
(\rho,\theta)
( ρ , θ ) 之间的转换
ρ
=
x
2
+
y
2
\rho =\sqrt{x^{2}+y^{2}}
ρ = x 2 + y 2
θ
=
arctan
(
y
/
x
)
\theta =\arctan (y/x)
θ = arctan ( y / x ) 首先编写函数文件 tran.m。
function [ rho, theta] = tran ( x, y)
rho= sqrt ( x* x+ y* y) ;
theta= atan ( y/ x) ;
end
x= input ( 'x= ' ) ;
y= input ( 'y=? ' ) ;
[ rho, theta] = tran ( x, y) ;
disp ( [ 'rho=' , num2str ( rho) ] )
disp ( [ 'theta=' , num2str ( theta) ] )
x= 45
y= 45
rho= 63.6396
theta= 0.7854
实际上,MATLAB 提供了直接坐标与极坐标之间转换的函数,分别如下: (1) [th,r]=cart2pol(x,y) 是将直角坐标转换为极坐标。 (2) [x,y]=pol2cart(th,r) 是将极坐标转换为直角坐标。
2.2 函数的递归调用
在 MATLAB 中,函数可以嵌套调用,即一个函数可以调用其他函数,甚至可以调用它自身。当一个函数调用它自身时称为函数的递归调用。 例如,我们利用函数的递归调用,求
n
!
n!
n ! 。
n
!
n!
n ! 本身就是以递归的形式定义的:
n
!
=
{
1
,
n
≤
1
n
(
n
−
1
)
!
,
n
>
1
n!=\left\{\begin{matrix}1,n≤1 \\n(n-1)!,n>1 \end{matrix}\right.
n ! = { 1 , n ≤ 1 n ( n − 1 )! , n > 1 显然,求
n
!
n!
n ! 需要求
(
n
−
1
)
!
(n-1)!
( n − 1 )! ,这时可采用递归调用。采用递归调用的函数文件 factor.m 如下:
function f= factor ( n)
if n< 1
f= 1 ;
else
f= factor ( n- 1 ) * n; % 递归调用求( n- 1 ) !
end
在脚本文件中调用函数文件 factor.m,求
s
=
1
!
+
2
!
+
3
!
+
4
!
+
5
!
s=1!+2!+3!+4!+5!
s = 1 ! + 2 ! + 3 ! + 4 ! + 5 ! 。
s= 0 ;
n= input ( 'n= ' ) ;
for i= 1 : n
s= s+ factor ( i) ;
end
disp ( [ '从1到' , num2str ( n) , '的阶乘和为:' , num2str ( s) ] )
n= 5
从1 到5 的阶乘和为:153
任意排列问题。MATLAB 提供的函数 randperm(n) 可以产生一个从整数 1 到整数 n 的任意排列。编写一个函数来实现 randperm(n) 函数的功能,即给出一个由任意数组成的行向量,然后产生这个行向量元素的任意排列。 考虑编写两个不同的函数,这两个函数的功能完全相同,只是控制结构不同。第一个函数用循环结构,可以用 for 语句或 while 语句控制循环,第二个函数使用递归调用。 (1) 函数用循环结构。
function Y= rndprm1 ( X)
% RNDPRM1 用for 循环产生一个行向量的任意排列
% rndprm1 ( X) 产生行向量X的任意排列
[ m, n] = size ( X) ;
if m> 1
error ( 'RNDPRM1 accepts as inputs only vectors' ) ;
end
Y= [ ] ; % 从一个空矩阵开始
l= n; % X的元素个数
for i= 1 : n
k= 1 + fix ( 1 * rand) ; % 随机选择Y的下一个元素的位置
x= X ( k) ; % 被选择的元素
Y= [ Y, x] ; % 将x添加到Y中
X ( k) = [ ] ; % 从X中删除x
l= l- 1 ; % 更新X的元素个数
end
function Y= rndprm2 ( X)
% RNDPRM2 用for 循环产生一个行向量的任意排列
% rndprm2 ( X) 产生行向量X的任意排列
[ m, n] = size ( X) ;
l= n;
if m> 1
error ( 'RNDPRM2 accepts as inputs only vectors' ) ;
end
if n<= 1
Y= X;
else
k= 1 + fix ( 1 * rand) ; % 随机选择Y的下一个元素的位置
x= X ( k) ; % 被选择的元素
X ( k) = [ ] ; % 从X中删除x
Z= rndprm2 ( X) ; % 将剩下的元素随机排列
Y= [ Z, x] ; % 构造输出向量
l= l- 1 ; % 更新X的元素个数
end
>> rndprm1 ( [ 34 , 6 , 3 , 54 , 2 , 5 , 454 ] )
ans =
34 6 3 54 2 5 454
>> rndprm2 ( [ 34 , 6 , 3 , 54 , 2 , 5 , 454 ] )
ans =
454 5 2 54 3 6 34
因为 MATLAB 将长度为n的字符串当作一个长度为 n 的向量,所以也可以用字符串作为函数的自变量。例如:
>> rndprm1 ( 'apple' )
ans =
'apple'
>> rndprm2 ( 'apple' )
ans =
'elppa'
2.3 函数参数的可调性
MATLAB 在函数调用上有一个特点,就是函数所传递参数数目的可调性。凭借这一点,一个函数可完成多种功能。 在调用函数时,MATLAB 用两个预定义变量 nargin 和 nargout 分别记录调用该函数时的输入实参和输出实参的个数。只要在函数文件中包含这两个变量,就可以准确地知道该函数文件被调用时的输入/输出参数个数,从而决定函数如何进行处理。 nargin 用法示例。 建立函数文件 charray.m。
function fout= charray ( a, b, c)
if nargin== 1
fout= a;
elseif nargin== 2
fout= a+ b;
elseif nargin== 3
fout= ( a* b* c) / 2 ;
end
a= 1 : 3 ;
b= a' ;
x= charray ( a) ;
y= charray ( a, b' ) ;
z= charray ( a, b, 3 ) ;
disp ( [ 'x= ' , num2str ( x) ] )
disp ( [ 'y= ' , num2str ( y) ] )
disp ( [ 'z= ' , num2str ( z) ] )
执行脚本文件 mydemo.m 后的输出结果如下:
x= 1 2 3
y= 2 4 6
z= 21
在脚本文件 mydemo.m 中,3 次调用函数文件 charray.m,因输入参数分别是 1、2、3,从而执行不同的操作,返回不同的函数值。
2.4 全局变量与局部变量
在 MATLAB 中,函数文件中的变量是局部的,与其他函数文件及 MATLAB 工作空间相互隔离,即在一个函数文件中定义的变量不能被另一个函数文件引用。 如果在若干函数中,都把某一变量定义为全局变量,那么这些函数将共用这个变量。全局变量的作用域是整个 MATLAB 工作空间,即全程有效,所有的函数都可以对它进行存取和修改,因此,定义全局变量是函数间传递信息的一种手段。 全局变量用 global 命令定义,格式如下:
global变量名
全局变量应用示例。 先建立函数文件 wadd.m,该函数将输入的参数加权相加。
function f= wadd ( x, y)
global ALPHA BETA
f= ALPHA* x+ BETA* y;
>> global ALPHA BETA
>> ALPHA= 1 ;
>> BETA= 2 ;
>> s= wadd ( 1 , 2 )
s =
5
由于在函数 wadd 和基本工作空间中都把 ALPHA 和 BETA 两个变量定义为全局变量,所以只要在命令行窗口中改变 ALPHA 和 BETA 的值,就可改变函数中 x、y 的权值,而无须修改 wadd.m 文件。 在实际程序设计时,可在所有需要调用全局变量的函数中定义全局变量,这样就可实现数据共享。在函数文件中,全局变量的定义语句应放在变量使用之前,为了便于了解所有的全局变量,一般把全局变量的定义语句放在文件的前部。为了在工作空间中使用全局变量,也要定义全局变量。 值得指出的是,在程序设计中,全局变量固然可以带来某些方便,但却破坏了函数对变量的封装,降低了程序的可读性,因此,在结构化程序设计中,全局变量是不受欢迎的,尤其当程序较大,子程序较多时,全局变量将给程序调试和维护带来不便,故不提倡使用全局变量。如果一定要用全局变量,最好给它起-一个能反映变量含义的名字,以免和其他变量混淆。
二、特殊形式的函数
除了最常用的通过函数文件定义一个函数,MATLAB 还可以使用子函数,此外还可以通过内联函数和匿名函数自定义函数。
1. 子函数
在 MATLAB 的函数定义中,如果函数较长,往往可以将多个函数分别写在不同的函数文件中,但有时函数可能很短,可能希望将多个函数定义放在同一个函数文件中,这就存在子函数的定义问题。 在 MATLAB 中,可以在一个函数文件中同时定义多个函数,其中函数文件中出现的第一个函数称为主函数(Primary Function),其他函数称为子函数(Subfunction),但需要注意的是子函数只能由同一函数文件中的函数调用。 在保存函数文件时,函数文件名一般和主函数名相同,外部程序只能对主函数进行调用。例如建立 func.m 文件,程序如下。
function d= func ( a, b, c) % 主函数
d= subfunc ( a, b) + c;
function c= subfunc ( a, b) % 子函数,此c非彼c,这里的c是形式输出变量
c= a* b;
>> func ( 3 , 4 , 5 )
ans =
17
注意,同一函数文件中主函数和子函数的工作区是彼此独立的,各个函数间的信息传递可以通过输入输出参数、全局变量来实现。
2. 内联函数
以字符串形式存在的函数表达式可以通过 inline
函数转化为内联函数。例如 a=‘(x+y)^2’,可以通过 f=inline(a) 生成内联函数 f(x,y)=(x+y)^2。
>> a= '(x+y)^2' ;
>> f= inline ( a)
f =
内联函数:
f ( x, y) = ( x+ y) ^ 2
>> f ( 3 , 4 )
ans =
49
3. 匿名函数
函数句柄变量= @ ( 匿名函数输入参数) 匿名函数表达式
其中,函数句柄变量相当于函数的别名,利用它可以间接调用函数;“@” 是创建函数句柄的运算符;“@” 后面定义了一个匿名函数,包括函数输入参数和函数表达式;函数有多个输入参数时,参数间用逗号分隔。例如:
>> sqr= @( x) x. ^ 2 % 定义匿名函数
sqr =
包含以下值的 function_handle:
@( x) x. ^ 2
>> sqr ( [ 1 , 2 , 3 ] ) % 调用匿名函数
ans =
1 4 9
>> f= @( x, y) x^ 2 + y^ 2 ;
>> f ( 3 , 4 )
ans =
25
也可以通过下列语句给已存在的函数定义函数句柄,并利用函数句柄来调用函数。
函数句柄变量= @函数名
其中,函数名可以是 MATLAB 提供的内部函数,也可以是用户定义的函数文件。例如:
>> h= @sin % 8 取正弦函数句柄
h =
包含以下值的 function_handle:
@sin
>> h ( pi/ 2 ) % 通过函数句柄变量h来调用正弦函数
ans =
1
匿名函数的执行效率要明显高于内联函数,在参数传递方面也要比内联函数方便、高效。内联函数在将来的MATLAB版本中将被删除,取而代之的是匿名函数,内联函数能实现的,匿名函数完全可以更好地实现,并且调用效率要比内联函数高得多。
三、程序调试与优化
程序调试(Debug)是程序设计的重要环节,MATLAB 提供了相应的程序调试功能,既可以通过 MATLAB 编辑器对程序进行调试,又可以在命令行窗口结合具体的命令进行。 程序设计的思路是多种多样的,针对同样的问题可以设计出不同的程序,而不同的程序其执行效率会有很大不同,特别是数据规模很大时,差别尤为明显,所以,有时需要借助于性能分析工具分析程序的执行效率,并充分利用 MATLAB 的特点,对程序进行优化,从而达到提高程序性能的目的。
1. 程序调试方法
一般来说,应用程序的错误有两类,一类是语法错误,另一类是运行时的错误。MATLAB 能够检查出大部分的语法错误,给出相应错误信息,并标出错误在程序中的行号。程序运行时的错误是指程序的运行结果有错误,这类错误也称为程序逻辑错误。 MATLAB 系统对逻辑错误是无能为力的,不会给出任何提示信息,这时可以通过一些调试方法来发 现程序中的逻辑错误。
1.1 利用调试函数进行程序测试
MATLAB 提供了一系列的程序调试函数,用于程序执行过程中的断点操作、执行控制等。在 MATLAB 命令行窗口输入以下命令将输出调试函数及其用途简介。
>> help debug
常用的调试函数有以下几个。 (1) dbstop
:在程序的适当位置设置断点,使得系统在断点前停止执行,用户可以检查各个变量的值,从而判断程序的执行情况,帮助发现错误。使用以下命令可以显示 dbstop
函数的常用格式。
>> help dbstop
(2) dbclear
:清除用 dbstop
函数设置的断点。 (3) dbcont
:从断点处恢复程序的执行,直到遇到程序的其他断点或错误。 (4) dbstep
:执行一行或多行语句,执行完后返回调试模式,如果在执行过程中遇到断点,程序将中止。 (5) dbquit
:退出调试模式并返回到基本工作区,所有断点仍有效。
1.2 利用调试工具进行调试
在 MATLAB 编辑器中新建一个 M 文件或打开一个 M 文件时,编辑器选项卡提供了断点命令组,通过对 M 文件设置断点可以使程序运行到某一行暂停运行, 这时可以查看和修改工作区中的变量。 单击断点命令按钮,弹出下拉菜单,其中有 6 个命令,分别用于清除所有断点,设置/清除断点,启用/禁用断点,设置或修改条件断点(条件断点可以使程序执行到满足一定条件时停止),出现错误时停止(不包括 try…catch 语句中的错误),出现警告时停止。 在 M 文件中设置断点并运行程序,程序即进入调试模式,并运行到第一个断点处,此时编辑器选项卡上出现调试命令组,命令行窗口的提示符相应变成 K>>。 进入调试模式后,最好将编辑器窗口锁定,即停靠到 MATLAB 主窗口上,便于观察代码运行中变量的变化。要退出调试模式,则在调试命令组中单击退出调试按钮。 控制单步运行的命令共有 4 个。在程序运行之前,有些命令按钮未激活。只有当程序中设置了断点,且程序停止在第一个断点处时这些命令按钮才被激活,这些命令按钮功能如下。 (1) 步进:单步运行。每单击一次,程序运行一条语句,但不进入函数。 (2) 步入:单步运行。遇到函数时进入函数内,仍单步运行。 (3) 步出:停止单步运行。如果是在函数中,跳出函数;如果不在函数中,直接运行到下一个断点处。 (4) 运行到光标处:直接运行到光标所在的位置。
2. 程序性能分析与优化
2.1 程序性能分析
利用探查器(Profiler)、 tic 函数和 toc 函数能分析程序各环节的耗时情况,分析报告能帮助用户寻找影响程序运行速度的瓶颈所在,以便于进行代码优化。 探查器以图形化界面让用户深入地了解程序执行过程中各函数及函数中的每条语句所耗费的时间,从而有针对性地改进程序,提高程序的运行效率。 假定有脚本文件 testp.m:
x= - 20 : 0.1 : 20 ;
y= 300 * sin ( x) . / x;
plot ( x, y) ;
>> profile on
>> testp
>> profile viewer
这时,MATLAB 将打开探查器窗口,显示分析结果。探查摘要表提供了运行文件的时间和相关函数的调用频率,反映出整个程序耗时 2.626s,其中绘制图形中调用的 newplot 函数耗时最多。单击某函数名,则打开相应函数的详细报告。
2.2 程序优化
MATLAB 是解释型语言,计算速度较慢,所以在程序设计时如何提高程序的运行速度是需要重点考虑的问题。优化程序运行可采用以下方法。 (1) 采用向量化运算。在实际 MATLAB 程序设计中,为了提高程序的执行速度,常用向量或矩阵运算来代替循环操作。首先生成一个向量
i
i
i ,然后用
i
i
i 生成向量
f
f
f ,
f
f
f 各元素值即对应于
y
y
y 的各累加项,再用 MATLAB 提供的 sum
函数求f各个元素之和。程序如下:
n= 100 ;
i= 1 : n;
f= 1. / ( i. * i) ;
y= sum ( f)
如果程序中的 n 值由 100 改成 100000,再分别运行这两个程序,则可以明显地看出,用向量计算方法编写的程序比循环程序要快得多。 (2) 预分配内存空间。通过在循环之前预分配向量或数组的内存空间可以提高 for 循环的处理速度。例如,下面的代码用函数 zeros
预分配 for 循环中用到的向量 a 的内存空间,使得这个 for 循环的运行速度显著加快。 程序 1:
clear;
a= 0 ;
for n= 2 : 1000
a ( n) = a ( n- 1 ) + 10 ;
end
clear;
a= zeros ( 1 , 1000 ) ;
for n= 2 : 1000
a ( n) = a ( n- 1 ) + 10 ;
end
程序 2 采用了预定义矩阵的方法,运行时间比程序 1 要短。 (3) 减小运算强度。在实现有关运算时,尽量采用运算量更小的运算,从而提高运算速度。一般来说,乘法比乘方的运算快,加法比乘法运算快。例如:
clear;
a= rand ( 32 ) ; % 生成一个32 ×32 矩阵
x= a. ^ 3 ;
y= a. * a. * a;
从 Profiler 的评估报告中可以看出,a.*a.*a 比 a.^3 运算所花的时间少得多。