目录
3、自增自减运算符与取值运算符(408 初试不需要掌握,初试考C的学校需要,部分机试答案含有)
1、位运算符<<、>>、~、|、^、&依次是左移、右移、按位取反、按位或、按位异或、按位与.
2、二级指针(不在 408 大纲范围,408 初试不考,初试考C 的学校可能需要,机试用到概率极低))
1、浮点数IEEE754 标准解析及实战计算演示(组成要考选择题)
第一章:C语言语法进阶
一、条件运算符与逗号运算符
1、条件运算符:
条件运算符是C语言中唯一的一种三目运算符。三目运算符代表有三个操作数;双目运算符代表有两个操作数,如逻辑与运算符就是双目运算符;单目运算符代表有一个操作数,如逻辑非运算符就是单目运算符。运算符也称操作符,三目运算符通过判断问号之前的表达式的真假,来确定整体表达式的值,如下例所示:如果a>b为真,那么三目表达式整体的值为 a,所以max 的值等于a,如果 a>b为假、那么三目表达式整体的值为b,所以 max的值等于 b。
基础语法:
条件表达式 ? 表达式1 : 表达式2
执行逻辑:
完整代码:
#include <stdio.h>
int main() {
int a,b,max;
while(scanf("%d%d",&a,&b))
{
max=a>b?a:b;
printf("max=%d\n",max);
}
//想让上述代码结束可以输入单个字符a
return 0;
}
2、逗号运算符:
逗号运算符的优先级最低,我们需要掌握的是,逗号表达式的整体值是最后一个表达式的值。
基础语法:
表达式1, 表达式2, ..., 表达式N
执行逻辑:
完整代码:
#include <stdio.h>
int main() {
int i,j;
i=10;
j=1;
if(i,j-1)
{
printf("if excute\n");
}
for(i=0,j=1;i<10;i++)
{
}
return 0;
}
二、自增自减运算符
1、自增、自减运算符和其他运算符有很大的区别:
因为其他运算符除赋值运算符可以改变量本身的值外,不会有这种效果。自增、自减就是对变量自身进行加1、减1操作,那么有了加法和减法运算符为什么还要发明这种运算符呢?原因是自增和自减来源于B语言,当时KenThompson 和 Dennis M.Ritchie(C语言的发明者)为了不改变程序员的编写习惯,在C语言中保留了 B语言中的自增和自减。因为自增、自减会改变变量的值,所以自增和自减不能用于常量!
2、例题:
下例中的j=i++>-1,对于后++或者后--,首先我们需要去掉++或--运算符,也就是首先计算j=i>-1,因为i本身等于-1,所以得到j的值为0, 接着单独计算i++,也就是对i加 1,所以i从-1加1得到 0,因此 printf("i=%d,j=%d\n",i,j);语句的执行结果是0和0。
完整代码:
#include <stdio.h>
//考研初试如果使用++,或者--,最好单独使用,不要和其他运算符混到一起
int main() {
int i=-1,j;
//5++;//如果打开该句,会造成编译不通
j=i++>-1;//后++等价于j=i>-1;i=i+1;
printf("i=%d,j=%d\n",i,j);
return 0;
}
3、自增自减运算符与取值运算符(408 初试不需要掌握,初试考C的学校需要,部分机试答案含有)
#include <stdio.h>
#include <stdlib.h>
//只有比后增优先级高的操作符,才会作为一个整体,如:[]、()
int main() {
int a[3]={3,7,8};
int *p;
int j;
p=a;//p指向数组起始元素
j=*p++;//先把*p的值赋给j,然后对p加1。j=*p;p++;
printf("a[0]=%d,j=%d,*p=%d\n",a[0],j,*p);//输出3 3 7
j=p[0]++; //先把 p[0]赋给j,然后对 p[0]加1
printf("a[0]=%d,j=%d,*p=%d\n",a[0],j,*p);//输出3 7 8
return 0;
}
三、位运算符
1、位运算符<<、>>、~、|、^、&依次是左移、右移、按位取反、按位或、按位异或、按位与.
- 位运算符只能用于对整型数据进行操作。
- 左移:高位丢弃,低位补0,相当于乘以2.工作中很多时候申请内存时会用左移,
例如:要申请1GB大小的空间,可以使用malloc(1<<30).
- 右移:低位丢弃,正数的高位补0(无符号数我们认为是正数),负数的高位补1,相当于除以 2.移位比乘法和除法的效率要高,负数右移,对偶数来说是除以2,但对奇数来说是先减1后除以2.
例如,-8>>1,得到的是-4,但-7>>1得到的并不是-3而是-4.另外,对于-1来说,无论右移多少位,值永远为-1.
- C语言的左移和右移相当于是算术左移与算术右移,考研中的逻辑左移与右移,左移和右移空位都补 0.
- 异或:相同的数进行异或时,结果为0,任何数和0异或的结果是其本身。
- 按位取反:数位上的数是1变为 0,0变为1.
- 按位与和按位或:用两个数的每一位进行与和或
#include <stdio.h>
int main() {
short i=5;//short是short int的缩写,short是整型,是2个字节的整型,int是4个字节
short j;
//左移 右移
j=i<<1;//一个变量移动以后自身不会变化
printf("i=%d\n",j);//左移是乘以2,结果为10
j=i>>1;
printf("j=%d\n",j);//右移是除2,结果是2
printf("------------------\n");
//无符号数的左移与右移
i=0x8011;
unsigned short s=0x8011;//在short前加unsigned就是无符号数
unsigned short r=0;
j=i>>1;//对i右移,对有符号数进行右移
r=s>>1;//对s右移,对无符号数进行右移
printf("j=%d,r=%u\n",j,r);//有符号数和无符号数右移之后的结果是不一样的
printf("------------------\n");
//接下来看 按位与,按位或,按位异或,按位取反
i=5,j=7;
printf("i & j=%d\n",i&j);
printf("i | j=%d\n",i|j);
printf("i ^ j=%d\n",i^j);
printf("~i=%d\n",~i);
return 0;
}
2、异或运算符特性:
异或运算符有以下特性:
- 归零律:
a ^ a = 0
(相同数异或结果为0) - 恒等律:
a ^ 0 = a
(任何数与0异或结果不变) - 交换律:
a ^ b = b ^ a
- 结合律:
(a ^ b) ^ c = a ^ (b ^ c)
我们可以完成下面的题目,在一堆数中找出出现1次的那个数,其他数是出现2次.
#include <stdio.h>
int main() {
int i;
int arr[5]={8,5,3,5,8};
int result = 0;
for(i=0;i<5;i++)
{
result ^= arr[i];
}
printf("%d\n",result);//输出为3
return 0;
}
四、switch & do-while
1、switch
判断的一个变量可以等于几个值或几十个值时,使用if和elseif语句会导致 elseif分支非常多,这时可以考虑使用switch语句,switch语句的语法格式如下:
switch(表达式)
{
case 常量表达式 1:语句1
case 常量表达式 2:语句2
...
case 常量表达式 n:语句n
default:语句 n+1
}
下面来看一个使用switch语句的例子。
如例1所示,输入一个年份和月份,然后打印对应月份的天数,如输入一个闰年和2月,则输出为29天.具体代码如下所示,switch语句中 case后面的常量表达式的值不是按照1到12的顺序排列的,这里要说明的是,switch语句匹配并不需要常量表达式的值有序排列,输人值等于哪个常量表达式的值,就执行其后的语句,每条语句后需要加上 break语句,代表匹配成功一个常量表达式时就不再匹配并跳出switch 语句.
如果一个 case语句后面没有break语句,那么程序会继续匹配下面的case 常量表达式例2中的代码是对例1中代码的优化。例2的代码执行效果和上面的代码执行效果一致,原理是只要匹配到1、3、5、7、8、10、12 中的任何一个,就不再拿 mon 与case 后的常量表达式的值进行比较, 而执行语句 printf("mon=%dis 31days\n",mon),完毕后执行 break 语句跳出switch语句.switch语句最后加入default 的目的是,在所有case后的常量表达式的值都未匹配时,打印输出错误标志或者一些提醒,以便让程序员快速掌握代码的执行情况。
#include <stdio.h>
int main() {
int mon,year;
while(scanf("%d%d",&year,&mon))
{
switch (mon)
{
case 2:printf("mon=%d is %d days\n",mon,28+(year%4==0&&year%100!=0||year%400==0));break;
case 1:printf("mon=%d is 31days\n",mon);break;
case 3:printf("mon=%d is 31days\n",mon);break;
case 5:printf("mon=%d is 31days\n",mon);break;
case 7:printf("mon=%d is 31days\n",mon);break;
case 8:printf("mon=%d is 31days\n",mon);break;
case 10:printf("mon=%d is 31days\n",mon);break;
case 12:printf("mon=%d is 31days\n",mon);break;
case 4:printf("mon=%d is 30days\n",mon);break;
case 6:printf("mon=%d is 30days\n",mon);break;
case 9:printf("mon=%d is 30days\n",mon);break;
case 11:printf("mon=%d is 30days\n",mon);break;
default:
printf("error mon\n");
}
}
return 0;
}
升级版:
#include <stdio.h>
int main() {
int mon,year;
while(scanf("%d%d",&year,&mon))
{
switch (mon)
{
case 2:printf("mon=%d is %d days\n",mon,28+(year%4==0&&year%100!=0||year%400==0));break;
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:printf("mon=%d is 31days\n",mon);break;
case 4:
case 6:
case 9:
case 11:printf("mon=%d is 30days\n",mon);break;
default:
printf("error mon\n");
}
}
return 0;
}
2、do while语句的特点是:
先执行循环体,后判断循环条件是否成立,其一般形式为
do
{
循环体语句;
}while(表达式);
执行过程如下:首先执行一次指定的循环体语句,然后判断表达式,当表达式的值为非零(真)时,返回重新执行循环体语句,如此反复,直到表达式的值等于0为止,例3是使用dowhile语句计算 1到100之间所有整数之和的例子,do while语句与while 语句的差别是,do while语句第一次执行循环体语句之前不会判断表达式的值, 也就是如果i的初值为101,那么依然会进入循环体,实际工作中 do while 语句应用较少.
do while 语句计算1到100 之间的所有整数之和:
#include <stdio.h>
int main() {
int i=1,total=0;
do{
total=total+i;
i++;
}while(i<=100);//无论这里判断条件是什么循环都会先执行一次
printf("total=%d\n",total);
return 0;
}
五、二维数组&二位指针
1、二维数组
二维数组定义的一般形式如下:
类型说明符 数组名[常量表达式][常量表达式];
例如,定义a为3x4(3行4列)的数组,b为5x10(5行10列)的数组:
float a[3][4].b[5][10];
可以将二维数组视为一种特殊的一维数组:一个数组中的元素类型是一维数组的一维数组.
例如,可以把二维数组 a[3][4]视为一个一维数组,它有3个元素 a[0]、a[1]和 a[2],每个元素又是一个包含 4个元素的一维数组,如图1所示。
二维数组中的元素在内存中的存储规则是按行存储,即先顺序存储第一行的元素,后顺序存储第二行的元素,数组元素的获取依次是从 a[0][0]到 a[0][1],直到最后一个元素 a[2][3].
图2中显示了存储二维数组 a[3][4]中每个元素时的顺序
#include <stdio.h>
int main()
{
//通过调试查看元素存放顺序
int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12 };
printf("sizeof(a)=%d\n",sizeof(a));//掌握二维数组大小
printf("a[2][3]=%d\n", a[2][3]);//最后一个元素是 a[2][3]
return 0;
}
2、二级指针(不在 408 大纲范围,408 初试不考,初试考C 的学校可能需要,机试用到概率极低))
如果掌握了 C++的引用,其实不需要去学习二级指针,对于考研必要性很低,二级指针是指针的指针,二级指针的作用是服务于一级指针变量,对一级指针变量实现间接访问。下面我们通过一个实例来让大家理解一下二级指针。
#include <stdio.h>
int main()
{
int i=10;
int *p=&i;
int **p2=&p;//如果我们需要把一个一级指针变量的地址存起来,那么就需要二级指针类型
printf("sizeof(p2)=%d\n",sizeof(p2));//p2和p同样大,都是8个字节
printf("**p2=%d\n",**p2);//通过两次取值可以拿到ic
return 0;
}
第二章:数据的的机器级表示
一、补码与内存
1、补码讲解及内存实战演示(组成常考选择题)
计算机的CPU无法做减法操作(硬件上没有减法器),只能做加法操作。CPU中有一个逻辑单元叫加法器。计算机所做的减法,都是通过加法器将其变化为加法实现的.那么减法具体是如何通过加法实现的呢?实现 2-5的方法是2+(-5),由于计算机只能存储0和1.因此我们编写程序来查看计算机是如何存储-5的,5的二进制数为101.称为原码。计算机用补码表示-5,补码是对原码取反后加1的结果,即计算机表示-5时会对5的二进制数(101)取反后加1,如图1所示.-5在内存中存储为0xffffb,因为对5取反后得0xffffffa,加1后得 OxffffHb(由于是X86架构是小端存储,小端存储是低字节在前,即低字节在低地址,高字节在后,即高字节在高地址,fb对于0xfffffffb是最低的字节,因此fb在最前面,大端和小端相反),对其加2后得0xfffffd,见图2,它就是k的值。当最高位为1(代表负数)时,要得到原码才能知道 0xfffffffd 的值,即对其取反后加1(当然,也可以减1后取反,结果是一样的)得到3,所以其值为-3.
考研注意,假设变量A的值为-5,通过8位表示,那么A 值为11111011,A厕的值为 10000101,符号位是不动的,只有值的部分是5,通过符号位不动,其他值取反加1,这个是考研考的一个原理,和不少C编程书籍上讲的存在差异,正数的补码和原码一致.
2、反码(代码不好演示,考试考的概率低)
反码是一种在计算机中数的机器码表示。对于单个数值(二进制的0和1)而言,对其进行取反操作就是将0为1,1为0.正数的反码和原码一样,负数的反码就是在原码的基础上符号位保持不变,其他位取反.
6是正数,补码与原码一致,-3的补码是11111101
二、整型的不同类型&&溢出
1、整型不同类型解析(组成要考,选择题)
如图1所示,整型变量包括6种类型,如图2所示,其中有符号短整型与无符号短整型的最高位所代表的意义不同。不同整型变量表示的整型数的范围如表1所示,超出范围会发生溢出现象,导致计算出错。
(有符号位第一位0是正数,1是负数,无符号位第一位不代表正负)
考研补充说明:
考研会考8位的,也就是1个字节的整型数的大小,对于1个字节的有符号类型的数值范围是-2的7次幂~(2的7次幂-1),也就是-128到127,对于8位的无符号(unsigned)类型的数值范围是0~(2的8次幂--1),也就是 0-255.
最小的负数是-32768,最大的负数是-1,有符号整数个数为32768,无符号整数个数为65535。
2、溢出解析
如下例所示,有符号短整型数可以表示的最大值为32767,当我们对其加1时,b的值会变为多少呢?实际运行打印得到的是-32768。为什么会这样?因为32767对应的十六进制数为0x7ff,加1后变为0x8000,其首位为1,因此变成了一个负数。取这个负数的原码后,就是其本身,值为 32768,所以 0x8000是最小的负数,即-32768.这时就发生了溢出,我们对32767加1,希望得到的值是 32768,但结果却是-32768,因此导致计算结果错误。在使用整型变量时,一定要注意数值的大小,数值不能超过对应整型数的表示范围.有的读者可能会问在编写的程序中数值大于 2的64次幂-1 时怎么办?答案是可以自行实现大整数加法。
#include <stdio.h>
//整型不同类型的演示,以及溢出演示
int main() {
int i=10;
short a=32767;
short b=0;
long c;//32位的程序是4个字节,64位的是8个字节
i=a+1;//发生了溢出,解决溢出的办法是用更大的空间来存储,比如:将long换成int
printf("%d\n",i);//b并不是32767
printf("----------------------\n");
unsigned int m=3;
unsigned short n=0x8056;//无符号类型,最高位不认为是符号位
unsigned long k=5;
b=0x8056;
printf("b=%d\n",b);//b是有符号类型,所以输出是负值
printf("n=%u\n",n);//无符号类型要用%u,用%d是不规范的
return 0;
}
三、浮点数IEEE754标准
1、浮点数IEEE754 标准解析及实战计算演示(组成要考选择题)
在C语言中,要使用 foat 关键字或 double 关键字定义浮点型变量.float型变量占用的内存空间为4字节,double型变量占用的内存空间为8字节。与整型数据的存储方式不同,浮点型数据是按照指数形式存储的,系统把一个浮点型数据分成小数部分(用M表示)和指数部分(用E表示)并分别存放。指数部分采用规范化的指数形式,指数也分正、负(符号位,用S表示),如图1所示
数符(即符号位)占1位,是0时代表正数,是1时代表负数,表1是IEEE-754浮点型变量存储标准。
S:S是符号位,用来表示正、负,是1时代表负数,是0时代表正数.
E:E代表指数部分(指数部分的值规定只能是1到254,不能是全0,全1),指数部分运算前都要减去127(这是IEEE-754的规定),因为还要表示负指数,这里的10000001转换为十进制数为129,129-127=2,即实际指数部分为2.
M:M 代表小数部分,这里为00100000000000000000000.底数左边省略存储了一个1(这是IEEE-754 的规定),使用的实际底数表示为1.00100000000000000000000
(1.001 小数部分 1.125(1+0+0+0.125) 小数部分乘以指数部分就是4.5)
(移位算法:1001.1 4+0.5=4.5)
上面表1可以变为如下表格:
2、执行方法如下:
#include <stdio.h>
int main() {
float f = 4.5;
float f1 = 1.456;
return 0;
}
在上图中我们可以看到 4.5,也就是变量f的内存是0000 9040,因为是小端存储,所以实际值是 0x40900000.
首先先看f的小数部分,如下表2所示,M(灰色)代表小数部分,这里为00100000000000000000000,总计23位,底数左边省略存储了一个1(这是IEEE-754的规定),使用的实际底数表示为1.00100000000000000000000
接着看指数部分,计算机并不能直接计算 10的幂次,f的指数部分是表2中的EEEEEEEE所对应的部分,也就是10000001,其十进制值为129,129-127=2,即实际指数部分为2指数值为2,代表2的2次幂。因此将1.001向左移动2位即可,也就是100.1;然后转换为十进制数,整数部分是4,小数部分是2-1,刚好等于0.5,因此十进制数为 4.5.浮点数的小数部分是通过2-1+2-2+2-+…来近似一个浮点数的.
可能你会疑惑,不应该是小数部分乘以指数部分么,怎么变成左移 2位,其实是等价的,对于小数部分 1.001,其十进制值为2°+2-3=1.125.那么 1.125*指数部分,也就会4,就是 1.125*4=4.5,也就是等于 4.5.
在上图中我们可以看到1.456,也就是变量f1的内存是35 5e ba 3f,因为是小端存储,所以实际值是 0x 3f ba 5e 35.
首先先看f1的小数部分,如下表3所示,M(灰色)代表小数部分,这里为0111010010111100011 0101,总计23位。底数左边省略存储了一个1(这是IEEE-754的规定),使用的实际底数表示为1.011 1010 0101 1110 0011 0101.
接着看指数部分,计算机并不能直接计算10的幂次,f1的指数部分是表3中的 EEEEEEEE所对应的部分,也就是01111 111,其十进制值为127,127-127=0,即实际指数部分为0指数值为0,代表2的0次幂。因此1.01110100101111000110101无需做移动.
浮点数的小数部分是通过 2°+22+23+2-+2-…来近似一个浮点数的,1+0.25+0.125+0.0625+0.015625=1.453125
四、浮点数精度丢失
浮点型变量分为单精度(foat)型、双精度(double)型,如表1所示,因为浮点数使用的是指数表示法,需要记忆数值的范围(考研会考),同时我们需要注意浮点数的精度问题。
上表中 double 类型是-1022到 1023,是通过 1-2046(因为不能是全0,全1,全1是2047)减去 1023,得到-1022到 1023。
如下例所示,我们赋给 a的值为1.23456789e10,加20 后,应该得到的值为 1.234567892e10但b输出结果却是b=12345678848.000000,变得更小了。我们将这种现象称为精度丢失,因为 float 型数据能够表示的有效数字为 7位,最多只保证 1.234567e10 的正确性,要使结果正确就需要把 a和b均改为 double 型,因为 double 可以表示的精度为 15~16 位。
#include <stdio.h>
//提醒 scanf 读取 double类型时,要用 if,如 double d;scanf("%f",&d);初试用不到,机试可能考
int main() {
//赋值的一瞬间发生 精度丢失,因为浮点常量默认是8 个字节存储,double 型
double a= 1.23456789e10;
double b;
b=a+ 20;//计算时,精度丢失 12345678920
printf("b=%f\n",b);
//%f 即可以输出 float,也可以输出 double类型
return 0;
}
另外针对强制类型转换,int 转foat 可能造成精度丢失,因为int是有10 位有效数字的,但是int强制转为 double 不会,foat转为 double也不会丢失精度。
第三章:汇编语言零基础入门
一、汇编指令格式讲解-C语言转汇编方法讲解
1、汇编指令格式
在去看汇编指令前,我们来看下CPU是如何执行我们的程序的,如下图所示,我们编译后的可执行程序,也就是 main.exe,是放在代码段的,PC指针寄存器存储了一个指针,始终指向要执行的指令,读取了代码段的某一条指令后,会交给译码器来解析,这时候译码器就知道要做什么事情了,CPU 中的计算单元加法器不能直接对栈上的某个变量a,直接做加1操作的,需要首先将栈,也就是内存上的数据,加载到寄存器中,然后再用加法器做加1操作,再从寄存器搬到内存上去。
(CPU 读写寄存器的速度比读写内存的速度要快很多)
操作码字段(你要实施什么操作):表征指令的操作特性与功能(指令的唯一标识)不同的
指令操作码(要操作哪里的内存):不能相同地址码字段:指定参与操作的操作数的地址码
指令中指定操作数存储位置的字段称为地址码,地址码中可以包含存储器地址,也可包含寄存器编号.
指令中可以有一个、两个或者三个操作数,也可没有操作数,根据一条指令有几个操作数地址可将指令分为零地址指令。一地址指令、二地址指令、三地址指令。4个地址码的指令很少被使用(考研考不到,这里不列了)
零地址指令:只有操作码,没有地址码(空操作止等)。
一地址指令:指令编码中只有一个地址码,指出了参加操作的一个操作数的存储位置,如果还有另一个操作数则隐含在累加器中。
eg:INC AL 自加指令
二地址指令:指令编码中有两个地址,分别指出了参加操作的两个操作数的存储位置,结果存储在其中一个地址中。
(op a1,a2) a1和 a2 的操作结果放人a1
eg: MOV AL,BL
ADD AL,30
三地址指令:指令编码中有3个地址码,指出了参加操作的两个操作数的存储位置和一个结果的地址(考研很少考)。
(op al,a2,a3:al和a2的结果放人 a3)
地址指令格式中,从操作数的物理位置来说有可归为三种类型
寄存器-寄存器(RR)型指令:需要多个通用寄存器或个别专用寄存器,从寄存器中取操作数把操作结果放入另一个寄存器,机器执行寄存器-寄存器型的指令非常快,不需要访存。
寄存器-存储器(RS)型指令:执行此类指令时,既要访问内存单元,又要访问寄存器。
存储器-存储器(SS)型指令:操作时都是涉及内存单元,参与操作的数都是放在内存里从内存某单元中取操作数,操作结果存放至内存另一单元中,因此机器执行指令需要多次访问内。
寄存器英文:register
存储器英文:storage
(*)复杂指令集:变长 x86 CISC Complex Instruction Set Computer
精简指令集:等长/定长 arm RISC Reduced Instruction Set Computinar
2、生成汇编方法
掌握把 C 语言转换汇编的手法,那么就不会再害怕汇编了!
编译过程(了解即可,是大纲范围,考的概率不高)
第一步:main.c-->编译器--》main.ss文件(.s文件就是汇编文件,文件内是汇编代码);
第二步:我们的 main.s汇编文件--》汇编器--》main.obj(main.obj 里面是机器码);
第三步:main.obj 文件-》链接器-》可执行文件exe。
CLion 生成汇编的步骤(很多书籍不讲这个,直接就给你看汇编指令)
1、首先我们需要设置环境变量,对 此电脑右键点击,选择属性
环境变量设置好后,永久生效的,后面生成汇编不需要再次设置!
Mac 电脑无需设置环境变量,直接进行下面操作即可。
首先完成C代码的编写,如下图所示
然后再终端窗口中输人 gcc -S- fverbose-asm main.c就可以生成汇编文件 main.s,如下图所示
因为 CLion 看汇编代码没有颜色,因此大家阅读汇编代码,通过《VScode 安装流程》安装VScode 及 asm 插件后,来看代码高亮后效果.
VS 生成汇编的步骤(针对部分同学使用的 VS,没有安装 CLion 的如何操作)1、如图1所示,右键项目,点击属性。
如果2所示,选择输出文件,汇编程序输出选择程序集、机器码和源代码
VS 的汇编后缀是 xx.cod
二、汇编常用指令
1、相关寄存器
除 EBP和 ESP外,其他几个寄存器的用途是比较任意的,也就是什么都可以存。
2、常用指令
汇编指令通常可以分为数据传送指令、逻辑计算指令和控制流指令,下面以Imtel格式为例(考研考的就是 Intel 的汇编),介绍一些重要的指令。以下用于操作数的标记分别表示寄存器、内存和常数。
- <reg>:表示任意寄存器,若其后带有数字,则指定其位数,如<reg32>表示32位寄存器(eax、ebx、ecx、edx、esi、edi、esp或ebp);<reg16>表示16 位寄存器(ax、bx、cx或dx);<reg8>表示8位寄存器(ah、al、bh、bl、ch、cl、dh、dl)。
- <mem>:表示内存地址(如[eax]、[var+4]或 dword ptr [eax+ebx])。
- <con>:表示8位、16位或32位常数。<con8>表示8位常数:<con16>表示16位常数;<con32>表示32 位常数。(也称为立即数)
(1)数据传送指令
1)mov 指令
将第二个操作数(寄存器的内容、内存中的内容或常数值)复制到第个操作数(寄存器或内存),但不能用于直接从内存复制到内存.其语法如下:
- mov <reg>,<reg>
- mov <reg>,<mem>
- mov <mem>,<reg>
- mov <reg>,<con>
- mov <mem>,<con>
举例:
- mov eax,ebx #将 ebx值复制到 eax
- mov byte ptr [var] 5 #将5保存到var值指示的内存地址的一字节中
2)push 指令
将操作数压人内存的栈,常用于函数调用。ESP是栈顶,压栈前先将ESP值减4(栈增长方向与内存地址增长方向相反),然后将操作数压人ESP指示的地址,其语法如下:
push <reg32>
push <mem>
push <con32>
举例(注意,栈中元素固定为32位):
push eax
:将eax的值压栈。
push [var]
:将var所指示的内存地址的4字节值压栈。
3)pop
指令
与push
指令相反,pop
指令执行的是出栈工作,出栈前先将ESP指示的地址中的内容出栈,然后将ESP值加4。其语法如下:
pop <reg>
pop <mem>
举例:
pop edi
:弹出栈顶元素送到edi。
pop [ebx]
:弹出栈顶元素送到ebx所指示的内存地址的4字节中。
(2)算术和逻辑运算指令
1)add/sub
指令
add
指令将两个操作数相加,相加的结果保存到第一个操作数中;sub
指令用于两个操作数相减,相减的结果保存到第一个操作数中。它们的语法如下:
add <reg>,<reg>
add <reg>,<mem>
add <mem>,<reg>
add <reg>,<con>
add <mem>,<con>
【或】
sub <reg>,<reg>
sub <reg>,<mem>
sub <mem>,<reg>
sub <reg>,<con>
sub <mem>,<con>
举例:
sub eax,10 #
eax <-eax - 10
add byte ptr [var],10 #
10与var所指示的内存地址的一字节值相加,并将结果保存在var所指示的内存地址的字节中。
2)inc/dec
指令
inc
、dec
指令分别表示将操作数自加1、自减1。它们的语法如下:
inc <reg>
inc <mem>
【或】
dec <reg>
dec <mem>
举例:
dec eax
:eax的值自减1。
inc dword ptr [var]
:var所指示的内存地址的4字节值自加1。
3)imul
指令
带符号整数乘法指令,有两种格式:①两个操作数,将两个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器;②三个操作数,将第二个和第三个操作数相乘,将结果保存在第一个操作数中,第一个操作数必须为寄存器。其语法如下:
imul <reg32>,<reg32>
imul <reg32>,<mem>
imul <reg32>,<reg32>,<con>
imul <reg32>,<mem>,<con>
举例:
imul eax,[var]
:eax = eax × [var]。
imul esi,edi,25
:esi = edi × 25。
操作结果可能溢出,则编译器置溢出标志OF=1,以使CPU调出溢出异常处理程序。
4)idiv
指令
带符号整数除法指令,它只有一个操作数,即除数,而被除数则为edx:eax中的内容(64位整数),操作结果有两部分:商和余数,商送到eax,余数则送到edx。其语法如下:
idiv <reg32>
idiv <mem>
举例:
idiv ebx
idiv dword ptr [var]
5)and/or/xor
指令
and
、or
、xor
指令分别是按位与、按位或、按位异或操作指令,用于操作数的位操作(按位与,按位或,异或),操作结果放在第一个操作数中。它们的语法如下:
and <reg>,<reg>
and <reg>,<mem>
and <mem>,<reg>
and <reg>,<con>
and <mem>,<con>
【或】
or <reg>,<reg>
or <reg>,<mem>
or <mem>,<reg>
or <reg>,<con>
or <mem>,<con>
【或】
xor <reg>,<reg>
xor <reg>,<mem>
xor <mem>,<reg>
xor <reg>,<con>
xor <mem>,<con>
举例:
and eax,0xF #
将eax中的前28位全部置为0,最后4位保持不变。
xor edx,edx #
将edx中的内容置为0。
6)not
指令
位翻转指令,将操作数中的每一位翻转,即0→1、1→0。其语法如下:
not <reg>
not <mem>
举例:
not byte ptr [var]
:将var所指示的内存地址的一字节的所有位翻转。
7)neg
指令
取负指令。其语法如下:
neg <reg>
neg <mem>
举例:
neg eax
:eax = -eax。
8)shl/shr
指令
逻辑移位指令,shl
为逻辑左移、shr
为逻辑右移,第一个操作数表示被操作数,第二个操作数指示移位的位数。它们的语法如下:
shl <reg>,<con8>
shr <reg>,<con8>
shl <mem>,<con8>
shr <mem>,<con8>
【或】
shl <reg>,cl
shr <reg>,cl
shl <mem>,cl
shr <mem>,cl
举例:
shl eax,1
:将eax值左移1位,相当于乘以2。
shr ebx,cl
:将ebx值右移n位(n为cl中的值),相当于除以2。
9)lea
指令。
地址传送指令,将有效地址传送到指定的寄存器。
举例:
lea eax, DWORD PTR _arrs[ebp]
:将DWORD PTR _arrs[ebp]对应空间的内存地址值放到eax中。
(3)控制流指令
x86处理器维持着一个指示当前执行指令的指令指针(IP),当一条指令执行后,此指针自动指向下一条指令。IP寄存器不能直接操作,但可以用控制流指令更新。通常用标签(label)指示程序中的指令地址,在x86汇编代码中,可在任何指令前加入标签。例如:
mov esi, [ebp+8]
begin: xor ecx,ecx
mov eax, [esi]
这样就用begin
(begin
代表标签名,可以为别的名字)指示了第二条指令,控制流指令通过标签就可以实现程序指令的跳转。
1)jmp
指令
jmp
指令控制IP转移到label所指示的地址(从label中取出指令执行)。其语法如下:
jmp <label>
举例:
jmp begin
:跳转到begin
标记的指令执行。
2)jcondition
指令
条件转移指令,依据CPU状态字中的一系列条件状态转移。CPU状态字中包括指示最后一个算术运算结果是否为0,运算结果是否为负数等。其语法如下:
je <label>
(jump when equal)
jne <label>
(jump when not equal)
jz <label>
(jump when last result was zero)
jg <label>
(jump when greater than)
jge <label>
(jump when greater than or equal to)
jl <label>
(jump when less than)
jle <label>
(jump when less than or equal to)
举例:
cmp eax, ebx
jle done
:如果eax的值小于等于ebx的值,跳转到done
指示的指令执行,否则执行下一条指令。
3)cmp/test
指令
cmp
指令用于比较两个操作数的值,test
指令对两个操作数进行逐位与运算,这两类指令都不保存操作结果,仅根据运算结果设置CPU状态字中的条件码。其语法如下:
cmp <reg>,<reg>
cmp <reg>,<mem>
cmp <mem>,<reg>
cmp <reg>,<con>
test <reg>,<reg>
test <reg>,<mem>
test <mem>,<reg>
test <reg>,<con>
举例:
cmp dword ptr [var], 10
:将var所指示的内存地址的4字节内容,与10比较。
jne loop
:如果不相等则跳转到loop
处执行。
test eax,eax
:测试eax是否为零。
jz xxxx
:如果为零则跳转到xxxx
处执行。
4)call/ret
指令
分别用于实现子程序(过程、函数等)的调用及返回。其语法如下:
call <label>
ret
call
指令首先将当前执行指令地址入栈,然后无条件转移到由标签指示的指令。与其他简单的跳转指令不同,call
指令保存调用之前的地址信息(当call
指令结束后,返回调用之前的地址)。ret
指令实现子程序的返回机制,ret
指令弹出栈中保存的指令地址,然后无条件跳转到保存的指令地址执行。call
和ret
是程序(函数)调用中最关键的两条指令。
3、条件码
编译器通过条件码(标志位)设置指令和各类转移指令来实现程序中的选择结构语句。3.2中的控制流指令中的Jcondition
条件转移指令,就是根据条件码来实现跳转。
(1)条件码(标志位)
除了整数寄存器,CPU还维护着一组条件码(标志位)寄存器,它们描述了最近的算术或逻辑运算操作的属性。可以检测这些寄存器来执行条件分支指令,最常用的条件码有:
CF:进(借)位标志,最近无符号整数加运算后的进位,或者无符号正数减法运算时有借位情况。有进(借)位CF=1;否则CF=0。例如:(unsigned)t < (unsigned)a,因为判断大小是相减,如果t是3,a是4,则CF=1。
ZF:零标志,最近的操作的运算结果是否为0。若结果为0,ZF=1;否则ZF=0。例如:(t=0)。
SF:符号标志,最近的带符号数运算结果的符号,负数时,SF=1;否则SF=0。
OF:溢出标志,最近带符号数运算的结果是否溢出,若溢出,OF=1;否则OF=0。
可见,OF和SF对无符号数运算来说没有意义,而CF对带符号数运算来说没有意义。ZF标志位既可以用于有符号数,也可以用于无符号数。
如何判断溢出:
简单来说,正数相加变负数为溢出,负数相加变正数为溢出。但考研通常会给出十六进制的两个数来考溢出,判断方法如下(针对有符号数):
数据位高位进位,符号位高位未进位,溢出(两个大正数相加,例如120和25)。
数据位高位未进位,符号位高位进位,溢出(两个小负数相加,例如-128和-113)。
数据位高位进位,符号位高位进位,不溢出(两个大负数相加,例如-1和-1)。
数据位高位未进位,符号位未进位,不溢出(两个小正数相加,例如2和3)。
简单一句话就是数据位高位和符号位高位进位不一样的时候会溢出。
如果是针对无符号数,判断方法如下:
数据位高位进位,进位发生,溢出(两个大正数相加,例如120和140)。
数据位高位未进位,进位未发生,无溢出(两个小正数相加,例如2和3)。
常见的算术逻辑运算指令(如add
、sub
、imul
、or
、and
、shl
、inc
、dec
、not
、sal
等)会设置条件码。但有两类指令只设置条件码而不改变任何其他寄存器,即cmp
和test
指令。cmp
指令和sub
指令的行为一样,test
指令与and指令的行为类似,但它们只设置条件码,而不更新目的寄存器。(在后面 408的组成原理课程中,将进行详细的讲解,这里先记住哪些指令会设置条件码即可)
【注意】乘法溢出后,可以跳转到“溢出自陷指令",例如imnt 0x2e 就是一条自陷指令,但是考研只需要掌握溢出,可以跳转到“溢出自陷指令”即可,不需要记自陷指令有哪些.
三、各种变量赋值汇编
我们针对整型、整型数组、整型指针变量的赋值(浮点与字符等价的),对应的汇编进行解析,首先我们编写下面的C代码:
#include<stdio.h>
int main() {
int arr[3] = {1, 2, 3};
int *p;
int i = 5;
int j = 10;
i = arr[2];
p = arr;
printf("=%d\n", i);
return 0;
}
然后根据上一小节生成汇编的方法,在终端窗口输入
gcc -m32 -masm=intel -S -fverbose-asm main.c
(苹果的M1电脑如果不可用,可以直接看课件我的汇编即可,也可以去掉-masm=intel
,这样汇编和考研大题格式有差异);
如下图所示,得到汇编文件main.s
。
接下来我们来分析转换后的汇编代码,首先#
号代表注释,我们从main
标签位置开始看。我们的C代码在让CPU去运行时,其实所有的变量名都已经消失了,实际是数据从一个空间,拿到另一个空间的过程。
栈内变量先定义在低地址还是高地址,取决于操作系统与CPU的组合,你的可能和我的不一样,因此不用去研究,没有意义(也不属于考研大纲范围)。
我们访问所有变量的空间都是通过栈指针(esp
时刻都存着栈指针,也可以称为栈顶指针)的偏移,来获取对应变量内存空间的数据的。
.text
.def _main; scl 2; type 32; .endef
.section .rdata,"dr"
LC0:
.ascii "=%d\n\0"
.text
.globl _main
_main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 48
# main.c:4: int arr[3] = {1, 2, 3};
mov DWORD PTR [esp+24], 1
mov DWORD PTR [esp+28], 2
mov DWORD PTR [esp+32], 3
# main.c:6: int i = 5;
mov DWORD PTR [esp+44], 5
# main.c:7: int j = 10;
mov DWORD PTR [esp+40], 10
# main.c:8: i = arr[2];
mov eax, DWORD PTR [esp+32]
mov DWORD PTR [esp+44], eax
# main.c:9: p = arr;
lea eax, [esp+24]
mov DWORD PTR [esp+36], eax
# main.c:10: printf("=%d\n", i);
mov eax, DWORD PTR [esp+44]
mov DWORD PTR [esp+4], eax
mov DWORD PTR [esp], OFFSET FLAT:LC0
call _printf
# main.c:11: return 0;
mov eax, 0
# main.c:12:
leave
ret
.ident "GCC: (x86_64-posix-sjlj-rev0, Built by MinGW-W64 project) 8.1.0"
.def printf; scl 2; type 32; .endef
大家转的汇编的偏移值可能和我这里有一些差异,这个没关系,这一节大家理解变量赋值的汇编指令及原理即可,主要掌握的指令是mov
,还有lea
,还有PTR
,下面是PTR
介绍。
PTR
(Pointer,即指针)的缩写。汇编里面PTR
是规定的字(既保留字,是用来临时指定类型的)。
(可以理解为,PTR
是临时的类型转换,相当于C语言中的强制类型转换)
如:
mov ax, bx
:是把BX寄存器“里”的值赋予AX,由于二者都是寄存器,长度已定(word型),所以没有必要加WORD PTR
。
mov ax, word ptr [bx]
:是把内存地址等于“BX寄存器的值”的地方所存放的数据,赋予AX,由于只是给出一个内存地址,不知道希望赋予AX的是byte
还是word
,所以可以用word ptr
明确指出。如果不用(即mov ax, [bx]
),则在8086中默认传递一个字(两个字节)给AX。
Intel中的:
dword ptr
:长字(4字节)
word ptr
:双字(2字节)
byte ptr
:一字节
四、选择与循环汇编
#include <stdio.h>
int main() {
int i = 5;
int j = 10;
if (i < j)
printf("i is small\n");
for (i = 0; i < 5; i++)
printf("this is loop\n");
return 0;
}
然后根据之前小节生成汇编的方法,在终端窗口输入
gcc -m32 -masm=intel -S -fverbose-asm main.c
,如下图所示,得到汇编文件main.s
。
接下来我们来分析转换后的汇编代码,首先#
号代表注释,我们从main
标签位置开始看即可。我们的C代码在让CPU去运行时,其实所有的变量名都已经消失了,实际是数据从一个空间,拿到另一个空间的过程。
栈内变量先定义在低地址还是高地址,取决于操作系统与CPU的组合,你的可能和我的不一样,因此不用去研究,没有意义(也不属于考研大纲范围)。我们访问所有变量的空间都是通过栈指针(esp
时刻都存着栈指针,也可以称为栈顶指针)的偏移,来获取对应变量内存空间的数据的。
.text # 这里是文字常量区,放了我们的字符串常量,LC0和LC1分别是我们要用到的两个字符串常量的起始地址
.def _main; scl 2; type 32; .endef
.section .rdata, "dr"
LC0:
.ascii "i is small\n\0"
LC1:
.ascii "this is loop\n\0"
.text
.globl _main
_main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 32
# main.c:5: int i = 5;
mov DWORD PTR [esp+28], 5 # 把常量5放入栈指针(esp寄存器存的栈指针)偏移28个字节的位置,这个位置是变量i的空间
# main.c:6: int j = 10;
mov DWORD PTR [esp+24], 10 # 把常量10放入栈指针(esp寄存器存的栈指针)偏移24个字节的位置,这个位置是变量j的空间
# main.c:7: if (i < j)
mov eax, DWORD PTR [esp+28] # 把栈指针(esp寄存器存的栈指针)偏移28个字节的位置内的值,放入eax寄存器
cmp eax, DWORD PTR [esp+24] # 比较eax寄存器内的值和栈指针偏移24个字节位置的值的大小,拿eax寄存器值减去DWORD PTR [esp+24],然后设置条件码
jge L2 # 如果eax寄存器大于等于DWORD PTR [esp+24],那么跳转到L2,否则直接往下执行,jge是根据条件码ZF和SF来判断的
# main.c:9: printf("i is small\n");
mov DWORD PTR [esp], OFFSET FLAT:LC0 # 把LC0(也就是上面那个字符串)的地址,放到寄存器栈指针指向的内存位置
call _puts
L2:
# main.c:11: for (i = 0; i < 5; i++)
mov DWORD PTR [esp+28], 0 # 把常量0放入栈指针(esp寄存器存的栈指针)偏移28个字节的位置,这个位置是变量i的空间
jmp L3 # 无条件跳转到L3
L4:
# main.c:13: printf("this is loop\n");
mov DWORD PTR [esp], OFFSET FLAT:LC1 # 把LC1(也就是上面那个字符串)的地址,放到寄存器栈指针指向的内存位置
call _puts
# main.c:11: for (i = 0; i < 5; i++)
add DWORD PTR [esp+28], 1
L3:
# main.c:11: for (i = 0; i < 5; i++)
cmp DWORD PTR [esp+28], 4 # 比较栈指针偏移28个字节位置的值与4的大小,拿DWORD PTR [esp+28]减去4,然后设置条件码
jle L4 # 如果小于等于4,跳转到L4
# main.c:15: return 0;
mov eax, 0
# main.c:16:
leave
ret
这一节大家理解选择、循环的汇编指令及原理即可,主要掌握的指令是cmp
、jge
、jmp
、jle
等,以及了解一下字符串常量是存在文字常量区的。
五、函数调用汇编
这一小节我们针对C语言的指针的间接访问,函数调用,返回值,对应的汇编进行解析,首先我们编写下面C代码:
#include <stdio.h>
int add(int a, int b) {
int ret;
ret = a + b;
return ret;
}
int main() {
int a, b, ret;
int *p;
p = &a;
b = *p + 2;
ret = add(a, b);
printf("add result=%d\n", ret);
return 0;
}
然后根据之前小节生成汇编的方法,在终端窗口输入gcc -m32 -masm=intel -S -fverbose-asm main.c
,如下图所示,得到汇编文件main.s
。
接下来我们来分析转换后的汇编代码,首先#
号代表注释,我们从main
标签位置开始看即可。我们的C代码在让CPU去运行时,其实所有的变量名都已经消失了,实际是数据从一个空间,拿到另一个空间的过程。
栈内变量先定义在低地址还是高地址,取决于操作系统与CPU的组合,你的可能和我的不一样,因此不用去研究,没有意义(也不属于考研大纲范围)。我们访问所有变量的空间都是通过栈指针(esp
时刻都存着栈指针,也可以称为栈顶指针)的偏移,来获取对应变量内存空间的数据的。
函数调用的汇编原理解析
先必须明确的一点是,函数栈是向下生长的。所谓向下生长,是指从内存高地址向低地址的路径延伸。于是,栈就有栈底和栈顶,栈顶的地址要比栈底的低。
对于x86体系的CPU而言,寄存器ebp
可称为基指针或基址指针(base pointer),寄存器esp
可称为栈指针(stack pointer)。这里需要说明的几点如下:
-
ebp
在未改变之前始终指向栈底的开始,所以ebp
的用途是在堆栈中寻址(寻址的作用会在下面详细介绍)。 -
esp
会随着数据的入栈和出栈而移动,即esp
始终指向栈顶。
如下图2所示,假设函数A调用函数B,称函数A为调用者,称函数B为被调用者,则函数调用过程可以描述如下:
-
首先将调用者(A)的堆栈的基址(
ebp
)入栈,以保存之前任务的信息。 -
然后将调用者(A)的栈顶指针(
esp
)的值赋给ebp
,作为新的基址(即被调用者B的栈底)。原有函数的栈顶,是新函数的栈底。 -
再后在这个基址(被调用者B的栈底)上开辟(一般用
sub
指令)相应的空间用作被调用者B的栈空间。 -
函数B返回后,当前栈顶的
ebp
恢复为调用者A的栈顶(esp
),使栈顶恢复函数B被调用前的位置;然后调用者A从恢复后的栈顶弹出之前的ebp
值(因为这个值在函数调用前一步被压入堆栈)。
这样,ebp 和 esp 就都恢复了调用函数B前的位置,即栈恢复函数B调用前的状态。相当于(ret 指令做了什么)
mov esp,ebp //把 ebp内的内容复制到esp寄存器中,也就是B函数的栈基作为原有调用者A 函数的栈顶
pop ebp //弹出栈顶元素,放到ebp寄存器中,因为原有A函数的基指针压到了内存里,所以弹出后,放人ebp,这样原函数A的现场恢复完毕
.text
.globl _add
.def _add; .scl 2; .type 32; .endef
_add: # add函数的入口
push ebp
mov ebp, esp
sub esp, 16
mov edx, DWORD PTR [ebp+8] # 拿到实参a的值,放入edx
mov eax, DWORD PTR [ebp+12] # 拿到实参b的值,放入eax
add eax, edx
mov DWORD PTR [ebp-4], eax # 把结果放入ebp偏移-4的位置
mov eax, DWORD PTR [ebp-4] # 将结果放入eax作为返回值
leave
ret
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 32
mov DWORD PTR [esp+16], 5 # a = 5
lea eax, [esp+16]
mov DWORD PTR [esp+28], eax # p = &a
mov eax, DWORD PTR [esp+28]
mov eax, DWORD PTR [eax]
add eax, 2
mov DWORD PTR [esp+24], eax # b = *p + 2
mov eax, DWORD PTR [esp+16]
mov edx, DWORD PTR [esp+24]
mov DWORD PTR [esp+4], edx
mov DWORD PTR [esp], eax
call _add # 调用add函数
mov DWORD PTR [esp+20], eax # ret = add(a, b)
mov eax, DWORD PTR [esp+20]
mov DWORD PTR [esp+4], eax
mov DWORD PTR [esp], OFFSET FLAT:LCO
call _printf
mov eax, 0
leave
ret
这一节大家理解指针变量的间接访问原理,函数调用的原理,到这里大家对于C语言的每一部分在机器上的运行已经非常清晰,这一节主要掌握的指令是 add
, sub
, call
, ret
等。下
面还需要掌握函数调用时,机器码的偏移值。我们前面转的都只有汇编,没有含有机器码。如何得到机器码,需要运行以下两条指令:
gcc -m32 -g -o main.exe main.c
(Mac一致)
objdump -source main.exe > main.dump
(Mac去掉exe后缀,写为main即可)
只需要掌握下面代码中的 e8 ab ff ff ff401510<_add>中的 e8 ab ffff ff 是call
什么含义即可,e8代表cal,而ab ffffff 是通过 00401510 减去401565所得
00401510 <_add>:
401510: 55 push %ebp
401511: 89 e5 mov %esp,%ebp
401513: 83 ec 10 sub $0x10,%esp
401516: 8b 55 08 mov 0x8(%ebp),%edx
401519: 8b 45 0c mov 0xc(%ebp),%eax
40151c: 01 d0 add %edx,%eax
40151e: 89 45 fc mov %eax,-0x4(%ebp)
401521: 8b 45 fc mov -0x4(%ebp),%eax
401524: c9 leave
401525: c3 ret
00401526 <_main>:
401526: 55 push %ebp
401527: 89 e5 mov %esp,%ebp
401529: 83 e4 f0 and $0xfffff0,%esp
40152c: 83 ec 20 sub $0x20,%esp
40152f: e8 ec 00 00 00 call 401620 <_main>
401534: c7 44 24 10 05 00 00 movl $0x5,0x10(%esp)
40153b: 00
40153c: 8d 44 24 10 lea 0x
这一节大家理解指针变量的间接访问原理
第四章:文件的操作(C语言)
一、文件操作原理
1、C文件概述
程序执行时就称为进程,进程运行过程中的数据均在内存中,需要存储运算后的数据时,就需要使用文件这样程序下次启动后,就可以直接从文件中读取数据(不像我们之前的程序,每次运行都需要手动输入数据)。
文件是指存储在外部介质(如磁盘、磁带)上的数据集合,操作系统(Windows、Linux、Mac等)是以文件为单位对数据进行管理的,如下图所示:
C语言对文件的处理方法如下
-
缓冲文件系统:系统自动地在内存区为每个正在使用的文件开辟一个缓冲区,用缓冲文件系统进行的输入/输出称为高级磁盘输入/输出。
-
非缓冲文件系统:系统不自动开辟确定大小的缓冲区,而由程序为每个文件设定缓冲区。用非缓冲文件系统进行的输入/输出称为低级输入/输出。
下面介绍缓冲区原理
缓冲区其实就是一段内存空间,分为读缓冲、写缓冲。C语言缓冲的三种特性如下:
-
全缓冲:在这种情况下,当填满标准I/O缓存后才进行实际I/O操作,全缓冲的典型代表是对磁盘文件的读写操作。
-
行缓冲:在这种情况下,当在输入和输出中遇到换行符时,将执行真正的I/O操作。这时,我们输入的字符先存放到缓冲区中,等按下回车键换行时才进行实际的I/O操作,典型代表是标准输入缓冲区(stdin)和标准输出缓冲区(stdout)。
-
不带缓冲:也就是不进行缓冲,标准出错情况(stderr)是典型代表,这使得出错信息可以直接尽快地显示出来(无需掌握,考研不考)。
2、文件指针介绍
打开一个文件后,我们会得到一个FILE*
类型的文件指针p
,然后通过该文件指针对文件进行操作。FILE
是一个结构体类型,其具体内容如下所示:
struct iobuf {
char* ptr; // 下一个要被读取的字符地址
int cnt; // 剩余的字符,若是读入缓冲区则表示缓冲区中还有多少个字符未被读取
char* base; // 缓冲区基地址
int flag; // 读写状态标志位
int file; // 文件描述符
int charbuf;
int bufsiz; // 缓冲区大小
char* tmpfname;
};
typedef struct iobuf FILE;
FILE* fp;
// 在main.c
中写了FILE* fp;
,然后Ctrl+左键
就可以跳转到上面的结构体类型定义位置。fp
是一个指向FILE
类型结构体的指针变量。可以使fp
指向某个文件的结构体变量,从而通过该结构体变量中的文件信息来访问该文件。
Windows操作系统下的FILE
结构体与Linux操作系统、Mac操作系统下的FILE
结构体中的成员变量名是不一致的,但是其原理可以互相参考。
二、文件的打开及关闭
fopen函数
fopen
函数用于打开由fname
(文件名)指定的文件,并返回一个关联该文件的流。如果发生错误,那么fopen
返回NULL
。mode
(方式)用于决定文件的用途(如输入、输出等),具体形式如下所示:
FILE* fopen(const char *fname, const char *mode);
fclose函数
fclose
函数用于关闭给出的文件流,并释放已关联到流的所有缓冲区。fclose
执行成功时返回0
,否则返回EOF
(-1),具体形式如下所示:
int fclose(FILE *stream);
fputc函数
fputc
函数用于将字符ch
的值输出到fp
指向的文件中。如果输出成功,那么返回输出的字符;如果输出失败,那么返回EOF
。具体形式如下所示:
int fputc(int ch, FILE *stream);
fgetc函数
fgetc
函数用于从指定的文件中读入一个字符,该文件必须是以读或读写方式打开的。如果读取一个字符成功,那么赋给ch
;如果遇到文件结束符,那么返回文件结束标志EOF
。具体形式如下所示:
int fputc(int ch, FILE *stream);
例1:fopen与fclose的使用
#include<stdio.h>
int main()
{
FILE* fp; // 文件类型指针
int i;
char c;
fp = fopen("file.txt", "r+");
if (NULL == fp)
{
perror("fopen");
return -1;
}
while ((c = fgetc(fp)) != EOF) // 循环读取文件内容,当读取到文件末尾时
{
printf("%c", c);
}
i = fputc('H', fp); // 往文件中写一个字符,写入失败会返回EOF
if (EOF == i)
{
perror("fputc");
}
fclose(fp); // 关闭文件
return 0;
}
perror函数
perror
函数用于得到打开失败的原因(对于定位函数失败的原因,常用perror
函数)。如果未新建一个文件,即文件不存在,那么会出现如下图1所示的失败提示:
fopen: No such file or directory
进程已结束,退出代码为1。
冒号之前的内容是我们写人perror函数内的字符串,冒号之后的内容是perror提示的函数失败原因,注意 perror
函数必须紧跟失败的函数,如果中间执行了printf
这样的打印函数,那么perror
函数将提示Success
,也就是没有错误。原因是每个库函数执行时都会修改错误码,一旦函数执行成功,错误码就会被改为零,而perror
函数是读取错误码来分析失败原因的。(perror
考研不考,是帮助大家定位文件操作失败原因的)
文件打开成功后,使用fgetc
函数可以读取文件的每个字符,然后循环打印整个文件,读到文件结尾时返回EOF
,所以通过判断返回值是否等于EOF
就可以确定是否读到文件结尾。注意要在自己新建的file.txt
文件(和.exe
在同级目录,注意查看视频操作)中先填写一些内容。
三、文件读写
1、fread函数与fwrite函数
fread
函数与fwrite
函数的具体形式如下:
int fread(void *buffer, size_t size, size_t num, FILE *stream);
int fwrite(const void *buffer, size_t size, size_t count, FILE *stream);
其中buffer
是一个指针,对fread
来说它是读入数据的存放地址,对fwrite
来说它是输出数据的地址(均指起始地址);size
是要读写的单个成员字节数;num
和count
是要进行读写多少size
字节的数据项;stream
是文件型指针。fread
函数的返回值是读取的内容数量,fwrite
写成功后的返回值是已写对象的数量。
例1:fread与fwrite的使用
#include <stdio.h>
#include <string.h>
int main()
{
char buf[20] = "hello\nworld";
FILE *fp;
int ret;
fp = fopen("file.txt", "r+"); // 把这里改为"rb+"后,再次执行程序视察,会发现文件大小是11个字节
if (NULL == fp)
{
perror("fopen");
return -1;
}
ret = fwrite(buf, sizeof(char), strlen(buf), fp); // 把buf中的字符串写入文件
ret = fread(buf, sizeof(char), sizeof(buf) - 1, fp); // 执行这个代码时,需要注释上面的fwrite
// puts(buf); // 打印buf的内容
fclose(fp);
return 0;
}
fread 和 fwrite 函数既可以以文本方式对文件进行读写,又可以以二进制方式对文件进行读
写。
以"r+"即文本方式打开文件进行读写时,向文件内写人的是字符串,写完后右键文件,选择属性(如下图所示),会发现大小是12个字节。
这是因为在文本方式下,向文本文件中写人"\n"时实际存人磁盘的是"\n",所有的接口调用都是 Windows的系统调用,这是 Windows的底层实现所决定的(Mac和 Linux不会).当然,以文本方式写人,一定要以文本方式读出,遇到"\r\n"时底层接口会自动转换为"\n",因此用fread 函数再次读取数据时,得到的依然是"hello\nworld",共11字节.
如果把 fopen 函数中的"r+"改为"rb+",也就是改为二进制方式,那么当我们向磁盘写人11字节时,磁盘实际存储的就是11字节,如果这时双击打开该文件,那么会发现没有换行,即helloworid 是连在一起的,中间没有换行符,原因是 tt 文本编辑器必须遇到"\r\n"时才进行换行操作.
相信大家此时已经理解了文本方式和二进制方式的差异(在Mac和Linux 操作系统下并不存在这样的问题),以文本方式下写人"\n"后,磁盘存储的是"\r\n",当然读取时会以"\n"的形式读出"\r\n",而以二进制方式写人"\n"后,磁盘存储的是"\n"。二者在其他方面没有差异。那么如何避免出错呢?如果是以文本方式写人的内容,那么一定要以文本方式读取;如果是以二进制方式写人的内容,那么一定要以二进制方式读取,不能混用!(大家主要理解文本文件与二进制文件的区别即可)
例2:写入整型数
#include <stdio.h>
int main()
{
FILE *fp;
int i = 123456;
int ret;
fp = fopen("file.txt", "rb+"); // 以rb+模式打开文件
if (NULL == fp)
{
perror("fopen");
return -1;
}
ret = fwrite(&i, sizeof(int), 1, fp); // 向文件中写入整型数,如果双击打开文件会发现乱码,因为打开文件是以字符格式去解析的
// ret = fread(&i, sizeof(int), 1, fp); // 打开本行注释及下面一行注释时,需要先注释fwrite代码
// printf("fread ret=%d\n", ret);
fclose(fp);
return 0;
}
上例中写人整型数,浮点数时,一定要用二进制方式时,需要以"rb+"方式打开文件,二进制方式下内存中存储的是什么,写人文件的就是什么,是一致的,例如,写入整型变量i,其值为123456,内存存储为4字节,即0x0001E240,那么写人内存的也是4字节,这时双击打开文件看到的是乱码,所以读取时也要用一个整型变量来存储,
2、fgets函数与fputs函数
-
fgets
函数从给出的文件流中读取num-1
个字符(num
是fgets
函数的第二个形参,如果其值为10,那么读取num-1
个字符,即9个),并且把它们转储到str
(字符串)中。fgets
在到达行末时停止。fgets
成功时返回str
(字符串),失败时返回NULL
,读到文件结尾时返回NULL
。其具体形式如下:
char *fgets(char *str, int num, FILE *stream);
-
fputs
函数把str
(字符串)指向的字符写到给出的输出流。成功时返回非负值,失败时返回EOF
。其具体形式如下:
int fputs(const char *str, FILE *stream);
例3:fgets与fputs的使用
#include <stdio.h>
int main()
{
char buf[20] = {0}; // 用于存储读取数据
FILE *fp;
fp = fopen("file.txt", "r+"); // 打开文件
if (NULL == fp)
{
perror("fopen");
return -1;
}
while (fgets(buf, sizeof(buf), fp) != NULL) // 读取到文件结尾时,fgets返回NULL
{
printf("%s", buf);
}
fclose(fp);
return 0;
}
使用 fgets 函数,我们可以一次读取文件的一行,这样就可以轻松地统计文件的行数,注意在做一些机试题目时(机试是用fgets读标准输人,因为gets 部分学校机试不可用),用于fgets函数的 buf不能过小buf 大于最长行的长度),否则可能无法读取"\n",导致行数统计出错.fputs 函数向文件中写一个字符串,不会额外写人一个"\n",可以不用 fputs,掌握fwrite 即可
四、文件位置指针偏移
fseek函数
fseek函数的功能是改变文件的位置指针,其具体调用形式如下:
int fseek(FILE *stream, long offset, int origin);
其中fseek的说明如下:
fseek(文件类型指针,位移量,起始点)
起始点的说明如下:
-
文件开头:
SEEK_SET
(0) -
文件当前位置:
SEEK_CUR
(1) -
文件末尾:
SEEK_END
(2)
位移量是指以起始点为基点,向前移动的字节数。一般要求为long
型。
fseek函数调用成功时返回零,调用失败时返回非零。
ftell函数
ftell函数返回stream(流)当前的文件位置,发生错误时返回-1。当我们想知道位置指针距离文件开头的位置时,就需要用到ftell函数,其具体形式如下所示:
long ftell(FILE *stream);
例1:ftell与fseek的使用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
FILE* fp;
char str[20] = "hello\nworld";
int len = 0; // 用于保存字符串长度
long pos;
int ret;
fp = fopen("file.txt", "r+"); // 打开文件
if (NULL == fp)
{
perror("fopen");
return -1;
}
len = strlen(str); // 保存字符串长度
ret = fwrite(str, sizeof(char), len, fp); // 写入字符串到文件中
ret = fseek(fp, -5, SEEK_CUR); // 从当前位置向后偏移5个字节
if (ret != 0)
{
perror("fseek");
fclose(fp);
return -1;
}
pos = ftell(fp); // 获取位置指针距离文件开头的位置
printf("Now pos=%ld\n", pos);
memset(str, 0, sizeof(str)); // 把str清空
ret = fread(str, sizeof(char), sizeof(str), fp); // 这时候会读到字符串"world"
printf("%s\n", str);
fclose(fp);
return 0;
}
运行效果
最终的运行效果如图1所示:
我们向文件中写入了"hello\nworld"
,因为是文本方式,所以总计为12字节。通过fseek
函数向前偏移5字节后,用ftell
函数得到的位置指针距离文件开头的位置即为7,这时再用fread
函数读取文件内容,得到的是"world"
。
第五章:socket实现网络编程实战
一、Socket实现网络编程原理
1、什么是网络协议,TCP/IP协议族有哪些?
在408计算机网络中,整个内容都是在讲解网络协议。那么什么是网络协议?网络协议是计算机网络中进行数据交换而建立的规则、标准或约定的集合。打个通俗的比方,就像两个人见面,对方说“你好”,你也会讲“你好”,你不会说“你考研不”,这就是一种交流规则。有了规则,任何两台计算机,无论是Windows系统、Mac系统还是手机,都可以顺畅地进行通信,因为它们都遵守TCP/IP协议。
TCP/IP协议总计分为4层,分别是应用层、传输层、网络层、网络接口层
TCP/IP协议族的每一层的作用
-
网络接口层:负责将二进制流转换为数据帧,并进行数据帧的发送和接收。数据帧是独立的网络信息传输单元。
-
网络层:负责将数据包封装成IP数据报,并运行必要的路由算法。
-
传输层:负责端对端之间的通信会话连接和建立,传输协议的选择根据数据传输方式而定。
-
应用层:负责应用程序的网络访问,通过端口号来识别各个不同的进程。
我们可以把数据传输看成我们现实生活中的邮寄快递,数据内容“你好”我们我们要邮寄的包裹,IP 地址就是包裹邮寄的目的地,在网络层的IP 头部中进行了加人,但是这个地址只能到到达某一个家庭,而一个家庭有很多个人,到底是哪个人要接这个包裹,就由TCP头部的端口来确认,到底是哪个人的包裹,为什么要有网络接口层呢,到达网络层时,是PP报文,如果我们发送的不是你好,而是一个很大的数据包,包裹过大了,下层是没办法一次性运输的,所以就要把一个IP 报文拆分为多个以太网帧报文进行发送.
详细的 TCP/IP 协议解析需要大家到 408 的计算机网络课程进行详细的学习
Socket实现TCP通信原理解析
每一台电脑,无论Windows,还是Mac,或者某台手机,操作系统已经实现了网络接口层,网络层,传输层的协议,所以电脑,或者手机你打开就可以连上互联网.Socket,中文名字是套接字,是操作系统(无论是Windows,Mac,Linux都叫Socket,只是接口使用实际存在差异)提供的一套接口,用于我们去使用TCP,或者UDP传输层协议,去实现各种各样的应用层协议,例如大家浏览器上网用的HTTP协议,收发邮件使用的SMTP,POP3 协议,微信 QQ 聊天的腾讯的即时通讯协议等等,均是通过Socket 去实现的,也可以说是通过TCP,或者 UDP 传输层协议实现的,
下面我们来看通过 Socket 去实现TCP 通信的流程图
二、Socket实现网络编程Windows系统
1、Socket 实现 TCP 通信 windows 实战
#pragma comment(lib,"ws2_32.lib")
#include<stdio.h>
#include<winsock2.h>
#include<windows.h>
int main(int argc, char *argv[]) {
// 初始化WSA加载winsock库,Windows需要的步骤,Mac和Linux不需要这一步
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
if (WSAStartup(sockVersion, &wsaData) != 0) { // 初始化失败,退出
return -1;
}
// 创建套接字IPv4, SOCK_STREAM代表初始化TCP套接字(TCP协议)
SOCKET sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sListen == INVALID_SOCKET) {
printf("socket error!");
return -1;
}
// 绑定IP和端口
struct sockaddr_in sin;
sin.sin_family = AF_INET; // 代表进行IPv4,如果是IPv6,则为AF_INET6
sin.sin_port = htons(9999); // 端口号,字节序转换
sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 将一个点分十进制的IP转换成一个整数型
if (bind(sListen, (SOCKADDR*)&sin, sizeof(sin)) == SOCKET_ERROR) {
printf("bind error!");
return -1;
}
// 开始监听
if (listen(sListen, 5) == SOCKET_ERROR) {
printf("listen error!");
return -1;
}
SOCKET sClient; // accept返回的新的套接字使用sClient进行保存
struct sockaddr_in remoteAddr;
int nAddrLen = sizeof(remoteAddr); // 计算remoteAddr的大小
printf("Waiting for connection...\n"); // 等待连接
// 接受连接返回一个新的套接字,这个套接字用于与连接的客户端通信,对方的地址信息保存在remoteAddr中
sClient = accept(sListen, (SOCKADDR*)&remoteAddr, &nAddrLen); // 这里完成了3次握手
if (sClient == INVALID_SOCKET) {
printf("accept error");
return -1;
}
// 接收到一个连接,打印客户端的IP地址和端口号
printf("Client connected from %s:%d\n", inet_ntoa(remoteAddr.sin_addr), ntohs(remoteAddr.sin_port));
char revData[255];
// 接收数据,recv函数接收数据到缓冲区revData中,返回值是实际接收到的字节数,失败返回-1
int ret = recv(sClient, revData, 255, 0);
if (ret > 0) {
revData[ret] = '\0'; // 字符串结尾符
printf("Received: %s\n", revData); // 打印接收到的数据
}
// 发送数据
char* sendData = "hi, TCP Client";
send(sClient, sendData, strlen(sendData), 0); // 发送数据给客户端
// 关闭套接字
closesocket(sClient); // 断开与客户端的连接,这里完成了4次挥手
closesocket(sListen); // 关闭监听,客户端不能connect了
WSACleanup(); // 清理Winsock库,释放相关资源
return 0;
}
2、客户端代码
#include<stdio.h>
#include<winsock2.h>
#pragma comment(lib,"ws2_32.lib")
int main() {
WSADATA wsaData;
SOCKET clientSocket;
struct sockaddr_in serverAddr;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
printf("Failed to initialize Winsock\n");
return -1;
}
// 创建客户端套接字
clientSocket = socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET) {
printf("Failed to create socket\n");
WSACleanup();
return -1;
}
// 设置服务器地址和端口
const char* serverIP = "127.0.0.1"; // 服务器IP地址
const int serverPort = 9999; // 服务器监听的端口号
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr(serverIP); // 将点分十进制,转换网络字节序
serverAddr.sin_port = htons(serverPort); // 将端口号转换为网络字节序
// 连接到服务器,如果服务器那边没有在accept,那么这里请求5次后,就会失败
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Failed to connect to server\n");
closesocket(clientSocket);
WSACleanup();
return -1;
}
printf("Connected to server\n");
// 发送数据
char buffer[1024] = "I am client, Wangdao niu";
if (send(clientSocket, buffer, strlen(buffer), 0) == SOCKET_ERROR) {
printf("Failed to send data\n");
return -1;
}
// 接收数据
int bytesRead = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesRead <= 0) {
printf("Connection closed by server\n");
return -1;
}
// 添加字符串结束符
buffer[bytesRead] = '\0';
printf("Received from server: %s\n", buffer);
closesocket(clientSocket);
WSACleanup();
return 0;
}
三、Socket实现网络编程Mac系统
1、服务器端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 1024
int main() {
int sockfd, client_sockfd;
struct sockaddr_in server_addr;
socklen_t client_addrlen;
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(9999); // 端口号转为网络字节序
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 点分十进制的IP地址,转为32位整型数(网络字节序)
// 绑定套接字到服务器地址
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Binding failed");
exit(EXIT_FAILURE);
}
// 监听连接请求
if (listen(sockfd, 5) < 0) {
perror("Listening failed");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d\n", 9999);
// 接受客户端连接
struct sockaddr_in client_addr; // 用来存储客户端的IP地址和端口号
client_addrlen = sizeof(client_addr);
client_sockfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_addrlen);
if (client_sockfd < 0) {
perror("Accepting connection failed");
exit(EXIT_FAILURE);
}
printf("Client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 接收客户端发送的数据
char buffer[BUFFER_SIZE] = {0};
if (recv(client_sockfd, buffer, BUFFER_SIZE, 0) < 0) {
perror("Receive failed");
exit(EXIT_FAILURE);
}
printf("Received from client: %s\n", buffer); // 打印收到的内容
// 发送数据给客户端
strcpy(buffer, "Hello, client!");
if (send(client_sockfd, buffer, strlen(buffer), 0) < 0) {
perror("Send failed");
exit(EXIT_FAILURE);
}
// 关闭客户端套接字连接
close(client_sockfd);
// 关闭服务器套接字
close(sockfd);
return 0;
}
2、客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 9999
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
// 设置服务器地址信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
perror("Invalid address");
exit(EXIT_FAILURE);
}
// 连接到服务器
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Connection failed");
exit(EXIT_FAILURE);
}
// 发送数据到服务器
strcpy(buffer, "Hello, server!");
if (send(sockfd, buffer, strlen(buffer), 0) < 0) {
perror("Send failed");
exit(EXIT_FAILURE);
}
// 接收服务器发送的数据
memset(buffer, 0, BUFFER_SIZE); // 把buffer中的数据全部填充为0
if (recv(sockfd, buffer, BUFFER_SIZE, 0) < 0) {
perror("Receive failed");
exit(EXIT_FAILURE);
}
printf("Received from server: %s\n", buffer);
// 关闭连接
close(sockfd);
return 0;
}
四、 网络通信两台机器
1、两台机器进行 TCP 通信演示
在 24.3 小节我们进行的是单台 Windows 机器上两个进程用Socket 进行网络通信,下面我们来演示两台 Windows 机器上的通信.
为了进行两台 Windows 机器的演示,我在VMware 内安装一台虚拟机 Windows,如下图所示:
我们来查看VMware内的Windows的IP
按照下面步骤依次点击即可获取IP
代码与24.3小节类似,我们将1-server项目放入VMware内的Windows中(如果想和寝室的同学那么就是把 1-server 发给室友,修改 IP),修改代码IP为192.168.2.65些练习如下图所示:
点击绿色三角形运行按钮,如下图所示:
这时我在本机,修改2-client 代码中的serverlP为192.168.2.65,然后点击绿色三角形按钮运行.
最终就可以看到双方通信成功的效果
大家如果需要实操,那么可以把 1-server 服务器端发给自己的寝室室友,室友的 1-server绑定他本机的IP地址,你们在一个局域网内,然后你自己电脑上的1-client 的IP 改为室友的IP地址,即可测试。
———————————————————————————————————————————