目录
第1章说明
1.1 程序启动
参考下面的C++代码:
int GetC() { return 2;}; int a; int b = 1; int c = GetC(); int main() { return a+b+c; } |
程序载入内存,全局变量a、b、c就完成了静态初始化(static initialization),此时a、b、c的数值分别为0、1、0。
系统会调用入口函数mainCRTStartup,后者会调用_initterm(__xc_a,__xc_z);执行_initterm函数时会调用GetC函数,完成全局变量c的动态初始化(dynamic initialization)。此时a、b、c的数值分别为0、1、2。
mainCRTStartup会接着调用函数main,至此完成程序的启动工作。
注意:C语言里的全局变量只能静态初始化;C++语言里的全局变量才支持动态初始化。
1.2 强符号、弱符号
上面示例代码中的全局变量a没有赋给初始值,它就是弱符号。弱符号全局变量会被编译器初始化为零。
上面示例代码中的全局变量b、c赋予了初始值,它们就是强符号。
弱符号可能会被合并。如:1.cpp、2.cpp里均有弱符号a,那么连接时它们将被当做一个全局变量。又如:1.cpp、2.cpp里均有弱符号a,3.cpp里有强符号a,那么连接时以强符号为准。
强符号是不会被合并的。如:1.cpp、2.cpp里均有强符号a,那么连接时就会出错。
建议:尽量使用强符号,否则可能会产生难以察觉的错误。
1.3 动态初始化顺序
C++代码里,可使用#pragma init_seg来调整动态初始化的顺序。其顺序一共分为52级,如下表所示:
#pragma init_seg(".CRT$XIA") #pragma init_seg(".CRT$XIB") ... ... ... #pragma init_seg(".CRT$XIZ") |
#pragma init_seg(".CRT$XCA") #pragma init_seg(".CRT$XCB") #pragma init_seg(".CRT$XCC") 或 #pragma init_seg(compiler) ... ... ... #pragma init_seg(".CRT$XCL") 或 #pragma init_seg(lib) ... ... ... #pragma init_seg(".CRT$XCU") 或 #pragma init_seg(user) ... ... ... #pragma init_seg(".CRT$XCZ") |
上表中,越靠上的段内全局变量越先被动态初始化。如下面的两个cpp文件内容:
#pragma init_seg(".CRT$XCC") int C1 = GetC1(); int C2 = GetC2(); |
#pragma init_seg(".CRT$XCL") int L1 = GetL1(); int L2 = GetL2(); |
全局变量C1、C2在段.CRT$XCC里,L1、L2在段.CRT$XCL里,所以C1、C2肯定比L1、L2优先完成动态初始化。
同一段内全局变量的初始化顺序完全无法预料。如:上面C1、C2的动态初始化顺序是无法预料的。
全局变量默认在段.CRT$XCU内,C++库的全局变量在段.CRT$XCC内,MFC库的全局变量在段.CRT$XCL内。所以,编写一个MFC程序并且静态连接MFC库时,C++库的全局变量首先被动态初始化,然后MFC库的全局变量被动态初始化,最后是MFC程序里的全局变量被动态初始化。这样的动态初始化顺序非常重要,因为MFC程序里的全局变量有可能会调用C++库或MFC库里的函数或变量。不调整好顺序就有可能出错。如下面的代码:
#include <stdio.h> #include <string> #pragma init_seg(".CRT$XCA") std::string s = "123"; void main() { puts(s.c_str()); } |
如果上述代码静态连接C运行时库,那么在C++库内的全局变量被初始化前,全局变量s就被动态初始化了。结果就是s动态初始化失败,其成员变量全部为零(静态初始化的结果)。
1.4 exe调用dll
上一节的例子,如果动态连接C运行时库(msvcrt.dll),那么全局变量s就能正常动态初始化。其初始化步骤为:
1、初始化dll内的全局变量
2、调用dll内的DllMain(...,DLL_PROCESS_ATTACH,...)
3、初始化exe内的全局变量
4、调用 exe 内的main 或 WinMain
5、从 exe 内的main 或 WinMain 返回
6、销毁exe内的全局变量
7、调用 dll 内的DllMain(...,DLL_PROCESS_DETACH,...)
8、销毁dll内的全局变量
第1步中,msvcrt.dll内部完成了C++库全局变量的初始化工作;第3步动态初始化exe里的全局变量s时,就不会有问题了。
1.5 禁用动态初始化
全局变量的动态初始化是入口函数mainCRTStartup 调用_initterm函数产生的结果。如果自行指定入口函数,就不再会有动态初始化了。如下面的代码:
#include <windows.h> int GetA() { return 1; } int a = GetA(); #pragma comment(linker,"/entry:MyEntry") //修改入口函数为 MyEntry void main(){} void MyEntry() { MessageBox(NULL,a ? TEXT("1") : TEXT("0"),TEXT(""),MB_OK); } |
全局变量a将无法完成动态初始化,其值为零。
注意:自行指定入口函数,静态连接C函数库时无法动态初始化C函数库里的全局变量,将导致C函数库里的函数无法使用。
1.6 应用实例
请参考如下几段MFC代码:
UINT GetMsgId() { static UINT uID = WM_USER + 100; return uID++; } |
//串口通讯模块 //收到串口数据后PostMessage(hWndMain,WM_SERIAL_RECV) UINT WM_SERIAL_RECV = GetMsgId(); |
//网络通讯模块 //收到网络数据后PostMessage(hWndMain,WM_SOCKET_RECV) UINT WM_SOCKET_RECV = GetMsgId(); |
//主窗口 CDlgMain BEGIN_MESSAGE_MAP(CDlgMain, CDialog) ON_MESSAGE(WM_SERIAL_RECV,OnSerialRecv) ON_MESSAGE(WM_SOCKET_RECV,OnSocketRecv) END_MESSAGE_MAP() |
串口通讯模块收到串口数据后,给主窗口寄送WM_SERIAL_RECV消息;网络通讯模块收到网络数据后,给主窗口寄送WM_SOCKET_RECV消息。为了防止WM_SERIAL_RECV和WM_SOCKET_RECV重复,特使用GetMsgId函数对它们进行动态初始化。
通过主窗口CDlgMain的消息映射表可知:收到WM_SERIAL_RECV消息,将调用函数OnSerialRecv进行处理;收到WM_SOCKET_RECV消息,将调用OnSocketRecv进行处理。
VC++6.0里展开BEGIN_MESSAGE_MAP、ON_MESSAGE就是如下代码:
const AFX_MSGMAP_ENTRY CDlgMain::_messageEntries[] = { { WM_SERIAL_RECV, 0, 0, 0, AfxSig_lwl, &OnSerialRecv }, { WM_SOCKET_RECV, 0, 0, 0, AfxSig_lwl, &OnSocketRecv }, } |
全局变量WM_SERIAL_RECV、WM_SOCKET_RECV、CDlgMain::_messageEntries均在段.CRT$XCU里,所以它们的初始化顺序是不可预知的。
如果CDlgMain::_messageEntries先于WM_SERIAL_RECV、WM_SOCKET_RECV动态初始化,那么很不幸,此时的WM_SERIAL_RECV、WM_SOCKET_RECV均为零,所以CDlgMain将无法处理WM_SERIAL_RECV、WM_SOCKET_RECV消息。
为了让WM_SERIAL_RECV、WM_SOCKET_RECV先于CDlgMain::_messageEntries动态初始化,可这样修改代码:
//串口通讯模块 //收到串口数据后PostMessage(hWndMain,WM_SERIAL_RECV) #pragma init_seg(lib) UINT WM_SERIAL_RECV = GetMsgId(); |
//网络通讯模块 //收到网络数据后PostMessage(hWndMain,WM_SOCKET_RECV) #pragma init_seg(lib) UINT WM_SOCKET_RECV = GetMsgId(); |
现在WM_SERIAL_RECV、WM_SOCKET_RECV在段.CRT$XCL里,而CDlgMain::_messageEntries在段.CRT$XCU里。这样就能保证CDlgMain::_messageEntries动态初始化前WM_SERIAL_RECV、WM_SOCKET_RECV已经动态初始化完毕。CDlgMain也就能够正常处理WM_SERIAL_RECV、WM_SOCKET_RECV消息了。