1.条件表达式:表达式1?表达式2:表达式3
①若表达式1为真,则该整体值为表达式2的值
②若表达式1为假,则该整体值为表达式3的值
2.举例:返回 a和b中较大的一个值:
(1)传统方法:if else实现
if(a>b) return a;
else return b;
(2)条件运算符(三目运算符):
return (a>b)?a:b;
3.条件运算符 ? :
是C语言中唯一的三目运算符
逗号表达式整体的值 是最后一个逗号后面表达式的值
常见于for循环的表达式1和表达式3
for(i=0,j=0; ;i++,j++)
1.自增自减运算符 (自增运算符:increment operator)
i++:先运算,再++
++i:先自增,再运算
2.自增自减运算符与取值运算符*
的结合
#include
int main() {
int a[3] = {1,5,10};
int *p; //指针p
int j;
p = a; //p指向数组起始元素
j = *p++; //等价于 j=*p;p++; p自增,是加一个单位,指向下一个元素int型的p自增,地址大4B
printf("a[0]=%d,j=%d,*p=%d\n",a[0],j,*p); //a[0]=1,j=1,*p=5
j = p[0]++;
printf("a[0]=%d,j=%d,p[0]=%d",a[0],j,p[0]); //a[0]=1,j=p[0]=5,p[0]=6
return 0;
}
1.位运算符
逻辑与:&&
按位与:&
(双目运算符)
<<
:左移
>>
:右移
移位操作比乘除法效率高。
负数的右移,高位补1 (操作的是补码)。无符号数右移高位补0。同样右移一位,但值会不同。
①按位与:&
②按位或:|
③按位异或:^
④按位取反:~
1.异或的特性
①自己和自己异或为0 【归零率】
②和零异或为自身【恒等率】
③异或满足交换律,从而有 a^b^a = (a^a)^b = 0^b = b
【交换律,找单数问题,找只出现一次的数】
2.异或符合的规律
①归零律:a⊕a = 0
②恒等律:a⊕0 = a
③交换律:a⊕b⊕c = a⊕c⊕b
④结合律:a⊕b⊕c = a⊕(b⊕c)
1.代替if-else过多的情况,用switch-case可以简化代码
2.break;
case只会进行一次的匹配
3.case后匹配的东西,只能放常量表达式
二维数组可视为元素是一维数组的一维数组,存储顺序是横着存满一个数组,再存下一个。因此初始化二维数组的时候也可以像一维数组一样横着写开。
1.二维数组中的元素在内存中的存储规则是按行存储,即先顺序存储第一行的元素,后顺序存储第二行的元素,直到最后一行。
2.定义int a[3][4];那么我们可以访问数组的最后一个元素是a[2][3]
二级指针是指针的指针。二级指针的作用是服务于一级指针变量,对一级指针变量实现间接访问。
如果已经掌握了C++引用,则不需要用到二级指针。
如果需要把一个一级指针变量的地址存起来,就要用到二级指针。
二级指针是存储一级指针变量的地址值,也是用于做间接访问
应用于C语言中 (没有C++引用的情况下),修改子函数中变量的值,能同步改回main函数
#include
int main(){
int a;
scanf("%d",&a);
printf("%2d\n",a<<1);
printf("%2d\n",a>>1);
return 0;
}
#include
int main(){
int A[5];
for(int i = 0; i < 5; i++){
scanf("%d",&A[i]);
}
printf("%d",A[0]^A[1]^A[2]^A[3]^A[4]);
return 0;
}
[ X ] 原 = [ X ] 补 ⇔ [ − X ] 补 [X]_原=[X]_补\Leftrightarrow[-X]_补 [X]原=[X]补⇔[−X]补:(X>0)
①符号位和数值位全部按位取反,再末位+1 (用于计算短的8-16位补码)
②找到最右边的1, 其左边符号位和数值位都取反 (用于计算长的32位补码)
例:5的原码是 0000 0000 0000 0000 0000 0101,5的补码=5的原码,-5的补码是 1111 1111 1111 1111 1111 1111 1111 1011。可用①、②两种方式分别尝试
负数越小,则其补码的值越大,1越多
1.X86数据在内存中按照小端存储方式存储
2.从右到左,是从高字节到低字节。(低字节,即数学上的低位数)
3.从左到右,是低地址到高地址。
4.小端存储就是,低地址存低字节,高地址存高字节。
大端存储:低地址存高字节,高地址存低字节,符合人类阅读习惯
有符号、无符号 unsigned + 短整型 short、基本整型 int、长整型 long
有符号数:0111 1111 1111 1111 B = 215-1 = 32767
无符号数:1111 1111 1111 1111 B = 216-1 = 65535
n位带符号整数补码的表示范围: − 2 n − 1 ∼ 2 n − 1 − 1 -2^{n-1}\sim 2^{n-1}-1 −2n−1∼2n−1−1 (n位带符号整数,1位符号位,n-1位数值位。n-1个1后再加1会舍去进位,全变0)
-127:原码 1111 1111,补码 1000 0001
-128:原码 1000 0000 (进位被舍去),补码 1000 0000
short:-32768~32767
unsigned short:0~65535
int:2-31~231-1,±21亿
float:2-126~2127
1.溢出:数据超出了该数据类型的表示范围
2.解决溢出:用拥有更表示大空间的整数来存储
3.若程序数值大于double类型的 264-1:自行实现大整数加法
浮点数的IEEE754 标准,用 ±1.M×2E-127 表示一个二进制数。类似十进制的科学计数法。
例如 十进制998可以由科学计数法表示为9.98×102。 二进制 1110 可以由IEEE754标准 表示为 1.110×23
第1位:符号位S,0为正数 1为负数
第2~9位:阶码E (8位无符号数),表示范围 1~254 (全为0和全为1是特殊情况),再减去127 为幂次
第10及以后:小数部分 尾数M
#include
int main(){
float f1 = 4.5;
float f2 = 1.456;
return 0;
}
打断点,看内存视图
int溢出、float精度丢失:改成double类型
解:赋值float j = 1.456,查看j在内存中的存储
j在内存中 以小端方式 保存为 35 5E BA 3F,大端方式为 3F BA 5E 35H,即 0011 1111 1011 1010 0101 1110 0011 0101B:
幂次为 0111 1111B,即127-127=0。
小数部分为:011 1010 0101 1110 0011 0101B,共有13个1
故输出,0,13。代码如下:
#include
int main() {
float i = 4.5;
float j = 1.456;
printf("%3d%3d\n",0,13);
return 0;
}
更多更详细内容见:计算机组成原理第四章:指令系统 - 程序的机器级代码表示
加法器不能直接修改内存的栈中变量的值,需要先将内存数据读取到寄存器,然后修改寄存器中变量的值,再写回内存。
CPU包括:
(1)运算器:加法器、ALU
(2)控制器:控制单元CU、IR、PC
零地址指令、一地址指令、二地址指令、三地址指令
寄存器:register
存储器:storage
新建C项目,点击终端,输入gcc -S -fverbose-asm main.c
,左侧会生成mian.s
。
ESP:栈顶指针
EBP:栈基指针
栈顶指针ESP:Extended Stack Pointer。其中"E" 表示扩展寄存器(Extended Register)。
1.mov指令:用于移动数据
mov destination,source
mov BX,AX //将寄存器 AX中的值转移到寄存器 BX
2.push指令:压栈指令
3.pop指令:弹栈指令
1.add/sub指令:加法、减法指令
2.inc/dec:自增自减指令
3.imul指令:带符号数乘法指令
4.idiv指令:带符号数除法指令
5.and/or/xor指令:按位与 / 按位或 / 按位异或 指令
6.not指令:取非指令
7.neg指令:取负指令
8.shl/shr指令:逻辑移位指令
9.lea指令:地址传送指令,“Load Effective Address”,用来计算并加载一个有效地址。
1.jmp指令:无条件转移指令
2.jxx / jcondition指令:条件转移指令
3.cmp/test指令:比较指令
4.call/ret指令: 函数调用指令/函数返回指令
标志位 / 状态字 / 条件码 / flagbit:
①结果为零:ZF = 1 (零标志 ZF)
②结果位负:SF = 1 (符号标志 SF)
③带符号整数溢出:OF = 1 (溢出标志 OF)
④无符号整数溢出:CF = 1 (进借位标志 CF)
判断溢出:正 + 正 = 负,负 + 负 = 正,正 - 负 = 负
OF和SF对无符号整数没有意义,CF对带符号整数没有意义,【11年17.】
生成汇编的指令:gcc -m32 -masm=intel -S -fverbose-asm main.c
生成汇编文件 main.s
变量赋值:variable assignment
#include
//整型、整型数组、整形指针的汇编
int main() {
int arr[3] = {1,2,3};
int *p;
int i = 5;
int j = 10;
i = arr[2];
p = arr;
printf("i=%d\n",i);
return 0;
}
ESP 栈顶指针的偏移,来获取对应变量内存空间的数据
p = &a;
lea eax,[esp+16]
mov DWORD PTR [esp+28],eax
mov eax, DWORD PTR [esp+28] //间接地址保存到寄存器eax
mov eax, DWORD PTR [eax] //取间接地址的内容
call后,被调函数的开头必然是:
push ebp
mov ebp,esp //或 enter指令
被调函数ret前,必然是:
mov esp,ebp
pop ebp //或 leave指令
#include
int main() {
printf("mov\npush\npop\n");
return 0;
}
分析:
(1)R1:unsigned int x = 134 = 128+6 = 1000 0110B = 86H
R2:unsigned int y = 246 = 255 -9 = 1111 0110B = F6H
R5:unsigned int z1 = x-y,R6:unsigned int z2 = x+y,
①法一:补码二进制加减法 (求十六进制,推荐此法)
z1 = x-y = x+(-y) = 1000 0110B + 0000 1010B = 1001 0000B = 90H
z2 = x+y = 1000 0110B + 1111 0110B = (1)0111 1100B = 7CH
②法二:真值取模加减法
z1 = (x-y)%2n = (134-246)%28 = -112%256 = 144 = 128 + 16 = 1001 0000B = 90H
z2 = (x+y)%2n = (134+246)%28 = 380%256 = 124 = 127-3 = 0111 1100B = 7CH
(2)考察数据类型转换:unsigned int → int,机器数(补码)不变,解释方式改变
①法一:补码二进制加减法
m的机器数与x的机器数相同,均为1000 0110B,m原 = 1111 1010B = -(127-4-1) = -122
int k1 = m - n的机器数与 x-y机器数相同,均为90H = 1001 0000B,则 k1原 = 1111 0000B = -(64+32+16) = -112
②法二:真值取模加减法 (求十进制,推荐此法)
int m补 = x = 1000 0110B,m原 = 1111 1010B = -(127-4-1) = -122
int n补 = y = 1111 0110B,n原 = 1000 1010B = - (8+2) = -10
int k1 = m-n = -122-(-10) = -112
答案:
(1)R1:86H、R2:90H、R3:7CH
(2)m:-122、k1:-112
(3)能。
理由:无论是带符号整数的加法运算还是无符号整数的加法运算,都可以用加法器实现。加法器只负责对二进制数进行计算并产生标志,运算前并不区分有无符号。
补码减法操作可以用补码加法操作实现,即 a-b = a+(-b)
(4)①OF = Cn⊕ Cn-1 = 1,即最高位的进位与次高位的进位不同,则带符号整数溢出。
②int k2 = m+n; 发生下溢
二进制:binary
八进制:octonary
十进制:decimal
十六进制:hexadecimal
答案:
(1)CISC。复杂指令集的指令长度不定长。
(2)60H = 96 字节
计算过程:0040107FH - 00401020H +1 = 5FH + 1 =60H = 96
(3)CF = 1
计算过程:unsigned int n = 0 = 0000 0000H,则 n-1 = FFFF FFFFH。i = 0 = 0000 0000H。
则 i-(n-1) = i+[-(n-1)] = 0000 0000H + 0000 0001H = 0000 0001H。则最高位进位 Cn=0。由于是减法,则sub = 1。∴CF = Cn⊕sub = 0⊕1 = 1
(4)不能。∵f2是float类型,遵循 IEEE754规则,float数值×2应当是阶码+1,而不是简单地整体左移一位。
答案:
(1)①10次
②执行第16行 call指令
(2)①第12行 jle 是条件转移指令
②第16行 call 函数调用指令、第20行 jmp 无条件转移指令、第30行 ret 子程序的返回指令
(3)①0040102AH
②目标地址 = (PC) + “1” + OFFSET,即 00401000H = 0040102AH + OFFSET,即OFFSET = 00401000H - 0040102AH = FF FF FF D6H。
③根据第16行的call指令后4字节,知偏移量字段为D6 FF FF FF,可以确定M采用小端方式。
(4)①f(13)=13!=6,227,020,800,超过了int值能表示的最大范围,发生了溢出,故值不相等。
②要使得f1(13)的返回值得到正确结果,应将int类型改为 double 或 long long 类型
(5)①若乘积的高33位既不是全0 也不是全1,则OF=1
②编译器应在imul指令后加一条 “溢出自陷指令”,即trap指令。使得CPU自动查询溢出标志OF,当OF=1时调出“溢出异常处理程序”。
Intel X86:小端方式、CISC、转移指令采用 相对寻址方式
FILE * fp 是FILE类型的指针
FILE是一个结构体,已经定义在
#include
//练习文件打开
int main() {
//1.打开文件
FILE *fp; //定义一个FILE类型的指针
fp = fopen("file.txt","r+"); //打开文件
if(NULL==fp){
perror("fopen"); //perror用于定位失败原因
return -1;
}
//2.读取文件
char c;
// c = fgetc(fp);
// printf("%c\n",c);
while((c=fgetc(fp))!=EOF){
printf("%c",c);
}
printf("\n");
//3.写文件
c = fputc('H',fp);
if(-1==c){
perror("fputc");
return -1;
}
//4.关闭文件
fclose(fp); //关闭文件
return 0;
}
FILE *fp; //定义一个FILE类型的指针
fp = fopen("file.txt","r"); //打开文件
1.r+ 与 rb+
“r+“是 文本模式 写入文件,”\n"会保存为”\r\n",多一个字节 (Windows)
"rb+"是 二进制模式 写入文件
2.rb+ 与 wb+
wb+:若没有该名字的文件,则可创建该名字的文件。若已有该名文件,则删除原文件,再新建一个该名字文件。
fclose(fp); //关闭文件
c = fgetc(fp);
fgetc读到文件结尾返回EOF
c = fputc('H',fp);
fputc读到文件结尾返回EOF
1.fwrite:将buf中的字符串等数据写入文件
fwrite(字符数组名,单个字符长度,数组长度,文件指针名)
fwrite(buf,sizeof(char),strlen(buf),fp); //把buf中的字符串写入文件
#include
#include
int main() {
char buf[20] = "hello\nworld";
FILE *fp = fopen("file.txt","r+"); //r+:以文本方式打开文件
if(NULL == fp){
perror("fopen");
return -1;
} //ret的值:fwrite读了多少个字符
int ret = fwrite(buf,sizeof(char),strlen(buf),fp); //把buf中的字符串写入文件
fclose(fp);
return 0;
}
2.fread
fread(str,sizeof(char),sizeof(str),fp);
fgets:读文件、读标准输入,一次读一行。读到文件结尾返回 NULL
while((fgets(buf,sizeof(buf),fp)) != NULL){ //一次读一行,把文件读空
printf("%s",buf); //读文件用fp,读标准输入用stdin
}
fputs:
fgtc、fputc、fgets、fputs只能以文本方式。文本模式只能读写字符串,二进制模式 字符串、整型数、浮点数、结构体都可以写。
fseek(文件类型指针,位移量,起点)
fseek(fp,-5,SEEK_CUR);
fseek调用成功返回0,调用失败返回非零。
#include
#include
int main() {
FILE *fp;
char str[20] = "hello\nworld";
int len = 0,ret;
long pos;
fp = fopen("file.txt","wb+");
if(NULL == fp){
perror("fopen");
return -1;
}
len = strlen(str);
fwrite(str,sizeof(char),len,fp);
fseek(fp,-11,SEEK_CUR);
while((fgets(str,sizeof(str),fp)) != NULL){ //一次读一行,把文件读空
printf("%s",str); //读文件用fp,读标准输入用stdin
}
return 0;
}
long pos;
pos = ftell(fp);
printf("now pos = %ld\n",pos);
完整版代码:文件写、文件指针偏移、文件指针位置查看、文件读
#include
#include
int main() {
FILE *fp;
char str[20] = "hello\nworld";
int len = 0,ret;
long pos;
fp = fopen("file.txt","wb+");
if(NULL == fp){
perror("fopen");
return -1;
}
len = strlen(str);
fwrite(str,sizeof(char),len,fp);
ret = fseek(fp,-len,SEEK_CUR); //文件指针返回开头位置
if(ret != 0){
perror("fseek");
fclose(fp);
return -1;
}
pos = ftell(fp);
printf("now pos = %ld\n",pos);
memset(str,0,sizeof(str)); //清空str
fread(str,sizeof(char),sizeof(str),fp);
printf("str=\n%s\n",str);
// while((fgets(str,sizeof(str),fp)) != NULL){ //一次读一行,把文件读空
// printf("%s",str); //读文件用fp,读标准输入用stdin
// }
return 0;
}