《白话C++》第8章 8.4.1 INI文件简介,8.4.2面向过程的设计 Page 761

8.4.1  INI文件简介

以下是一个INI例子文件的内容:

[DISPLAY_SETTING]
#是否显示启动窗口:
will_show_splash_window = yes
default_title = welcom...
[NETWORK_SETTING]
svc_host = www.d2school.com
svc_port = 80

带中括号的行代表一个“配置段(section)”的开始,该段所含内容直到碰上下一段或文件结束。

段下的内容是注释或配置项,注释以#开始。配置项一行为一项,使用“=”分为左边的“Key(键)”和右边的“Value(值)”。

例子中有两个配置段:[DISPLAY_SETTING]和[NETWORK_SETTING],正好每个配置段又有两行配置项。

程序需实现读指定段、指定键的配置值,比如指定“NETWORK_SETTING”和“svc_host”,得到“www.d2school.com”。还需实现对指定段、指定键的配置值的修改,并写回INI文件。

如果指定段或指定键不存在,则自动添加一项。

本例所示并非标准的INI语法,几点格式要求会影响程序的实现,特别说明如下:

(1)所有段名、键名、值的内容,均区分大小写;

(2)键名称不能以“#”开头;

(3)配置项的“键”和“值”,前后可以存在空格,实际操作时将被忽略。如“svc_port=80”,程序将自动去除等号前后的空格;

(4)“段”名称则指[ ]的所有内容,不作去空格处理。意思是[ ABC ]和[ABC]是不同的两个段。不过所在行首、行末的空格同样会被去除;

(5)注释应独占一行或多行,不能和配置项同行;

(6)修改配置项并回写文件后,原有注释内容应能保留,同时确保原文件各行次序不被改变。”

8.4.2  面向过程的设计

“读”和“写”分别是两个独立的过程,采用函数表示是:

//读INI文件指定段,指定键的值,value以string类型返回
//如果指定配置项不存在,则返回default_value
string ReadINIvalue(string const& filename
                    , string const& section
                    , string const& key
                    , string const& default_value);
//写INI文件指定段,指定键的值,value以string类型传入
//返回是否写成功。
string WriteINIvalue(string const& filename
                    , string const& section
                    , string const& key
                    , string const& default_value);

ReadINIValue()实现过程又可分解为:打开文件,然后,在文件中前进到指定“段(section)”,再在该段内,找到指定“键(key)”,找到则读出其值,否则返回“默认值(default_value)”。前进到指定“段”的函数命名为“GotoSection()”;查找指定“键”的函数命名为“FindValue()”:

//读INI文件指定段,指定键的值,value以string类型返回
//如果指定配置项不存在,则返回default_value
string ReadINIvalue(string const& filename
                    , string const& section
                    , string const& key
                    , string const& default_value)
{
    assert(!key.empty() && !section.empty());
    //打开配置文件
    ifstream file(filename);
    if(!file)
    {
        return default_value; //配置文件不存在?也返回默认值
    }
    //在"文件/file"中前进到指定“段/section”
    if(!GotoSection(file, section))
    {
        return default_value;
    }
    string value;
    //在"文件/file"中读取指定"键/key"的值"值/value"
    if(!FindValue(file, key, value))
    {
        return default_value; //找不到指定"键",返回默认值
    }

    return value; //如果找到了,则返回value
}

“前进到指定段(GotoSection)”子过程的实现很简单:循环读取文件的每一行,然后和加了中括号的段名称比较,相等就是找到了:

//循环读取文件的每一行,
//然后和加了中括号的段名称比较,相等就是找到了
bool GotoSection(ifstream& file, string const& section)
{
    //比如待找的段是"NETWORK_SETTING",则参与比较的应是
    //"[NETWORK_SETTING]"
    string secion_line = "[" + section + "]";
    while(!file.eof()) //文件未结束..
    {
        string line;
        getline(file, line); //从文件中读出一行
        Trim(line); //去除行首尾的空格
//        cout << line << endl;
        if(line == secion_line)
        {
            //找到了。
            //注意!此时ifstream的位置在段名的下一行

            return true;
        }
    }

    return false; //找不到
}

使用“Trim”函数将入参的字符串去除其前后的空格(‘ ’、‘\t’),避免配置文件中的某一行前后有意无意地有个空格,造成找不到。Trim函数也由我们手动打造

//去掉字符串两端的空格
void Trim(string& str)
{
    //注意“\t”后有一空格
    str.erase(0, str.find_first_not_of("\t ")); 
    str.erase(str.find_last_not_of("\t ") + 1); //同上
}

再来考虑“FindValue”的实现。刚才“GotoSection()”返回本段第一行的位置,FindValue就在本段中找有指定“键”的行。在“本段中”的意思是:只要碰上有新的一段就结束查找。那么查找指定键呢 ?我们暂时将该操作命名为“SplitKeyValue()”,则“FindValue()”的实现如下:

//将从file中读到的值,写入value
bool FindValue(ifstream& file, string const& key, string& value)
{
    while(!file.eof()) //文件未结束..
    {
        string line;
        getline(file, line); //从文件中读出一行
        Trim(line);//去除行首行尾空格
        if(line.empty())
        {
            continue; //空行?跳过...
        }
        if(line[0] == '[') //碰上新一段了呀,放弃查找
        {
            break; //碰上新一段,放弃查找
        }
        //按"="拆成左键右值
        KeyValue kv = SplitKeyValue(line);
        if(kv.key == key) //是要找的键...
        {
            value = kv.value;
            return true; //找到了
        }
    }

    return false; //找不到
}

“SplitKeyValue()”的逻辑是:查找给定字符串中,包含有第一个“=”符号的位置,再以该位置将字符串拆成两段,分别去除前后空格,就得到key和value。KeyValue是一个结构,二者定义如下:

struct KeyValue
{
    string key;
    string value;
};
//将字符串分解成KeyValue结构体
KeyValue SplitKeyValue(string const& str)
{
    KeyValue result;
    //提醒:= 前后并无空格
    string::size_type pos = str.find("=");
    //find()返回值是字母在母串中的位置(下标记录),
    //如果没有找到,就会返回一个特别的
    //标记npos,也就是-1. 返回值可以看成是一个int类型的数
    if(pos == string::npos) 
    {                       
        return result;
    }
    //取子串:从位置0开始,长度pos
    result.key = str.substr(0, pos); 
     //取子串;从位置pos + 1到结束
    result.value = str.substr(pos + 1);
    Trim(result.key);
    Trim(result.value);
    return result;
}

有关“ReadINIValue()”操作的所有子过程都实现了。将以上代码按正确的依赖次序,组织到同一文件中,就可以进行读INI文件的测试了:

#include <iostream>
#include <list>
#include <fstream>
#include <cassert>

.../* 以上代码略 */

int main()
{
    string const& filename = "demo.ini";
    string value = ReadINIvalue(filename
                                , "NETWORK_SETTING"  //section
                                , "svc_host"         //key
                                , "127.0.0.1");      //default_value
    cout << value << endl; //www.d2school.com 
    return 0;
}

接下来实现“WriteINIValue()”操作。

是否只需把“ifstream”换成“ofstream”,再改改一些小逻辑就能解决?常规的文件改写操作,并不能直接在文件流上处理。假设某文件中有一行内容:abcdefg,然后要将其中的‘c’改成“CCC”,并不能先定位到‘c’的位置,然后直接写上“CCC”了事,那样后面的内容会丢失。

正确的做法是将全部内容读入,然后在程序中(内存中)将“abcdefg”替换成“abcCCCdefg”,再整串输出到源文件中。所以,变化很大:我们会将文件所有内容读入到一个list再做处理。

下面的代码,增加了修改操作所需添加的代码,

#include <list>

//去除字符串前后空白,但并不是在原字符串身上操作
//而是使用一个复制品,在复制品上去除,并返回该复制品。
string TrimCopy(string const& str)
{
    string str_copy = str;
    Trim(str_copy);
    return str_copy;
}

//又一个GotoSection,但这个是在list <string> 上操作,
//返回迭代器位置,也是执行指定section的下一行
list <string> :: iterator 
    GotoSection(list <string>& lines, string const& section)
{
    string section_line = "[" + section + "]";
    for(auto it = lines.begin(); it != lines.end(); ++it)
    {
        //去除空格只是为了比较,并不会真正修改原有内容
        string line = TrimCopy(* it);
        if(line == section_line)
        {
            ++ it;
            return it;
        }
    }
    //没找到指定段,添加之
    lines.push_back(section_line);
    return lines.end();
}

//beg是之前GotoSection()返回的位置,在这个位置开始,向后查找
//含有指定键的行。如果找不到会在本段结束位置插入一行
void ChangeValue(list <string>::iterator beg
                ,list <string> & lines
                ,string const& key
                ,string const& value)
{
    auto it = beg;
    for(it; it != lines.end(); ++ it)
    {
        string line_trim = TrimCopy(*it);
        if(line_trim.empty())
        {
            continue;
        }
        if(line_trim[0] == '[')  //碰到下一段了?放弃查找,跳出循环...
        {
            break;
        }
        //line_trim既不是空,又不是段名,将期分解为键值对
        KeyValue kv = SplitKeyValue(line_trim); 
        if(kv.key == key)
        {
            if(kv.value != value)//值不相等才修改
            {
                string line_with_new_value = (key + "=" +value);
                * it = line_with_new_value; //在list中改掉这行的内容
            }
            return;
        }
    }
    //如果lines中找不到指定的键值对,
    string new_line = (key + "=" +value);
    lines.insert(it, new_line);
}

//把文件所有行,读入到内存中的list
list <string> ReadLines(ifstream& ifs)
{
    list <string> lines;
    while(!ifs.eof())
    {
        string line;
        getline(ifs, line); //将line读入到ifs中
        lines.push_back(line);
    }
    return lines;
}

//对应ReadLines, 将list中的所有行,写入文件
void WriteLines(ofstream& ofs, list <string> const& lines)
{
    for(auto it = lines.begin(); it != lines.end(); ++ it)
    {
        ofs << *it;
        if(it != --lines.end()) //避免每次写,都多产生一个空行
        {
            ofs << endl;
        }
    }
}

//修改指定INI文件,指定段,指定配置项的值
bool WriteINIvalue(string const& filename
                   , string const& section
                   , string const& key
                   , string const& value)
{
    assert(!key.empty() && !section.empty());
    list <string> lines;
    //打开指定INI文件
    //注意,如果源文件不存在也不要紧,后面ofstream会创建它
    //将文件读入一个输入流中,文件名为filename
    ifstream ifs(filename); 
    //如果打开成功,就读入原文件所有行
    if(ifs.is_open()) 
    {
        lines = ReadLines(ifs);
        ifs.close();
    }
    //尝试找指定section在list中的位置(返回该位置下一行)
    //前进到指定的section的下一行
    list <string> :: iterator it = GotoSection(lines, section); 
    //在指定段中修改指定配置项的值
    ChangeValue(it, lines, key, value); //改变这一行的键和值
    //输出:
    ofstream ofs(filename);
    if(!ofs)
    {
        return false; //输出文件打不开,只能返回错误
    }
    //将lines内容输出到输出流,进而写到文件中
    WriteLines(ofs, lines);  

    return true;
}

以下是完整的读写INI测试的代码:

int main(int argc, char** argv)
{
    //新文件,第一次运行时不存在
    string const& filename = "demo_new.ini";
    string value = ReadINIvalue(filename
                                , "NETWORK_SETTING"  //section
                                , "svc_host"         //key
                                , "127.0.0.1");      //default_value
    cout << value << endl; //127.0.0.1
    value = "www.d2school.com";
    WriteINIvalue(filename, "NETWORK_SETTING", "svc_host", value);
    WriteINIvalue(filename, "USER_INFO", "name", "Tom");

    return 0;
}

新需求:

在保留现有功能的同时,要求采集操作过程中的出错信息。在现有代码的基础上要扩展此功能,简单粗暴的做法是为函数再增一个列表参数,以读为例:

string ReadINIvalue(string const& filename
                    , string const& section
                    , string const& key
                    , string const& default_value
                    , list <string>& warning_list
                    );
  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值