文章目录
-
- ini文件概要
- 代码实例分析
- 小结
ini文件概要
ini文件是一种系统配置文件,它有特定的格式组成。通常做法,我们读取ini文件并按照ini格式进行解析即可。在c++语言中,提供了模板类的功能,所以我们可以提供一个更通用的模板类来解析ini文件。
1、为什么要使用ini或者其它(例如xml,json)配置文件?
如果我们程序没有任何配置文件时,这样的程序对外是全封闭的,一旦程序需要修改一些参数必须要修改程序代码本身并重新编译,这样很不好,所以要用配置文件,让程序发布后还能根据需要进行必要的配置;配置文件有很多如INI配置文件,XML配置文件,还有就是可以使用系统注册表等。注意:ini的后缀名也不一定是".ini"也可以是".cfg",“.conf ”或者是”.txt"。因为ini文件实质就是txt文本文件。配置文件除了读取外还可以写入:例如用户设置了习惯模式,这样下次启动读取文件的时候也是该模式
kv文件,是ini执行后出现的文件
2、ini配置文件的格式
- 注释:以分号(;)开头,在我这个IniConfigParser实现中会视为注释行
- Section:这是划分不同用途的配置选项分类管理的逻辑划分单元,一个Section下包含多个Option
- Option:每个Section下就包含多个Option,一个Option就对应一个Key-Value-Pair.
- Document:是表示整个ini配置文档的,他直接管理对象单元是不同名称的Section.
补充:
ini文件有特定格式,由节、键、值组成。
节表示一段数据开头,用[]包含。之后的内容为“键=值”组成。注释使用分号表示(;)。在分号后面的文字,直到该行结尾都全部为注解。
如下为一个ini文件的例子:
1)对节进行说明:
所有的parameters都是以sections为单位结合在一起的。所有的section名称都是独占一行,并且sections名字都被方括号包围着([ and ])。在section声明后的所有parameters都是属于该section。对于一个section没有明显的结束标志符,一个section的开始就是上一个section的结束,或者是end of the file。Sections一般情况下不能被nested,当然特殊情况下也可以实现sections的嵌套。因为INI文件可能是项目中共用的,所以使用[Section Name]段名来区分不同用途的参数区。
2)对参数进行说明:INI所包含的最基本的“元素”就是parameter;每一个parameter都有一个name(key)和一个value,name和value是由等号“=”隔开。name在等号的左边。
3)对注释内容进行说明:在INI文件中注释语句是以分号 “;” 开始的(有些人定义类是由#注释,例如下面的RrConfig)。所有的所有的注释语句不管多长都是独占一行直到结束的,在分号和行结束符之间的所有内容都是被忽略的。
2、ini配置文件实现方式
解析文件后按照状态流转进行读取, 如下:
全部读取文件内容到内存后, 按照状态流转一次循环全部字符完成解析,解析内容存放成2级map结构。
代码实例分析
解析ini配置文件源码
CParseIniFile.h #pragma once #include <fstream> #include <iostream> #include <string> #include <map> using namespace std; #define COMMENT_CHAR '#' class CParseIniFile { public: CParseIniFile(); ~CParseIniFile(); bool ReadConfig(const string& filename, map<string, string>& mContent, const char* section); bool AnalyseLine(const string & line, string & key, string & val); void Trim(string & str); bool IsSpace(char c); bool IsCommentChar(char c); void PrintConfig(const map<string, string> & mContent); private: };
CParseIniFile.cpp #include "CParseIniFile.h" CParseIniFile::CParseIniFile() { } CParseIniFile::~CParseIniFile() { } bool CParseIniFile::ReadConfig(const string& filename, map<string, string>& mContent, const char* section) { mContent.clear(); ifstream infile(filename.c_str()); if (!infile) { //LOG4CXX_ERROR(logger, "file open error!"); return false; } string line, key, value; int pos = 0; string Tsection = string("[") + section + "]"; // string Tsection = "[" + section + "]";//错误写法invalid operands of types ‘const char [2]’ and ‘const char*’ to binary ‘operator+’ cout<< "Tsection: " << Tsection << endl; //输出:Tsection: [Student1] bool flag = false; while (getline(infile, line)) { if (!flag) { pos = line.find(Tsection, 0); // 查找节 Tsection 第一次出现的位置 string中的 find 返回的是int类型,找到就返回所在位置,未找到返回-1 ;0 代表从0位置开始查找 if (-1 == pos) { continue; //继续找 } else //找到第一个出现的[节] { flag = true; getline(infile, line);//将流对象infile 中的数据逐行存取到line中 cout<< "line: "<< line << " length: "<< line.length() << endl; //输出:line: name = liubei length: 13 此处length包含空格和“=” } } //length()函数是string的内置成员方,用于返回string类型字符串的实际长度。 if (0 < line.length() && '[' == line.at(0)) //line.at(0) 表示获取字符串line的第0号位字符 { cout<< "line.length():" << line.length() << " line.at(0): " << line.at(0) << endl; //输出:line.length():10 line.at(0): [ break; } if (0 < line.length() && AnalyseLine(line, key, value)) { if (value.length() > 0) { if (value[value.size() - 1] == '\r') { value[value.size() - 1] = '\0'; //查看string赋值操作,可以直接把字符赋值给当前的字符串,此处表示对最后一位字符赋值0 } } mContent[key] = value; } } infile.close(); return true; } bool CParseIniFile::AnalyseLine(const string & line, string & key, string & val) { if (line.empty()) { return false; } int start_pos = 0, end_pos = line.size() - 1, pos = 0; if ((pos = line.find(COMMENT_CHAR)) != -1) { if (0 == pos) {//行的第一个字符就是注释字符 return false; } end_pos = pos - 1; } string new_line = line.substr(start_pos, start_pos + 1 - end_pos); // 预处理,删除注释部分 if ((pos = new_line.find('=')) == -1) { return false; // 没有=号 } key = new_line.substr(0, pos); val = new_line.substr(pos + 1, end_pos + 1 - (pos + 1)); Trim(key); if (key.empty()) { return false; } Trim(val); return true; } void CParseIniFile::Trim(string & str) { if (str.empty()) { return; } int i, start_pos, end_pos; for (i = 0; i < str.size(); ++i) { if (!IsSpace(str[i])) { break; } } if (i == str.size()) { //全部是空白字符串 str = ""; return; } start_pos = i; for (i = str.size() - 1; i >= 0; --i) { if (!IsSpace(str[i])) { break; } } end_pos = i; str = str.substr(start_pos, end_pos - start_pos + 1); } bool CParseIniFile::IsSpace(char c) { if (' ' == c || '\t' == c) { return true; } return false; } bool CParseIniFile::IsCommentChar(char c) { switch (c) { case COMMENT_CHAR: return true; default: return false; } } void CParseIniFile::PrintConfig(const map<string, string> & mContent) { map<string, string>::const_iterator mite = mContent.begin(); for (; mite != mContent.end(); ++mite) { cout << mite->first << "=" << mite->second << endl; } }
config.ini文件
[Student1] name = liubei age = 28 record = 8 [Student2] ;name = zhangfei //注释 age = 29 record = 4 [Student3] name = guanyu age = 26 record = 6
主函数
#include "CParseIniFile.h" #include <iostream> #include <stdio.h> #define _KEY_ "name" //用宏定义name为_KEY_ int main() { // string fileName; // string section = "Student1"; map<string, string> fname; CParseIniFile config; bool flage = config.ReadConfig("config.ini", fname, "Student1"); //参数一:配置文件名(或路经名);参数二:map容器 参数三:需要写入map容器中的节 if (flage) { //判断节内的内容是否成功读取到map容器 auto name = fname["name"]; auto age = fname["age"]; // age是key,此处用到map容器的一种赋值方式map[key] = value // 此处key是字符串,必须加“”,要不然找不到变量定义 auto record = stod(fname["record"]); // stod 将字符串转化成double型 cout << name << endl; cout << age << endl; cout << record << endl; } bool flage2 = config.ReadConfig("config.ini", fname, "Student2"); if (flage2) { //name的另一种获取方法 auto Iter = fname.find(_KEY_); //利用map容器的find()查找节Student2下的name //判断该节中是否有name,或者查到name,但其对应的value为空,我们会给其赋一个默认值 string name; if ((fname.cend() == Iter) || (!(Iter->second.size()))) { name = "zhaoyun"; } else { name = Iter->second; } auto age = atof(fname["age"].c_str()); auto record = stod(fname["record"]); cout << name << endl; cout << age << endl; cout << record << endl; } // config.PrintConfig(fname); }
注意:
1 map容器的查找和key和value的赋值请参考:
https://blog.csdn.net/zxy_ZXY123/article/details/135966846
map 容器的详细总结-CSDN博客
2 c_str()及atof()和stod()的用法参考:
c++中c_str()及atof()和stod()的用法详细解析-CSDN博客
3 string 字符串长度的获取以及find()查找及子串获取 substr()参考:
C++ string的详细总结-CSDN博客
基于知识补充:
‘\n’ 换行,光标移到下一行的开头;
'\r' 回车,光标移到当前行的开头,不会换到下一行,如果接着输出的话,本行以前的内容会被逐一覆盖;
\t:表示水平制表空行操作,相当于Tab键,不会换行
{ cout << "this is the first line\n"; cout << "this is the second line\r"; cout << "this is the third line\n"; //会覆盖第二行length长度的部分 cout << "this is the fouth line\r"; cout << "this is the fifth line\n"; cout<<"First"<<"\n"<<"Second"<<endl; cout<<"First123"<<"\r"<<"Second"<<endl; //不换行,光标回到本行首位开始等长度覆盖 cout<<"First123"<<"\t"<<"Second"<<endl; cout<<"First123"<<"\0"<<"Second"<<endl; } 输出: this is the first line this is the third linee this is the fifth line First Second Second23 First123 Second First123Second
关于C++字符串中的"\0"问题
C++中没有字符串对象,字符串可以看成是字符数组,不过它们之间又有区别。
简单的来说就是区别在最后的一个元素"\0"上,它标志着一串字符是否是字符串。用字符串初始化字符数组时,"\0"附带在后面与前面的字符一起作为字符数组的元素。
在内存中,就是根据"\0"来确认字符串,如果找不到就会沿着字符一直找下去。它占用内存空间,但是不计入串长。
用字符串初始化字符数组时,系统会在字符数组的末尾自动加上一个字符"\0",因此数组的大小比字符串中实际字符的个数大。如:sizeof(str1)=strlen(str1) +1;
如果是用字符初始化数组,则一定要把"\0"作为一个元素放在初始值表中,不然就不会成为一个字符串。
① ‘0’ 代表 字符0 ,对应ASCII码值为 0x30 (也就是十进制 48) ② '\0' 代表 空字符(转义字符)【输出为空】, 对应ASCII码值为 0x00(也就是十进制 0), 用作字符串结束符 ③ 0 代表 数字0, 若把 数字0 赋值给 某个字符,对应ASCII码值为 0x00(也就是十进制0) ④ “0” 代表 一个字符串, 字符串中含有 2个字符,分别是 '0' 和 '\0' 下面补充说明(帮助理解) ① char ch_0 = ‘0’; // 字符0 赋值给一个字符,实际赋的 码值 为 0x30,十进制48 std::cout << ch_0 << '\n'; // 输出的 是 码值0x30 对应的 字符 0, 界面上看到的是0 std::cout << int(ch_0) << '\n'; // 输出的 是字符 ‘0’ 对应的码值 0x30,即十进制48 ,界面上看到的是 48 ② char ch_0 = '\0'; // 字符‘\0' 赋值给一个字符,实际赋的 码值 为 0x00,十进制0 std::cout << ch_0 << '\n'; // 输出的 是 码值0x00 对应的 空字符【NULL】, 界面上看到的是 空白,什么也看不见 std::cout << int(ch_0) << '\n'; // 输出的 是字符 ‘\0’ 对应的码值 0x00,即十进制0 ,界面上看到的是 0 ③ char ch_0 = 0; // 数字0 赋值给一个字符,实际赋的是 码值 std::cout << ch_0 << '\n'; // 输出的 是 码值0 对应的 字符,此处为 空白字符,即输出为空,界面上什么也看不见 std::cout << int(ch_0) << '\n'; // 输出的 是码值 0x00,即十进制0 ,界面上看到的是0 ④ char ch_0[ ] = "0"; // 字符串 “0” 初始化字符数组 std::cout << sizeof(ch_0) << ‘\n’; // 输出 ch_0 字节数, 界面显示 为2 std::cout << ch_0[0] << '\n'; // 输出字符 ‘0’,界面上看到的是 0 std::cout << ch_0[1] << '\n'; // 输出字符 ‘\0’,界面上看到的是 空白 std::cout << int( ch_0[0] )<< '\n'; // 输出字符 ‘0’ 对应的码值 0x30,界面上看到的是 48 std::cout << int ( ch_0[1] )<< '\n'; // 输出字符 ‘\0’ 对应的码值 0x00,界面上看到的是 0 注:以上代码 若是想拷贝试试,可能需要做相应的调整。因为为了方便大家学习,我有些地方多加了空格,能看得更清楚些,有问题欢迎留言探讨。 总结:记住几点 ① 用 数值 给 某个 字符变量 赋值时,相当赋 与该数字相同码值 所对应的字符 ② 用 字符 给 某个 字符变量赋值时, 即 赋 字符本身 ③ ‘\0’ 对应的 码值为0, 界面显示为 空白
\0是C++中字符串的结尾标志,存储在字符串的结尾。比如char cha[5]表示可以放4个字符的字符串,由于c/c++中规定字符串的结尾标志为'\0',它虽然不计入串长,但要占内存空间,而一个汉字一般用两个字节表示,且c/c++中如一个数组cha[5],有5个变量,分别是 cha[0] , cha[1] , cha[2] , cha[3] , cha[4] , 所以cha[5]可以放4个字母(数组的长度必须比字符串的元素个数多1,用以存放字符串结束标志'\0')或者放2个汉字(1个汉字占2个字节,1个字母占一个字节),cha[5]占5个字节的内存空间。
'\0'的ASCII是0 例如: char sText[5]; sText[0]='a'; sText[1]='a'; sText[2]='a'; sText[3]='a'; sText[4]='\0'; cout<<sText<<endl; //这样输出就是4个a // 如果数组的第五个元素即: sText[4]='a'; cout<<sText<<endl; //这样输出就是5个a和一堆乱码,甚至发生系统错误,因为该字符串没有字符串结尾符
{ // string s("liubei"); //string s("zhangfeel\r"); string s("zhangfeel"); cout << "s.size(): " << s.size() << endl; // \r占一个字符长度 cout << "sizeof(s): " << sizeof(s) << endl; cout << "s.size() - 1: " << s.size() - 1 << endl;//实际上取的最后一位l cout<< "s1 = " << s << endl; cout << "s[s.size() - 1]1: " << s[s.size() - 1] << endl; s[s.size() - 1] = '\0';//对字符串最后一位置空 cout<< "s2 = " << s << endl; cout << "s[s.size() - 1]2: " << s[s.size() - 1] << endl; if (s[s.size() - 1] == '\r') { s[s.size() - 1] = '\0'; cout << "s[s.size() - 1]_if: " << s[s.size() - 1] << endl; } 输出: s.size(): 9 sizeof(s): 32 s.size() - 1: 8 s1 = zhangfeel s[s.size() - 1]1: l s2 = zhangfee s[s.size() - 1]2:
string类对象以'\0'作为结束标志吗?
从图中我们可以看见,'\0'被插入到了字符串中并计入到了size中,但是string类型在直接输出时,并没有以'\0'作为输出的结束标志;而转为C类型字符串输出时,把'\0'当作了结束标志。
因此可以得出结论,string类型对象并不会以'\0'作为结束标志。
注意:
①C++中的string类对象会在末尾补上'\0',这是因为C++有C语言的历史包袱。因为C语言的字符串以'\0'结尾,所以为了方便在必要时将string字符串转为C类型字符串,所以string类型对象会在末尾补上一个不计入size和capacity的'\0'。②C++中的string类对象并不会将'\0'作为结束标志,因为string类对象内部维护了一个记录自身长度的成员变量size,在输出string类对象时会根据size的大小决定输出多少个字符,而不是看'\0'的位置决定输出到哪个字符结束。
注:用双引号括起来的字符串也属于C类型的字符串,所以它也是会以'\0'结尾的。
参考:【C++】string类构建的字符串以‘\0‘结束吗?_string输出是遇到\0停止还是不读取\0-CSDN博客
小结
声明:本文为学习总结,转载请注明出处!
参考:
1 C++实现解释ini配置文件的原理 - 知乎
2 https://jingyan.baidu.com/article/67508eb4c2fcf1ddcb1ce439.html
3 C++读取ini配置文件-CSDN博客
4 读写ini配置文件(C++)-CSDN博客