0%

c

C

C基础

一、c语言分步编译

1.预处理:宏定义展开 头文件展开 条件编译 去注释

2.汇编 检查语法 将C语言转变成汇编语言

3.汇编 将汇编语言转成机器语言

4.链接 将C语言依赖库链接到程序中

image-20231204140915723

image-20231204141249224

一步编译

1
gcc -o hello.exe hello1.c hello2.c

常见代码异常

1.编辑时异常

2.编译时异常

3.运行时异常

程序执行过程

image-20231204142152340

二、汇编语言和程序调试

image-20231204145742260

image-20231204145845807

汇编语言

mov 移动
add 添加
push 压栈
pop 出栈
call 调用
eax 32位寄存器
简单的加法计算

1
2
3
4
5
6
mov a, 3
mov b, 4
mov eax, a
add eax, b
mov c, eax

快捷键:
断点 F9
调试 F5
逐语句执行 F11
逐过程执行 F10

寄存器名字

image-20231204151902735

三、数据类型

image-20231204152719971

变量与常量

1.定义常量名

const 数据类型 常量名 = 值;(不建议,不安全

#define 常量名 值;

2.整形变量的定义和输出

打印格式 含义
%d 输出一个有符号的10进制int类型
%o 输出8进制的int类型
%x 输出16进制的int类型,字母以小写输出
%X 输出16进制的int类型,字母以大写输出
%u 输出一个10进制的无符号数

3.进制计算

整形输入

1
2
3
4
5
6
int main(void)
{
int a;
scanf("%d",a);
printf("%d\n",a);
}

出现报错:

error C4996: ‘scanf’: This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS

解决scanf()安全问题

使用

1
#define _CRT_SECURE_NO_WARNINGS

1
#pragma warning(disable:4996)

short、int、long、long long

数据类型 占用空间
short 2字节
int 4字节
long Windows为4字节,Linux为4字节(32位),8字节(64位)
long long 8字节

输出时的占位符:

image-20231204202543936

sizeof计算数据类型大小

image-20231204203738931

字符型:char

定义:char 数据名 = ‘字符’;

sizeof(char) 1字节

对应ASCII码表

float double

image-20231204205801024

float和double认为保留小数点后6位是精确的

打印地址编号

image-20231204211118884

科学计数法打印float:占位符 %e

image-20231204211620965

将科学计数法转换成正常浮点数

image-20231204211903963

四、进制相互转化

二进制十进制互转:转低进制,除二反序取余法,转高进制,权值法

image-20231204213533505

八进制十进制互转:除八反序取余法,权值法

image-20231204214714937

十进制十六进制互转:除十六反序取余法

image-20231204214848129
进制转变简单方法

二进制十进制互转:8421法则

① 11101 16+8+4+0+1=29

② 29 29不包含32,从16开始,29-16=13,13包含8,13-8=5,5包含4,5-4=1

​ 所以1 4 8 16分别对应1 100 1000 10000,将这四个数相加得11101

二进制八进制互转:三位一隔

① 11001000100111110101010 从后向前三位一隔
​ 11 001 000 100 111 110 101 010
​ 3 1 0 4 7 6 5 2

所以11001000100111110101010 => 31047652

② 31047652 将每一位数转换成三位二进制数

​ 3 1 0 4 7 6 5 2
​ 11 001 000 100 111 110 101 010

​ 所以31047652 => 所以11001000100111110101010

二进制十六进制互转:四位一隔

① 7 3 a b c
​ 0111 0011 1010 1011 1100

②0111 0011 1010 1011 1100

​ 7 3 a b c

五、计算机内存数值存储方式

原码:

①最高位作为符号位,0为正,1为负
②其他数值部分就是数值本身绝对值的二进制数
③负数是在绝对值的基础上将最高位变为1

+15 0000 1111
-15 1000 1111
+0 0000 0000
-0 1000 0000

原码表示法简单易懂,与带符号数本身转换比较方便,只要符号还原即可,但是,当两个正数相减或不同符号数相加时,必须比较两个数哪一个绝对值大,才能决定谁减谁,才能决定结果是正是负,所以原码不便于加减运算。

反码:

①对于正数,反码与原码相同

②对于负数,符号位不变,其他部分取反(1,0)

+15 0000 1111
-15 1111 0000
+0 0000 0000
-0 1111 1111

反码运算起来也不方便,通常用来作为求补码的中间过度。

补码:

①对于正数,原码、反码、补码相同
②对于负数,补码为它的反码+1
③补码符号位不动,其他位求反,最后整个数+1,得到反码

计算:

56 - 45 => 56 + (-45)
原码:0011 1000
反码:0011 1000
补码:0011 1000

原码:1010 1101
反码:1101 0010
补码:1101 0011

56补码 0011 1000
-45补码 1101 0011

56 - 45 补码:1 0000 1011
多一位,将第一位舍弃
得0000 1011 补码和原码相同 => 11

26 - 68
原码:0001 1010
反码:0001 1010
补码:0001 1010

原码:1100 0100
反码:1011 1011
补码:1011 1100

26补码:0001 1010
-68补码:1011 1100

26 + (-68)
补码:1101 0110
反码:1101 0101
原码:1010 1010 => -42

数据区间

8bit:-2^7 ~ 2^7-1 -128~127

32bit:-2^31 ~ 2^31-1 -2,147,483,648~2,147,483,647

64bit:-2^63 ~ 2^63-1 -9,223,372,036,854,775,808~9,223,372,036,854,775,807

无符号 数据存储在计算器中不存在符号位
unsigned char 0~255

最大值:1111 1111 2^8-1
最小值:0000 0000 0

unsigned int 0~4,294,967,295

最大值:1111 1111 1111 1111 1111 1111 1111 1111 2^32-1=4,294,967,295
最小值:0000 0000 0000 0000 0000 0000 0000 0000 0

数据溢出

当超出一个数据类型能够存放的最大范围时,数值会溢出

有符号位 最高位溢出的区别:符号位溢出会导致数的正负发生改变,但最高位溢出会导致最高位丢失

如:image-20231205151823426

类型限定

限定符 含义
extern 声明一个变量,extern声明的变量没有建立存储空间。extern int a;//变量在定义的时候创建存储空间
const 定义一个常量,常量的值不能修改。
const int a = 10;
Volatile 防止编译器优化代码
register 定义寄存器变量,提高效率。这是建议型指令,而不是命令型指令,如果CPU有空闲寄存器,那么register生效

printf函数和putchar函数

printf是输出一个字符串,putchar输出一个char

printf格式字符:

打印格式 对应数据类型 含义
%d int 接收整数值并将他表示为有符号的十进制整数
%hd short int 短整数
%hu unsigned short 无符号短整数
%o unsigned int 无符号8进制整数
%u unsigned int 无符号10进制整数
%x,%X unsigned int 无符号16进制整数,x对应abcdef,X对应ABCDEF
%f float 单精度浮点数
%lf double 双精度浮点数
%e,%E double 科学计数法表示的数,e的大小写代表输出是使用的E的大小写
%c char 字符型。可以把输入的数字按照ASCII码相应转换为对应的字符
%s char * 字符串。输出字符串中的字符直至字符串中的空字符
(字符串以‘\0结尾,这个\0即空字符’)
%p void * 以16进制形式输出指针
%% % 输出一个百分号

printf附加格式:

image-20231205190810519 image-20231205191222083 image-20231205192818629

运算符优先级别

20231205200611

数据类型转换

image-20231205201220882

强制转换

格式:(数据类型)变量名;

类型转换原则:占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的数据类型转换,以保证精度不降低。
从占用字节多的数据类型向占用字节数少的数据类型转换时,会降低精度:

image-20231205202152032

程序流程结构

1.分支结构

if结构
1
2
3
4
5
6
7
8
9
10
if(condition)
{
//表达式
}else if(consition2)
{
//表达式
}else
{
//表达式
}
三目运算符
1
表达式1?表达式2:表达式3

如果表达式1为真,则用表达式2作为结果,为假用表达式3作为结果

image-20231205204226805

三目运算符的嵌套

image-20231205204525348

使用#define宏定义表达式

image-20231205205033609

2.选择结构

1
2
3
4
5
6
7
8
9
10
11
12
switch (switch_on)
{
case condition1:
//
break;
case condition2:
//
break;
default:
//
break;
}

3.循环结构

while循环结构
1
2
3
while(condition){
//执行语句
}
1
2
3
do{
//执行语句
}while(condition)

do…while可以用于第一句执行条件为假,当第一句执行完后条件为真再进行后面循环的情况。

for循环结构
1
2
3
4
for (int i = 0; i < value; i++)
{

}

for循环嵌套实例:时钟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>

int main() {
for (int i = 0; i < 24; i++)
{
for (int j = 0; j < 60; j++)
{
for (int k = 0; k < 60; k++)
{
system("cls");
printf("%02d:%02d:%02d",i,j,k);
Sleep(980);
}
}
}
return 0;
}

实例:九九乘法表

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main() {
for (int i = 1; i <= 9; i++)
{
for (int j = 1; j <= i; j++)
{
printf("%d*%d=%d\t", j, i, i * j);
}
putchar('\n');
}
return 0;
}

4.跳转语句

break语句

1.在switch中,跳出case并结束switch
2.在循环中结束循环
3.在嵌套循环中,跳出最近循环

continue语句

结束本次循环,跳到下一次循环

如只输出偶数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
for (int i = 1; i <= 100; i++)
{
if (i % 2 == 0)
{
printf("%d\n", i);
}
else
{
continue;
}
}
return 0;
}
goto语句
image-20231206145941501

六、数组与字符串

概述

定义数组:数据类型 数组名[元素个数] = {值1,值2,值3,……};

数组在内存中存储方式和大小

数组存储在内存中连续的相同类型鹅变量空间。同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的。因此可以通过地址找到数组的下一个

image-20231206152557219

数组名是一个地址常量 指向数组首地址的常量

image-20231206153111166

数组占用内存的大小与数据个数和类型有关

image-20231206155931438

数组逆置

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
#include <stdio.h>

int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int length = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int j = length - 1;
while (i < j)
{
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;

i++;
j--;
}

for (int k = 0; k < length; k++)
{
printf("%d ",arr[k]);
}

return 0;
}
image-20231206185958346
冒泡排序
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
#include <stdio.h>

int main()
{
int arr[] = { 10,56,48,79,51,46,95,32,65,98 };
int length = sizeof(arr) / sizeof(arr[0]);

for (int i = 0; i < length - 1; i++)
{
for (int j = 0; j < length - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;

}
}
}

for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}

return 0;
}

二维数组

判断二维数组行数和列数:

image-20231206192724601

** 打印二维数组 **

image-20231206192958569

多维数组

三维数组
1
数据类型 数组名[层][列][行]
image-20231206195845078
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
#include <stdio.h>
#pragma warning(disable:6328)

int main()
{
int arr[2][3][4] = { // 一个数组
{ // 第一层
{1,2,3,4},
{2,3,4,5},
{3,4,5,6}
},
{ // 第二层
{8,7,6,5},
{7,6,5,4},
{6,5,4,3}
}
};

printf("三维数组大小:%d\n", sizeof(arr)); //96
printf("三维数组每层大小:%d\n", sizeof(arr[0])); //48
printf("三维数组每层中每行大小:%d\n", sizeof(arr[0][0])); //16
printf("三维数组每层每行中每个元素的大小:%d\n", sizeof(arr[0][0][0])); //4
putchar('\n');
printf("层数:%d\n", sizeof(arr) / sizeof(arr[0]));
printf("行数:%d\n", sizeof(arr[0]) / sizeof(arr[0][0]));
printf("列数:%d\n", sizeof(arr[0][0]) / sizeof(arr[0][0][0]);;

return 0;
}

字符数组

定义字符数组:

1
char 数组名[元素个数] = {'h','e','l','l','o'};
image-20231206203151661

字符串最后一个字符为 \0,所以有6位。

字符串拼接

image-20231206211934907
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
#include <stdio.h>
#pragma warning(disable:6328)

int main()
{
char ch1[] = "hello";
char ch2[] = "world";

char ch3[20];
int i = 0;
int j = 0;

while (ch1[i] != '\0')
{
ch3[i] = ch1[i];
i++;
}

while (ch2[j] != '\0')
{
ch3[i + j] = ch2[j];
j++;
}

ch3[i + j] = 0;

printf("%s\n", ch3);
return 0;
}

字符串

字符串与字符的区别

C语言中没有字符串这种数据类型,可以通过char的数组来替代:
字符串一定是一个char的数组,但char的数组未必是字符串;
数字0(和字符’\0’等价)结尾的char数组就是一个字符串,但如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char的数组。

字符串的输入与输出
gets()

1.功能:从标输入读入字符,并保到指定的内存间,直到出现换行符或读到文件结尾为止。

2.gets(str)与scanf(“%s”,str)的区别:
gets(str)允许输入的字符串含有空格
scanf(“%s”,str)不允许含有空格

但是scanf()可以通过正则表达式输入带空格的字符串:

1
scanf("%[^\n]",  ch)

意思是接收非回车以外的所有数值。

注意:由于scanf()和gets()无法知道字符串s大小,必须遇到换行符或读到文件结尾为止才接收输入,因此容易导致字篮颗组画界(缓冲区溢出)的情况

fgets()

1.功能:从stream指的文件内读入字符,保存轾到所指定的内存空间,直到出现换行字符、读到文件结尾或已读了size-1个字符为止,最后会自动加上字符’\0’作为序符束。

可以接受空格。

2.参数:

​ s:字符串
​ size:指定最大读取字符串的长度(size - 1)
​ stream:文件指针,如果读键盘输入的字符串,固定写为stdin

1
2
3
4
5
6
7
int main()
{
char ch[10];
fgets(ch, sizeof(ch), stdin);
printf("%s", ch);
return 0;
}
puts()

功能:标准设备输出s字符串,在输出完成后自动输出一个’\n’

fputs()

1.功能:将str所指定的字符串写入到stream指定的文件中,字符串结束符’\0’ 不写入文件。

2.参数:

​ str:字符串
​ stream:文件指针,如果把字符串输出到屏幕,固定写为stdout

1
2
3
4
5
6
int main()
{
char ch[] = "hello world";
fputs(ch, stdout); //等价于printf("%s", ch)
return 0;
}
strlen()

1.功能:计算字符串长度(有效长度),计算的是第一个\0之前的长度,不包含\0

2.返回值:返回值为unsigned int类型。

image-20231206224553253 image-20231206224718177

​ ↑↑↑↑↑↑↑ 12,因为还有一个\0

使用strlen()函数要导入头文件 #include <string.h>

自己实现字符串长度计算

image-20231206225332152

strlen()

image-20231206225420024

七、函数

函数调用:产生随机数

当调用函数时,需要关心5要素:

头文件:包含指定的头文件
函数名字:函数名字必须和头文件声明的名字一样
功能:需要知道此函数能干啥后才调用。
参数:参数类型要匹配
返回值:根据需要接收返回值。

1
2
3
4
5
6
#include <time.h>
time_t time(time_t*t);

功能:获取当前系统时间
参数:常设置为NULL
返回值:当前系统时间time_t相当于long类型,单位为毫秒
1
2
3
4
5
6
#include <stdlib.h>
void srand(unsigned int seed);

功能:用来设置rand()产生随机数时的随机种子
参数:如果每次设置的seed相等,rand()产生随机数相等
返回值:无
1
2
3
4
5
6
#include <stdlib.h>
int rand(void);

功能:返回一个随机数值
参数:无
返回值:随机数

直接产生的随机数

image-20231206231811470
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <time.h>

int main()
{
// 添加随机数种子
srand((size_t)time(NULL));

for (int i = 0; i < 20; i++)
{
printf("%d\n", rand() % 100); // 产生0~99之间的随机数
}

return 0;
}

生成特定范围的随机数

image-20231207135553294 image-20231207135640611

双色球案例:注意去重

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
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <time.h>
#define LENGTH 6

int main()
{
int arr[LENGTH] = { 0 };
int length = sizeof(arr) / sizeof(arr[0]);

// 添加随机数种子
srand((size_t)time(NULL));
for (int i = 0; i < length; i++)
{
int flag = 0;
int value = rand() % 32 + 1;
arr[i] = value;

for (int j = 0; j < length; j++)
{
if (value == arr[j])
{
flag++;
}
}
if (flag != 1)
{
i--;
}
}

int blue = rand() %16 + 1;

for (int i = 0; i < length; i++)
{
printf("%d ", arr[i]);
}
printf("+ %d\n", blue);

return 0;
}

1.函数的定义与使用

函数的定义
1
2
3
4
5
返回值类型 函数名(参数列表)
{
代码体;
return 返回值;
}

定义静态函数static与非静态函数,静态函数与非静态函数的区别

  1. 可见性:静态函数只能在当前文件内使用,不能被其他文件调用,而非静态函数可以被当前文件以外的其他文件调用,具有全局可见性
  2. 生命周期:静态函数在程序运行期间一直存在,不会被释放,而非静态函数则是在被调用时动态地创建并在函数返回时被释放,拥有较短的生命周期。
  3. 内存分配:非静态函数调用时从堆栈中动态分配内存空间,函数返回时空间被释放,而静态函数分配内存空间仅一次,且在程序运行期间一直存在,调用时并不会重新分配内存空间。
  4. 其他:静态函数不能被其他文件调用,因此可以被认为是一种信息隐藏方法,能够有效地维护代码的安全性。同时,在一些对性能要求较高的场景,静态函数相比非静态函数性能更好,因为静态函数的调用时间更短。

函数实例:判断字符串是否相等

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
30
31
32
33
34
35
36
37
38
#include <stdio.h>


int main()
{
char ch1[] = "hallo";
char ch2[] = "hello";
int value = strcompair(ch1, ch2);

if (value == 0)
{
printf("两个字符串相同");

}
else
{
printf("两字符串不相同\t%d\n", value);
}

return 0;
}

int strcompair(char ch1[], char ch2[])
{
int i = 0;
while (ch1[i] == ch2[i])
{
// 判断是否到了字符串结尾
if (ch1[i] == '\0')
{
return 0;
}
//没到结尾继续判断下一位
i++;
}
return ch1[i] > ch2[i] ? 1 : -1;
}

2.函数样式

1.无参函数

1
2
3
4
int function()
{

}

2.有参函数

1
2
3
4
int function(int a,int b)
{

}

3.函数的声明

如果函数定义在主函数之后,则函数需要在主函数之前声明。

4.main函数和exit函数

exit()函数:结束程序运行

在主函数中,main(),使用return和exit基本没有区别,都会结束程序运行

在函数中,使用return会返回返回值,使用exit()也会停止整个程序

例如:一个程序中有一个加载图片的函数,图片加载不了程序不能正常运行,则此时使用exit()函数使程序终止。

八、多文件编程

·可以再头文件中声明函数,在主函数中只导入头文件即可,不用一直声明函数。

image-20231207213854267

自定义函数文件和自定义头文件可以文件名相同,可以方便捋顺程序

自定义的头文件中要写,防止main函数 头文件包含

1
#pragma once

如果头文件包含则会报错:

image-20231207215038085

为了避免同一个文件被include多次,C/C++中有两种方式,一种是#ifndef方式,一种是pragma once方式

#pragma once:

1
2
#pragma once
//声明语句

#ifndef

1
2
3
4
5
6
#indef __SOMEFILE_H__
#define __SOMEFILE_H__

// 声明语句

#endif

九、指针

1.定义指针

1
2
int a = 10;
int* p;

定义指针时,想存储什么类型的数据就要定义什么类型的指针。

通过指针赋值:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void)
{
int a = 0;
int* p = &a;
*p = 100;

printf("%d\n", a);

}
运算符
* 取值运算符
& 取地址运算符

可以通过 &a 取出 a 的地址赋值给指针p,也可以通过 * p 取出指针p所指向的变量存储的值

指针所占内存大小:

image-20231207224226442 image-20231207224336967

在32位操作系统中,指针大小为4字节 在64位操作系统中,指针大小为8字节

2.野指针和空指针

野指针
image-20231207230337071

所以,存在野指针不会出现错误,但是操作或访问野指针时可能会出错。

不建议把一个变量的值直接赋值给指针。

空指针

空指针是指内存地址编号为0的空间

1
int* p = NULL;

空指针也不能访问或操作

应用:空指针可以用作条件判断

1
2
3
4
if (p==NULL)
{

}
万能指针 void*

所占字节大小

在32为操作系统下占4个字节,在64为操作系统下占8个字节

万能指针可以接受任意类型变量的内存地址

但是通过万能指针操作(访问、修改)所指变量时,必须找到变量对应的指针类型,如:

image-20231207232205725

必须将指针p强制转换为int类型指针:

image-20231207232316993
const修饰的指针类型
image-20231207233230201 通过指针修改了const修饰的常量
1.const修饰指针类型

可以修改指针变量的值,不可以修改指针指向内存空进的值。(const离谁近就不能改谁

2.const修饰指针变量

可以修改指针指向内存空间的值

3.const同时修饰指针类型和指针变量(只读指针)

当const同时修饰指针类型和指针变量时,可以使用二级指针对一级指针所指的变量进行修改。

image-20231207235602859

3.指针与数组

指针与数组(p与arr)的区别:

image-20231208091528010

数组arr所指的地址不仅有地址,还包含数组的长度,使用指针p接收arr的地址,p只是一个指针变量,不具有整个数组的长度。

当数组作为函数参数会退化为指针。变成指针后,一个指针变量的大小为4字节(32位),丢失了数组的元素个数

image-20231208092320722

4.指针的加减运算

1.加法运算

指针计算不是简单得整数相加,

如果是一个int *,+1的结果是增加一个int的大小;

如果是一个char*,+1的结果是增加一个char大小。

通过指针运算,计算字符串长度。

image-20231208094010583

复制字符串

image-20231208095300785
2.减法运算

C 汇编

1.参数与返回值

函数定义

1
2
3
4
返回类型 函数名(参数列表)
{
return;
}

返回类型和参数列表的参数类型,如int(4byte), short(2byte),char(1byte)等用来说明数据宽度是多大

函数返回值

image-20231228141753125 image-20231228141919911

函数返回后使用eax存储,函数ret后,eax存储的返回值被压到堆栈中。

2.变量

全局变量

1
2
3
4
5
6
7
#include <stdio.h>

int x;
int main(void)
{
x = 123456;
}

1)编译的时候就已经确定了内存地址和宽度,变量名就是内存地址的别名。

2)如果不重写编译,全局变量的内存地址不变。游戏外挂中的找“基址”,其实就是找全局变量。

3)全局变量中的值任何程序都可以改,是公用的。

局部变量

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int function()
{
int x = 123456;
return x;
}

int main(void)
{
funtion();
}

1)局部变量是函数内部申请的,如果函数没有执行,那么局部变量没有内存空间。

2)局部变量的内存是在堆栈中分配的,程序执行时才分配。我们无法预知程序何时执行,这也就意味着,我们无法确定局部变量的内存地址。

3)因为局部变量地址内存是不确定的,所以,局部变量只能在函数内部使用,其他函数不能使用。

函数调用的内存布局

image-20231228202155115

函数嵌套调用的内存布局

变量类型

基本类型

整数类型

整数类型的宽度:char short int long

char 8BIT 1字节
short 16BIT 2字节
int 32BIT 4字节
long 32BIT 4字节

浮点类型

浮点类型的存储格式:float和double在存储方式上都是遵从IEEE编码规范的

image-20240102144902818

8.25转成浮点存储:整数部分

整数部分8转成2进制
8/2 = 4 0
4/2 = 2 0
2/2 = 1 0
1/2 = 0 1
从下往上读,所以8转成2进制是1000

9转2进制:
9/2 = 4 1
4/2 = 2 0
2/2 = 1 0
1/2 = 0 1
所以9转2进制是1001

所以,所有的整数部分一定能转成2进制

8.25转2进制:小数部分

小数部分转2进制用乘法

0.25转2进制:
0.25 * 2 = 0.5 0
0.5 * 2 = 1.0 1

当小数部分乘出0时停止
从上往下读:0.25转成2进制是01

如小数部分0.4转成2进制:
0.4 * 2 = 0.8 0
0.8 * 2 = 1.6 1
0.6 * 2 = 1.2 1
0.2 * 2 = 0.4 0
……

会发现当0.4转成2进制的时候,小数部分永远得不到0,所以用二进制描述小数,不可能做到完全精确

使用IEEE规则存储小数8.25 -> 1000.01 -> 1.00001 * 2(10)^3

第一位是符号位,整数填0负数填1
尾数部分00001从前往后填,float类型尾数23位,所以尾数是00001000000000000000000
指数部分:当使用科学计数法时,小数点向左移,指数的最高位填1,向右移时填0

因为是3次方,所以3-1=2,将2转换为2进制,2 => 10

所以指数最后填10

所以8.25的2进制存储格式为 0 10000010 00001000000000000000000

这个数用16进制表示为 -> 0x41040000

image-20240102153351929

if, if…else

if语句在编译器编译后,汇编语句比较灵活

image-20240104195701535

image-20240104200120721

当条件为a > b时,汇编指令实际上做的是判断a <= b:当a > b时,JLE指令不执行,直接按步执行;当a <= b时,执行JLE指令,跳过if中的执行语句。

当条件较简单时,编译器也会将汇编编译成TEST。

if else

image-20240104200606048

当if中判断不满足条件时,会跳转到else语句内部,当满足条件时,汇编语句会按步向下执行,在else上面的JMP跳过else的执行语句。

所以当判断语句是if…else时,一般中间会有一个JMP指令,用来跳过else执行语句。

switch

一般的switch

一般来说,使用switch分支语句比if的执行效率要高。

image-20240104230139273 image-20240104230208618

当情况较少时,使用if和switch的分支语句的执行效率差不多

但当情况较多时,使用switch的执行效率较高:

image-20240104230606347

发现switch的跳转语句只有上面一点

1
2
3
4
5
6
7
8
9
00FA18A8	 8B 45 F8             	mov         eax,dword ptr [a]  
00FA18AB 89 85 30 FF FF FF mov dword ptr [ebp-0D0h],eax
00FA18B1 8B 8D 30 FF FF FF mov ecx,dword ptr [ebp-0D0h]
00FA18B7 83 E9 01 sub ecx,1
00FA18BA 89 8D 30 FF FF FF mov dword ptr [ebp-0D0h],ecx
00FA18C0 83 BD 30 FF FF FF 05 cmp dword ptr [ebp-0D0h],5
00FA18C7 77 67 ja $LN15+0Fh (0FA1930h)
00FA18C9 8B 95 30 FF FF FF mov edx,dword ptr [ebp-0D0h]
00FA18CF FF 24 95 54 19 FA 00 jmp dword ptr [edx*4+0FA1954h]

switch语句在程序启动时已经在内存中生成了一个“跳转表”,在上面的这段汇编指令中,这个跳转表的地址就是从最后一句的0x0FA1954开始的,所以转到内存中查看这个内存地址:

image-20240104231216347 image-20240104231939317

在内存地址中刚好存了这一段跳转表。

当跳转时,先有一句

1
00FA18B7	 83 E9 01             	sub         ecx,1

这里减的是switch中最小的那个数,因为原码中写的switch条件是:

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
int a = 2;
switch (a)
{
case 1:
printf("1111\n");
break;
case 2:
printf("2222\n");
break;
case 3:
printf("3333\n");
break;
case 4:
printf("4444\n");
break;
case 5:
printf("5555\n");
break;
case 6:
printf("6666\n");
break;
default:
printf("default\n");
break;
}

所以最后减完的值会放到EDX中,使用EDX偏移计算要跳转的地址

switch运行速度快的第一点是:判断switch_on与最大的值的大小,当switch_on的值比最大的case还要大时,直接跳出switch

1
2
00FA18C0	 83 BD 30 FF FF FF 05 	cmp         dword ptr [ebp-0D0h],5  
00FA18C7 77 67 ja $LN15+0Fh (0FA1930h)

计算完SUB之后,将计算完的值存到EDX寄存器,然后根据EDX计算跳转地址偏移。

当switch之中的值不是连续的

image-20240104233957452

c原码:

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
int a = 2;
switch (a)
{
case 1:
printf("1111\n");
break;
case 2:
printf("2222\n");
break;
case 3:
printf("3333\n");
break;
case 8:
printf("4444\n");
break;
case 9:
printf("5555\n");
break;
case 10:
printf("6666\n");
break;
default:
printf("default\n");
break;
}

当case的值没有规律时

当case的值不连续且没有啥规律时,再多的case也可能跟if一样,每一个跳转都会有一个JMP

当原码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
switch (a)
{
case 1:
printf("1111\n");
break;
case 2:
printf("2222\n");
break;
case 3:
printf("3333\n");
break;
case 998:
printf("4444\n");
break;
case 999:
printf("5555\n");
break;
case 1000:
printf("6666\n");
break;
default:
printf("default\n");
break;
}

发现case的值有规律,但是从3到998差了很多,因此用EDX比例因子查内存地址的话,中间要差几百个default的地址,所以在这种情况下也不会使用EDX比例因子找,只会生成一堆JCC跳转指令。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
006A5438 8B 45 F8             mov         eax,dword ptr [a]  
006A543B 89 85 30 FF FF FF mov dword ptr [ebp-0D0h],eax
006A5441 81 BD 30 FF FF FF E6 03 00 00 cmp dword ptr [ebp-0D0h],3E6h
006A544B 7F 29 jg __$EncStackInitStart+0AAh (06A5476h)
006A544D 81 BD 30 FF FF FF E6 03 00 00 cmp dword ptr [ebp-0D0h],3E6h
006A5457 74 64 je __$EncStackInitStart+0F1h (06A54BDh)
006A5459 83 BD 30 FF FF FF 01 cmp dword ptr [ebp-0D0h],1
006A5460 74 2E je __$EncStackInitStart+0C4h (06A5490h)
006A5462 83 BD 30 FF FF FF 02 cmp dword ptr [ebp-0D0h],2
006A5469 74 34 je __$EncStackInitStart+0D3h (06A549Fh)
006A546B 83 BD 30 FF FF FF 03 cmp dword ptr [ebp-0D0h],3
006A5472 74 3A je __$EncStackInitStart+0E2h (06A54AEh)
006A5474 EB 74 jmp __$EncStackInitStart+11Eh (06A54EAh)
006A5476 81 BD 30 FF FF FF E7 03 00 00 cmp dword ptr [ebp-0D0h],3E7h
006A5480 74 4A je __$EncStackInitStart+100h (06A54CCh)
006A5482 81 BD 30 FF FF FF E8 03 00 00 cmp dword ptr [ebp-0D0h],3E8h
006A548C 74 4D je __$EncStackInitStart+10Fh (06A54DBh)
006A548E EB 5A jmp __$EncStackInitStart+11Eh (06A54EAh)

// 在这上面生成了好多JCC指令用来跳转

006A5490 68 EC 7C 6A 00 push offset string "1111\n" (06A7CECh)
006A5495 E8 38 BC FF FF call _printf (06A10D2h)
006A549A 83 C4 04 add esp,4
006A549D EB 58 jmp __$EncStackInitStart+12Bh (06A54F7h)
006A549F 68 F4 7C 6A 00 push offset string "2222\n" (06A7CF4h)
006A54A4 E8 29 BC FF FF call _printf (06A10D2h)
006A54A9 83 C4 04 add esp,4
006A54AC EB 49 jmp __$EncStackInitStart+12Bh (06A54F7h)
006A54AE 68 20 7E 6A 00 push offset string "3333\n" (06A7E20h)
006A54B3 E8 1A BC FF FF call _printf (06A10D2h)
006A54B8 83 C4 04 add esp,4
006A54BB EB 3A jmp __$EncStackInitStart+12Bh (06A54F7h)
006A54BD 68 28 7E 6A 00 push offset string "4444\n" (06A7E28h)
006A54C2 E8 0B BC FF FF call _printf (06A10D2h)
006A54C7 83 C4 04 add esp,4
006A54CA EB 2B jmp __$EncStackInitStart+12Bh (06A54F7h)
006A54CC 68 30 7E 6A 00 push offset string "5555\n" (06A7E30h)
006A54D1 E8 FC BB FF FF call _printf (06A10D2h)
006A54D6 83 C4 04 add esp,4
006A54D9 EB 1C jmp __$EncStackInitStart+12Bh (06A54F7h)
006A54DB 68 38 7E 6A 00 push offset string "6666\n" (06A7E38h)
006A54E0 E8 ED BB FF FF call _printf (06A10D2h)
006A54E5 83 C4 04 add esp,4
006A54E8 EB 0D jmp __$EncStackInitStart+12Bh (06A54F7h)
006A54EA 68 E0 7C 6A 00 push offset string "default\n" (06A7CE0h)
006A54EF E8 DE BB FF FF call _printf (06A10D2h)
006A54F4 83 C4 04 add esp,4

因此当使用switch时,连续的case会提高代码的执行速度

do…while

do…while一般用于先执行后判断

for

for循环在反汇编中的形式

image-20240108191715316

for循环的执行效率可能不是特别高,但是可读性比较好,比较容易写

for循环的第一个和第三个表达式可以使用逗号连接多个表达式,比如:

1
2
3
4
5
6
7
int i;
int j;
int k;
for(i=0,j=0,k=0; i<=10, i++,j++,k++)
{
print("%d %d %d\n", i, j, k);
}

结构体数组

结构体数组的定义

定义结构体

类型 变量名[常量表达式]

定义结构题类型
struct stStudent
{
int Age;
int Level;
}

定义结构体变量
struct stStudent st;

定义结构题数组
struct stStudent arr[10] 或者 stStudent arr[10]

结构体数组的赋值

1
2
3
4
struct MyStruct arr[3] = { {0,0},{1,1},{2,2} };

arr[0].Age = 10;
arr[0].Level = 100;
image-20240227084341723

结构体数组中字符串成员的处理

image-20240227085527133

指针类型

任何类型都可以带上*,加上*以后是新的类型,统称为“指针类型”。

*可以是任意多个。

1
2
3
4
5
6
char* x;
short* y;
int* z;
float* f;
double* d;
Student* st;(结构体)

指针变量的赋值

image-20240227092130958 image-20240227092215037

指针变量宽度

指针类型的变量宽度永远是4字节、无论指针类型是什么,无论是几级指针

指针变量的自加自减

1、不带*类型的变量,++或者–都是加1或者减1
2、带*类型的变量,++或者–新增(减少)的数量是去掉一个*后变量的宽度

也就是当指针变量是一级指针时,指针自加后,char类型会加1,short类型会加2.int类型会加4

当指针变量是二级以上时,指针自加后,都会加4,因为这时去掉一个*后,宽度都是指针的宽度4.

指针的加减运算

指针不能进行乘除运算,只能加减。

当指针时多级指针时:

image-20240227094643479

当指针是一级指针时

image-20240227094725042

总结:

image-20240227094928263

指针取值方式

1
2
3
4
5
(p+i)=p[i]
*(*(p+i)+k)=p[i][k]
*(*(*(p+i)+k)+m)=p[i][k][m]
*(*(*(*(*(p+i)+k)+m)+w)+t)=p[i][k][m][W][t]
*()与[]可以相互转换

数组

数组参数传递

在反汇编中,arr[i]等价于*(p+i),使用函数传递数组参数时,传递的是数组的首地址,并且传送时不会传递数组的宽度。要使用数组的宽度时,必须要把数组的宽度也传递进去。

在函数中使用数组,如果是按照指针传递的数组,在使用时也可以按照数组来使用;如果是按照数组传递的参数,也可以按照指针的方式来使用。

字符串

字符串的定义方式

1
2
char str[5] = {'a','b','c','d','\0'}
printf("%s\n", str);

程序会找到'\0'或者0来停止寻找字符串

1
char str[] = "ABCDEF";		// 在常量区找一段空间放,然后在复制到数组中,因为复制到数组中才可以对其进行修改,比如str[0]=‘K',然后就变成了"KBCDE"
1
char* str = "ABCDE"

这种方式是将字符串放在了常量区,但是没有复制到字符数组中,所以这样定义的字符串只能读,不能写(也有可以改的方法)

处理字符串函数

1.int strlrn(char * str)

返回值是字符串s的长度。不包括结束符’\0’。、

2.char * strcpy(char * dest, char * src);

复制字符串src到dest中,返回值为dest的值。

3.char * strcat(char * dest, char * src);

将字符串src添加到dest尾部,返回值为dest的值。

4.int strcmp(char * str1, char * str2);

一样返回0,不一样返回非0

结构体指针

首先定义一个结构体

1
2
3
4
5
struct Point
{
int a;
int b;
}

给结构体赋值、定义结构体指针

1
2
Point p = { 1, 2 };
Point* px = &p;

通过指针读取结构体时不用.,而是用->

1
2
int x = px->x		| x = 1
int y = px->y | y = 2

通过结构体指针修改结构体成员数值

1
px->y = 100			| y = 100

指针数组 与 数组指针

指针数组的赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char * a = "Hello";
char * b = "张三";

<1>
char * arr[2] = {a, b};

<2>
char * arr[2];
arr[0] = 'a';
arr[1] = 'b';

<3>
char * arr[2] = {
"Hello",
"张三"
}

数组指针的定义

1
2
3
4
5
6
int(*px) [5];			//一维数组指针
char(*px) [3];

int(*px)[2][2]; //二维数组指针

char(*px)[3][3][3]; //三维数组指针

int* p[5]int (*p)[5]的区别:

int* p[5][]的优先级高于*,所以int* p[]是一个(int*)类型的指针数组,p本质上是一个数组

int (*p)[5]()的优先级高于[],所以p先与*结合,是一个指针,指针的类型是一个int[]数组,int (*p)[]是一个数组指针,p本质上是一个指针

???

image-20240228003021496

image-20240228004935994

调用约定

调用约定 参数压栈顺序 平衡堆栈方式
__cdecl 从右至左入栈 调用者清理栈
__stdcall 从右至左入栈 自身清理堆栈
__fastcall ECX/EDX传递前两个参数,剩下还多的,从右至左入栈 自身清理堆栈

函数指针

定义函数指针变量:

1
int (__cdecl *pFun)(int, int);

为指针变量赋值:

1
pFun = (int (__cdecl *)(int, int))10;		// 这里随便赋了一个10,使用时应该赋一个函数的地址

使用函数指针变量:

1
int r = pFun(1, 2);