Windows 程序设计(3) 关于字符串
阅读原文时间:2023年07月10日阅读:1

1.1 宽窄字节简介:

C语言/C++语言,使用的字符串指针就是 char* 类型,C++中的字符串是 string,内部也是对 char* 的封装

窄字节

其实最早的系统都是窄字节的,也就是我们很常用的 char 因为都是英文的,英文本身就26个字母,再加上其他的一些标点符号之类的,char 也能表示的下,无符号的 char 最多能表示 255个字符,所以足够用了!

操作系统的国际化,比如:Windows 系统不仅有英文的,也有中文的,韩文的,日文的,所以原来用一个 char 来表示一个英文字符的方式已经无法表达中文的一个汉字了。汉字是很多的,好几万个,单纯的 char 的取值范围已经无法表达的下了。这时候有大牛就想到了,既然用一个char表示不下,那么就用2个char来表示一个汉字,这样就可以解决了,所以窄字节的表示方法就是数字、字母之类的仍然用一个char来表示,一个汉子或者全角字符使用2个 char 来表示。

没错,这样可以解决大多数问题,在中文的系统上能正常的显示中文,在日文的系统上也能正常的显示日文,但是如果把一个在中文系统上写的软件,界面上带有汉字的程序拿到一个日文的Windows操作系统上就会有问题了,乱码了,汉字无法正常显示,同理,把一个界面上带有日文或者韩文的软件拿到中文的系统上也显示乱码!

宽字节

为了解决这个国际化的问题,微软在Windows操作系统中引入了宽字节的功能,即:Unicode,Unicode中规定任意一个字符都占用两个字节的存储空间,即2个char,不管是数字或者字母,还是一个汉字 都占用2个字节。用两个char难免不方面,所以微软直接使用一个新的类型:wchar_t,大家看起来比较陌生,不过他的原型实际上就是 unsigned short,这个大家比较熟悉吧,占用2个字节的存储空间。

所以,微软就是利用Unicode编码来解决这个国际化的问题

1.2 操作系统及VS编译器对宽窄字节的编码支持:

1.2.1 Windows操作系统提供了两种类型的 API 函数

例如 MessageBox 函数,其实 MessageBox 他只是一个宏,他对应的两个版本的函数分别为:MessageBoxA 和 MessageBoxW

VS编译器中使用的时候系统会根据是否定义了_UNICODE 宏来进行判断当前工程使用的是宽字节的Unicode编码还是窄字节编码,根据这个来决定该使用哪个版本的函数

如果你的工程没有定义 _UNICODE 宏,也就是非Unicode编码,那么就使用窄字节的 MessageBoxA,

如果定义了,那么就使用宽字节的 MessageBoxW

查看DLL中的导出函数可以使用 depends 这个工具来查看!

备注:Windows 2000 及其以后的 系统都是使用Unicode从头进行开发的,如果调用任何一个Windows API 函数并给它传递一个 ANSI 字符串,那么系统首先要将字符串转换成Unicode,然后将Unicode字符串传递给操作系统。如果希望函数返回ANSI字符串,系统就会先将Unicode字符串转换成ANSI字符串,然后将结果返回给你的应用程序。进行这些字符串的转换需要占用系统的时间和内存。通过从头开始用Unicode来开发应用程序,就能够使你的应用程序更加高效的运行!

1.2.2 编译器对宽窄字节的支持:

VC++ 6.0 默认为窄字节编码

vs2005、vs2008、vs2010、vs2012、vs2013、vs2015、vs2017 等默认都是Unicode编码,当然可以进行工程的设置从而进行编码的转换

备注:从vs2013开始,如果要让工程从默认的Unicode编码转到窄字节编码,需要安装vs2013的多字节补丁才行

具体在工程的属性对话框中的如下位置可更改编码类型:

1.3 宽窄字符串的优缺点:

宽字节也有缺点,并非一味的用Unicode宽字节。

因为宽字节的占用空间比窄字节多了一倍,所以如果是单纯在本机的话还好,如果是进行字符串的网络传输,那么传输量就会是窄字节的二倍

一般来说只是涉及到界面,或者是跟字符串操作相关的建议大家使用宽字节,其他地方还是可以用窄字节。

微软也为我们提供好了相应的 API 函数,可用于宽字节和窄字节的转换

1.4 复杂的宽窄字节数据类型:

  • 窄字节

C/C++

char

char*

const char*

微软

CHAR

PCHAR、PSTR、LPST

LPCSTR

  • Unicode 宽字节:

C/C++

wchar_t

wchar_t*

const wchar_t*

微软

WCHAR

PWCHAR、PWSTR、LPWSTR

LPCWSTR

  • T 通用类型:

TCHAR

TCHAR * 、PTCHAR、PTSTR、LPTSTR

LPCTSTR

其中:P代表指针的意思,STR代表字符串的意思,L是长指针的意思,在WIN32平台下可以忽略,C代表const常量的意思,W代表wide宽字节的意思,T大家可以理解为通用类型的意思

可以根据工程中是否定义_UNICODE 宏,来判断当前工程的编码类型是宽字节还是窄字节,之后分别定义成不同的类型,比如:TCHAR 类型,如果工程中定义了_UNICODE 宏,那么就表明工程是宽字节编码,他最终就被定义成 wchar_t 类型,如果工程中没有定义_UNICODE 宏,就表明工程当前是窄字节编码,那么 TCHAR 被最终定义成 char 类型。

其方便性就是修改了工程的编码格式之后不用修改代码,建议编写程序的时候使用通用类型!

参考:https://www.cctry.com/thread-297534-1-1.html

2.1 宽窄字符串类型指针的定义:

  • 窄字节:char *p_str = "hello";
  • Unicode宽字节:wchar_t *p_wstr = L"hello";
  • 通用类型:TCHAR *p_tstr = _T("hello"); 或者 TCHAR *p_tstr= _TEXT("hello");
  • 动态申请内存:TCHAR *pszBuf = new TCHAR[100];

其中,_TEXT 和 _T 是一样的,定义如下:

define _T(x) __T(x)

define _TEXT(x) __T(x)

__T 的最终定义:
#ifdef _UNICODE
#define __T(x)    L##x
#else
#define __T(x)    x
#endif

其中,##为连接的意思。

2.2 常用的宽窄字节字符串处理函数:

字符串长度:

  • Ansi:strlen(char *str);

  • Unicode:wcslen(wchar_t *str);

  • 通用函数:_tcslen(TCHAR *str);

  • Ansi:int atoi(const char *str);

  • Unicode:int _wtoi(const wchar_t *str);

  • 通用函数:_tstoi(const TCHAR *str);

字符串拷贝:

  • Ansi:strcpy(char *strDestination, const char *strSource);
  • Unicode:wcscpy(wchar_t *strDestination, const wchar_t *strSource);
  • 通用函数:_tcscpy(TCHAR *strDestination, const TCHAR *strSource);

以上函数不安全,在vs2003等以上版本的编译器中会有warnning警告提示,以下为安全函数(VC++6.0不支持):

  • Ansi:strcpy_s(char *strDestination, size_t numberOfElements, const char *strSource);
  • Unicode:wcscpy_s(wchar_t *strDestination, size_t numberOfElements, const wchar_t *strSource);
  • 通用函数:_tcscpy_s(TCHAR *strDestination, size_t numberOfElements, const TCHAR *strSource);

numberOfElements

Size of the destination string buffer. 目的缓冲区的大小,以字节为单位,不是字符

size_t unsigned integer,在MSDN中的解释:Result of sizeof operator,也就是说 size_t 是 unsigned integer 即无符号整数。那为什么会有size_t这个类型呢?

因为不同平台的操作系统(32/64)中 int/long 等类型所占的字节并不一样,而 size_t 在不同的平台下有不同的定义。有点类似于TCHAR类型:

#ifndef  _SIZE_T_DEFINED
#ifdef   _WIN64
 typedef  unsigned  __int64      size_t;  //8个字节,64位
#else
 typedef  _W64  unsigned  int    size_t;  //4个字节,32位
#endif
#define  _SIZE_T_DEFINED
#endif

2.3 sizeof 求宽窄字节字符串的注意事项:

2.3.1 宽、窄字节求占用的字节数

窄字节

char* p_str = "hello";

strlen(p_str) + 1

宽字节

_wchar_t p_wstr = L"hello";_*

(wcslen(p_str) + 1) * sizeof(wchar_t)

通用类型

_TCHAR p_tstr = _T("hello");_*

(_tcslen(p_tstr) + 1) * sizeof(TCHAR)

定义:

TCHAR *pszBuf = new TCHAR[100];

字符串数组清空:

memset(pszBuf, 0, 100 * sizeof(TCHAR))

这样做是最安全的,不管当前的 TCHAR 是 char 也好,是 wchar_t 也好,都可以满足。

所有的字符串都用宽字节的Unicode来表示,比如网络发送的字符串就可以用窄字节的,当对方收到之后默认收到的是窄字节的,因为对方的程序可能用宽字节Unicode来写的界面,所以要显示的时候就要转换成Unicode宽字节的字符串。这样就涉及到宽窄字节的转换,类似的情况很常见

3.1使用微软提供的API函数来实现宽窄字节的转换:

WideCharToMultiByte 实现宽字节转换到窄字节

int WideCharToMultiByte(
  UINT CodePage,
  DWORD dwFlags,
  LPCWSTR lpWideCharStr,
  int cchWideChar,
  LPSTR lpMultiByteStr,
  int cbMultiByte,
  LPCSTR lpDefaultChar,
  LPBOOL lpUsedDefaultChar
);

MultiByteToWideChar 实现窄字节转换到宽字节

int MultiByteToWideChar(
  UINT                              CodePage,//针对窄字节
  DWORD                             dwFlags,
  _In_NLS_string_(cbMultiByte)LPCCH lpMultiByteStr,
  int                               cbMultiByte,
  LPWSTR                            lpWideCharStr,
  int                               cchWideChar
);

封装

char *cctryWideCharToAnsi(wchar_t *pWideChar)
{
 if (!pWideChar) return NULL;
 char *pszBuf = NULL;
 int needBytes = WideCharToMultiByte(CP_ACP, 0, pWideChar, -1, NULL, 0, NULL, NULL);
 if (needBytes > 0){
  pszBuf = new char[needBytes+1];
  ZeroMemory(pszBuf, (needBytes+1)*sizeof(char));
  WideCharToMultiByte(CP_ACP, 0, pWideChar, -1, pszBuf, needBytes, NULL, NULL);
 }

 return pszBuf;
}

wchar_t *cctryAnsiCharToWide(char *pChar)
{
 if (!pChar) return NULL;
 wchar_t *pszBuf = NULL;
 int needWChar = MultiByteToWideChar(CP_ACP, 0, pChar, -1, NULL, 0);
 if (needWChar > 0){
  pszBuf = new wchar_t[needWChar+1];
  ZeroMemory(pszBuf, (needWChar+1)*sizeof(wchar_t));
  MultiByteToWideChar(CP_ACP, 0, pChar, -1, pszBuf, needWChar);
 }

 return pszBuf;
}

4.1CString简介**:

MFC中对于CString字符串的使用可以说是很频繁的,CString类也可以说是MFC中针对字符串的一个封装类。其实CString归属于ATL,不过在MFC中一样使用,而且被重点使用。

4.2 CString类的方便之处

在 C语言中,对于字符串的操作有字符串数组,字符串指针之类的,比如: char* p_str = "hello"; char szbuf[100] = {'h', 'e', 'l', 'l', 'o'}; 再对字符串进行拷贝、连接、比较 的时候也要借助 strcpy、strcat、strcmp 之类的函数,而且还要考虑目标空间是否够用之列的,很是麻烦,一旦操作不好就会导致数组越界了,造成缓冲区溢出。

有的网友会说了,C语言的字符串处理确实比较麻烦,但是我可以使用C++ STL 中的 string 啊,这样要比C语言的方便很多吧,比如: string str = "hello"; str += " world."; bool is_equal = (str == "123"); 这样就不用担心字符串空间是否够用,连接、比较什么的也十分方便,直接 += 或者 == 就可以判断出来了。

其实 C++ STL 中的 string 还提供了很多其他的算法,例如: http://www.cplusplus.com/reference/string/string/ 虽然 C++ STL 中的 string 照比C语言中的字符串处理方便很多,但是这里我要说,跟成熟的字符串处理还是差很多,起码跟 CString来说就差了不少。

比如: trim 操作:去除掉首尾的不可见字符,比如回车,制表符,空格之类的;

reverse 操作:进行字符串的首尾颠倒反转;

upper操作:将字符串中的英文全变成大写字母;

lower操作:将字符串中的英文全变成小写字母;

right操作:直接返回字符串中结尾的指定字符;

span_including操作:返回包含指定字符串中任意一个字符的子串

span_excluding操作:返回不包含指定字符串中任意一个字符的子串

format:格式化字符串

replace:替换字符串中的指定字符

stricmp:不区分大小写进行字符串比较 等

4.3 CString 对于 TCHAR 的封装

针对当前编码,决定当前内部的指针是 char* 还是 wchar_t_,_

实际上CString 本身是个模板类,实际上有三种:

typedef ATL::CStringT< wchar_t, StrTraitMFC< wchar_t > > CStringW;
typedef ATL::CStringT< char, StrTraitMFC< char > > CStringA;
typedef ATL::CStringT< TCHAR, StrTraitMFC< TCHAR > > CString;

CStringA 内部使用的是 char_,_

CStringW 内部使用的是 wchar_t

CString内部使用的是 TCHAR_,_

所以 CString 本身会根据当前工程的编码决定使用 char* 还是 wchar_t*,这一点也比较方便。 我们可以先简单使用下 CString,之后改变下工程的编码,看看编译结果就知道了。

4.4 CString 类对于宽窄字节的转换

当前工程编码是宽字节Unicode的前提下,CString支持从char* 类型的窄字节字符串来构造和赋值。所以,是可以方便的从窄字节转换到宽字节。但是反之则没有办法实现。

4.5 使用 CString 类的优缺点:

优点:

①、使用方便,包含了很多实现好的操作,包括:trim、reverse、format、replace 等等;

②、无缝兼容MFC;

③、自适应当前工程编码,匹配宽窄字节;

④、能实现从窄字节到宽字节的自动转换工作;

缺点

①、在非MFC的工程中无法使用,例如:控制台类型的工程,或者Win32类型的API项目;

②、无法完全实现宽窄字节的任意转换;

③、非C++标准,无法跨平台,在除了VC以外的编译器无法使用,更无法在Linux等其他平台上使用。

4.6 解决方案:

扩展 C++ 标准模板库 STL 中的 string 字符串,使其既方便使用,又可以自由的转换宽窄字节,接下来的课程给大家讲解!

跨平台的考虑,支持 Windows系统,也要支持 Linux 系统,就必须使用 C++ 的东西了,不能使用 MFC 之类的,因为不跨平台。

但是C++ 标准模板库 STL 中的 stirng 还比较简陋,用起来确实很不方便,于是也是为了自己方便使用,就新写了个 string 扩展的工具类:string_util,实现对 string 功能的全面扩展。把我们平时常用的一些操作统统都封装进去,需要用的时候直接调用就可以了