一、Win32基本程序观念 (学习笔记)

PE文件格式之MS-DOS头,PE文件头,区块 PE文件格式之MS-DOS头,PE文件头,RVA,VA,虚拟地址,PE文件格式 阅读详情

>>>   基础:对于Windows   程序的事件驱动特性的了解(包括消息的产生、获得、分派、判断、处理)

>>>   基础:对C++多态(polymorphism)的精确体会

Win32程序开发流程

Windows   程序分为「程序代码」和「U I(User  Interface)资源」两大部份,两部份最后以RC编译器整合为一个完整的EX E 文件(图1-1)。所谓U I 资源是指功能菜单、对话框外貌、程序图标、光标形状等等东西。这些U I  资源的实际内容(二进制代码)系借助各种工具产生,并以各种扩展名存在,如. ico、. bmp、. cur 等等。程序员必须在一个所谓的资源描述档(. rc )中描述它们。RC 编译器(RC.EXE)读取RC 档的描述后将所有U I资源档集中制作出一个. RES 档,再与程序代码结合在一起,这才是一个完整的Windows可执行档。

需要什么函数库(.LIB )

众所周知Windows支持动态联结。换句话说,应用程序所调用的Windows API 函数是在「执行时期」才联结上的。那么,「联结时期」所需的函数库做什么用?有哪些?并不是延伸档名为. dll 才是动态联结函数库(DLL,Dynamic Link Library ),事实上. exe 、. dll、. fon、. mod、. drv、. ocx 都是所谓的动态联结函数库。Windows程序调用的函数可分为C  Runtimes以及Windows API 两大部份。早期的CRun times并不支持动态联结,但Visual C++4. 0之后已支持,并且在32 位操作系统中已不再有small/medium/large等内存模式之分。以下是它们的命名规则与使用时机:■   LIBC. LIB -  这是C  Runtime  函数库的静态联结版本。■   MSVCRT. LIB -  这是C  Runtime  函数库动态联结版本(MSVCRT40.DLL)的import函数库。如果联结此函数库,你的程序执行时必须有MSVCRT40.DLL   在场。另一组函数Windows API,由操作系统本身(主要是Windows三大模块GDI32.DLL和USER32.DLL和KERNEL32.DLL)提供(注)。虽说动态联结是在执行时期才发生「联结」事实,但在联结时期,联结器仍需先为调用者(应用程序本身)准备一些适当的信息,才能够在执行时期顺利「跳」到DLL执行。如果该API 所属之函数库尚未加载,系统也才因此知道要先行加载该函数库。这些适当的信息放在所谓的「import函数库」中。32位Windows的三大模块所对应的import函数库分别为GDI32.LIB 和USER32.LIB和KERNEL32.LIB。Windows发展至今,逐渐加上的一些新的API函数(例如CommonDialog、ToolHelp )并不放在GDI和USER和KERNEL三大模块中,而是放在诸如COM MDLG.DLL、TOOLHELP.DLL之中。如果要使用这些APIs ,联结时还得加上这些DLLs所对应的import函数库,诸如COMDLG32.LIB和TH32.LIB。很快地,在稍后的范例程序Generic的makefile中,你就可以清楚看到联结时期所需的各式各样函数库(以及各种联结器选项)。

需要什么头文件(.H)

所有Windows程序都必须包含WINDOWS.H。早期这是一个巨大的头文件,大约有5000行左右,Visual C++4.0已把它切割为各个较小的文件,但还以WINDOWS.H总括之。除非你十分清楚什么API动作需要什么头文件,否则为求便利,单单一个WINDOWS.H也就是了。不过,WINDOWS.H只照顾三大模块所提供的API函数,如果你用到其它system DLLs,例如COMMDLG.DLL或MAPI.DLL或TAPI.DLL等等,就得包含对应的头文件,例如COMMDLG.H或MAPI.H或TA PI.H等等。

以消息为基础,以事件驱动之(message based,event driven)

Windows   程序的进行系依靠外部发生的事件来驱动。换句话说,程序不断等待(利用一个while  回路),等待任何可能的输入,然后做判断,然后再做适当的处理。上述的「输入」是由操作系统捕捉到之后,以消息形式(一种数据结构)进入程序之中。操作系统如何捕捉外围设备(如键盘和鼠标)所发生的事件呢?噢,USER  模块掌管各个外围的驱动程序,它们各有侦测回路。

如果把应用程序获得的各种「输入」分类:

由硬件装置所产生的消息(如鼠标移动或键盘被按下),放在系统队列(system queue)中

由Windows系统或其它Windows程序传送过来的消息,放在程序队列(application queue )中

以应用程序的眼光来看,消息就是消息,来自哪里或放在哪里其实并没有太大区别,反正程序调用GetMessage API就取得一个消息,程序的生命靠它来推动。所有的GUI系统,包括U N IX的XWindow以及O S /2的Presentation Manager ,都像这样,是以消息为基础的事件驱动系统。可想而知,每一个Windows   程序都应该有一个回路如下:

MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL)) {
	TranslateMessage(&msg);
	DispatchMessage(&msg);
}
// 以上出现的函数都是Windows API  函数

消息,也就是上面出现的MSG结构,其实是Windows内定的一种资料格式:

/* Queued message structure */
typedef struct tagMSG
{
	HWND hwnd;
	UINT message; // WM_xxx ,例如WM_MOUSEMOVE ,WM_SIZE...
	WPARAM wParam;
	LPARAM lParam;
	DWORD time;
	POINT pt;
} MSG;

接受并处理消息的主角就是窗口。每一个窗口都应该有一个函数负责处理消息,程序员必须负责设计这个所谓的「窗口函数」(window  procedure ,或称为window function)。如果窗口获得一个消息,这个窗口函数必须判断消息的类别,决定处理的方式。以上就是Windows程序设计最重要的观念。至于窗口的产生与显示,十分简单,有专门的API函数负责。稍后我们就会看到Windows程序如何把这消息的取得、分派、处理动作表现出来。


一个具体的Win32 程序


许多相关书籍或文章尝试以各种方式简化Windows程序的第一步,因为单单一个H ello程序就要上百行,怕把大家吓坏了。我却宁愿各位早一点接触正统写法,早一点看到全貌。Windows的东西又多又杂,早一点一窥全貌是很有必要的。而且你会发现,经过有条理的解释之后,程序代码的多寡其实构不成什么威胁(否则无字天书最适合程序员阅读)。再说,上百行程序代码哪算得了什么!你可以从图1-2 得窥Win32  应用程序的本体与操作系统之间的关系。Win32 程序中最具代表意义的动作已经在该图显示出来,完整的程序代码展示于后。本章后续讨论都围绕着此一程序。


程序工程:mfc11


程序进入点WinMain


main是一般C程序的进入点:
int main(int argc, char *argv[ ], char *envp[ ]);
{
	...
}
WinMain则是Windows程序的进入点:
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
...
}
// 在Win32  中CALLBACK  被定义为__stdcall ,是一种函数调用习惯,关系到
// 参数挤压到堆栈的次序,以及处理堆栈的责任归属。其它的函数调用习惯还有
// _pascal和_cdecl

当Windows的「外壳」(shell,例如Windows3. 1的程序管理员或Windows 95 的文件总管)侦测到使用者意欲执行一个Windows 程序,于是调用加载器把该程序加载,然后调用Cstartupcode,后者再调用WinMain,开始执进程序。WinMain  的四个参数由操作系统传递进来。


窗口类别之注册与窗口之诞生


一开始,Windows程序必须做些初始化工作,为的是产生应用程序的工作舞台:窗口。这没有什么困难,因为A P I 函数CreateWindow 完全包办了整个巨大的工程。但是窗口产生之前,其属性必须先设定好。所谓属性包括窗口的「外貌」和「行为」,一个窗口的边框、颜色、标题、位置等等就是其外貌,而窗口接收消息后的反应就是其行为(具体地说就是指窗口函数本身)。程序必须在产生窗口之前先利用A P I函数RegisterClass设定属性(我们称此动作为注册窗口类别)。Register Class需要一个大型数据结构WNDCLASS 做为参数,CreateWindow 则另需要11 个参数。


从图1-3 可以清楚看出一个窗口类别牵扯的范围多么广泛,其中wc. lpfnWndProc所指定的函数就是窗口的行为中枢,也就是所谓的窗口函数。注意,CreateWindow 只产生窗口,并不显示窗口,所以稍后我们必须再利用ShowWindow将之显示在屏幕上。又,我们希望先传送个WM_PAINT给窗口, 以驱动窗口的绘图动作, 所以调用UpdateWindow。消息传递的观念暂且不表,稍后再提。请注意,在程序中,Register Class被我包装在InitApplication 函数之中,Creat eWindow 则被我包装在InitInstance  函数之中。这种安排虽非强制,却很普遍:

int CALLBACK WinMain(
	HINSTANCE hInstance, 
	HINSTANCE hPrevInstance,
	LPSTR lpCmdLine, 
	int nCmdShow
)
{
	if (!hPrevInstance)
	if (! InitApplication (hInstance))
	return (FALSE);
	if (! InitInstance (hInstance, nCmdShow))
	return (FALSE);
	...
}
//--------------------------------------------------
BOOL  InitApplication (HINSTANCE hInstance)
{
	WNDCLASS wc;
	...
	return ( RegisterClass (&wc));
}
//--------------------------------------------------
BOOL  InitInstance (HINSTANCE hInstance, int nCmdShow)
{
	_hWnd =  CreateWindow (...);
	...
}


两个函数(InitApplicat ion 和InitInstance)的名称别具意义:

■  在Windows3. x时代,窗口类别只需注册一次,即可供同一程序的后续每一个执行实例(instance)使用(之所以能够如此,是因为所有进程共在一个地址空间中),所以我们把Register Class这个动作安排在「只有第一个执行个体才会进入」的InitApplication 函数中。至于此一进程是否是某个程序的第一个执行实例,可由WinMain的参数hPrevInstance判断之;其值由系统传入。

 ■ 产生窗口,是每一个执行实例(instance)都得做的动作, 所以我们把Creat eWindow 这个动作安排在「任何执行实例都会进入」的InitInstance函数中。


以上情况在Windows NT和Windows95中略有变化。由于Win32程序的每一个执行实例(instance)有自己的地址空间,共享同一窗口类别已不可能。但是由于Win32系统令hPrevInstance永远为0,所以我们仍然得以把Register Class和Creat eWindow 按旧习惯安排。既符合了新环境的要求,又兼顾到了旧源代码的兼容。InitApplication和InitInstance只不过是两个自定函数,为什么我要对此振振有词呢?原因是M F C把这两个函数包装成CWinApp 的两个虚拟成员函数。第6章「M F C程序的生与死」对此有详细解释。


消息循环

初始化工作完成后,WinMain  进入所谓的消息循环:
while (GetMessage(&msg,...)) {
	TranslateMessage(&msg); //  转换键盘消息
	DispatchMessage(&msg); //  分派消息
}
其中的TranslateMessage是为了将键盘消息转化,DispatchMessage会将消息传给窗口函数去处理。没有指定函数名称,却可以将消息传送过去,岂不是很玄?这是因为消息发生之时,操作系统已根据当时状态,为它标明了所属窗口,而窗口所属之窗口类别又已经明白标示了窗口函数(也就是wc. lpfnWndProc   所指定的函数),所以DispatchMessage自有脉络可寻。
请注意图1-2 所示,DispatchMessage  经过USER  模块的协助,才把消息交到窗口函数手中。
消息循环中的GetMessage  是Windows   3. x非强制性(non-preemptive )多任务的关键。应用程序藉由此动作,提供了释放控制权的机会:如果消息队列上没有属于我的消息,我就把机会让给别人。透过程序之间彼此协调让步的方式,达到多任务能力。Windows   95 和Windows   N T  具备强制性(preempt ive )多任务能力,不再非靠GetMessage释放CPU控制权不可,但程序写法依然不变,因为应用程序仍然需要靠消息推动。它还是需要抓消息!

窗口的生命中枢:窗口函数


消息循环中的DispatchMessage把消息分配到哪里呢?它透过USER模块的协助,送到该窗口的窗口函数去了。窗口函数通常利用switch/case方式判断消息种类,以决定处置方式。由于它是被Windows系统所调用的(我们并没有在应用程序任何地方调用此函数),所以这是一种callback函数,意思是指「在你的程序中,被Windows系统调用」的函数。这些函数虽然由你设计,但是永远不会也不该被你调用,它们是为Windows系统准备的。程序进行过程中,消息由输入装置,经由消息循环的抓取,源源传送给窗口并进而送到窗口函数去。窗口函数的体积可能很庞大,也可能很精简,依该窗口感兴趣的消息数量多寡而定。至于窗口函数的形式,相当一致,必然是:
LRESULT CALLBACK WndProc(
	HWND hWnd,
	UINT message,
	WPARAM wParam,
	LPARAM lParam
)


注意,不论什么消息,都必须被处理,所以switch/case指令中的default:处必须调用DefWindowProc ,这是Windows内部预设的消息处理函数。窗口函数的wPar am和lParam的意义,因消息之不同而异。wParam在16 位环境中是16 位,在32 位环境中是32 位。因此,参数内容(格式)在不同操作环境中就有
了变化。
我想很多人都会问这个问题:为什么Windows Programming Modal  要把窗口函数设计为一个call back函数?为什么不让程序在抓到消息(GetMes sage)之后直接调用它就好了?原因是,除了你需要调用它,有很多时候操作系统也要调用你的窗口函数(例如当某个消息产生或某个事件发生)。窗口函数设计为callback形式,才能开放出一个接口给操作系统叫用。


消息映射(Message Map)的雏形


有没有可能把窗口函数的内容设计得更模块化、更一般化些?下面是一种作法。请注意,以下作法是MFC「消息映射表格」(第9章)的雏形,我所采用的结构名称和变量名称,都与MFC相同,藉此让你先有个暖身。首先,定义一个MSGMAP_ENTRY结构和一个dim宏:

struct MSGMAP_ENTRY 
{
	UINT nMessage;
	LONG (*pfn)(HWND, UINT, WPARAM, LPARAM);
};
#define dim(x) (sizeof(x) / sizeof(x[0]))


请注意MSGMAP_ENTRY的第二元素pfn是一个函数指针,我准备以此指针所指之函数处理nMessage  消息。这正是对象导向观念中把「资料」和「处理资料的方法」封装起来的一种具体实现,只不过我们用的不是C++语言。接下来,组织两个数组_messageEntries [  ]  和_commandEntries [  ],把程序中欲处理的消息以及消息处理例程的关联性建立起来:

// 消息与处理例程之对照表格
struct MSGMAP_ENTRY _messageEntries[] =
{
	WM_CREATE, OnCreate,
	WM_PAINT, OnPaint,
	WM_SIZE, OnSize,
	WM_COMMAND, OnCommand,
	WM_SETFOCUS, OnSetFocus,
	WM_CLOSE, OnClose,
	WM_DESTROY, OnDestroy,
} ;  	    ↑         ↑
	这是消息    这是消息处理例程

 //Command-ID与处理例程之对照表格
struct MSGMAP_ENTRY _commandEntries =
{
    IDM_ABOUT,      OnAbout,
    IDM_FILEOPEN,   OnFileOpen,
    IDM_SAVEAS,     OnSaveAs,
} ;     ↑                        ↑
   这是WM_COMMAND  命令项这是命令处理例程
               



于是窗口函数可以这么设计:

//----------------------------------------------------------------------//
//窗口函数
//----------------------------------------------------------------------
LRESULT CALLBACK WndProc(
			HWND  hWnd,     
			UINT  message,
                        WPARAM wParam, 
			LPARAM lParam
)
{
int i;
  for(i=0; i < dim(_messageEntries); i++) {  // 消息对照表
      if (message ==  _messageEntries[i].nMessage)
          return((*_messageEntries[i].pfn)(hWnd,  message, wParam,  lParam));
  }
  return(DefWindowProc(hWnd, message, wParam, lParam));
}
//----------------------------------------------------------------------
// OnCommand -- 专门处理WM_COMMAND 
//----------------------------------------------------------------------
LONG OnCommand(
		HWND hWnd,
	        UINT message,
                WPARAM  wParam, 
		LPARAM lParam)
{
int i;
  for(i=0; i < dim(_commandEntries); i++) {  // 
      if (LOWORD(wParam) ==  _commandEntries[i].nMessage)
          return((*_commandEntries[i].pfn)(hWnd, message, wParam, lParam));
  }
  return(DefWindowProc(hWnd, message, wParam, lParam));
}
//----------------------------------------------------------------------
LONG OnCreate(HWND hWnd, UINT wMsg, UINT wParam, LONG lParam)
{
   ...
}
//----------------------------------------------------------------------
LONG OnAbout(HWND hWnd, UINT wMsg, UINT wParam, LONG lParam)
{
...
}
//----------------------------------------------------------------------


这么一来,WndProc和OnCommand 永远不必改变,每有新要处理的消息,只要在_messageEntries [  ]  和_commandEntries [  ]  两个数组中加上新元素,并针对新消息撰写新的处理例程即可。

这种观念以及作法就是MFC的Message Map的雏形。MFC把其中的动作包装得更好更精致(当然因此也就更复杂得多),成为一张庞大的消息地图;程序一旦获得消息,就可以按图上溯,直到被处理为止。我将在第3章简单仿真MFC的Message Map ,并在第9章「消息映射与绕行」中详细探索其完整内容。

对话框的运作


Windows的对话框依其与父窗口的关系,分为两类:
1 . 「令其父窗口除能,直到对话框结束」,这种称为modal对话框。
2 . 「父窗口与对话框共同运行」,这种称为modeless对话框。

比较常用的是modal对话框。我就以程序对话框做为说明范例。


为了做出一个对话框,程序员必须准备两样东西:
1 . 对话框模板(dialog template)。这是在RC 文件中定义的一个对话框外貌,以各种方式决定对话框的大小、字形、内部有哪些控制组件、各在什么位置等等。
2 . 对话框函数(dialog procedure )。其类型非常类似窗口函数,但是它通常只处理WM_INITDIALOG和WM_COMMAND两个消息。对话框中的各个控制组件也都是小小窗口,各有自己的窗口函数,它们以消息与其管理者(父窗口,也就是对话框)沟通。而所有的控制组件传来的消息都是WM_COMMAND,再由其参数分辨哪一种控制组件以及哪一种通告(notification)。


Modal对话框的激活与结束,靠的是DialogBox和EndDialog两个API函数。请看图1-4。对话框处理过消息之后,应该传回TRUE;如果未处理消息,则应该传回FALSE。这是因为你的对话框函数之上层还有一个系统提供的预设对话框函数。如果你传回FALSE,该预设对话框函数就会接手处理。



模块定义文件(. DEF)

obsolete


资源描述档(. RC)

RC 文件是一个以文字描述资源的地方。常用的资源有九项之多,分别是ICON 、CURSOR、BITMAP 、FONT、DIALOG 、MENU 、ACCELERATOR、STRING 、VERSION INFO 。还可能有新的资源不断加入,例如Visual C++  4. 0  就多了一种名为TOOLBAR的资源。这些文字描述需经过RC 编译器,才产生可使用的二进制代码。本例示范ICON 、MENU和DIALOG三种资源。



Windows程序的生与死


我想你已经了解Windows程序的架构以及它与Windows系统之间的关系。对Windows消息种类以及发生时机的透彻了解,正是程序设计的关键。现在我以窗口的诞生和死亡,说明消息的发生与传递,以及应用程序的兴起与结束,请看图1-5 。

1 . 程序初始化过程中调用Creat eWindow,为程序建立了一个窗口,做为程序的萤幕舞台。CreateWindow 产生窗口之后会送出WM_CREATE直接给窗口函数,后者于是可以在此时机做些初始化动作(例如配置内存、开文件、读初始资料. . . )。
2 . 程序活着的过程中,不断以GetMessage从消息贮列中抓取消息。如果这个消息是WM_QUIT ,GetMessage会传回0而结束while循环,进而结束整个程序。
3 . DispatchMessage透过Windows USER模块的协助与监督,把消息分派至窗口函数。消息将在该处被判别并处理。
4 .
程序不断进行2 . 和3 . 的动作。
5 . 当使用者按下系统菜单中的Close命令项,系统送出WM_CLOSE。通常程序的窗口函数不栏截此消息,于是DefWindowProc处理它。
6 . DefWindowProc收到WM_CLOSE  后, 调用Destroy Window 把窗口清除。DestroyWindow 本身又会送出WM_DESTROY 。
7 . 程序对WM_D ESTROY的标准反应是调用PostQuitMessage。
8 . PostQuitMessage没什么其它动作,就只送出WM_QUIT消息,准备让消息循环中的GetMessage取得,如步骤2 ,结束消息循环。



为什么结束一个程序复杂如斯?因为操作系统与应用程序职司不同,二者是互相合作的关系,所以必需各做各的份内事,并互以消息通知对方。如果不依据这个游戏规则,可能就会有麻烦产生。你可以作一个小实验,在窗口函数中拦截WM_DESTROY ,但不调PostQuitMessage。你会发现当选择系统菜单中的Clos e 时,屏幕上这个窗口消失了,(因为窗口摧毁及数据结构的释放是D efWindowP r oc   调用D es tr oy Window 完成的),但是应用程序本身并没有结束(因为消息循环结束不了),它还留存在内存中。


空闲时间的处理:OnIdle

所谓空闲时间(idle time),是指「系统中没有任何消息等待处理」的时间。举个例子,没有任何程序使用定时器(timer ,它会定时送来WM_TIMER),使用者也没有碰触键盘和鼠标或任何外围,那么,系统就处于所谓的空闲时间。空闲时间常常发生。不要认为你移动鼠标时产生一大堆的WM_MOUSEMOVE ,事实上夹杂在每一个WM_MOUSEMOVE之间就可能存在许多空闲时间。毕竟,计算机速度超乎想像。背景工作最适宜在空闲时间完成。传统的SDK程序如果要处理空闲时间,可以以下列循环取代WinMain中传统的消息循环:

while (TRUE) {
	if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE) {
	if (msg.message ==  WM_QUIT)
	break;
	TranslateMessage(&msg);
	DispatchMessage(&msg);
}
	else {
	OnIdle ();
}
}

原因是PeekMessage和GetMessage的性质不同。它们都是到消息队列中抓消息,如果抓不到,程序的主执行线程(primary thread ,是一个UI 执行线程)会被操作系统虚悬住。当操作系统再次回来照顾此一执行线程,而发现消息队列中仍然是空的,这时候两个API函数的行为就有不同了:

 ■ GetMessage会过门不入,于是操作系统再去照顾其它人。

 ■ PeekMessage会取回控制权,使程序得以执行一段时间。于是上述消息循环进入OnIdle函数中。


第6章的HelloMFC将示范如何在MFC程序中处理所谓的idle time(p. 403)。



Console程序


说到Windows程序,一定得有WinMain、消息循环、窗口函数。即使你只产生一个对话窗(Dialog Box)或消息窗(Message Box),也有隐藏在Windows  API(DialogBox和MessageBox )内里的消息循环和窗口函数。

过去那种单单纯纯纯的C/C++程序,有着简单的main和printf的好时光到哪里去了?夏天在阴凉的树荫下嬉戏,冬天在温暖的炉火边看书,啊,Where did the   good times go?

其实说到Win32程序,并不是每个都如Windows GUI程序那么复杂可怖。是的,你可以在Visual C++ 中写一个" DOS-like "   程序,而且仍然可以调用部份的、不牵扯到图形使用者接口(GUI)的Win32 API。这种程序称为console  程序。甚至你还可以在console程序中使用部份的MFC类别(同样必须是与GUI没有关连的),例如处理数组、串行等数据结构的collection classes (CArray、CList、CMap)、与文件有关的CFile、CStdioFile。

我在BBS论坛上看到很多程序设计初学者,还没有学习C/C++,就想直接学习Visual C++。并不是他们好高骛远,而是他们以为Visual C++是一种特殊的C++ 语言。吃过苦头的过来人以为初学所说的Visual C++ programming是指MFC programming,所以大吃一惊(没有一点C++  基础就要学习MFC programming,当然是大吃一惊)。

在Visual  C++ 中写纯种的C/C++  程序?当然可以!不牵扯任何窗口、对话窗、控制组件,那就是console程序! 虽然我这本书没有打算照顾C++初学者,然而我还是决定把console程序设计的一些相关心得放上来,同时也是因为我打算以console程序完成稍后的多线程程序范例。第3章的MFC六大技术仿真程序也都是console程序。

其实,除了" DOS-like" ,console程序还另有妙用。如果你的程序和使用者之间是以巨量文字来互动,或许你会选择使用edit控制组件(或MFC的CEditView)。但是你知道,计算机在一个纯粹的「文字窗口」(也就是console  窗口)中处理文字的显现与卷动比较快,你的程序动作也比较简单。所以,你也可以在Windows程序中产生console窗口,独立出来操作。


这也许不是你所认知的console程序。总之,有这种混合式的东西存在。这一节将以我自己的一个极简易的个人备份软件JBACKUP为实例,说明Win32 consol e程序的撰写,以及如何在其中使用Win32 API(其实直接调用就是了)。再以另一个极小的程序MFCCON示范MFC console程序(用到了MFC的CStudioFile  和CString)。对于这么小的程序而言,实在不需动用到整合环境下的什么项目管理。至于复杂一点的程序,就请参考第4章最后一节「Console程序的项目管理」。



Console程序与DOS程序的差别


不少人把DOS程序和console程序混为一谈,这是不对的。以下是各方面的比较。

制造方式

在Windows环境下的DOS Box中,或是在Windows版本的各种C++ 编译器套件的整合环境(IDE)中(第4章「Consol e  程序项目管理」),利用Windows   编译器、联结器做出来的程序,都是所谓Win32  程序。如果程序是以main为进入点,调用Cruntime函数和「不牵扯G U I」的Win32 API函数,那么就是一个console  程序,console窗口将成为其标准输入和输出装置(cin和cout)。过去在DOS环境下开发的程序,称为DOS程序,它也是以main为程序进入点,可
以调用C runtime函数。但,当然,不可能调用Win32 API函数。

程序能力

过去的DOS程序仍然可以在Windows的DOS Box中跑(Win95的兼容性极高,WinNT的兼容性稍差)。Console程序当然更没有问题。由于console程序可以调用部份的Win32 API(尤其是KERNEL32. DLL模块所提供的那一部份),所以它可以使用Windows提供的各种高级功能。它可以产生进程(processes ),产生执行线程(threads )、取得虚拟内存的信息、刺探操作系统的各种资料。但是它不能够有华丽的外表--因为它不能够调用与GUI有关的各种API函数。DOS   程序和console程序两者都可以做printf 输出和cout输出,也都可以做scanf 输入和cin输入。

可执行档格式

DOS程序是所谓的MZ格式(MZ是Mark Zbikowski的缩写,他是DOS系统的一位主要构造者)。Console程序的格式则和所有的Win32程序一样,是所谓的P E(PortableExecutable)格式,意思是它可以被拿到任何Win32平台上执行。Visual C++附有一个DUMPBIN工具软件,可以观察PE文件格式。拿它来观察本节的JBACKUP程序和MFCCON程序(以及第3章的所有程序),得到这样的结果:

H:\u004\prog\jbackup.01>dumpbin /summary jbackup.exe
	Microsoft (R) COFF Binary File Dumper Version 5.00.7022
	Copyright (C) Microsoft Corp 1992-1997. All rights reserved.
	Dump of file jbackup.exe
	File Type: EXECUTABLE IMAGE
		Summary
		5000 .data
		1000 .idata
		1000 .rdata
		5000 .text

拿它来观察D O S程序,则得到这样的结果:

C:\UTILITY>dumpbin /summary dsize.exe
	Microsoft (R) COFF Binary File Dumper Version 5.00.7022
	Copyright (C) Microsoft Corp 1992-1997. All rights reserved.
	Dump of file dsize.exe
	DUMPBIN : warning LNK4094: "dsize.exe" is an MS-DOS executable;
		use EXEHDR to dump it
  		Summary


Console程序的编译联结


ellipsis...



JBACKUP:Win32 Console程序设计


撰写console程序,有几个重点请注意:
1 . 进入点为main。
2 . 可以使用printf、scanf、cin、cout等标准输出入装置。
3 . 可以调用和GUI无关的Win32 API。


我的这个JBACKUP程序可以有一个或两个参数,用法如下:

C: \SomeoneDir> JBACKUP SrcDir [DstDir]

代码:JBackup项目编译通过


MFCCON:MFC Console程序设计


当你的进度还在第1章的Win32  基本程序观念,我却开始讲如何设计一个MFC console程序,是否有点时地不宜?是有一点!所以我挑一个最单纯而无与别人攀缠纠葛的MFC类别,写一个40 行的小程序。目标纯粹是为了做一个导入,并与Win32  console程序做一比较。我所挑选的两个单纯的MFC类别是CStdioFile和CString:



在M F C  之中,CFile用来处理正常的文件I/ O动作。CStdioFile衍生自CFile,一个CStdioFile对象代表以Cruntime函数fopen所开启的一个stream文件。Stream  文件有缓冲区,可以文字模式(预设情况)或二进制模式开启。CString对象代表一个字符串,是一个完全独立的类别。我的例子用来计算小于100的所有费伯纳契数列(Fabonacci sequence )。费伯纳契数列的计算方式是:
1 . 头两个数为1。
2 . 接下来的每一个数是前两个数的和。

程序:工程1MFCCON


这么简单的例子中,我们看到MFC Console 程序的几个重点:
1 . 程序进入点仍为main
2 . 需包含所使用之类别的头文件(本例为AFX.H)
3 . 可直接使用与GUI无关的MFC类别(本例为CStdioFile和CString)
4 . 编辑时需指定/MT,表示使用多执行线程版本的C runtime 函数库。


第4点需要多做说明。在MFC console  程序中一定要指定多线程版的C  runt ime  函数库,

在VS项目设置中 要使用MFC的DLL库



进程与执行线程(Process and Thread)


OS /2、Windows NT以及Windows 95 都支持多执行线程,这带给PC程序员一股令人兴奋的气氛。然而它带来的不全然是利多,如果不谨慎小心地处理执行线程的同步问题,程序的错误以及除错所花的时间可能使你发誓再也不碰「执行线程」这种东西。我们习惯以进程(process )表示一个执行中的程序,并且以为它是CPU排程单位。事实上执行线程才是排程单位。


核心对象

首先让我解释什么叫作「核心对象」(kernel object )。「GDI对象」是大家比较熟悉的东西,我们利用GDI函数所产生的一支笔(Pen)或一支刷(Brush)都是所谓的「GDI对象」。但什么又是「核心对象」呢? 你可以说核心对象是系统的一种资源(噢,这说法对GDI 对象也适用),系统对象一旦产生,任何应用程序都可以开启并使用该对象。系统给予核心对象一个计数值(usagecount )做为管理之用。核心对象包括下列数种:
                                                       

前三者用于执行线程的同步化:file-mapping 对象用于内存映射文件(memory  mappingfile),process和thread对象则是本节的主角。这些核心对象的产生方式(也就是我们所使用的API)不同,但都会获得一个handle做为识别;每被使用一次,其对应的计数值就加1。核心对象的结束方式相当一致,调用CloseHandle即可。「process对象」究竟做什么用呢?它并不如你想象中用来「执进程序代码」;不,程序代码的执行是执行线程的工作,「process对象」只是一个数据结构,系统用它来管理进程。



一个进程的诞生与死亡


执行一个程序,必然就产生一个进程(process )。最直接的程序执行方式就是在shell  (如Win95的文件总管或Windows 3. x  的文件管理员)中以鼠标双击某一个可执行文件图标(假设其为App.exe ),执行起来的App 进程其实是shell调用CreateProcess激活的。让我们看看整个流程:


1 . shell调用CreateProcess激活App.exe 。


2 . 系统产生一个「进程核心对象」,计数值为1。


3 . 系统为此进程建立一个4GB  地址空间。


4 . 加载器将必要的码加载到上述地址空间中,包括App.exe   的程序、资料,以及所需的动态联结函数库(DLLs )。加载器如何知道要加载哪些DLLs呢?它们被记录在可执行文件(PE文件格式)的. idatas ection 中。


5 . 系统为此进程建立一个执行线程,称为主执行线程(primary thread )。执行线程才是CPU时间的分配对象。


6 . 系统调用Cruntime函数库的Start up code。


7 . Start up code  调用App程序的WinMain函数。


8 . App 程序开始运作。


9 . 使用者关闭App 主窗口,使WinMain中的消息循环结束掉,于是WinMain结束。


1 0 . 回到Start up code。


1 1 . 回到系统,系统调用ExitProcess结束进程。


可以说,透过这种方式执行起来的所有Windows   程序,都是shell的子进程。本来,母进程与子进程之间可以有某些关系存在,但shell在调用CreateProcess   时已经把母子之间的脐带关系剪断了,因此它们事实上是独立实例。稍后我会提到如何剪断子进程的脐带。



产生子进程


你可以写一个程序,专门用来激活其它的程序。关键就在于你会不会使用CreateProcess 。这个API函数有众多参数:
CreateProcess(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);

第一个参数:lpApplicationName指定可执行档档名。


第二个参数:lpCommandLine指定欲传给新进程的命令列(command line )参数。如果你指定了lpApplicationName,但没有扩展名,系统并不会主动为你加上. EXE扩展名;如果没有指定完整路径,系统就只在目前工作目录中寻找。但如果你指定lpApplicationName为NULL的话,系统会以lpCommandLine的第一个「段落」(我的意思其实是术语中所谓的token)做为可执行档档名;如果这个档名没有指定扩展名,就采用预设的" . EXE"   扩展名;如果没有指定路径,Windows   就依照五个搜寻路径来寻找可执行文件,分别是:

1 . 调用者的可执行文件所在目录

2 . 调用者的目前工作目录

3 . Windows   目录

4 . Windows   System  目录

5 . 环境变量中的pa th  所设定的各目录


第三、四个参数和第五个参数:建立新进程之前,系统必须做出两个核心对象,也就是「进程对象」和「执行线程对象」CreateProcess的第三个参数和第四个参数分别指定这两个核心对象的安全属性。至于第五个参数(TRUE  或FALSE)则用来设定这些安全属性是否要被继承。关于安全属性及其可被继承的性质,碍于本章的定位,我不打算在此介绍。


第六个参数:dwCreationFlags  可以是许多常数的组合,会影响到进程的建立过程。这些常数中比较常用的是CREATE_SUSPENDED ,它会使得子进程产生之后,其主执行线程立刻被暂停执行。


第七个参数lpEnvironment:可以指定进程所使用的环境变量区。通常我们会让子进程继承父进程的环境变量,那么这里要指定NULL。


第八个参数lpCurrentDirectory:用来设定子进程的工作目录与工作磁盘。如果指定NULL,子进程就会使用父进程的工作目录与工作磁盘。


第九个参数lpStartupInfo:是一个指向STARTUPINFO结构的指针。这是一个庞大的结构,可以用来设定窗口的标题、位置与大小,详情请看API使用手册。


最后一个参数是一个指向PROCESS_INFORMAT ION  结构的指针:


typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
   } PROCESS_INFORMATION;


当系统为我们产生「进程对象」和「执行线程对象」,它会把两个对象的handle填入此结构的相关字段中,应用程序可以从这里获得这些handles 。


如果一个进程想结束自己的生命,只要调用:

VOID ExitProcess(UINT fuExitCode);

就可以了。如果进程想结束另一个进程的生命,可以使用:

BOOL TerminateProcess(HANDLE hProcess, UINT fuExitCode);

很显然,只要你有某个进程的handle,就可以结束它的生命。TerminateProcess并不被建议使用,倒不是因为它的权力太大,而是因为一般进程结束时,系统会通知该进程所开启(所使用)的所有DLLs ,但如果你以TerminateProcess结束一个进程,系统不会做这件事,而这恐怕不是你所希望的。



前面我曾说过所谓割断脐带这件事情,只要你把子进程以CloseHandle  关闭,就达到了目的。下面是个例子:


PROCESS_INFORMATION ProcInfo;
BOOL fSuccess;
fSuccess = CreateProcess(...,&ProcInfo);
if (fSuccess) {
		CloseHandle(ProcInfo.hThread);
		CloseHandle(ProcInfo.hProcess);
}


一个执行线程的诞生与死亡


程序代码的执行,是执行线程的工作。当一个进程建立起来,主执行线程也产生。所以每一个Windows程序一开始就有了一个执行线程。我们可以调用Creat eThread 产生额外的执行线程,系统会帮我们完成下列事情:
1 . 配置「执行线程对象」,其handle将成为CreateThread的传回值。
2 . 设定计数值为1。
3 . 配置执行线程的context 。
4 . 保留执行线程的堆栈。
5 . 将context   中的堆栈指针缓存器(SS)和指令指针缓存器(IP )设定妥当。


看看上面的态势,的确可以显示出执行线程是CPU分配时间的单位。所谓工作切换(context switch)其实就是对执行线程的context的切换。程序若欲产生一个新执行线程,调用CreateThread 即可办到:

CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);

第一个参数表示安全属性的设定以及继承,请参考API手册。Windows 95忽略此一参数。第二个参数设定堆栈的大小。第三个参数设定「执行线程函数」名称,而该函数的参数则在这里的第四个参数设定。第五个参数如果是0,表示让执行线程立刻开始执行,如果是CREATE_SUSPENDED   , 则是要求执行线程暂停执行( 那么我们必须调用ResumeThread 才能令其重新开始)。最后一个参数是个指向DWORD的指针,系统会把执行线程的ID放在这里。


上面我所说的「执行线程函数」是什么?让我们看个实例:


VOID ReadTime(VOID);
HANDLE hThread;
DWORD ThreadID;
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ReadTime ,
                           NULL, 0, &ThreadID);
...
//--------------------------------------------------------------------
// thread 函数。
// 不断利用 GetSystemTime  取系统时间,
// 并将结果显示在对话框 _hhwndDlg 的 IDE_TIMER  栏位上。
//--------------------------------------------------------------------
VOID  ReadTime (VOID)
{
char str[50];
SYSTEMTIME st;
    while(1)  {
        GetSystemTime(&st);
        sprintf(str,"%u:%u:%u",  st.wHour, st.wMinute,  st.wSecond);
        SetDlgItemText (_hWndDlg, IDE_TIMER,  str);
        Sleep (1000);  //  延迟一秒。
    }
}

当Creat eT hr ead 成功,系统为我们把一个执行线程该有的东西都准备好。执行线程的主体在哪里呢?就在所谓的执行线程函数。执行线程与执行线程之间,不必考虑控制权释放的问题,因为Win32  操作系统是强制性多任务。


执行线程的结束有两种情况,一种是寿终正寝,一种是未得善终。前者是执行线程函数正常结束退出,那么执行线程也就自然而然终结了。这时候系统会调用ExitT hr ead 做些善后清理工作(其实执行线程中也可以自行调用此函数以结束自己)。但是像上面那个例子,执行线程根本是个无穷循环,如何终结?一者是进程结束(自然也就导至执行线程的结束),二者是别的执行线程强制以T er m inateT hr ead 将它终结掉。不过,T er m inateT hr ead 太过毒辣,非必要还是少用为妙(请参考A P I  手册)。


以_begint hreadex取代CreateThread


别忘了Windows程序除了调用Win32 API,通常也很难避免调用任何一个Cruntime  函数。为了保证多线程情况下的安全,Cruntime 函数库必须为每一个执行线程做一些簿记工作。没有这些工作,Crunt ime  函数库就不知道要为每一个执行线程配置一块新的内存,做为执行线程的区域变量用。因此,CreateThread 有一个名为_beginthreadex的外包函数,负责额外的簿记工作。

请注意函数名称的底线符号。它必须存在,因为这不是个标准的ANSI Cruntime函数。_beginthreadex的参数和CreateThread 的参数其实完全相同,不过其型别已经被「净化」了,不再有Win32型别包装。这原本是为了要让这个函数能够移植到其它操作系统,因为微软希望_beginthreadex能够被实作于其它平台,不需要和Windows有关、不需要包含w indows.h。但实际情况是,你还是得调用CloseHandle以关闭执行线程, 而CloseHandle却是个Win32 API,所以你还是需要包含windows.h、还是和Windows脱离不了关系。微软空有一个好主意,却没能落实它。


把_be gi nt hr eadex  视为Creat eT hr ead 的一个看起来比较有趣的版本,就对了:

unsigned long _beginthreadex(
void *security,
unsigned stack_size,
unsigned (__stdcall *start_address)(void *),
void *arglist,
unsigned initflag,
unsigned* thrdaddr
);

_beginthreadex所传回的unsigned long事实上就是一个Win32  HANDLE,指向新执行线程。换句话说传回值和CreateT hread 相同,但_beginthreadex另外还设立了errno和doserrno 。



执行线程优先权(Priority)

优先权是排程的重要依据。优先权高的执行线程,永远先获得CP U的青睐。当然啦,操作系统会视情况调整各个执行线程的优先权。例如前景执行线程的优先权应该调高一些,背景执行线程的优先权应该调低一些。执行线程的优先权范围从0(最低)到31(最高)。当你产生执行线程,并不是直接以数值指定其优先权,而是采用两个步骤。

第一个步骤是指定「优先权等级(Priority Class)」给进程,

第二步骤是指定「相对优先权」给该进程所拥有的执行线程。图1-7 是优先权等级的描述,其中的代码在Creat eProcess的dwCreationFlags参数中指定。如果你不指定,系统预设给的是NORMAL _PRIORITY_CLASS --  除非父进程是IDL E_PRIORITY_CLASS(那么子进程也会是IDLE_PRIORITY_CLASS)。



■  " idle" 等级只有在CPU时间将被浪费掉时(也就是前一节所说的空闲时间)才执行。此等级最适合于系统监视软件,或屏幕保护软件。


■  " normal" 是预设等级系统可以动态改变优先权,但只限于" normal" 等级。当进程变成前景,执行线程优先权提升为9,当进程变成背景,优先权降低为7。


■ " high"  等级是为了立即反应的需要,例如使用者按下Ctrl +Esc时立刻把工作管理器(task manager )带出场。


■  " realtime"   等级几乎不会被一般应用程序使用。就连系统中控制鼠标、键盘、磁盘状态重新扫描、Ct rl +Alt+Del 等的执行线程都比" realtime"   优先权还低。这种等级使用在「如果不在某个时间范围内被执行的话,资料就要遗失」的情况。这个等级一定得在正确评估之下使用之,如果你把这样的等级指定给一般的(并不会常常被阻塞的)执行线程,多任务环境恐怕会瘫痪,因为这个执行线程有如此高的优先权,其它执行线程再没有机会被执行。


上述四种等级,每一个等级又映射到某一范围的优先权值。IDLE_ 最低,NORMAL _  次之,HIGH _  又次之,REALTIME _  最高。在每一个等级之中,你可以使用SetThr eadPriority设定精确的优先权,并且可以稍高或稍低于该等级的正常值(范围是两个点数)。你可以把SetThreadPriority  想象是一种微调动作。





多线程程序设计实例 (项目mfc12)


我设计了一个M ltiThrd  程序,放在书附盘片的M ltiThrd. 01 子目录中。这个程序一开始产生五个执行线程,优先权分别微调-2 、-1 、0、+1、+2,并且虚悬不执行:

HANDLE _hThread[5];  // global  variable
...
LONG APIENTRY MainWndProc (HWND hWnd, UINT message,
                                UINT wParam, LONG lParam)
{
    DWORD  ThreadID[5];
    static DWORD  ThreadArg[5] = {HIGHEST_THREAD,    // 0x00
                                  ABOVE_AVE_THREAD,  // 0x3F
                                     NORMAL_THREAD,     // 0x7F
                                      BELOW_AVE_THREAD,  // 0xBF
                                      LOWEST_THREAD      // 0xFF
                                      };     // 用来调整四方形顏色
    ...
    for(i=0;  i<5; i++)   // 产生  5  个 threads
        _hThread[i] = CreateThread(NULL,
                                        0,
                                        (LPTHREAD_START_ROUTINE) ThreadProc ,
                                        &ThreadArg[i],
                                        CREATE_SUSPENDED,
                                        &ThreadID[i]);
    //  設定 thread priorities
    SetThreadPriority(_hThread[0], THREAD_PRIORITY_HIGHEST);
    SetThreadPriority(_hThread[1], THREAD_PRIORITY_ABOVE_NORMAL);
   SetThreadPriority(_hThread[2], THREAD_PRIORITY_NORMAL);
   SetThreadPriority(_hThread[3], THREAD_PRIORITY_BELOW_NORMAL);
   SetThreadPriority(_hThread[4], THREAD_PRIORITY_LOWEST);
   ...
}

当使用者按下【Resume Threads 】菜单项目后,五个执行线程如猛虎出柙,同时冲出来。这五个执行线程使用同一个执行线程函数ThreadProc 。我在Thr eadProc中以不断的Rectangle动作表示执行线程的进行。所以我们可以从画面上观察执行线程的进度。我并且设计了两种延迟方式,以利观察。第一种方式是在每一次循环之中使用Sleep (10) ,意思是先睡10 个毫秒,之后再醒来;这段期间,CPU可以给别人使用。第二种方式是以空循环30000次做延迟;空循环期间CPU不能给别人使用(事实上CPU正忙碌于那30000次空转)。


UINT   _uDelayType=NODELAY;  // global variable
...
VOID  ThreadProc (DWORD *ThreadArg)
{
RECT rect;
HDC  hDC;
HANDLE hBrush, hOldBrush;
DWORD dwThreadHits = 0;
int   iThreadNo, i;
   ...
   do
   {
     dwThreadHits++;       //  计数器
      // 画出四方形,代表 thread  的进行
      Rectangle(hDC, *(ThreadArg), rect.bottom-(dwThreadHits/10),
                 *(ThreadArg)+0x40, rect.bottom);
      // 延迟...
     if (_uDelayType == SLEEPDELAY)
          Sleep (10);
     else if (_uDelayType == FORLOOPDELAY)
          for (i=0; i<30000; i++);
     else // _uDelayType == NODELAY)
         {   }
   } while (dwThreadHits <  1000);      //  循环  1000 次
   ...
}

图
1-9 是执行画面。注意,先选择延迟方式(" forloop delay"   或" sleep  delay" ),再按下【Resume Thread】。

如果你选择forloopdelay你会看到执行线程0  (优先权最高)几乎一路冲到底,然后才是执行线程1(优先权次之),然后是执行线程2(优先权再次之)


但如果你选择的sleep delay,所有执行线程不分优先权高低,同时行动。关于执行线程的排程问题,我将在第14 章做更多的讨论。



注意:为什么图中执行线程1尚未完成,执行线程2~4   竟然也有机会偷得一点点CPU 时间呢?这是排程器的巧妙设计,动态调整执行线程的优先权。是啊,总不能让优先权低的执行线程直到天荒地老,没有一点点获得。关于执行线程排程问题,第14章有更多的讨论


下图是以Process Viewer (Visual  C++  5. 0  所附工具)观察执行结果。图上方出现目前所有的进程,点选,果然在窗口下方出现六个执行线程,其中包括主执行线程(优先权已被调整为10)。




[Win32] 服务程序开发(1)基本概念和服务程序的框架 一。服务程序基本概念 二。服务程序的编写框架 1。StartServiceCtrlDispatcher函数 BOOL WINAPI StartServiceCtrlDispatcher( _In_ const SERVICE_TABLE_ENTRY *lpServiceTable ); 函数功能:连接到服务控制管理器 参数:一个SERVICE_TABLE_ENTRY结构的指针 阅读详情

相关推荐

Hey,别搞错了Win32项目和Win32控制台程序

你是否和我一样呢,写了很多控制台程序,特别熟悉cout、main函数等等。 但是你同样也听说过win32 api !!当在你的控制台应用程序中写下某个api函数的时候,你也许会窃喜api函数带来的方便。但是当你编译代码的时候,编译器会无情的通知你“不能识别XX标识符”。 所以,从现在开始我的blog.你兴致勃勃的打开vs2010,选择“文件”-》“新建”-》“项目”,你会看到如下: 正如你

一蓑烟雨任平生 也无风雨也无晴 1万+

CMake系列讲解(入门篇)1.4 基础命令CMake-add_executable()

CMake学习,Cmake之add_executable指令 add_executable命令主要功能是生成可执行文件目标,本文带你读懂CMake-add_executable()

在下马农的碎碎念 9485

win32控制台、win32工程、MFC工程的区别

空项目   控制台 win 32 mfc 问2个问题即可。 1.控制台 VS win32 mfc? 嗯,有不有脸? 有脸,选win32或mfc。没有脸,选控制台。 (脸就是窗口,就是window......s) (这个有脸) (这个没脸) 控制台与win32 mfc:交互方式的不同,前者是CML(命令模式),后者是GUI(用户界面)。 ...

qq_41786318的博客 3664

Windows环境CMake学习笔记(二)

第一篇学习笔记通过构建编译最简单的cpp项目了解CMake的使用方法。本篇文章将深入分析如下CMakeLists.txt涉及到的3个命令。CMake 支持大写、小写和大小写混合命令,参数之间使用空格或跨行分隔。本篇文章命令参照官方示例使用小写。

weixin_36114953的博客 816

win32汇编环境,窗口程序使用树形视图示例一

树形视图控件Treeview,就是那种点击后,会展开的控件,类似于文件夹列表。这里展示了最基本的应用,纯文本模式的展开树形视图,同时获得选中项的内容

一品人家的博客 1188

win32桌面应用程序初始代码解析(一)

新建一个C++win32桌面应用程序项目假设我们叫hellowindows,我们可以在hellowindows.cpp里看到会有一大段初始的代码: #include "stdafx.h" #include "hellowindows.h" #define MAX_LOADSTRING 100 // 全局变量: HINSTANCE hInst; ...

T-T 1955

CMake中的变量: 控制生成过程的变量

CMake中的变量: 控制生成过程的变量

qq_30150579的博客 1581

win32汇编环境,窗口程序中创建托盘示例一

托盘,就是电脑桌面右下角那个角落里的图标,这里展示基本的应用方法。

一品人家的博客 1004

CMake常用命令(五)add_executable命令 生成可执行文件

使用指定的源文件来生成目标可执行文件。这里的目标可执行文件分为三类:==普通可执行目标文件==、==导入可执行目标文件==、==别名可执行目标文件==。分别对应上面的三种命令格式。

qq_26849933的博客 1万+

win32项目和win32控制台程序的区别

回答1: 1、win32应用程序是指窗口类的,像QQ之类的,有图形化界面,而控制台是指运行在黑框下的,像cmd之类的; 2、win32控制台项目指在32位Windows命令提示符(即所谓的dos)环境下运行的应用程序... win32项目指运行于32位Windows操作系统中的应用程序...包括win32控制台项目... 3、最大的区别当然是程序入口了,console的入口是main,Windows的入口时WinMain。而程序的入口点是可以重新设置的。我们可以在console应用程序中设置...

digitalkee的博客 3897

高通Camera PDAF调试实战:从Type2到Type3的避坑指南与Log分析技巧

本文深入解析高通Camera平台PDAF调试实战,重点对比Type2与Type3的核心差异,提供详细的配置迁移方案和Log分析技巧。针对常见兼容性问题如对焦速度变慢、低光失效等,给出具体解决方案,并分享性能优化案例与高级调试工具链,帮助开发者快速提升PDAF调试效率。

weixin_29164185的博客 413

Win32 行程通讯的观念与技术

Win32 行程通讯的观念与技术    窗子提供的永远只是局部的风景。身为窗子的制造者以及使用者的我们不可能不明白这个道理;对於窗子的使用者日益挑剔的品味,窗子的制造者所能提供的不仅止於窗子的大小,往往是窗子的数量。的确,探出窗去看得将更多一些,外头天空地宽朗朗白日,别的窗子也许正有我们想要的风景。  这篇文章谈的是 Interprocess Communication (IPC),我将与你分

zou5655的专栏 969

合宙Air780EP速连OneNET:AT+MQTT应用指南

本文详细讲解了合宙Air780EP使用AT指令通过MQTT接入OneNET的过程。

2401_82812461的博客 1390

新版Win32高级编程教程-学习笔记01:应用程序分类

VS创建的应用程序分类有哪些呢?

深耕CV 1712

大连海事大学课设题目/基于LM331的频率电压转换器

部分基于LM331的频压转换的实物连接问题

2301_76649592的博客 3227

OFDM时频脉冲形状与子载波正交性的理解

从多载波调制的角度来理解子载波间正交,首先要明确OFDM默认的脉冲成型是时域矩形窗,时域上每个子载波 ej2πfnte^{j2πf_nt}ej2πfn​t 都被一个矩形窗脉冲gT(t)g_T(t)gT​(t)成型。其次,OFDM子载波间隔 等于 OFDM符号周期的倒数,这是子载波正交的条件。 ​ 根据时频脉冲形状,可以从时域上理解如左图(注意是矩形脉冲 乘 复指数),从频域上理解如右图(sinc脉冲 卷 频点)。 ​

weixin_43803202的博客 2215

Win32学习笔记 第一章 开始

Win32学习笔记作者: 姜学哲(netsail0@163.net)教材: Windows程序设计(第五版)北京大学出版社 [美]Charles Petzold 著 北京博彦科技发展有限公司 译  ¥:160参考资料: Windows 应用程序设计原理_方法_技术(因为是PDF格式的EBOOK,作者等不详) 新编Windows API 参考大全  电子工业出版社  ¥:98 C++ Pri

villager的专栏 1315

MIT Cheetah Learning (一):State Estimate 从理论到实践:EKF融合IMU与编码器的状态估计全解析

本文深入解析了MIT Cheetah机器人中基于Extended Kalman Filter的状态估计技术,详细探讨了如何融合IMU与编码器数据以实现精准的State Estimate。通过四元数建模和传感器误差分析,揭示了EKF在复杂地形中的工程实践价值,为四足机器人的实时状态估计提供了完整解决方案。

weixin_30292843的博客 305

C++ Win32程序编写入门

Win32++由用于创建窗口应用程序的C++库组成。它是MFC的免费替代品,可用于更广泛的编译器,包括Borland、Microsoft和MinGW的编译器。该库可用于创建各种程序,包括简单的窗口、对话框、属性表以及框架和MDI框架应用程序。教程部分提供了有关如何使用这些类构建自己的Win32应用程序的信息。基于框架的应用程序通常有一个通用框架窗口,该窗口可能有例如菜单、工具栏和状态栏,并且在客户端区域上有一个单独的窗口(有时称为视图窗口)。您熟悉的许多windows应用程序都可能是基于框架的应用程序。

火柴盒 4880

【提示学习代码】CoOp代码详读

CoOp是对CLIP的改进工作,主要是对prompt进行学习从而不用来手动设置prompt。自定义CLIP模型:上述两个模块结合而成的CLIP。文本编码器,接收文本的输入,并输出相应的编码表示。将图像和文本进行编码并计算它们之间的相似性。permute后变成了(4,2,3)Prompt学习器,用于学习生成提示。

weixin_51293984的博客 5816

[cshell]个性化配置你的cshrc/tcshrc

本文介绍了csh/tcsh shell的配置优化方法,主要包括:1. 配置文件管理(全局和个人配置);2. 按键绑定设置(解决远程登录按键问题);3. 提示符个性化(包含颜色配置方案);4. 环境变量设置(如LSCOLORS、CLICOLOR等);5. 安全防护措施(防误删、防覆盖等);6. 实用alias命令集(简化操作、提高效率)。文章提供了详细的配置示例,并建议通过man tcsh获取更多帮助信息。这些配置能显著提升shell使用体验和工作效率。

元直的博客 6114

WIN32 预定义宏WIN32,_WIN32,_WIN64介绍使用

1.WIN32宏 在 Win32 配置下,WIN32 在“项目属性-C/C++-预处理器-预处理器定义”里声明了,而在 x64 配置下,这个常量并不在项目预定义列表中。这是否说明可以根据 WIN32 来判断是否在 x64 平台呢?不。在 Windows SDK 的 minwindef.h 下第 37 行有如下定义: #ifndef WIN32 #define WIN32 #endif ...

kevin的博客 2万+

SimpleITK使用——1. 进行Resample/Resize操作

文章目录1. 变换到一个新的spacing1.1 使用Resample()的方法1.1.1 完整代码1.1.2 转换前后图像1.2 使用ResampleImageFilter()的方法2. 已知原图和变换后目标图像的大小2.1 代码2.2 转换后图像3. 相关内容说明3.0 体素和spacing3.1 Resample() 函数说明3.2 itk::simple::ResampleImageFilter函数说明 1. 变换到一个新的spacing 1.1 使用Resample()的方法 1.1.1

啷个哩个啷 8081

Win32应用程序的基本结构

0 引言 Win32 API是用于创建 Windows 应用程序的应用程序编程接口。通常情况下,一个Win32应用包含一下几个部分: 1) 应用程序入口; 2) 注册窗口类; 3) 创建窗口; 4) 显示窗口; 5) 更新窗口 6) 消息循环; 7) 派发消息; 1 应用程序入口 Win32应用程序以WinMain函数作为程...

weixin_30393907的博客 850

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

Win32控制台应用程序,Win32项目,MFC应用程序的区别与联系

Win32控制台应用程序,Win32项目,MFC应用程序的区别与联系 Win32控制台程序 初始化代码模板以Main为程序入口,默认情况下,只链接C++运行时库和一些核心的Win32库,链接器subsystem参数为console类型,所以编译出来的程序会有黑色的控制台窗口作为呈现标注输入(stdin)和标准输出(stdou) Win32项目 初始代码实现一个简单的windows窗口,以W...

qq_40982049的博客 1460

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

Win32控制台程序是什么

刚接触Windows那一套,大多数概念都还没建立起来,整理了一下网上对“Win32控制台程序”的理解,谢谢各位网友了。 win32控制台项目指在32位Windows命令提示符(即所谓的dos)环境下运行的应用程序... win32项目指运行于32位Windows操作系统中的应用程序...包括win32控制台项目... 1、Win32项目包括:Win32 应用程序、控制台应用程序、DLL 或其

学习专栏 3万+

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

下一篇: .C与.CPP 在编译时的区别
imsaws
博客等级 码龄18年 9粉丝 25原创
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值