1. 为什么说Matlab的灵魂是矩阵
1.1 矩阵在Matlab里的地位
很多入门教程第一节课都会说“Matlab是矩阵实验室的缩写”,但真把这个信息当回事的人并不多。我见过不少从其他语言转过来的开发者,习惯性地把矩阵当成二维数组,然后处处用for循环去遍历,结果代码又慢又啰嗦。这其实是用写C或Python的思维在写Matlab,相当于拿法拉利去犁地。
在Matlab里,矩阵不是“一种数据结构”,它几乎是全部。你输入一个数字,它是一个1×1的矩阵;你读入一张图片,它本质上是三个二维矩阵叠在一起;你处理一段音频,它就是一列矩阵。万物皆矩阵,这句话在这门语言里不是比喻,而是字面意义上的事实。很多初学者学不下去的根源,就是没有建立这种“矩阵优先”的思维。
比如你想给一组数做平方,如果脑子里只有“循环遍历”,你会写成for i = 1:n这一套,但Matlab的思路是直接把整组数当成一个矩阵,用点运算符.^一次性搞定。同样的结果,第二种写法不仅短了一个量级,运行速度还快得多。后面我会详细展开这些操作,现在你只需要记住两条:第一,Matlab默认一切都是矩阵;第二,优先考虑整块操作,而不是逐个操作。
1.2 这篇能解决什么问题,适合谁看
这篇文章是“从零开始学Matlab”系列的第一篇,聚焦三个最基础也最关键的能力:矩阵的基本操作、数据的输入输出、函数的编写。这个顺序是有讲究的,矩阵操作是根,输入输出是管道,函数是组装方式。掌握这三样,你就能看懂网上大部分入门级别的Matlab代码,也足够支撑你处理课程作业里那些最简单的数值计算任务。
适合看这篇教程的读者有三类:完全没接触过Matlab的纯新手,想快速上手跑通第一个程序;学过其他编程语言但对Matlab的矩阵思维不熟悉的老开发;以及用过Matlab但一直靠到处复制代码、没系统梳理过基础操作的半熟手。这三类人读完这一篇,应该都能挖到一些以前忽略的细节。我的建议是别光看,打开Matlab跟着敲,一篇文章下来,肌肉记忆就建立起来了。
2. 矩阵的基本操作:从创建到修改
2.1 手工输入一个矩阵:第一条命令
在Matlab命令行窗口里输入下面这行,然后回车:
A = [1 2 3; 4 5 6; 7 8 9]你会看到工作区里多了一个叫A的变量,命令行窗口会回显一个3×3的矩阵。这就是手工创建矩阵最直接的方式。这里有几个隐藏规则需要立即搞清楚:方括号[]表示把元素拼成矩阵;空格(或逗号)分隔同一行的不同元素;分号;表示这一行结束、换到下一行。所以[1 2 3; 4 5 6; 7 8 9]就是三行三列,而不是三列三行。
有个细节初学者经常踩坑,就是分号的作用。在Matlab里,分号有两种完全不同的身份:在方括号内部时,它表示矩阵换行;在语句结束时,它表示“执行但不显示结果”。比如你输入A = [1 2 3]; 这个分号让命令行不打印结果,但不影响A这个变量已经赋值。刚开始你可能觉得这样很绕,实际用两天就会习惯,反而会觉得方便——不想看中间过程时加分号,想看时就不加,完全由你控制。
另一个实用技巧是换行输入。如果一个矩阵太大,一行写不下,可以在方括号内直接换行,再补一个分号,比如:
B = [1, 2, 3 4, 5, 6 7, 8, 9];这种方式尤其适合录入系数矩阵,视觉上跟数学课本里的写法几乎一致,不容易错位。我个人在项目里录小矩阵时基本都用这种换行格式,看着舒服,排查问题也快。
2.2 用命令生成特殊矩阵:零矩阵、单位矩阵、随机矩阵
手工输入适合小矩阵,但如果你需要一个100×100的零矩阵,手工敲显然不现实。Matlab内置了一批生成特殊矩阵的命令,这几个是必须背下来的:
zeros(3, 4) % 3行4列的全零矩阵 ones(2, 5) % 2行5列的全1矩阵 eye(3) % 3阶单位矩阵(对角线为1) rand(3, 2) % 3行2列,元素在[0,1]均匀分布的随机矩阵 randn(2, 3) % 2行3列,服从标准正态分布的随机矩阵 magic(3) % 3阶幻方矩阵,每行每列和对角线和相等这些命令的共同特点是第一个参数是行数,第二个是列数。如果只给一个参数,比如eye(3),那就是方阵。这是Matlab一个贯穿始终的约定,很多初学者搞反行列,就是因为没注意这个参数顺序。
另外还有两个非常常用的生成工具:冒号运算符和linspace。冒号运算符用来生成等差向量,比如1:0.5:3会生成[1 1.5 2 2.5 3],意思是“从1开始,步长0.5,到3为止”。linspace(0, 1, 5)则是在0到1之间均匀取5个数,结果类似。两者区别在于:冒号严格按步长走,可能恰好停不到终点;linspace永远把首尾和数量算死,均匀分布。做坐标轴、画图横坐标时,linspace用得更多。
2.3 从矩阵里取数据:索引与切片
矩阵创建好了,下一步就是怎么把里面的某个值、某一行、某一列拿出来。Matlab的索引规则跟主流编程语言有一个特别大的差异:下标从1开始,不是从0开始。也就是说A(1,1)才是第一行第一列的元素。很多从C、Python转过来的朋友,第一周总会习惯性写0,然后报错“索引超出范围”,这个坑几乎是必踩的,只能说习惯就好。
常见的索引写法有这些:
A(2, 3) % 取第2行第3列的元素 A(1, :) % 取第1行的所有列,结果是一个行向量 A(:, 2) % 取第2列的所有行,结果是一个列向量 A(2:3, 1:2) % 取第2到3行、第1到2列组成的子矩阵 A(end, end) % 取最后一行、最后一列的元素 A(:) % 把A按列从上到下拉成一个列向量冒号在这里的意思是“全部或一段范围”,这个语义非常灵活。A(2:3, 1:2)里的2:3和1:2就是前一个小节讲的冒号运算符生成的范围向量。end关键字代表“这一维度的末尾”,当你不知道矩阵到底有多大的时候,它可以救你一命。
切片取出来的数据可以直接参与赋值。比如A(:, 3) = [0; 0; 0]就能把第三列整体替换成0。这里要注意,右边的向量元素数量必须跟左边被选中的位置数量完全一致,否则会报错“赋值维度不匹配”。实际项目里,我经常先用size(A)看一眼矩阵尺寸,再动手切片,尤其是从外部文件读进来的数据,尺寸往往跟你预期的不一样。
2.4 修改矩阵:拼接、换形状、整体替换
除了改单个元素,我们还经常需要把两个矩阵拼起来。横向拼接用空格或逗号,纵向拼接用分号,这一点跟创建矩阵的规则一脉相承:
C = [A, B]; % A在左,B在右,横向拼成更宽的矩阵 D = [A; B]; % A在上,B在下,纵向拼成更高的矩阵拼接的前提是尺寸匹配:横向拼接时,两个矩阵行数必须相等;纵向拼接时,列数必须相等。这个匹配检查是Matlab自动做的,不满足会直接报错,倒也不会造成更严重的问题。
reshape命令用来改变矩阵的形状,但不会改变元素总个数,也不会改变元素在内存里的顺序。比如一个3×2的矩阵,里面有6个元素,可以reshape成2×3或者1×6,只要乘积能对上就行。这里容易犯错的是reshape的填充顺序,它是按列逐个填充的,跟前面A(:)的拉直方向一致。如果你想按行填充,可以先对矩阵转置再reshape,或者用reshape(..., 的转置绕一下。
说到转置,A.'表示普通转置,A'表示共轭转置。对于实数矩阵,两者结果完全一样,所以大多数人直接写A'。但如果矩阵里有复数,这两个差别就大了,A'会把虚部变号。这点在做信号分析的时候会用到,现在记住就好。
3. 矩阵运算:加减乘除与点运算
3.1 加减运算和数乘
矩阵加减非常简单,就是要求两个矩阵维度完全一致,对应位置的元素相加减。比如一个3×2矩阵加另一个3×2矩阵,没问题;但如果一个是3×2,一个是2×3,Matlab会直接拒绝,因为维度不匹配。这个规则跟线性代数教材里完全一样,学过线性代数的同学可以无缝对接。
数乘就更简单了,一个标量跟矩阵相乘、相除,结果是矩阵里每个元素都乘以或除以这个标量。比如2 * A就是A的每个元素翻倍,A / 4就是每个元素除以4。这里的乘号不用加点,因为它本质是广播到每个元素的运算。需要注意,在Matlab里,标量与矩阵的加减其实也是支持的,比如A + 1会把1自动加到每个元素上,这在很多传统编程语言里反而没有这么方便。
3.2 真正的矩阵乘法:维度匹配才是关键
矩阵乘法的符号是*,但它跟元素之间的乘法完全不是一回事。两个矩阵A和B相乘,要求A的列数等于B的行数,结果的第i行第j列是“A的第i行”和“B的第j列”对应元素相乘再求和。这是线性代数里的标准定义,Matlab严格执行。
举个例子,A是3×2,B是2×4,那AB的结果是3×4。如果你不小心把A和B反过来,BA因为“4不等于3”直接报错。这个维度规则是矩阵运算里最容易翻车的点,我经常在代码里调试半天,最后发现是因为把两个矩阵的乘除顺序写反了。
一个值得养成的习惯是:拿不准乘法能不能做的时候,先分别size(A)和size(B),看一眼列数和行数对不对,再动手。这一步十秒钟就能完成,却能为后面省下大量排错时间。
3.3 点运算:逐个元素计算的精髓
点运算是Matlab最迷人的特性,也是区分“会用数组语言”和“只会写循环”的分水岭。所谓点运算,就是在运算符前面加一个小数点:.*,./,.^。它表示“两个矩阵对应位置的元素逐个计算”,而不是线性代数里的矩阵乘法。
我举个例子你就明白了。假设有两个同维矩阵:
A = [1 2; 3 4]; B = [5 6; 7 8];A * B是标准矩阵乘法,结果是一个2×2矩阵。而A .* B是把A(1,1)乘以B(1,1)、A(1,2)乘以B(1,2),以此类推,结果同样是2×2,但数值完全不同。 .^也是同样道理,A .^ 2是每个元素平方,而A^2是矩阵自己乘自己,也就是A*A,尺寸都不一定一样。
点运算最大的价值在于向量化。假设你有一个包含1000个数据点的列向量x,想计算y = x.^2 - 2*x + 1,一条语句就完成,不需要任何循环。这一点在实际项目里太重要了,因为Matlab的for循环效率相对其他编译型语言要慢,能用点运算解决的绝不写循环,这是Matlab性能优化的第一条铁律。
3.4 转置、逆矩阵、行列式与线性方程组的求解
除了四则运算,矩阵相关的几个“高等操作”也属于基本功。转置用A'或A.',逆矩阵用inv(A),行列式用det(A)。这几个命令我在处理线性代数问题时会成套使用。
逆矩阵的作用是解线性方程组。假设方程组是A*x = b,理论解法是x = inv(A) * b,但实际更推荐用左除运算符:x = A \ b。为什么?因为左除在底层会优先尝试高斯消元等数值稳定性更好的方法,速度更快,结果也更稳。inv求逆在某些病态矩阵下可能出现很大的数值误差,而左除能绕开显式求逆这一步。所以我在项目里的习惯是:能不用inv就不用inv,解方程一律用\。
行列式det(A)则常用来判断矩阵是否奇异。如果行列式等于0或者非常接近0,意味着矩阵不可逆,这时候继续求逆或者解方程都会出错。在实操里,我一般先用abs(det(A)) < 1e-10这种阈值做个判断,再决定后续步骤。这样虽然简单粗暴,但至少能在早期拦住一大堆数值灾难。
4. 输入输出:让数据顺畅进出
4.1 命令行交互:input、disp、fprintf
Matlab最直接的输入方式就是用户在命令行里手动输入变量,这在编写交互式小工具时非常常见。你需要input函数:
n = input('请输入一个数字:');这里要注意,如果你的输入是数字,这样写没问题。但如果想输入字符串,需要加一个参数:
name = input('请输入姓名:', 's');未加's'的话,读者输入字符串时会直接被Matlab当作变量名去解析,十有八九报错“未定义函数或变量”。这是个特别典型的新手错误,因为官方文档里写得不算醒目,很多人用到了才发现。
输出方面,最基础的是disp,它只是把内容打印出来,比如disp(A)会显示矩阵内容。想要控制格式,就得用fprintf。fprintf的用法继承自C语言的printf,最大价值在于格式化:可以用%f、%d、%s等占位符把数值、整数、字符串嵌进一段文字里。比如:
fprintf('圆的面积是 %.2f\n', 3.14159 * 2^2);这样会输出“面积是 12.57”,保留两位小数。格式化输出在调试和生成报告时非常有用,特别是跟循环配合,可以按行输出一张表格。
4.2 读取Excel和CSV:readmatrix、readtable、writematrix
在实际项目中,数据很少是在Matlab里手敲的,更多情况是从Excel、CSV文件读进来。这里要分情况选择函数:如果文件里全是数值,clean一点的读法是readmatrix,它直接返回一个矩阵;如果你需要同时读表头和混合类型数据,那就用readtable,它返回一个表格对象。
data = readmatrix('成绩表.xlsx'); % 全部数值时用 tbl = readtable('学生信息.csv'); % 含文本表头时用写入方向则对应writematrix和writetable。这两个函数都是新版Matlab推荐的做法,旧一点的xlswrite、csvwrite现在已经不太建议用了,功能弱还经常出兼容问题。我刚开始用的时候还在用旧函数,后来发现新版文件读取函数对中文路径、特殊字符的处理也更好,索性全部切换过来了。
这里有个细节:readmatrix读取的文件里如果有非数值单元格(比如标题行、文字备注),它会自动处理成NaN或直接忽略,具体行为取决于参数设置。我的建议是,如果文件结构是“第一行是表头,下面全是数”,最好用readtable甚至直接用readmatrix('文件', 'NumHeaderLines', 1)跳过表头,不要让标题影响后面的计算。
4.3 .mat文件:保存和加载工作区变量
.mat是Matlab的原生数据格式,它的好处是能把整个工作区的变量打包保存下来,下次要用时一键加载。使用方式就两条命令:
save('data.mat'); % 保存工作区所有变量 save('data.mat', 'A', 'B');% 只保存A和B load('data.mat'); % 加载所有变量这个机制在长项目里几乎是救命稻草。比如一个模拟计算要跑几十分钟,你不可能每次都从头算,把中间结果保存成.mat,下次load进来接着算就行。load会把变量直接放进工作区,不需要手动赋值,这一点跟readmatrix返回结果给变量的风格不同,初次用可能会愣一下。
还有一个实用操作是load函数的第二种调用方式:
S = load('data.mat');这样不是把变量撒进工作区,而是把所有变量装进结构体S里,通过S.A、S.B访问。当你不确定文件里到底有哪些变量,或者想避免覆盖工作区已有变量时,这种用法更安全。我在处理别人传来的数据文件时,几乎都用这种形式,因为真的遇到过load之后工作区变量被同名覆盖的惨案。
4.4 文件读写的几个雷区
文件读写看着简单,实际项目里坑其实是重灾区。第一个雷区是中文路径和文件名。老版本Matlab对中文路径支持不好,open、readmatrix都可能直接报错或者读到乱码。现在新版基本没问题了,但如果你的同事还在用老版本,最好提醒对方把路径改成英文。
第二个雷区是分隔符。CSV文件的默认分隔符是逗号,但如果数据里某些字段本身含逗号,或者从某些地区来的文件用了分号当分隔符,readmatrix就乱了。这时候需要显式指定:readmatrix('文件.csv', 'Delimiter', ';')。判断文件用的是什么分隔符,拿记事本打开看一眼就行,别盲目跑代码。
第三个雷区是数据维度。读取Excel时,如果你读的区域里面有多余的空行空列,readmatrix会把它们读成NaN,导致后续所有计算出现NaN传染。我的排查习惯是读进来之后立刻用size、ismissing、sum(isnan(data(:)))检查一遍数据健康度,发现问题马上回头清理源文件,而不是在计算结果里追根溯源。
5. 函数:写一次用十次
5.1 脚本与函数的本质区别
Matlab代码有两种基本文件类型:脚本(.m)和函数(.m)。脚本就是一字排开的命令序列,按顺序执行,里面出现的变量全部存放在工作区,执行完就留下来了。函数则是一个“黑箱”,它只负责接收输入参数、执行内部逻辑、返回结果,除了return出来的东西,其他中间变量都不会污染工作区。
用生活类比来说,脚本像是你亲手按菜谱一步步做菜,做完厨房里堆了一堆用过的碗碟;函数像是点外卖,输入菜单点下单,拿到的是成品,厨房保持干净。在项目里,脚本适合写“一次性流程”,函数适合写“会反复调用的逻辑单元”。
还有一个关键区别:函数文件名必须与函数名一致。比如你定义function y = myFunc(x),文件名就必须是myFunc.m,否则Matlab会找不到这个函数,或者调用时行为诡异。这个规则很死板,但遵守它其实是在帮你建立好习惯,一看文件名就知道函数是干嘛的。
5.2 函数定义、参数和返回值
一个最基础、最标准的函数定义长这样:
function y = myFunc(x) y = x.^2 + 2*x + 1; endfunction关键字开头,后面跟返回变量y、函数名myFunc、输入参数x,end结尾。函数体可以是任意多行计算。如果有多个返回值,用方括号包起来,中间用逗号分隔:
function [sumVal, meanVal] = computeStats(v) sumVal = sum(v); meanVal = mean(v); end调用时就用[a, b] = computeStats(data);来接收两个结果。如果调用时只写computeStats(data),那么只有第一个返回值sumVal会被接收,第二个会被忽略。这个小细节在写大项目时容易让人困惑:函数明明算出了两个结果,但你怎么调用都拿不到第二个,检查半天才发现原来返回值没有接全。
函数默认是按值传递的,也就是说你在函数内部改输入参数的值,不会影响外部的原变量。想修改外部变量,最标准的做法是把修改后的值作为返回值传回来,而不是在函数内部直接改。这是Matlab跟某些语言的明显差异。
5.3 匿名函数:轻量好用的临时函数
如果你只需要一个简单的数学表达式,不想单独建一个.m文件,匿名函数是最合适的工具。它的语法是:
f = @(x) x.^2 + 1; f(2) % 结果是5@符号后面括号里写参数名,括号后面直接写表达式。匿名函数可以捕获当前工作区里的变量,这是它非常好用的一个特性。比如你有一个常数k = 3,然后定义g = @(x) k * x,那么g(2)算出6。如果你后续修改k,g的函数行为也会跟着变吗?答案是不变。匿名函数在定义那一刻把k的值快照下来,之后改k影响不了它。这个坑我踩过好几次,写代码时要特别注意。
匿名函数还有一个高频用途是作为参数传给其他函数。比如Matlab内置的fzero、integral、fminsearch这些数值求解函数,都要求你传一个函数句柄进去。匿名函数给了你一个不建文件就能传函数对象的途径,非常轻量。
5.4 函数调试与命名心得
编程里最浪费时间的事之一就是调试一个拼写错误。Matlab里的变量名和函数名是区分大小写的,所以MyFunc、myfunc、MYFUNC是三个完全不同的名字。这也是新手最容易卡的坑。我的建议是统一用小写字母和下划线命名,比如compute_mean、load_data,手感统一且不容易出错。
调试函数时,我强烈建议学会用断点。在编辑器的行号旁边点一下,会出现一个红点,运行到这一行时程序会暂停,你可以查看所有变量的当前值。这一步比任何打印语句都好用,尤其是在函数里,你可以看到工作区里被隔离的内部变量长什么样。配合fprintf打印关键变量,基本可以应付入门阶段百分之九十五的调试需求。
6. 常见报错和排查技巧
6.1 索引相关的报错
“索引超出矩阵维度”是我见过出现频率最高的报错之一。典型场景是这样的:你从数据文件读进来一个矩阵,以为它有5列,但实际上文件只有4列,然后你写data(:, 5),Matlab二话不说就报错。解决办法也很直白:报错之后先不要急着改代码,先看看size(data)到底是不是你想象中的尺寸,很多时候问题出在数据本身,而不是代码逻辑。
另一个高频索引错误是写成了A(0)或者A(0, 1)。因为Matlab索引从1开始,所以任何索引为0的操作都会触发报错“数组索引必须为正整数或逻辑值”。这个报错只要记住规则就能避免。如果你是从其他语言转来的,需要做的不是说“我知道”,而是真的在敲代码时保持警惕,这大概需要一两周的适应期。
6.2 运算符相关的经典错误
点运算忘写,是另一个每天都能在论坛里看到的错误。比如你有两个维度一样的矩阵A和B,你想做元素级相乘,却写了A * B,结果Matlab告诉你维度不对。如果两个矩阵恰好都是方阵,那更麻烦,程序不报错但结果算错了,这种错误最难发现。我的建议是:凡是涉及“逐个元素”的计算,比如图像处理、信号逐点运算、批量数据变换,一律先写上点,再检查一遍。
还有一个小坑是^和.^的混用。标量对矩阵求幂A^2是矩阵自乘,元素平方必须用A.^2。尤其是当A是向量时,A^2一定会报错,因为你没法直接做“向量自乘”。遇到这类问题,去看报错信息里提示的“矩阵维度必须一致”或者“用于矩阵幂的维度不正确”,基本就能定位到是点运算问题了。
6.3 工作区变量被覆盖和clear、clc
脚本执行后,变量全部留在工作区,这本身很自由,但也带来一个麻烦:如果你脚本里用了x = 5,而工作区里早就有一个x是用来做别的,那你的程序里所有使用x的地方都会变成5,而且毫无提示。这个“隐式覆盖”非常让人头疼,尤其当你分段执行代码块的时候。
我的对策是两条:第一,在每个脚本开始时习惯性加一段开头三件套:
clear all; % 清除所有变量 close all; % 关闭所有图形窗口 clc; % 清空命令行第二,给变量起有意义的名字,避免全用a、b、x这种单字母。别小看这件事,命名清晰能帮你避免一半的“看起来代码没错但结果就是不对”的诡异问题。我后来接手过一个没命名规范的项目,里面全是x1、x2、x3这种变量,排查问题像开盲盒,极其痛苦。
6.4 提升排查效率的两个习惯
第一个习惯是“分段运行”。在编辑器中选中一段代码,按F9,只执行选中的这部分;或者用%%分成一个个小节,可以单独运行某一节。这样出错时能快速定位是哪一段的问题,而不是每次从头跑一遍。
第二个习惯是“先小后大”。换到一个新的数据集,或者改写了一段核心逻辑之后,先用一个5×5的小矩阵测试一遍,确认结果与手算一致,再上几百上千行的大矩阵。很多人喜欢直接用真实数据跑,一旦结果不对,很难分清是算法问题还是数据问题。小规模验证能逼出数据层面的错误,让问题暴露在可控范围内。这个方法几乎不需要额外成本,但对新手来说收效极快。
写在后面的一些个人体会
教过不少新手之后我发现,学Matlab最大的障碍其实不是语法,而是心态。很多人一上来就想做出一个看起来很厉害的东西,结果遇到一个“索引超出范围”就卡住,然后开始怀疑自己。实际上,这些报错每一个都是必经之路,我都踩过,甚至到现在偶尔还会踩。矩阵、输入输出、函数这三块东西,你只要敲得足够多,自然就通了,没有捷径。
另一个我真心的建议是学一点线性代数的基础知识。Matlab把矩阵运算做得再方便,你至少要知道矩阵乘法的维度规则、转置是什么意思、逆矩阵存在的前提。不用多深,理解概念就行,因为你在网上看到的每一个Matlab例子里,几乎都在用这些东西。数学概念和代码实现两件事互相加持,你会学得比只盯代码的人快很多。
最后,如果读完这篇你觉得“好像都明白了”,那我还得泼一盆冷水:趁热打铁,把文章里每段代码都亲手敲一遍,然后试着改一改参数,看看输出会怎么变。动手试错的过程,才是真正把知识焊进脑子的过程。下一篇我会接着讲矩阵的高级操作、循环与向量化的取舍,以及如何用Matlab做基础的数据可视化,到时候咱们继续。