裸函数
void __declspec(naked) Function(){} 调用该函数会报错
void __declspec(naked) Function(){
__asm ret
}//正确写法
无返回值,无参数
#include "stdafx.h"
void __declspec(naked) Function()
{
__asm
{
push ebp
mov ebp,esp
sub esp,0x40
push ebx
push esi
push edi
lea edi,dword ptr ds:[ebp-0x40]
mov eax,0xCCCCCCCC
mov ecx,0x10
rep stosd
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
}
}
有返回值,有参数
int __declspec(naked) Function2(int x,int y)
{
__asm
{
push ebp
mov ebp,esp
sub esp,0x40
push ebx
push esi
push edi
lea edi,dword ptr ds:[ebp-0x40]
mov eax,0xCCCCCCCC
mov ecx,0x10
rep stosd
mov eax,dword ptr ds:[ebp+0x8]
add eax,dword ptr ds:[ebp+0xC]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
}
}
带局部变量的函数框架
int __declspec(naked) Function3(int x,int y)
{
__asm
{
push ebp
mov ebp,esp
sub esp,0x40
push ebx
push esi
push edi
lea edi,dword ptr ds:[ebp-0x40]
mov eax,0xCCCCCCCC
mov ecx,0x10
rep stosd
mov dword ptr ds:[ebp-0x4],3
mov dword ptr ds:[ebp-0x8],2
mov eax,dword ptr ds:[ebp+0x8]
add eax,dword ptr ds:[ebp+0xc]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret
}
}
使用汇编写c函数
#include "stdafx.h"
int __declspec(naked) function(int x,int y,int z)
{
__asm
{
//保存原来栈底
push ebp
//提升堆栈
mov ebp,esp
sub esp,0x40
//保存现场
push edi
push esi
push ebx
//填充缓冲区
mov eax,0xCCCCCCCC
mov ecx,0x10
lea edi,dword ptr ds:[ebp-0x40]
rep stosd
//该方法真正要执行的功能
//局部变量:ebp-4开始的 参数:ebp+8开始的 ebp+4:eip ebp:原来的栈底(调用前的ebp)
mov dword ptr ds:[ebp-0x4],2
mov dword ptr ds:[ebp-0x8],3
mov dword ptr ds:[ebp-oxC],4
mov eax,dword ptr ds:[ebp+0x8]
add eax,dword ptr ds:[ebp+0xC]
add eax,dword ptr ds:[ebp+0x10]
add eax,dword ptr ds:[ebp-0x4]
add eax,dword ptr ds:[ebp-0x8]
add eax,dword ptr ds:[ebp-0xC]
//恢复现场
pop ebx
pop esi
pop edi
//恢复堆栈
mov esp,ebp
pop ebp
ret
}
}
int main(int argc, char* argv[])
{
function(1,2,3);
return 0;
}
函数调用约定
调用约定 | 参数压栈顺序 | 平衡堆栈 |
---|---|---|
_cdecl | 从右至左入栈 | 调用者清理栈 |
_stdcall | 从右至左入栈 | 自身清理堆栈 |
_fastcall | ECX,EDX传送前两个,剩下的:从右至左入栈 | 自身清理堆栈 |
int _cdecl plus(int a,int b)
{
return a+b;
}
push 2
push 1
call
add esp,8
内存图
if…else while 可读,可执行 | 代码区 |
参数、局部变量、临时数据 | 堆栈 |
动态申请的,大小是可变的 可读可写 | 堆 |
int x; 可读可写 | 全局变量区 |
只读 | 常量区 |
类型转换
movsx/movzx
MOVSX 先符号扩展,再传送(有符号)
MOV AL,OFF 1111 1111
MOVSX CX,AL 1111 1111 1111 1111
MOV AL,80 1111 0000
MOVSX CX AL 1111 1111 1111 0000
MOVZX 先零扩展,再传送(无符号)
MOV AL,OFF 1111 1111
MOVSX CX,AL 0000 0000 1111 1111
MOV AL,80 1111 0000
MOVSX CX AL 0000 0000 1111 0000
float类型转16进制
IEEE规则
将float类型的12.5转换成16进制
整数:12 -> C:1100
0.5*2=1.0 1
1100.1 =1.1001 * 2^3 指数:3
1.1001中的第一个1不用记,127+(3)=130 =0x82=1000 0010
31符号位(必须指定正负) | (小数点后的值) | |
---|---|---|
0 | 00000000 | 00000000000000000000000 |
0 | 1000 0010 | 10010000000000000000000 |
0100 0001 0100 1000 0000 0000 0000 0000
函数
返回值
char类型的返回值存储到al中,即返回值数据类型宽度为8位时,用al存储结果
short类型的返回值存储到ax中
int类型的返回值存储到eax中
宽度为64位的返回值数据类型怎么存储?使用两个32位寄存器来存储
局部变量
数据类型小于等于32位的局部变量,空间在分配时,按32位分配;但使用时按实际的宽度使用
不要定义char/short类型的局部变量(32位计算机用int效率最高,原理还是按照本机尺寸规则)
局部变量会影响编译器在函数调用初始化时的缓冲区空间:VC6中的编译器默认提升堆栈,开辟的缓冲区(内存)大小为0x40字节–64字节,即16块内存单元(一块内存单元32位,即4字节),如果函数中每定义了一个任意类型局部变量,开辟的缓冲区大小就增加4字节(32位)。比如现在函数中定义了两个任意类型局部变量,那么开辟的缓冲区大小为0x48
注意不同的编译器开辟的大小规则是不一样的(vs2019为0xC)
传参
将上层函数的变量,或者表达式的值“复制一份”,传递给下层函数
先把局部变量值复制一份,存到eax中再入栈
在函数中运算的是复制过来的值,而不是main函数中局部变量x本身的值
结构体
字节对齐
为什么要有字节对齐:本质上是因为效率问题。编译器在存储数据时要考虑两个因素:效率和空间。有时为了查找数据时更快而舍弃了空间占用小的原则;有时候为了使占用空间最小不浪费而舍弃了存与读取的效率问题
给结构体指定字节对齐数
#pragma pack的基本用法为
/*
#pragma pack(n)
结构体。。。
#pragma pack()
*/
#pragma pack(8)
struct Test{
int a ;
__int64 b ;
char c ;
};
#pragma pack()
n为字节对齐数,其取值为1、2、4、8,VC6默认是8
为结构体成员分配内存时最终使用的字节对齐数是–结构体的字节对齐数与结构体成员的sizeof值小的那个
字节对齐数通用规律
四种字节对齐数分配结构体内存的通用规律:
1.为结构体成员分配内存时最终使用的字节对齐数是–结构体的字节对齐数与结构体成员的sizeof值小的那个
2.第一个数据成员放在offset为0的地方,以后每个结构体成员存储的起始位置是此成员最终使用的字节对齐数的整数倍。(上一个成员有空出来的字节就空出来不管)
3.如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储
字节对齐数为1
#pragma pack(1) //字节对齐数为1
struct Test{
int a;
__int64 b;
char c;
};
#pragma pack() //按照1字节对齐数分配内存给结构体,一共分配了4 + 8 + 1 = 13字节内存
字节对齐数为2
#pragma pack(2) //字节对齐数为2
struct Test{
int a;
char c;
};
#pragma pack() //按照2字节对齐数分配内存给结构体,一共分配了4 + 2 = 6字节内存
字节对齐数为4
#pragma pack(4) //字节对齐数为4
struct Test{
int a;
char b
__int64 c;
char d;
};
#pragma pack() //按照4字节对齐数分配内存给结构体,一共分配了4+1+3+8+1+3=20字节内存
字节对齐数为8
#pragma pack(8) //字节对齐数为8
struct Test{
int a;
__int64 b;
char c;
};
#pragma pack() //按照8字节对齐数分配内存给结构体,一共分配了4 + 4 +8 + 1 + 7 = 24字节内存
- 分配
int a
的内存时,int宽度为4,结构体字节对齐数为8,则取最小值4作为此成员的字节对齐数。那么前4个字节内存从offset0开始分配给int a
- 接着分配
__int64 b
的内存,__int64
宽度为8字节,结构体字节对齐数为8,相等则取8作为此成员的字节对齐数。但是由于__int64 b
的起始地址要为8的整数倍,所以只能空出来4字节内存,从offset8开始存储,分配8字节内存 - 最后分配char c的内存,char宽度为1字节,结构体字节对齐数为8,取最小值1作为此成员的字节对齐数,起始地址为1的倍数,所以直接接着上一个成员的内存分配即可,即从offset16开始存储,分配1字节内存。但是由于结构体的字节对齐数8 = 成员最大宽度8,满足规则3.1,那么最后要补7字节。综上最终结构体的分配的内存大小为4 + 4 +8 + 1 + 7 = 24字节
按照数据类型由小到大的顺序进行书写,这样浪费的内存空间更多少。
分析下面结构体的内存分配
#pragma pack(4)
struct S1{//12
char a;
double b;
};
#pragma pack()
#pragma pack(4)
struct S2{//16
int a;
char b[10];
};
#pragma pack()
#pragma pack(4)
struct S3{//20
char a;
S1 s;
char b;
char c;
};
#pragma pack()
#pragma pack(4)
struct S4{//28
char a;
S1 s;
char b;
double c;
};
#pragma pack()
void main(int argc,char* argv[]){
printf("%d %d %d %d",sizeof(S1),sizeof(S2),sizeof(S3),sizeof(S4));
//12 16 20 28
}
指针
带\*
类型的变量宽度永远是4字节、无论类型是什么,无论有几个\*
运算
带*类型变量做++或者–
带\*
类型的变量,++ 或者 – 新增(减少)的数量是去掉一个*后变量的宽度
int main(int argc,char* argv[])
{
char* x;
short* y;
int* z;
x = (char*)100;
y = (short*)100;
z = (int*)100;
x++; //char*去掉*此时类型为char,所以x++即100 + 1
y++; //short*去掉*此时类型为short,所以y++即100 + 2
z++; //int*去掉*此时类型为int,所以z++即100 + 4
//得到的结果依然是原来的带*类型
printf("%d %d %d",x,y,z); //101 102 104
return 0;
}
带两个*
号变量++(或者–),同样先去掉一个*
号,然后加上或者减去此时类型的宽度
int main(int argc,char* argv[])
{
char** x;
short** y;
int** z;
x = (char**)100;
y = (short**)100;
z = (int**)100;
x--; //char**去掉一个*此时类型为char*,所以x--即100 - 4
y--; //short**去掉一个*此时类型为short*,所以y--即100 - 4
z--; //int**去掉一个*此时类型为int*,所以z--即100 - 4
printf("%d %d %d",x,y,z);//96 96 96
return 0;
}
带*类型变量加或减一个整数
带\*
类型的变量可以加、减一个整数,但不能乘或者除(也不能加减浮点数)。得到的结果依然是带\*
类型
- 带
*
类型变量 + N = 带*
类型变量 + N * (去掉一个*
后类型的宽度) - 带
*
类型变量 - N = 带*
类型变量 - N * (去掉一个*
后类型的宽度)
int main(int argc,char* argv[])
{
char* a ;
short* b ;
int* c ;
a = (char*)100;
b = (short*)100;
c = (int*)100;
a = a + 10; //char*去掉一个*号后为char,即宽度为1字节,所以a + 5 = 100 + 10 * 1
b = b + 10; //short*去掉一个*号后为short,即宽度为2字节,所以b + 5 = 100 + 10 * 2
c = c - 10; //int*去掉一个*号后为int,即宽度为4字节,所以c - 5 = 100 - 10 * 4
printf("%d %d %d",a,b,c); //110 120 60
return 0;
}
两个带*类型变量求差值
-
两个类型相同(带
*
的数量相同)的带*
类型的变量可以进行减法操作(不能进行加法!) -
相减的结果要除以去掉一个
\*
的数据的宽度,因为结果是int类型,结果向下取整(因为汇编中使用的sar,表示右移)
int main(int argc,char* argv[])
{
short* a ;
short* b ;
a = (short*)200;
b = (short*)100;
int x = a - b; //int*去掉一个*后为int,宽度为4
printf("%d\n",x); //(200-100)/4 = 25
char**** c ;
char**** d ;
c = (char****)200;
d = (char****)100;
int y = c - d; //char****去掉一个*后为char***,宽度为4
printf("%d\n",y); //(100-200)/4 = 25
return 0;
}
模拟CE的查找功能
查找这些数据中,有几个id=1 level=8的结构体信息
#include<stdio.h>
//定义结构体
struct Test {
int id;
int level;
};
//数据
char data[100] = { //全局变量
0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x07,0x09,
0x00,0x20,0x10,0x03,0x03,0x0C,0x00,0x00,0x44,0x00,
0x00,0x33,0x00,0x47,0x0C,0x0E,0x00,0x0D,0x00,0x11,
0x00,0x00,0x00,0x02,0x64,0x00,0x00,0x00,0xAA,0x00,
0x00,0x00,0x64,0x10,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x02,0x00,0x74,0x0F,0x41,0x00,0x00,0x00,
0x01,0x00,0x00,0x00,0x08,0x00,0x00,0x00,0x0A,0x00,
0x00,0x02,0x74,0x0F,0x41,0x00,0x06,0x08,0x00,0x00,
0x00,0x00,0x00,0x64,0x00,0x0F,0x00,0x00,0x0D,0x00,
0x00,0x00,0x23,0x00,0x00,0x64,0x00,0x00,0x64,0x00
};
void finddata(int id,int level)
{
for (int i = 0; i < 100 - 7; i++)
{
Test* p = (Test*)(data + i);
if (p->id == 1&& p->level==8)
printf("%x\t%x\t%d\n", *(data + i),*(data+i+4),i);
}
}
int main(int argc, char* argv[]) {
Test t;
t.id = 1;
t.level = 8;
finddata(t.id, t.level);
return 0;
}
数组指针
和指针数组不同
指针数组:本质上数组,只是当中的元素是指针类型的
数组指针:本质上就是指针。和结构体指针、指针的指针类似,结构体指针指向结构体,指针的指针指向指针,那么数组指针就是指向数组的指针
定义
int arr[5] = {1,2,3,4,5}; //这是数组
int (*p)[5]; //这是数组指针。int* p[5]是指针数组!
p = (int (*)[5])arr; //使数组指针指向arr数组,p中存的是arr数组的首地址
运算
int (*p)[5];
p = (int (*)[5])10;
p++; //10 + 4 * 5 = 30
获取元素
*§是int [5]类型
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int (*p)[2];
p = (int (*)[2])arr;
printf("%d",*(*p));//arr[0]
printf("%d",*(*(p+1));//arr[2]
printf("%d",*(*p+1));//arr[1]
多维数组指针
不管是一维数组、二维数组还是三维数组本质上他们的数据在内存中都是一维的形式存储的
二维
py是int (*)[2][3]类型,去掉一个*号后的类型为int [2][3],宽度为2*3*4,所以py+2相当于从py的初始地址+ 2*3*4*2。
*(py)类型为int [2][3],去掉一个*号后的类型为int [3]类型,宽度为3*4,所以*(py)+2相当于此时的地址 + 3*4*2 = 24
*(*(py))的类型为int [3]类型,去掉一个*号后的类型为int类型,所以*(*(py)+2)相当于此时的地址 + 4*2
int arr[20] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19 };
int(*py)[2][3]; //定义int (*)[2][3]类型的二维数组指针py
py = (int(*)[2][3])arr; //py指向data数组首地址
printf("%d\n",**(*py);//arr[0]
printf("%d\n", *(*(*(py + 2))));//4*2*3*2=48 48/4=12 arr[12]
printf("%d\n", *(*(*(py)+2)));//4*3*2=24 24/4=6 arr[6]
printf("%d\n", *(*(*(py)) + 2));//4*2*2=16 16/4=4 arr[2]
三维
pz是int (*)[2][3][2]类型,去掉一个*号后的类型为int [2][3][2],宽度为2*3*2*4,所以pz+2相当于初始地址+ 2*3*2*4*2
*(pz)类型为int [2][3][2],去掉一个*号后的类型为int [3][2]类型,宽度为3*2*4,所以*(pz)+2相当于此时的地址+3*2*4*2
*(*(pz))的类型为int [3][2]类型,去掉一个*号后的类型为int[2]类型,宽度为2*4,所以*(*(pz))+2相当于此时的地址+2* 4*2
*(*(*(pz)))的类型为int [2]类型,去掉一个*号后的类型为int类型,所以*(*(*(pz)))+2相当于此时的地址 + 4*2
int arr[30] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29};
int(*pz)[2][3][2];
pz = (int(*)[2][3][2])arr;
printf("%d\n", ***(*pz));//arr[0]
printf("%d\n", *(*(*(*(pz + 2)))));//4*2*3*2*2=96 96/4=24 arr[24]
printf("%d\n", *(*(*(*(pz)+2))));//4*3*2*2=48 48/4=12 arr[12]
printf("%d\n", *(*(*(*(pz)) + 2)));//4*2*2=16 16/4=4 arr[4]
printf("%d\n", *(*(*(*(pz))) + 2));//4*2=8 8/4=2 arr[2]
printf("%d\n", *(*(*(*(pz + 1) + 1) + 1)+ 1));//arr[21]
函数指针
声明与赋值
函数指针声明:返回类型 (*函数指针名)(参数列表)
函数指针赋值: 函数指针名 = 函数名
函数的格式必须和声明的函数指针名一致
int Function(int x,int y){
return x+y;
}
int main(int argc,char* argv[]){
int (*pFun)(int,int); //函数指针的声明。函数指针的返回值、名字、参数类型都可以根据自己的意愿来
pFun = Function; //但是如果将函数指针指向一个函数,那么此函数的格式必须和此函数指针格式相同,比如返 回值类型、参数列表等信息都必须相同。因为格式类型都相同了,所以不用强转
//另一种方式
pFun = (int (*)(int,int))10; //也可以使用强转让函数指针指向一个数值,但是这样没有意义,给一个 函数的地址才有意义
return 0;
}
如果函数指针指向的函数的格式和此函数指针不同,则使用强转编译是可以通过的,但是执行时报错。所以这是规定记住就行
函数指针的使用
int Function(int x,int y){
return x + y;
}
int main(int argc,char* argv[]){
int (*pFun)(int,int); //声明
pFun = Function; //赋值
int sum = pFun(1,2); //使用
printf("%d",sum); //1+2=3
return 0;
}
文件操作
文件分类
文本文件
文本文件是以ASCII码值进行存储与编码的文件,其文件的内容就是字符,存储的是ASCII码的二进制,也就是我们能够直观的看得懂的,比如:‘2’,‘34’,‘5’这样,文件的数据流为字符流。
二进制文件
二进制文件是存储二进制数据的文件,存储的是数据的补码,文件的数据流为二进制流。
FILE
FILE是C语言为了具体实现对文件的操作而定义的一个包含文件操作相关信息的结构类型,FILE *fp定义了一个结构指针,FILE类型是用typedef重命名的,在头文件stdio.h中定义,因此,使用文件读写函数时都需要#include<stdio.h>。
下面是FILE文件类型的说明:
typedef struct{
short level;//缓冲区使用量
unsigned flags;//文件状态描述
char fd;//文件描述符
shotr bsize;//缓冲区大小
unsigned char *buffer;//文件缓冲区的首地址
unsigned char *curp;//指向文件缓冲区的首地址
unsigned char hold;//其他信息
unsigned istemp;
short token;
}FILE;
打开文件
打开文件功能用于建立系统与要操作的某个文件之间的关联,指定这个文件名并请求系统分配相应的文件缓冲区内存单元。打开文件由标准函数fopen()实现,其调用方式一般为:
fopen("文件名","打开方式");
这里我们给出函数原型:
FILE *open(char *name,char *mode);
函数原型中的name即文件名,mode为打开方式。在这里我们给出C语言所有的文件打开方式:
文件打开方式
使用方式 | 含义 | 使用方式 | 含义 |
---|---|---|---|
“r" | 打开文本文件进行只读 | ”rb" | 打开二进制文件只读 |
“w” | 新建新文本文件进行只写 | “wb” | 建立二进制文件进行只写 |
“a" | 打开文本文件进行追加 | “ab" | 建立二进制文件进行写/追加 |
”r+" | 打开文本文件读/写 | “rb+” | 打开二进制文件进行读/写 |
“w+” | 建立新文本文件进行读/写 | “wb+” | 建立二进制文件进行读/写 |
“a+" | 打开文本文件进行读/写/追加 | ”ab+“ | 打开二进制文件进行读/写/追加 |
值得注意的是:fopen( )函数有返回值,如果文件成功打开或建立,则返回包含文件缓冲区等信息的FILE结构指针,如果未能成功打开或建立,则返回NULL(空值)的FILE指针。
为保证文件操作的可靠性,调用fopen( )函数时最好做一个判断,以确保正常打开后再进行读写。其形式为:
if((fp=fopen("abc.txt","r"))==NULL){
printf("File open error!\n");
exit(0);
}
这里的exit(0)是系统标准函数,作用是关闭所有文件,并且终止程序的执行。参数0表示程序正常结束,非0参数通常表示不正常的程序结束。
这里还需要注意:文件一旦经fopen()正常打开,则对该文件的操作方式就被确定,并且直至文件关闭都不变,即若文件以r方式打开,则只能对该文件进行读操作,而不能进行写入数据操作。
一般进行文件读写操作时,常用到一下规则:
if 读文件
指定的文件必须存在,否则出错;
if 写文件(指定的文件可以存在也可以不存在)
if 以“w”方式写
if 文件已存在
源文件将被删去后重新建立;
else
按指定的名字创建一个新文件;
if 以“a”方式写
if 文件已存在
写入的数据将被添加到指定文件原有的数据后面,不会删去原有的内容;
else
按指定的名字新建一个文件(与“w”相同);
if 文件同时读和写
使用“r+”“w+”或“a+”打开文件;
关闭文件
当文件操作完成后,应及时关闭它以防止不正常的操作。
关闭文件通过调用标准函数fclose()实现,其一般格式为:
fclose(文件指针);
该函数将返回一个整数,若该数为0表示正常关闭文件,否则标志无法正常关闭文件,所以关闭文件时也应该使用条件判断:
if(fclose(fp){
printf("Can not close the file!\n");
exit(0);
}
关闭文件操作除了强制把缓冲区的数据写入磁盘外,还将释放文件缓冲区单元和FILE结构指针,使文件指针与具体文件脱钩。
文件读写
字符方式读写函数fgetc()和fputc()
对于文本文件,存取的数据都是ASCII码字符文本,使用这两个函数读写文件时,逐个字符地进行文件读写。
ch=fgetc();//实现从fp所指示的磁盘文件读入一个字符到ch。
futc(ch,fp);//把一个字符ch写到fp所指示的磁盘文件上。返回值:若写文件成功,则返回ch,若写文件失败,则返回EOF。
字符串方式文件读写函数fputs( )和pgets( )
fgets(s,n,fp);//从文本文件中读取字符串
//其中s可以是字符数组名或者字符指针,n是指定读入的字符个数,fp时文件指针。
//值得注意的是:函数被调用时,最多读取n-1个字符,当函数读取的字符达到指定个数,或接收到换行符,或接收到文件结束标志EOF时,将在读取的字符后面自动添加一个‘\0’字符。若有换行符,则将换行符保留(换行符在’\0’字符之前),若有EOF,则不保留EOF。
fputs(s,fp);//向指定的文本写入一个字符串
//其中s为要写入的字符串,可以是字符数组名,字符型指针变量或者是字符串常量,fp是文件指针。该函数把s写入文件时,字符串结束符’\0’不会被写入文件。返回值:若成功写入,则返回所写的最后一个字符,若写入失败,则返回EOF。
格式化方式文件读写函数fscanf( )和fprintf( )
fscanf(fp,格式字符串,输入表);//从文件中按照给定的控制格式读取数据
fscanf(fp,格式字符串,输出表);//按照给定的控制格式向文件中写入数据
数据块方式文件读写函数fread( )和fwrite( )
这两个函数多用于读写二进制文件。
fread(buffer,size,count,fp);//可以用来读一组数据,如一个数组元素,一个结构变量的值等。二进制文件中的数据流是非字符的,它包含的是数据在计算机内部的二进制形式。
//其中buffer是一个指针,在函数fread( )中,他表示存放输入数据的首地址,size表示数据块的字节数,count表示要读写的数据块数。
//如:fread(fa,4,5,fp);表示从fp所指的文件中,每次读4个字节送入实数组fa中,连续读5次,即读5个实数到fa中。
//返回值:成功读取到的次数
//注意:size未读满时,算作读取失败
fwrite(buffer,size,count,fp);//可以用来写入一组数据,如一个数组元素,一个结构变量的值等。
//其中buffer表示存放输出数据的首地址,size表示数据块的字节数,count表示要读写的数据块数。
//返回值:成功写入的次数
其他函数
文件指针操作函数
在文件读写的整个过程中,每一次成功的操作都将改变文件指针的位置,依次完成文件数据的访问与处理。
//重定位文件首函数rewind( )
//当访问某个文件,进行了文件读写,使指针指向了文件中间或末尾,又想回到文件的首地址重新进行读写时,可使用该函数
rewind(FILE *fp);//定位文件读写位置指针,使其指向读写文件的首地址,即打开文件时文件读写位置指针所指向的位置。
//指针移动控制函数fseek( )
fseek(fp,offset,form);//控制指针移动
//其中fp为文件指针,offset表示移动偏移量,它应该是long型数据,使用常量时,应加上后缀“L”,offset可为负值,form表示从哪个位置开始计算偏移量,位置可取三种:文件首部,当前位置和文件尾部,实际表示时分别对应值0、1、2,或常量SEEK_set,SEEK_CUR,SEEK_END。
//获取指针当前位置函数ftell( )
ftell(文件指针);//获取当前文件读写位置,即相对于文件开头的偏移量(字节数)。
//函数出错时返回-1L。
检测文件指针状态函数
//文件末尾检测函数feof( )
feof(fp);//判断fp指针是否已经到文件末尾,即读文件是否读到了文件结束的位置。
//返回值:成功返回1表示已经读到了文件末尾的位置,0表示文件未结束。
//读写错误检查函数ferror( )
ferror(fp);//检查文件在用各种输入输出函数进行读写时是否出错。
//返回值:若为出错,返回0,否则表示出错。
//出错标记清除函数clearerr( )
clearerr(fp);//清楚出错标志和文件结束标志,使他们为0值
//其中fp为文件指针,offset表示移动偏移量,它应该是long型数据,使用常量时,应加上后缀“L”,offset可为负值,form表示从哪个位置开始计算偏移量,位置可取三种:文件首部,当前位置和文件尾部,实际表示时分别对应值0、1、2,或常量SEEK_set,SEEK_CUR,SEEK_END。
//获取指针当前位置函数ftell( )
ftell(文件指针);//获取当前文件读写位置,即相对于文件开头的偏移量(字节数)。
//函数出错时返回-1L。
移位指令
汇编
算数移位指令
-
SAL(Shift Arithmetic Left):算术左移(和SHL效果一样)
-
SAR(Shift Arithmetic Right):算术右移
-
格式:
SAL/SAR Reg/Mem, CL/Imm
即算数移位指令后面的第一个操作数是寄存器或者内存;第二个操作数是寄存器或者立即数
SAL eax,1
,表示将eax中的数(4字节)左移1位,最高位的数移入到CF进位标志位中,最低位补0
SAR ax,1
,表示将ax中的数(2字节)右移1位,最高位的补原来的ax中的符号位(原来的最高位),最低位移入到CF进位标志位中
SAR al,1 //al:10000001右移一位最高位补原来符号位,最低位移入CF,即11000000 / CF:1
SAL al,2 //al:10000001左移两位最高位移入CF,最低位补0,即00000100 / CF:0
逻辑移位指令
-
SHL(Shift Left):逻辑左移(和SAL效果一样)
-
SHR(Shift Right):逻辑右移
-
格式:
SHL/SHR Reg/Mem, CL/Imm
SHL eax,1
,表示将eax中的数(4字节)左移1位,最高位的数移入到CF进位标志位中,最低位补0
SHR al,1
,表示将al中的数(1字节)右移1位,最高位补0,最低位移入到CF进位标志位中
SHR al,1 //al:10000001右移一位最高位补0,最低位移入CF,即01000000 / CF:1
SHL al,1 //al:01000001左移一位最高位移入CF,最低位补0,即10000010 / CF:0
循环移位指令
-
ROL(Rotate Left):循环左移
-
ROR(Rotate Right):循环右移
-
格式:
ROL/ROR r/m, i8/CL
ROL eax,1
,表示将eax中的数循环左移1位,最高位的数补到最低位,并且最高位的数移入CF标志位
ROR al,1
,表示将al中的数循环右移1位,最低位的数补到最高位,并且最低位的数移入CF标志位
ROL al,1 //al:10000001循环左移一位最高位补到最低位,最高位移入CF,即00000011 / CF:1
ROR al,1 //al:10000001循环右移一位最低位补到最高位,最低位移入CF,即11000000 / CF:1
带进位的循环移位指令
-
RCL(Rotate through Carry Left):带进位循环左移
-
RCR(Rotate through Carry Right):带进位循环右移
-
格式:
RCL/RCR r/m, i8/CL
RCL al,1
,表示将al中的数循环左移一位,最高位移入CF标志位,并且CF原来的数补到最低位
RCR al,1
,表示将al中的数循环右移一位,最低位移入CF标志位,并且CF原来的数补到最高位
RCL al,1 //al:10000001,CF:0循环左移一位最高位移入CF,CF原来的数补到最低位,即al:00000010 / CF:1
RCR al,1 //al:10000001,CF:0循环右移一位最低位移入CF,CF原来的数补到最高位,即al:01000000 / CF:1
C语言
与运算 &
printf("%d",2&3); //2
或运算 |
printf("%d",2|3); //3
非运算 ~
printf("%d",~2); //-3,因为%d打印的是有符号的整数
异或运算 ^
printf("%d",2^3); //1
移位运算 <<
>>
//左移运算<<,有符号和无符号是无区别的
int a = 8;
printf("%d",a<<1); //16
unsigned int b = 8;
printf("%d",b<<1); //16
//右移运算,有符号对应SAR即算数右移,无符号对应SHR即逻辑右移
int a = 0xF0000000;
unsigned int b = 0xF0000000;
printf("%x\n", a >> 1);//F8000000
printf("%x", b >> 1);//78000000