学习目标
上一节我们了解了进程、入口函数和进程实例句柄等内容,在进入进程的命令行学习前,有一个全局变量初始化问题需要测试一波。本节的学习目标如下:
1.测试C/C++运行库启动函数初始化哪些全局变量
2.进程的命令行
3.进程的环境变量
4.进程的当前驱动器和目录
5.判断系统版本
6.创建进程(CreateProcess函数详解)
测试启动函数初始化哪些全局变量
我们知道C/C++运行库的启动函数会做一些事后再调用我们的入口函数,而入口函数的参数都是在调用前就初始化好了的。那么我就产生了一个疑问,全局变量随入口函数的不同(四种入口函数,分别是main、wmain、wWinMain、WinMain)都分别初始化了哪些全局变量?我做出了下面的测试:
(1)在CUI程序下测试Unicode字符集和多字节字符集两种情况的全局变量的初始化:
#include<windows.h>
#include<tchar.h>
#include<iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
//测试两次:第一次是在Unicode环境下,第二次是在多字节字符集环境下,注意输出的不同。
//测试_environ有无被初始化成有用的值
char** p1 = _environ;
if (p1 != NULL)
{
while (*p1)
{
cout << *p1 << endl;
p1++;
}
cout << "---------------------------上面的是_environ输出的值---------------------------------" << endl;
}
//测试_wenviron有无被初始化成有用的值
wchar_t** p2 = _wenviron;
if (p2 != NULL)
{
while (*p2)
{
wcout << *p2 << endl;
p2++;
}
cout << "--------------------------上面的是_wenviron输出的值--------------------------" << endl;
}
//测试__argv有无被初始化成有用的值
char** p3= __argv;
if (p3 != NULL)
{
while (*p3)
{
cout << *p3 << endl;
p3++;
}
cout << "-------------------------上面的是__argv输出的值----------------------------" << endl;
}
//测试__wargv有无被初始化成有用的值
wchar_t** p4 = __wargv;
if (p4 != NULL)
{
while (*p4)
{
wcout << *p4 << endl;
p4++;
}
cout << "-------------------------上面的是__wargv输出的值----------------------------" << endl;
}
system("pause");
return 0;
}
测试结果:输出结果太长不好截图,这里只给出总结,运行结果可以自己运行查看。如果你写的主函数是_tmain,那么其中_environ和_wenviron全局变量,在Unicode环境下,_environ和_wenviron全局变量都被初始化成有用的值了。而在多字节字符集下,_environ全局变量被初始化成有用的值,_wenviron全局变量才被置NULL。明显,和书中P69页表格的描述有差异
(2)在GUI程序下测试Unicode字符集和多字节字符集两种情况的全局变量的初始化:
1.很明显,下面的测试结果和在wmain函数测试情况相同。
2.可以看出,下面的测试结果和在main函数测试情况也相同。
(3)大总结:
对于书中P69页的全局变量初始化表的描述我产生了质疑。如果你写的主函数是_tmain,那么其中_environ和_wenviron全局变量,在Unicode环境下,_environ和_wenviron全局变量都被初始化成有用的值了。而在多字节字符集下,_environ全局变量被初始化成有用的值,_wenviron全局变量才被置NULL。简单来说就是无论_UNICODE有无被定义,_environ都会被初始化成有用的值,而_wenviron就受字符集影响,跟书产生了歧义。而wargv和argv就是符合书本的情况定义。如果你写的主函数是_tWinMain,那么其中_environ和_wenviron全局变量,在Unicode环境下,_environ和_wenviron全局变量都被初始化成有用的值了。而在多字节字符集下,_environ全局变量被初始化成有用的值,_wenviron全局变量才被置NULL。而wargv和argv就是符合书本的情况定义。注意,如果Windows编程,不使用_tmain和_tWinMain函数,而是使用main或wmain,那么上述的总结不一定成立,但由于兼顾两种字符集,建议以后写的入口函数就写_tmain和_tWinMain函数。
进程的命令行
(1)如果是运行CUI应用程序,在C/C++运行库启动函数执行时,就已经初始化好全局变量(包括命令行参数argc、argv或wargv。如果在Unicode字符集下,初始化了argc、argv;如果在多字符集下,初始化了argc、__wargv。)然后调用入口点函数_tmain,将参数argc、argv或wargv传入_tmain函数。
现在对_tmain函数的参数进行测试:
#include<windows.h>
#include<tchar.h>
#include<iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
/*
有两种方式可以输入命令行参数:
1.属性->配置属性->调试->命令参数:例如:wo ai ni
2.在可执行文件目录下打开命令行窗口(cmd),输入文件名+命令行参数:例如:ConsoleApplication9 wo ai ni
但有一点需要注意,就是字符集问题,当项目字符集是Unicode字符集,那么在C++利用wcout输出命令行。当项目字符集是多字节字符集,那么在C++利用cout输出命令行。
注意,不论通过以上两种方式输入的命令行参数都会在C/C++运行库启动函数中被初始化全局变量argc、__argv、__wargv。
所以传入_tmain函数的argv参数也是对应字符集编码的字符串。例如:如果在Unicode下,argv数组内的元素就是宽字符串,如果在多字节字符集下,argv数组内的元素就是ANSI字符串。
注意第一种方式和第二种方式在输出上的区别,第一种输出的第一个文件名字符串,这个字符串也包括路径。而第二种输出只有命令行参数,因为就算没有填写命令行参数也会输出文件名,那个文件名
只是起到运行这个程序的象征。
*/
for (int i = 0; i < argc; i++)
{
//cout只能输出ANSI字符和字符串,要想输出宽字符可以使用wcout。
wcout << argv[i] << endl;
}
system("pause");
return 0;
}
(2)如果是运行CUI应用程序,在C/C++运行库启动函数执行时,会调用Windows函数GetCommandLine来获取进程的完整命令行(文件名+命令行参数,其中文件名也就是绝对路径)然后启动函数进行忽略可执行文件的名称,包括路径,接着将指向命令行剩余部分的一个指针传给WinMain的pszCmdLine参数。下面给出函数的签名:
LPTSTR WINAPI GetCommandLine(void);
下面举个例子:
可以看出cmdLine包含绝对路径的文件名称和命令行参数。而pszCmdLine参数只有命令行参数,因为在启动函数处理中已经忽略了文件名了。
(3)我们也可以利用CommandLinetoArgvW函数将GetCommandLineW函数获取的完整命令行分解成单独的标记。
该函数原型如下:
LPWSTR* CommandLinetoArgvW(LPCWSTR,int*);
参数1是指向一个命令行字符串,通常利用GetCommandLineW获取。
参数2是获取命令行实参的个数。
返回的字符串数组所使用的内存,用LocalFree来释放!
以下是MSDN的示例代码:是在CUI程序下CommandLinetoArgvW函数的使用
#include<windows.h>
#include<tchar.h>
#include<iostream>
using namespace std;
int _tmain(int argc, _TCHAR* argv[])
{
LPWSTR *szArglist;//用于
int nArgs;
int i;
/*
CommandLineToArgvW函数只有Unicode版本的,所以参数1也必须使用Unicode版本的GetCommandLineW来获取完整的命令行
参数2是存储完整命令行中一共有多少个命令行参数,包括文件名参数。
CommandLineToArgvW函数返回的是一个Unicode字符串指针数组的地址。
这个函数将参数1完整命令行分解成单独的标记。
*/
LPTSTR cmdLine;
cmdLine = GetCommandLine();
printf("%ws\n", cmdLine);//这个是输出完整命令行
szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs);
if (NULL == szArglist)
{
wprintf(L"CommandLineToArgvW failed\n");
return 0;
}
else for (i = 0; i<nArgs; i++) printf("%d: %ws\n", i, szArglist[i]);//这个是输出分解后放到字符串数组中的内容
LocalFree(szArglist);
system("pause");
return 0;
}
在CUI程序下,入口点函数的argv函数就已经分解好了命令行参数,其实这个函数更大的用处是在GUI程序中,例如下面代码的使用:
#include<windows.h>
#include<tchar.h>
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, PTSTR pszCmdLine, int nCmdShow)
{
LPWSTR *szArglist;//用于
int nArgs;
LPTSTR cmdLine;
cmdLine = GetCommandLineW();
szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs);
LocalFree(szArglist);
return 0;
}
测试结果如下:
进程的环境变量
在我们熟知的Windows系统里,一直有环境变量这一说,我们都还知道环境变量可在Windows界面里的高级系统设置里的环境变量中获取或设置。但其实,环境变量真正是存储在注册表里的,每个Windows系统的注册表编辑器都在C:\Windows\regedit.exe,我们知道在可视化界面下(高级系统设置中打开)有两种环境变量,分别是系统变量和用户变量。而系统变量和用户变量分别在注册表编辑器下的两个路径(系统变量路径:HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Session Manager\Environment;用户变量路径:HKEY_CURRENT_USER\Environment)。下面放个注册表编辑器的示意图:
好了,言归正传。其实每个进程被创建后都会有一个与它关联的环境块,也就是在进程地址空间内分配的一块内存,内存块包含的字符串大概长这样:
=::=::\ ...
VarName1=VarValue1\0
VarName2=VarValue2\0
VarName3=VarValue3\0
VarNameX=VarValueX\0
\0
我们要注意的是等号左边的VarName1、VarName2等都是环境变量的名称,而等号右边的VarValue1、VarValue2等都是环境变量的值。还有一个更重要的一点就是每行环境变量的赋值最后都有个‘\0’,这是字符串结束符,后边GetEnvironmentStrings函数遍历完整的环境变量字符串时有用。
我们有两种方式来获取完整的环境块,第一种方式是调用GetEnvironmentStrings函数获取完整的环境变量(还有GetEnvironmentVariable函数获取的是单个指定环境变量名的值,下面会有使用案例)得到的完整环境块的格式和前面描述的一样;第二种方式是CUI程序专用的,就是通过入口函数所接收的TCHAR *envp[]参数来实现。不同于GetEnvironmentStrings返回的值,GetEnvironmentStrings返回的是完整的环境块,而envp是一个字符串指针数组,每个指针都指向一个不同的环境变量(其定义采用常规的“名称=值”的格式),在数组最后一个元素是一个NULL指针,代表这是数组的末尾,那么我们就可以通过这个NULL指针作为遍历的终止处,我们需要注意的是以等号开头的那些无效字符串在我们接收到envp之前就已经被移除了,所以不必进行处理只要获取数组元素即可。
(1)下面先讲GetEnvironmentStrings函数的使用案例:
这里先放上等会要用到的两个函数的函数签名。
1.GetEnvironmentStrings函数用于获取所有环境变量字符串:
LPTCH WINAPI GetEnvironmentStrings(void);
返回值:成功时,返回指向保存环境变量的缓冲区;失败时,返回值为NULL。
2.FreeEnvironmentStrings函数用来释放由GetEnvironmentStrings返回的内存块:
BOOL WINAPI FreeEnvironmentStrings(
__in LPTCH lpszEnvironmentBlock
);
返回值:成功时,返回非零值;失败时,返回零值,可调用GetLastError()查看进一步错误消息。
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include<strsafe.h>
int _tmain()
{
LPTSTR lpszVariable;
LPTCH lpvEnv;//LPTCH就是WCHAR *数据类型,指向宽字符的指针变量
size_t iTarget;
//调用GetEnvironmentStrings函数获取完整的环境变量内存块,并让lpvEnv指向这个内存块
lpvEnv = GetEnvironmentStrings();
//如果获取的环境块为空,则该函数调用失败,并获取错误代码
if (lpvEnv == NULL)
{
_tprintf(TEXT("GetEnvironmentStrings failed(%d)\n"), GetLastError());
return 0;
}
//lpvEnv指向的环境变量字符串是以NULL分隔的,即‘\0‘分隔,可以回去看前面我展示的环境字符串的大概格式。而字符串最后是以NULL结尾的
lpszVariable = (LPTSTR)lpvEnv;
while (*lpszVariable)
{
_tprintf(TEXT("%s\n"), lpszVariable);
StringCchLength(lpszVariable, 1000, &iTarget);//PATH的值太长,我设1000为最大允许字符数
lpszVariable += iTarget + 1;//移动指针,访问下一环境变量的值
}
//如果GetEnvironmentStrings函数返回的内存块不用了,记得要释放掉
FreeEnvironmentStrings(lpvEnv);
system("pause");
return 1;
}
运行结果如下:
(2)下面是GetEnvironmentVariable函数的使用案例:
这里先放上GetEnvironmentVariable函数签名。
1.GetEnvironmentVariable函数用于获取指定的环境变量:
DWORD WINAPI GetEnvironmentVariable(
__in_opt LPCTSTR lpName, //环境变量名
__out_opt LPTSTR lpBuffer, //指向保存环境变量值的缓冲区
__in DWORD nSize //缓冲区大小(字符数)
);
返回值:成功时,返回真实的环境变量值大小,不包括null结束符;如果lpBuffer大小不足,则返回值是实际所需的字符数大小,lpBuffer内容就未被赋值;失败时,返回0;如果指定的环境变量找不到,GetLastError()返回ERROR_ENVVAR_NOT_FOUND。
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include<strsafe.h>
int _tmain()
{
TCHAR szBuffer[1000];
DWORD dwResult = GetEnvironmentVariable(TEXT("PATH"), szBuffer, 1000);
if (dwResult != 0)
{
_tprintf(TEXT("PATH=%s"), szBuffer);
}
else
{
_tprintf(TEXT("function call falid!"));
}
system("pause");
return 1;
}
运行结果如下:
(2)下面是SetEnvironmentVariable函数的使用案例:
这里先放上SetEnvironmentVariable函数签名,后面使用案例有几个注意点需要重视。
1.SetEnvironmentVariable函数用于设置指定的环境变量:
BOOL WINAPI SetEnvironmentVariable(
__in LPCTSTR lpName, //环境变量名,当该值不存在且lpValue不为NULL时,将创建一个新的环境变量
__in_opt LPCTSTR lpValue //环境变量值
);
返回值:
成功时,返回非零值;
失败时,返回零值,调用GetLastError()查看具体的错误信息。
该函数对系统环境变量以及其他进程的环境变量不起作用!
在写测试程序前,我先在我的电脑->属性->高级系统设置->环境变量->用户变量处添加一个自定义的环境变量MyPath,环境变量值为woaini。呃。。。值不是重点,大概长下面那样。
好了,准备工作做好了,现在重点要关闭VS,重新开VS再运行测试代码(先思考为什么,如果不重开VS会有什么现象,后面讲注意点有解释重开VS的原因),现在放测试代码:
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
int _tmain(int argc,TCHAR *argv[],TCHAR *envp[])
{
TCHAR szBuffer[1000];//用于存储获取的环境变量的值
DWORD dwResult1 = GetEnvironmentVariable(TEXT("MyPath"), szBuffer, 1000);//先获取我们前面已经设置好的MyPath环境变量的值,如果没错应该是woaini,但如果你测试时获取不到,该函数返回0,那么就要看看后面我讲的注意点了哦。
if (dwResult1 != 0)
{
_tprintf(TEXT("MyPath=%s\n"), szBuffer);
}
else
{
_tprintf(TEXT("function call falid!\n"));
}
SetEnvironmentVariable(TEXT("MyPath"), TEXT("I love you"));//这里为我新建的MyPath环境变量重新修改值为I love you,注意,其实这只是修改当前进程的环境块,而未影响系统或用户的环境块
DWORD dwResult2 = GetEnvironmentVariable(TEXT("MyPath"), szBuffer, 1000);//这里重新获取以下修改后的MyPath环境变量的值
if (dwResult2 != 0)
{
_tprintf(TEXT("MyPath=%s\n"), szBuffer);
}
else
{
_tprintf(TEXT("function call falid!\n"));
}
system("pause");
return 0;
}
如果执行步骤没错,那么运行结果是下面这样的:
好了,注意点有以下几点:
1.为什么要重开VS,GetEnvironmentVariable函数才能正确获取前面我们新建的环境变量MyPath?这是因为我们之前讲过每个进程在创建时就被分配了一个环境块,而这个环境块就是Windows系统赋予的,那么我们可以猜测,当运行VS,就已经在内部存好了我们将要分配的环境块内容,而我们是VS运行后再新建环境变量MyPath,那么VS保存的这块内容还没更新呢,所以函数当然获取不到,我们只能重开VS了。这也只是我的猜测,是为了更好理解GetEnvironmentVariable函数,如有其他看法的,可以留言探究哦。
2.GetEnvironmentVariable函数对系统环境变量以及其他进程的环境变量不起作用,因为创建了一个进程,就已经为进程分配好环境块了,我们通过GetEnvironmentVariable函数添加、修改或删除环境块内容,也只是添加、修改或删除进程的环境块,而非Windows系统或用户的环境块。
(3)下面是CUI程序入口函数TCHAR *envp[]参数的使用案例:
这里就不自己写代码了,直接放上书本P76页的示例代码(修改过)。
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
int _tmain(int argc,TCHAR *argv[],TCHAR *envp[])
{
int current = 0;//用于环境变量计数
PTSTR *pElement = (PTSTR *)envp;//创建新的指针指向CUI程序的envp数组
PTSTR pCurrent = NULL;//用于遍历envp数组元素的指针
while (pElement != NULL)
{
//取数组的元素
pCurrent = (PTSTR)(*pElement);
//前面说过数组末尾是NULL指针,所以当遍历到NULL则将pElement置NULL,接着就跳出循环了
if (pCurrent == NULL)
{
pElement = NULL;
}
else
{
//打印遍历到的环境变量
_tprintf(TEXT("[%u] %s\r\n"), current, pCurrent);
current++;//计数+1
pElement++;//指向下一个数组元素
}
}
system("pause");
return 0;
}
运行结果如下:
(4)下面是ExpandEnvironmentStrings函数的使用案例:
通过前面注册表的了解,我们可以细心发现,有些环境变量的值含有两个百分号(%)之间的字符串,这种字符串叫做可替换字符串,顾名思义,我们可以通过函数ExpandEnvironmentStrings函数替换掉可替换字符串。也可以发现,这种可替换字符串只有在注册表才能看到,而在我的电脑->属性->高级系统设置->环境变量或通过其他方式获取整个完整的环境变量都看不到可替换字符串这种形式。下面,我先放上ExpandEnvirnmentStrings函数的函数签名:
DWORD WINAPI ExpandEnvironmentStrings(
_In_ LPCTSTR lpSrc,
_Out_opt_ LPTSTR lpDst,
_In_ DWORD nSize
);
参数1:一个包含可替换字符串的字符串地址(也叫扩展字符串),例如:TEXT("PATH=%PATH%")
参数2:用于接收扩展字符串的一个缓冲区的地址
参数3:这个缓冲区的最大大小,用字符数来表示。
返回值:保存扩展字符串所需的缓冲区的大小,用字符数表示,若参数3小于这个返回值,%%变量就不会扩展,而是被替换为空字符串,所以一般要调用两次ExpandEnvironmentStrings函数。
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
int _tmain(int argc,TCHAR *argv[],TCHAR *envp[])
{
//第一次调用ExpandEnvironmentStrings是为了获取保存扩展字符串所需的缓冲区大小,所以函数参数2可以为NULL,参数3为0
DWORD chValue = ExpandEnvironmentStrings(TEXT("USERPROFILE=‘%USERPROFILE%‘"), NULL, 0);
PTSTR pszBuffer = new TCHAR[chValue];//动态创建chValue大小的缓冲区,最后记得释放掉动态创建的空间
chValue = ExpandEnvironmentStrings(TEXT("USERPROFILE=‘%USERPROFILE%‘"), pszBuffer, chValue);//这次调用才是真正获取替换后的字符串
_tprintf(TEXT("%s\r\n%d"), pszBuffer,chValue);//打印扩展字符串的缓冲区和字符数目
delete[]pszBuffer;//释放动态创建的空间
system("pause");
return 0;
}
运行结果如下:
进程的当前驱动器和目录
有一些Windows函数的调用需要提供路径,例如:CreateFile函数打开一个文件(未指定完整路径名,只有一个文件名),那么该函数就会在当前驱动器(例如:C、D、E磁盘)的当前目录查找文件和目录。系统在内部跟踪记录着一个进程的当前驱动器和目录,我们可以获取进程的当前驱动器和目录,也可以修改进程的当前驱动器和目录。
下面给出分别获取和设置当前驱动器和目录的函数签名:
1.GetCurrentDirectory函数获取进程当前目录:
DWORD WINAPI GetCurrentDirectory(
_In_ DWORD nBufferLength,
_Out_ LPTSTR lpBuffer
);
nBufferLength:lpBuffer指针指向内存块的大小(单位TCHAR);
lpBuffer:接收当前路径的内存块。
2.SetCurrentDirectory函数设置进程当前目录
BOOL WINAPI SetCurrentDirectory(
_In_ LPCTSTR lpPathName
);
lpPathName:需要被设置的目录路径
3.GetFullPathName函数获取指定文件的当前路径:
DWORD WINAPI GetFullPathName(
__in LPCTSTR lpFileName,
__in DWORD nBufferLength,
__out LPTSTR lpBuffer,
__out LPTSTR *lpFilePart
);
lpFileName:文件名
nBufferLength:获取全路径的内存大小(TCHAR)
lpBuffer:内存指针
lpFilePart:文件名最后一个元素,在lpBuffer中的位置。
注意:这个函数,只是将当前路径,粘贴到你给的文件上,其他什么也没有做。
下面,我给出使用案例来领会这些函数的使用:
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
int _tmain(int argc,TCHAR *argv[],TCHAR *envp[])
{
TCHAR szPath[MAX_PATH];
GetCurrentDirectory(MAX_PATH, szPath);//获取进程当前路径
_tprintf(L"%s\n", szPath);
TCHAR *str = L"D:\\360Downloads\\";//设置的当前路径
SetCurrentDirectory(str); //设置文件的当前路径,如果指定的str参数在电脑中存在这个路径,那么就设置成功,否则设置无效,还是采用前一个有效的当前进程目录
GetCurrentDirectory(MAX_PATH, szPath);
_tprintf(L"%s\n", szPath);
TCHAR *str1 = L"D:\\ddsdf\\";//设置的当前路径
SetCurrentDirectory(str1); //设置文件的当前路径,如果指定的str参数在电脑中存在这个路径,那么就设置成功,否则设置无效,还是采用前一个有效的当前进程目录
GetCurrentDirectory(MAX_PATH, szPath);
_tprintf(L"%s\n", szPath);//因为"D:\\ddsdf\\"路径在我电脑里不存在,所以SetCurrentDirectory函数设置失败了
GetFullPathName(L"wxf1", MAX_PATH, szPath, NULL);
//这个函数只是将进程当前路径(szPath)粘贴到你给的文件名(wxf1)上,其他什么也没有做,不做检查
_tprintf(L"%s\n", szPath);
system("pause");
return 0;
}
运行结果如下:
通过上面的测试,我们可以得出以下几点:
1.GetCurrentDirectory函数是获取进程的当前目录,而函数参数1一般用MAX_PATH(宏定义为260)就很安全了,因为这是目录名称或文件名称得最大字符数了。
2.SetCurrentDirectory函数设置进程的当前目录,而如果该函数参数指定的路径在本电脑中不存在,那么就设置无效,还是采用前一个有效的当前进程目录。
3.GetFullPathName函数只是将进程当前路径(szPath)粘贴到你给的文件名(wxf1)上,其他什么也没有做,不做检查 。
4.为了更好理解GetCurrentDirectory函数获取进程的当前目录这一功能,你可以将上面代码生成的可执行文件放到桌面,再运行,那么进程的当前目录就改变啦
判断系统版本
......明天补充
原文地址:http://blog.51cto.com/12731497/2130213