当前位置: 首页 > news >正文

【C++ -Day7】封装实战 | 用类封装日志、配置和文件操作模块

引言

封装是面向对象三大特性(封装、继承、多态)中最基础也最重要的一环。在嵌入式开发中,代码的安全性可维护性可复用性直接决定了项目的成败。通过封装,我们可以将数据和操作隐藏在类内部,只暴露简洁的接口,彻底告别 “全局变量满天飞” 的混乱局面。

本文将带你完成三个嵌入式开发中最高频使用模块的封装实战:日志模块、配置文件读取模块和文件操作模块。所有代码均基于 Linux C++ 实现,遵循工程化规范,可直接复用到你的项目中。


核心知识点

1. 封装的意义

  • 隐藏实现细节:外部调用者无需关心内部逻辑,只需关注接口如何使用。
  • 提高安全性:防止成员变量被意外修改,所有数据访问都通过受控接口。
  • 增强可维护性:修改内部实现(如换一种日志存储方式)时,外部调用代码完全无需改动。
  • 提升复用性:封装好的类可以在多个项目中直接复用,避免重复造轮子。

2. 封装的原则

  • 成员变量私有:所有数据成员设为private,禁止外部直接访问。
  • 接口最小化:只暴露必要的public函数,接口越简洁,耦合度越低。
  • 单一职责:一个类只负责一个功能(如日志类只负责日志,不要混进配置逻辑)。
  • 异常安全:内部处理好所有可能的异常(如文件打开失败),通过返回值或异常告知调用者。

3. 嵌入式开发常见封装场景

  • 日志模块(调试与运行记录)
  • 配置文件读取模块(参数管理)
  • 文件操作模块(数据存储与读取)
  • 设备驱动模块(硬件抽象)
  • 网络通信模块(协议封装)

工程实战

任务 1:封装日志类

需求

  • 支持分级日志(DEBUG、INFO、WARN、ERROR)
  • 支持同时输出到控制台和文件
  • 支持动态设置日志级别(如只打印 WARN 及以上)
  • 日志格式自动包含时间、级别、内容

1.1 头文件Logger.h

#ifndef LOGGER_H #define LOGGER_H #include <string> #include <fstream> #include <mutex> // 日志级别枚举 enum LogLevel { DEBUG, INFO, WARN, ERROR }; class Logger { private: LogLevel currentLevel; // 当前日志级别 std::ofstream logFile; // 文件输出流 bool consoleEnabled; // 控制台输出开关 bool fileEnabled; // 文件输出开关 mutable std::mutex logMutex; // 互斥锁,保证线程安全 // 获取当前时间字符串(格式:YYYY-MM-DD HH:MM:SS) std::string getCurrentTime() const; // 日志级别转字符串 std::string levelToString(LogLevel level) const; // 实际输出日志的内部函数 void output(LogLevel level, const std::string& message) const; public: // 构造函数:默认只开启控制台输出,级别为 INFO Logger(); // 析构函数:自动关闭文件 ~Logger(); // 禁止拷贝构造和赋值(避免文件流被多次关闭) Logger(const Logger&) = delete; Logger& operator=(const Logger&) = delete; // 设置日志级别(低于该级别的日志将被忽略) void setLevel(LogLevel level); // 开启/关闭控制台输出 void enableConsoleOutput(bool enable); // 开启文件输出(append=true 表示追加写入,false 表示覆盖) bool enableFileOutput(const std::string& filename, bool append = true); // 分级日志接口 void debug(const std::string& message) const; void info(const std::string& message) const; void warn(const std::string& message) const; void error(const std::string& message) const; }; #endif // LOGGER_H

1.2 实现文件Logger.cpp

#include "Logger.h" #include <iostream> #include <iomanip> #include <chrono> #include <sstream> // 构造函数:初始化默认状态 Logger::Logger() : currentLevel(INFO), consoleEnabled(true), fileEnabled(false) {} // 析构函数:关闭文件流 Logger::~Logger() { if (logFile.is_open()) { logFile.close(); } } // 设置日志级别 void Logger::setLevel(LogLevel level) { std::lock_guard<std::mutex> lock(logMutex); currentLevel = level; } // 开启/关闭控制台输出 void Logger::enableConsoleOutput(bool enable) { std::lock_guard<std::mutex> lock(logMutex); consoleEnabled = enable; } // 开启文件输出 bool Logger::enableFileOutput(const std::string& filename, bool append) { std::lock_guard<std::mutex> lock(logMutex); if (logFile.is_open()) { logFile.close(); } std::ios_base::openmode mode = std::ios_base::out; if (append) { mode |= std::ios_base::app; } else { mode |= std::ios_base::trunc; } logFile.open(filename, mode); fileEnabled = logFile.is_open(); return fileEnabled; } // 获取当前时间字符串 std::string Logger::getCurrentTime() const { auto now = std::chrono::system_clock::now(); std::time_t nowTime = std::chrono::system_clock::to_time_t(now); std::tm localTime; #ifdef _WIN32 localtime_s(&localTime, &nowTime); #else localtime_r(&nowTime, &localTime); #endif std::ostringstream oss; oss << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S"); return oss.str(); } // 日志级别转字符串 std::string Logger::levelToString(LogLevel level) const { switch (level) { case DEBUG: return "DEBUG"; case INFO: return "INFO"; case WARN: return "WARN"; case ERROR: return "ERROR"; default: return "UNKNOWN"; } } // 内部输出函数(统一处理格式和目标) void Logger::output(LogLevel level, const std::string& message) const { if (level < currentLevel) { return; // 低于当前级别,直接忽略 } std::lock_guard<std::mutex> lock(logMutex); // 组装日志格式:[时间] [级别] 内容 std::ostringstream oss; oss << "[" << getCurrentTime() << "] " << "[" << levelToString(level) << "] " << message; std::string logStr = oss.str(); // 输出到控制台 if (consoleEnabled) { std::ostream& os = (level >= WARN) ? std::cerr : std::cout; os << logStr << std::endl; } // 输出到文件 if (fileEnabled && logFile.is_open()) { logFile << logStr << std::endl; logFile.flush(); } } // 分级日志接口实现 void Logger::debug(const std::string& message) const { output(DEBUG, message); } void Logger::info(const std::string& message) const { output(INFO, message); } void Logger::warn(const std::string& message) const { output(WARN, message); } void Logger::error(const std::string& message) const { output(ERROR, message); }

任务 2:封装配置文件读取类

需求

  • 支持读取标准 INI 格式配置文件([Section]+Key=Value
  • 支持获取字符串、整数、浮点数类型配置
  • 支持默认值(配置项不存在时返回默认值)
  • 支持运行时重新加载配置文件

2.1 头文件Config.h

#ifndef CONFIG_H #define CONFIG_H #include <string> #include <map> #include <mutex> class Config { private: // 存储结构:map<Section, map<Key, Value>> using SectionMap = std::map<std::string, std::string>; std::map<std::string, SectionMap> data; mutable std::mutex configMutex; std::string filename; // 去除字符串首尾空白 std::string trim(const std::string& str) const; // 解析一行配置 bool parseLine(const std::string& line, std::string& currentSection); public: Config(); ~Config() = default; // 禁止拷贝 Config(const Config&) = delete; Config& operator=(const Config&) = delete; // 加载配置文件 bool load(const std::string& filepath); // 重新加载(用于配置文件更新后) bool reload(); // 获取配置项(带默认值) std::string getString(const std::string& section, const std::string& key, const std::string& defaultValue = "") const; int getInt(const std::string& section, const std::string& key, int defaultValue = 0) const; double getDouble(const std::string& section, const std::string& key, double defaultValue = 0.0) const; }; #endif // CONFIG_H

2.2 实现文件Config.cpp

#include "Config.h" #include <fstream> #include <sstream> #include <algorithm> Config::Config() {} // 去除首尾空白 std::string Config::trim(const std::string& str) const { auto start = str.find_first_not_of(" \t\r\n"); auto end = str.find_last_not_of(" \t\r\n"); if (start == std::string::npos) return ""; return str.substr(start, end - start + 1); } // 解析一行配置 bool Config::parseLine(const std::string& line, std::string& currentSection) { std::string trimmed = trim(line); // 忽略空行和注释(以 # 或 ; 开头) if (trimmed.empty() || trimmed[0] == '#' || trimmed[0] == ';') { return true; } // 解析 [Section] if (trimmed.front() == '[' && trimmed.back() == ']') { currentSection = trim(trimmed.substr(1, trimmed.size() - 2)); return true; } // 解析 Key=Value size_t pos = trimmed.find('='); if (pos != std::string::npos) { std::string key = trim(trimmed.substr(0, pos)); std::string value = trim(trimmed.substr(pos + 1)); if (!key.empty() && !currentSection.empty()) { data[currentSection][key] = value; } return true; } return false; // 格式错误 } // 加载配置文件 bool Config::load(const std::string& filepath) { std::lock_guard<std::mutex> lock(configMutex); std::ifstream file(filepath); if (!file.is_open()) { return false; } data.clear(); filename = filepath; std::string currentSection; std::string line; while (std::getline(file, line)) { parseLine(line, currentSection); } file.close(); return true; } // 重新加载 bool Config::reload() { if (filename.empty()) { return false; } return load(filename); } // 获取字符串配置 std::string Config::getString(const std::string& section, const std::string& key, const std::string& defaultValue) const { std::lock_guard<std::mutex> lock(configMutex); auto secIt = data.find(section); if (secIt != data.end()) { auto keyIt = secIt->second.find(key); if (keyIt != secIt->second.end()) { return keyIt->second; } } return defaultValue; } // 获取整数配置 int Config::getInt(const std::string& section, const std::string& key, int defaultValue) const { std::string value = getString(section, key, ""); if (value.empty()) return defaultValue; try { return std::stoi(value); } catch (...) { return defaultValue; } } // 获取浮点数配置 double Config::getDouble(const std::string& section, const std::string& key, double defaultValue) const { std::string value = getString(section, key, ""); if (value.empty()) return defaultValue; try { return std::stod(value); } catch (...) { return defaultValue; } }

任务 3:封装文件操作类

需求

  • 封装文件的打开、关闭、读、写操作
  • 支持文件是否存在判断、大小获取、删除
  • 使用 RAII 管理资源(自动关闭文件)

3.1 头文件FileHandler.h

#ifndef FILEHANDLER_H #define FILEHANDLER_H #include <string> #include <fstream> #include <mutex> class FileHandler { private: std::fstream file; std::string filePath; mutable std::mutex fileMutex; public: FileHandler(); ~FileHandler(); // 禁止拷贝 FileHandler(const FileHandler&) = delete; FileHandler& operator=(const FileHandler&) = delete; // 打开文件(mode: in/out/ate/app/trunc/binary) bool open(const std::string& path, std::ios_base::openmode mode); // 关闭文件 void close(); // 检查文件是否打开 bool isOpen() const; // 写入数据 bool write(const std::string& data); bool writeLine(const std::string& data); // 读取数据 bool readAll(std::string& data); bool readLine(std::string& line); // 静态工具函数(无需打开文件即可使用) static bool exists(const std::string& path); static size_t size(const std::string& path); static bool remove(const std::string& path); }; #endif // FILEHANDLER_H

3.2 实现文件FileHandler.cpp

#include "FileHandler.h" #include <sys/stat.h> #include <unistd.h> FileHandler::FileHandler() {} FileHandler::~FileHandler() { close(); } bool FileHandler::open(const std::string& path, std::ios_base::openmode mode) { std::lock_guard<std::mutex> lock(fileMutex); if (file.is_open()) { file.close(); } file.open(path, mode); filePath = path; return file.is_open(); } void FileHandler::close() { std::lock_guard<std::mutex> lock(fileMutex); if (file.is_open()) { file.close(); filePath.clear(); } } bool FileHandler::isOpen() const { std::lock_guard<std::mutex> lock(fileMutex); return file.is_open(); } bool FileHandler::write(const std::string& data) { std::lock_guard<std::mutex> lock(fileMutex); if (!file.is_open()) return false; file << data; return file.good(); } bool FileHandler::writeLine(const std::string& data) { return write(data + "\n"); } bool FileHandler::readAll(std::string& data) { std::lock_guard<std::mutex> lock(fileMutex); if (!file.is_open()) return false; file.seekg(0, std::ios::end); size_t size = file.tellg(); file.seekg(0, std::ios::beg); data.resize(size); file.read(&data[0], size); return file.good(); } bool FileHandler::readLine(std::string& line) { std::lock_guard<std::mutex> lock(fileMutex); if (!file.is_open()) return false; return std::getline(file, line).good(); } // 静态工具函数实现 bool FileHandler::exists(const std::string& path) { struct stat buffer; return (stat(path.c_str(), &buffer) == 0); } size_t FileHandler::size(const std::string& path) { struct stat buffer; if (stat(path.c_str(), &buffer) == 0) { return buffer.st_size; } return 0; } bool FileHandler::remove(const std::string& path) { return (unlink(path.c_str()) == 0); }

今日踩坑指南

坑点 1:接口设计过度暴露内部细节

场景:一开始把日志类的std::ofstream直接设为 public,导致外部代码可以随意操作文件流,最终引发数据竞争和文件损坏。解决:严格将所有成员变量设为 private,只通过 public 函数间接操作。例如文件的打开关闭通过enableFileOutput()控制,而不是直接暴露文件流。

坑点 2:忽略线程安全

场景:多线程环境下同时写日志,导致日志内容交错混乱。解决:在所有成员函数中加std::lock_guard<std::mutex>保护共享数据。注意mutable关键字的使用(允许在 const 函数中修改 mutex)。

坑点 3:异常处理缺失

场景:配置文件不存在时直接崩溃,而不是优雅地返回默认值。解决:在Config::load()中检查文件是否打开成功,在getInt()等函数中捕获std::stoi可能抛出的异常,确保程序不会因配置错误而终止。


今日总结

通过今天的实战,我们完成了三个核心模块的封装:

  1. 日志类:实现了分级、多目标输出,解决了嵌入式调试信息的规范化记录问题。
  2. 配置类:实现了 INI 文件解析,支持运行时重载,告别了硬编码参数。
  3. 文件操作类:封装了底层文件系统调用,通过 RAII 保证资源安全释放。

这三个类的共同特点是:无全局变量、接口简洁、线程安全、可直接复用。将它们应用到你的嵌入式项目中,代码质量将得到质的提升。


后续预告

下一篇文章: 【C++ -Day8】 继承 | 抽象公共能力,提高代码复用性

我们将在今天封装的基础上,通过继承抽象出公共接口(如 “输出设备” 接口),实现日志模块的进一步解耦(例如支持同时输出到串口、网络、文件)。

http://www.jsqmd.com/news/793332/

相关文章:

  • 电子热量表设计:PIC16F913微控制器应用与热力计算
  • Scarpet脚本语言深度解析:在Fabric Carpet中编写高级自动化程序的完整指南
  • android C++ opencv 年龄 性别识别深度神经网络模型
  • CANN/asc-devkit向量最小值函数
  • 告别理论!用TI毫米波雷达开发板实测多普勒测速(附Python代码)
  • 从DES到AES:被‘遗忘’的IDEA算法,它的设计思想给现代密码学留下了什么?
  • CTO 每月烧 600 亿 token,3 个月完成百名程序员七八年写的 800 万行代码
  • AI编码助手经验治理:ExperienceEngine解决重复错误与智能进化
  • 2026年AI大模型接口中转站排行榜新鲜出炉!五大平台硬核数据对比,为开发者提供权威选型指南
  • 别再只用Matplotlib画图了!用Python这3个库(SciPy, NumPy, Scikit-learn)给你的数据曲线做个‘美容’
  • CANN/asc-devkit向量减法ReLU函数
  • 我们只能要求手机在拍摄的时候呈现45度-----采用常用模型
  • Tacacs+协议报文解密与全流程实战抓包解析
  • Groundhog:基于Git仓库的开发者时间自动追踪工具
  • 大语言模型评测框架解析:从公平对比到工程选型实践
  • 视频技术演进:从模拟到数字的革命与压缩技术解析
  • FiveM服务器智能运维:基于CoPaw多智能体的自动化技能包实战
  • “内存对比工具V2.6版”的基础功能说明!
  • 人脸检测主流模型----第一名---占有率90%---MIT协议
  • 机器视觉(MV)与机器人视觉(RV)的本质区别(4)
  • 8.4.3 开始屏幕和任务栏的优化:StartAllBack 找回高效 Windows 11 使用体验
  • 别再傻傻切片了!PyTorch Tensor高级索引实战:用index_select、masked_select和gather提升数据处理效率
  • WebGLM:开源高效的网络增强问答系统架构解析与部署实践
  • 【Prometheus】 如何处理指标名称或标签中包含特殊字符的情况?
  • AI赋能区域创新评估:融合记分板与政策文本分析的协同框架与实践
  • Stable Mean Teacher for Semi-supervised Video Action Detection
  • Spring 第四天:AOP 面向切面编程与声明式事务管理
  • AI赋能风景园林设计:技术原理、实践案例与未来挑战
  • crawdad-openclaw:开源通用爬虫框架的设计、实战与工程化部署
  • Arm GNU工具链技术解析与实战应用指南