代码规范学习笔记_工程与刷题双视角

代码规范学习笔记:从工程到刷题

前言

最近在做两件事:写 SerialDAQ 的 Qt 代码,和刷力扣的 C++ 算法题。

两边的代码风格差异实在太大了——工程代码里我一个变量名要斟酌半天,刷题代码里却可以写 int l=0, r=n-1。我就想搞清楚:到底什么是"规范"?两套标准各自的底线是什么?

今天花了时间把我的 SerialDAQ 项目代码送去挨了一顿"代码审查",又把力扣刷题的套路整理了一遍,写成这篇笔记留给自己。


先搞清楚前提:两套规范的根本区别

维度 工程代码(SerialDAQ那种) 刷题代码(力扣那种)
生命周期 跑几年,多人维护 提交一次,自己用
可读性要求 极高,别人要能看懂 自己看懂就行
注释要求 Doxygen + 每个函数都要注释 一般不需要,顶多一行说明
变量命名 m_serialPort,见名知意 l, r, k, i 都合法
错误处理 防御性编程,到处检查 题目保证输入合法,不用检查
目标 可维护 + 可扩展 快速写出 + 通过测试

核心就一句话:工程代码是给团队写的,刷题代码是给评测机写的。 搞清楚了这个前提,下面分开讲。


第一部分:力扣刷题代码规范

我在刷力扣的时候踩了不少坑,很多都是"代码逻辑对,但写法不规范"导致 WA。

1. 函数签名绝对不要动

力扣给你的框架:

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        // 你填这里
    }
};

class Solution 不能改名,removeDuplicates 不能改名,参数列表不能加东西或删东西。形参名字可以改numsa 也合法),但不建议改,保持力扣原样最安全。

2. 短变量名是竞赛传统,但要有原则

刷题代码里以下命名完全合规,是竞赛界的惯例:

变量名 含义 适用场景
i, j 循环下标 任何循环
l, r 左右指针 双指针
k 慢指针/计数 快慢指针模板
n 数组长度 int n = nums.size()
res / ans 答案 最终结果
cnt 计数 统计数量

但是,如果你要发博客或者面试现场写代码,变量名要有意义——不是为了编译器,是为了看代码的人。

3. int n = (int)nums.size() — 这行必须写

我之前经常这样写:

for (int i = 0; i < nums.size() - 1; i++)  // ❌ 有陷阱!

问题nums.size() 返回的是 size_t(无符号整数)。当数组为空时,0 - 1 不会等于 -1,而是无符号下溢变成一个天文数字(18446744073709551615)——你的循环会直接炸掉或者永远不进去。

正确写法

int n = (int)nums.size();
for (int i = 0; i < n - 1; i++)  // ✅ 安全

而且 n 提前算一次,不用每次循环都调 nums.size(),更高效。

4. 边界条件写在最前面

这个是我被 WA 打出来的经验——很多题逻辑没错,就是边界没考虑:

int removeDuplicates(vector<int>& nums) {
    if (nums.empty()) return 0;     // ✅ 空数组放最前面
    
    int k = 1;
    for (int i = 1; i < (int)nums.size(); i++) {
        if (nums[i] != nums[k - 1]) {
            nums[k++] = nums[i];
        }
    }
    return k;
}

常见边界

  • 数组为空(nums.empty()
  • 数组只有一个元素
  • 全部相同 / 全部不同

5. 返回值要写真正的答案,不要糊弄

我踩过一个坑,代码最后写了 return nums.size() 而不是 return k,虽然恰好能通过,但语义上全错了——函数要求返回的是唯一元素个数,不是数组大小。

任何地方都不要靠运气过题

6. 不需要写 #include,力扣帮你做了

力扣的隐藏代码已经帮你包含了所有需要的头文件和 using namespace std;。你自己加 #include <bits/stdc++.h> 虽然不扣分,但在面试白板上这样写会显得不专业。面试手写代码时,该 std::vector 就老老实实写 std::vector

7. auto 的使用原则

// ✅ 迭代器用 auto(类型太长)
for (auto it = seen.begin(); it != seen.end(); it++) {}

// ✅ 范围 for 用 auto
for (auto& num : nums) {}

// ❌ 普通变量不建议用 auto——别人不知道是什么类型
auto k = 0;   // int? long? 看不出来
int k = 0;    // ✅ 一目了然

8. 提交前检查有没有调试代码

我之前提交时留着 cout 调试输出忘记删,虽然不报 WA,但这是坏习惯。以后提交前扫一眼:有没有 coutprintfcerr 残留。


第二部分:工程代码规范 — 以 SerialDAQ 为例

我让 AI 把我的 SerialDAQ 项目全部核心代码扫了一遍,挨个挑问题。下面是我被挑出来的毛病,按严重程度排列。

🔥 问题 1:#include 顺序乱七八糟

我写的(mainwindow.cpp):

#include "mainwindow.h"
#include "logger.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
...
#include <cmath>

项目自己的头文件和 Qt 的头文件混在一起,没有分组。

规范做法(Google C++ 风格 + Qt 惯例):

// 1. 自己的头文件(对应的 .h)
#include "mainwindow.h"

// 2. 项目内其他头文件
#include "logger.h"

// 3. Qt 头文件
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTimer>

// 4. C++ 标准库
#include <cmath>

为什么自己的头文件要放第一位? 这样能让编译器强制验证这个头文件是"自给自足的"——如果你自己的 .h 漏了哪个依赖,编译器会直接报错,而不是被前面 include 的东西默默兜住。

🔥 问题 2:魔数太多了

我的代码里到处都是含义不明的数字:

m_maxHistoryCount(20)          // 为什么是 20?
m_condition.wait(&m_mutex, 100) // 100 是什么?100毫秒?
frameLen - 4                    // 4 是什么?
3 + i * 4                       // 为什么起点是 3?步长为什么是 4?

这些问题面试官一眼就能看到,然后追问:"这些数字什么意思?如果协议改了你怎么改?"

规范做法:给每个数字起名字,名字就是注释:

namespace SerialProtocol {
    constexpr int  HEADER_SIZE    = 2;   // 帧头 0x55 0xAA
    constexpr int  LEN_FIELD_SIZE = 1;   // 帧长字段
    constexpr int  CHECKSUM_SIZE  = 1;   // 校验字段
    constexpr int  OVERHEAD       = HEADER_SIZE + LEN_FIELD_SIZE + CHECKSUM_SIZE;
    constexpr int  FLOAT_SIZE     = 4;   // IEEE754 float 字节数
    constexpr int  WAIT_TIMEOUT_MS = 100;
}

// 使用时代码变这样:
int floatDataLen = frameLen - SerialProtocol::OVERHEAD; // 一目了然

🔥 问题 3:C 风格强转

我在 serialreceivethread.cpp 里用了大量 C 风格的强转:

quint8 frameLen = (quint8)m_buffer[2];     // 这样写
bytes[0] = (quint8)data[3 + i * 4];        // 还有这

(quint8) 是 C 风格转换,会绕过 C++ 的类型安全检查,而且很容易在代码里看漏。

规范做法:用 C++ 的 static_cast

quint8 frameLen = static_cast<quint8>(m_buffer[2]);

static_cast 明确告诉读代码的人:"我知道我在做类型转换,这是有意的。"

🟡 问题 4:setupUI() 函数太长了

我的 setupUI() 有大约 180 行,把串口设置、存储设置、Tab 页面、状态栏全堆在一起。

规范做法:一个函数只做一件事:

void MainWindow::setupUI()
{
    setupSerialPortGroup();    // 串口设置区域
    setupStorageGroup();       // 数据存储区域
    setupTabWidget();          // Tab页面区域
    setupStatusBar();          // 状态栏区域
}

判断标准:函数超过 50 行考虑拆分,超过 100 行基本必须拆。

🟡 问题 5:头文件 include 太多,应该用前向声明

我的 mainwindow.h 里 include 了 15 个 Qt 头文件。问题是:每个包含 mainwindow.h.cpp 都要重新编译这 15 个文件——编译时间白长了。

规范做法:头文件里能用前向声明就用,把真正的 #include 推到 .cpp

// mainwindow.h 里(只声明,不引入完整定义)
class QComboBox;
class QPushButton;
class QLabel;

// mainwindow.cpp 里再完整 include
#include <QComboBox>
#include <QPushButton>

🟡 问题 6:废话注释

/**
 * @brief 构造函数实现
 * 
 * 初始化线程运行标志为false。   ← 这句话代码已经告诉我了,不需要你再说
 */

注释原则:说明为什么,不说明是什么。代码自己能说清楚的事情,不要写注释:

// ❌ 描述是什么
// 初始化线程运行标志为 false

// ✅ 解释为什么
// 线程默认不启动,等待 start() 显式调用——避免构造期间就开始处理数据

🔴 问题 7:潜在的越界风险(这个可能要修一下)

parseFrame 里虽然做了 floatDataLen % 4 != 0 的检查,但计算偏移时没有二次确认:

bytes[0] = (quint8)data[3 + i * 4];
bytes[1] = (quint8)data[3 + i * 4 + 1];
bytes[2] = (quint8)data[3 + i * 4 + 2];
bytes[3] = (quint8)data[3 + i * 4 + 3];

工控场景一定要防御性编程,每一层都确认边界安全:

int offset = HEADER_SIZE + LEN_FIELD_SIZE + i * FLOAT_SIZE;
if (offset + FLOAT_SIZE > data.size() - 1) return false;  // 防御

顺便可以用 memcpy 替代手动拆字节,更简洁。


第三部分:工控行业额外的规范要求

我的目标岗位是长沙工控/制造业 Qt/C++ 桌面端开发,这个行业对代码有额外的要求:

要求 原因
可维护性极高 工控软件可能跑十几年,原作者早走了
命名明确 变量名含义直接决定排错速度
错误处理完整 串口通信随时失败,硬件异常不能让软件崩
线程安全 多线程是工控日常,资源竞争不能有

第四部分:面试被问道"代码规范"怎么回答

我现在可以这样回答:

"我的项目遵循 Qt 的编码惯例,成员变量统一 m_ 前缀,函数命名 camelCase,头文件有 Doxygen 格式注释,多线程用 RAII 的 QMutexLocker 管理锁,不手动 unlock。在代码审查时我也发现了一些不足,比如有魔数没有抽成 constexpr 常量、部分地方还在用 C 风格强转——这些是我在后续迭代中会去改进的。"

承认问题比不承认问题加分——面试官看的是你有没有规范意识。


总结:两套规范的核心差异

规范 工程代码 刷题代码
变量命名 m_serialPort, frameLength l, r, k 都可以
注释 Doxygen 格式,解释为什么 不做要求
魔数 必须抽成 constexpr 常量 可以写 int n=nums.size()
类型转换 static_cast<> 不需要自己写
函数长度 < 50 行 只要主逻辑清晰就行
错误处理 防御性编程,层层检查 题目保证输入合法
include 严格分组排序,前向声明 力扣帮你全做好了

一句话记住:工程规范是给别人看的,刷题规范是防止自己踩坑的。两边都要刻意练习,养成肌肉记忆。


本文档由 AI 辅助生成,作者提供问题和思路,综合获得以上内容。

posted @ 2026-06-07 11:31  STA_running  阅读(15)  评论(0)    收藏  举报