C++学习笔记(一):中文字符的处理——批量读取和修改文件夹下文件名,以及wchar_t/wstring与char/string不得不说的故事

前几天在网上下载了一堆教程,但是名字是用中文数字命名的,在windows文件管理下无法按数字顺序进行排序,这让强迫症的我很不爽,所以就想写个程序批量修改一下。

作为C++小白的我,花了好长时间,终于大致搞明白了。因为路径名和文件名都涉及到中文字符,所以花了很长时间,走了很多弯路,于是想写篇博客记录一下心路历程。

以后可能会经常写博客来记录一些学习心得(希望能坚持下来),也方便以后查阅。

本篇文章主要介绍用C++读取和修改文件名的方法,提供将中文数字修改为两位阿拉伯数字的算法,并介绍C++中对于中文字符(串)的处理。

第一次写,如有错误不当之处,敬请批评指正。


概述

C++中使用_findfirst、_findnext、_findclose这几个函数来读取文件名,并将文件名等信息存储在_finddata_t结构体中。而修改文件名使用的是rename函数。
但是,由于路径名和文件名都涉及到中文字符,所以单纯使用这几个函数会出现乱码等问题,为了方便,统一使用宽字符wchar_t、宽字符串wstring,以及宽字符下的_wfindfirst、_wfindnext、_wfindclose函数。
这些数据类型和普通类型功能都是一样的,唯一不同的是底层的存储。为了方便,在读取和修改文件名部分使用普通类型来说明。

读取和修改文件名

先声明一个结构体_finddata_t,用来存储文件信息,但无需初始化。然后可以使用三个函数来读取文件名。注意需要包含头文件< io.h >

结构体_finddata_t定义如下:

struct _finddata64i32_t {
        unsigned    attrib;
        __time64_t  time_create;    /* -1 for FAT file systems */
        __time64_t  time_access;    /* -1 for FAT file systems */
        __time64_t  time_write;
        _fsize_t    size;
        char        name[260];
};

其中attrib指的是文件属性(attribute),有以下六种:

_A_ARCH(存档)
_A_HIDDEN(隐藏)
_A_NORMAL(正常)
_A_RDONLY(只读)
_A_SUBDIR(文件夹)
_A_SYSTEM(系统)

三个函数的使用方法如下:

intptr_t _findfirst( char *filespec, struct _finddata_t *fileinfo );
返回值:
如果查找成功的话,将返回一个intptr_t型的唯一的查找用的句柄,实际上相当于int。这个句柄将在_findnext函数中被使用。若失败,则返回-1。
参数:
filespec:标明文件的字符串,可支持通配符。比如:*.c,则表示当前文件夹下的所有后缀为C的文件。 这个字符串要包括完整的路径名。
fileinfo :这里就是用来存放文件信息的结构体的指针。已经说过,这个结构体必须在调用此函数前声明。
函数成功后,函数会把找到的文件的信息放入这个结构体中。

注意:如果将返回值赋给long型变量,有时可能会出现编译不通过的问题,因为这个函数的返回值实际上是intptr_t而不是long,可能会造成类型不兼容。这时候把定义的long型变量改为intptr_t即可。

int _findnext( intptr_t handle, struct _finddata_t *fileinfo );
返回值:
若成功返回0,否则返回-1。
参数:
handle:即由_findfirst函数返回回来的句柄。
fileinfo:文件信息结构体的指针。找到文件后,函数将该文件信息放入此结构体中。

int _findclose( long handle );
返回值:成功返回0,失败返回-1。
参数: handle :_findfirst函数返回回来的句柄。
读取完毕后,用该函数关闭文件结束查找。

为了解决中文路径的问题,实际使用的是兼容中文字符的_wfinddata_t结构体,以及_wfindfirst、_wfindnext、_wfindclose三个函数,与不带w的几个函数功能完全相同,只是路径名参数也必须使用宽字符以兼容。

而修改文件名,使用的是rename函数,使用方法如下:
int rename( char const* OldFileName, char const* NewFileName );
返回值:
成功返回0;失败(如遇到同一文件夹下文件重名)返回-1。
参数:
OldFileName是原文件名字符串的指针,NewFileName是新文件名字符串的指针。

注意:此函数不支持宽字符类型的重载,因此中文在使用时还要转换为普通字符串。

wchar_t与wstring

一开始在网上找过一份批量改名的源码,虽然是用C++写的但是C风格特别浓重(分配内存用的是malloc/free且大量使用C风格字符串),这让我一个没有系统学习过C的人看着比较难受,所以又大改了一下,基本全部改为了使用string类(实际上是wstring类)的方法。

注意使用string类、wstring类需要包含头文件< string >。

C/C++中对于字符串处理的常用函数有:

char *strcat( char *str1, const char *str2 );
功能:函数将字符串str2 连接到str1的末端,并返回指针str1。

char *strchr( const char *str, int ch );
功能:函数返回一个指向str 中ch 首次出现的位置,当没有在str 中找ch到返回NULL。

char *strcpy( char *to, const char *from );
功能:复制字符串from 中的字符到字符串to,包括空值结束符。返回值为指针to。

size_t strlen( char *str );
功能:函数返回字符串str 的长度( 即空值结束符之前字符数目)。

int strcmp( const char *str1, const char *str2 );
功能:比较字符串str1 and str2, 返回负值说明str1比str2短,返回正值说明str1比str2长,返回0说明str1与str2一样长。

而在C++的string类中,可以用重载的+、=实现字符串连接和复制的功能,且有常用的几个成员函数:

函数名称 功能
append() 在字符串的末尾添加文本
at() 按给定索引值返回字符
c_str() 将字符串以C字符数组的形式返回
substr() 返回某个子字符串
insert() 替换字符
erase() 删除字符
replace() 替换字符
length() 返回字符串的长度
size() 返回字符串中字符的数量

这里只列出了修改文件名可能用到的一些函数,string类包含的成员函数远不止这么多,具体可以查阅手册。

对于中文字符的处理其实十分简单,只需要把char改为wchar_t类型,string改为wstring类型即可,二者的功能几乎是完全一样的,只需要注意以下不同:

1、C语言是不支持宽字符类型的,C风格字符串的strcat、strcpy等函数在使用宽字符时,只需把str改为wcs,即函数名写成wcscat、wcscpy等即可。

2、若要在控制台输出宽字符/宽字符串,必须使用宽字节流对象wcin、wcout,并且要绑定为中文地区语言。
在使用前加入以下代码即可(一次即可):

//使用宽字节流对象,绑定为中文
    locale china("chs");//use china character
    wcin.imbue(china);//use locale object
    wcout.imbue(china);

3、对于string、wstring类各自来说,成员函数length()、size()以及C风格的strlen()功能是完全相同的,都是返回该字符串除结束符外的字符数量。但是对于string类(每个字符是char)来说,英文和数字是占1个字节,算作一个字符,汉字是占两个字节,算作两个字符。而对于wstring类(每个字符是wchar_t),无论中文、英文、数字,都是占两个字节,算作一个字符。

如以下代码:

string stra = "CPPstring";
    cout << stra << endl;
    cout << "测试string.size: " << stra.size() << endl;
    cout << "测试string.length: " << stra.length() << endl;
    cout << "测试strlen(string.c_str()): " << strlen(stra.c_str()) << endl;
    cout << endl;

    string strb = "我是一个字符串abc123";      //string中英文和数字视为一个字符,汉字视为两个字符
    cout << strb << endl;
    cout << "测试string.size: " << strb.size() << endl;
    cout << "测试string.length: " << strb.length() << endl;
    cout << "测试strlen(string.c_str()): " << strlen(strb.c_str()) << endl;
    cout << endl;

    wstring strc = L"我是一个宽字符串abc123";   //wstring中无论中英文,一个字视为一个字符
    wcout << strc << endl;
    cout << "测试wstring.size: " << strc.size() << endl;
    cout << "测试wstring.length: " << strc.length() << endl;
    cout << "测试wcslen(wstring.c_str()): " << wcslen(strc.c_str()) << endl;
    cout << endl;

在控制台输出结果为:
C++学习笔记(一):中文字符的处理——批量读取和修改文件夹下文件名,以及wchar_t/wstring与char/string不得不说的故事_第1张图片

这里顺带一提sizeof操作符。相比较strlen()等,sizeof更像一种特殊的编译预处理而非函数,因为它的值是在编译阶段就确定的。如果对一个字符串str使用sizeof,是计算其指针所占的字节数,而字符串本身所占的空间是在堆内存分配的。在vs2017下编译,sizeof(string)和sizeof(wstring)的值都是28(这一点对于不同的库可能有所不同),而sizeof(string.c_str())和sizeof(wstring.c_str())的值都是4。

更多关于char、wchar_t的不同,如二者在底层的编码方式,以及关于ASCII、Unicode等问题,可以参考这篇文章:
c++汉字字符处理

宽字符与普通类型的转换

由于rename函数不支持宽字节作为参数,所以还要考虑二者相互转换的问题。关于这部分这篇文章讲的很详细:
C/C++多字节与宽字符串的相互转换
此处限于篇幅不再赘述。

将中文数字转换为阿拉伯数字

最后提供将中文转换为阿拉伯数字的思路。由于我下载的文件全都是一百以内的编号,所以写的时候是全部转换为两位数字,而一位数如5命名为05,算法比较简单。具体的思路就是,先找到字符十,如果没有就依次寻找一~九,找到了看前后有无数字,然后决定十字是改为1还是直接删除。如果都没有,函数返回0。

完整代码如下,VS2017环境下编译:
#include "stdafx.h"
//"stdafx.h"中包含的头文件有

using namespace std;

bool ModifyNumber(wstring & wstr);  //中文数字替换为两位的阿拉伯数字字符串,成功返回1,未找到数字返回0
string ws2s(const wstring & ws);    //宽字符串转换为普通字符串

wchar_t ChsNum[11] = { L'零', L'一', L'二', L'三', L'四', L'五', L'六', L'七', L'八', L'九', L'十' };
wchar_t ArbNum[10] = { L'0', L'1', L'2', L'3', L'4', L'5', L'6', L'7', L'8', L'9' };

int main()
{
    //使用宽字节流对象,绑定为中文
    locale china("chs");//use china character
    wcin.imbue(china);//use locale object
    wcout.imbue(china);

    wstring dirpath = L"F:\\测试\\"; //注意宽字符或宽字符串在初始化时要加前缀L

    _wfinddata_t file;  //使用宽字节的_wfinddata_t对象而非_finddata_t
    long lf;    //是否遍历完毕的标志位

    wchar_t suffixs[] = L"*.txt";   //要寻找的文件类型后缀,也统一使用宽字符串
    vector fileNameList;   //文件夹下该类型文件的名字向量表
    wchar_t *p;
    int psize = dirpath.size() + 6; //后面要把后缀加上,为了防止数组越界需要多开一点空间,6个正好
    p = new wchar_t[psize];
    wcscpy(p, dirpath.c_str());

    //获取文件名,存入向量表
    if ((lf = _wfindfirst(wcscat(p, suffixs), &file)) == -1l)
    {
        cout << "文件没有找到!\n";
    }
    else
    {
        cout << "\n文件列表:\n";
        do {
            wcout << file.name << endl;
            wstring str(file.name);
            fileNameList.push_back(str);
            cout << endl;
        } while (_wfindnext(lf, &file) == 0);
    }
    _findclose(lf); //使用完毕后要关闭文件
    delete[] p;

    //遍历文件名向量表,并进行修改
    cout << "\n开始修改文件名:" << endl;
    for (vector::iterator iter = fileNameList.begin(); iter != fileNameList.end(); ++iter)
    {
        wstring oldName = dirpath + *iter;
        wstring newName = oldName;

        //找到需要修改处并修改
        bool foundNum = ModifyNumber(newName);
        cout << "foundNum=" << foundNum << endl;

        wcout << "oldName:" << oldName << endl;
        wcout << "newName:" << newName << endl;

        wcout << "oldName size = " << oldName.size() << endl;
        wcout << "newName size = " << newName.size() << endl;

        //为了使用rename函数还要先转换回普通字符串
        string str_oldName = ws2s(oldName);
        string str_newName = ws2s(newName);

        //进行重命名
        if (foundNum)
        {
            rename(str_oldName.c_str(), str_newName.c_str());
        }
        cout << endl;
    }
    system("pause");
    return 0;
}

/*中文数字替换为两位的阿拉伯数字字符串,成功返回1,未找到数字返回0*/
bool ModifyNumber(wstring & wstr)
{
    unsigned int locTen = wstr.find(ChsNum[10]);
    if (locTen == wstring::npos)    //找不到字符十,1~9
    {
        int i = 1;
        unsigned int locUnit;
        for (i = 1; i <= 9; ++i)
        {
            locUnit = wstr.find(ChsNum[i]);
            if (locUnit != wstring::npos)
            {
                wstr.replace(locUnit, 1, 1, ArbNum[i]);
                wstr.insert(locUnit, 1, ArbNum[0]);
                break;
            }
        }
        if (locUnit == wstring::npos && i == 10)    //未找到数字
            return 0;
    }
    else    //能找到字符十,组合前后的数
    {
        wchar_t beforeten = L'零';
        int _isfrom10to20 = 1;
        if (locTen > 0) //考虑到可能字符串开头就是十
        {
            beforeten = wstr.at(locTen - 1);
            for (int count = 1; count <= 9; count++)
            {
                if (beforeten == ChsNum[count])
                {
                    _isfrom10to20 = 0;
                    break;
                }
            }
        }

        if (locTen == 0 || _isfrom10to20 == 1)
        {
            int i = 1;
            wstr.replace(locTen, 1, 1, ArbNum[1]);
            wchar_t afterten = wstr.at(locTen + 1);
            for (i = 1; i <= 9; ++i)
            {
                if (afterten == ChsNum[i])  //11~19
                {
                    wstr.replace(locTen + 1, 1, 1, ArbNum[i]);
                    break;
                }
            }
            if (i == 10)    //10
                wstr.insert(locTen + 1, 1, ArbNum[0]);
        }
        else   //21-99
        {
            int i = 1, j = 1;
            wchar_t afterten = wstr.at(locTen + 1);
            for (i = 1; i <= 9; ++i)
            {
                if (beforeten == ChsNum[i])
                {
                    wstr.replace(locTen - 1, 1, 1, ArbNum[i]);
                    break;
                }
            }
            for (j = 1; j <= 9; ++j)
            {
                if (afterten == ChsNum[j])  //非整十
                {
                    wstr.replace(locTen + 1, 1, 1, ArbNum[j]);
                    wstr.erase(locTen, 1);
                    break;
                }
            }
            if (j == 10)    //整十
            {
                wstr.replace(locTen, 1, 1, ArbNum[0]);
            }
        }
    }
    return 1;
}

/*宽字符串转换为普通字符串*/
string ws2s(const wstring & ws)
{
    string curLocale = setlocale(LC_ALL, NULL);     //curLocale="C"
    setlocale(LC_ALL, "chs");
    const wchar_t* wcs = ws.c_str();
    size_t dByteNum = sizeof(wchar_t)*ws.size() + 1;
    cout << "ws.size():" << ws.size() << endl;

    char* dest = new char[dByteNum];
    wcstombs_s(NULL, dest, dByteNum, wcs, _TRUNCATE);
    string result = dest;
    delete[] dest;
    setlocale(LC_ALL, curLocale.c_str());
    return result;
}

关于更完善的数字转换算法,可以参考这个代码,不过是用Java写的:
java实现中文数字与阿拉伯数字互相转换

参考资料:

https://blog.csdn.net/xiexu911/article/details/79990774
https://blog.csdn.net/orz_3399/article/details/53415987
https://blog.csdn.net/k346k346/article/details/50082705
https://blog.csdn.net/rentian1/article/details/78498975

你可能感兴趣的:(C/C++)