首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
105 阅读
2
nvim番外之将配置的插件管理器更新为lazy
78 阅读
3
2018总结与2019规划
62 阅读
4
PDF标准详解(五)——图形状态
40 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
314
篇文章
累计收到
31
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE
菜谱
页面
归档
友情链接
关于
搜索到
37
篇与
的结果
2016-02-28
C/C++中define定义的常量与const常量
常量是在程序中不能更改的量,在C/C++中有两种方式定义常量,一种是利用define宏定义的方式,一种是C++中新提出来的const型常变量,下面主要讨论它们之间的相关问题;define定义的常量:define是预处理指令的一种,它用来定义宏,宏只是一个简单的替换,将宏变量所对应的值替换,如下面的代码:#define NUM 2 int main() { printf("%d", NUM); }编译器在编译时处理的并不是这样的代码,编译器会首先处理预处理指令,根据预处理指令生成相关的代码文件,然后编译这个文件,得到相关的.obj文件,最后通过链接相关的.obj文件得到一个可执行文件,最典型的是我们一般在.cpp文件中写的#include指令,在处理时首先将所需包含的头文件整个拷贝到这个.cpp文件中,并替换这个#include指令,然后再编译生成的文件,这个中间文件在Windows中后缀为.i,在Visual C++ 6.0中以此点击Project-->Settings-->C/C++,在Project Options最后一行加上'/P'(P为大写)这样在点击编译按钮时不会编译生成obj文件,只会生成.i文件,通过这个.i文件可以看到在做预处理的时候会将 NUM替换成2然后在做编译处理,这个时候点击生成时会出错,因为我们将编译选项修改后没有生成.obj文件但是在生成时需要这个文件,因此会报错,所以在生成时要去掉这个/P选项。而我们看到在使用const 定义的时候并没有这个替换的操作,与使用正常的变量无异。const型变量只是在语法层面上限定这个变量的值不可以修改,我们可以通过强制类型转化或者通过内嵌汇编的形式修改这个变量的值,比如下面的代码:// 强制类型转化 int main(int argc, char* argv[]) { const nNum = 10; int *pNum = (int*)&nNum; printf("%d\n", nNum); return 0; }//嵌入汇编的形式 const nNum = 10; __asm { mov [ebp - 4], 10 } printf("%d\n", nNum); return 0;但是我们看到,这两种方式修改后,输出的值仍然是10,这个原因我们可以通过查看反汇编代码查看;printf("%d\n", nNum); 00401036 push 0Ah 00401038 push offset string "%d\n" (0042001c) 0040103D call printf (00401070) 00401042 add esp,8在调用printf的时候,入栈的参数是10,根本没有取nNum值得相关操作,在利用const定义的常量时,编译器认为既然这是一个常量,应该不会修改,为了提升效率,在使用时并不会去对应的内存中寻址,而是直接将它替换为初始化时的值,为了防止这种事情的发生,可以利用C++中的关键字:volatile。这个关键字保证每次在使用变量时都去内存中读取。我们可以总结出const和define的几个不同之处:define是一个预处理指令,const是一个关键字。define定义的常量编译器不会进行任何检查,const定义的常量编译器会进行类型检查,相对来说比define更安全define的宏在使用时是替换不占内存,而const则是一个变量,占内存空间define定义的宏在代码段中不可寻址,const定义的常量是可以寻址的,在数据段或者栈段中。define定义的宏在编译前的预处理操作时进行替换,而const定义变量是在编译时决定define定义的宏是真实的常量,不会被修改,const定义的实际上是一个变量,可以通过相关的手段进行修改。
2016年02月28日
8 阅读
1 评论
0 点赞
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日
4 阅读
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日
3 阅读
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 点赞
2015-08-21
C++继承
在封装的过程中,我们发现有很多地方有问题,比如我们在封装Windows API 的过程中,每个窗口都有各自的消息处理,而我们封装时不同的窗口要针对不同的消息而编写不同的消息处理函数,不可能所有窗口对于某些消息都进行相同的处理,所以在面向对象的程序设计中,提供了一种新的方式——继承与派生;在c++中将继承的一方称作派生类或者子类,将被继承的一方叫做基类或者父类继承的基本格式如下(CB 继承CA):class CA { public: CA(); ~CA(); } class CB : public CA { public: CB(); ~CB(); }派生类中前面相应大小空间的内存保存的是基类的内容,而后面的部分保存的是派生类的内容,这样派生类就可以拥有基类的所有成员,而不必重写代码达到了代码重用的目的。在设计中一般将类的共性提取出来作为基类,而将不同的部分从基类派生,作为每个类的特性,对于共性的内容我们只需要在基类中编写,而在派生类中直接使用。下面我们来探讨一下,基类与派生类中构造与析构的调用关系,通过写下面一个简单的小例子:class CA { public: CA(){ cout <<"CA()"<<endl; } ~CA(){ cout <<"~CA()" << endl; } }; class CB : public CA { public: CB(){ cout <<"CB()" << endl; } ~CB(){ cout <<"~CB()" << endl; } }; int _tmain(int argc, _TCHAR* argv[]) { CB objB; return 0; }最终的结果是先调用基类的构造函数在调用派生类的构造函数,而对于析构的调用顺序正好相反,先调用派生类在调用基类:对于继承来说有三种:共有继承、私有继承以及保护继承,继承的方式不同,派生类对于基类的各种不同属性之间成员的访问权限不同,下面再给出一个表格用以说明这个问题:通过这个表我们可以总结出一下几点:1)私有成员在任何情况下都不能被派生类访问;2)公有继承下其他基类成员在派生类中的访问属性不变;3)私有继承下其他基类成员在派生类中全部变为私有;4)保护继承下其他类成员在派生类中全部变为保护属性;从这个表中我们可以看出,私有继承与保护继承对于基类的访问属性完全相同,那么它们有何区别呢?保护成员的访问情况与私有相同,即类的保护成员在类内可以访问在类外不能访问,它们二者的区别在这个表中确实没有体现出来,主要的区别可以在下一层的继承中体现比如有三个类继承关系为CC->CB->CA,继承类型分别为,我们知道基类的非私有成员在保护继承下公有的变为保护,保护的仍然为保护,而私有继承则是将所有都变为私有,他们之间如果都是保护继承的方式,那么CA中的其他成员在CB中都变为保护类型那么在CC中仍然能够访问到CA的成员;当他们之间都是以私有继承的方式,那么CA中的成员在CB中都为私有,在CC中就不能访问CA中的成员,所以在一般情况下,我们将基类的数据成员声明为保护类型,这样既起到了封装的作用,又方便派生类的访问;
2015年08月21日
5 阅读
0 评论
0 点赞
2015-07-24
从唯一实例谈静态成员
在实际的项目中我们可能需要这样一种类,它的对象在整个项目中只能有一个,在整个程序中只能创建一个类的对象,比如说,最常用的mp3播放软件Winamp,由于它需要独占计算机中的音频设备,因此该程序只允许自身运行唯一的一个例程。c++并没有提供这一特性,我们可以自己封装一个唯一实例的类,为了完成这个工作,我们需要一个静态的整型数据成员用于记录当前程序中类的实例个数,还需要一个指向自身的静态指针,指向新创建的类的对象。实现的代码如下:class CSingle { private: CSingle(){}; CSingle(CSingle &r){}; <span style="white-space:pre"> </span>void operator = (CSingle &r){}; ~CSingle(){}; public: static CSingle* Create(); static void Release(); private: static int m_nCount; static CSingle *m_pInstance; }; int CSingle::m_nCount = 0; CSingle *CSingle::m_pInstance = NULL; CSingle* CSingle::Create() { if (0 == m_nCount) { m_nCount++; m_pInstance = new CSingle; }else { return NULL; } return m_pInstance; } void CSingle::Release() { if (0 != m_nCount) { delete m_pInstance; m_nCount--; } return; }利用一下代码测试:CSingle *pObj = CSingle::Create(); CSingle *pObj1 = CSingle::Create(); cout<<pObj<<endl; cout<<pObj1<<endl; CSingle::Release();上述代码使用的是类的静态成员,静态成员用static关键字进行说明,类的静态成员可以是静态数据成员,也可以是静态函数成员,下面分别来说明这两个内容:类的成员函数:我们知道一般程序用来存储变量的内存分为静态存储区,和堆栈段,静态存储区用于存储静态变量和全局变量,而堆栈段用来存储函数中定义的局部变量,也就是说静态变量和全局变量在同一段内存空间中,也可以相当于一个全局变量,关键字static除了在定义静态变量的时候使用外,还可以作为限定符,限定定义的变量只能在某一区域内可见,在函数中定义的只在该函数中可见,在文件中定义的只在该文件中可见。所以说静态变量是一种限定了可见区域的全局变量,在类中定义的静态数据变量被限定只在类中可见,与类的普通成员间最大的区别在于普通成员只有在类存在时才会在内存中真正存在,而静态成员一旦定义就已经存在,且在整个内存中只有一份,这也就是说所有类共享这一个静态变量,任何一个类修改了变量的值,都会影响到其他类。因为它们有类的生命周期无关,所以不论类是否创建了对象,都可以使用它们,只是需要加上类域,因为static已经限定了它们只在类中可见。类的成员函数:静态函数成员与静态数据成员类似,也是相当于一个全局函数,所有类公用一份,它与类函数的不同在于,普通的类函数都会由编译器隐式的传入一个this 指针,而静态成员没有传入这样一个值,也就意味着这个函数并不知道是哪个类的对象在调用它,自然也就不知道该如何使用哪个类对象的普通成员,所以说c++规定静态函数成员不能访问非静态数据成员,而静态数据成员,所有类共享,所以只能访问静态数据成员。静态函数成员也受类的保护限制,即不能通过对象打点调用对象的私有成员。接下来分析这个程序,首先唯一实例必须用静态数据成员计算当前有几个类的实例,不能用普通的内成员,否则每个类都有一个计数器,而每个类的计数器可能都为1,这样达不到唯一的效果。如果用构造函数初始化的话,每个类对象都共享这块内存,所以我们如果用构造函数初始化,每个类的构造函数都有可能会修改这个值,导致错误的结果,所以我们不用类的构造函数,只能将它私有化,不让系统调用构造函数,系统提供了3中默认的构造函数,分别是不带参的构造函数、拷贝构造函数、赋值运算符重载的构造函数,所以为了不让系统调用任何默认构造,我们将这3个构造函数私有化,并且在构造函数中不做任何事情。不能用构造函数创建对象,也就是说我们需要利用其它方法创建对象,既然没有构造函数,那么通过对象打点调用其它函数创建肯定也是不可能的,我们需要一种函数即使类对象没有它们也存在,静态函数成员正是这种函数,所以我们需要创建的静态函数成员,并且通过动态内存创建一个类的对象,并用静态数据成员保存该指针。至此我们已经完成了一个唯一实例的类。
2015年07月24日
7 阅读
0 评论
0 点赞
2015-07-12
8086cpu中的标志寄存器与比较指令
标志寄存器在8086CPU中有一个特殊的寄存器——标志寄存器,该寄存器不同于其他寄存器,普通寄存器是用来存放数据的读取整个寄存器具有一定的含义,但是标志寄存器是每一位都有固定的含义,记录在运算中产生的信息,标志寄存器的机构如下图:寄存器中的第1、3、5、 12、 13、 14 、15位在8086CPU中没有使用,其他位置代表不同的含义,各个位置的意思如下(该表截自百度知百科中的标志寄存器):一般我们常用到的是如下几个标志CF:CF标志表示进位,我们知道对于8086CPU来说,寄存器只能存储16位二进制数,但是有些指令产生的结果可能大于16位,比如:mov ax,200h add ax, 0fffffh产生的结果已经超过16位,由于ax寄存器只能保存16位数据,因此高位产生的数据必然被丢弃,但是也不是简单的丢弃,这个时候CF标志位会变成1,表示结果产生了进位;PF:表示标志,这个奇偶不是数字本身是奇数或者是偶数,表示的是某个数据中有奇数个1或者是偶数个1;ZF:0标志:表示计算结果是否为0;SF:符号标志记录相关计算结束后得到的结果是否为负,若为负则标志位为1,否则标志位为0;方向标志:方向标志用于内存单元的拷贝,我们在将一段内存单元拷贝到另一段内存中去时使用循环一个字节一个字节的拷贝,但是8086CPU提供了一个指令movsb 、movsw分别是按字节拷贝和按字拷贝,这两个指令所对应的源内存地址只能用ds:[si],目的地址只能用es:[di]表示,其中DF标志指明我们是从低到高字节拷贝还是从高到低字节拷贝,当df = 1时,地址递减, = 0时地址递增;OF:溢出标志,它的作用与CF相同,当得到的操作数大于16位的时候,该标志置为1,但是OF用于有符号数,而CF用于无符号数。IF:在DEBUG中使用,当我们启动DEBUG模式的时候,一条指令执行完后,该寄存器被置为1,这个时候会调用相应的中断程序,使我们的代码在该位置停止执行,以便我们查看相应的结果;CMP指令CNP指令使用的格式为CMP 操作数1,操作数2;cmp指令的作用是将两个操作数相减,并根据结果改变标志寄存器的值,但是并不保存计算结果,当两个数都为正时,如果ZF = 0则说明两个数相等,这个指令一般用于判断两个数据的大小关系,如果我们只是使用它,那么在判断两个数字的大小关系上,可能还会判断其他内容,假设我们使用了cmp n1, n2这样的指令的话,那么可能出现三种情况:n1 = n2:要判断是不是出现这种情况只需要判断ZF是否为0,当ZF为0时两数相等;n1 >n2:我们知道大数减去小数结果一定为正,是不是只需要判断SF呢?不是!在数学上大数减去小数结果一定为正这是肯定的,但是在计算机中确并不一定是这样的,我们需要考虑到是否溢出的问题比如“ffffh - (-2)”这个结果在数学上肯定是负数,但是在计算机上结果却为正,,这个时候除了要校验SF还需要校验OF,当溢出产生的时候结果正好与我们使用SF校验的相反;n1 < n2:这个结果的校验与上述的校验类似;然而幸运的是,在我们实际比较两个数据大小的时候我们并不需要这样,80886CPU为我们提供了一系列指令用来做这个工作:指令含义检测的相关标志位je(jmp equal)当两数相等时跳转ZF = 1jne(jmp not equal)当两数不相等的时候跳转ZF = 0jb(jmp blow)小于时跳转CF = 1jnb(jmp not blow)不小于时跳转CF = 0ja(jmp above)大于时跳转CF= 0且ZF = 0jna(jmp not above)不大于时跳转CF = 1或ZF = 0以上指令指示检测标志寄存器中相应位置的值来判断,至于在它的前面是否使用了cmp指令CPU并不关心,在执行这些指令的时候只要CPU检测到相关的标志满足条件则会自动跳转,比如执行下面的指令:mov ax,0 add ax,ax je s1 inc ax s1: inc axCPU执行到je的时候检测到ZF寄存器为0,这个时候会自动跳转到s1处的代码中执行,不会执行je的下一行代码。为了实现比较功能最好将cmp与这些指令配套使用。高级语言中的if语句正是用着一套指令实现的一般在破解时可能需要修改某些标识,以达到跳转或者不跳转的目的,下面是我从小甲鱼网站上找到的图片,记录了各个跳转指令实现所需要的条件,根据这个表中的内容,修改相应标识,就可以控制程序执行流程
2015年07月12日
3 阅读
0 评论
0 点赞
1
...
3
4