纯C++实现ini文件操作

纯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();
}
相关推荐
zhixingheyi_tian1 小时前
JVM 之 GCLocker
java·jvm
码哥字节1 小时前
把 Cursor 的烂输出变成好代码,这 10 个 Prompt 改造让我省了 80% 改稿时间
java·ai编程工具·cursor 配置·prompt 技巧
2503_931712481 小时前
10m/s超高速电梯:西奥XO-NEWIII如何树立行业速度标杆
java·大数据·数据库
折哥的程序人生 · 物流技术专研1 小时前
第4篇:抽象工厂模式——产品族一键创建
java·设计模式·抽象工厂模式·创建型模式·编程进阶·产品族·扩充系列
正儿八经的少年1 小时前
Spring Boot
java·spring boot·后端
未秃头的程序猿2 小时前
虚拟线程用了一年,我真香了——10倍吞吐量是真实存在的
java·后端·面试
沙蒿同学2 小时前
用 RAG+FAISS 构建古诗词 AI 取名系统:从向量检索到六层质量过滤的实战
后端·aigc·ai编程
豆瓣鸡2 小时前
封装 Jackson 配置 Starter:统一解决 LocalDateTime 请求解析和响应格式化问题
java·spring boot
xiaogai_gai2 小时前
钉钉数据集成到金蝶云星空案例分享:传给钉钉后,回传金蝶字段
java·前端·钉钉