行业资讯
C++ INI文件读写器实现:从设计到源码的完整指南
1. 项目概述为什么INI文件在C项目中依然重要在接手一个C项目尤其是涉及到配置管理时我们常常会面临一个选择配置文件用什么格式XML、JSON、YAML还是INI尽管INI格式看起来有些“古老”远不如JSON或YAML那样功能强大和结构化但在很多场景下它依然是C开发者的心头好。原因很简单简单、直观、易读。一个典型的INI文件比如config.ini内容可能就是下面这样[Database] Server127.0.0.1 Port3306 Usernameroot Passwordsecret [Log] LevelINFO FilePath/var/log/myapp.log MaxSize10MB这种键值对加节Section的结构对于人类阅读和手动编辑极其友好。在开发桌面应用、游戏、嵌入式系统或者是一些需要轻量级、快速启动的工具时INI文件因其零依赖不需要引入额外的解析库和极低的解析开销成为了一个非常务实的选择。很多Windows平台上的遗留软件和系统配置比如经典的win.ini也大量使用INI格式这使得在C特别是涉及Win32 API的编程中读写INI文件成为一项基础技能。然而C标准库并没有提供原生的INI文件解析功能。这意味着我们需要自己动手从文件I/O开始实现一套解析逻辑。这个过程看似简单实则暗藏玄机如何高效地解析节和键值如何处理空格和注释如何保证读写操作的线程安全如果需要的话如何设计一个清晰易用的API这正是本篇文章要深入探讨的。我将不仅提供一个可以直接编译使用的源码更会拆解其中的每一个设计决策和实现细节分享我在实际项目中踩过的坑和总结的经验。无论你是C新手想了解文件操作和字符串处理还是有经验的开发者想重构自己的配置管理模块相信都能从中获得启发。2. 核心设计思路从需求到接口的权衡在动手写代码之前我们先要明确这个INI读写器需要满足哪些核心需求。这决定了我们的设计方向和接口形态。2.1 功能需求拆解首先一个最基本的INI读写器应该能做什么读取从指定的INI文件中加载配置并能通过节名和键名快速获取对应的字符串值。写入将内存中的配置数据可能是修改后的保存回INI文件。修改支持在内存中增加、删除、修改节和键值对。基础解析正确识别节如[Section]、键值对如KeyValue、忽略注释通常以;或#开头和空行。但仅仅满足这些离一个“好用”的工具还有距离。我们还需要考虑一些增强需求 5.类型转换配置值通常是字符串但我们在代码中可能需要int,double,bool等类型。提供类型安全的获取和设置接口会极大方便调用方。 6.默认值当请求的键不存在时返回一个用户指定的默认值而不是抛出异常或返回空字符串这能使代码更健壮。 7.值存在性检查能够查询某个节或键是否存在。 8.空格处理INI格式宽松键和值前后的空格是否保留我们通常选择自动修剪trim避免因格式不统一导致的问题。 9.编码问题简单起见我们通常假设INI文件使用系统本地编码如Windows下的GBK或ANSI或UTF-8 without BOM。对于更复杂的Unicode支持需要额外处理。2.2 接口设计在易用性与灵活性之间找平衡基于以上需求我设计了一个名为IniFile的类。它的公共接口力求简洁明了class IniFile { public: // 构造与析构 explicit IniFile(const std::string filename); ~IniFile() default; // 核心读写接口 bool Load(); // 从文件加载 bool Save(const std::string filename ); // 保存到文件默认保存到加载的文件 // 值读取接口支持默认值 std::string GetStringValue(const std::string section, const std::string key, const std::string default_value ); int GetIntValue(const std::string section, const std::string key, int default_value 0); double GetDoubleValue(const std::string section, const std::string key, double default_value 0.0); bool GetBoolValue(const std::string section, const std::string key, bool default_value false); // 值设置接口 void SetStringValue(const std::string section, const std::string key, const std::string value); void SetIntValue(const std::string section, const std::string key, int value); void SetDoubleValue(const std::string section, const std::string key, double value); void SetBoolValue(const std::string section, const std::string key, bool value); // 工具接口 bool HasSection(const std::string section) const; bool HasKey(const std::string section, const std::string key) const; void DeleteSection(const std::string section); void DeleteKey(const std::string section, const std::string key); // 获取所有节名或某个节下所有键名用于遍历 std::vectorstd::string GetSectionNames() const; std::vectorstd::string GetKeyNames(const std::string section) const; private: // 内部数据存储与解析实现 std::string m_filename; std::mapstd::string, std::mapstd::string, std::string m_data; // 嵌套map存储数据 // ... 其他私有成员和方法 };设计考量文件名作为构造参数在构造时即绑定一个文件符合“一个对象对应一个文件”的直观认知。Load和Save方法则专注于执行I/O操作。重载的Get/Set方法为不同类型提供专门的方法内部进行字符串转换。这比让用户自己调用std::stoi或std::to_string更安全、更方便。Get方法提供默认值参数是防止访问不存在的键导致程序逻辑中断的常见做法。使用std::map嵌套存储这是最自然的数据结构。外层map的key是节名std::stringvalue是另一个map内层map的key是键名value是字符串值。这种结构保证了节和键的唯一性并且查找效率是O(log n)对于通常不大的配置文件来说完全足够。const正确性查询方法如GetStringValue,HasSection标记为const表明它们不会修改对象状态这是良好的API设计习惯。返回std::vector用于遍历虽然暴露了内部数据结构的部分信息但提供了遍历所有配置的可行性满足了高级需求。注意这里没有采用单例模式或全局静态类。因为一个程序完全可能读写多个不同的INI文件。将功能封装在一个可以实例化的类中灵活性更高。3. 关键实现细节与避坑指南有了清晰的接口设计接下来我们深入实现层。这里面的每一个细节都决定了代码的健壮性和效率。3.1 文件读取与行解析Load()函数的核心是逐行读取文件并解析。我们使用std::ifstream和std::getline。bool IniFile::Load() { std::ifstream inFile(m_filename); if (!inFile.is_open()) { // 文件打开失败可能是文件不存在。这里不视为错误只是清空现有数据。 m_data.clear(); return false; // 或者可以根据需求返回true表示“加载了一个空配置” } m_data.clear(); // 加载新文件前清空旧数据 std::string current_section; // 当前所属的节名默认为空表示全局节 std::string line; while (std::getline(inFile, line)) { // 第一步预处理行 Trim(line); // 移除首尾空白字符 // 跳过空行和注释行 if (line.empty() || line[0] ; || line[0] #) { continue; } // 第二步判断是否为节头 if (line.front() [ line.back() ]) { current_section line.substr(1, line.length() - 2); // 提取节名 Trim(current_section); // 节名也可能需要修剪空格 if (current_section.empty()) { // 节名为空是无效的可以选择忽略或记录警告 current_section.clear(); } // 确保在m_data中为该节创建一个空的map条目即使它下面还没有键值对。 m_data[current_section]; continue; } // 第三步解析键值对 size_t equals_pos line.find(); if (equals_pos ! std::string::npos) { std::string key line.substr(0, equals_pos); std::string value line.substr(equals_pos 1); Trim(key); Trim(value); if (!key.empty()) { // 如果current_section为空我们可以选择将其存储在一个特殊的“全局”节下如空字符串 // 但更常见的INI规范是节外的键值对无效。这里我们选择忽略没有节的键值对。 if (!current_section.empty()) { m_data[current_section][key] value; } // 否则忽略此行 } } // 如果没有找到‘’则忽略该行可能是格式错误的行 } inFile.close(); return true; }关键点与避坑指南Trim函数这是一个需要自己实现的辅助函数用于去掉字符串首尾的空白字符空格、制表符\t等。使用std::isspace在循环中判断是实现方式之一。务必在解析节名、键、值后立即调用避免后续比较和存储时受空格干扰。注释处理这里只处理了行首的;和#。更严格的解析器可能还需要处理行内注释如KeyValue ; comment。如果支持需要在找到键值对后在value中查找注释符号并截断。为了简单和通用性我的实现通常不支持行内注释。节名有效性[ ]是无效的节名。遇到这种情况我选择清空current_section后续的键值对将被忽略直到遇到下一个有效的节。你也可以选择抛出一个异常或记录错误。“全局”节问题有些INI解析器允许节定义之前的键值对将它们归属于一个隐式的全局节。为了符合更严格的标准如Windows的GetPrivateProfileStringAPI和避免歧义我的实现要求所有键值对必须位于某个明确的节定义之后。节定义之前的键值对会被忽略。文件打开失败如果文件不存在Load()失败。这里的设计选择是返回false并清空内部数据。调用方可以检查返回值如果为false且希望文件不存在时使用默认配置可以随后调用一系列的SetValue来初始化默认值最后调用Save()创建文件。3.2 数据类型转换的实现这是提升易用性的关键。以GetIntValue和SetIntValue为例int IniFile::GetIntValue(const std::string section, const std::string key, int default_value) { std::string str_val GetStringValue(section, key, ); if (str_val.empty()) { return default_value; } try { // 使用std::stoi它能处理进制如0x开头的十六进制 // 第二个参数size_t* idx用于指示转换停止的位置第三个参数是进制基数0表示自动检测 size_t pos; int val std::stoi(str_val, pos, 0); // 检查是否整个字符串都被成功转换了 if (pos str_val.length()) { return val; } else { // 字符串只有部分被转换说明格式可能有问题返回默认值 return default_value; } } catch (const std::invalid_argument e) { // 转换失败例如字符串是“abc” return default_value; } catch (const std::out_of_range e) { // 数字超出int范围 return default_value; } } void IniFile::SetIntValue(const std::string section, const std::string key, int value) { SetStringValue(section, key, std::to_string(value)); }关键点与避坑指南异常安全std::stoi,std::stod在转换失败时会抛出异常std::invalid_argument或std::out_of_range。务必使用try-catch块包裹并在捕获异常时返回默认值。这是保证程序不会因为一个错误配置项而崩溃的关键。完全转换检查std::stoi的pos参数非常有用。如果字符串是“123abc”std::stoi会转换前三个字符pos会变成3。这通常不是我们想要的用户可能输错了。通过检查pos str_val.length()我们可以确保整个字符串都是一个有效的数字否则返回默认值。布尔值转换GetBoolValue的实现需要更灵活。通常我们认为“1”,“true”,“yes”,“on”不区分大小写都表示true“0”,“false”,“no”,“off”表示false。实现时需要将这些字符串统一转换为小写再比较。3.3 数据存储与文件写入数据存储在嵌套的std::map中std::mapstd::string, std::mapstd::string, std::string m_data;。Save()函数需要将这个结构体优雅地写回文件bool IniFile::Save(const std::string filename) { std::string save_filename filename.empty() ? m_filename : filename; std::ofstream outFile(save_filename); if (!outFile.is_open()) { return false; } // 为了保持输出顺序map默认按键排序我们可以直接遍历。 // 如果需要保持加载时的原始顺序则需要更复杂的数据结构如std::vector std::unordered_map。 for (const auto section_pair : m_data) { const std::string section_name section_pair.first; const auto key_value_map section_pair.second; // 写入节头 outFile [ section_name ] std::endl; // 写入该节下的所有键值对 for (const auto kv_pair : key_value_map) { outFile kv_pair.first kv_pair.second std::endl; } // 节之间添加一个空行提高可读性 outFile std::endl; } outFile.close(); return true; }关键点与避坑指南保存顺序std::map会按照键节名、键名的字典序进行排序后输出。这可能导致保存的文件与加载时的原始顺序不同。如果顺序对你很重要例如为了版本对比或人工阅读习惯你需要改用std::vectorstd::pair...来保存顺序并用std::unordered_map来提供快速查找。但这会增加实现的复杂性。对于大多数应用排序输出是可接受的甚至更整洁。文件覆盖std::ofstream默认会覆盖原文件。这正是我们想要的行为。如果保存失败如磁盘已满、权限不足函数返回false原文件内容不会被破坏。空节处理如果一个节下面没有任何键值对上面的代码仍然会输出[Section]和一个空行。这符合INI文件的常见表现。如果你认为空节是无效的可以在写入节头前检查key_value_map是否为空。4. 完整源码实现与注解下面是将所有部分组合起来的完整IniFile类头文件和实现文件。代码包含了必要的注释和前面提到的最佳实践。ini_file.h#ifndef INI_FILE_H #define INI_FILE_H #include string #include map #include vector class IniFile { public: explicit IniFile(const std::string filename); // 加载与保存 bool Load(); bool Save(const std::string filename ); // 读取值带默认值 std::string GetStringValue(const std::string section, const std::string key, const std::string default_value ); int GetIntValue(const std::string section, const std::string key, int default_value 0); double GetDoubleValue(const std::string section, const std::string key, double default_value 0.0); bool GetBoolValue(const std::string section, const std::string key, bool default_value false); // 设置值 void SetStringValue(const std::string section, const std::string key, const std::string value); void SetIntValue(const std::string section, const std::string key, int value); void SetDoubleValue(const std::string section, const std::string key, double value); void SetBoolValue(const std::string section, const std::string key, bool value); // 工具函数 bool HasSection(const std::string section) const; bool HasKey(const std::string section, const std::string key) const; void DeleteSection(const std::string section); void DeleteKey(const std::string section, const std::string key); std::vectorstd::string GetSectionNames() const; std::vectorstd::string GetKeyNames(const std::string section) const; // 获取关联的文件名 std::string GetFileName() const { return m_filename; } private: // 内部工具函数 static void Trim(std::string str); static std::string ToLower(const std::string str); std::string m_filename; // 使用嵌套map存储数据 section_name - (key - value) std::mapstd::string, std::mapstd::string, std::string m_data; }; #endif // INI_FILE_Hini_file.cpp#include “ini_file.h” #include fstream #include sstream #include cctype #include algorithm #include stdexcept using namespace std; IniFile::IniFile(const string filename) : m_filename(filename) { // 构造函数里可以不加载提供显式的Load方法更清晰 } bool IniFile::Load() { ifstream inFile(m_filename); if (!inFile.is_open()) { // 文件不存在或其他原因无法打开清空数据并返回false m_data.clear(); return false; } m_data.clear(); string current_section; string line; while (getline(inFile, line)) { Trim(line); if (line.empty() || line[0] ; || line[0] #) { continue; } // 检查节头 if (line.front() [ line.back() ]) { current_section line.substr(1, line.length() - 2); Trim(current_section); // 即使节名为空也创建一个条目但后续解析会忽略其下的键值对这里选择创建。 // 更严格的做法是如果节名为空则忽略该节定义。 if (!current_section.empty()) { m_data[current_section]; // 确保节存在即使为空map } else { // 如果节名为空将current_section置空后续键值对无归属将被忽略 current_section.clear(); } continue; } // 解析键值对 size_t eq_pos line.find(); if (eq_pos ! string::npos) { string key line.substr(0, eq_pos); string value line.substr(eq_pos 1); Trim(key); Trim(value); if (!key.empty() !current_section.empty()) { m_data[current_section][key] value; } // 如果current_section为空即键值对在第一个节定义之前则忽略此行 } // 没有‘’的行忽略 } inFile.close(); return true; } bool IniFile::Save(const string filename) { string save_name filename.empty() ? m_filename : filename; ofstream outFile(save_name); if (!outFile.is_open()) { return false; } bool first_section true; for (const auto section_pair : m_data) { if (section_pair.first.empty()) { continue; // 跳过空节名如果有的话 } if (!first_section) { outFile endl; // 节之间加空行 } first_section false; outFile [ section_pair.first ] endl; for (const auto kv_pair : section_pair.second) { outFile kv_pair.first kv_pair.second endl; } } outFile.close(); return true; } // --- 字符串工具函数 --- void IniFile::Trim(string str) { // 删除左边的空白字符 str.erase(str.begin(), find_if(str.begin(), str.end(), [](unsigned char ch) { return !isspace(ch); })); // 删除右边的空白字符 str.erase(find_if(str.rbegin(), str.rend(), [](unsigned char ch) { return !isspace(ch); }).base(), str.end()); } string IniFile::ToLower(const string str) { string lower_str str; transform(lower_str.begin(), lower_str.end(), lower_str.begin(), ::tolower); return lower_str; } // --- 值获取函数 --- string IniFile::GetStringValue(const string section, const string key, const string default_value) { auto sec_it m_data.find(section); if (sec_it ! m_data.end()) { const auto key_map sec_it-second; auto key_it key_map.find(key); if (key_it ! key_map.end()) { return key_it-second; } } return default_value; } int IniFile::GetIntValue(const string section, const string key, int default_value) { string str_val GetStringValue(section, key, ); if (str_val.empty()) return default_value; try { size_t pos; int val stoi(str_val, pos, 0); // 支持十进制、十六进制(0x)、八进制(0) if (pos str_val.length()) { return val; } } catch (const invalid_argument e) { // 不是有效数字 } catch (const out_of_range e) { // 数字超出范围 } return default_value; } double IniFile::GetDoubleValue(const string section, const string key, double default_value) { string str_val GetStringValue(section, key, ); if (str_val.empty()) return default_value; try { size_t pos; double val stod(str_val, pos); if (pos str_val.length()) { return val; } } catch (const invalid_argument e) { } catch (const out_of_range e) { } return default_value; } bool IniFile::GetBoolValue(const string section, const string key, bool default_value) { string str_val GetStringValue(section, key, ); if (str_val.empty()) return default_value; str_val ToLower(str_val); if (str_val 1 || str_val true || str_val yes || str_val on) { return true; } else if (str_val 0 || str_val false || str_val no || str_val off) { return false; } // 无法识别的字符串返回默认值 return default_value; } // --- 值设置函数 --- void IniFile::SetStringValue(const string section, const string key, const string value) { if (section.empty()) { // 根据我们的设计不允许空节名 return; } m_data[section][key] value; } void IniFile::SetIntValue(const string section, const string key, int value) { SetStringValue(section, key, to_string(value)); } void IniFile::SetDoubleValue(const string section, const string key, double value) { SetStringValue(section, key, to_string(value)); } void IniFile::SetBoolValue(const string section, const string key, bool value) { SetStringValue(section, key, value ? true : false); } // --- 工具函数 --- bool IniFile::HasSection(const string section) const { return m_data.find(section) ! m_data.end(); } bool IniFile::HasKey(const string section, const string key) const { auto sec_it m_data.find(section); if (sec_it ! m_data.end()) { return sec_it-second.find(key) ! sec_it-second.end(); } return false; } void IniFile::DeleteSection(const string section) { m_data.erase(section); } void IniFile::DeleteKey(const string section, const string key) { auto sec_it m_data.find(section); if (sec_it ! m_data.end()) { sec_it-second.erase(key); } } vectorstring IniFile::GetSectionNames() const { vectorstring sections; for (const auto pair : m_data) { sections.push_back(pair.first); } return sections; } vectorstring IniFile::GetKeyNames(const string section) const { vectorstring keys; auto sec_it m_data.find(section); if (sec_it ! m_data.end()) { for (const auto pair : sec_it-second) { keys.push_back(pair.first); } } return keys; }5. 使用示例与高级技巧有了完整的类使用起来就非常直观了。下面是一个简单的示例程序main.cpp#include “ini_file.h” #include iostream #include vector int main() { // 1. 创建IniFile对象并加载文件 IniFile config(“my_config.ini”); if (!config.Load()) { std::cout “配置文件不存在将使用默认配置并创建新文件。” std::endl; // 初始化默认值 config.SetStringValue(“Database”, “Host”, “localhost”); config.SetIntValue(“Database”, “Port”, 3306); config.SetBoolValue(“Features”, “EnableLogging”, true); config.SetDoubleValue(“Physics”, “Gravity”, 9.81); config.Save(); // 保存默认配置到文件 } // 2. 读取配置 std::string db_host config.GetStringValue(“Database”, “Host”, “127.0.0.1”); int db_port config.GetIntValue(“Database”, “Port”, 3306); bool logging_enabled config.GetBoolValue(“Features”, “EnableLogging”, false); double gravity config.GetDoubleValue(“Physics”, “Gravity”, 9.8); std::cout “Database Host: “ db_host std::endl; std::cout “Database Port: “ db_port std::endl; std::cout “Logging Enabled: “ (logging_enabled ? “Yes” : “No”) std::endl; std::cout “Gravity Constant: “ gravity std::endl; // 3. 修改并保存配置 config.SetIntValue(“Database”, “Port”, 3366); config.SetStringValue(“User”, “Name”, “Alice”); if (config.Save()) { std::cout “配置已更新并保存。” std::endl; } // 4. 遍历所有节和键 std::vectorstd::string sections config.GetSectionNames(); for (const auto sec : sections) { std::cout “[“ sec “]” std::endl; std::vectorstd::string keys config.GetKeyNames(sec); for (const auto key : keys) { std::string val config.GetStringValue(sec, key); std::cout “ “ key “ “ val std::endl; } } return 0; }编译与运行 使用g或你喜欢的C编译器进行编译g -stdc11 -o ini_demo main.cpp ini_file.cpp ./ini_demo高级技巧与扩展思路线程安全当前的IniFile类不是线程安全的。如果需要在多线程环境中使用一个简单的方法是在调用类方法的上一层加锁。更优雅的做法是在类内部使用std::mutex在每个会修改内部数据Load,Save,SetValue,Delete和读取数据GetValue,HasKey的公共方法里加锁。注意这可能会成为性能瓶颈但对于配置读写这种低频操作通常可以接受。值变更通知观察者模式在大型项目中可能希望当某个配置项改变时自动通知相关的模块。可以在SetValue方法内部维护一个观察者列表当值真正发生变化时调用注册的回调函数。这会使类变得复杂但解耦了配置管理和业务逻辑。支持节/键的注释在内存中存储注释是很困难的因为注释与节/键的关联性在解析后丢失了。一个高级的实现可能会用一个结构体来存储每个条目键值对结构体里包含值字符串和它上方的注释字符串。在保存时将注释写回。这需要完全重写内部存储结构。默认节可以定义一个特殊的节名如“DEFAULT”或空字符串“”。当在指定节中找不到键时可以回退到默认节中查找。这需要修改GetValue系列函数的逻辑。文件监控可以启动一个后台线程使用平台特定的API如inotifyon Linux,ReadDirectoryChangesWon Windows来监控配置文件的变化并在文件被外部修改时自动重新加载。这非常适合需要热重载配置的场景。6. 常见问题排查与性能考量在实际使用中你可能会遇到一些问题。这里列出一些典型场景和排查思路。问题1读取的数值总是默认值但配置文件里明明有。可能原因1节名或键名大小写或空格不匹配。INI解析通常是大小写敏感的且Trim函数可能没有正确工作。检查你的Trim函数实现并确保在代码中使用的节/键名与文件中的完全一致包括大小写。可能原因2键值对写在节定义之前。根据我们的实现节定义之前的键值对会被忽略。确保所有KeyValue都在[Section]之后。可能原因3值包含空格且未用引号包裹。对于Key value with spaces我们的解析器会得到key为“Key “value为“ value with spaces”。Trim后key变成“Key”value变成“value with spaces”这是正确的。但如果值是“value with inside”我们的简单解析器会在第一个处分割导致错误。复杂的值建议避免使用特殊字符或考虑使用其他格式如JSON。问题2保存文件后中文或其他非ASCII字符变成乱码。原因文件读写时的编码不一致。Windows记事本默认使用ANSI如GBK编码保存而你的C程序可能以UTF-8无BOM方式读取和写入。解决方案统一编码确保你的源代码文件、配置文件、程序读写都使用同一种编码推荐UTF-8 without BOM。宽字符版本在Windows上可以使用std::wifstream/std::wofstream并与std::wstring配合同时使用std::locale设置全局编码如std::locale::global(std::locale(“.65001”))用于UTF-8。但这会大幅增加代码复杂性。实用建议对于简单的配置尽量避免在INI文件中直接使用非ASCII字符。如果必须使用明确团队内使用的编码规范并在代码注释中写明。问题3程序运行时修改了INI文件但程序没有感知到新配置。原因Load()只在启动时调用了一次后续配置被缓存在内存的m_data中。解决方案实现一个Reload()方法重新读取文件并更新m_data。可以在固定的时间间隔、收到特定信号如SIGHUP或用户请求时调用它。注意重载时需要考虑线程安全。性能考量解析性能对于大小在KB级别的配置文件使用std::getline逐行解析和std::map存储性能完全不是问题。即使是上万行的配置加载也在一瞬间完成。内存占用使用std::map存储字符串会有些内存开销。但对于配置数据来说这点开销微乎其微。查找性能std::map的查找是O(log n)。对于配置访问这足够快。如果配置文件极大几十万条且需要极高频的查找可以考虑改用std::unordered_map平均O(1)但会失去排序特性且自定义键类型需要提供哈希函数。一个我踩过的坑关于“原地修改”早期我的Save方法实现中没有使用临时变量save_filename而是直接修改m_filename。这导致了一个问题如果用户调用Save(“backup.ini”)保存了一个副本那么后续再调用无参数的Save()时就会错误地保存到backup.ini而不是原始文件。因此在Save函数中永远不要修改m_filename除非这是类的明确设计意图即“另存为”后对象就绑定到新文件。当前的实现是安全的Save()使用传入的文件名若为空则用m_filename但m_filename始终保持不变。这个简单的INI读写器虽然代码量不大但涵盖了C中文件I/O、字符串处理、数据结构设计、API封装和错误处理等多个核心概念。你可以直接使用这份源码也可以根据自己项目的特定需求进行裁剪和扩展。希望这份详细的拆解和源码能成为你工具箱里一件称手的小工具。
郑州网站建设
网页设计
企业官网