代码规范学习笔记_工程与刷题双视角
代码规范学习笔记:从工程到刷题
前言
最近在做两件事:写 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 不能改名,参数列表不能加东西或删东西。形参名字可以改(nums 换 a 也合法),但不建议改,保持力扣原样最安全。
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,但这是坏习惯。以后提交前扫一眼:有没有 cout、printf、cerr 残留。
第二部分:工程代码规范 — 以 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 辅助生成,作者提供问题和思路,综合获得以上内容。

浙公网安备 33010602011771号