引言
封装是面向对象三大特性(封装、继承、多态)中最基础也最重要的一环。在嵌入式开发中,代码的安全性、可维护性和可复用性直接决定了项目的成败。通过封装,我们可以将数据和操作隐藏在类内部,只暴露简洁的接口,彻底告别 “全局变量满天飞” 的混乱局面。
本文将带你完成三个嵌入式开发中最高频使用模块的封装实战:日志模块、配置文件读取模块和文件操作模块。所有代码均基于 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_H1.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_H2.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_H3.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可能抛出的异常,确保程序不会因配置错误而终止。
今日总结
通过今天的实战,我们完成了三个核心模块的封装:
- 日志类:实现了分级、多目标输出,解决了嵌入式调试信息的规范化记录问题。
- 配置类:实现了 INI 文件解析,支持运行时重载,告别了硬编码参数。
- 文件操作类:封装了底层文件系统调用,通过 RAII 保证资源安全释放。
这三个类的共同特点是:无全局变量、接口简洁、线程安全、可直接复用。将它们应用到你的嵌入式项目中,代码质量将得到质的提升。
后续预告
下一篇文章: 【C++ -Day8】 继承 | 抽象公共能力,提高代码复用性
我们将在今天封装的基础上,通过继承抽象出公共接口(如 “输出设备” 接口),实现日志模块的进一步解耦(例如支持同时输出到串口、网络、文件)。