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

欢迎访问我的个人博客:https://midoq.github.io/

前几天在网上下载了一堆教程,但是名字是用中文数字命名的,在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;

在控制台输出结果为:
测试宽字符串长度

这里顺带一提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"中包含的头文件有<iostream><string><vector><io.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<wstring> 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<wstring>::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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值