今天下午在寫程式的時候被一個奇怪的問題給卡住,我寫的程式去呼叫 CreateProcess() 但一直跳出下面的錯誤訊息:
原來問題出在我使用 Windows 的 CreateProcess() 這個 api 的方法錯誤,先來偷看一下 MSDN 的函式原型:
BOOL WINAPI CreateProcess( __in_opt LPCTSTR lpApplicationName, __inout_opt LPTSTR lpCommandLine, __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes, __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in BOOL bInheritHandles, __in DWORD dwCreationFlags, __in_opt LPVOID lpEnvironment, __in_opt LPCTSTR lpCurrentDirectory, __in LPSTARTUPINFO lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation );
其實問題的癥結很簡單,就出在第二個參數身上,為什麼 lpCommandLine 的型別是 LPTSTR 而不是 LPCTSTR 呢?理由很簡單,因為系統會去更改這個參數,所以 MSDN 也用了 __inout_opt 來修飾這個參數,因此我們不能傳一個 read only 的記憶體區塊到這個參數來。引用一下 MSDN 的說明:
The Unicode version of this function, CreateProcessW, can modify the contents of this string. Therefore, this parameter cannot be a pointer to read-only memory (such as a const variable or a literal string). If this parameter is a constant string, the function may cause an access violation.
有趣吧?!只有 Unicode 版本的 CreateProcess 會修改這個參數,所以下面的程式碼可以 work:
wchar_t cmd[ 100 ] = L"notepad D:\\mt.txt"; CreateProcessW( NULL, cmd, NULL, NULL, false, 0, NULL, NULL, &si, &pi ); CreateProcessA( NULL, "notepad D:\\mt.txt", NULL, NULL, false, 0, NULL, NULL, &si, &pi );
但下面的程式碼是不能 work:
CreateProcessW( NULL, L"notepad D:\\mt.txt", NULL, NULL, false, 0, NULL, NULL, &si, &pi );
是不是有點不 consistent 呢?
此外,眼尖的人可能會發現為什麼一個 LPTSTR (即TCHAR*) 型別可以接受一個型別為 const TCHAR array 呢?C++ standard 2.13.4 不是這樣說的嗎?
A string literal is a sequence of characters (as defined in 2.13.2) surrounded by double quotes, optionally beginning with the letter L, as in "..."or L"...". A string literal that does not begin with L is an ordinary string literal, also referred to as a narrow string literal. An ordinary string literal has type “array of n const char” and static storage duration (3.7), where n is the size of the string as defined below, and is initialized with the given characters. A string literal that begins with L, such as L"asdf", is a wide string literal. A wide string literal has type “array of n const wchar_t” and has static storage duration, where n is the size of the string as defined below, and is initialized with the given characters.
怎麼 VC++ 連個 warning 都不給呢?這是因為 C++ 為了相容於 C 所做出的讓步,來看一下 4.2 Array-to-pointer conversion 的描述:
A string literal (2.13.4) that is not a wide string literal can be converted to an rvalue of type “pointer to char”; a wide string literal can be converted to an rvalue of type “pointer to wchar_t”. In either case, the result is a pointer to the first element of the array. This conversion is considered only when there is an explicit appropriate pointer target type, and not when there is a general need to convert from an lvalue to an rvalue.
因此,比較好的習慣是:總是用 const char/wchar_t* 去指向一塊 literal string。Scott Meyer 不就說了嗎?
Use const whenever possible
: )
#include <iostream> #include <typeinfo> using namespace std; void foo( char* msg ) { cout << "[foo( char* msg )] " << msg << endl; } void foo( const char* msg ) { cout << "[foo( const char* msg )] " << msg << endl; } template<typename T> void printType( T* x ) { cout << "type of T: " << typeid( T ).name() << endl; } void badCall() { throw "Exception"; } int main() { foo( "Hello World" ); printType( "Hello World" ); try { badCall(); } catch ( const char* msg ) { cerr << "[const char* msg] " << msg << endl; } catch ( char* msg ) { cerr << "[char* msg] " << msg << endl; } return 0; }
我可沒說上面的 code 可以順利 compile 唷~
4 則留言:
以前只要用Windows API,看到一堆:LPXSTR的都當作是同一種東西,基本上就算有warning也不會理它。不過你說在CreateProcess時沒有出現warning,那整支程式在執行時是出現什麼問題?找不到D:\\mt.txt?
另外,文中有說填入const variable可能發生access violation,為何你的結論是Use const whenever possible?
我可能又漏看了什麼重點了,請周大師開示。 > <"
我不是大師,我也正在找姓周的大師!!
1. 重看了一下文章,果然是我表達不好,其實問題在:CreateProcess() 的 unicode 版本會去修改第二個參數,因此它會要求這個參數不能是 read only 的記憶體。如果我們應塞了一個 read only 的記憶體給它,會產生一個 access violation 的 runtime error (晚點我會把文章更新,放上一個執行期錯誤的 screenshot)
2. 因為 CreateProcess() 會嘗試去修改第二個參數,因此遵循正規的介面設計法則,第二個參數的型別是 LPTSTR 而不是 LPCTSTR 。
此時,因為 C/C++ 都號稱自己是 strong type 的 language ,怎麼當我傳入一個 const pointer 到 LPTSTR 時卻不會有 const to non-const 的錯誤訊息呢?
a. 根據 C++ standard 4.2 的描述,當我們 explicit 把 literal string 從 const char/wchar_t * 轉到 char/wchar_t * 是合法的,因此:
CreateProcess( NULL, L"notepad D:\\mt.txt", NULL, NULL, false, 0, NULL, NULL, &si, &pi ); 是段合法的程式碼!
b. 而若是今天程式變成:
const wchar_t* cmd = L"notepad D:\\mt.txt";
reateProcess( NULL, cmd, NULL, NULL, false, 0, NULL, NULL, &si, &pi );
這樣就會是不合法,因為這是個 implicit 的 conversion,standard 並不允許。
所以整篇文章,其實只是我從 CreateProcess() 介面設計不當(?!)的痛苦中突然想到 C++ 對於 literal string 為了相容所作的讓為了相容所作的讓步,一時興起去看 standard 的心得,對不起,我跳 tone 了 XD
而最後的例子說 compiling 不過,是因為 standard 針對 throw expression 的 literal string 的 conversion 又有另一套作法(猜測因為這邊就不必為了跟 C 相容?!),理論上這程式碼可能編譯不過是因為:
1. exception filtering 時, const char* 會吃掉 char*
2. 然後 throw expression 是不允許 explicit conversion (上文 a. 的例子),但是 VC++ 2005 是允許,g++ 是不允許的~
(希望這個週末能有空再整理一下這邊,不好意思,原本要分享點心得,沒想到更困惱你了!)
對了,忘了說,上面那個例子,也是為了是示範當我傳遞 literal string 當作 argument 時,function overloading 的解析,可以幫我們瞭解 literal string 的確是呼叫到 const char* 版本!
不過有趣的是,為什麼 typeid( T ).name() 卻是印出 char* 呢?這也是牽扯到 C++ standard 的規定,它會把 cv 修飾詞(const, volatile)都拿掉,因此透過 typeid() 去 get type 是不好的行為,其實這方面還有更多的討論,像是typeid().name()傳回的型別字串是長怎樣?大寫?小寫?有的沒的,都是 standard 沒有定義的,是 compiler-dependent ,或許,沒錯又是或許,哪天有機會可以整理一下這方面的 sharing …
ps. bbb 我 volitale 沒拼錯吧 XD
講解的十分清楚,而且看懂意思後,我反而瞭解你原文想舖的梗是什麼。我懂的東西太少所以你一次要解釋一堆來龍去脈,真是辛苦了。:D
張貼留言