纯C++实现ini文件操作(可以自从此处下载 https://download.csdn.net/download/huangjiazhi_/93079858)
头文件
cpp
#ifndef INIRW_H
#define INIRW_H
#include <string>
#include <map>
#include <vector>
/**
* @brief INI配置文件读写类
* 特性:
* 1. 行首 # 为整行注释,空行、注释保存时原样保留
* 2. 支持读取 string / int / double / bool
* 3. 支持修改、新增分段与键值,保存不破坏原有注释排版
* 4. 提供格式合法性校验函数,输出详细错误行号与原因
* 5. 纯标准C++,仅依赖STL,无第三方库,跨平台
*/
/// 存储文件原始单行数据,区分注释行与配置行
struct IniLine
{
bool isComment; ///< true=空行/#注释行,false=[Section]或key=value配置行
std::string rawText; ///< 该行原始完整文本
};
class IniRW
{
public:
/**
* @brief 构造函数,自动加载指定INI文件
* @param filePath INI文件路径
*/
explicit IniRW(const std::string& filePath);
/// 析构,默认
~IniRW() = default;
/**
* @brief 从磁盘重新加载INI文件,清空原有缓存
* @return 加载成功返回true;文件不存在/打开失败返回false
*/
bool load();
/**
* @brief 将内存中修改后的配置写回原文件,保留全部原始注释与空行
* @return 写入成功true,文件无法打开返回false
*/
bool save();
/**
* @brief 校验当前加载的INI文件格式是否合法
* 校验规则:
* 1. [分段]必须成对括号,段名不可为空
* 2. 配置行必须包含=分隔符,键名不可为空
* 3. 所有key=value必须归属在某个[Section]下,禁止裸键值
* 4. 纯注释/空文件视为合法
* @return 格式合法true;存在语法错误false
*/
bool checkValid() const;
/**
* @brief 获取checkValid校验失败的错误描述
* @return 错误字符串(包含出错行号+原因),校验通过返回空字符串
*/
std::string getErrorMsg() const;
/**
* @brief 判断指定分段是否存在
* @param sec 分段名称
* @return 存在true,不存在false
*/
bool hasSection(const std::string& sec) const;
/**
* @brief 判断指定分段下某个键是否存在
* @param sec 分段名
* @param key 键名
* @return 存在true,不存在false
*/
bool hasKey(const std::string& sec, const std::string& key) const;
// ===================== 读取接口 =====================
/**
* @brief 读取字符串类型配置
* @param sec 分段名
* @param key 键名
* @param def 不存在分段/键时返回的默认值
* @return 配置值字符串
*/
std::string getString(const std::string& sec, const std::string& key, const std::string& def = "");
/**
* @brief 读取整型配置
* @param sec 分段名
* @param key 键名
* @param def 默认值
* @return 解析后整数;解析失败/不存在返回def
*/
int getInt(const std::string& sec, const std::string& key, int def = 0);
/**
* @brief 读取浮点型配置
* @param sec 分段名
* @param key 键名
* @param def 默认值
* @return 解析后浮点数;解析失败/不存在返回def
*/
double getDouble(const std::string& sec, const std::string& key, double def = 0.0);
/**
* @brief 读取布尔配置
* 识别真值:1 / true / yes;假值:0 / false / no
* @param sec 分段名
* @param key 键名
* @param def 默认值
* @return 布尔结果;无法识别返回def
*/
bool getBool(const std::string& sec, const std::string& key, bool def = false);
// ===================== 修改/写入接口 =====================
/**
* @brief 设置字符串键值,分段不存在自动创建;仅更新已有key行,不会追加新行
* @param sec 目标分段
* @param key 键名
* @param val 待写入字符串值
*/
void setString(const std::string& sec, const std::string& key, const std::string& val);
/**
* @brief 设置整型键值
*/
void setInt(const std::string& sec, const std::string& key, int val);
/**
* @brief 设置浮点键值
*/
void setDouble(const std::string& sec, const std::string& key, double val);
/**
* @brief 设置布尔键值,文件中存储true/false文本
*/
void setBool(const std::string& sec, const std::string& key, bool val);
/**
* @brief 【新增接口】在已有分段末尾追加新键值对
* @param sec 目标分段名
* @param key 新增键名
* @param val 键对应字符串值
* 说明:setString仅更新原有行,该接口会在对应分段尾部插入全新key=value行;
* 若分段不存在,则自动新建分段存放该键值
*/
void appendKeyInSection(const std::string& sec, const std::string& key, const std::string& val);
private:
/// 单分段内键值对存储
using SectionKV = std::map<std::string, std::string>;
std::map<std::string, SectionKV> m_data; ///< 解析后的键值缓存
std::vector<IniLine> m_lines; ///< 文件原始全部行(用于保存还原注释)
std::string m_filePath; ///< INI文件路径
mutable std::string m_errMsg; ///< 校验错误信息(mutable允许const函数修改)
/**
* @brief 去除字符串首尾空白字符(空格/制表符/换行)
* @param str 原始字符串
* @return 修剪后字符串
*/
static std::string trim(const std::string& str);
/**
* @brief 清空所有缓存数据、错误信息
*/
void clearAll();
/**
* @brief 查找指定分段在原始行数组中的起始下标与结束下标
* @param sec 分段名
* @param outStart 输出:分段[xxx]所在行下标
* @return 分段下一个其他分段的下标;无后续分段返回m_lines.size()
*/
size_t findSectionLineRange(const std::string& sec, size_t& outStart) const;
};
#endif
源文件
cpp
#include "IniRW.h"
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cctype>
#include <iomanip>
// 去除字符串首尾空白
std::string IniRW::trim(const std::string& str)
{
if (str.empty())
return "";
// 跳过开头空白
auto start = str.begin();
while (start != str.end() && std::isspace(static_cast<unsigned char>(*start)))
++start;
// 跳过末尾空白
auto end = str.end();
do
--end;
while (std::distance(start, end) > 0 && std::isspace(static_cast<unsigned char>(*end)));
return std::string(start, end + 1);
}
// 清空所有缓存与错误信息
void IniRW::clearAll()
{
m_data.clear();
m_lines.clear();
m_errMsg.clear();
}
// 构造自动加载文件
IniRW::IniRW(const std::string& filePath)
: m_filePath(filePath)
{
load();
}
// 从磁盘加载并解析INI
bool IniRW::load()
{
clearAll();
std::ifstream fin(m_filePath);
if (!fin.is_open())
return false;
std::string curSec;
std::string lineBuf;
while (std::getline(fin, lineBuf))
{
IniLine lineItem;
lineItem.rawText = lineBuf;
std::string trimLine = trim(lineBuf);
// 空行 / 行首#注释 标记为注释行
if (trimLine.empty() || trimLine.front() == '#')
{
lineItem.isComment = true;
m_lines.push_back(lineItem);
continue;
}
// 配置/分段行
lineItem.isComment = false;
m_lines.push_back(lineItem);
// 解析 [Section]
if (trimLine.front() == '[' && trimLine.back() == ']')
{
curSec = trim(trimLine.substr(1, trimLine.size() - 2));
continue;
}
// 解析 key=value
size_t eqPos = trimLine.find('=');
if (eqPos == std::string::npos)
continue;
std::string key = trim(trimLine.substr(0, eqPos));
std::string val = trim(trimLine.substr(eqPos + 1));
// 存在当前分段才存入缓存
if (!curSec.empty())
{
m_data[curSec][key] = val;
}
}
fin.close();
return true;
}
// 查找分段起始与结束行下标
size_t IniRW::findSectionLineRange(const std::string& sec, size_t& outStart) const
{
outStart = m_lines.size();
size_t secEndIdx = m_lines.size();
bool targetSecFound = false;
std::string tempSecName;
for (size_t i = 0; i < m_lines.size(); ++i)
{
const IniLine& item = m_lines[i];
if (item.isComment)
continue;
std::string s = trim(item.rawText);
if (s.front() == '[' && s.back() == ']')
{
tempSecName = trim(s.substr(1, s.size() - 2));
// 已经找到目标分段,遇到下一个分段则终止
if (targetSecFound)
{
secEndIdx = i;
break;
}
// 匹配目标分段,记录起始行
if (tempSecName == sec)
{
targetSecFound = true;
outStart = i;
}
}
}
return secEndIdx;
}
// 保存内存配置回文件,保留注释
bool IniRW::save()
{
std::ofstream fout(m_filePath, std::ios::trunc);
if (!fout.is_open())
return false;
std::vector<IniLine> newLineList = m_lines;
// 遍历所有分段,更新原有key、新增缺失分段
for (const auto& secPair : m_data)
{
const std::string& secName = secPair.first;
const SectionKV& kvMap = secPair.second;
size_t secStart = newLineList.size();
size_t secEnd = findSectionLineRange(secName, secStart);
// 场景1:分段已存在,更新分段内所有键值行
if (secStart != newLineList.size())
{
for (size_t i = secStart + 1; i < secEnd; ++i)
{
IniLine& lineItem = newLineList[i];
if (lineItem.isComment)
continue;
std::string s = trim(lineItem.rawText);
size_t eq = s.find('=');
if (eq == std::string::npos)
continue;
std::string k = trim(s.substr(0, eq));
// 缓存中存在该key则覆盖文本
if (kvMap.find(k) != kvMap.end())
{
lineItem.rawText = k + " = " + kvMap.at(k);
}
}
// ===================== 新增代码:追加分段内不存在的key =====================
std::map<std::string, bool> existKeys;
// 收集当前分段原有所有key
for (size_t i = secStart + 1; i < secEnd; ++i)
{
const IniLine& lineItem = newLineList[i];
if (lineItem.isComment)
continue;
std::string s = trim(lineItem.rawText);
size_t eq = s.find('=');
if (eq == std::string::npos)
continue;
std::string k = trim(s.substr(0, eq));
existKeys[k] = true;
}
// 将缓存中不存在的key插入到分段末尾
for (const auto& kv : kvMap)
{
const std::string& k = kv.first;
const std::string& v = kv.second;
if (!existKeys.count(k))
{
IniLine newKvLine;
newKvLine.isComment = false;
newKvLine.rawText = k + " = " + v;
newLineList.insert(newLineList.begin() + secEnd, newKvLine);
secEnd += 1;
}
}
// ==========================================================================
}
// 场景2:不存在该分段,追加全新[Section]与所有键值
else
{
IniLine secLine;
secLine.isComment = false;
secLine.rawText = "[" + secName + "]";
newLineList.push_back(secLine);
for (const auto& kv : kvMap)
{
IniLine kvLine;
kvLine.isComment = false;
kvLine.rawText = kv.first + " = " + kv.second;
newLineList.push_back(kvLine);
}
}
}
// 逐行写入文件
for (const auto& item : newLineList)
{
fout << item.rawText << "\n";
}
fout.close();
return true;
}
// 校验INI格式合法性
bool IniRW::checkValid() const
{
m_errMsg.clear();
std::string currentSection;
for (size_t lineIdx = 0; lineIdx < m_lines.size(); ++lineIdx)
{
const IniLine& item = m_lines[lineIdx];
std::string trimLine = trim(item.rawText);
// 注释、空行无需校验
if (item.isComment)
continue;
// 处理分段行 [xxx]
if (trimLine.front() == '[')
{
// 缺少右中括号
if (trimLine.back() != ']')
{
m_errMsg = "第" + std::to_string(lineIdx + 1) + "行错误:分段标记'['无对应']'";
return false;
}
// 段名不能为空
std::string secName = trim(trimLine.substr(1, trimLine.size() - 2));
if (secName.empty())
{
m_errMsg = "第" + std::to_string(lineIdx + 1) + "行错误:分段名称不能为空 []";
return false;
}
currentSection = secName;
continue;
}
// 普通配置行必须存在等号
size_t eqPos = trimLine.find('=');
if (eqPos == std::string::npos)
{
m_errMsg = "第" + std::to_string(lineIdx + 1) + "行错误:无效配置行,缺少'='分隔符";
return false;
}
// 键名不能为空
std::string key = trim(trimLine.substr(0, eqPos));
if (key.empty())
{
m_errMsg = "第" + std::to_string(lineIdx + 1) + "行错误:键名不能为空";
return false;
}
// 键值行不能在任何分段之外
if (currentSection.empty())
{
m_errMsg = "第" + std::to_string(lineIdx + 1) + "行错误:键值无归属分段,必须写在[Section]下方";
return false;
}
}
// 全部校验通过
return true;
}
// 获取校验错误信息
std::string IniRW::getErrorMsg() const
{
return m_errMsg;
}
// 判断分段是否存在
bool IniRW::hasSection(const std::string& sec) const
{
return m_data.find(sec) != m_data.end();
}
// 判断分段下键是否存在
bool IniRW::hasKey(const std::string& sec, const std::string& key) const
{
auto secIt = m_data.find(sec);
if (secIt == m_data.end())
return false;
return secIt->second.find(key) != secIt->second.end();
}
// 读取字符串
std::string IniRW::getString(const std::string& sec, const std::string& key, const std::string& def)
{
auto secIt = m_data.find(sec);
if (secIt == m_data.end())
return def;
auto keyIt = secIt->second.find(key);
if (keyIt == secIt->second.end())
return def;
return keyIt->second;
}
// 读取int
int IniRW::getInt(const std::string& sec, const std::string& key, int def)
{
std::string valStr = getString(sec, key, "");
if (valStr.empty())
return def;
std::istringstream iss(valStr);
int res;
if (iss >> res)
return res;
return def;
}
// 读取double
double IniRW::getDouble(const std::string& sec, const std::string& key, double def)
{
std::string valStr = getString(sec, key, "");
if (valStr.empty())
return def;
std::istringstream iss(valStr);
double res;
if (iss >> res)
return res;
return def;
}
// 读取bool
bool IniRW::getBool(const std::string& sec, const std::string& key, bool def)
{
std::string valStr = getString(sec, key, "");
std::string lowerStr;
lowerStr.resize(valStr.size());
std::transform(valStr.begin(), valStr.end(), lowerStr.begin(), ::tolower);
if (lowerStr == "1" || lowerStr == "true" || lowerStr == "yes")
return true;
if (lowerStr == "0" || lowerStr == "false" || lowerStr == "no")
return false;
return def;
}
// 设置字符串
void IniRW::setString(const std::string& sec, const std::string& key, const std::string& val)
{
m_data[sec][key] = val;
}
// 设置int
void IniRW::setInt(const std::string& sec, const std::string& key, int val)
{
std::ostringstream oss;
oss << val;
m_data[sec][key] = oss.str();
}
// 设置double
void IniRW::setDouble(const std::string& sec, const std::string& key, double val)
{
std::ostringstream oss;
oss << std::fixed << val;
m_data[sec][key] = oss.str();
}
// 设置bool
void IniRW::setBool(const std::string& sec, const std::string& key, bool val)
{
m_data[sec][key] = val ? "true" : "false";
}
// ===================== 新增接口实现:分段尾部追加键值 =====================
void IniRW::appendKeyInSection(const std::string& sec, const std::string& key, const std::string& val)
{
m_data[sec][key] = val;
}
// ==========================================================================
主函数
cpp
#include "mainwindow.h"
#include <QApplication>
#include "inirw.h"
#include <iostream>
#include <QTextCodec> // Qt编码头文件
#include <QTextCodec>
#include <QMessageBox>
#include <QScreen>
#include <QRect>
#include <QDebug>
#include <windows.h>
int main(int argc, char *argv[])
{
// 1. 获取 UTF-8 编码器
QTextCodec *utf8Codec = QTextCodec::codecForName("UTF-8");
if (utf8Codec) {
// ① 关键:设置 locale 编码为 UTF-8(影响文件 I/O、系统接口、QString 与 const char* 转换等)
QTextCodec::setCodecForLocale(utf8Codec);
// ② 兼容旧代码:设置 C 字符串(const char*)默认编码(Qt5 已废弃,但部分场景仍需)
//QTextCodec::setCodecForCStrings(utf8Codec);
// ③ 兼容 tr() 翻译:设置 tr() 函数的字符串编码(Qt5 已废弃,若用 tr 需设置)
//QTextCodec::setCodecForTr(utf8Codec);
}
// 2. (可选)设置 Windows 控制台编码为 UTF-8(避免控制台输出乱码)
SetConsoleOutputCP(CP_UTF8); // 控制台输出用 UTF-8
SetConsoleCP(CP_UTF8); // 控制台输入用 UTF-8
// // 解决MinGW控制台cout中文乱码核心代码
QTextCodec *codec = QTextCodec::codecForName("UTF-8");
QTextCodec::setCodecForLocale(codec);
QApplication a(argc, argv);
MainWindow w;
w.show();
// 初始化INI对象,自动加载config.ini
IniRW ini("config.ini");
// 1. 合法性校验
if (!ini.checkValid())
{
qDebug() << (QString)"INI文件格式校验失败:" + ini.getErrorMsg().c_str();
return -1;
}
qDebug() << "==== INI文件格式校验通过 ====\n\n";
// 2. 读取各类配置
std::string appName = ini.getString("Global", "AppName", "DefaultApp");
int port = ini.getInt("Net", "Port", 8080);
double ratio = ini.getDouble("Logic", "Rate", 1.0);
bool logSwitch = ini.getBool("Log", "Enable", false);
qDebug() << "【读取原始配置】\n";
qDebug() << (QString)"AppName: " + appName.c_str() ;
qDebug() << (QString)"Port: " << port << "\n";
qDebug() << (QString)"Rate: " << ratio << "\n";
qDebug() << (QString)"LogEnable: " << (logSwitch ? "true" : "false") << "\n\n";
// 3. 修改已有键值
ini.setString("Global", "AppName", "IniDemoClient");
ini.setInt("Net", "Port", 9999);
ini.setDouble("Logic", "Rate", 3.1415926);
ini.setBool("Log", "Enable", true);
// 4. 【新增功能测试】在已有分段末尾追加全新键值
ini.appendKeyInSection("Global", "new", "1024");
ini.setString("Global", "Describe", "主要修改了xxx");
// 4. 新增分段与键
ini.setString("NewSection", "Msg", "Hello INI RW");
ini.setInt("NewSection", "NumTest", 666);
ini.setString("Global", "Describe", "主要修改了xxx");
// 5. 保存修改到文件(原有#注释全部保留)
if (ini.save())
{
qDebug() << "配置修改完成,保存文件成功\n\n";
}
else
{
qDebug() << "保存INI文件失败!";
return -2;
}
// 6. 重新加载验证修改结果
ini.load();
if (ini.checkValid())
{
qDebug() << "【重新加载后读取修改值】\n";
qDebug() << "新AppName: " << ini.getString("Global", "AppName").c_str() << "\n";
qDebug() << "新端口: " << ini.getInt("Net", "Port") << "\n";
qDebug() << "新增字段Msg: " << ini.getString("NewSection", "Msg").c_str() << "\n";
}
return a.exec();
}