通常,在VC++中获取命令行参数的有如下几种方式:
在控制台程序中:
C++运行时库通过入口函数main传递进来的参数int argc 和 char* argv[]。其中第二个参数将一个完整的命令行分割成指向各参数的字符串指针数组,数组中的每一个元素是一个指向参数的字符串指针。其中, argv[0] 指向应用程序名自身。
如果想获得像窗口形式的完整命令行参数CmdLine的话,可以调用API GetCommandLine() 获得。
在窗口程序中:
C++运行时库通过入口函数WinMain传递进来的参数LPTSTR pszCmdLine。pszCmdLine是一个完整的命令行参数,包含应用程序名自身。
如果想获得像控制台形式的被分割的命令行参数指针数组的话。可以使用如下代码获得:
需要注意的是, CommandLineToArgvW只有Unicode的版本,因此省略第二行的代码,而直接将入口函数中提供的参数lpCmdLine传给 CommandLineToArgvW 可能会存在问题,这取决于项目使用的字符集属性,幸好从 VS2005 开始,项目默认使用的字符集就是 Unicode!
然而,在实际使用命令行参数,其实并不像如上所述那么简单,还有几个不确定的因素会导致获得的命令行参数发生变化。
首先,给出三个测试程序, ConApp 是一个控制台程序,WinApp 是一个窗口程序,这两个程序在内部将获取命令行参数并进行显示,AppCaller是另外一个控制台程序,它将分几次调用 ConApp 和 WinApp ,并为它们传递不同的参数。
测试步骤为:
AppCaller创建ConApp 和 WinApp 子进程,并在控制台上输出传给 ConApp 和 WinApp 的参数,然后等待子进程结束。 ConApp 和 WinApp 显示命令行参数后返回, AppCaller再次调用 ConApp 和 WinAPP 并传递其他参数。
三个程序的代码为:
WinApp:
ConApp:
AppCaller:
如果单独测试 ConApp 和 WinApp ,不为它们传递任何参数,
WinApp 在 VS 调试环境中按 F5 调试启动 或者在资源管理器中双击文件启动,显示的消息框内容都为:
ConApp 在 VS 调试环境中按 F5 调试启动,输出的内容为:
GetCommandLineW="E:/test/CppConsole/ConApp/Debug/ConApp.exe" (2200 4500 3a00 5c
00 7400 6500 7300 7400 5c00 4300 7000 7000 4300 6f00 6e00 7300 6f00 6c00 6500 5c
00 4300 6f00 6e00 4100 7000 7000 5c00 4400 6500 6200 7500 6700 5c00 4300 6f00 6e
00 4100 7000 7000 2e00 6500 7800 6500 2200 2000 2000)
CommandLineToArgvW:
argc=1
argv:
[#0]E:/test/CppConsole/ConApp/Debug/ConApp.exe(4500 3a00 5c00 7400 6500 7300 740
0 5c00 4300 7000 7000 4300 6f00 6e00 7300 6f00 6c00 6500 5c00 4300 6f00 6e00 410
0 7000 7000 5c00 4400 6500 6200 7500 6700 5c00 4300 6f00 6e00 4100 7000 7000 2e0
0 6500 7800 6500)
如果在控制台中启动,输出的内容为:
GetCommandLineW=ConApp(4300 6f00 6e00 4100 7000 7000)
CommandLineToArgvW:
argc=1
argv:
[#0]ConApp(4300 6f00 6e00 4100 7000 7000)
首先,在窗口和控制台应用程序都未获得参数时,调用 GetCommandLine 和 CommandLineToArgvW 都会成功,返回的内容都是应用程序文件名(含扩展名),且 argc 等于1。
但是,通过比较 ConApp 在两种启动方式中,获得的内容是不同的。
在 VS 调试模式中启动,命令行参数包含文件的完整路径,在控制台中启动,不包含文件路径。
然后,启动 AppCaller 后, ConApp 输出的内容与 WinApp 输出的内容基本上是一致的,且 AppCaller 是在 VS 中启动还是在控制台中启动,都没有差异。并且当没有参数传递给 WinApp 和 ConApp 时,这两个子进程所获得的命令行参数是它们对应的文件名,且不含文件路径。
AppCaller 和 ConApp 在控制台中输出的内容为:
------------------------------------------
pszCmdLine -- NULL
GetCommandLineW="ConApp.exe"(2200 4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6
500 2200)
CommandLineToArgvW:
argc=1
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
------------------------------------------
------------------------------------------
pszCmdLine -- "ConApp.exe"
GetCommandLineW="ConApp.exe"(2200 4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6
500 2200)
CommandLineToArgvW:
argc=1
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
------------------------------------------
------------------------------------------
pszCmdLine -- ConApp.exe
GetCommandLineW=ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
CommandLineToArgvW:
argc=1
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
------------------------------------------
------------------------------------------
pszCmdLine -- 123
GetCommandLineW= 123(0900 3100 3200 3300)
CommandLineToArgvW:
argc=2
argv:
[#0]()
[#1]123(3100 3200 3300)
------------------------------------------
------------------------------------------
pszCmdLine -- 123
GetCommandLineW= 123(2000 3100 3200 3300)
CommandLineToArgvW:
argc=2
argv:
[#0]()
[#1]123(3100 3200 3300)
------------------------------------------
需要注意的问题是,比较 CASE 5 和 CASE 6 :
CASE 5 中, pszCmdLine 是 NULL ,这意味着没有参数传给 ConApp ,ConApp 调用 GetCommandLineW 获得的文件名被双引号扩住(Unicode值0x2200),但调用 CommandLineToArgvW 获得的 argv[0] ,对应的文件名是不包含双引号的。而且,如果不是通过CreateProcess启动 ConApp ,而是在控制台中直接启动 ConApp ,正如上面给出的结果, ConApp 调用 GetCommandLineW 是不包含双引号的。
在 CASE 6 中,pszCmdLine 为 "ConApp.exe" ,注意它是参数,不是程序名,完整的命令行类似于 ConApp "ConApp.exe" ,在参数中特意加了双引号,此时,ConApp 调用 GetCommandLineW 和 CommandLineToArgvW 后,获得的命令行参数与 CASE 5 中的内容完全一致, 即:不为 ConApp 传递参数 和为 ConApp 传递且仅传递一个参数 "ConApp" 时,两者在获取命令行方面没有差异!
因此,在写关于获取及解析命令行参数的代码时,需要考虑这种极为特殊的情况。事实上,似乎是没有办法来区分用户到底是用哪种方式调用了 ConApp 。也许,在控制台输入 ConApp "ConApp"的用户,本意是想调用ConApp,并传递一个参数 "ConApp" ,但如果在程序中只是简单解析成,该命令行不包含任何参数(因为无法区分 CASE 5 还是 CASE 6),并如传统做法那样显示一个关于程序的 Usage ,会造成用户的困惑。
因此,在开发程序时,如果程序允许用户传递参数给程序,则该程序应当设计成,参数含前导字符的形式: 如 ConApp /"ConApp" 或 ConAPP -"ConApp", 这样的程序才是正确的设计方法。同时,也能避免 CASE 8 和 CASE 9 将前导空格符(Unicode值0x2200)和TAB制表符(Unicode值0x0900)当做参数进行解析的问题。
以上讨论的结果是基于这样一种情况:调用 CreateProcess 时,为第一个参数传递文件名,为第二个参数传递子进程需要使用的参数。根据 MSDN 文档解释, 调用 CreateProcess 时,可以将第一个参数设为 NULL ,为第二个参数传递一个完整的命令行参数即可,即第二个参数的形式为 ConApp /123 。尽管这两种调用方式都能成功创建子进程,然而,对于要解析命令行的子进程而言,这两种不同的调用方式会直接影响到子进程对命令行参数的解析。原因是, 子进程在不同的调用方式中被创建后, 子进程调用 GetCommandLine 和 CommandLineToArgvW 所获得的内容是不一样的。以上已列出了其中一种调用方式,传递各种不同参数, 子进程获得命令行参数的结果。
接下去,需要修改 AppCaller 的代码,对 CreateProcess 使用两种调用方式,观察它对子进程的影响。
新的 AppCaller代码为:
输出结果为:
------------------------------------------
pszCmdLine -- NULL
GetCommandLineW="ConApp.exe"(2200 4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6
500 2200)
CommandLineToArgvW:
argc=1
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
------------------------------------------
------------------------------------------
pszCmdLine -- /123
GetCommandLineW=/123(2f00 3100 3200 3300)
CommandLineToArgvW:
argc=1
argv:
[#0]/123(2f00 3100 3200 3300)
------------------------------------------
------------------------------------------
pszCmdLine -- /123
GetCommandLineW= /123(0900 2f00 3100 3200 3300)
CommandLineToArgvW:
argc=2
argv:
[#0]()
[#1]/123(2f00 3100 3200 3300)
------------------------------------------
------------------------------------------
pszCmdLine -- /123
GetCommandLineW= /123(2000 2f00 3100 3200 3300)
CommandLineToArgvW:
argc=2
argv:
[#0]()
[#1]/123(2f00 3100 3200 3300)
------------------------------------------
------------------------------------------
pszCmdLine -- ConApp.exe
GetCommandLineW=ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
CommandLineToArgvW:
argc=1
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
------------------------------------------
------------------------------------------
pszCmdLine -- "ConApp.exe"
GetCommandLineW="ConApp.exe"(2200 4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6
500 2200)
CommandLineToArgvW:
argc=1
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
------------------------------------------
------------------------------------------
pszCmdLine -- ConApp.exe
GetCommandLineW=ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
CommandLineToArgvW:
argc=1
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
------------------------------------------
------------------------------------------
pszCmdLine -- ConApp.exe /123
GetCommandLineW=ConApp.exe /123(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 650
0 2000 2f00 3100 3200 3300)
CommandLineToArgvW:
argc=2
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
[#1]/123(2f00 3100 3200 3300)
------------------------------------------
------------------------------------------
pszCmdLine -- ConApp.exe /123
GetCommandLineW=ConApp.exe /123(4300 6f00 6e00 4100 7000 7000 2e00 6500 780
0 6500 2000 0900 2f00 3100 3200 3300)
CommandLineToArgvW:
argc=2
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
[#1]/123(2f00 3100 3200 3300)
------------------------------------------
------------------------------------------
pszCmdLine -- ConApp.exe /123
GetCommandLineW=ConApp.exe /123(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 65
00 2000 2000 2f00 3100 3200 3300)
CommandLineToArgvW:
argc=2
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
[#1]/123(2f00 3100 3200 3300)
------------------------------------------
------------------------------------------
pszCmdLine -- ConApp.exe ConApp.exe
GetCommandLineW=ConApp.exe ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 78
00 6500 2000 4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
CommandLineToArgvW:
argc=2
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
[#1]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
------------------------------------------
------------------------------------------
pszCmdLine -- ConApp.exe "ConApp.exe"
GetCommandLineW=ConApp.exe "ConApp.exe"(4300 6f00 6e00 4100 7000 7000 2e00 6500
7800 6500 2000 2200 4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500 2200)
CommandLineToArgvW:
argc=2
argv:
[#0]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
[#1]ConApp.exe(4300 6f00 6e00 4100 7000 7000 2e00 6500 7800 6500)
------------------------------------------
对于新的输出内容进行比对,例如对 CASE 1 和 CASE 7 进行比对可以发现:
CASE 1 调用 CreateProcess ,为第一个参数传递了子进程的文件名 ConApp.exe ,为第二个参数传递了1个参数 /123 ,子进程调用 GetCommandLine 获得的字符窜为 /123 ,调用 CommandLineToArgvW 后, argc 等于 1 , argv[0] 为 /123 ;
CASE 7 调用 CreateProcess ,为第一个参数传递 NULL ,为第二个参数传递 ConApp.exe /123 ,子进程调用 GetCommandLine 获得的字符串为 ConApp.exe /123 ,调用 CommandLineToArgvW 后, argc 等于 2 , argv[0] 为 ConApp.exe , argv[1] 为 /123 。
最后,总结来看无论是解析接受到的命令行参数,还是作为父进程创建子进程并需要为子进程传递参数时,都应该要小心,否则被调用的子进程会出现预期之外的结果。一般而言,开发解析命令行参数的程序时, 规定传递的参数要带参数前缀,如 / 、 - 等常用的参数前缀。在开发创建子进程的程序时,如果调用 CreateProcess ,将第一个参数设置为 NULL ,为第二个参数传递包含子进程文件名的完整命令行参数。需要小心的是第一个参数类型是一个 const 类型的,这通常没问题,第二个参数不是一个 const 类型,是一个 LPTSTR ,这要求只能自己构造一个命令行缓冲区传递给 CreateProcess ,传递一个字符串常量就会调用失败。还有就是第一个参数指定子进程的文件名不能省略文件扩展名,第二个参数指定子进程的文件名时,可以省略扩展名。