流程控制

在划分控制结构所对应的代码块时,编程语言通常采用如下三种做法:

  • 基于大括号 {}:例如 C/C++,Java
  • 基于 end 标记:例如 FORTRAN,MATLAB
  • 基于缩进:Python

MATLAB 受到 FORTRAN 的影响很大,也采用基于 end 的代码块标记,并不使用大括号 {} 来划分代码结构。

if 条件语句

提供例子即可

1
2
3
if x>1
x=1;
end
1
2
3
4
5
if x>1
y=x;
else
y=1;
end
1
2
3
4
5
6
7
if x>10
y=x;
elseif x>0
y=1;
else
y=0
end

switch 条件语句

MATLAB 支持基本的 switch 语句,我们可以判断表达式的值以进入不同的分支,不需要在分支结束使用 break,因为不会进入下一个分支,默认分支为 otherwise。MATLAB 并不要求 case 后面的结果是常量。

1
2
3
4
5
6
7
8
switch x
case 1
z=1
case 2
z=2
otherwise
z=3
end

对于更复杂的 switch 情况,我们还可以使用下面的“反转”技巧,判断表达式取为 true,在 case 中加入不同的布尔表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
score = 85;

switch true
case (score >= 90)
disp('Grade: A');
case (score >= 80)
disp('Grade: B');
case (score >= 70)
disp('Grade: C');
case (score >= 60)
disp('Grade: D');
otherwise
disp('Grade: F');
end

for 循环语句

非常不建议基于 for 循环(尤其是多重 for 循环)来遍历矩阵进行计算,因为这种做法的效率非常低下,不能照搬使用 C/C++ 时直接写 for 循环的习惯,使用 MATLAB 提供的向量化运算是更好的选择。

for 循环语句的标准格式如下

1
2
3
for index = startValue:step:endValue
% 循环体代码
end

和其他编程语言通常采用的左闭右开区间不同,这里的索引区间实际上是闭区间

例如

1
2
3
4
for i = 1:2:11
disp(i);
end
% 1 3 5 7 9 11

对于步长为 1 的情况,可以省略步长

1
2
3
4
for i = 1:10
disp(i);
end
% 1 2 3 4 5 6 7 8 9 10

可以用双重循环来遍历一个矩阵

1
2
3
4
5
6
B = [1, 2;3, 4];
for i = 1:size(B, 1)
for j = 1:size(B, 2)
disp(B(i, j));
end
end

和其他语言一样,在循环中支持如下选项:

  • break 跳出当前循环;
  • continue 跳转进入下一次循环。

注意:

  • 关于循环指标:由于 i,j 默认是虚数单位,如果将其作为循环变量,虽然在语法上不会报错,但是这会修改它们的值,后续不能将其用作虚数单位。(可以使用 i=1i, j=1j 来恢复)
  • 如果不使用向量化的语法,那么大规模的 for 循环(尤其是多层循环)很可能就是可行计算程序的性能瓶颈,需要特别注意循环体内部的语句细节,这里不做讨论。

for 遍历语句

我们可以用下面的语法方便地遍历行向量中的每一项

1
2
3
4
A = [1 2 3];
for i=A
disp(i)
end

对于矩阵,遍历语句每次会获取一列

1
2
3
4
5
6
A = [1 2 3
4 5 6];

for i=A
disp(i)
end

输出

1
2
3
4
5
6
7
8
1
4

2
5

3
6

while 循环语句

while 循环没什么好说的,和其他语言没什么区别,例如

1
2
3
4
5
w=0;u=0;

while u<10
w=w+u;u=u+1;
end

同样也支持 breakcontinue 语句。

补充

上述结构可以随意嵌套,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
for c = 1:ncols
for r = 1:nrows

if r == c
A(r,c) = 2;
elseif abs(r-c) == 1
A(r,c) = -1;
else
A(r,c) = 0;
end

end
end

在单条语句的简单情况下,也可以使用单行形式,例如

1
2
3
4
5
6
7
8
9
10
% if 条件, 语句; end
if a > 0, disp('a is positive'); end
if ~isempty(TitleStr), title(TitleStr); end


% if 条件, 语句1; else 语句2; end
if x > 0, disp('Positive'); else disp('Non-positive'); end

% for 变量 = 范围, 语句; end
for i = 1:5, fprintf('%d ', i); end

虽然 MATLAB 将字符数组和字符串,字符串数组进行了区分,但是出于兼容性考虑,在下面的各种输入输出方式中无论是使用字符数组还是字符串都是一样的效果。

显示变量的值

disp 函数可以用来显示一个变量的值,如果这个变量是字符串的话,也可以达到输出信息的效果,但是会自动添加一个回车。

例如

1
2
disp('hello,world!');
% hello,world!

这里的 disp(X) 语句加不加 ; 都是一样的。

disp 函数只接受一个参数,我们可以将字符数组拼接起来进行显示

1
2
disp(['hello', ',', 'world']);
% hello,world

在很多默认行为中都会调用 disp 函数,例如一个普通的赋值语句如果不以 ; 结尾,可能会对赋值结果调用 disp 函数以展示它的值。 对于自定义类型也可以通过定义 disp 方法来达到自定义输出效果的目的。

格式化字符串

MATLAB 支持和 C 语言几乎一样的字符串格式化函数,包括 fprintfsprintf

fprintf 格式化输出到控制台或文件中,返回值只是输出的字节数,没什么用,例如

1
2
3
4
5
fprintf('Hello, %s!\n', 'World'); % 缺省时输出到控制台
Hello, World!

fileID = fopen('exp.txt','w'); % 打开文件
fprintf(fileID,'Hello, %s!\n', 'World'); % 写入文件中

fileID 是获取的文件句柄(其实就是一个大于 2 的整数),1 代表标准输出流,2 代表标准错误输出流。

sprintf 格式化输出到一个字符串,返回值就是格式化得到的字符串,例如

1
2
3
4
5
6
name = 'Alice';
age = 30;
height = 5.5;
str = sprintf('Name: %s, Age: %d, Height: %.1f feet', name, age, height);

disp(str);

除此之外,下面几个函数也是支持格式化字符串并输出的,不需要额外生成 message(得益于 MATLAB 对不定参数的支持,不需要像 C 语言那么麻烦)

1
2
3
4
5
6
7
8
9
10
11
n = 7;

assert(isa(c,'double'),'Product is type %s, not double.',class(c))

if ~ischar(n)
warning('Input must be a character vector, not a %s',class(n))
end

if ~ischar(n)
error('Error. \nInput must be a char, not a %s.',class(n))
end

标准文件读写

在 MATLAB 中,文件打开模式与 C 语言非常类似。(底层的操作系统提供的接口就是这样的,以这种方式可以尽量保持完整的接口)

使用 fopen 函数打开文件,除了文件名之外,还可以指定不同的模式来控制文件的读写行为,常见的文件打开模式包括:

  • r:只读模式。文件必须存在,否则会出错。
  • w:写入模式。如果文件存在,将覆盖文件;如果文件不存在,将创建新文件。
  • a:追加模式。如果文件存在,数据将写入文件末尾;如果文件不存在,将创建新文件。
  • r+:读写模式。文件必须存在,否则会出错。
  • w+:读写模式。如果文件存在,将覆盖文件;如果文件不存在,将创建新文件。
  • a+:读写模式。如果文件存在,数据将写入文件末尾;如果文件不存在,将创建新文件。

此外,还可以指定文本模式或二进制模式:

  • t:文本模式(默认)。
  • b:二进制模式。

fopen 函数返回的是文件 ID,后续对这个文件的操作都需要传入文件 ID,包括最后的关闭文件 fclose

下面列举几个常见的文件操作:

逐行读取文本并展示

1
2
3
4
5
6
7
8
fileID = fopen('example.txt', 'r');

while ~feof(fileID)
line = fgetl(fileID);
disp(line);
end

fclose(fileID);

逐行写入文本

1
2
3
4
5
6
fileID = fopen('output.txt', 'w');

fprintf(fileID, 'This is a test.\n');
fprintf(fileID, 'The value of pi is approximately %.4f\n', pi);

fclose(fileID);

将数组写入到文件中(每一行只写入一个元素)

1
2
3
fid = fopen('output.txt', 'w');
printf(fid, '%.12f\n', v);
fclose(fid);

写入二进制文件

1
2
3
4
5
6
7
fileID = fopen('binaryoutput.bin', 'wb');

data = [1.1, 2.2, 3.3];

fwrite(fileID, data, 'double'); % 以double格式写入

fclose(fileID);

读取二进制文件,这里重新读取出来的 data 变成列向量了。

1
2
3
4
5
6
7
fileID = fopen('binaryoutput.bin', 'rb');

data = fread(fileID, 'double'); % 以double格式读取

disp(data);

fclose(fileID);

在写入文件前确保目录存在

1
2
3
4
5
[filePath, ~, ~] = fileparts(fileName);
if ~isempty(filePath) && ~exist(filePath,'dir')
mkdir(filePath);
end
fileID = fopen(fileName, 'w');

MAT 文件读写

MATLAB 专门提供 saveload 函数,用于保存和加载 .mat 文件,这是一种 MATLAB 特有的二进制文件格式,可以完整存储工作空间中的变量,包括变量的类型、大小和其他元数据。

保存变量

使用 save 函数可以直接把当前工作区的所有变量保存到 MAT 文件中

1
save('all_data.mat')

我们也可以将指定的部分变量保存到 MAT 文件中

1
2
3
p = rand(1,10);
q = ones(10);
save('pqfile.mat','p','q')

可以以追加形式把数据添加到 MAT 文件中

1
2
3
D = ones(2);

save('mydata.mat', 'D', '-append')

注意:

  • 如果文件已经存在,默认会直接覆盖原始文件,除非加上 -append 选项;
  • load 命令其实需要一个格式选项,默认是 -mat,也支持 -ascii 等其他格式,但是对文本格式的操作不够灵活,建议使用 fprintf;对格式的判断与文件后缀名无关。

可以使用 whos 命令查看 MAT 文件当前存储的变量列表,得到的是一个结构体数组

1
2
fileInfo = whos('-file', 'mydata.mat');
disp(fileInfo);

可以加上 -struct 选项解包一个结构体数组,将其中的所有字段作为单独变量存储到 MAT 文件中

1
2
3
4
s1.a = 12.7;
s1.b = {'abc',[4 5; 6 7]};
s1.c = 'Hello!';
save('newstruct.mat','-struct','s1')

可以加上 MAT 文件的格式版本,默认 7.0 版本,但是只有 7.3 版本可以支持超过 2GB 的数据

1
2
3
A = rand(5);
B = magic(10);
save('example.mat','A','B','-v7.3')

可以使用 -nocompression选 项,这个选项会阻止存储过程中的压缩,优点是存储过程更快,缺点是 MAT 文件变大

1
2
3
A = rand(5);
B = magic(10);
save('example.mat','A','B','-v7.3','-nocompression')

一个常见的需求是,MAT 文件中已经存储了我们关注的若干变量,我们希望使用工作区中的同名变量更新 MAT 文件中的数据,但是不引入工作区中的其他变量

1
2
existingVars = whos('-file','mydata.mat');
save('mydata.mat',existingVars.name)

加载变量

使用 load 函数可以直接把 MAT 文件中的所有变量加载到工作区中

1
load('mydata.mat');

也可以指定加载 MAT 文件中的部分变量到工作区中(这里甚至支持通过正则匹配加载满足的部分变量)

1
load('mydata.mat', 'var1', 'var2', ...);

注意:

  • 如果当前工作区的变量和 MAT 文件中的变量重复,那么 MAT 文件中的变量值就会直接覆盖它!
  • 在加载MAT文件时我们可能需要传递文件格式,默认格式是 -mat,也支持 -ascii 等选项,格式的判断与文件后缀无关;

我们可以使用变量接受 load 的返回值,这样会把所有变量加载到一个结构体数组中,作为结构体数组的字段,而不是直接暴露在工作区中

1
2
3
4
data = load('mydata.mat');

disp(data.A);
disp(data.B);

补充

loadsave 相关函数接口看起来非常奇怪,几乎所有参数都是字符数组或字符串形式,这其实是为了与命令式语法兼容。

可以完全用命令式的语句完成变量的保存和加载

1
2
3
4
5
6
save all_data.mat
save pqfile.mat p q
save mydata.mat D -append

load mydata.mat
load mydata.mat var1 var2