在c中基本数据类型分为:char,short,int,long,float,double
以上数据类型除float和double外均可以分为有符号(singed)和无符号(unsigned)两类
有符号时最高位为符号位,用来表示数据的正负
无符号情况下最高位为正常的数据位不做特殊含义
类型 | 占位 |
---|---|
Char | 1 |
short | 2 |
int | 4 |
long | 8 |
float | 4 |
Double | 8 |
浮点数类型是比较特殊的,首先他是交给专门的cpu来处理的,比如在80386中就引入了8087协处理器来专门处理浮点数的计算
C中的浮点数存储方式采用了浮点实数存储方式,也就是在全部二进制位上选取一段用来表示实数另一段表示小数点的位置,如952.7可以分为9527和0.1
C的浮点数的编码采用的是ieee标准编码格式,
如float类型下将浮点数分为三部分:符号位(1bit)、小数位(8bit)、实数位(23bit)
double:符号位(1bit)、小数位(11bit)、实数位(52bit)
举例:12.25f拆分:符号位:0
小数位:1000 0010
实数位:10001 后续均为0
字符类型是根据字符的编码格式将对应字符的数字表示存储为二进制。
具体的字符编码解析可以看我另外一篇文章:https://blog.csdn.net/qq_43147121/article/details/127968159
在C中用指针类型(TYPE*)来表示一个用来存储一个地址的DWORD类型,用&符号来表示取一个变量的地址
如:int* a;此时a则会被认为是一个指针类型,在对a进行操作时则会被编译器编译为汇编中的间接操作
举例:
int tmp = 10;
int* a = &tmp;
(*a)+=1;
对应的汇编简单来写如下:
mov dword ptr [esp],0Ah; 将10存在栈中
lea eax,[esp] 取得tmp所在的地址
mov dword ptr [esp-4],eax 将tmp所在的地址存储到栈中
mov ecx,dword ptr [esp-4] 取出tmp所在的地址
mov ecx,dword ptr [eax]
add dword ptr ecx,1 将tmp所在地址所指向的内容加一
mov dword ptr[eax],ecx
在c中以引用类型(type&)来表示一个操作的集合,每次对这个引用类型的操作都是取变量的内容将内容作为地址修改此地址中的数据并写回的一个操作的集合
举例:
int tmp = 10;
int& a = &tmp;
a+=1;
对应的汇编简单来写如下:
mov dword ptr [esp],0Ah; 将10存在栈中
lea eax,[esp] 取得tmp所在的地址
mov dword ptr [esp-4],eax 将tmp所在的地址存储到栈中
mov ecx,dword ptr [esp-4] 取出tmp所在的地址
mov ecx,dword ptr [eax]
add dword ptr ecx,1 将tmp所在地址所指向的内容加一
mov dword ptr[eax],ecx
可以看到引用类型和指针类型操作编译为汇编其实是基本一样的,区别就在于指针类型变量所存储的地址也可以进行算术运算
举例:
int tmp = 10;
int* a = &tmp;
a++;
对应的汇编简单来写如下:
mov dword ptr [esp],0Ah; 将10存在栈中
lea eax,[esp] 取得tmp所在的地址
mov dword ptr [esp-4],eax 将tmp所在的地址存储到栈中
mov eax,dword ptr [esp-4]
add eax,4 此时加的不在是1而是当前指针所表示类型的大小
mov dowrd ptr [esp-4],eax
常量类型表示在程序运行前便已久可以确认的数据,一般存储在只读数据区,这块内存在页的属性上便是不可写只可读,所以对这段内存的写操作都会抛出内存访问异常。
常量举例:如define所定义的常量,或者char* str = “ABC”;这种方式所定义的字符串。
注意const修饰符所修饰的变量并不意味着是在内存层面上的常量,他仅仅是编译器会在编译过程中进行检测,在程序运行中完全可以通过取地址并修改的间接修改方式对其内存数据进行修改。
在内存的识图中并没有函数这一个说法只存在段的层级每个段都有自己的内存属性可读可写可执行等待,函数的目的便是能够将某一段内存明确的用一种概念来分开,而不至于将全部的代码片段都混杂在一段内存中而没有明确的一个分界和定义。
函数简单的来看便是将一块代码封装到一起。下面直接反汇编一个函数的调用看一下
首先要说明的是ebp代表了栈底指针,esp代表了栈顶指针
c代码
int test(int a,int b){
return a+b;
}
int _tmain(int argc, _TCHAR* argv[])
{
int a=10,b=1;
int res = test(a,b);
printf("%d",res);
return 0;
}
简单汇编代码:
int test(int a,int b){
009D1A50 push ebp //同样是保存和初始化堆栈
009D1A51 mov ebp,esp
009D1A53 sub esp,0C0h
009D1A59 push ebx
009D1A5A push esi
009D1A5B push edi
009D1A5C lea edi,[ebp-0C0h]
009D1A62 mov ecx,30h
009D1A67 mov eax,0CCCCCCCCh
009D1A6C rep stos dword ptr es:[edi]
return a+b;
009D1A6E mov eax,dword ptr [a] //取出将a,b做合
009D1A71 add eax,dword ptr [b] //此处的a是ebp+4h,b是ebp+8h
}
009D1A74 pop edi
009D1A75 pop esi
009D1A76 pop ebx
009D1A77 mov esp,ebp
009D1A79 pop ebp //回退堆栈
009D1A7A ret //返回
int _tmain(int argc, _TCHAR* argv[])
{
009D1AF0 push ebp //保存ebp
009D1AF1 mov ebp,esp //将栈底指向当前栈顶
009D1AF3 sub esp,0E4h //提升堆栈
009D1AF9 push ebx //保存寄存器
009D1AFA push esi
009D1AFB push edi
009D1AFC lea edi,[ebp-0E4h] //初始化堆栈内容
009D1B02 mov ecx,39h
009D1B07 mov eax,0CCCCCCCCh
009D1B0C rep stos dword ptr es:[edi]
int a=10,b=1; //这里开始进入我们在main中写的代码
009D1B0E mov dword ptr [a],0Ah //a其实是ebp-4h,这里将10存入到ebp-4,也就是栈底的第 一个4字节内存
009D1B15 mov dword ptr [b],1 //这里同上b是ebp-8h,将1放入栈底开始的第二个4字节中
int res = test(a,b); //下面要注意,下面压栈是从esp开始压栈,前面的通过ebp 所操作的赋值语句是将内容存放到开始提升堆栈所占有的内存
009D1B1C mov eax,dword ptr [b] //这里是取出1到eax
009D1B1F push eax //将eax压栈
009D1B20 mov ecx,dword ptr [a] //取出10到ecx
009D1B23 push ecx //ecx压栈
009D1B24 call func (9D126Ch) //调用我们的test方法此时可以看做一个 jmp详细的后续再讲
009D1B29 add esp,8 //平衡传入参数时提升的堆栈
009D1B2C mov dword ptr [res],eax //eax便是返回值
printf("%d",res);
009D1B2F mov esi,esp
009D1B31 mov eax,dword ptr [res]
009D1B34 push eax
009D1B35 push offset string "%d" (9D774Ch)
009D1B3A call dword ptr [__imp__printf (9DA40Ch)]
009D1B40 add esp,8
009D1B43 cmp esi,esp
009D1B45 call @ILT+435(__RTC_CheckEsp) (9D11B8h)
return 0;
009D1B4A xor eax,eax
}
从上面的例子可见函数的调用便是从代码段中的一块跳转到另一块去执行,在执行结束后再返回,
函数的参数是通过栈来传递的,在函数结束后要重新保证栈回退到调用函数之前的状态。
其次call命令可以分为两个部分
函数调用的约定分为三类
结构体就是将一系列数据整合到一起的一块内存,下面通过例子来看一下
struct test_struct{
int a;
char b;
int c;
};
int _tmain(int argc, _TCHAR* argv[])
{
struct test_struct s;
s.a = 10;
s.b = 11;
s.c = 12;
test(&s);
return 0;
}
首先建立了一个结构体有三个参数
先来看一下结构体在内存中的存储方式
int _tmain(int argc, _TCHAR* argv[])
{
。。。。。。。
struct test_struct s;
s.a = 10;
00E524DE mov dword ptr [s],0Ah //这里的s可以简单看为ebp-4
s.b = 11;
00E524E5 mov byte ptr [ebp-0Ch],0Bh
s.c = 12;
00E524E9 mov dword ptr [ebp-8],0Ch
test(&s);
00E524F0 lea eax,[s] //lea为取地址的指令,前面我们也遇到过
00E524F3 push eax //将这个地址作为参数传递
00E524F4 call test (0E511B8h)
00E524F9 add esp,4
return 0;
00E524FC xor eax,eax
。。。。。
}
可以看出来结构体在内存中的存储方式便是将数据按顺序排放在内存中并根据字段类型的大小计算偏移量来取得对应的字段内容
如果我们直接将struct关键字改为class看看会不会出错
class test_struct{
public:
int a;
char b;
int c;
};
void test(test_struct* s){
printf("%d",s->a);
}
int _tmain(int argc, _TCHAR* argv[])
{
test_struct s;
s.a = 10;
s.b = 11;
s.c = 12;
test(&s);
return 0;
}
改后的代码,完全可以运行
并且如果看返汇编的话会发现汇编代码也没有变化
下面我们将函数放到class中看一下汇编是否会有变化
class test_struct{
public:
int a;
char b;
int c;
void test(test_struct* s){
printf("%d",s->a);
}
};
int _tmain(int argc, _TCHAR* argv[])
{
test_struct s;
s.a = 10;
s.b = 11;
s.c = 12;
s.test(&s);
return 0;
}
汇编只看main这部分的代码
int _tmain(int argc, _TCHAR* argv[])
{
。。。。。
test_struct s;
s.a = 10;
00D3339E mov dword ptr [s],0Ah
s.b = 11;
00D333A5 mov byte ptr [ebp-0Ch],0Bh
s.c = 12;
00D333A9 mov dword ptr [ebp-8],0Ch
s.test(&s);
00D333B0 lea eax,[s]
00D333B3 push eax
00D333B4 lea ecx,[s]
00D333B7 call test_struct::test (0D311D6h)
return 0;
00D333BC xor eax,eax
。。。。。。。。
}
注意 lea ecx,[s] 这段代码,这个ecx便是所谓的this指针,通过编译器将结构体自己的地址作为参数传入函数这样就可以通过this符号访问结构体自己了。其余的部分完全没有变化,调用class的函数时也是通过地址调用的。
注意:数据在内存中的存储还取决于数据对齐,这部分的知识在我前面的笔记中有详细解析
面向对象的特性有
封装在上一块我们已经看过了,便是将操作数据的算法和存放数据的结构体封装到一起来调用,真正的实现通过编译器来实现。
下面说一下继承
首先写一个结构体