实际项目中 c++针对这三种特性都有哪些常用方法?
好问题。我来梳理一下实际项目中真正常用的方法,而不是教科书上的理论。我会用最基础的 C++ 语法,每行注释。
一、可维护性(Maintainability)------ 实际项目常用方法
方法1:拆分函数(最基础、最常用)
原则:一个函数不超过 50 行,一个函数只做一件事
// ❌ 坏:一个函数 200 行,什么都做 void processOrder(const json& data) { // 第1-30行:解析数据 // 第31-50行:校验数据 // 第51-80行:计算价格 // 第81-100行:检查库存 // 第101-130行:生成订单 // 第131-150行:发送邮件 // 第151-180行:记录日志 // 第181-200行:返回结果 } // ✅ 好:拆成多个小函数 Order parseOrderData(const json& data); // 只负责解析 bool validateOrder(const Order& order); // 只负责校验 double calculatePrice(const Order& order); // 只负责计算 bool checkStock(const Order& order); // 只负责检查 OrderId generateOrder(const Order& order); // 只负责生成 void sendEmail(const Order& order); // 只负责发送 void logOrder(const Order& order); // 只负责记录实际项目中怎么用:
- 团队一般约定:一个函数不要超过一屏(约 50 行)
- 如果超过,就拆
- 函数名要能说清楚"我做了什么"
方法2:常量命名(防止"魔法数字")
// ❌ 坏:不知道 60、4096、3 是什么意思 if (age > 60) { ... } buffer = new char[4096]; if (retryCount > 3) { ... } // ✅ 好:用常量或枚举 const int MAX_RETRY_COUNT = 3; const int MAX_NAME_LENGTH = 50; const int BUFFER_SIZE = 4096; const int RETIREMENT_AGE = 60; if (age > RETIREMENT_AGE) { ... } buffer = new char[BUFFER_SIZE]; if (retryCount > MAX_RETRY_COUNT) { ... }实际项目中怎么用:
- 文件开头定义常量
- 或者用
const全局变量- 或者用
#define(但尽量用 const)方法3:尽早返回(减少嵌套)
// ❌ 坏:嵌套太深,逻辑绕 void process(const json& data) { if (data.contains("order")) { if (data["order"].contains("id")) { if (data["order"]["id"].is_string()) { // 这里才真正开始处理 // ... } } } } // ✅ 好:尽早返回,减少嵌套 void process(const json& data) { // 检查条件,不满足就返回 if (!data.contains("order")) { return; // 没有 order 字段,直接返回 } if (!data["order"].contains("id")) { return; // 没有 id 字段,直接返回 } if (!data["order"]["id"].is_string()) { return; // id 不是字符串,直接返回 } // 到这里,所有条件都满足了,开始处理 // ... }实际项目中怎么用:
- 先检查所有错误条件,尽早返回
- 最后才写正常逻辑
- 这样可以减少 if-else 的嵌套层数
方法4:用枚举代替字符串比较
// ❌ 坏:字符串比较,容易写错 if (type == "user") { ... } // 如果写成 "uesr" 就 bug 了 else if (type == "order") { ... } // ✅ 好:用枚举,编译器会检查 enum MessageType { TYPE_USER, // 0 TYPE_ORDER, // 1 TYPE_PRODUCT, // 2 TYPE_COUNT // 3(用于数组大小) }; // 然后可以用 switch switch (type) { case TYPE_USER: // ... break; case TYPE_ORDER: // ... break; // 编译器会警告:如果少了某个 case }实际项目中怎么用:
- 如果类型是固定的(比如不超过 10 种),用枚举
- 如果类型是动态的(比如用户自定义),用 map
二、可扩展性(Extensibility)------ 实际项目常用方法
方法1:函数指针/回调(最常用)
// ========== 实际项目中最常用的扩展方式 ========== // 1. 定义一个函数指针类型 typedef int (*Callback)(int data); // 2. 写一个处理函数,接受回调 void processData(int* array, int size, Callback callback) { for (int i = 0; i < size; i++) { array[i] = callback(array[i]); // 调用回调 } } // 3. 不同的调用方提供不同的回调 int doubleValue(int x) { return x * 2; } int squareValue(int x) { return x * x; } int addTen(int x) { return x + 10; } // 4. 使用 int data[] = {1, 2, 3, 4, 5}; processData(data, 5, doubleValue); // 翻倍 processData(data, 5, squareValue); // 平方 processData(data, 5, addTen); // 加10 // 不需要改 processData 函数本身实际项目中怎么用:
- 排序函数:可以传入不同的比较函数
- 遍历函数:可以传入不同的处理函数
- 网络库:可以传入不同的回调(收到数据做什么)
方法2:虚函数 + 继承(面向对象方式)
// ========== 实际项目中:插件系统、UI 框架 ========== // 1. 定义接口 class IPlugin { public: virtual ~IPlugin() {} virtual void init() = 0; // 初始化 virtual void execute() = 0; // 执行 virtual void cleanup() = 0; // 清理 virtual const char* getName() = 0; // 插件名 }; // 2. 不同的插件 class LogPlugin : public IPlugin { public: void init() { /* 初始化日志系统 */ } void execute() { /* 写日志 */ } void cleanup() { /* 关闭日志文件 */ } const char* getName() { return "LogPlugin"; } }; class MonitorPlugin : public IPlugin { public: void init() { /* 初始化监控 */ } void execute() { /* 收集性能数据 */ } void cleanup() { /* 停止监控 */ } const char* getName() { return "MonitorPlugin"; } }; // 3. 管理器(不用改,支持任意插件) class PluginManager { private: IPlugin* plugins[10]; // 最多支持10个插件 int count; public: PluginManager() : count(0) {} // 注册插件(扩展点) void registerPlugin(IPlugin* plugin) { if (count < 10) { plugins[count++] = plugin; plugin->init(); // 自动初始化 } } // 执行所有插件 void executeAll() { for (int i = 0; i < count; i++) { plugins[i]->execute(); } } }; // 4. 使用 int main() { PluginManager manager; // 注册插件(加新插件只需要 new 一个对象 + 注册) manager.registerPlugin(new LogPlugin()); manager.registerPlugin(new MonitorPlugin()); // 执行 manager.executeAll(); }实际项目中怎么用:
- 游戏引擎:不同的角色继承自同一个基类
- UI 框架:不同的控件继承自同一个基类
- 插件系统:所有插件实现同一个接口
方法3:模板(编译期扩展)
// ========== 实际项目中:容器、算法库 ========== // 1. 模板函数:可以处理任意类型 template <typename T> T max(T a, T b) { return (a > b) ? a : b; } // 2. 使用 int a = max(3, 5); // T = int double b = max(3.14, 2.56); // T = double std::string c = max(std::string("abc"), std::string("xyz")); // T = string // 不需要为每种类型重写实际项目中怎么用:
- STL 容器 :
std::vector<int>、std::vector<string>- 算法 :
std::sort、std::find- 不需要为每种类型重写同样的逻辑
方法4:策略模式(把"变化的部分"封装起来)
// ========== 实际项目中:压缩、加密、序列化 ========== // 1. 定义策略接口 class ICompressionStrategy { public: virtual ~ICompressionStrategy() {} virtual std::string compress(const std::string& data) = 0; virtual std::string decompress(const std::string& data) = 0; }; // 2. 不同的策略 class NoCompression : public ICompressionStrategy { public: std::string compress(const std::string& data) { return data; } std::string decompress(const std::string& data) { return data; } }; class GzipCompression : public ICompressionStrategy { public: std::string compress(const std::string& data) { /* gzip 压缩 */ } std::string decompress(const std::string& data) { /* gzip 解压 */ } }; class ZipCompression : public ICompressionStrategy { public: std::string compress(const std::string& data) { /* zip 压缩 */ } std::string decompress(const std::string& data) { /* zip 解压 */ } }; // 3. 使用策略的类 class FileSaver { private: ICompressionStrategy* compressor_; // 策略对象 public: // 可以在运行时切换策略 void setCompression(ICompressionStrategy* compressor) { compressor_ = compressor; } void save(const std::string& filename, const std::string& data) { std::string compressed = compressor_->compress(data); // 保存到文件... } }; // 4. 使用 int main() { FileSaver saver; // 使用不同的压缩策略,不需要改 FileSaver 的代码 saver.setCompression(new NoCompression()); saver.save("test.txt", "hello"); saver.setCompression(new GzipCompression()); saver.save("test.gz", "hello"); }实际项目中怎么用:
- 压缩算法:可以切换 zip、gzip、7z
- 加密算法:可以切换 AES、DES、RSA
- 序列化格式:可以切换 JSON、XML、Protobuf
三、可复用性(Reusability)------ 实际项目常用方法
方法1:封装成独立模块(最常用)
// ========== 实际项目:把通用功能封装成类 ========== // 1. 封装一个"日志模块" // 这个模块可以放在任何项目里使用 class Logger { private: FILE* file_; bool enabled_; public: Logger(const char* filename) { file_ = fopen(filename, "a"); enabled_ = true; } ~Logger() { if (file_) fclose(file_); } void log(const char* level, const char* message) { if (!enabled_ || !file_) return; // 获取当前时间 time_t now = time(NULL); char* timeStr = ctime(&now); // 去掉换行符 timeStr[strlen(timeStr) - 1] = '\0'; fprintf(file_, "[%s] [%s] %s\n", timeStr, level, message); fflush(file_); } void info(const char* msg) { log("INFO", msg); } void warn(const char* msg) { log("WARN", msg); } void error(const char* msg) { log("ERROR", msg); } void setEnabled(bool enabled) { enabled_ = enabled; } }; // 2. 任何项目都可以直接使用 // 项目A Logger logger("app.log"); logger.info("项目A 启动成功"); // 项目B(同一个 Logger 类) Logger logger("server.log"); logger.info("项目B 开始处理请求");实际项目中怎么用:
- 公司内部一般有公共库,放这些通用模块
- 日志、配置、网络、文件操作等,都封装成独立模块
- 新项目直接引用,不用重复造轮子
方法2:用参数化代替硬编码
// ❌ 坏:硬编码,不能复用 void processFile() { const char* filename = "C:\\data\\input.txt"; int bufferSize = 1024; const char* encoding = "UTF-8"; // ... } // ✅ 好:参数化,可以复用 // 这个函数可以处理任何文件 void processFile( const char* filename, // 文件名(可配置) int bufferSize, // 缓冲区大小(可配置) const char* encoding // 编码(可配置) ) { // ... }实际项目中怎么用:
- 不要硬编码路径、大小、数量
- 用参数传进来
- 或者用配置文件读取
方法3:模板 + 类型参数
// ========== 实际项目:通用容器、通用算法 ========== // 1. 写一个通用的"缓存"类 template <typename T> class Cache { private: T* data_; int capacity_; int size_; public: Cache(int capacity) : capacity_(capacity), size_(0) { data_ = new T[capacity]; } ~Cache() { delete[] data_; } void add(const T& item) { if (size_ < capacity_) { data_[size_++] = item; } } T get(int index) { if (index >= 0 && index < size_) { return data_[index]; } return T(); // 返回默认值 } int size() { return size_; } }; // 2. 可以缓存任何类型 Cache<int> intCache(100); // 缓存 int Cache<std::string> strCache(50); // 缓存 string Cache<Order> orderCache(20); // 缓存 Order 对象实际项目中怎么用:
- STL 容器都是模板:
vector、map、set- 自己写通用类时,也尽量用模板
方法4:组合优于继承
// ========== 实际项目:通过组合复用功能 ========== // 1. 先写一些小的、可复用的功能类 class Logger { public: void log(const char* msg) { /* ... */ } }; class Database { public: void save(const char* data) { /* ... */ } }; class EmailSender { public: void send(const char* to, const char* content) { /* ... */ } }; // 2. 通过组合使用它们(而不是继承) class OrderProcessor { private: // 组合:把需要的功能"组合"进来 Logger logger_; Database db_; EmailSender email_; public: void process(const Order& order) { logger_.log("开始处理订单"); // 复用日志功能 db_.save("order data..."); // 复用数据库功能 email_.send("user@test.com", "订单已处理"); // 复用邮件功能 logger_.log("订单处理完成"); // 复用日志功能 } };实际项目中怎么用:
- 不要为了复用而继承(比如"订单处理器继承自日志器"是不对的)
- 把功能封装成小类,然后通过组合使用
四、总结:实际项目中最常用的方法速查表
表格
目标 最常用方法 适用场景 复杂度 可维护性 拆分函数 所有场景 ⭐ 低 可维护性 尽早返回 减少嵌套 ⭐ 低 可维护性 用枚举代替魔法数字 固定类型 ⭐ 低 可扩展性 函数指针/回调 变化的行为 ⭐⭐ 中 可扩展性 虚函数+继承 插件系统、UI ⭐⭐ 中 可扩展性 模板 通用算法 ⭐⭐⭐ 高 可复用性 封装独立模块 日志、网络、文件 ⭐⭐ 中 可复用性 参数化 避免硬编码 ⭐ 低 可复用性 组合 功能复用 ⭐⭐ 中
五、一个完整的实际项目示例
假设你要写一个数据导入工具,支持从不同格式(CSV、JSON、XML)导入数据。
// ========== 完整的示例:展示三者的结合 ========== // 1. 可复用性:封装成独立模块(日志模块) class Logger { public: void info(const char* msg) { printf("[INFO] %s\n", msg); } void error(const char* msg) { printf("[ERROR] %s\n", msg); } }; // 2. 可扩展性:定义接口,支持不同格式 class IDataReader { public: virtual ~IDataReader() {} virtual bool open(const char* filename) = 0; virtual bool readRow(std::string* columns, int colCount) = 0; virtual void close() = 0; }; // CSV 读取器 class CsvReader : public IDataReader { private: FILE* file_; public: bool open(const char* filename) { file_ = fopen(filename, "r"); return file_ != NULL; } bool readRow(std::string* columns, int colCount) { // 读取一行,按逗号分割 // ... } void close() { if (file_) fclose(file_); } }; // JSON 读取器 class JsonReader : public IDataReader { // 类似的实现... }; // 3. 可维护性:主函数简洁明了 class DataImporter { private: Logger logger_; // 日志(可复用) IDataReader* reader_; // 读取器(可扩展) public: void setReader(IDataReader* reader) { reader_ = reader; // 可以在运行时切换 } bool import(const char* filename) { logger_.info("开始导入数据"); // 尽早返回(可维护性) if (!reader_->open(filename)) { logger_.error("无法打开文件"); return false; } // 读取数据 std::string columns[10]; int rowCount = 0; while (reader_->readRow(columns, 10)) { // 处理每一行... rowCount++; } reader_->close(); logger_.info("导入完成,共处理"); // 用 char 数组拼接数字 char countMsg[50]; sprintf(countMsg, "导入完成,共处理 %d 行", rowCount); logger_.info(countMsg); return true; } };这个例子同时体现了:
表格
特性 体现在哪里 可维护性 函数简短、尽早返回、日志清晰 可扩展性 可以加 XML、Excel 等新格式,只需要实现 IDataReader接口可复用性 Logger模块可以放在任何项目里使用
最后,实际项目中的"黄金法则":
- 先写能跑的代码,不要一开始就想太多
- 重构时再考虑这些:看到代码重复了,就抽函数;看到 if-else 多了,就用 map
- 不要过度设计:如果只有 2-3 种类型,用 if-else 完全没问题
- 当类型超过 5 个时,再考虑用多态或 map
记住:好的代码是"改"出来的,不是"写"出来的。
可维护性 可扩展性 可复用性
老赵的博客2026-08-14 11:06
相关推荐
sibylyue1 小时前
工作流表单和流程设计前端一个游离的指针1 小时前
JS中的对象的相关概念jaysee-sjc2 小时前
【JavaWeb】Tlias智能学习辅助系统|后端Web实战(登录认证)淡海水2 小时前
C#与常用数据结构源码剖析-全篇导览问天_观心2 小时前
python之uv库的学习郑州光合科技余经理2 小时前
餐饮预定系统架构拆解:订单链路、权限组织与私有化源码交付在世修行2 小时前
从零打造 C# 工业视觉检测系统(十五):项目打包部署与进阶展望Nebula_g2 小时前
JavaSE基础语法:特殊类(特殊情景下的设计模式)不正经学生3 小时前
C语言大小端字节序:内存里字节的排列顺序