到 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 Studio2005年以來預設值並應在現代 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 (可從 C + + 14 和Visual Studio2013年中實現的):

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

衷心感謝以下技術專家對本文的審閱:DavidCravey (GlobalSCAPE) 和Stephant。 Lavavej (微軟)
DavidCravey 是在 GlobalSCAPE 的企業架構師,帶領幾個 c + + 使用者組,和四次Visual C++MVP。
StephanT。 Lavavej 是在微軟高級開發人員。2007 年以來,他被同 Dinkumware 保持 Visual c + + 實現的 c + + 標準庫。他還設計了幾個 C + + 14 功能:make_unique 和透明運算子函子。