关于字串拷贝(strcpy之类)和内存拷贝(memcpy)

本文探讨了内存拷贝函数memcpy及其改进版本mymemcpy的实现方式,同时对比了memcpy与memmove的区别。此外,还详细介绍了字符串复制函数strcpy、strncpy的功能及使用注意事项,并解释了sprintf与snprintf在字符串格式化输出方面的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

先说一下关于memcpy,有参考http://blog.youkuaiyun.com/xiaobo620/article/details/7488827,是直接拷贝内存,因而不会有类型限制,最简单的实现就是:

void *mymemcpy(void *dst,const void *src,size_t num)
{
	assert((dst!=NULL)&&(src!=NULL));
          //assert(des>=src+num||src>dst+num);
	byte * psrc = (byte *)src;//byte 既为unsigned char类型
	byte * pdst = (byte *)dst;
	while(num-->0)*pdst++ = *psrc++;
	return dst;
}

但是这种写法,并没有考虑到两个拷贝内存的重叠的问题,所以,进一步的改进,(通过从尾部读起可解决):

void * mymemcpy(void *dest, const void *src, size_t count)
{
    if (dest == NULL || src == NULL)
          return NULL;
    char *pdest = static_cast <char*>(dest);
    const char *psrc  = static_cast <const char*>(psrc);
    int n = count;
    
    if (pdest > psrc && pdest < psrc+count)
    {
        for (size_t i=n-1; i != -1; --i)
        {
                pdest[i] = psrc[i];
        }
    }
    else
    {
        for (size_t i= 0; i < n; i++)
        {
                pdest[i] = psrc[i];
        }
    }
    
    return dest;
}
如果加之最求效率,可以通过字长来拷贝,不对齐的部分在通过字节拷贝:

void *mymemcpy(void *dst,const void *src,size_t num)
{
	assert((dst!=NULL)&&(src!=NULL));
	int wordnum = num/4;//计算有多少个32位,按4字节拷贝
	int slice = num%4;//剩余的按字节拷贝
	int * pintsrc = (int *)src;
	int * pintdst = (int *)dst;
	while(wordnum--)*pintdst++ = *pintsrc++;
	while (slice--)*((char *)pintdst++) =*((char *)pintsrc++);
	return dst;
}
默认的memcpy是 没有考虑重叠的,另外的一个函数memmov则是考虑了这个情况,具体实现就如同上面源码类似。

下来就说一下字串的复制,当然memcpy就行,他很一般比如strcpy的区别就是:(参考 http://www.cnblogs.com/stoneJin/archive/2011/09/16/2179248.htmlhttp://blog.youkuaiyun.com/zzy7075/article/details/7835361

1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

实现也是简单的了:

char * strcpy(char * dest, const char * src) // 实现src到dest的复制
{
  if ((src == NULL) || (dest == NULL)) //判断参数src和dest的有效性
  {
 
      return NULL;
  }
  char *strdest = dest;        //保存目标字符串的首地址
  while ((*strDest++ = *strSrc++)!='\0'); //把src字符串的内容复制到dest下
  return strdest;
}
还有就是strncpy字串复制 :char * strncpy(char *dest, char *src, size_t n);
  功能:将字符串src中最多n个字符复制到字符数组dest中(它并不像strcpy一样遇到NULL才停止复制,而是等凑够n个字符才开始复制),返回指向dest的指针。
  说明:
  如果n > dest串长度,dest栈空间溢出产生崩溃异常。
  否则:
  1)src串长度<=dest串长度,(这里的串长度包含串尾NULL字符)
  如果n=(0, src串长度),src的前n个字符复制到dest中。但是由于没有NULL字符,所以直接访问dest串会发生栈溢出的异常情况。
  如果n = src串长度,与strcpy一致。
  如果n = dest串长度,[0,src串长度]处存放于desk字串,(src串长度, dest串长度]处存放NULL。
  2)src串长度>dest串长度
  如果n =dest串长度,则dest串没有NULL字符,会导致输出会有乱码。如果不考虑src串复制完整性,可以将dest最后一字符置为NULL。
  综上,( 一般正常情况下不会补\0,只有在n>src时,不足的位数会进行补\0)一般情况下,使用strncpy时,建议将n置为dest串长度(除非你将多个src串都复制到dest数组,并且从dest尾部反向操作),复制完毕后,为保险起见,将dest串最后一字符置NULL,避免发生在第2)种情况下的输出乱码问题。当然喽,无论是strcpy还是strncpy,保证src串长度<dest串长度才是最重要的。

还有就是sprintf和snprintf了:

sprintf:把格式化的数据写入某个字符串, 可以连接多个不同类型的字符串

  头文件:stdio.h函数原型:int sprintf( char *buffer, const char *format [, argument] … );
  返回值:字符串长度(strlen)

一般就是类似于 sprintf(dest,"%s",src)之类的了。

最后snprintf:

int snprintf(char *restrict buf, size_t n, const char * restrict format, ...);
函数说明:最多从源串中拷贝n-1个字符到目标串中,然后再在后面加一个0。所以如果目标串的大小为n
的话,将不会溢出。若成功则返回欲写入的字符串长度,若出错则返回负值。

就这样现了。

#ifndef _INI_DOCUMENT_H_ #define _INI_DOCUMENT_H_ #define ASCIILINESZ 1024 //每行文本能容纳文字的最大长度 #define ASCIISECTIONSZ 256 //每个Section的最大长度 #define ASCIIKEYSZ 256 //每个Key的最大长度 #define ASCIIVALUESZ 256 //每个Value的最大长度 #define ASCIIDESCSZ 512 //每个注释的最大长度 #include "RBTree.h" #include "Section.h" #include "Key.h" #include "Value.h" namespace INI { using KeyMap = RBTREE::RBTree<Key, Value>; using SectionMap = RBTREE::RBTree<Section, KeyMap>; class IniDocument { typedef enum class _line_status_ : unsigned char { LINE_ERROR,//未知行 LINE_DESCRIPTION,//注释行 LINE_SECTION,//节点行 LINE_KEY//键值行 } line_status; public: IniDocument(const char* _szFile); ~IniDocument(); KeyMap& operator[](const char* _szSection); void Clear(); private: void load(); line_status parse(char* _szLine, char* _szSection, char* _szKey, char* _szValue, char* _szDesc); private: char* m_szFile;//文件路径 SectionMap m_sectionMap; //存储section的映射 }; } #include "../inc/IniDocument.h" INI::IniDocument::IniDocument(const char* _szFile) { if (!_szFile || _szFile[0] == '\0') { //错误处理: 文件路径不能为空 throw("文件路径不能为空."); } size_t nLen = strlen(_szFile); size_t nSize = ((nLen + 1) + 3) & ~3; if (nLen >= _MAX_PATH) { //错误处理: 文件路径过长 throw("文件路径过长."); } m_szFile = new char[nSize]; memcpy(m_szFile, _szFile, nLen + 1); load(); //加载文件内容到内存 } INI::IniDocument::~IniDocument() { Clear(); } INI::KeyMap& INI::IniDocument::operator[](const char* _szSection) { //返回对应Section的映射 return m_sectionMap[Section(_szSection)]; } void INI::IniDocument::Clear() { if (m_szFile) { delete[] m_szFile; m_szFile = nullptr; } for(SectionMap::iterator it=m_sectionMap.begin();it!=m_sectionMap.end();++it) { //清空每个Section的映射 it->second.Clear(); } m_sectionMap.Clear(); //清空Section映射 } void INI::IniDocument::load() { /*特别注意 读出的ini内容一般都是utf-8,而保存在内存中的内容也是utf-8.直接保存到文件没问题 但如果查找就会出问题。要把查找的中文字串 转成utf8再查找才能找到 但每次查找section或者key都转一次utf8就浪费了 解决方案: 从文件解析出来的utf8内容转回普通char字串。再保存到内存中,这样代码直接就能查找到。 而保存回文件的时候,把内存字串转回utf8再保存即可 如果不想频繁转换,直接使用ANSI编码文件即可 */ #pragma warning(disable:4996) FILE* pFile = fopen(m_szFile, "r"); #pragma warning(default:4996) if (!pFile) { printf("<IniDocument::load> 打开Ini文件失败,路径: %s\n", m_szFile); return; } char szLineBuffer[ASCIILINESZ] = { 0 };//把文件中的内容逐行读入此内存中 char szSection[ASCIISECTIONSZ] = { 0 };;//把节点解析完成后放入此内存中 char szKey[ASCIIKEYSZ] = { 0 };;//把Key解析完成后放入此内存中 char szValue[ASCIIVALUESZ] = { 0 };;//把Value解析完成后放入此内存中 char szDesc[ASCIIDESCSZ] = { 0 };;//把注释读入此内存中 size_t nLines = 0; //记录当前的行数 size_t nLen = 0; //记录当前行的长度 KeyMap* pKeyMap = nullptr; //用于存储当前解析的Key映射 Desc desc; //用于存储当前解析的全行注释 while (fgets(szLineBuffer, ASCIILINESZ, pFile))//每次读取一行内容 { ++nLines; //行数自增 if (szLineBuffer[0] == '\n')continue;//如果取出来的内容第一个字符就是换行符,无条件取下一行内容 nLen = strlen(szLineBuffer);//更新行内容的字串长度 if (szLineBuffer[nLen - 1] != '\n' && !feof(pFile)) { //如果此行的结束符 不是换行符\n 并且 文件并未结束 -> 如果都满足条件,说明此行肯定是太长了。应当调整行的最大长度。默认:ASCIILINESZ=1024 printf("<IniDocument::load> 行 %zu 内容过长,请检查文件内容.\n", nLines); continue; //跳过此行 } //开始进行字串解析 switch (parse(szLineBuffer, szSection, szKey, szValue, szDesc))//进入解析过程 { case line_status::LINE_ERROR://未知行 break; case line_status::LINE_DESCRIPTION://全行注释 { desc = szLineBuffer; //将注释内容存储到Desc对象中 break; } case line_status::LINE_SECTION://节点 { Section section(szSection); //创建Section对象 if(desc.m_szDesc)section.m_vecDesc[0] = std::move(desc); //将注释内容存储到Section对象中 if (szDesc[0] != '\0')section.m_vecDesc[1] = szDesc; //如果有第二个注释内容,存储到Section对象中 pKeyMap = &m_sectionMap[section]; //获取当前Section映射 break; } case line_status::LINE_KEY://键值 { if (!pKeyMap) { printf("<IniDocument::load> 键值行 %zu 解析失败<没有Section>,请检查文件内容.\n", nLines); continue; //如果没有Section映射,跳过此行 } Key key(szKey); //创建Key对象 Value value(szValue); //创建Value对象 if (desc.m_szDesc) //如果有全行注释内容 { //将注释内容存储到Key对象中 key.m_vecDesc[0] = std::move(desc); } if(szDesc[0] != '\0') //如果有行块注释内容 { key.m_vecDesc[1] = szDesc; //将行块注释内容存储到Key对象中 } (*pKeyMap)[key] = std::move(value); //将KeyValue存储到当前Section映射中 break; } default: break; } szLineBuffer[0] = 0; //清空行内容 szSection[0] = 0; //清空Section内容 szKey[0] = 0; //清空Key内容 szValue[0] = 0; //清空Value内容 szDesc[0] = 0; //清空注释内容 } fclose(pFile); //关闭文件 } INI::IniDocument::line_status INI::IniDocument::parse(char* _szLine, char* _szSection, char* _szKey, char* _szValue, char* _szDesc) { #pragma warning(disable:4996) line_status sta = line_status::LINE_ERROR;//析结果 size_t nCount = 0; //记录sccanf匹配的数量 if (_szLine[0] == '#' || _szLine[0] == ';')//开头就是单行注释的解析符 {//说明全行都是注释 sta = line_status::LINE_DESCRIPTION; } //开始解析节点 Section //(必须遵守一个规则,从有到无,比如结尾带号的应该先做匹配。 //然后再到结尾不带号的。因为如果先匹配不带号的,带号的也会匹配得上。就会莫名多个号了) //例如: key = value; 如果先匹配无号的,那么值就是 value; 达不到预期效果,结尾多了个号 else if ( //以下匹配 节点(后面带注释) (nCount = sscanf(_szLine, "[%[^]]]%[^\n]", _szSection, _szDesc)) == 2 ||//例子: [section01]#this is description 01或者[section01];this is description 01 (nCount = sscanf(_szLine, "[%[^]]]%[^\n]", _szSection, _szDesc)) == 1//例子: [section] ) { sta = line_status::LINE_SECTION; } //开始解析键值 key = value else if ( //(必须遵守一个规则,从有到无,比如结尾带号的应该先做匹配。然后再到结尾不带号的。因为如果先匹配不带号的,带号的也会匹配得上。就会莫名多个号了) (nCount = sscanf(_szLine, "%[^=]=%[^;]%[^\n]", _szKey, _szValue, _szDesc)) == 3 || //例子: key1=value1;this is description 01 (nCount = sscanf(_szLine, "%[^=]=%[^#]%[^\n]", _szKey, _szValue, _szDesc)) == 3 ||//例子: key2=value2#this is description 01 (nCount = sscanf(_szLine, "%[^=]=%[^\n]", _szKey, _szValue)) == 2//例子: key1=value1 ) { sta = line_status::LINE_KEY; } #pragma warning(default:4996) return sta; } 总体架构就是这样的,帮我析一下有没有问题
最新发布
07-25
<think>我们析给定的INI文件解析器代码。主要关注以下几个方面: 1. 头文件保护:使用#ifndef _INI_DOCUMENT_H_ ... #endif 防止重复包含。 2. 宏定义:定义了各种字符串的最大长度。 3. 命名空间:使用命名空间INI。 4. 类IniDocument:用于表示整个INI文档。 - 内部枚举:定义了行状态(错误行、注释行、节点行、键值行)。 - 构造函数:接受文件路径,并加载文件。 - 析构函数:清理资源。 - 操作符[]:用于通过节点名(section)获取对应的键值映射(KeyMap)。 - Clear方法:释放资源。 - 私有方法load:加载并解析文件。 - 私有方法parse:解析一行文本,判断其类型并提取信息。 5. 成员变量: - m_szFile:文件路径。 - m_sectionMap:存储整个INI结构的红黑树(SectionMap),其中每个节点是一个Section,对应一个KeyMap(也是红黑树,存储键值对)。 问题析: 1. 构造函数中,文件路径长度检查使用了_MAX_PATH,但_MAX_PATH是Windows特有的,如果跨平台需要注意。 2. 内存配:构造函数中内存时,使用`((nLen + 1) + 3) & ~3`进行对齐(对齐到4字节),但实际没有必要,因为字符串不需要对齐。而且,如果文件路径很长(接近_MAX_PATH)且超过,会抛出异常,但异常是C风格的字符串,应该使用C++异常类型。 3. 在load函数中,使用fopen打开文件,但未指定二进制模式(在Windows上,文本模式二进制模式有区别,可能会影响换行符的读取)。另外,使用fgets读取行,如果行长度超过ASCIILINESZ-1,则会被截断,但代码中有一个检查:如果一行读完后没有遇到换行符且文件未结束,则报错跳过。这里处理基本合理。 4. parse函数使用sscanf进行解析,这种方式容易出错且难以处理空格等。例如: - 解析section时,格式为`[%[^]]]`,这可以匹配`[section]`,但不能匹配中间有空格的情况(如`[ section ]`),而且如果section中含有右括号']'则会提前结束。 - 解析键值行时,格式`%[^=]`会读取直到等号,但键名等号之间如果有空格,则空格会被保留在键名中。同样,等号值之间的空格也会被包含在值中。通常,INI文件允许键名值两端的空格被忽略。 - 注释部:在解析键值行时,如果值后面有注释,则注释会被当作值的一部,然后又被单独提取到_szDesc中?实际上,在键值行中,注释符(#或;)只能出现在行尾,且前面可能有空格。但sscanf格式`%[^;]`会一直匹配到号,但号可能出现在值中(如果值被引号包围,则号不应被当作注释)。因此,这种解析方式不够健壮。 5. 注释处理:在解析section行时,如果该行有注释(在section后),则会被提取到_szDesc。但是,在解析键值行时,注释同样被提取到_szDesc。然而,在section行中,注释被别存到section对象的两个描述成员(m_vecDesc[0]m_vecDesc[1])中,而键值行中,注释被存到key对象的两个描述成员中。这里的设计是每个对象最多存储两个注释?但注释的来源不同:全行注释(以#或;开头的行)行尾注释。在section行中,第一个注释(desc)是来自之前的一个全行注释(如果有),然后第二个注释是来自该行行尾的注释。在键值行中,同样处理。但这里存在一个问题:全行注释在遇到下一个非注释行时会被清空(desc对象在每次循环中都会被重置),但代码中在解析section行时使用了之前存储的desc(即上一个全行注释),这可能导致将上一个全行注释错误地关联到当前的section上。例如: # 这是section1的注释 [section1] ; 这是section1的行尾注释 # 这是key1的注释 key1=value1 ; 这是key1的行尾注释 在解析section1行时,会将之前的全行注释(“这是section1的注释”)存到section1的m_vecDesc[0],而将行尾注释存到m_vecDesc[1]。然后,在解析key1行时,会将全行注释(“这是key1的注释”)存到key1的m_vecDesc[0],行尾注释存到m_vecDesc[1]。这样看起来是合理的,但注意:在解析section行后,desc被使用了,但并没有被清空,所以下一个键值行解析时,如果之前没有全行注释,那么desc可能还是之前的值(但在循环末尾,desc被重置了吗?)。实际上,在循环中,desc是一个局部变量,在每次循环开始时,desc是未指定的(因为Desc结构体没有默认构造函数,我们假设其内部指针初始化为空?)。另外,在循环末尾,将desc赋值给key后,使用了std::move,那么desc会被置为空吗?这取决于Desc类的移动构造函数。如果没有定义移动构造函数,则可能只是拷贝,然后desc仍然保留值。但代码中Desc类没有给出,所以这里存在隐患。 6. 在解析键值行时,如果遇到等号,则键名值之间的空格也会被包含。通常,INI解析器会去除键名值两端的空格。 7. 在parse函数中,使用sscanf匹配多个模式,但是同一个模式被重复写了两次(如匹配section的两个sscanf完全一样)。另外,匹配键值行的模式中,第三个模式(没有注释)只匹配了两个变量,所以当匹配数量为2时成立。但是,如果键值行中包含了等号但没有值(如`key=`),那么_szValue将为空字符串,这是允许的。 8. 在load函数中,当解析到键值行时,会检查pKeyMap是否为空(即当前是否在某个section内),如果没有则报错。这是合理的,因为键值对必须属于某个section。 9. 内存管理:在Clear函数中,先删除m_szFile,然后遍历m_sectionMap,对每个KeyMap调用Clear(注意:KeyMap是红黑树,其Clear函数应该会释放所有节点)。然后调用m_sectionMap.Clear()。但要注意,如果红黑树的节点中存储的对象(如Section、Key、Value)有动态内存,需要确保它们的析构函数正确释放内存。由于代码中没有给出这些类的定义,我们假设它们有适当的析构函数。 10. 编码问题:在load函数的开头有一段关于UTF-8的注释,说明了解析UTF-8的问题。但代码中并没有进行任何编码转换,所以如果文件是UTF-8,而程序使用ANSI(如GBK)来解析,那么中文字符可能会乱码。根据注释,作者建议在保存时转换,但代码中未实现。 11. 异常处理:构造函数中,如果文件路径为空或过长,会抛出字符串异常。这不符合C++最佳实践(应抛出std::exception派生类)。另外,在load函数中,如果打开文件失败,只是打印错误信息并返回,没有抛出异常,这可能导致后续使用空的数据。 12. 移动语义:在存储Value时使用了移动语义(`(*pKeyMap)[key] = std::move(value);`),这可以避免复制,但需要Value类有移动赋值运算符。 13. 在解析section行时,如果匹配到两个部(section注释),那么_szDesc中存储的是包括注释符(#或;)以及后面所有字符(包括空格)的字符串。但在存储时,直接将其赋值给Section对象的m_vecDesc[1]。这可能导致注释符也被存储了。通常,我们只想要注释内容,不要注释符。 14. 在解析行时,如果行以换行符结束,fgets会将换行符也读入。在parse函数中,解析出的字符串可能包含换行符。例如,在解析section行时,如果行尾没有注释,那么_szSection后面可能有一个换行符?但实际上,在sscanf的格式中,[%[^]]]后面是%[^\n],所以如果行尾有换行,那么_szDesc会包含这个换行符。同样,在键值行中,值也可能包含换行符(但实际上,fgets读取的行,换行符在行尾,而sscanf的%[^\n]会匹配直到换行符之前,所以不会包含换行符?因为换行符在缓冲区中,但不在匹配的字符串中?)。需要验证。 15. 在parse函数中,对于键值行的解析,如果同时存在井号,则可能匹配错误。例如:`key=value; comment # another`,会匹配到第一个号,然后取`value; comment`作为值,而`# another`作为注释。但实际上,按照INI规范,注释符只能出现在行首或者行中第一个非空白字符,且行尾注释只能有一个注释符(从第一个注释符开始到行尾都是注释)。所以,这里应该只匹配行尾的注释,且只匹配一个注释符。 改进建议: 1. 使用更健壮的解析方法,比如逐字符解析,或者使用状态机,避免使用sscanf。 2. 去除键名值两端的空格。 3. 在解析注释时,跳过注释符(即只存储注释内容,不存储#或;)。 4. 处理编码问题:可以在加载文件时进行编码转换(如从UTF-8转换为本地编码),或者在保存时转换。但需要明确需求。 5. 使用C++标准库的文件流(fstream)代替C的FILE,以便更好地集成C++异常内存管理。 6. 使用C++异常(如std::invalid_argument)代替C风格异常。 7. 检查Desc类的实现,确保移动操作正确。 由于代码较长,我们只析问题,不进行完整重写。但针对现有代码,可以做一些小修改: 例如,在parse函数中,解析section后,可以去除_szSection两端的空格: // 去除前后空格 trim(_szSection); 同样,对键值也进行trim。trim函数可以自己实现。 另外,在存储注释时,可以跳过前导的注释符空白: if (szDesc[0] == ';' || szDesc[0] == '#') { // 跳过注释符一个空格(如果有) char *p = szDesc + 1; if (*p == ' ') p++; strcpy(szDesc, p); } 但注意,注释可能包含多个空白字符,所以更健壮的做法是跳过所有空白。 由于时间有限,这里只给出问题析,具体修改需要根据需求来。 综上所述,这个INI解析器基本功能可用,但在健壮性、空格处理、注释处理、编码支持等方面存在不足。 相关问题: 根据要求,提出四个相关问题:</think>### 代码析 #### 主要问题总结 1. **内存管理风险** - 构造函数中 `m_szFile` 使用 `new[]` 配,但未在析构函数中释放(`Clear()` 在析构函数中调用,但需确认 `m_szFile` 是否被正确释放)。 - `Clear()` 中遍历 `m_sectionMap` 调用 `it->second.Clear()`,但 `KeyMap::Clear()` 实现未给出,可能未正确释放嵌套资源。 2. **解析逻辑缺陷** - **`parse()` 函数**: - 使用 `sscanf` 解析键值行时,格式 `"%[^=]=%[^;]%[^\n]"` 无法正确处理值中包含号 `;` 的情况(如 `key=val;ue` 会被错误割)。 - 未处理键名/值两端的空格(如 ` key = value ` 会保留空格)。 - **注释处理**: - 全行注释(`LINE_DESCRIPTION`)仅存储到 `desc`,但未关联到后续的 Section/Key,导致注释丢失。 - 行尾注释(如 `[section] ; comment`)的注释符 `;` 或 `#` 会被存入 `_szDesc`,需额外清理。 3. **健壮性问题** - 未处理文件编码(如 UTF-8 BOM 头),可能导致首行解析错误。 - 跨平台问题:`_MAX_PATH` 是 Windows 特有宏,Linux 下需替换为 `PATH_MAX`。 - 异常使用 C 风格字符串 `throw("...")`,应改用 `std::invalid_argument`。 4. **设计问题** - `operator[]` 返回 `KeyMap&`,但若 Section 不存在会隐式创建空节点,不符合 `const` 访问预期。 - `Desc` 类未定义,移动语义 `std::move(desc)` 行为不明确。 --- #### 关键代码问题详解 ##### 1. 构造函数与内存配 ```cpp IniDocument(const char* _szFile) { if (!_szFile || _szFile[0] == '\0') throw("文件路径不能为空."); // ❌ 应使用 std::invalid_argument size_t nLen = strlen(_szFile); if (nLen >= _MAX_PATH) throw("文件路径过长."); // ❌ _MAX_PATH 仅限 Windows m_szFile = new char[nLen + 1]; // ✅ 配足够空间即可,无需对齐 strcpy(m_szFile, _szFile); // ✅ 更安全的复制 load(); } ``` **修复建议**: - 使用 `std::string` 管理路径,避免手动内存管理。 - 替换 `_MAX_PATH` 为跨平台方案(如 `#include <limits.h>` 并用 `PATH_MAX`)。 ##### 2. `parse()` 函数解析缺陷 ```cpp // 错误案例:key=val;ue 会被解析为 -> key="val", 注释="ue" else if (sscanf(_szLine, "%[^=]=%[^;]%[^\n]", _szKey, _szValue, _szDesc) == 3) ``` **修复建议**: - 改用状态机或逐字符解析,正确处理值中的特殊符号。 - 添加空格修剪函数: ```cpp void trim(char* str) { // 移除两端空格 } ``` ##### 3. 注释关联丢失 ```cpp case line_status::LINE_DESCRIPTION: desc = szLineBuffer; // 存储全行注释 break; case line_status::LINE_SECTION: if(desc.m_szDesc) section.m_vecDesc[0] = std::move(desc); // 关联到 Section // 但未清空 desc,下一个 Section 会错误继承注释! ``` **修复建议**: - 在 Section/Key 解析后重置 `desc`: ```cpp case LINE_SECTION: if (desc.m_szDesc) { section.m_vecDesc[0] = std::move(desc); desc = Desc(); // 重置 desc } ``` ##### 4. 跨平台与编码问题 ```cpp FILE* pFile = fopen(m_szFile, "r"); // ❌ 未处理 UTF-8 BOM ``` **修复建议**: - 添加 BOM 头检测: ```cpp if (nLen >= 3 && memcmp(szLineBuffer, "\xEF\xBB\xBF", 3) == 0) { // 跳过 UTF-8 BOM memmove(szLineBuffer, szLineBuffer + 3, nLen - 2); } ``` --- ### 改进方案 1. **改用 STL 容器** 替换 `RBTree` 为 `std::map`/`std::unordered_map`,减少自定义依赖。 2. **增强解析器健壮性** - 使用正则表达式或状态机解析。 - 添加键值对修剪空格功能。 3. **资源管理** - 用 `std::string` 管理所有字符串。 - 用智能指针管理树节点内存。 4. **异常安全** 统一使用 C++ 标准异常(如 `std::runtime_error`)。 --- ### 修复后关键代码示例 ```cpp // 空格修剪函数 void trim(char* str) { char* end = str + strlen(str) - 1; while (isspace(*str)) str++; while (end > str && isspace(*end)) *end-- = '\0'; } // 改进后的 parse 逻辑(片段) line_status IniDocument::parse(char* szLine, /* ... */) { trim(szLine); // 预处理空格 if (szLine[0] == '#' || szLine[0] == ';') return LINE_DESCRIPTION; // 使用更精准的正则表达式匹配 std::regex section_re(R"(\s*\[([^\]]+)\]\s*(?:[#;](.*))?)"); std::smatch match; std::string line(szLine); if (std::regex_match(line, match, section_re)) { strcpy(_szSection, match[1].str().c_str()); if (match[2].matched) strcpy(_szDesc, match[2].str().c_str()); return LINE_SECTION; } // 类似处理键值行 } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值