首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
80 阅读
2
nvim番外之将配置的插件管理器更新为lazy
58 阅读
3
2018总结与2019规划
54 阅读
4
PDF标准详解(五)——图形状态
33 阅读
5
为 MariaDB 配置远程访问权限
30 阅读
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
Java
emacs
linux
文本编辑器
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
308
篇文章
累计收到
27
条评论
首页
栏目
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
页面
归档
友情链接
关于
搜索到
190
篇与
的结果
2016-01-03
地址、指针与引用
计算机本身是不认识程序中给的变量名,不管我们以何种方式给变量命名,最终都会转化为相应的地址,编译器会生成一些符号常量并且与对应的地址相关联,以达到访问变量的目的。 变量是在内存中用来存储数据以供程序使用,变量主要有两个部分构成:变量名、变量类型,其中变量名对应了一块具体的内存地址,而变量类型则表明该如何翻译内存中存储的二级制数。我们知道不同的类型翻译为二进制的值不同,比如整型是直接通过数学转化、浮点数是采用IEEE的方法、字符则根据ASCII码转化,同样变量类型决定了变量所占的内存大小,以及如何在二进制和变量所表达的真正意义之间转化。而指针变量也是一个变量,在内存中也占空间,不过比较特殊的是它存储的是其他变量的地址。在32位的机器中,每个进程能访问4GB的内存地址空间,所以程序中的地址采用32位二进制数表示,也就是一个整型变量的长度,地址值一般没有负数所以准确的说指针变量的类型应该是unsigned int 即每个指针变量占4个字节。还记得在定义结构体中可以使用该结构体的指针作为成员,但是不能使用该结构的实例作为成员吗?这是因为编译器需要根据各个成员变量的大小分配相关的内存,用该结构体的实例作为成员时,该结构体根本没有定义完整,编译器是不会知道该如何分配内存的,而任何类型的指针都只占4个字节,编译器自然知道如何分配内存。我们在书写指针变量时给定的类型是它所指向的变量的类型,这个类型决定了如何翻译所对应内存中的值,以及该访问多少个字节的内存。对指针的间接访问会先先取出值,访问到对应的内存,再根据指针所指向的变量的类型,翻译成对应的值。一般指针只能指向对应类型的变量,比如int类型的指针只能指向int型的变量,而有一种指针变量可以指向所有类型的变量,它就是void类型的指针变量,但是由于这种类型的变量没有指定它所对应的变量的类型,所以即使有了对应的地址,它也不知道该取多大内存的数据,以及如何解释这些数据,所以这种类型的指针不支持间接访问,下面是一个间接访问的例子:int main() { int nValue = 10; float fValue = 10.0f; char cValue = 'C'; int *pnValue = &nValue; float *pfValue = &fValue; char *pcValue = &cValue; printf("pnValue = %x, *pnValue = %d\n", pnValue, *pnValue); printf("pfValue = %x, *pfValue = %f\n", pfValue, *pfValue); printf("pcValue = %x, *pcValue = %c\n", pcValue, *pcValue); return 0; }下面是它对应的反汇编代码(部分):10: int nValue = 10; 00401268 mov dword ptr [ebp-4],0Ah 11: float fValue = 10.0f; 0040126F mov dword ptr [ebp-8],41200000h 12: char cValue = 'C'; 00401276 mov byte ptr [ebp-0Ch],43h 13: int *pnValue = &nValue; 0040127A lea eax,[ebp-4] 0040127D mov dword ptr [ebp-10h],eax 14: float *pfValue = &fValue; 00401280 lea ecx,[ebp-8] 00401283 mov dword ptr [ebp-14h],ecx 15: char *pcValue = &cValue; 00401286 lea edx,[ebp-0Ch] 00401289 mov dword ptr [ebp-18h],edx 16: printf("pnValue = %x, *pnValue = %d\n", pnValue, *pnValue); 0040128C mov eax,dword ptr [ebp-10h] 0040128F mov ecx,dword ptr [eax] 00401291 push ecx 00401292 mov edx,dword ptr [ebp-10h] 00401295 push edx 00401296 push offset string "pnValue = %x, *pnValue = %d\n" (00432064) 0040129B call printf (00401580) 004012A0 add esp,0Ch从上面的汇编代码可以看到指针变量会占内存空间,它们的地址分别是:[ebp - 10h] 、 [ebp - 14h]、 [ebp - 18h],在给指针变量赋值时首先将变量的地址赋值给临时寄存器,然后将寄存器的值赋值给指针变量,而通过间接访问时也经过了一个临时寄存器,先将指针变量的值赋值给临时寄存器(mov eax,dword ptr [ebp-10h])然后通过这个临时寄存器访问变量的地址空间,得到变量值( mov ecx,dword ptr [eax]),由于间接访问进过了这几步,所以在效率上是比不上直接使用变量。下面是对char型变量的间接访问:004012BF mov edx,dword ptr [ebp-18h] 004012C2 movsx eax,byte ptr [edx] 004012C5 push eax首先也是将指针变量的值取出来,放到寄存器中,然后根据寄存器寻址找到变量对应的地址,访问变量。其中”bye ptr“表示只操作该地址中的一个字节。对于地址我们可以进行加法和减法操作,地址的加法主要用于向下寻址,一般用于数组等占用连续内存空间的数据结构,一般是地址加上一个数值,表示向后偏移一定的单位,指针同样也有这样的操作,但是与地址值不同的是指针每加一个单位,表示向后偏移一个元素,而地址值加1则就是在原来的基础上加上一。指针偏移是根据其所指向的变量类型来决定的,比如有下面的程序:int main(int argc, char* argv[]) { char szBuf[5] = {0x01, 0x23, 0x45, 0x67, 0x89}; int *pInt = (int*)szBuf; short *pShort = (short*)szBuf; char *pChar = szBuf; pInt += 1; pShort += 1; pChar += 1; return 0; }它的汇编代码如下:9: char szBuf[5] = {0x01, 0x23, 0x45, 0x67, 0x89}; 00401028 mov byte ptr [ebp-8],1 0040102C mov byte ptr [ebp-7],23h 00401030 mov byte ptr [ebp-6],45h 00401034 mov byte ptr [ebp-5],67h 00401038 mov byte ptr [ebp-4],89h 10: int *pInt = (int*)szBuf; 0040103C lea eax,[ebp-8] 0040103F mov dword ptr [ebp-0Ch],eax 11: short *pShort = (short*)szBuf; 00401042 lea ecx,[ebp-8] 00401045 mov dword ptr [ebp-10h],ecx 12: char *pChar = szBuf; 00401048 lea edx,[ebp-8] 0040104B mov dword ptr [ebp-14h],edx 13: 14: pInt += 1; 0040104E mov eax,dword ptr [ebp-0Ch] 00401051 add eax,4 00401054 mov dword ptr [ebp-0Ch],eax 15: pShort += 1; 00401057 mov ecx,dword ptr [ebp-10h] 0040105A add ecx,2 0040105D mov dword ptr [ebp-10h],ecx 16: pChar += 1; 00401060 mov edx,dword ptr [ebp-14h] 00401063 add edx,1 00401066 mov dword ptr [ebp-14h],edx根据其汇编代码可以看出,对于int型的指针,每加1个会向后偏移4个字节,short会偏移2个字节,char型的会偏移1个,所以根据以上的内容,可以得出一个公式:TYPE P p + n = p + sizeof(TYPE) n根据上面的加法公式我们可以推导出两个指针的减法公式,TYPE p1, TYPE p2: p2 - p1 = ((int)p2 - (int)p1) / sizeof(TYPE),两个指针相减得到的结果是两个指针之间拥有元素的个数。只有同类型的指针之间才可以相减。而指针的乘除法则没有意义,地址之间的乘除法也没有意义。引用是在C++中提出的,是变量的一个别名,提出引用主要是希望减少指针的使用,引用于指针在一个函数中想上述例子中那样使用并没有太大的意义,大量使用它们是在函数中,作为参数传递,不仅可以节省效率,同时也可以传递一段缓冲,作为输出参数来使用。这大大提升了程序的效率以及灵活性。但是在一些新手程序员看来指针无疑是噩梦般的存在,所以C++引入了引用,希望代替指针。在一般的C++书中都说引用是变量的一个别名是不占内存的,但是我通过查看反汇编代码发现引用并不是向书上说的那样,下面是一段程序及它的反汇编代码:int nValue = 10; int &rValue = nValue; printf("%d\n", rValue);10: int nValue = 10; 00401268 mov dword ptr [ebp-4],0Ah 11: int &rValue = nValue; 0040126F lea eax,[ebp-4] 00401272 mov dword ptr [ebp-8],eax 12: printf("%d\n", rValue); 00401275 mov ecx,dword ptr [ebp-8] 00401278 mov edx,dword ptr [ecx] 0040127A push edx 0040127B push offset string "%d\n" (0042e01c) 00401280 call printf (00401520)从汇编代码中可以看到,在定义引用并为它赋值的过程中,编译器其实是将变量的地址赋值给了一个新的变量,这个变量的地址是[ebp - 8h],在调用printf函数的时候,编译器将地址取出并将它压到函数栈中。下面是将引用改为指针的情况:10: int nValue = 10; 00401268 mov dword ptr [ebp-4],0Ah 11: int *pValue = &nValue; 0040126F lea eax,[ebp-4] 00401272 mov dword ptr [ebp-8],eax 12: printf("%d\n", *pValue); 00401275 mov ecx,dword ptr [ebp-8] 00401278 mov edx,dword ptr [ecx] 0040127A push edx 0040127B push offset string "%d\n" (0042e01c) 00401280 call printf (00401520)两种情况的汇编代码完全一样,也就是说引用其实就是指针,编译器将其包装了一下,使它的行为变得和使用变量相同,而且在语法层面上做了一个限制,引用在定义的时候必须初始化,且初始化完成后就不能指向其他变量,这个行为与常指针相同。
2016年01月03日
3 阅读
0 评论
0 点赞
2015-12-27
C/C++中整数与浮点数在内存中的表示方式
在C/C++中数字类型主要有整数与浮点数两种类型,在32位机器中整型占4字节,浮点数分为float,double两种类型,其中float占4字节,而double占8字节。下面来说明它们在内存中的具体表现形式:整型整型变量占4字节,在计算机中都是用二进制表示,整型有无符号和有符号两种形式。无符号变量在定义时只需要在相应类型名前加上unsigned 无符号整型变量用32位的二进制数字表示,在与十进制进行转化时只需要知道计算规则即可轻松转化。需要注意的是在计算机中一般使用主机字节序,即采用“高高低低的方式”,数字高位在高地址位,低位在低地址位,例如我们有一个整数0x10203040那么它在内存中存储的格式为:04 03 02 01。有符号数将最高位表示为符号位,0为正数,1为负数其余位都表示具体的数值,对于负数采用的是补码的方式,补码的规则是用0x100000000减去这个数的绝对值,也可以简单的几位将这个数的绝对值取反加1,这样做是为了方便将减法转化为加法,在数学中两个互为相反数的和为0,比如现在有一个负数数x,那么这个x + |x| = 0这个x的绝对值是一个正数,但是用二级制表示的两个数相加不会等于0,而计算机对于溢出采用的是简单的将溢出位丢弃,所以令x + |x| = 0x100000000,这个最高位1,已经溢出,所以这个结果用四字节保存结果肯定会是0,所以最终得到的x = 0x100000000 - |x|。浮点数:早期的小数表示采用的固定小数点的方式,比如规定在32位二级制数字当中,哪几位表示整数部分,其余的表示小数部分,这样表示的数据范围有限,后来采用的是小数点浮动变化的表示方式,也就是所谓的浮点数。浮点数采用的是IEEE的表示方式,最高位表示符号位,在剩余的31位中,从左往右8位表示的是科学计数法的指数部分,其余的表示整数部分。例如我们将12.25f化为浮点数的表示方式:首先将它化为二进制表示1100.01,利用科学计数法可以表述为:1.10001 * 2^3分解出各个部分:指数部分3 + 127= 011 + 0111111、尾数数部分:10001需要注意的是:因为用科学计数法来表示的话,最高位肯定为1所以这个1不会被表示出来指数部分也有正负之分,最高位为1表示正指数,为0表示负指数,所以算出来指数部分后需要加上127进行转化。将这个转化为对应的32位二级制,尾数部分从31位开始填写,不足部分补0即:0 | 10000010 | 10001 |000000000000000000,隔开的位置分别为符号位、指数位,尾数位。因为有的浮点数没有办法完全化为二进制数,会产生一个无限值,编译器会舍弃一部分内容,也就说只能表示一个近似的数,所以在比较浮点数是否为0的时候不要用==而应该用近似表示,允许一定的误差,比如下面的代码:float fTemp = 0.0001f if(fFloat >= -fTemp && fFloat <= fTemp) { //这个是比较fFloat为0 }double类型的浮点数的编码方式与float相同,只是位数不同。double用11位表示指数部分,其余的表示尾数部分。浮点数的计算在CPU中有专门的浮点数寄存器,和对应的计算指令,在效率上比整型数据的低。在写程序的时候,我们利用变量名来进行变量的识别,但是计算机根本不认识这些变量名,计算机中采用的是直接使用地址的方式找到对应的变量,同时为了能准确找到对应的变量,编译器会生成一个结构专门用于保存变量的标识名与对应的地址,这个标识名不是我们定义的变量名,而是在此基础上添加了一些符号,如下面的例子:extern int nTemp; int main() { cout<<nTemp<<endl; }我们申明一个变量,然后在不定义它的情况下,直接使用,这个时候编译器会报错,表示找不到这个变量,报错的截图如下:我们可以看到编译器为这个变量准备的名称并不是我们所定义的nTemp,而是添加了其他标示。在声明变量的时候编译器会为它准备一个标示名称,在定义时会给它一个对应的内存地址,以后在访问这个标示的时候编译器直接去它对应的内存位置去寻找它,下面我们添加这个变量的定义代码:extern int nTemp;int nTemp = 0;int main(){cout<<nTemp<<endl; return 0;}我们查看对应的汇编代码:11: ;int nTemp = 0;00401798 mov dword ptr [ebp-4],012: ;cout<<nTemp<<endl;我们可以看到在为这个变量初始化的时候编译器是直接找到对应的地址[ebp - 4],没有出现相关的变量名,所以说我们定义的变量名只是为了程序员能够识别,而计算机是直接采用寄存器寻址的方式来取用变量。在编译器中同时也看不到与变量类型相关的代码,编译器在使用变量是只关心它的位置,存储的值,以及如何将其中的二进制翻译为对应的内容,代码如下:int main(){int nTemp = 0x00010101; float *pFloat = (float*)&nTemp; char *pChar = (char*)&nTemp; cout<<nTemp<<endl; cout<<*pFloat<<endl; cout<<pChar<<endl; return 0;}结果如下:从这可以看出同一块内存因为编译器根据类型将它翻译为不同的内容,所展现的内容不同。
2015年12月27日
2 阅读
0 评论
0 点赞
2015-12-22
windows API实现用户选择文件路径的对话框
在编写应用程序时,有时需要用户选择某个文件,以供应用程序使用,比如在某些管理程序中需要打开某一个进程,这个时候需要弹出一个对话框来将文件路径以树形图的形式表示出来,以图形化的方式供用户选择文件路径,而不是需要用户自己输入文件路径。在MFC中能够弹出对话框供用户选择文件路径的类是CFileDialog,但是这个类的主要问题是当用户选择文件路径后,会打开相关的文件,与我们的要求不符,在Windows平台下有两个函数SHBrowseForFolder、SHGetPathFromIDList。这两个函数的说明如下:LPITEMIDLIST WINAPI SHBrowseForFolder( LPBROWSEINFO lpbi );该函数的主要作用是弹出一个对话框,便于用户选择文件的路径,传递的参数是一个LPBROWSEINFO的结构体,这个结构体的原型如下:typedef struct _browseinfo { HWND hwndOwner;//该对话框的属主窗口句柄 LPCITEMIDLIST pidlRoot;//对话框中显示的最上层目录 LPTSTR pszDisplayName;//指向一个缓冲区,用于返回用户选择的文件名 LPCTSTR lpszTitle;//文件对话框的标题 UINT ulFlags;//文件对话框相关标志 BFFCALLBACK lpfn;//文件对话框对应的回调函数的地址 LPARAM lParam;//附加参数 int iImage;//返回用户选中的图片的索引 } BROWSEINFO, *PBROWSEINFO, *LPBROWSEINFO;其中最主要的参数是pszDisplayName,这个参数指向一个缓冲区,用于存储用户选择的文件名(只是文件名不包括具体的路径);lpszTitle这个参数表示的是对话框的具体名称,这两个参数有一个没有给则会造成程序的错误,主要是对话框不能出来。另外的是标志的变量,一般使用的是BIF_BROWSEINCLUDEFILES(允许用户选择文件)、BIF_RETURNONLYFSDIRS(只能选择目录,不能选择文件)在用户选择了相关的文件并点击对话框中的确定时,会返回一个LPITEMIDLIST的指针,这个结构表示的是文件系统的相关信息,接下来就是利用函数SHGetPathFromIDList来真正获取用户选择的文件路径,该函数的原型如下:WINSHELLAPI BOOL WINAPI SHGetPathFromIDList( LPCITEMIDLIST pidl, LPSTR pszPath );该函数主要利用pidl对应的文件系统的相关信息,通过第二个参数返回文件的路径,下面是具体的例子:#include <windows.h> #include <Shlobj.h> #include <tchar.h> #include <Commctrl.h> #pragma comment(lib, "comctl32.lib") int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { LPITEMIDLIST pil = NULL; INITCOMMONCONTROLSEX InitCtrls = {0}; TCHAR szBuf[4096] = {0}; BROWSEINFO bi = {0}; bi.hwndOwner = NULL; bi.iImage = 0; bi.lParam = NULL; bi.lpfn = NULL; bi.lpszTitle = _T("请选择文件路径"); bi.pszDisplayName = szBuf; bi.ulFlags = BIF_BROWSEINCLUDEFILES; InitCommonControlsEx(&InitCtrls);//在调用函数SHBrowseForFolder之前需要调用该函数初始化相关环境 pil = SHBrowseForFolder(&bi); if (NULL != pil)//若函数执行成功,并且用户选择问件路径并点击确定 { SHGetPathFromIDList(pil, szBuf);//获取用户选择的文件路径 wprintf_s(_T("%s"), szBuf); } return 0; }
2015年12月22日
9 阅读
0 评论
0 点赞
2015-11-04
Win32 API 三态按钮的实现
Windows平台提供了丰富的控件,但是在使用中我们不会使用它提供的默认风格,有时候需要对控件进行改写,让它展现出更友好的一面,这次主要是说明三态按钮的实现。三态按钮指的是按钮在鼠标移到按钮上时显示一种状态,鼠标在按下时展现一种状态,在鼠标移开时又展现出另外一种状态,总共三种。当然鼠标按下和移出按钮展示的状态系统自己提供的有,这个时候在处理这两种状态只需要贴相应的图片就行了,三态按钮的实现关键在于如何判断鼠标已经移动到按钮上以及鼠标移出按钮,然后根据鼠标的位置将按钮做相应的调整。判断鼠标在按钮的相应位置,系统提供了一个函数_TrackMouseEvent用户处理鼠标移出、移入按钮。函数原型如下:BOOL _TrackMouseEvent( LPTRACKMOUSEEVENT lpEventTrack );函数需要传入一个TRACKMOUSEEVENT类型的指针,该结构的原型如下:typedef struct tagTRACKMOUSEEVENT { 2 DWORD cbSize;//该结构体所占空间大小 3 DWORD dwFlags;//指定服务的请求(指定它需要侦听的事件),这次主要用到的是TME_HOVER和TME_LEAVE(侦听鼠标移开和移入事件) 4 HWND hwndTrack;//指定我们需要侦听的控件的句柄 5 DWORD dwHoverTime;//HOVER消耗的时间,可以用系统提供的一个常量HOVER_DEFAULT由系统默认给出,也可以自己填写,单位是毫秒 6 } TRACKMOUSEEVENT, *LPTRACKMOUSEEVENT;在使用该函数时需要包含头文件commctrl.h和lib文件comctl32.lib解决了鼠标行为的检测之后,就是针对不同的鼠标行为重绘相应的按钮。重绘按钮需要在消息WM_DRAWITEM中,这个消息的处理是在相应控件的父窗口中实现的,而在一般情况下父窗口不会收到该消息,需要我们手工指定控件资源的属性为的OWNERDRAW为真,或者在创建相应的按钮窗口时将样式设置为BS_OWNERDRAW 。设置完成后就可以在对应的父窗口处理函数中接收并处理WM_DRAWITEM,在该消息中重绘按钮该消息中主要使用的参数是lpParam它里面包含的是一个指向DRAWITEMSTRUCT的结构体:typedef struct tagDRAWITEMSTRUCT { UINT CtlType; //控件类型 UINT CtlID; //控件ID UINT itemID; //子菜单项的ID主要用于菜单 UINT itemAction; //控件发出的动作,如ODA_SELECT表示控件被选中 UINT itemState; //控件状态,这次需要用到的状态为ODS_SELECTED表示按钮被按下 HWND hwndItem; //控件句柄 HDC hDC; RECT rcItem;//控件的矩形区域 ULONG_PTR itemData; } DRAWITEMSTRUCT;该结构体中的一些成员需要根据控件类型赋值,同时结构体中的itemAction、itemState是可以由多个值通过位或组成在判断是否具有某种状态时需要使用位与运算而绘制控件时我们可以使用函数DrawFrameControl,该函数可以根据指定的控件类型、控件所处的状态来绘制控件的样式,绘制出来的任然是系统的之前的标准样式,处理WM_DRAWITEN消息的具体代码如下:LPDRAWITEMSTRUCT lpdis = (LPDRAWITEMSTRUCT)lParam; char szBuf[50]; GetWindowText(lpdis->hwndItem,szBuf,50); if (ODT_BUTTON ==lpdis->CtlType)<BR>{ UINTuState = DFCS_BUTTONPUSH; if(lpdis->itemState & ODS_SELECTED) { uState |= DFCS_PUSHED; } DrawFrameControl(lpdis->hDC,&(lpdis->rcItem),DFC_BUTTON,uState); SetTextColor(lpdis->hDC,RGB(255,0,0)); DrawText(lpdis->hDC,szBuf,strlen(szBuf) + 1,&(lpdis->rcItem),DT_CENTER | DT_VCENTER | DT_SINGLELINE); }函数_TrackMouseEvent根据其检测的鼠标状态不同可以返回不同的消息,这次主要用的是WM_MOUSEHOVER(表示鼠标移动到按钮上)、WM_MOUSELEAVE(鼠标移出按钮),还需要注意的是这个函数每次检测完成返回后不会再次检测,需要我们自己主动调用函数检测鼠标状态,由于要多次调用,而每次调用都需要初始化所需要的结构体指针,所以我们封装一个函数专门用于调用_TrackMouseEvent:void Track(HWND hWnd) { TRACKMOUSEEVENT tme; tme.cbSize =sizeof(TRACKMOUSEEVENT); tme.dwFlags = TME_HOVER | TME_LEAVE; tme.dwHoverTime = 10; tme.hwndTrack = hWnd; _TrackMouseEvent(&tme); }消息WM_MOUSEHOVER和消息WM_MOUSELEAVE的处理是在对应的窗口过程中处理的,而按钮的窗口过程由系统提供我们并不知道,所以只有使用子类化的方法在我们的窗口过程中处理这两个消息。在按钮创建后立马要检测鼠标所以可以按钮对应的父窗口完成创建后子类化,对于窗口可以在它的WM_CREATE消息中处理,对于对话框可以在WM_INITDIALOG消息中处理,子类化调用函数SetWindowLong:g_OldProc = (LRESULT*)SetWindowLong(GetDlgItem(hDlg,IDC_BUTTON1),GWL_WNDPROC,(LONG)BtnProc); return0; 在新的窗口过程中处理消息,完成三态按钮:switch(uMsg) { caseWM_MOUSEMOVE: Track(hBtn);//当鼠标移动时检测 break; caseWM_MOUSEHOVER: { charszBuf[50]; RECT rtBtn;<BR> GetClientRect(hBtn,&rtBtn); HDChDc = GetDC(hBtn); DrawFrameControl(hDc,&(rtBtn),DFC_BUTTON,DFCS_BUTTONPUSH); HBRUSHhBr = CreateSolidBrush(RGB(255,255,255)); FillRect(hDc,&rtBtn,hBr); GetWindowText(hBtn,szBuf,50); SetBkMode(hDc,TRANSPARENT); DrawText(hDc,szBuf,strlen(szBuf),&rtBtn,DT_CENTER | DT_VCENTER | DT_SINGLELINE); ReleaseDC(hBtn,hDc); } break; caseWM_MOUSELEAVE: { charszBuf[50]; RECT rtBtn; GetClientRect(hBtn,&rtBtn); HDChDc = GetDC(hBtn); DrawFrameControl(hDc,&(rtBtn),DFC_BUTTON,DFCS_BUTTONPUSH); GetWindowText(hBtn,szBuf,50); SetBkMode(hDc,TRANSPARENT);//设置字体背景为透明 DrawText(hDc,szBuf,strlen(szBuf),&rtBtn,DT_CENTER | DT_VCENTER | DT_SINGLELINE); ReleaseDC(hBtn,hDc); } break; default: returnCallWindowProc((WNDPROC)g_OldProc,hBtn,uMsg,wParam, lParam);//在处理完我们感兴趣的消息后一定要记得将按钮的窗口过程还原 } return0;到这个地方为止,已经实现了三态按钮的基本样式,通过检测鼠标的位置设置按钮样式,上述代码只是改变了按钮的背景颜色和文字颜色,可能效果不好看。
2015年11月04日
4 阅读
0 评论
0 点赞
2015-10-05
程序隐藏到任务栏的实现
我们在使用软件的时候,有的软件允许最小化到任务栏,然后双击任务栏的图标时又会显示出来,这篇文章主要说明如何实现这种功能;实现这种功能主要分为两步,一是将程序窗口隐藏,二是将程序图标添加到任务栏,再次显示也是分为两步:第一步是将任务栏上的图标删除,第二步是将窗口显示出来。窗口的隐藏与显示我们用API函数ShowWindow,而添加和删除任务栏中的程序图标用的是Shell_NotifyIcon函数,ShowWindow函数平时用的比较多,而且也比较简单,这里就不在阐述,下面主要说明Shell_NotifyIcon的用法:BOOL Shell_NotifyIcon( DWORD dwMessage, PNOTIFYICONDATA lpdata ); 该函数有两个参数,第一个表示你希望对图标做何种操作主要有这几个值:NIM_ADD、NIM_DELETE、NIM_MODIFY、NIM_SETFOCUS、NIM_SETVERSION;常用的是前面3个主要是向任务栏添加图标、删除图标、修改图标;第二个参数是一个结构体该结构体的定义如下:typedef struct _NOTIFYICONDATA { DWORD cbSize;//该结构的大小 HWND hWnd; //表明当对任务栏图标进行操作是将消息发送给那个窗口 UINT uID; //应用程序的ID UINT uFlags; //一个标志 UINT uCallbackMessage;//对任务栏图标操作时向窗口发送的一个消息 HICON hIcon; //放到任务栏中的图标句柄 WCHAR szTip[64]; //当鼠标停在图标上时显示的提示信息 } NOTIFYICONDATA, *PNOTIFYICONDATA;UINT uID 参数是应用程序的ID,这个ID并不是必须的可以任意给值UINT uFlags 参数是一个标志,主要用于控制图标的行为:NIF_ICON:有这个标志hIcon才是有效值NIF_MESSAGE:有这个标志uCallbackMessage才有效,也就是说有这个标志,当我们进行操作时才有消息产生NIF_TIP:当有这个标志时szTip,才有效,才会出现提示信息;UINT uCallbackMessage:当我们对任务栏图标进行操作时会发送一条消息这个消息由用户自己定义,并且在窗口过程中处理。函数介绍完了,接下来就是实现的代码://这里是将移出图标与添加图标放到一个函数中,根据第二个参数判断是需要移出或是添加 BOOL TrackIcon(HWND hWnd, BOOL bTrak) { NOTIFYICONDATA nid = {0}; nid.cbSize = sizeof(NOTIFYICONDATA); nid.hWnd = hWnd; nid.uID = 0; if (bTrak) { nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; nid.uCallbackMessage = WM_TRAKICON_MSG; nid.hIcon = LoadIcon(NULL, IDI_APPLICATION); _tcscpy_s(nid.szTip, sizeof(nid.szTip), _T("提示信息")); ShowWindow(hWnd, SW_MINIMIZE); ShowWindow(hWnd, SW_HIDE); return Shell_NotifyIcon(NIM_ADD, &nid); }else { //当需要移出图标时,窗口也应该完全显示,因此不需要对图标操作,后面的几个值就是无效值,这里可以不用管它们 ShowWindow(hWnd, SW_SHOWNA); return Shell_NotifyIcon(NIM_DELETE, &nid); } } //这是对我们自定义的消息进行处理,这个消息中的lParam保存了相应的消息ID case WM_TRAKICON_MSG: { switch (lParam) { case WM_LBUTTONDBLCLK: TrackIcon(hwndDlg,FALSE); ShowWindow(hwndDlg,SW_SHOWNORMAL); break; } } break;最后程序的运行结果如下:
2015年10月05日
5 阅读
0 评论
0 点赞
2015-09-23
MFC中属性表单和向导对话框的使用
每次在使用MFC创建一个框架时,需要一步步选择自己的程序的外观,基本功能等选项,最后MFC会生成一个基本的程序框架,这个就是向导对话框;而属性表单则是另外一种对话框,表单上有多个属性页,每点击某一页,会显示该页的内容,最好的例子是Visual C++6.0中的Option对话框;如图:属性表单的创建:属性表单上由许多属性页组成,每个属性页都可以在可视化的编辑环境中编辑,需要添加的资源名称是对话框下面的IDD_PROPPAGE_LARGE、IDD_PROPPAGE_MEDIUM, IDD_PROPPAGE_SMALL,如下图:创建资源时也可以直接创建对话框,在属性中将Style属性选为Child、Border选为Thin、勾上Disable选项;创建了资源,下面就是关联MFC的类,属性页的类是CPropertyPage,该类是继承于CDialog类,在使用时需要从CPropertyPage中派生。创建了多个属性页就需要派生多个新类;创建了属性页,下面就需要创建属性表单,属性表单不需要编辑资源,可以从类CproppertyPage中派生一个新类,用来表示新表单类;为了将属性页加到属性表单上需要在对应的构造函数中调用AddPage函数,最后需要调用该类的DoModal或者Create函数创建一个模态或者非模态的属性表单;在一下代码中有三个对应的属性页的类(CProp1、CProp2、CProp3)和一个属性表单的类(CPropSheet);//在CPropSheet中创建三个属性页的对象 public: CProp1 m_Prop1; Cprop2 m_Prop2; CProp3 m_Prop3;//在构造函数中添加属性页 AddPage(&m_Prop1); AddPage(&m_Prop2); AddPage(&m_Prop3);至于它的使用则是于普通的对话框类似向导的创建与使用:向导所使用的类与属性表单相同,这里就不在说明,为了创建向导,需要在调用DoModal或者Create之前调用SetWizardMode()函数,这样之前的属性表单就变为了向导程序,向导程序上通过下一步来转到下一个属性页,每个页面上都有“下一步”、“上一步”、“取消”按钮,这个特性不便于用户的操作,我们一般习惯于将第一个向导页的“上一步”隐藏,最后一页的“下一步”变为“完成”,为了实现这个需要使用函数SetWizardButtons(),这个函数只有一个参数表示的是页面上按钮的特性,它的取值可以是PSWIZB_BACK、PSWIZB_NEXT、PSWIZB_FINISH、PSWIZB_DISABLEDFINISH中的一个或者几个,分别用来设置该页上的一个“上一步”按钮、“下一步”按钮、“完成”按钮、和一个禁用的“完成”按钮,一般来说在属性页中的OnSetActive函数中调用,当属性页被选中,从而被激活时程序会响应WM_ACTIVATE,而该消息在函数OnSetActive中处理,由于在基类中有了这个函数,所以我们需要重写这个函数,下面是一个例子:BOOL CProp1::OnSetActive() { ((CPropertySheet*)GetParent())>SetWizardButtons(PSWIZB_NEXT); return CPropertyPage::OnSetActive(); }有时候需要实现这样的功能:属性页上有一些信息需要用户填写或选择,当用户没有选择或填写完整时不允许进入下一个页面。每次需要进入下个页面时用户会单击“下一步”按钮,而这个时候程序会调用OnWizardNext函数进入下一个页面(根据页面按钮的不同,点击不同的按钮程序会调用OnWizardNext、OnWizardBack、OnWizardFinish),当该函数返回-1时会禁止属性页发生变更,返回0时会正常进入下一页,下面是一个例子代码:LRESULT CProp1::OnWizardNext() { // TODO: 在此添加专用代码和/或调用基类 UpdateData(TRUE); if (-1 == m_occupation) { MessageBox(TEXT("请选择你的职业")); return -1; } if (TEXT("") == m_workAddr) { MessageBox(TEXT("请选择你的工作地点")); return -1; } return CPropertyPage::OnWizardNext(); }注意:将变量与控件相关联时为了获取控件返回的值需要调用UpdateData()函数,当该函数参数为TRUE时会调用DoDataExchange,该函数会根据控件返回的值,动态更新变量的值;一般情况下只有当用户点击完成时才保存用户输入的信息当用户点击取消时应该取消信息的保存;一般情况下CPropertySheet类的DoModal函数返回值是IDOK或者IDCANCLE。但是当属性表单被创建为向导时会返回ID_WIZFINISH和IDCANCLE这个时候我们可以根据返回值来判断是否保存;
2015年09月23日
7 阅读
0 评论
0 点赞
2015-09-21
对话框伸缩功能的实现
对话框的伸缩功能是指当触发某一操作时只显示部分对话框的内容,再次触发时显示全部的对话框范围,就比如画图软件上的选择颜色对话框,我们可以使用系统预定义的颜色,也可以选择自定义颜色,当点击自定义的按钮时,对话框出现原先隐藏的另一边,让用户填写颜色的RGB值。为了实现这个功能,我们需要记录两个矩形范围:全部对话框的大小和我们希望显示的部分对话框的大小,利用函数SetWindowPos来设置显示的对话框的大小,该函数的原型如下:BOOL SetWindowPos( HWND hWnd, // 需要设置的窗口的句柄 HWND hWndInsertAfter, // Z序中下一个窗口的句柄 int X, // int Y, // 窗口所在矩形的顶点坐标(x, y) int cx, // 矩形宽 int cy, // 矩形高 UINT uFlags // 显示属性 ); 下面是对该函数的补充:hWndInsertAfter:除了给出具体的窗口句柄外还可以是这几个值:HWND_BOTTOM、HWND_NOTOPMOST、HWND_TOP、HWND_TOPMOST;uFlags主要的一些标志: SWP_NOMOVE:调用该函数不改变窗口之前的顶点位置,当设置这个这个值的时候,x、y参数将被忽略; SWP_NOZORDER:忽略Z序,这个标志被设置时将忽略hWndInsertAfter参数;具体的信息可以在MSDN中查找;以下是具体的实现代码://按钮的WM_COMMAND消息处理 case WM_COMMAND: { if (LOWORD(wParam) == IDC_BUTTON) { if (HIWORD(wParam) == BN_CLICKED) { TCHAR szBuf[255] = TEXT(""); GetWindowText(GetDlgItem(hDlg, IDC_BUTTON), szBuf, 255); if (0 == _tcscmp(szBuf, TEXT("收缩>>"))) { SetWindowText(GetDlgItem(hDlg, IDC_BUTTON), TEXT("扩张<<")); }else { SetWindowText(GetDlgItem(hDlg, IDC_BUTTON), TEXT("收缩>>")); } Extern(hDlg, szBuf); } } }//改变对话框大小的函数 void Extern(HWND hWnd, const TCHAR *pszStr) { //保存对话框在扩张和收缩状态下的矩形大小 static RECT rtSmall; static RECT rtLarge; //当两个量不是有效值时,获取这两种状态下的矩形大小 if (IsRectEmpty(&rtLarge)) { RECT rtSpecrator; GetWindowRect(GetDlgItem(hWnd, IDC_SPERATOR), &rtSpecrator); rtSmall.left = rtLarge.left; rtSmall.top = rtLarge.top; rtSmall.right = rtSpecrator.right; rtSmall.bottom = rtSpecrator.bottom; } if (0 == _tcscmp(pszStr, TEXT("收缩>>"))) { SetWindowPos(hWnd, NULL, 0, 0, rtSmall.right - rtSmall.left, rtSmall.bottom - rtSmall.top, SWP_NOZORDER | SWP_NOMOVE); } else { SetWindowPos(hWnd, NULL, 0, 0, rtLarge.right - rtLarge.left, rtLarge.bottom - rtLarge.top, SWP_NOZORDER | SWP_NOMOVE); } }IDC_SPERATOR是一个分割线的ID,分割线我们采用的是一个图片控件,将这个控件的高度尽量缩小,这个控件本身也是一个矩形,可以用GetWindowRect函数获取它的矩形大小,缩小时只保存控件之上的部分;
2015年09月21日
5 阅读
0 评论
0 点赞
2015-09-15
select模型
在Windows中所有的socket函数都是阻塞类型的,也就是说只有网络中有特定的事件发生时才会返回,在没有发生事件时会一直等待,虽说我们将它们设置为非阻塞状态,但是在对于服务器段而言,肯定会一直等待客户端的消息,也就是说即使设置为非阻塞状态,时间到了函数返回,但是程序不能结束,需要一个循环不断的侦听,特别是对于有多个客户端需要管理的时候,每一个与客户端通信的socket都需要一个侦听,这样管理起来非常麻烦,我们希望系统帮助我们管理,告诉我们有哪些socket现在可以操作。为了实现这个,我们可以使用select模型select模型中需要一个结构体fd_set,该结构体是一个socket的集合,我们可以看到该结构体的定义:typedef struct fd_set { u_int fd_count; /* how many are SET? */ SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */ } fd_set;从这个定义中可以看到,结构体中主要保存了一个socket的数组和一个保存数组的大小变量;使用select模型主要使用函数select,该函数原型如下:int select ( int nfds, //系统保留,无意义 fd_set FAR * readfds,//可读的socket集合 fd_set FAR * writefds,//可写的socket集合 fd_set FAR * exceptfds,//外带socket集合 const struct timeval FAR * timeout//该函数的超时值 );在程序中使用该函数前需要在特定的集合中放入需要检测的socket值,当发生某一时间导致该函数返回时,函数会将特定集合中未待决的socket全部剔除出去,保留待决套接字,比如在readfds集合中放入几个套接字并执行完成函数,那么留下的套接字都是可以从系统的相应缓冲区读数据的。通过遍历相应的集合我们知道如何对套接字做相应的操作;select模型最多支持64个套接字,这个值由FD_SETSIZE宏定义的,我们可以修改这个宏的值,以便支持更多的套接字,修改时尽量不要在系统文件中修改,在我们的工程文件中修改,可用使用如下方式:#ifdef FD_SETSIZE #undef FD_SETSIZE #endif #define FD_SETSIZE 200这段代码使得select模型支持200个套接字;虽然可以修改,但是这个数组太大,会消耗过多的系统资源,每次在遍历数组时总会从头到尾遍历,数组太大效率必然底下,所以最好不要修改这个值,处理大于64个套接字的情况下可以使用多线程的方式,多定义几个集合处理;为了操作这个集合,Windows专门定义了一组宏,他们分别是:FD_SET(fd, &set) //将fd套接字压入集合set中 FD_ISSET(fd, &set)//判断fd是否在set中 FD_ZERO(&set)//将集合set清零 FD_CLR(fd, &set)//将fd从集合set中删除下面说一下服务端一个简单的select模型的编写1)创建套接字,绑定、侦听;2)等待客户端链接3)将连接返回的套接字压入一个数组中保存4)将数组的套接字填入集合中5)调用select函数6)检测特定集合中的套接字7)进行读写操作8)返回到第四步,等待客户端下一步请求在编写时需要注意以下几点:1)为了与多个客户端保持连接,需要一个数组保存与客户端连接的所有的socket,由于select函数只会执行一次,每次返回后需要再次将徐监控的套接字压入集合,调用select,以便进行下一次检测;所以一般将这一步写在一个死循环中2)注意select是一个阻塞函数,所以为了可以支持多个客户端可以采用一些方法:第一种就是采用多线程的方式,每有一个客户端连接都需要将新开一个线程处理并调用select监控;另一种就是调用select对侦听套接字以及与客户端通信的套接字;为什么可以这样呢,这就要说到TCP/IP中的三次握手,首先一般由客户端发起链接,发送一条数据包到服务器,服务器接收到数据,发送一条确认信息给客户端,然后客户端再发送一条数据,这样就正式建立连接,所以在客户端与服务器建立连接时必然会发送数据,而服务器一定会收到数据,所以将侦听套接字放入到read集合中,当有客户端需要连接时自然会收到一条数据,这个时候select会返回,我们需要校验集合中的套接字是否是侦听套接字,如果是则表明有客户端需要连接;这样当客户端有请求select会返回,可以进行下一次的侦听,没有请求,会死锁在select函数上,但是对于所有客户端并没有太大的影响;3)我们用数组存储所有的套接字时,每当有客户端链接,我们需要添加,而有客户端断开链接我们需要在数组中删除,并将下一个套接字添加进该位置,为了管理套接字数组,我们另外需要一个队列用来记录退出客户端的socket在数组中的位置,下一次有新的链接进来就将相应的套接字放到这个位置。下面是一个简单的例子:SOCKET g_sockArray[FD_SETSIZE] = { 0 }; int g_nCount = 0; int _tmain(int argc, _TCHAR* argv[]) { WSAData wd; WSAStartup(MAKEWORD(2, 2), &wd); SOCKET sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (INVALID_SOCKET == sockListen) { cout << "创建套接字失败,错误码为:" << WSAGetLastError() << endl; closesocket(sockListen); WSACleanup(); return 0; } SOCKADDR_IN SrvAddr = { AF_INET }; SrvAddr.sin_port = htons(6666); SrvAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (SOCKET_ERROR == bind(sockListen, (SOCKADDR*)&SrvAddr, sizeof(SOCKADDR))) { cout << "绑定失败,错误码:" << WSAGetLastError() << endl; closesocket(sockListen); WSACleanup(); return 0; } if (SOCKET_ERROR == listen(sockListen, 5)) { cout << "侦听失败,错误码:" << WSAGetLastError() << endl; closesocket(sockListen); WSACleanup(); } g_sockArray[g_nCount] = sockListen; g_nCount++; fd_set fdRead = { 0 }; while (true) { FD_ZERO(&fdRead); for (int i = 0; i < g_nCount; i++) { FD_SET(g_sockArray[i], &fdRead); } select(0, &fdRead, NULL, NULL, NULL); for (int i = 0; i < g_nCount; i++) { if (FD_ISSET(g_sockArray[i], &fdRead)) { if (sockListen == g_sockArray[i]) { SOCKET sockClient = accept(sockListen, NULL, NULL); if (INVALID_SOCKET == sockClient) { cout << "连接失败, 错误码为:" << WSAGetLastError() << endl; break; } cout << "有客户端链接进来" << endl; if (g_nCount == FD_SETSIZE) { cout << "以达到服务器管理上限" << endl; break; } if (NULL == g_pFirst) { g_sockArray[g_nCount] = sockClient; } else { int n = Pop(); g_sockArray[n] = sockClient; } g_nCount++; } else { char szBuf[255] = ""; recv(g_sockArray[i], szBuf, 255, 0); cout << "客户端发送消息:"<< szBuf << endl; if (0 == strcmp(szBuf, "exit")) { Push(i); closesocket(g_sockArray[i]); cout << "与客户端链接断开" << endl; g_nCount--; } break; } } } } WSACleanup(); return 0; }上述代码中,每当检测到有待决套接字就处理,处理完一个后就不在继续检测了,我们知道在理论上select执行完成后,保留的是所有待决套接字,那么待决套接字可不可能有多个呢,我觉得这个基本上不可能,因为服务器端判定在某一时刻该套接字是否处于待决状态是在毫秒级别的,就算有几个客户端在某时刻毫秒不差的向服务器发送数据,那么我们还要考虑双方之间的距离(虽说光速很快可以忽略不计但是当单位是毫秒是应该还是有影响)以及网络状况,虽说在大规模的情况可能出现在毫秒级别上同时响应,但是我们的select只支持64个(超过64时需要另外开线程再创建一个相应的集合),在64个客户端中找到这样的两个客户端是不可能的,所以我们就假定每次只有一个待决套接字,使用break为了让其跳出循环,避免做无用功;
2015年09月15日
5 阅读
0 评论
0 点赞
2015-09-12
socket模型处理多个客户端
最近学完了简单的socket编程,发现其实socket的网络编程其实并没有什么难度,只是简单的函数调用,记住客户端与服务端的步骤,写起来基本没有什么问题。在服务器程序的设计中,一个服务器不可能只相应一个客户端的链接,为了响应多个客户端的链接,需要使用多线程的方式,每当有一个客户端连接进来,我们就开辟一个线程,用来处理双方的交互(主要是利用recv或者recvfrom用于收发信息),由于但是在网络中可能出现这样一种情况:由于处理比较复杂,下一条信息到来之后,上一条信息的处理还没有完成,这样信息太多了之后系统的缓冲占满之后可能会发生丢包的现象,所以为了解决这个问题,需要另外再开一个线程,专门用来处理接收到的数据,这样总共至少有3个线程,主线程,收发信息的线程,处理线程;这样可能也不完整,处理的操作种类多了的话可能需要根据不同的请求来开辟不同的线程用来处理这一类请求,下面是实现这一思路的部分代码:全局变量:DWORD WINAPI AcceptThread(LPVOID lpParameter); DWORD WINAPI RecvThread(LPVOID lpParameter); DWORD g_nAcceptID = 123; DWORD g_nRecvID = 234; HANDLE g_hAccpetThread; HANDLE g_hRecvThread;主线程函数:int _tmain(int argc, _TCHAR* argv[]) { WSADATA wd; WSAStartup(MAKEWORD(2, 2), &wd); SOCKET sockListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); if (INVALID_SOCKET == sockListen) { cout << "创建侦听套接字失败,错误码为:" << WSAGetLastError() << endl; return -1; } SOCKADDR_IN srvAddr = { 0 }; srvAddr.sin_family = AF_INET; srvAddr.sin_port = htons(6666); srvAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (SOCKET_ERROR == bind(sockListen, (SOCKADDR*)&srvAddr, sizeof(SOCKADDR))) { cout << "绑定失败,错误码为:" << WSAGetLastError() << endl; WSACleanup(); return -1; } if (SOCKET_ERROR == listen(sockListen, 5)) { cout << "侦听失败,错误码为:" << WSAGetLastError() << endl; WSACleanup(); return -1; } while (true) { SOCKET sockConn = accept(sockListen, NULL, 0); if (INVALID_SOCKET == sockConn) { cout << "本次连接失败,即将进入下一次连接,错误码为:" << WSAGetLastError() << endl; closesocket(sockConn); closesocket(sockListen); WSACleanup(); continue; } g_hAccpetThread = CreateThread(NULL, 0, AcceptThread, &sockConn, 0, &g_nAcceptID); } WaitForSingleObject(g_hAccpetThread, INFINITE); WSACleanup(); return 0; }收发数据函数:DWORD WINAPI AcceptThread(LPVOID lpParameter) { cout << "有客户端连接进来" << endl; SOCKET sockConn = *(SOCKET*)lpParameter; while (true) { char *pszBuf = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY | HEAP_NO_SERIALIZE, 255); if (SOCKET_ERROR == recv(sockConn, pszBuf, 255, 0)) { cout << "接受数据失败,错误码为:" << WSAGetLastError() << endl; cout << "准备进行下一次接受数据....." << endl; continue; } g_hRecvThread = CreateThread(NULL, 0, RecvThread, pszBuf, 0, &g_nRecvID); WaitForSingleObject(g_hRecvThread, INFINITE); if (0 == strcmp("exit", pszBuf)) { cout << "正在断开与该客户端的连接" << endl; HeapFree(GetProcessHeap(), 255, pszBuf); return 0; } } return 0; }信息处理子线程:DWORD WINAPI RecvThread(LPVOID lpParameter) { cout << "接受到客户端的数据:" << (char*)lpParameter << endl; return 0; }虽说这个解决了多个客户端与服务器通信的问题,但是这样写确定也很明显:所有的与客户端通信的socket都有程序员自己管理,无疑加重了程序员的负担;每有一个连接都需要创建一个线程,当有大量的客户端连接进来开辟的线程数是非常多的,线程是非常耗资源的,所以为了解决这些问题就提出了异步的I/O模型,它们解决了这些问题,由系统管理套接字,不要要人为的一个个管理,同时不需要开辟多个线程来处理与客户端的连接,我们可以将线程主要用于处理客户端的请求上;
2015年09月12日
5 阅读
0 评论
0 点赞
2015-09-11
VC++平台上的内存对齐操作
我们知道当内存的边界正好对齐在相应机器字长边界上时,CPU的执行效率最高,为了保证效率,在VC++平台上内存对齐都是默认打开的,在32位机器上内存对齐的边界为4字节;比如看如下的代码:struct MyStruct { int i; char c; }; int _tmain(int argc, _TCHAR* argv[]) { cout<<sizeof(MyStruct)<<endl; return 0; } 此时输出的结果并不是sizeof(int) + sizeof(char) = 5而是8,因为内存对齐的原因,将char分配为4个字节效率更高;在VC平台上我们可以通过预处理指令:#pragma pack(show)来查看当前内存对齐的方式,我们在代码前加上一句#pragma pack(show),再次编译,在编译器的“生成”窗口中看到一个警告:“warning C4810: 杂注 pack(show) 的值 == 8”说明这时编译器采用的是8字节的对齐方式,另外可以通过这个预处理指令更改对齐方式,比如将代码改写一下:#pragma pack(show) #pragma pack(1) struct MyStruct { int i; char c; }; int _tmain(int argc, _TCHAR* argv[]) { cout<<sizeof(MyStruct)<<endl; system("PAUSE"); return 0; }这个时候得到结果为5,也就是说我们已经将对齐方式改为了1;除了这个预处理指令我们也可以通过VC++扩展关键字align来改变内存的对齐方式:#pragma pack(show) #pragma pack(1) struct MyStruct { int i; char c; }; struct __declspec(align(1)) MyStruct1 { int i; char c; }; int _tmain(int argc, _TCHAR* argv[]) { cout<<sizeof(MyStruct)<<endl; cout<<sizeof(MyStruct1)<<endl; system("PAUSE"); return 0; }这个时候输出的结果为两个5;
2015年09月11日
3 阅读
0 评论
0 点赞
1
...
17
18
19