第二章:基本内置数据类型、常量、变量
先打个广告——欢迎加入编程<->逆向群: 90569473 ,本群无任何不良目的,欢迎加入!
目录:
1:基本内置数据类型
1.1:void类型
1.2:整型
1.2.1:整型的区分
1.2.2:溢出问题
1.2.3:存储顺序问题
1.3:浮点类型
1.3.1:浮点编码
1.3.2:IA32 CPU的浮点处理单元
1.3.3:IA32 CPU浮点处理过程
2:变量
2.1:存储类
2.2:初始化与字面常量
2.3:符号常量
在系列一中有朋友谈到了阴阳学说的形而上学性,首先声明我很愿意在这个彼此尊重的论坛上分享自己的感受、学习并吸收别人的精华,同时我想表达三点个人的看法,其一作为哲学来说本身就是一个没有绝对正确的东西,假设把人类定位于时间与空间的二维坐标上,在这个坐标上发生的事情如果适合了人类的利益趋向那么就被人们称为正确的,相反则是错误的,即对与错分时、分地、分事、分人,在你认为正确的时候,世界上不知有多少人以为那是错误的,当你陷入这个圈子的时候,也许就会承认它的正确性;其二,我想说明只所以提到哲学的概念,是因为21世纪是一个信息时代,面对信息的大爆炸,稍不留神,我们就可能陷入复杂的现象中而不难自拔,而错综复杂的信息往往具有某些共性,这些共性可能用哲学的观点去讨论会收到意想不到的效果,也就是说哲学的意义关键在于用,我们不要去讨论哲学本身的问题,而是要学会吸收别人的观点,正所谓不容细流,何成汪洋!基三,当我提到阴阳学说的时候,也许很多人讨论的是阴阳学说本身的内容,我想说,阴阳学说本质上也是有人类观察总结而得到的,在这个过程中值得学习的是观察能力而不是阴阳学说本身,即使现在诸多的数学公式,也是有人们不断的观察分析得到的,只不过是在一些基本元素的基础上而已,举一个简单的例子,在数学中,有一个最简单的概念集合,可能你会作很多关于集合的数学题,但这并代表你就懂的集合,观察往往让我们收到意想不到的效果,比如去买鸡蛋,当我们数鸡蛋个数的时候,实际上就已经建立了两个集合,集合A={鸡蛋},集合B={1,2,3,4…..},他们之间的关系就是一一映射关系或函数关系,当然这个例子只是在说明观察的重要性,而我们已经生活在一个被人类创造了的社会里,因此我们需要在学习知识的时候再返回去观察,这不正是逆向的乐趣与意义所在吗?
当然我们所学的知识实际上一个大的class,而面对具体的object时需要有具体的方法 ,接下来的内容很大一部分是结合OD动态反汇编调试器分析C++的一些特性,当然如果你已经很精通C++则无需再关注,本系列的核心思想都在系列一中!
在系列一中,通过阴阳学说及简单的问题提出与解决知道了C++的整体架构,接下来的问题,我想讨论一下C++的基本数据类型及C++中两个基本的概念——变量与常量,如果以面向对象来看待C++,那么它只有一种数据类型——类类型,但实际上正如系列一中提到的C++只所以称为C++而非++C,是因为它是在C语言的基础上结合semula语言中的面向对象特性而产生的一门新语言,因此可能我们需要继续使用学习C过程的一些方法来学习讨论C++!
一:基本数据类型
C++的基本数据类型包括void、整型和浮点三种类型!依次对其讨论:
1.1:void类型
相信只有为数不多的人不知道孙悟空这个名字,悟空二字即是西游记这部巨著的核心思想 ,如果你懂得悟空的道理,那么void类型就一定能理解!
空类型的意义在于即能代表任何数据类型,又只能带表一种数据类型,正如一个不放任何东西的木桶,在没有放入东西之间,它是可以放入任何东西的,一旦放入一种东西,则只能表示某一种东西的桶,比如放入水称水桶,放入油叫油桶,还有马桶等等!看下面的程序理解:
#define FAST
#include "head.h"
int main()
{
void *p;
int a=2;
char b=0x62;
p=&a;
cout<<p<<" "<<*(int *)p<<endl;
p=&b;
cout<<p<<" "<<*(char *)p<<endl;
return 0;
}
正如上面所描述的,一个void指针可以指向任何一种数据类型,即空代表任何事物,一旦指向一种数据类型,则必须进行数据类型转换,即空只能代表一种事物!
在很多C++初级教程中,经常在vc开发环境中,使用void main(),这是不标准的,或者说压根就是错的,在G++中如果使用void main()则编译器会提示编译无法通过,而在VC++中却能通过,我们不妨编写如下代码:
//1.exe
#define FAST
#include "head.h"
void main()
{
cout<<"hello world!"<<endl;
return ;
}
//2.exe
#define FAST
#include "head.h"
void main()
{
cout<<"hello world!"<<endl;
}
然后在Cmd.exe中分别执行1.exe &&dir 和2.exe&&dir,都不会成功执行dir命令,通过这点可以证明void类型用于main(),程序不会执行成功!
而void类型真正用于规范化编程指的是非main以外的函数使用void类型为返回值类型或函数参数,表示不接收任何参数和没有返回值,因为一个C++程序的退出始终是以main()的结束而结束的,因此在main()中请尽量不要使用void返回值类型,而在非main函数中使用则没有这么多要求!
//void规范化编程
#define FAST
#include "head.h"
void Cout(void)
{
cout<<"hello world!"<<endl;
}
int main()
{
Cout();
return 0;
}
上面的void Cout(void)函数不会接收任何的参数同时也不具有返回值,在实际编程中void Cout()与void Cout(void)的意义是一样的,前者也不会传递任何参数,编译器在编译时会检测到是否向这样的函数传递了参数!但是我想加上void关键字后可能使编译器的编译更容易一些!
1.2整型
整型包括bool、char、wchar_t、short、int、long 几种类型!
1.2.1:整型数据的区别
基本数据类型的区别关键在于其内存所占字节数的不同,占相同的内存字节数的类型在机器级实际上没有任何区别!要获得整数数据类型所占的内存字节数,需要用到编译时运算符sizeof,如下程序演示了如何获得整型数据类型所占内存字节数的:
#define FAST
#include "head.h"
int main()
{
cout<<sizeof(bool)<<" "<<sizeof(char)<<" "<<sizeof(short)<<" "<<sizeof(long)<<" "<<sizeof(wchar_t)<<endl;
return 0;
}
到这里,我想聪明的网友朋友们已经想到了如下几个问题
1:CPU读取整型数据类型的数据时时如何知道要读取多少内存字节?
2:既然整型类型具有有一定的内存所占字节数,那么它们所描述的数据范围必然是有限的,当超出数据类型所描述的数据范围时,计算机怎么处理?
3:当要使用4个char类型的数据初始化一个int类型的内存空间时,该如何将数据存储到int类型中呢?
下面主要讨论这三个问题!
先看第一个问题,CPU如何知道要读取多少字节的数据,如下程序:
#define FAST
#include "head.h"
int main()
{
int a=2;
__asm mov dword ptr ss:[ebp-4],0xa
cout<<"now the value of a is 10"<<endl
<<a<<endl;
return 0;
}
当程序执行到mov dword ptr ss:[ebp-4],0xa的时候,a的值变成了10,其中dword ptr中的dword指明了存储数据的区[ebp-4]为一个32位的内存空间,而ptr则是属性操作符,即IA32 CPU在执行指令时通过ptr属性操作符得知操作多大的内存空间!
1.2.2:溢出问题解决
C++提供有符号数与无符号数两种类型,对于溢出问题的解决,我想应该讨论一下补码(总感觉书本上的补码说的复杂而费解),对于有符号整数的机器级编码一般计算机提供原、反、补三种方式,原码编码方式又称绝对值编码,即不管正数负数都取绝对值,这样做有一个好处,有利于乘法与除法运算,但对加减运算则不太适用,因此人们引入了反码与补码的编码方式,事实上只有补码方式最通用,也最适合!
我们暂且将有限的重复操作称为补码操作,因为整型数据类型有固定大小的内存存储空间,因此所描述的整数范围将是有限的,当超出这个范围时即产生溢出,计算机把它当作描述范围内的重复操作处理,即用补码解决!
以char类型为例,char类型占8个字节,排列组合分步相乘得2*2….*2=2^8=256种编码方式,即可以表示256个符号,当为无符号char型时,其编码范围为0-255共256个编码符号,当为有符号时char类型时,人们约定最高位为符号位,注意虽然人们将符号位单纯的拿出来作为一种约定,这是对人来说的,对于计算机来说,它还是数据的一个组成部分!显然正数大于负数,所以有符号char类型所能描述的最大值为
0 111 1111即127
同时-1是负数中最大的值,它的编码为
1 111 1111即0xff
依次递减到1 000 0000时为最小负数,即为0x80=-128!因此有符号数所能能描述的范围为-128-127!
不管是有符号数还是无符号数,当溢出发生时,都通过补码进行运算,整型数据类型的模为2^n(n为其所占内存字节数)!以char类型为例:
#define FAST
#include "head.h"
int main()
{
char x=-129;
if(x==127)
{
cout<<"-129+模(256)"<<endl;
}
unsigned char y=257;
if(y==1)
{
cout<<"1+模为(256)"<<endl;
}
return 0;
}
结果:会看到两次输出的内容!
如果还不能理解这个过程,可以想象太极图中的阴阳,阴之末为阳之始,阳之末为阴之始,当不考虑阴阳时,只讨论太极图时即为无符号数(假设一个太极圈为256(char类型的模),那么257=1个太极圈+1,故为1),当考虑阴阳不考虑太极图时即为有符号数,而阴之末为阳,即-129为负数(阴)之末,为阳(正数)之始(127)! 1.2.3:存储顺序的问题
对于前面提到的第三个问题,实际上一个存储顺序的问题,那么根据char类型占1字节,int类型占4字节,则可以抽象如下数据结构:
union U
{
int i;
struct
{
char x1;
char x2;
char x3;
char x4;
};
}u;
问题:使用上面的数据结构通过赋值u联合体的char数据成员,来输出u.i成员,使输出结果为0x12345678!
对于这个问题实际上分类只有两种方法一种是依次赋值0x12345678,一种是依次赋值0x78563412,前者称顺序存储,后者称逆序存储!IA32 CPU为逆序存储,编写如下源码:
#define FAST
#include "head.h"
union U
{
int i;
struct
{
char x1;
char x2;
char x3;
char x4;
};
}u;
int main()
{
u.x1=0x12;
u.x2=0x34;
u.x3=0x56;
u.x4=0x78;
cout<<hex<<u.i<<endl;
u.x1=0x78;
u.x2=0x56;
u.x3=0x34;
u.x4=0x12;
cout<<hex<<u.i<<endl;
return 0;
}
第二次才会输出0x12345678的值,这也证明了IA32 CPU的逆序存储特性!具体来说,如果以0x00000000为起始地址,存储0x12345678的32位数据,则0x00000000为78则为逆序存储,而在0x00000004为78则为顺序存储!
我们知道在网络编程中数据都是顺序存储的,因此在逆序存储的平台上或者说编写跨平台的C++源码时,需要考虑一个问题——如何判断存储顺序:这个问题可以参考上面的数据结构来思考,当然方法还有N多种!
1.3:浮点类型
浮点类型包括float、double、long double三种数据类型,在我的机器上double 和long double都是64位的,在后面讨论的一些内容可能因CPU的不同而不同,我尽量描述交集部分!
1.3.1:浮点编码问题
首先我们来看一下浮点数的格式,如果你的记忆力不是很差,那么一定记得初中数学中提到的科学计数法,举例比如1.1*10^20、-1.1*10^20就是科学计数法的小数描述!抽象一样这些数包括浮号位+有效数+底数+指数组成!
在计算机中要编码小数,则浮点位可以用(-1)^s表示,当为0时为正数,当为1时为负数,底数一定为2,而指数和有效数在根据需要规定一定的存储空间!
综上所述得出如下公式:
(-1)^sM2^E
实际上国际标准就是规定了M底数与E指数的位数而已,在IEEE 754中定义了这个指数与有效位数的存储空间,如下所示:
类型 内存所占位数 符号位数 指数位数 有效位数
float 32 1 8 23
double 64 1 11 52
long double 80 1 15 64
在我的CPU上double与long double都占64字节!
IEEE 754制定如下规定:
1:当指数位全为0时,则浮点指数=1-[2^(e-1)-1],e表示指数位所占的bit位数,这时候有效位为0.xxx!
2:当指数位不全为0时,则浮点指数=E'-[2^(e-1)-1] E'表示不全为0的指数位所表示的数值,e表示指数所占的bit位1.xxx!
3: 当指数位全为1,如果M有效位数全为0则表示无穷大,相反则表示NAN Not Any Number!
就我的CPU来说,它根据实际情况有选择遵守了上面的标准,那么为了浮点运算IA32CPU提供了哪些浮点处理部件呢?
1.3.2:IA32 CPU的浮点处理单元
接下来讨论一下IA32CPU的浮点处理单元,包括如下几个部件:
1:数据寄存器st(0)-st(7)
这几个数据寄存器组成一个环形栈结构,就是一种栈结构,只不过就是数据进栈与出栈时需要进行各位数据的移位操作而已!下面看如下程序理解这个环境栈结构:
//理解IA32浮点存储单元的数据寄存器
#define FAST
#include "head.h"
int main()
{
int a,b=8;
for(int i=0;i!=8;++i)
{
__asm
{
mov eax,i
mov dword ptr ss:[ebp-4],eax
fild dword ptr ss:[ebp-4]
}
}
//outside the registers
__asm fild dword ptr ss:[ebp-8]
for(int j=0;j!=8;++j)
{
__asm
{
fistp dword ptr ss:[ebp-4]
}
cout<<a<<endl;
}
return 0;
}
//fild 1时的寄存器状态
ST0 valid 1.0000000000000000000
ST1 zero 0.0
ST2 empty -UNORM BBB0 01050104 00000000
ST3 empty 0.0
ST4 empty 0.0
ST5 empty 0.0
ST6 empty 0.0
ST7 empty 0.0
//fild 7时寄存器状态
ST0 valid 7.0000000000000000000
ST1 valid 6.0000000000000000000
ST2 valid 5.0000000000000000000
ST3 valid 4.0000000000000000000
ST4 valid 3.0000000000000000000
ST5 valid 2.0000000000000000000
ST6 valid 1.0000000000000000000
ST7 zero 0.0
//fild 8时的寄存器状态
ST0 bad -NAN FFFF C0000000 00000000
ST1 valid 7.0000000000000000000
ST2 valid 6.0000000000000000000
ST3 valid 5.0000000000000000000
ST4 valid 4.0000000000000000000
ST5 valid 3.0000000000000000000
ST6 valid 2.0000000000000000000
ST7 valid 1.0000000000000000000
//fistp1次时
ST0 valid 7.0000000000000000000
ST1 valid 6.0000000000000000000
ST2 valid 5.0000000000000000000
ST3 valid 4.0000000000000000000
ST4 valid 3.0000000000000000000
ST5 valid 2.0000000000000000000
ST6 valid 1.0000000000000000000
ST7 empty -NAN FFFF C0000000 00000000
2:浮点标志寄存器
如上你所看到的,诸如valid empty bad zero就是有标志寄存所标识的,它指明了当前数据寄存器的状态,valid是可用的,empty表示空,bad表示数据烂掉了,zero表示为0
3:其它
另外还有一个控制寄存器、一个标识运算结果状态的状态寄存器和最后一次数据指针寄存器、最后一次指令指针寄存器!
1.3.3:理解浮点数在IA32平台上的处理过程
以0x00600000为float类型为例来学习浮点处理过程:
//推理
0x00 60 00 00
S e M
0 000 0000 0 110 0000 0000 0000 0000 0000
s=0正浮点数,指数位 全为0使用1-[2^(e-1)-1]=-126,即指数为-126!底数为0.M即为0.11=0x75!
即0x00600000的浮点值表示为8.8162076311671563097655240291668e-39!
//验证对比
#define FAST
#include "head.h"
union U
{
float f;
struct
{
char x1;
char x2;
char x3;
char x4;
};
}u;
int main()
{
u.x1=0x00;
u.x2=0x00;
u.x3=0x60;
u.x4=0x00;
cout<<u.f<<endl;
return 0;
}
结果:8.81621e-039
结论:基本相同,出现舍入!
浮点的舍入问题是在精密编程中需要注意的,舍入问题同样有好几个标准,有兴趣的朋友可以在网上搜索相关内容,这里也将此部分内容直接舍入了!
二:变量
正如系列一中所讲到的变量的语法格式包含了C++的大部分基础内容,以此还是以变量定义语法来讨论变量的内容::
<存储类><数据类型><变量名>=<初值表达>,<变量名1>=<初值表达式>………;
存储类指明了变量存储的位置,数据类型指明了变量存储的内存大小,变量名用于标识这块内存,初值表达式用于初始内存空间!
2.1:存储类——rase
在C++中有register、auto、static 、extern四种存储类,我们简称为rase!数据存储的内存位置在机器级不外乎堆与栈,在现代并发操作系统中,register存储类基本不可能实现,因此不讨论!在代码级相对于人来说变化的量称变量,根据在{}内还是外分为全局与局部变量!同时在代码级为了编写大型软件,我们可能需要编写多个源码文件 ,为了使用其它文件 中的变量,引入了链接性的概念,因此存储类讨论的主要内容是外链接性和作用域!
1:局部auto——进栈,无链接性,作用域为局部
2:全局auto——进堆,具有外链接性,即可在其它文件中访问,作用域为全局
3:局部static——进堆,无外链接性,作用域局部
4:全局static——进堆,无外链接性,作用域全局
5:extern用于声明为了引入全局变量,用于定义为了引出全局符号常量
作用域遵守——强龙不压地头蛇的原则如下程序:
#define FAST
#include "head.h"
int a=0x61;
int main()
{
int a=0x62;
cout<<"now the value of a is 0x62"<<endl
<<hex<<a<<endl;
return 0;
}
强龙不压地头蛇,即全局变量与局部变量重名时,全局服从局部!
//extern在本文件中的声明
#define FAST
#include "head.h"
int main()
{
extern char *p;
cout<<p<<endl;
return 0;
}
char *p="caodan";
//extern声明其它文件中的全局变量
//head.h
char *p="caodan";
//api.cpp
#define FAST
#include "head.h"
int main()
{
extern char *p;
cout<<p<<endl;
return 0;
}
//extern用于定义引出符号常量
.//head.h
extern const int a=2;
//api.cpp
#define FAST
#include "head.h"
int main()
{ cout<<a<<endl;
return 0;
}
2.3:初始化化与字面常量
在C++中初始化有两种方法 ,一种是C语言中保留的初始化方法即依靠=(0x3d)来初始化,另外一种是面向对象中的构造函数来初始化!因此对于如下语句都是正确的:
int a=0x61;
int b(0x62);
对于构造函数我想在后面与类类型共同讨论,正如你看到的大部语句一样,初始化往往需要你0x61、0x62这样的数据进行操作,这些数据我们称为字面常量,对于字面常量来说,可以简单的分为整型字面常量、浮点字面常量和字符相关常量!
整型数据常量在C++中引用了计算机的三种进制描述——十六进制、八进制、十进制,十六进制用0x开头如上面所表示的,八进制用数字0开头,八进制与十六进制都无符号,而十进制则可以有正负表示!整型默认常量默认为int或long类型的!在讨论基本数据类型时我始终没有讨论bool类型,这是C++中很重要的一个类型,作为整型常量中的布尔常量来说,它只有两个值——true和false,为了方便以后看反汇编代码,我们先来分析一下debug版本程序中的一些入门级指令,编写如下代码:
#define FAST
#include "head.h"
bool g_a;
int main()
{
bool i_b;
if(!g_a)
{
cout<<"why it cout (check g_a)?"<<endl;
}
if(i_b)
{
cout<<"why it cout(chek i_b)?"<<endl;
}
return 0;
}
首先生成debug版的程序,然后执行程序会出现两次输出,而生成release版时,则只出现一次输出,分别反汇编这两个程序,这两个程序的不同代码主要体现在如下指令上
00401270 /> \55 PUSH EBP ;保存前一个栈帧的EBP
00401271 |. 8BEC MOV EBP,ESP;开辟一个新的栈帧
00401273 |. 83EC 44 SUB ESP,44 ;开辟一定的栈空
00401276 |. 53 PUSH EBX ;保存EBX
00401277 |. 56 PUSH ESI ;保存ESI
00401278 |. 57 PUSH EDI ;保存EDI
00401279 |. 8D7D BC LEA EDI,DWORD PTR SS:[EBP-44] ;得到EBP-44的地址
0040127C |. B9 11000000 MOV ECX,11 ;ecx计数为11次,11是有=44(总分配的内存空间字节数)/4(一个栈空间的字节数)得到的
00401281 |. B8 CCCCCCCC MOV EAX,CCCCCCCC;eax=0xcccccccc即int3
00401286 |. F3:AB REP STOS DWORD PTR ES:[EDI];重复11次初始化ebp-44开始的内存空间为eax的内容,int3!
在上面的变量g_a因为是全局变量,所以其值被系统初始化为0,因此!g_a=1即为真,所以不管debug还是release版都能看到这次输出第一输出语句,但是在release版中却只能输出第一条输出语句,而不能输出i_g中的判断语句,这就需要了解上面debug版程序添加的指令,栈帧实际上也是相对于编程人员来说的,对于栈帧的讨论我想在函数部分讨论,在这一部分我们重要的知道,debg版本的开始处将i_b变量的真赋值为0xcc,所以当if(i_b)的时候,实际上是判断的0xcc的值是否为0,关键反汇编指令如下:
004012C9 |> \8B45 FC MOV EAX,DWORD PTR SS:[EBP-4]
004012CC |. 25 FF000000 AND EAX,0FF
004012D1 |. 85C0 TEST EAX,EAX
这三条指令最需要理解的是and eax,0ff,这是一条mask运算指令,它的作用是将eax的低字节与ff进行与运算,而将高字节置为0!这时候test eax,eaxt实际上在比较0xcc与0xcc作为跳转的条件的,因此会输出相应的输出内容!
通过这个例子,我们看到debug版程序与release版程序的不同,这两种版本的程序,其根本不同在于是否进行了代码优化,因为debug版的程序没有进行代码优化所以在OD中使用bp xxxx下int 3断点时,往往不能断下相应的程序,比如下面的程序:
#define FAST
#include "head.h"
#pragma comment(linker,"/entry:\"mainCRTStartup\" /subsystem:\"windows\"")
#include <windows.h>
int main()
{
MessageBox(NULL,"dan","cao",MB_OK);
return 0;
}
当我们用OD载入debug版的程序时,试图bp MesageBoxA断下这个程序时,是无法断下程序的,而相反在生成release版本的程序上下上面的断点,却能奇际般断下,这除了int 3的调试机制外更重要的在于debug版程序与release版程序在编译最后是不事进行代码优化!
讨论了这些内容,主要是为了了解vc++开发环境!
浮点字面常量,浮点字面常量有两种描述方法一种是通过小数点描述,一种是科学计数法的描述,一定要注意我们现在讨论的内容是在代码级讨论的,对于这两种描述在机器级都会用IEEE 754的实际标准进行存储!
正如系列1所说的人机模型一样,C++所能描述的任何符号都可以当作字符串进行处理,而使得符号成为字符串的方法是将字符用””括起来,当然与一般的事物一样,字符串也是有字符组成的,对于字符我们使用单引号将其括起来表示为字符!要理解字符与字符串只要理解如下几个常量的存储大小就可以了,示例程序如下:
#define FAST
#include "head.h"
int main()
{
cout<<sizeof('a')<<" "<<sizeof("a")<<" "<<sizeof(L'a')<<" "<<sizeof(L"a")<<endl;
return 0;
}
结果为:1 2 2 4
字符’a’是一个char类型,因此占一个字节
字符串”a”,是一个字符串,其结尾为\0,即NULL控制符,因此占两个字节
宽字符L’a’是一个wchar_t类型,它是一个short类型,因此占两个字节
宽字符串L”a”是一个wchar_t类型的字符串,其结尾也为\0即NULL控制符,因此占4个字节!
通过实例说明,字符串有字符+NULL结束符组成,那么为什么要在结尾加上一个NULL结束符呢?答案是为了方便字符指针的自动操作!当字符指针指向最后个元素为NULL时,指针无效,即停止操作!
举例说明:输出如下图形:
*******
******
*****
****
***
**
*
可能你会使用各种for循环嵌套来完成,实际上使用字符指针接合NULL控制位可以更容易的理解:
#define FAST
#include "head.h"
int main()
{
char *p="*******";
while(*p)
{
cout<<p<<endl;
++p;
}
return 0;
}
这个例子说明了两点关键的内容,即字符串是为了与计算机交互而引入的,上面的符号输出即为与计算机进行交互,所以也是一种字符串,在此基础上结合字符串的特性,编写出了上面的程序!
对于字符串的讨论,后面会进行详细的讨论,这里作为了解,理解计算机的三种常量就可以了,实际上字面常量只有两种作用,一是初始化变量,二是实现人机交互!
在系列一中,我们提到了变量与常量的阴阳关系,阳中有阴指的是字面常量,而阴中有阳指的是符号常量,所以接下来我们会讨论符号常量的内容。
2.3:符号常量
符号常量指的是分配内存空间的常量, 符号常量的定义方法有两种一种是使用#define 宏处理,一种是使用const关键字定义!
本系列教程中不会涉及预处理的内容,因此这里我们将#define讨论的细一点!
#define宏处理机制是进行符号替换,比如下面的代码:
#define cao 1
在编译时,预处理阶断会将所有cao的符号,替换为1,当然使用#define替换为字符串时需要遵守字符串的组成规则即字符+NULL控制位!如下:
#define dan “cao”
在编译时将dan替换为字符串cao!
在C++的函数库中有些函数使用的是宏定义,比如下面定义了一个Cout(s)的宏
#define FAST
#include "head.h"
#define Cout(s) cout<<s<<endl;
int main()
{
Cout(1);
return 0;
}
预处理会将Cout(1)替换为cout<<1<<endl;语句,对于字符串的输出需要另外一个预处理指令——#,我们知道每条预处理语句都是以#号开头,实际上#号本身也是一个预处理指令,它的作用是将要处理的数据当做字符串进行处理比如:
#define FAST
#include "head.h"
#define Cout(s) cout<<#s<<endl;
int main()
{
Cout(hello world!);
return 0;
}
预处理器会将Cout(hello world!)替换为cout<<”hello world!”<<endl;提到了#号就不得不提及##,它也是一个预处理指令,它的作用是将两个数值连接到一起,如下程序:
#define FAST
#include "head.h"
#define Cout(s1,s2) cout<<#s1 ## #s2<<endl;
int main()
{
Cout(hello,world!);
return 0;
}
预处理器会将Cout(hello,world!),替换为cout<<”helloworld!”<<endl;!
正如你所看到的#dfine宏只是进行简单的替换,而C++中提供了另一个有意思的关键字typedef,它的作用是将已知类型进行更名,比如typedef int cao;那么int类型的数据我们可以使用cao来指明其数据类型为int!它与宏定义有着本质的区别,比如下面的程序:
#define FAST
#include "head.h"
#define pchar char *
typedef char * ppchar;
int main()
{
pchar a,b;
ppchar c,d;
cout<<sizeof(a)<<" "<<sizeof(b)<<" "<<sizeof(c)<<" "<<sizeof(d)<<endl;
return 0;
}
结果:4 1 4 4
通过分析内存所占字节可知,当使用宏定义char *的时候,只进行了替换,所以pchar a,b实际上为char *a,b;而b为一个char类型,故所占字节为1,而typedef则与之不同!typedef与#define在形式上的不同是前者是一条语句而后者则是预处理,因此前者有结束符;而后者则没有!
与#define符号常量定义的机制一样,const定义的常量也是替换,只不过替换发生在编译时,如下面的程序:
#define FAST
#include "head.h"
int main()
{
const int a=2;
cout<<a<<endl;
return 0;
}
//反汇编
00401288 C745 FC 02000000 mov dword ptr ss:[ebp-4],2
0040128F 8BF4 mov esi,esp
00401291 A1 E0504100 mov eax,dword ptr ds:[<&MSVCP60D.std::endl>]
00401296 50 push eax
00401297 8BFC mov edi,esp
00401299 6A 02 push 2
0040129B 8B0D DC504100 mov ecx,dword ptr ds:[<&MSVCP60D.std::cout>] ; MSVCP60D.std::cout
我们看到符号常量进行了赋值,然后当调用cout<<a<<时,直接将2作为参数传递给操作符重载函数operator<<(),而并没有像常规变量那样mov eax,dword ptr ss:[ebp-4] push eax,所以const的机制还是一种替换,只不过是在编译时进行的替换而已!
另外需要注意的一点是,符号常量既然是常量那么它的值不能修改,这句话应该加上一句,在源码级,在机器级符号常量依旧是一个变量,考虑下面程序的输出结果:
#define FAST
#include "head.h"
int main()
{
const int a=2;
__asm mov dword ptr ss:[ebp-4],10;
int b=a;
cout<<b<<endl;
return 0;
}
结果为2,正是因为const类型在编译时进行替换,所以当我们使用汇编指令修改其值的时候,虽然能修改成功,但是程序的指令并不会因此而改变,还是push 2将参数压入栈空间!因此const常量的安全性实际上源码级的,有编译器实现的,而对机器来说,它只是和变量一样的一个内存空间,所以我们称其为是一种值不变的变量,即阴中有阳!
提到const我们往往需要讨论c-v限定,这里的V即volatile关键字,实际上volatie的前缀主要是为了限制编译器进行代码优化,对比如如下两个程序:
//1.exe
#define FAST
#include "head.h"
int main()
{
int a=2;
cout<<a<<endl;
return 0;
}
反汇编如下:
00401000 >/$ 8B0D 08204000 MOV ECX,DWORD PTR DS:[<&MSVCP60.std::cou>; MSVCP60.std::cout
00401006 |. 6A 02 PUSH 2
00401008 |. FF15 04204000 CALL DWORD PTR DS:[<&MSVCP60.std::basic_>; MSVCP60.std::basic_ostream<char,std::char_traits<char> >::operator<<
0040100E |. 50 PUSH EAX
0040100F |. FF15 00204000 CALL DWORD PTR DS:[<&MSVCP60.std::endl>] ; MSVCP60.std::endl
00401015 |. 83C4 04 ADD ESP,4
00401018 |. 33C0 XOR EAX,EAX
0040101A \. C3 RETN
在输出a时,代码优化后将2直接做为参数入栈了!
//2.exe
#define FAST
#include "head.h"
int main()
{
volatile int a=2;
cout<<a<<endl;
return 0;
}
反汇编如下:
00401000 >/$ 51 PUSH ECX
00401001 |. C74424 00 020>MOV DWORD PTR SS:[ESP],2
00401009 |. 8B4424 00 MOV EAX,DWORD PTR SS:[ESP]
0040100D |. 8B0D 08204000 MOV ECX,DWORD PTR DS:[<&MSVCP60.std::cou>; MSVCP60.std::cout
00401013 |. 50 PUSH EAX
00401014 |. FF15 04204000 CALL DWORD PTR DS:[<&MSVCP60.std::basic_>; MSVCP60.std::basic_ostream<char,std::char_traits<char> >::operator<<
0040101A |. 50 PUSH EAX
0040101B |. FF15 00204000 CALL DWORD PTR DS:[<&MSVCP60.std::endl>] ; MSVCP60.std::endl
00401021 |. 33C0 XOR EAX,EAX
00401023 |. 83C4 08 ADD ESP,8
00401026 \. C3 RETN
参数通过mov eax,先保存变量a的值,然后通过push eax将参数入栈,这些指令实现了变量a的调用是一个即时值,比如从某个端口中读取数据,而这个数据在编程的时候是并不能知道的,而通过volatile限定可以保证每次读取的数据都是新的而对比1.exe,编译器直接优化为push 2,如果数据从端口中得到,那么这时候得到的数据就伪数据,因此程序实际上坏掉的!正是volatile能保证编译器生成的代码读取限时数据,而const变量是在编译时进行替换,于是将它们合称为C-V限定!
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!