到 2015 2015年 7 月

30 卷数 7

C + +-在 Win32 API 边界使用 STL 字符串

Giovanni Dicanio |到 2015 2015年 7 月

Win32 API 公开使用纯 C 接口的几个功能。这意味着没有本机可交换文本在 Win32 API 边界的 c + + 字符串类。相反,使用原始的 C 样式字符指针。例如,Win32 SetWindowText 函数具有以下原型 (从相关的 MSDN 文档,在 bit.ly/1Fkb5lw):

BOOL WINAPI SetWindowText(
  HWND hWnd,
  LPCTSTR lpString
);

字符串参数被表示形式的 LPCTSTR,相当于 const TCHAR *。在 Unicode 版本中 (其中 Visual Studio 2005 以来默认值并应在现代 Windows c + + 应用程序中使用),TCHAR typedef 对应 wchar_t,所以 SetWindowText 的原型如下:

BOOL WINAPI SetWindowText(
  HWND hWnd,
  const wchar_t* lpString
);

所以,基本上,输入的字符串作为传递一个常数 (即,只读) wchar_t 的字符指针,该字符串指向的假设是以 NUL 结尾,在经典的纯 C 的风格。这是在 Win32 API 边界传递的输入的字符串参数的典型模式。

另一边,输出字符串在 Win32 API 边界通常表示使用两三件的信息:指向缓冲区的指针目的地,由调用方,并表示调用方提供的缓冲区的总大小的尺寸参数分配。一个例子是 GetWindowText 功能 (bit.ly/1bAMkpA):

int WINAPI GetWindowText(
  HWND hWnd,
  LPTSTR lpString,
  int nMaxCount
);

在这种情况下,与目标字符串缓冲区 ("输出"字符串参数) 相关的信息存储在最后两个参数:消息和拷贝。前者是一个指向目标字符串缓冲区 (使用导出 LPTSTR Win32 typedef,这将转换成 TCHAR *,表示或 wchar_t * 在 Unicode 中生成)。后者,拷贝,代表中 wchar_ts; 目标字符串缓冲区的总大小 注意此值包括终止 NUL 字符 (别忘了那些 C 样式字符串是以 NUL 结尾的字符数组)。

当然,特别是使用 c + + 而不纯 C 是非常富有成效的选项为发展用户模式 Windows 代码和 Windows 应用程序。事实上,在一般情况下,使用 c + + 提出了语义级别的代码和提高程序员的工作效率,没有对应用程序性能的负面影响。尤其是,使用方便的 c + + 字符串类是更好 (更容易、 更有成效,更容易导致 bug) 比处理原始的 C 像以 NUL 结尾的字符数组。

所以现在的问题就变成:什么样的 c + + 字符串类可以用于与 Win32 API 层,以本机方式公开一个纯 C 接口进行交互?

活动模板库 (ATL) / Microsoft 基础类 (MFC) 图书馆 CString ,ATL/MFC CString 类是一种选择。CString 很好结合 c + + 窗口框架简化了使用 c + + 的 Win32 编程的 ATL、 MFC 和 Windows 模板库 (WTL) 等。因此它意义使用 CString 来表示字符串在 c + + Windows 应用程序的 Win32 API 特定平台层,如果你使用这些框架。此外,CString 提供方便的 Windows 特定于平台的功能,比如能够加载字符串从资源,等等; 那些都是跨平台标准库像标准模板库 (STL) 只是不能提供,由定义的平台相关的功能。因此,例如,如果您需要设计并实现一个新的 c + + 类,从一些现有 ATL 或 MFC 类派生,肯定考虑使用 CString 表示字符串。

标准的 STL 字符串然而,有更好地在弥补 Windows 应用程序的自定义设计的 c + + 类接口使用标准字符串类的情况。例如,您可能想要抽象出来,尽快在 c + + 代码中,Win32 API 层宁愿而不是 Windows 特定类像 CString 在公共接口的自定义设计 c + + 类 STL 字符串类的使用。所以,让我们考虑一下存储在 STL 字符串类的文本的大小写。在这一点上,您需要将这些 STL 字符串传递跨越 Win32 API 边界 (这也可以使一个纯 C 的接口,如本文开头所述)。使用 ATL,WTL 和 MFC,框架将实现"胶水"代码之间的 Win32 C 接口层和 CString,隐藏引擎盖下,但这种便利与 STL 字符串不可用。

为了这篇文章,让我们假设字符串存储在 Unicode utf-16 格式,这默认的 Unicode 编码为 Windows Api。事实上,如果这些字符串使用另一种格式 (如 Unicode utf-8),那些可以转为 UTF-16 在 Win32 API 边界,满足上述要求的这篇文章。对于这些转换,Win32 MultiByteToWide­可以使用 Char 和 WideCharToMultiByte 函数:前者可以调用来从一个 Unicode UTF-8 编码 ("多字节") 的字符串转换为 Unicode utf-16 ("宽") 的字符串; 后者可以用于相反的转换。

在 Visual c + +,std::wstring 类型都是适合来表示 Unicode utf-16 字符串,因为其基本的性格类型是 wchar_t,在 Visual c + +,UTF-16 代码单元的确切大小具有 16 位的大小。请注意,在其他平台上,如海湾合作委员会 Linux wchar_t 是 32 位,因此在这些平台上的 std::wstring 将非常适合表示 Unicode UTF 32 编码的文本。若要删除此二义性,介绍了在 C + + 11 新标准字符串类型:std::u16string。这是 std::basic_string 类的专门化元素的类型 char16_t,就是 16 位字符为单位。

输入的字符串大小写

如果一个 Win32 API 期望的 PCWSTR (或 LPCWSTR 在旧术语),那就是,const 的 wchar_t * NUL 终止的 C 样式输入的字符串参数,只需调用 std::wstring::c_str 方法将是很好。事实上,此方法返回只读的 NUL 终止的 C 样式字符串的指针。

例如,若要设置窗口的标题栏的文本或使用存储在 std::wstring 中的内容控件的文本,SetWindowText Win32 API 可以调用像这样:

std::wstring text;
::SetWindowText(hWnd, text.c_str());

请注意,虽然 ATL/MFC CString 提供隐式转换为字符的原始 const 指针 (const TCHAR *,相当于现代的 Unicode 常量 wchar_t * 生成),STL 字符串不提供这样的隐式转换。相反,您必须使 STL 字符串 c_str 方法的显式调用。那里是隐式转换往往不是一件好事,因此 STL 字符串类的设计者选择了一种显式调用 c_str 方法的现代 c + + 中的共同理解。(你会发现相关的讨论上,缺乏现代 STL 智能指针在博客中的隐式转换 bit.ly/1d9AGT4.)

输出字符串大小写

把事情变得有点多复杂与输出字符串。通常的模式组成的第一次调用 Win32 API 输出字符串中获取目标缓冲区的大小。这可能包括或不包括终止 NUL; 特殊的文档 Win32 API 必须为此目的阅读。

然后,由调用方动态分配适当大小的缓冲区。该缓冲区的大小是在上一步中确定的大小。

以及最后,对 Win32 API 作出另一个调用,实际的字符串内容读入的调用方分配的缓冲区。

例如,若要检索控件的文本,GetWindowTextLength API 可以调用来获取中 wchar_ts,文本字符串的长度。(请注意,在这种情况下,返回的长度并 notinclude 终止 NUL)。

然后,可以使用该长度分配一个字符串缓冲区。这里的选项可以使用 std::vector < wchar_t > 若要管理字符串缓冲区中,例如:

// Get the length of the text string
// (Note: +1 to consider the terminating NUL)
const int bufferLength = ::GetWindowTextLength(hWnd) + 1;
// Allocate string buffer of proper size
std::vector<wchar_t> buffer(bufferLength);

请注意,这是比使用原料简单得叫"新 wchar_t [bufferLength]",因为这将需要正确释放缓冲区调用删除 [] (和到忘了做那会引起内存泄漏)。使用 std::vector 是只是更简单,即使使用向量有一个较小的开销相对于原始的新 [] 呼叫。事实上,在这种情况下 std::vector 的析构函数将自动删除已分配的缓冲区。

这也有助于建立异常安全的 c + + 代码:如果在代码中某个地方引发一个异常,std::vector 析构函数会自动调用。相反,与新的 [],其指针存储在原始拥有指针,动态分配的缓冲区会被泄露。

另一种选择,考虑作为替代 std::vector,可能在特定的情况下,std::unique_ptr std::unique_ptr,使用 < wchar_t [] >。此选项已自动销毁 (和异常安全性) 的 std::vector (感谢 std::unique_ptr 的析构函数),以及较少的系统开销比 std::vector,因为 std::unique_ptr 是非常微小的 c + + 包装原始的拥有指针周围。基本上,unique_ptr 是保护安全 RAII 边界内拥有指针。RAII (bit.ly/1AbSa6k) 是一种很常见的 c + + 编程风格。如果你熟悉它,只要想想 RAII 作为实现技术自动调用删除 [] 包装的指针上 — — 例如,在 unique_ptr 的析构函数 — — 释放关联的资源和防止内存泄漏 (和资源泄漏,一般)。

与 unique_ptr,代码可能看起来像这样:

// Allocate string buffer using std::unique_ptr
std::unique_ptr< wchar_t[] > buffer(new wchar_t[bufferLength]);

或者,使用 std::make_unique (可用以来 14 C + + 和 Visual Studio 2013 年实施):

auto buffer = std::make_unique< wchar_t[] >(bufferLength);

然后,一旦分配适当大小的缓冲区,并准备好使用,可以调用 GetWindowText API,则将指针传递给该字符串缓冲区。要获取一个指针,指向原始缓冲区由 std::vector,std::vector::data 方法的开始 (bit.ly/1I3ytEA) 可以使用,就像这样:

// Get the text of the specified control
::GetWindowText(hWnd, buffer.data(), bufferLength);

与 unique_ptr,可以调用其 get 方法:

// Get the text of the specified control (buffer is a std::unique_ptr)
::GetWindowText(hWnd, buffer.get(), bufferLength);

并且,最后,控件的文本可以深从临时缓冲区拷贝到一个 std::wstring 实例:

std::wstring text(buffer.data()); // When buffer is a std::vector<wchar_t>
std::wstring text(buffer.get()); // When buffer is a std::unique_ptr<wchar_t[]>

在前面的代码片段中,我使用 wstring,构造函数重载以恒定的原始 wchar_t 指针,指向一个 NUL 结尾的输入字符串。这只是正常工作,因为被调用的 Win32 API 将在由调用方提供的目标字符串缓冲区中插入一个 NUL 结束符。

作为一种轻微的优化,如果 (wchar_ts) 中的字符串的长度已知的 wstring 的构造函数重载以指针和一个字符串字符计数参数可供使用。在这种情况下,在调用站点,提供了字符串长度和 wstring 构造函数不需要 (通常与 o (n) 操作,类似于在 Visual c + + 实现中调用 wcslen) 把它找出来。

对于输出情况快捷方式:与 std::wstring 在地方工作

关于技术的分配一个临时 string 缓冲使用 std::vector (或 std::unique_ptr),然后将它复制到 std::wstring 多深,你可以采取一个快捷方式。

基本上,可以直接作为目标缓冲区使用 std::wstring 的实例传递给 Win32 Api。

事实上,std::wstring 具有一个大小调整方法,可以用来生成一个适当大小的字符串。请注意,在这种情况下,您不关心实际的初始内容的调整大小后的字符串,因为它将被调用 Win32 API 所覆盖。图 1 包含示例代码段演示如何读取字符串在使用 std::wstring 的地方。

我想澄清几点有关中的代码图 1

图 1 读字符串在地方使用 std::wstring

// Get the length of the text string
// (Note: +1 to consider the terminating NUL)
const int bufferLength = ::GetWindowTextLength(hWnd) + 1;
// Allocate string of proper size
std::wstring text;
text.resize(bufferLength);
// Get the text of the specified control
// Note that the address of the internal string buffer
// can be obtained with the &text[0] syntax
::GetWindowText(hWnd, &text[0], bufferLength);
// Resize down the string to avoid bogus double-NUL-terminated strings
text.resize(bufferLength - 1);

获取写访问内部字符串缓冲区第一,考虑 GetWindowText 电话:

::GetWindowText(hWnd, &text[0], bufferLength);

C + + 程序员可能会使用 std::wstring::data 方法来访问内部字符串内容,通过指针传递给 GetWindowText 调用。但 wstring::data 返回常量的指针,不允许内部字符串缓冲区被修改的内容。GetWindowText 预计写访问 wstring 的内容,因为该调用不会编译。所以,替代方法是使用 & 文本 [0] 语法以获得要作为输出传递的内部字符串缓冲区的开始的地址 (也就是说,可修改) 到所需的 Win32 API 的字符串。

与以往的方法相比,这种技术是更有效是有没有临时的 std::vector,第一次分配,然后深复制到 std::wstring,并最后,丢弃的缓冲区。事实上,在这种情况下,代码只是运行在一个 std::wstring 实例的地方。

避免假 Double-NUL-Terminated 字符串 中的代码的最后一行的注意 图 1:

// Resize down the string to avoid bogus double-NUL-terminated strings
text.resize(bufferLength - 1);

与初始 wstring::resize 调用 (text.resize(bufferLength); 没有"-1"修正),内部 wstring 缓冲区允许 GetWindowText Win32 API 涂鸦在其 NUL 终结器中分配足够的空间。然而,除了由 GetWindowText 写此 NUL 结束符,std::wstring 隐式提供另一个 NUL 结束符。因此,生成的字符串结束作为双精度-NUL-­结尾的字符串:NUL 终结者写的 GetWindowText 和由 wstring 自动添加 NUL 终结器。

若要修复此错误的双 NUL 结尾的字符串,wstring 实例的大小可以调整到印章 NUL 终结者添加的 Win32 API,留下只有 wstring 的 NUL 结束符。这是 text.resize(bufferLength-1) 调用的目的。

处理的争用条件

在结束之前这篇文章,它值得讨论如何处理潜在的争用条件,可能会出现一些 Api。例如,假设您有从 Windows 注册表中读取一些字符串值的代码。遵循的模式表明在上一节,c + + 程序员将首先调用 RegQuery­ValueEx 函数来获取字符串值的长度。然后在此基础上,将分配的字符串缓冲区,,最后 RegQueryValueEx 也会叫第二次来读入缓冲区在上一步中创建的实际字符串值。

在这种情况下可能出现争用条件是修改这两个 RegQueryValueEx 调用之间的字符串值的另一个进程。第一次调用所返回的字符串的长度可能是毫无意义的价值,无关写在注册表中的其他进程的新字符串值。所以,RegQueryValueEx 第二次调用会读错的大小分配的缓冲区中的新字符串。

要修复这个 bug,您可以使用一种编码模式类似于图 2

图 2 样本编码模式以处理潜在的争用条件中获取字符串

LONG error = ERROR_MORE_DATA;
std::unique_ptr< wchar_t[] > buffer;
DWORD bufferLength = /* Some initial reasonable length for the string buffer */;
while (error == ERROR_MORE_DATA)
{
  // Create a buffer with bufferLength size (measured in wchar_ts)
  buffer = std::make_unique<wchar_t[]>(bufferLength);
  // Call the desired Win32 API
  error = ::SomeApiCall(param1, param2, ..., buffer.get(), &bufferLength);
}
if (error != ERROR_SUCCESS)
{
  // Some error occurred
  // Handle it e.g. throwing an exception
}
// All right!
// Continue processing
// Build a std::wstring with the NUL-terminated text
// previously stored in the buffer
std::wstring text(buffer.get());

使用 while 循环在图 2 确保因为验证返回,每次一个新的缓冲区分配适当的 bufferLength 值,直至 API 调用成功 (返回写入) 或因其他原因而提供不够大小的缓冲区失败,在适当长度的缓冲区中读取的字符串。

请注意,代码段在图 2 是只是骨架代码示例; 其他 Win32 Api 可以使用不同的错误代码 ERROR_INSUFFICIENT_BUFFER 代码的调用方,例如,提供的缓冲区不足有关。

总结

虽然在 Win32 API 边界使用 CString — — 像 ATL/WTL 以及 MFC 框架的帮助 — — 隐藏的力学与 Win32 纯 C 接口层,当使用 STL 字符串 c + + 程序员的互操作必须注意某些细节。在这篇文章中,我讨论了一些编码模式的 Win32 纯 C 接口函数与 STL wstring 类的互操作。在输入的情况下,调用 wstring 的 c_str 方法是没事就去在 Win32 C 接口边界,简单恒定 (只读) 以 NUL 结尾的字符串的字符指针的形式传递输入的字符串。对于输出字符串,必须由调用方分配临时字符串缓冲区。这可以实现使用 std::vector STL 类或略少开销,STL std::unique_ptr 智能指针模板类。另一个选项是使用 wstring::resize 方法,作为为 Win32 API 函数的目标缓冲区分配一些房间内的字符串实例。在这种情况下,就必须指定足够的空间来允许被调用的 Win32 API,涂在其 NUL 结束符,然后调整到印章,下车离开只有 wstring NUL 结束符。最后,覆盖潜在的争用条件,并提出了一种编码模式来解决此争用条件的示例。


Giovanni Dicanio 是一个专业从事 c + + 和 Windows 操作系统、 Pluralsight 作者和 Visual c + + MVP 的计算机程序员。除了编程和课程创作,他还喜欢帮助别人在论坛上和各社区致力于 c + +,可以致电 giovanni.dicanio@gmail.com

衷心感谢以下技术专家对本文的审阅:David Cravey (GlobalSCAPE) 和 Stephan t。 Lavavej (微软)
David Cravey 是在 GlobalSCAPE 的企业架构师,带领几个 c + + 的用户群体,也是四个时间 Visual C + + MVP。
Stephan t。 Lavavej 是在微软高级开发人员。2007 年以来,他被同 Dinkumware 保持 Visual c + + 实现的 c + + 标准库。他还设计了几个 C + + 14 功能:make_unique 和透明运算符函子。