C语言入门——第九课

目录

一、CPU寄存器

1.通用寄存器

2.堆栈寄存器

3.基址寄存器

4.指令指针寄存器

5.对于ESP和EBP的补充

①通俗解释

②应用场景

6.函数返回值存储——寄存器

①示例代码

②Q&A

返回值大小超出寄存器容量

函数返回值能否是局部变量地址

二、断言

1.概念

2.示例代码

3.注意事项

三、scanf和gets_s输入字符串区别

1.区别

2.示例代码

四、switch

1.default语句

2.case标签同时满足两个常量要求

3.Q&A

五、作业

        1.编写代码计算输入字符串中单词的个数

2.读取cpp文件中单词的个数


一、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

你正在玩一个巨大的积木游戏,你需要建立一个高塔。你是一个聪明的建造者,想要确保建造塔楼时不会出错。

  1. ESP(堆栈指针)

    • 假设你手头上有一堆积木(代表堆栈),你需要用这些积木建造塔楼。
    • ESP 是一个小标记,它总是指向你堆积木堆(堆栈)的最顶部。
    • 当你需要在塔楼上添加新的积木时,你会从堆积木堆的顶部取一个积木(推入数据到堆栈),然后你会把 ESP 往下移动,指向新的顶部积木。这样,你总是知道哪个积木是最新添加的。
  2. 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;
}

程序一直弹出弹框,中止操作

不知道哪里出了问题很讨厌,一直找不出来。老师说的统计文件中单词出现次数的方法我也不会欸,待更新。

小技巧

如果一行代码写不下 ,在引号后面直接回车

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值