---
layout: doc
title: M 文件与自定义函数规范
breadcrumb: M 文件与函数规范
description: MATLAB 脚本与函数的编写差异、变量及函数的命名规范、代码布局、健壮性逻辑设计、控制流结构，以及命令行矩阵计算器实践项目。
---

# M 文件与自定义函数规范

为了写出易读的代码，本章将介绍 MATLAB 的代码编写规范和结构化基础。

## 1. 脚本 (Script) 与函数 (Function)

MATLAB 代码文件均以 `.m` 为后缀，主要分为脚本与函数两类。理解它们在作用域上的区别非常重要。

### 1.1 核心知识点：内存隔离与作用域

*   **脚本 (Script)**：脚本内的代码直接运行于**主工作区 (Base Workspace)**。脚本执行过程中产生的所有变量均对全局可见，且在程序结束后驻留内存。如果多次运行不同的脚本，极易发生隐式变量覆盖与内存泄漏。
*   **函数 (Function)**：函数运行于独立的**局部工作区 (Local Workspace)**。除通过参数列表显式传递的输入输出变量外，函数内部创建的所有中间变量在函数调用结束时会立刻被内存回收，从而实现了严格的作用域隔离与模块化。

### 1.2 特征对比表

| 维度 | 脚本文件 (Script) | 函数文件 (Function) |
| :--- | :--- | :--- |
| **定义形式** | 顺序执行的命令集合，不包含函数声明头。 | 以 `function` 关键字开头，显式声明输入输出参数。 |
| **数据传递** | 无法直接进行参数传递，不具备返回值。 | 通过输入与输出参数列表进行明确的数据交换。 |
| **使用场景** | 流程控制入口、系统初始化或测试绘图。 | 独立数学算法模块封装或高内聚功能模块实现。 |

### 1.3 代码结构直观对比

```matlab
% --- 脚本示例 (main_script.m) ---
% 直接顺序执行，变量 a, b 会永久留在主工作区，直到手动清除
a = 10;
b = 20;
disp(a + b);

sum = add_numbers(a, b);
disp(sum);

% --- 函数示例 (add_numbers.m) ---
% 以 function 声明，拥有独立的局部工作区，执行完毕后变量 x, y 自动释放
function result = add_numbers(x, y)
    result = x + y;
end
```

### 闯关题目 1：内存与作用域

假设在主工作区（Command Window）中已经存在变量 `count = 10`。此时你运行了一个脚本文件 `test_script.m`，其内容仅有一行 `count = 5;`。紧接着你又调用了一个函数 `test_func()`，该函数内部也包含一行 `count = 100;`。
请问：依次执行完脚本和函数后，在主工作区查询 `count` 的值，返回结果是多少？

<details>
<summary>▶ 查看参考答案</summary>

返回结果是 `5`。
**解析**：
1. 主工作区初始有 `count = 10`。
2. 运行脚本 `test_script.m` 时，由于脚本**不隔离内存**，直接在主工作区运行，因此主工作区的 `count` 被覆盖为 `5`。
3. 调用函数 `test_func()` 时，由于函数拥有独立的**局部工作区**，它内部的 `count = 100` 只是局部变量，函数执行完毕后即被销毁，**不会**影响主工作区的数据。

</details>

## 2. 命名规范与风格

### 2.1 核心知识点：标识符解析机制

MATLAB 解释器对外部函数的检索和解析完全依赖于计算机文件系统中的文件名（`.m` 文件名），而非文件内部第一行的函数声明名称。因此，文件命名必须严谨且与主函数绑定。

### 2.2 命名约束

*   **文件命名与主函数绑定**：使用全小写字母并以下划线分隔（`snake_case`），如 `class_manager.m`。M 文件中的**主函数名称必须与文件名完全保持一致**。
*   **局部变量**：采用小驼峰命名法（`camelCase`），即首字母小写，后续各单词首字母大写。例如 `studentTable`、`numStudents`。
*   **常量命名**：采用全大写字母并使用下划线分隔。例如 `MAX_ITERATIONS`、`TOLERANCE`。
*   **循环计数器**：通常使用简短的 `i`、`j`、`k`。但需注意，若工程中涉及复数分析，建议使用 `ii`、`jj` 作为循环索引，以避免覆盖 MATLAB 默认的虚数单位 $\sqrt{-1}$。

### 闯关题目 2：规范辨析

指出下列变量或文件命名存在的问题，并给出推荐的修改方案：
1. 文件名 `CalculateSum.m`，内部函数为 `function y = calc_sum(x)`
2. 变量名 `Student_score`（表示单个学生的成绩）
3. 常量名 `pi_value`

<details>
<summary>▶ 查看参考答案</summary>

1. **问题**：文件名与函数名不一致，且文件名没有采用全小写下划线命名法。
   **修改**：文件名应改为 `calc_sum.m`，或者将内部函数名改为 `function y = CalculateSum(x)`（推荐遵循 `snake_case` 文件名标准，即两者都用 `calc_sum`）。
2. **问题**：局部变量没有采用小驼峰命名法。
   **修改**：应改为 `studentScore`。
3. **问题**：常量没有采用全大写下划线命名法。
   **修改**：应改为 `PI_VALUE`。

</details>

## 3. 自定义函数编写标准结构

一个规范的自定义函数应当包含文档说明，并且能处理错误的输入参数。

### 3.1 核心知识点：防御性编程与函数封装

*   **接口说明 (Help Block)**：规范的注释不仅用于阅读，MATLAB 的内置命令 `help` 和 `lookfor` 会直接解析函数文件头部的注释，生成官方标准格式的帮助文档。
*   **错误处理与校验**：在执行计算前，应当检查外部输入的正确性。可以使用内置指令（如 `size`、`isempty`）对输入的维度或边界进行检验。
*   **主函数与局部函数 (Primary & Local Functions)**：在同一个 `.m` 文件中允许定义多个函数。文件的第一个函数为**主函数**（对外可见，其名称必须与文件名一致）；其后定义的所有辅助函数为**局部函数 / 子函数**。局部函数仅供当前文件内部调用，有效防止了辅助模块污染外部命名空间。

### 3.2 标准结构示例

```matlab
function [out1, out2] = my_algorithm(in1, in2)
% MY_ALGORITHM  计算两个矩阵的特征差异 (H1 描述行)
%   [OUT1, OUT2] = MY_ALGORITHM(IN1, IN2) 接收两个形状相同的矩阵 IN1 与 IN2，
%   并计算出它们的相似度 OUT1 以及残差 OUT2。
%
%   输入参数:
%       in1 - 矩阵 A (double)
%       in2 - 矩阵 B (double)
%
%   输出参数:
%       out1 - 相似度评分 (0~1)
%       out2 - 二次残差值
%
%   创建时间: 2026-06-25, 作者: MatNoble

    % 1. 输入边界校验 (防御性编程)
    if ~isequal(size(in1), size(in2))
        error('输入矩阵 in1 和 in2 的维度必须严格一致。');
    end

    % 2. 核心算法执行
    temp_diff = calculate_diff(in1, in2);
    
    % 3. 赋值返回
    out1 = 1 - temp_diff;
    out2 = temp_diff^2;
end

% ---------------------------------------------------------------------
% 局部子函数 (仅对当前文件内的函数可见，增强封装性)
% ---------------------------------------------------------------------
function diff = calculate_diff(x, y)
    diff = norm(x - y) / (norm(x) + norm(y));
end
```

### 闯关题目 3：接口封装与防御性编程

在日常的工程开发中，对矩阵维度进行校验非常关键。请根据要求补全以下代码片段：
编写一个函数 `matrix_add` 接收矩阵 `A` 和 `B`，若二者尺寸不匹配或为空，则触发错误。

```matlab
function ______ = matrix_add(______)
    % 如果 A 或 B 是空矩阵，则抛出错误
    if ______(A) || ______(B)
        error('矩阵不能为空！');
    end
    
    % 如果 A 和 B 尺寸不相等，则抛出错误
    if ~isequal(______(A), ______(B))
        error('矩阵维度不匹配！');
    end
    
    result = A + B;
end
```

<details>
<summary>▶ 查看参考答案</summary>

```matlab
function result = matrix_add(A, B)
    % 如果 A 或 B 是空矩阵，则抛出错误
    if isempty(A) || isempty(B)
        error('矩阵不能为空！');
    end
    
    % 如果 A 和 B 尺寸不相等，则抛出错误
    if ~isequal(size(A), size(B))
        error('矩阵维度不匹配！');
    end
    
    result = A + B;
end
```

</details>

## 4. 编程规范与效率优化

### 4.1 I/O 输出缓冲机制与分号

所有进行赋值或运算执行的语句末尾，原则上均应显式加上分号 `;`。

**核心考量**：不加分号的指令会将运算结果推送到系统命令窗口（Command Window）。在庞大的循环迭代中，频繁触发控制台的 I/O 输出缓冲刷新，会导致解释器执行效率出现数量级的衰减。

### 4.2 异常处理机制 (try-catch)

对于涉及外部状态的操作（如文件读取 `load`/`save`、网络请求或用户控制台输入 `input`），可以使用 `try-catch` 结构来捕获错误。

> **术语解析：什么是 `ME`？**
> `ME` 是 **MException** (MATLAB Exception) 的行业缩写约定。当 `try` 块中的代码发生错误时，MATLAB 会自动生成一个包含错误详细信息的“异常对象”。通过 `catch ME`，我们将这个对象捕获并赋值给了变量 `ME`（你也可以自定义命名为 `err` 或 `e`）。随后，我们可以通过访问 `ME.message` 属性来提取具体的报错文本内容，从而实现友好的错误日志打印，避免程序直接崩溃。

```matlab
% 反面案例：若外部文件丢失，抛出异常将直接导致进程或主循环非正常中止
load('data.mat');

% 规范案例：异常隔离与安全降级
try
    load('data.mat');
    disp('数据加载成功。');
catch ME
    fprintf('数据加载失败，错误信息：%s\n', ME.message);
    % 在此处载入备用默认数据或安全退出当前操作流
end
```

### 4.3 向量化原理 (Vectorization)

在处理矩阵或大规模向量数据时，应尽量采用 MATLAB 内置的矩阵整体运算符，避免使用传统的 `for` 循环逐个遍历元素。

**原因**：MATLAB 中的 `for` 循环执行较慢，而内置的矩阵运算经过底层优化，执行效率显著更高。

```matlab
% 传统遍历：解释执行开销大，效率低下
x = 1:1000000;
y = zeros(1, 1000000); % 预分配内存可缓解一定压力，但并非最优解
for i = 1:length(x)
    y(i) = sin(x(i));
end

% 向量化计算：底层高度优化，执行效率显著提升
x = 1:1000000;
y = sin(x); 
```

### 闯关题目 4：性能与健壮性

**任务 A：异常捕获**
编写一段脚本尝试加载文件 `config.mat`。如果文件不存在，捕获异常并在控制台输出“配置文件丢失，已加载默认设置”，且不能让程序因报错而中断。

```matlab
______
    load('config.mat');
    disp('数据加载成功。');
______ ME
    disp('配置文件丢失，已加载默认设置');
______
```

**任务 B：向量化思维**
给定一个包含十万个随机数的数组 `X = rand(1, 100000)`，请分别用 `for` 循环和逻辑索引（向量化）两种方法，将其中所有小于 0.5 的元素替换为 0。

```matlab
% 传统 for 循环
for i = 1:______(X)
    if X(i) < 0.5
        X(i) = 0;
    end
end

% 向量化计算（一行代码实现）
X(______) = 0;
```

<details>
<summary>▶ 查看参考答案</summary>

**任务 A 参考答案：**
```matlab
try
    load('config.mat');
    disp('数据加载成功。');
catch ME
    disp('配置文件丢失，已加载默认设置');
end
```

**任务 B 参考答案：**
```matlab
% 传统 for 循环
for i = 1:length(X)
    if X(i) < 0.5
        X(i) = 0;
    end
end

% 向量化计算
X(X < 0.5) = 0;
```

</details>

## 5. 控制流结构

MATLAB 支持典型的结构化控制流，通过条件分支与循环实现算法逻辑的调度。

### 5.1 分支结构 (if-elseif-else)
```matlab
score = 85;
if score >= 90
    disp('优秀');
elseif score >= 80
    disp('良好');
else
    disp('需进一步巩固');
end
```

### 5.2 循环结构 (for 与 while)
```matlab
% for 循环：适用于已知迭代次数的场景
sum_val = 0;
for i = 1:100
    sum_val = sum_val + i;
end

% while 循环：适用于依据动态条件控制退出状态的场景
count = 1;
while count <= 5
    count = count + 1;
end
```

### 闯关题目 5：控制流实战

**任务 A：矩阵最值查找**
给定一个 $4 \times 5$ 的随机矩阵，请补全代码以求取其全局最大值，并返回该最大值所在的行号与列号。
```matlab
A = rand(4, 5); 
% 将矩阵拉平为单列向量寻找最大值及其线性索引
[max_val, linear_idx] = max(______); 
% 将线性索引转换为对应的行列下标
[row, col] = ______(size(A), linear_idx); 
fprintf('全局最大值为: %f, 位于第 %d 行, 第 %d 列\n', max_val, row, col);
```

**任务 B：阶乘求和优化**
补全程序计算 $\sum_{n=1}^{20} n!$ 的结果，提供两种实现版本。
```matlab
% 版本 1：基于控制流的传统计算
total_sum = 0;
curr_factorial = 1;
for n = ______
    curr_factorial = curr_factorial * n;
    total_sum = total_sum + curr_factorial;
end

% 版本 2：向量化计算
n = 1:20;
vectorized_sum = ______(______(n));
```

**任务 C：物理衰减模拟**
一弹性小球从 100 米高度自由落下，每次落地后反跳回原高度的一半。请通过控制流编程求解：它在第 10 次落地时，共经过的总量程是多少米？第 10 次反弹的高度是多少？
```matlab
height = 100;
total_distance = height; % 初始化为首次自由落体的距离

for i = 1:9 % 统计前 9 次的弹起和落下过程
    height = height / 2; % 衰减后的反弹高度
    total_distance = total_distance + ______; % 累加单次完整的弹起与下落量程
end

height_10 = ______; % 第 10 次的独立反弹高度
fprintf('第 10 次落地时累计经过: %f 米\n', total_distance);
fprintf('第 10 次反弹高度为: %f 米\n', height_10);
```

<details>
<summary>▶ 查看参考答案</summary>

**任务 A 参考答案：**
```matlab
[max_val, linear_idx] = max(A(:)); 
[row, col] = ind2sub(size(A), linear_idx); 
```

**任务 B 参考答案：**
```matlab
for n = 1:20

vectorized_sum = sum(factorial(n));
```

**任务 C 参考答案：**
```matlab
    total_distance = total_distance + height * 2; 
    
height_10 = height / 2; 
```

</details>

## 6. 实践项目：命令行矩阵计算器 (Matrix Calculator)

作为本章的阶段性实战，需独立完成一个基于命令行交互的矩阵计算器应用。

### 6.1 考核知识点映射

1.  **结构化程序设计**：通过 `while` 循环维持计算器的主线程存活，运用 `switch-case` 进行多状态路由调度。
2.  **错误处理**：使用 `try-catch` 机制，在遇到非法的矩阵维度冲突时，向控制台输出警告而不会导致程序崩溃。
3.  **线性代数工程化**：使用内置指令实现秩 (`rank`)、行列式 (`det`) 的求解，并实现非奇异方阵的逆矩阵求解，同时使用左除算子 `\` 求解线性方程组 $Ax = B$。

### 6.2 实践开发规范说明

1.  **输入与边界校验**：在执行任何运算前，必须通过 `isempty` 检查操作数是否存在。
2.  **运算约束验证**：矩阵乘积操作需验证 $A$ 矩阵列数等于 $B$ 矩阵行数；点乘需验证双方维度严格一致。
3.  **超定与欠定方程组的处理**：
    *   在求解线性方程组 $Ax = B$ 时，程序不应局限于方阵。
    *   若 $A$ 是矩形矩阵，需根据行数与列数的关系，向用户明确提示该问题属于超定问题（返回最小二乘解）或欠定问题（返回最小范数特解）。
    *   求解完成后，要求计算并打印方程的残差范数 $||Ax - B||$ 以验证数值解精度。

### 6.3 框架代码模板

本模板已定义计算器的基础交互逻辑与 I/O 方法，请在标有 `TODO` 的区块补充核心计算逻辑：

```matlab
function matrix_calculator()
    % 初始化数据内存区域
    A = [];
    B = [];
    
    fprintf('==========================================\n');
    fprintf('           命令行矩阵计算系统             \n');
    fprintf('==========================================\n');
    
    while true
        fprintf('\n当前缓存状态:\n');
        show_matrix('A', A);
        show_matrix('B', B);
        
        fprintf('------------------------------------------\n');
        fprintf('  [1] 构建或录入矩阵 A\n');
        fprintf('  [2] 构建或录入矩阵 B\n');
        fprintf('  [3] 矩阵加减运算 (A ± B)\n');
        fprintf('  [4] 矩阵乘积 (A * B) 与 逐元素点乘 (A .* B)\n');
        fprintf('  [5] 矩阵转置变换\n');
        fprintf('  [6] 矩阵特征分析 (行列式 det / 秩 rank / 逆 inv)\n');
        fprintf('  [7] 线性方程组求解 (Ax = B)\n');
        fprintf('  [Q] 退出系统\n');
        fprintf('------------------------------------------\n');
        
        choice = strtrim(input('请键入操作指令 >> ', 's'));
        
        if strcmpi(choice, 'q')
            fprintf('系统安全退出。\n');
            break;
        end
        
        % 运用 try-catch 隔离计算错误，防止应用崩溃
        try
            switch choice
                case '1'
                    A = input_or_generate_matrix('A');
                case '2'
                    B = input_or_generate_matrix('B');
                case '3'
                    % TODO: 实现矩阵 A 与 B 的加减法
                    % 校验要求：校验非空状态，确保维度一致
                    
                case '4'
                    % TODO: 实现普通乘法与点乘运算
                    % 校验要求：建立子菜单选择运算类型，分别执行相应的维度校验规则
                    
                case '5'
                    % TODO: 实现矩阵的转置运算
                    
                case '6'
                    % TODO: 矩阵特征分析
                    % 提示：秩求解适用于任意矩阵。行列式与求逆要求矩阵必须为方阵。
                    
                case '7'
                    % TODO: 求解 Ax = B
                    % 要求 1：验证 A 与 B 行数一致
                    % 要求 2：调用左除算子求解，并计算残差 norm(A*x - B)
                    
                otherwise
                    fprintf('>> [警告] 无法识别的指令。\n');
            end
        catch ME
            fprintf('>> [错误异常] 计算流程中断，系统反馈: %s\n', ME.message);
        end
    end
end

%% 辅助模块：矩阵格式化输出
function show_matrix(name, M)
    if isempty(M)
        fprintf('  %s: [空]\n', name);
    else
        [r, c] = size(M);
        fprintf('  %s (%dx%d):\n', name, r, c);
        disp(M);
    end
end

%% 辅助模块：数据录入与生成
function M = input_or_generate_matrix(name)
    fprintf('\n--- 正在构建矩阵 %s ---\n', name);
    fprintf('  [M] 基于 MATLAB 语法手动录入 (例: [1 2; 3 4])\n');
    fprintf('  [I] 实例化单位矩阵\n');
    fprintf('  [R] 实例化随机矩阵\n');
    method = upper(strtrim(input('请指定构建模式 >> ', 's')));
    
    switch method
        case 'M'
            M = input('输入合法矩阵数据：');
        case 'I'
            n = input('输入目标阶数 n: ');
            M = eye(n);
        case 'R'
            r = input('输入行数: ');
            c = input('输入列数: ');
            M = randi([0, 10], r, c);
        otherwise
            fprintf('>> [警告] 操作取消。\n');
            M = [];
    end
end
```
