目录
一、CPU寄存器
CPU(Central Processing Unit,中央处理单元)的寄存器是CPU内部的一组小型、高速的存储器单元,用于存储和处理数据。这些寄存器在CPU内部用于执行计算和操作,是计算机的核心组成部分。不同的CPU架构可能会有不同的寄存器集合,但通常包括以下一些常见的寄存器类型:
1.通用寄存器
- 通用寄存器用于存储临时数据和执行各种运算。它们可以用于存储整数、内存地址和其他数据。
- 常见的通用寄存器包括:EAX、EBX、ECX、EDX(在x86架构中,通常每个寄存器32位,4个字节)和R0、R1、R2、R3(在ARM架构中)。
2.堆栈寄存器
- ESP 是堆栈指针寄存器,用于指示当前堆栈的顶部位置。
- 它在函数调用和堆栈操作中非常重要,用于推入和弹出数据以及访问局部变量和参数。
- 当数据被推入堆栈时,ESP 减小;当数据从堆栈弹出时,ESP 增加。
3.基址寄存器
- EBP 是基址指针寄存器,通常用于访问函数的局部变量和参数。
- 它在函数内部创建一个固定的引用点,以便访问函数的参数和局部变量,而不受堆栈操作的影响。
- EBP 的值在函数开始时通常被保存,然后在函数结束时还原,以确保函数内的访问局部数据的一致性。
4.指令指针寄存器
- EIP 是指令指针寄存器,用于存储当前正在执行的指令的地址。
- 当CPU执行指令时,它会不断递增 EIP,以执行下一条指令。
- EIP 在控制程序的执行流程中非常重要,用于确定要执行的下一条指令。
5.对于ESP和EBP的补充
①通俗解释
当涉及到 ESP(堆栈指针寄存器)和 EBP(基址指针寄存器)时,可以将它们比喻为一个大堆积木玩具,比如乐高积木。考虑下面的情景:
堆积木游戏中的ESP和EBP
你正在玩一个巨大的积木游戏,你需要建立一个高塔。你是一个聪明的建造者,想要确保建造塔楼时不会出错。
ESP(堆栈指针):
- 假设你手头上有一堆积木(代表堆栈),你需要用这些积木建造塔楼。
- ESP 是一个小标记,它总是指向你堆积木堆(堆栈)的最顶部。
- 当你需要在塔楼上添加新的积木时,你会从堆积木堆的顶部取一个积木(推入数据到堆栈),然后你会把 ESP 往下移动,指向新的顶部积木。这样,你总是知道哪个积木是最新添加的。
EBP(基址指针):
- 现在,你要在塔楼中间的某个高度上开始建造一个小房间,你不想让这个小房间的高度受到整个堆积木堆的变化影响。
- EBP 就像是一个小木块,你可以放在塔楼的某个高度上,然后把小房间的墙壁(局部变量和参数)建造在这个小木块上。EBP 帮助你不断记住这个特定高度,不管你在塔楼的顶部或底部添加积木。
所以,ESP 帮助你管理整个积木堆的顶部,而 EBP 帮助你在积木堆的特定高度上建造小房间,确保你的塔楼的不同部分可以互相独立而不会互相干扰。这就是 ESP 和 EBP 在堆栈和函数调用中的作用。在计算机程序中,它们有助于确保数据的正确传递和局部变量的访问。
②应用场景
场景一:函数调用
主要应用在函数调用中,在函数调用过程中,负责数据的传递和访问。
ESP:负责管理函数调用过程中的堆栈,负责将参数压入堆栈,并弹出函数返回后的结果。
EBP:用来建立一个固定的基地址,用来访问其参数和局部变量,不受堆栈的影响。
场景二:异常处理
在发生异常时,程序可能需要将堆栈状态还原到先前的状态。ESP 和 EBP 的值在这种情况下可以用于恢复堆栈。
6.函数返回值存储——寄存器
在 x86 架构中,函数的返回值通常存储在 EAX 寄存器中。 EAX 寄存器是一个通用寄存器,用于存储函数返回的整数值。这是函数返回值的标准寄存器。
如果函数的返回值不是整数或指针,而是浮点数,那么浮点返回值通常存储在浮点寄存器(例如 XMM0 或 ST0)中,具体规则取决于编译器和操作系统的调用约定。
①示例代码
int func(int x)
{
int a = x;
a += x;
return a;
}
int main()
{
int z = 0;
z = func(10);
printf("%d\n", z);
return 0;
}
函数调用函数值的返回,使用CPU寄存器中的EAX存储返回值的值,然后传递给主函数。上面的代码在函数结束之前将a=20返回值传给寄存器了,然后再给z。
②Q&A
返回值大小超出寄存器容量
Q:如果函数返回值的数据大小大于寄存器的存储容量,此时计算机会怎样处理返回数据?
A:会使用返回值溢出的方法进行执行。
当返回值数据大于寄存器存储容量时,计算机会从内存中分配一部分空间来存储返回值,并将该返回值在内存中的地址存储在堆栈上,这样当函数返回时,程序就知道从哪里进行执行了。
为了避免额外的开销,开发人员一般不会设置较大数据量的返回值。
PS:老师还提供了一个思路,就是使用结构体返回。
当函数返回结构体时,编译器会为结构体分配一块临时内存,然后将结构体的数据复制到这个内存中,然后返回该内存的地址,这个地址被存放在EAX寄存器中。
这个地址实际上是一个指向存储结构体的内存块的指针。(这个思路等同于重新开辟内存,并返回内存地址的方法。)
示例代码:
//定义结构体 struct Node { int s_id; int s_name; char s_sex; }; //返回值为结构体的函数 struct Node MyStruct(int id,int name,int sex) { struct Node p;//定义一个结构体,名称为p p.s_id = id; p.s_name = name; p.s_sex = sex; return p;//返回值 } int main() { struct Node m; m = MyStruct(1, 2, 'a'); printf("%d %d %c", m.s_id,m.s_name,m.s_sex); }
函数返回值能否是局部变量地址
Q:函数的返回值能否是局部变量的地址?
A:不能,函数一定不能返回局部变量地址,因为此地址在函数结束的时候被释放了。
就算你认为函数被释放时地址被存储在ESP寄存器中,函数结束后,原本的地址可能用作它用,会导致获得错误结果
错误的示例代码如下:
int* func(int x) { int a = x; a += x; return &a; } int main() { int z = 0; z = *func(10); printf("%d\n", z); return 0; }
运行该代码,程序报错,错误显示:不能够返回局部变量的地址。
修改方式是:可以将a设置成全局变量,或者可以给a加上静态关键字static,它就生存期变长,数据被存储在.data区。
只有在生存期不受函数影响的时候可以取地址。
二、断言
1.概念
C语言中的断言是一种用于在程序运行时检查条件是否满足的机制。
如果 assert()中止了程序, 它会显示失败的测试、 包含测试的文件名和行号。
C语言中的断言由<assert.h>
头文件提供支持。
#include <assert.h>
assert(expression);
断言表达式是bool表达式,为真向下执行,为假不执行,为假会终止程序,并且提示。
2.示例代码
#include<assert.h>
int main()
{
int x = -5;
assert(x > 0);
printf("x is positive.\n");
}
3.注意事项
断言只在debug版本起作用,release版本不起作用。
debug是调试版本,不会对代码进行优化。
release是交付版本,会对代码进行优化。
三、scanf和gets_s输入字符串区别
1.区别
scanf接受字符串的时候,以空格作为结束符。scanf无法输入空格,空格即结束。
gets_s可以接受空格字符 ,回车才认为接收字符串结束。
2.示例代码
scanf输入字符串
int main()
{
char str[100];
scanf("%s", &str);
printf("%s", str);
}
gets_s输入
int main()
{
char str[100];
gets_s(str, 100);
printf("%s", str);
}
四、switch
1.default语句
switch语句中,default这行代码无论在哪里都是最后执行。无论将它放在所有case前面还是放在所有case后面,它都会先执行case语句,最后执行default。当然default语句并不是必须的,可以不写。
2.case标签同时满足两个常量要求
Q:在下面代码中,case后面的标签为case 'A'|| 'a',可以执行吗?
A:不可以执行,因为这一部分被计算出来了,所以case后面的标签为0或1。
尽管它是一个常量,此时因为这个字符不符合所要求的字符标准,这行代码无法执行。
int main()
{
char ch;
scanf("%c", &ch);
switch (ch)
{
case 'A'||'a':printf("输出A"); break;
case 'B':printf("输出B"); break;
case 'C':printf("输出C"); break;
default:printf("啥也不是"); break;
}
}
此时输出:啥也不是
如果你想要一个case标签满足两个条件,正确写法如下:
int main()
{
char ch;
scanf("%c", &ch);
switch (ch)
{
case 'A':
case 'a':printf("输出A"); break;
case 'B':printf("输出B"); break;
case 'C':printf("输出C"); break;
default:printf("啥也不是"); break;
}
}
3.Q&A
Q:为什么switch()要求是整型变量表达式?
A:整数值在硬件层面易于处理,方便编译器优化,使switch语句方便快速跳转,避免在switch里面使用过于复杂的代码。
五、作业
1.编写代码计算输入字符串中单词的个数
要求:统计单词的个数的功能写成函数。
思路图
初始代码:
enum MyEnum
{
BEGIN,
WORD_IN,
WORD_OUT,
END
};
int main()
{
int i = 0;
int num = 0;
MyEnum state = BEGIN;
char str[100];
gets_s(str, 100);
while ((str[i])!='\0')
{
char ch = str[i];
switch (state)
{
case BEGIN:
if (isalpha(ch))
{
state = WORD_IN;
}
else
{
state = WORD_OUT;
}
break;
case WORD_IN:
if (!isalpha(ch))
{
state = WORD_OUT;
num++;
}
break;
case WORD_OUT:
if (isalpha(ch))
{
state = WORD_IN;
}
break;
default:
break;
}
i++;
}
printf("%d", num);
}
此时出现问题:单词输入是否有空格,会影响状态机跳转,导致少一次跳转。少空格会导致代码从word_in无法跳转到word_out,解决方法:判断循环结束后的状态是不是word_in,是的话就单独在加一次计数。
enum MyEnum
{
BEGIN,
WORD_IN,
WORD_OUT,
END
};
int main()
{
//上面省略while省略
//判断最后状态,处理最后一个单词
if (state == WORD_IN)
{
num++;
}
printf("%d", num);
}
此时出现的问题是:
①not's不认为是一个单词
②self-employed(自由职业)被认为是两个单词,但是其实是一个
解决方法:使用空格作为单词里和单词外的判断。
enum MyEnum
{
BEGIN,
WORD_IN,
WORD_OUT,
END
};
int main()
{
int i = 0;
int num = 0;
MyEnum state = BEGIN;
char str[100];
gets_s(str, 100);
while ((str[i])!='\0')
{
char ch = str[i];
switch (state)
{
case BEGIN:
if (ch!=' ')
{
state = WORD_IN;
}
else
{
state = WORD_OUT;
}
break;
case WORD_IN:
if (ch==' ')
{
state = WORD_OUT;
num++;
}
break;
case WORD_OUT:
if (isalpha(ch))
{
state = WORD_IN;
}
break;
default:
break;
}
i++;
}
if (state == WORD_IN)
{
num++;
}
printf("%d", num);
}
最后,要求把计数功能写在函数里面。
enum MyEnum
{
BEGIN,
WORD_IN,
WORD_OUT,
END
};
int Count_Word(char str[])
{
int i = 0;
int num = 0;
MyEnum state = BEGIN;
while ((str[i]) != '\0')
{
char ch = str[i];
switch (state)
{
case BEGIN:
if (ch != ' ')
{
state = WORD_IN;
}
else
{
state = WORD_OUT;
}
break;
case WORD_IN:
if (ch == ' ')
{
state = WORD_OUT;
num++;
}
break;
case WORD_OUT:
if (!isspace(ch))
{
state = WORD_IN;
}
break;
default:
break;
}
i++;
}
if (state == WORD_IN)
{
num++;
}
return num;
}
int main()
{
int number = 0;
char string[100];
gets_s(string, 100);
number = Count_Word(string);
printf("%d", number);
}
所以以上代码就是最终的实现代码。
2.读取cpp文件中单词的个数
编写代码实现以下功能:
①打开文件,使用状态机读取文件中每行单词,统计单词的个数
②计算重复单词的个数
需要注意的问题如下:一行的概念:\r\n
初始代码
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<math.h>
#include<ctype.h>
enum MyEnum
{
BEGIN,
WORD_IN,
WORD_OUT,
END
};
int Count_Word(char str[])
{
int i = 0;
int num = 0;
MyEnum state = BEGIN;
while ((str[i]) != '\n')
{
char ch = str[i];
switch (state)
{
case BEGIN:
if (!isspace(ch))
{
state = WORD_IN;
}
else
{
state = WORD_OUT;
}
break;
case WORD_IN:
if (isspace(ch))
{
state = WORD_OUT;
num++;
}
break;
case WORD_OUT:
if (!isspace(ch))
{
state = WORD_IN;
}
break;
default:
break;
}
i++;
}
if (state == WORD_IN)
{
num++;
}
return num;
}
int main()
{
FILE* fp = fopen("E:\\图论\\lizeyu.txt","r");
char line[200];//读取存取的字符
int number = 0;
int totalnum = 0;
if (fp != NULL)
{
while ((fgets(line, sizeof(line), fp)) != NULL)
{
number = 0;
number = Count_Word(line);
totalnum += number;
}
printf("%d", totalnum);
fclose(fp);
}
else
{
printf("当前文件为空文件\n");
return 1;
}
return 0;
}
程序一直弹出弹框,中止操作
不知道哪里出了问题很讨厌,一直找不出来。老师说的统计文件中单词出现次数的方法我也不会欸,待更新。
小技巧
如果一行代码写不下 ,在引号后面直接回车