“深度学习“C语言中的32个关键字

目录

学习C语言必须要知道的小秘密 !

变量的定义与使用

第一个关键字auto-修饰局部变量

 第二个关键字register关键字-最快关键

第三个关键字static-静态变量

浅谈extern关键字-声明

static修饰全局变量

static修饰局部变量

static修饰局部变量未显示初始化

第四个sizeof关键字

变量命名规则

整型存储

有符号正数

16进制表示方法

有符号负数

无符号数

补码如何转原码?

第五个关键字unsigned(无符号整型)

无符号整除存与取的过程

第六个关键字signed(有符号整型)

有符号负数被singned修饰存与取

数据截断

当发生数据截断如何存数据?

当发生数据截断如何取数据?

有符号整数(通常默认为 signed)

无符号整数(unsigned)

第七个关键字if else(条件判断语句)

C语言中有bool(布尔)类型吗?

Bool类型用法推荐 

if判断浮点数

指针变量和零值比较

if与else匹配问题

第八个关键字switch case(多条件匹配)

如何使用switch和if

switch有什么作用呢?

default语句

case语句

多条case执行同一条语句

default语法上是不是只能放在最后?

switch如何实现多组输入?

case可以用宏定义常量

​编辑

case语句排列顺序

第九个关键 - while循环

while循环语法结构:

while循环死循环实现

第十个关键字 - for循环

for循环语法结构:

for循环实现死循环

第十一个关键字 - do while循环

什么场景下要使用do while循环?

do while实现死循环

什么是默认三流?

getchar函数获取字符 

getchar使用细节1:

getchar使用细节2:

getchar使用细节3:

第十二个关键字break-跳出循环

第十三个关键字-continue(结束当前循环)

while中出现continue

do while中出现continue

for循环中出现continue

for循环“半闭半开”写法

第十四关键字goto(跳转)

goto语句向下跳转

goto语句向上跳转

第十五关键字void(空)

void可以修饰变量吗?void大小是多少?

void最大的价值是什么?

强制类型转换为void

void修饰函数返回值和参数

void 作为函数参数

void指针        

void * 定义的指针变量可以进行运算操作吗

void*指针可以直接解引用吗?

C语言有没有字符串类型?

第十六个关键字return

计算机中释放空间是将数据全部清0吗?

函数的栈帧是什么?

函数栈帧被释放

函数栈帧被覆盖

函数调用时如何形成足够的栈帧空间?

为什么临时变量具有临时性?

reruen关键字的最终奥秘

1.函数返回值的基本概念

2.返回值如何传递?

3.临时变量的生命周期

4.返回值没有接收时的行为

5. 汇编代码的例子

6. 接收返回值的情况

 esp eax mov call ret这些是什么?

第十七个关键字const

const修饰的变量,不可以“直接”被修改

const可以放在类型之前,也可以放在类型之后

coust修改的变量可以间接被修改

那count修饰变量,有什么意义?

字符串常量真正意义上不可被修改

 数组的长度可以用const修饰的变量表示吗?

const修饰数组

cinst修饰指针

第一种情况:const int* p

第二种情况: int const * p

第三种情况:int* const p

第四种情况:const int* const p


目录

学习C语言必须要知道的小秘密 !

变量的定义与使用

第一个关键字auto-修饰局部变量

 第二个关键字register关键字-最快关键

第三个关键字static-静态变量

浅谈extern关键字-声明

static修饰全局变量

static修饰局部变量

static修饰局部变量未显示初始化

第四个sizeof关键字

变量命名规则

整型存储

有符号正数

16进制表示方法

有符号负数

无符号数

补码如何转原码?

第五个关键字unsigned(无符号整型)

无符号整除存与取的过程

第六个关键字signed(有符号整型)

有符号负数被singned修饰存与取

数据截断

当发生数据截断如何存数据?

当发生数据截断如何取数据?

有符号整数(通常默认为 signed)

无符号整数(unsigned)

第七个关键字if else(条件判断语句)

C语言中有bool(布尔)类型吗?

Bool类型用法推荐 

if判断浮点数

指针变量和零值比较

if与else匹配问题

第八个关键字switch case(多条件匹配)

如何使用switch和if

switch有什么作用呢?

default语句

case语句

多条case执行同一条语句

default语法上是不是只能放在最后?

switch如何实现多组输入?

case可以用宏定义常量

​编辑

case语句排列顺序

第九个关键 - while循环

while循环语法结构:

while循环死循环实现

第十个关键字 - for循环

for循环语法结构:

for循环实现死循环

第十一个关键字 - do while循环

什么场景下要使用do while循环?

do while实现死循环

什么是默认三流?

getchar函数获取字符 

getchar使用细节1:

getchar使用细节2:

getchar使用细节3:

第十二个关键字break-跳出循环

第十三个关键字-continue(结束当前循环)

while中出现continue

do while中出现continue

for循环中出现continue

for循环“半闭半开”写法

第十四关键字goto(跳转)

goto语句向下跳转

goto语句向上跳转

第十五关键字void(空)

void可以修饰变量吗?void大小是多少?

void最大的价值是什么?

强制类型转换为void

void修饰函数返回值和参数

void 作为函数参数

void指针        

void * 定义的指针变量可以进行运算操作吗

void*指针可以直接解引用吗?

C语言有没有字符串类型?

第十六个关键字return

计算机中释放空间是将数据全部清0吗?

函数的栈帧是什么?

函数栈帧被释放

函数栈帧被覆盖

函数调用时如何形成足够的栈帧空间?

为什么临时变量具有临时性?

reruen关键字的最终奥秘

1.函数返回值的基本概念

2.返回值如何传递?

3.临时变量的生命周期

4.返回值没有接收时的行为

5. 汇编代码的例子

6. 接收返回值的情况

 esp eax mov call ret这些是什么?

第十七个关键字const

const修饰的变量,不可以“直接”被修改

const可以放在类型之前,也可以放在类型之后

coust修改的变量可以间接被修改

那count修饰变量,有什么意义?

字符串常量真正意义上不可被修改

 数组的长度可以用const修饰的变量表示吗?

const修饰数组

cinst修饰指针

第一种情况:const int* p

第二种情况: int const * p

第三种情况:int* const p

第四种情况:const int* const p


学习C语言必须要知道的小秘密 !

C语言函数如何查询使用方法?

查询函数使用方法的网站:cplusplus.com - The C++ Resources Networkicon-default.png?t=O83Ahttps://legacy.cplusplus.com/

cplusplus.com是一个专注于C++编程语言的资源网络,提供了广泛的教程、参考资料和用户贡献的文章。

VS2022使用scanf安全报警应该如何处理?

第一种方法:

使用宏定义 _CRT_SECURE_NO_WARNINGS 1可以关闭警告,注意一定要放在代码的第一行!

#define _CRT_SECURE_NO_WARNINGS 1

第二种方法: 

#pragma warning(disable:4996) 是一个编译器指令,用于告诉编译器禁用特定的警告。这里的 4996 是一个警告编号

#pragma warning(disable:4996)

在低版本的VS中,代码运行后代码一闪而过,如何解决?

使用函数 system("pause); 包含头文件window.h 让控制台停屏

写好的C语言代码如何运行的?

文本代码->可执行程序(二进制代码)->双击启动该程序

运行后C语言源代码可执行文件在哪里?

项目路径:/Debug文件下:/ .exe文件(可执行文件)

VS2022编译器编译运行的快捷键是什么?

Ctrl+F5 编译链接并执行程序

C语言编译器与windows系统的小秘密

编译链接并执行程序是把代码生成可执行程序,然后运行,运行动作与双击是等价的。双击是打开,意味着将程序是数据加载到内存当中,让计算机运行。

1.在win中,双击的本质是运行程序,将程序加载到内存当中。

2.任何程序在被运行之前都必须加载在内存当中 。     

为什么所有的程序运行都要被加载到内存当中?

因为快!我们数据通过输入设备(键盘) 写入内存中,内存再送到CPU,CPU通过计算又返回到内存,最终打印到我们输出设备上(显示器)。

程序在没有被加载到内存当中之前是在哪?    

程序没有被加载之前都是在硬盘当中,程序中所有的变量是在程序运行的时候去开辟,或者说有些变量是在编译的时候才开辟内存空间的,只要是在函数内或者是代码块内定义的变量,都叫做临时变量,临时变量是程序运行时在栈上开辟的。变量是在程序运行的时候开辟的,所有的变量,本质都是要在内存的某个位置开辟空间的。

变量的定义与使用

变量是什么?

在内存中开辟空间,用来存放数据。程序在运行过程中可以发生改变的量。

如何定义变量并初始化?

以下代码是定义一个int类型变量并且初始化为10,初始化的含义可以理解成:变量a开始空间后,里面所存放的值就是10;
代码示例:
#include <stdio.h>
int main()
{
	int a = 10;//定义并初始化
	return 0;
}

什么是赋值操作?

修改变量原有的值,就称为赋值操作。

变量a初始化为10,我们通过给a赋值操作,把8赋值给a,此时的变量a存储的值就是8。

代码示例:(注释有详解)

#include <stdio.h>
int main()
{
	int a = 10;//定义并初始化
	a = 8;//赋值
	printf("%d\n", a);
	return 0;
}
为什么要定义变量?
因为我们有些数据不是要立刻被处理,而是要先开辟内存保存起来等待后续处理。所以我们要开辟空间将数据先存放起来。

定义与声明

定义是开辟内存空间

声明是告知

变量的作用域

作用域概念:指的是该变量的可以被正常访问的代码区域!

局部变量的作用域:只在本代码块内有效!

代码示例:(注释有详解)

#include <stdio.h>
int main()
{
	int a=20;//变量a在main函数中定义,可以在main函数中的函数体使用
	{
		int b = 10;//变量b在花括号内定义,访问范围仅限花括号
		printf("%d %d\n", a, b);//a可以使用
	}
	printf("%d %d", a,b);//变量b超出作用域范围
	return 0;
}

全局变量的作用域是:从定义变量的位置开始到源文件(程序) 结束。

代码示例:(注释有详解)

#include <stdio.h>
int c = 101;//全局变量

void fun()
{
	printf("fun:%d\n", c);
}

int main()
{
	fun();//调用函数
	printf("main:%d",c);
	return 0;
}
生命周期概念:指的是该变量从定义到被释放的时间范围,所谓的释放,指的是曾经开辟的空间”被释放“,衡量的是生存时间长短问题。作用域是从什么地方开始有效到什么地方开始无效。

就近原则:

总结:函数体内若定义的局部变量 与全局变量同名,则将全局变量屏蔽。

#include <stdio.h>

int a = 101;//全局变量

int main()
{
	int a=20;//变量a在main函数中定义,可以在main函数中的函数体使用
	
	printf("%d", a);
	return 0;
}

第一个关键字auto-修饰局部变量

一般在代码块中定义的变量,即局部变量,默认都是auto修饰的,不过一般省略。(auto可以延伸出变量的作用域与生命周期这两个概念)
1.默认的所有变量都是auto吗?
不是,auto用来修饰局部变量
注意:后面我们所到的,局部变量,自动变量,临时变量,都是一回事。我们统称局部变量
#include <stdio.h>
auto int c = 60;//全局变量 不可使用auto 
int main()
{
	 int a = 10;
	 {
		 auto int b = 30;//auto修饰局部变量 
		 printf("%d\n",b);
	 }
}

 第二个关键字register关键字-最快关键

register修饰的变量会尽量将一个变量定义到寄存器当中,编译器很根据当前CPU的状态来调整我们用register修饰的变量,尽量定义到寄存器中。从而达到提高效率的目的。

计算机五大硬件是:运算器,存储器,控制器,输入设备,输出设备。

计算机内部采用二进制来表示指令和数据

其中硬件中的中央处理器即CPU,CPU包括运算器与控制器,运算器的作用是控制机器各个部门协调一致的工作,运算器则可以执行算术运算和逻辑运算 。其中运算器与控制器都有使用寄存器,寄存器它是特殊的CPU区域能提高计算机性能。它可以在分析指令的时候存储程序指令,可以在运算器出来数据的时候存储或者存储计算结果,CPU可以直接访问寄存器中的数据。

寄存器靠近CPU硬件性能是很好的,可以更高效的处理数据。

90530204bf3c4cf79db8ef9837737d9e.png

使用寄存器的小建议:

1.局部变量有需要可以使用register关键字,因为如果全局变量使用register会导致我们的CPU被长时间占用,从而影响性能。

2.不会被写入内存的变量可以使用register关键字,写入就需要就需要写回内存,后续还要读取检测的话,register的意义在哪呢?

3.高频被读取的(提高效率所在),因为变量开辟空间时时被放在寄存器当中,就不用在内存读取,直接在CPU中读取。从CPU读取的效率会远远大于寄存器的效率。

4.如果要使用register,请不要大量使用,因为寄存器数量有限

register修饰的变量是开辟在寄存器中,我们是无法进行取地址操作来查看所在的地址空间

#include <stdio.h>
int main()
{
	 register int a = 10;
	 printf("%p", &a);
	 return 0;
}
register关键字,不用管,因为现在的编译器,已经很智能了,能够进行比人更好的代码优化。
早期编译器需要人为指定register,来进行手动优化,现在不需要了。

第三个关键字static-静态变量

想要学好static关键字就要知道,多文件操作的必要性,如果我们把所以代码都写在一个源文件,它会很乱不利于我们管理代码。在说static之前先简单跟大家聊一聊extern关键字。给文件起名字要求名字要有"意义",建议使用英文,源文件使用.c结尾,头文件使用.h结尾。.c结尾的就是我们的源文件。

我使用多文件来讲解extern关键字,创建两个源文件,main.c和test.c

a909bcea519f423ea6941373c759e35e.png

1058c3e7d5434acbae5320a9b3e7365a.png

我们在test.c文件中写一个无返回值的自定义函数fun,让其输出一句字符串。

238319b8542d4e249fb27b37c0b945c9.png如果这个时候我们在main.c文件中,写一个main函数,在函数体内部直接调用fun函数的话,是可以调用的,但是会有报错信息提醒。因为我们并没有函数声明。2fe85f3377f845e89d799b2e43f98915.png

如果我们在test.c文件中,定义一个全局变量,是不可以在main.c使用的。如果全局变量想跨文件操作就要使用extent注释全局变量。

041f0ebd73af43ca83a187bde11984ab.png

浅谈extern关键字-声明

我们需要使用关键字extern在main.c文件中修饰该全局变量,这样我们全局变量就可以在main.c文件中使用。extern是变量的声明关键字。

635d73d3581a46efbf9e4a18f10f8510.png

extern关键字是声明变量,声明实际上是告知的意思,我们如果在main.c文件中,使用extern关键字的时候是不可以进行初始化的,因为声明是告知并没有开辟空间,开辟空间是定义。

b80f188c2da34103bb7aa91bde2d2615.png

总结:extern关键字所声明变量的时候,不能设置初始值。

为什么要有头文件?

单纯的使用源文件,组织项目结构的时候,项目越大越复杂的情况下,维护成本就会越高。.h是头文件,组织项目结构的时候,减少大型项目的维护成本。

.h头文件基本上都是被多个源文件包含,可能会出现一个问题,头文件重复包含问题。

解决方案:#pragam once

我们先创建一个头文件,给他起个名字。注意头文件是.h结尾的文件。

b3a14d058fae4a7190783aec25aed2f9.png

我们先在头文件开头写上#pragam once  防止出现头文件重复包含问题。并且在头文件中我们把其他源文件中所使用到的都文件都包含在内,之后只需要在源文件中包含头文件即可使用头文件中所包含的源文件,包含自定义头文件需要用" "双引号,并且函数的声明与变量的声明也写在头文件内,建议用extern修饰,变量声明必须用extern修饰。因为变量的声明不加extern修饰会误认为是定义,函数如果不加extern它知道是声明,因为函数没有函数体。带上是好习惯,可以让阅读代码的人,知道规则。

15ac96e348904f9ebad4d0f5114ee169.png

函数传递参数的时候,我们如何定义的函数,就要如何声明

7e39314d270d4f31bb3900317f7fc79c.png

static修饰全局变量

项目维护,提供安全保证。

理论上我们写的代码是要给别人暴露接口的,暴露的接口越多,用户的使用成本越高,暴露了太多的接口出去,代码的业务逻辑是可以被猜出来的,在C语言中我们可以使用static关键字修饰函数名和全局变量进行封装起来,对外就暴露一个简单函数出去,提高了安全性。

static修饰全局变量,该变量自在本文件内被访问,不能被外界,其他文件访问。

static修饰函数,该函数只能在本文件内访问,不能在外部其他文件直接访问。

头文件中我们只要把想暴露出去的函数进行声明,其他不想暴露的函数我们可以在源文件使用static修饰,在头文件则不需要声明。我们想要调用被封装的函数或者变量的时候,我们只要在源文件中写一个简单的函数,来调用当前源文件中被static修饰的函数或者变量即可

bd76166ef1664b1d9f91a19eb25301a3.png

以上代码给大家演示的在test.c源文件中,static修饰fun函数和int a全局变量,我们需要再在test.c源文件中一个简单的函数,用于外界的调用,外界即可使用test.c源文件中的fun函数与int a变量。并且头文件中被static修饰的函数或者变量可不用声明。

c8cafc195b2541d385f63e28114faf82.png

static修饰局部变量

static修饰局部变量 局部变量不会被释放,更改了该局部变量的声明周期,相当于延长了它的使用时间。由临时声明周期->全局声明周期,简单的说可以跟随mian函数的程序结束而结束,但是作用域不变,只能在本代码块中有效

#include <stdio.h>
static void fun()
{
	static int i=0;;//static修饰局部变量 变量不会被释放
	i++;
	printf("%d\n", i);
	
}

int main()
{
	int i;
	for (i = 0; i < 10; i++)
	{
		fun();
	}
	return 0;
}

a2ce44b3f0324df29a784eed88fd5c02.png

如果我们不使用static修饰局部变量,我们每次函数调用,函数中的局部变量每次执行结束后都会被释放掉。

#include <stdio.h>
static void fun()
{
	int i=0;;//static修饰局部变量 变量不会被释放
	i++;
	printf("%d\n", i);
	
}

int main()
{
	int i;
	for (i = 0; i < 10; i++)
	{
		fun();
	}
	return 0;
}

 0e2e3839b38a43f9a3fc36a3254ed15d.png

 为什么临时变量具有临时性,全局变量具有全局的性质?

ffe68b9c214c47c583a8de1ea69efe33.png

static修饰的局部变量会被重新放入全局数据区,全局数据区在整个进程的生命周期都是有效的。局部变量则是存放在栈区,栈区的原理类似于手枪上子弹,压一颗子弹上一颗,退子弹的时候也是最后压进的,最先退出来。

static修饰局部变量未显示初始化

一个被 static 修饰的局部变量,如果未显式初始化,它的默认初始值是0,而不是不确定的。这与自动变量(auto,即普通的局部变量,如果不显式初始化则其值是不确定的)不同。这是因为在C语言标准中规定,静态分配的对象如果没有显式初始化,则会被自动初始化为0。

#include<stdio.h>
int i = 1;
main()
{
	static int a;
	register int b = -10;
	int c = 0;
	printf("-----MAIN------\n");
	printf("i:%d a:%d\ b:%d c:%d\n", i, a, b, c);
	c = c + 8;
	other();
	printf("-----MAIN------\n");
	printf("i:%d a:%d\ b:%d c:%d\n", i, a, b, c);
	i = i + 10;
	other();
}
other()
{
	static int a = 2;
	static int b;
	int c = 10;
	a = a + 2; i = i + 32; c = c + 5;
	printf("-----OTHER------\n");
	printf("i:%d a:%d\ b:%d c:%d\n", i, a, b, c);
	b = a;
}

第四个sizeof关键字

sizeof关键字(操作符),求特定类型对应开辟空间的大小。

#include <stdio.h>
int main()
{
	printf("%d\n", sizeof(char));
	printf("%d\n", sizeof(short));
	printf("%d\n", sizeof(int));
	printf("%d\n", sizeof(long));
	printf("%d\n", sizeof(long long));
	printf("%d\n", sizeof(float));
	printf("%d\n", sizeof(double));
	return 0;
}

0737d066441d4b138a90bcb07500dcf1.png

为什么出现这么的类型?

本质上是对内存进行合理的划分,应用场景不同,解决应用场景对应的计算方式不同,需要空间也是不同,本视上是用最小的成本,解决各种多样化的场景。比方说在我们生活中就有浮点数这样的小数存在,就拿我们身高一样,可能你今天测试的身高是1.83,这个数据恐怕也是不精确的,可能我们实际的身高会是1.8345…….厘米。但是通常情况下不会精确到那个地步。存在这么多的类型,其实是为了更加丰富的表达生活中的各种值。

sizeof是函数嘛?

函数的右边是肯定是有括号的,我们可以使用sizeof a来查看变量在内存中所开辟的空间,所以很明显,sizeof不是函数,它是关键字或着说是操作符。

4fd64968831d4500bfd595a28e9104e4.png

sizeof不光可以求内置变量的大小,指针,数组也都是可以的!

#include <stdio.h>
int main()
{
	int a[3];
	int* p=NULL;
	int* arr[3];
	printf("%d\n", sizeof(a));
	printf("%d\n", sizeof(p));
	printf("%d\n", sizeof(arr));
	return 0;
}

 82ac8aeed3a74b6eab43fc5260275427.png

变量命名规则

1.见名知义,看到名字就知道什么意思

2.定义全局变量用 g_ 开头

3.程序命名不得出现仅靠大小写区分的变量

4.禁止函数名与变量名重名

5.大驼峰命名法

示例

firstNamegetUserNameisRunning

6.蛇底命名法

示例:total_pricecalculate_areamain_function.c

7.遵守标识符的命名规则

C语言的标识符一般应遵循如下的命名规则:

1 标识符必须以字母或下划线开头,后面可跟任意个(可为0)字符,这些字符可以是字母、下划线和数字,其他字符不允许出现在标识符中。(可以出现美元符号$)

2 标识符区分大小写字母

3 标识符的长度,c99规定63个字符以内

4 C语言中的关键字,有特殊意义,不能作为标识符

整型存储

原码,反码,补码最终都是二进制,因为任何数据在计算机中,都必须转化成为二进制,计算机只认识二进制,但是计算机内存储必须是补码!

十制转二进制的技巧

我们只要记住2的几次方就算我们二进制跟几个0

  • 2^0=1(二进制:01)
  • 2^1=2   (二进制:10)
  • 2^2= 4  (二进制:100)
  • 2^3=8   (二进制:1000) 
  • 2^4=16 (二进制:1 0000)
  • 2^5=32 (二进制:10 0000)
  • 2^6=64 (二进制:100 0000)
  • 2^7=128 2^8=256(二进制:10000 0000)

在数学中,任何非零数的0次方都定义为1。这是指数法则的一部分。

10 转换成二进制就是 10-2^3= 2  2-2^1=0 对应二进制就是1010

20 转换成二进制就是 20-2^4=4 4-2^2=0 对应的二进制就是 0001 0100

有符号数,如果是32位,它最高的比特位,是要作为符号位的,不管是正数还是负数最高比特位要区别,最终是正数还是负数。

有符号正数

有符号正数的原码,反码,补码是相同的

任何数据在计算机中,都必须被转化成为二进制,为什么?计算机只认识二进制

符号位(0,1)+数据位  0表示正数,1表示负数。

10 转换成二进制 0000 0000 0000 0000 0000 0000 0000 1010

我们写一个代码通过调试,看一下如果定义一个int类型初始化为10,该变量在整型中存储的情况。可以发现内存中是0a 00 00 00跟转化的相反是因为大小端存储导致的。

02c473038139498d95317a65ab054e96.png

16进制表示方法

0到9的数字直接用其本身的符号表示,即0, 1, 2, 3, 4, 5, 6, 7, 8, 9。

10到15的数字用A到F表示,其中A=10, B=11, C=12, D=13, E=14, F=15。

在表示一个十六进制数时,通常会在数字前面加上前缀“0x”或者后缀“H”来明确指出这是一个十六进制数。例如:

  • 十进制数10在十六进制中表示为A,    写作0x0A 或者 0AH。
  • 十进制数255在十六进制中表示为FF,写作0xFF 或者 FFH。
  • 十进制数16在十六进制中表示为10,   写作0x10 或者 10H。

以下是一些十六进制数的例子:

  • 0x01 = 1
  • 0x02 = 2
  • 0x03 = 3
  • 0x04 = 4
  • 0x05 = 5
  • 0x06 = 6
  • 0x07 = 7
  • 0x08 = 8
  • 0x09 = 9
  • 0x0A = 10
  • 0x0B = 11
  • 0x0C = 12
  • 0x0D = 13
  • 0x0E = 14
  • 0x0F = 15
  • 0x10 = 16
  • 0xFF = 255
  • 0x100 = 256

有符号负数

-20转成原码

原码: 1000 0000 0000 0000 0000 0000 0001 0100

原码转反码,符号位不变其余按位取反1变0,0变1

反码:  1111 1111 1111  1111  1111  1111  1110 1011 

反码转补码,反码+1

在实际给一个二进制序列加1的时候,就像反码转补码一样,符号位有没有参与运算?

算!但是符号位是转化过程是不会出现溢出的,一直计算至符号位的情况也是非常少见,但是加1的时候,符号位是进行运算的。有参与运算。

我们再来看一下,如果定义一个整型变量里面存储-20它在内存是如何存储的。

277db7f9a74f43ff87ed1aa5fbf4400e.png

无符号数

无符号数的原码->补码->反码是相等的并且没有符号位,直接存。

补码如何转原码?

方法1:

补码减1之后,按位取反,就是原码

拿-20举例,-20的

补码: 1111 1111 1111  1111  1111  1111  1110 1100 

减1后

补码: 1111 1111 1111  1111  1111  1111  1110 1011

符号位不变,其他位按位取反

原码: 1000 0000 0000  0000 0000  0000  0001 0100        

方法2:

补码->符号位不变,其他位按位取反->加1就是原码

拿-20举例,-20的

补码: 1111 1111 1111  1111  1111  1111  1110 1100 

按位取反后

1000 0000 0000  0000 0000  0000  0001 0011 反码+1

1000 0000 0000  0000 0000  0000  0001 0100 原码

第五个关键字unsigned(无符号整型)

无符号整除存与取的过程

unsigned关键字修饰整型变量的时候,带符号的数据为什么不报错?

#include<stdio.h>
int main()
{
    unsigned int a = 10;
    unsigned int b = -10;

    return 0;
}

存储的本质是计算机在存储数据的时候一定是把数据转化成二进制补码存储到变量当中,定义的本质是开辟空间,空间开辟好把我们的数据存放在里面。本质上不管初始化还是赋值,都是先有空间,再有数据。所以我们第一步是先开辟空间,开辟好了我们才会把-10存放到空间里面,存储之前是把我们的内容转化成二进制补码,我们拿- 10举例

-10 原码 1000 0000 0000 0000 0000 0000 0000 1010

      反码 1111 1111 1111  1111 1111 1111 1111 0101

      补码 1111 1111 1111  1111 1111 1111 1111 0110

相当于我们把空间开辟好,然后把补码存进去。注意我们在存补码的时候,空间是不关心内容的。也就意味着开辟了空间,我们把二进制补码放进去就可以。在将我们的数据保存在我们空间内的时候,数据已经被在转化成二进制。所以定义变量只提供了一个空间的概念,所以我们数据存放在空间,是负数正数取决于补码自身的符号位和保存的空间是没有关系的。 数据保存进空间里,空间只能存储二进制补码,也就是意味着-10要存到空间里面,一定把-10转成二进制补码,然后存放到空间里面。数据存进空间的时候已经是二进制补码了,补码是由数据决定的不是空间决定的。       

unsigned关键字修饰的变量什么时候能有效果?

我们数字只有加上类型才会有意义,比方说有一个二进制代码,你应该怎么理解跟解释??

例如:1111 1111 1111  1111 1111 1111 1111 0110

单纯一个二进制代码是没有意义的,在读取的时候,才有意义,类型决定了如何解释空间内部保存的二进制序列。

下面我们用代码,演示一下unsigned修饰变量的时候,变量如果存放的是一个有符号数,如-10的情况,我们用无符号说明符u来输出对应的数据,它的结果为什么不是-10?

e3c4cabeae634c8f8271523d02baa61a.png

0ef0eb1bf108450ea3d9cb31043152f2.png

-10,转成二进制是1111 1111 1111  1111 1111 1111 1111 0110,我们在打印的时候,相当于是取出数据进行输出,简单的说存是原码转补码,取是补码转原码转十进制,先看数据的自身类型unsigneds是无符号整型,无符号数的原码->补码->反码是相等的并且没有符号位,所以我们把它直接转成10进制,10进制转完后,结果就是4294967286

第六个关键字signed(有符号整型)

有符号负数被singned修饰存与取

#include<stdio.h>
int main()
{
   
    signed int b = -10;
    printf("%d\n", b);
    return 0;
}

1.先看自身类型signed有符号类型

2.看符号位,符号位为1,证明是负数

3.确定原码,反码,补码

-10,存进计算机转成二进制补码是1111 1111 1111  1111 1111 1111 1111 0110,我们取出的时候也就是打印输出的时候,先看自身类型signed是有符号数,再看符号位,符号位是1证明是负数,那它就是补码,我们把该补码转化成对应的原码。简单的说存是原码转补码,取是补码转原码转十进制按照上文我们讲过的解法,符号位不变其他位按位取反,再加1 .

补码:1111 1111 1111  1111 1111 1111 1111 0110     

原码:   1000 0000 0000 0000 0000 0000 0000 1010

整型存储总结:存储的时候不关系变量的类型,直接将数据转成二进制,然后转成补码放进开辟的内存空间,取的时候一定要看自身类型,看看是被unsigned修饰的无符号数还是signed修饰的有符号数,unsigned修饰的是没有符号位的,无符号数的原码->补码->反码是相等的并且没有符号位,所以我们实际上打印的是“补码”,因为原码,反码,补码是一样的,signed修饰的是有符号位的,我们需要通过计算原码,输出原码的对应进制。

我们再通过两个小例子,加深一下印象

例子1:

#include<stdio.h>
int main()
{
   
    signed int b = 10;
    printf("%d\n", b);
    return 0;
}
 

存储:有符号正数的原码,反码,补码是相同的,我们把10的原码写出来就是它的补码。

存储计算机中的补码:0000 0000 0000 0000 0000 0000 0000 1010

读取:先看自身是被什么类型修饰,再看符号位是0那就是正数,直接可以确定原码,有符号整数原码补码相同。那再次输出的时候就是10!

例子2:

#include<stdio.h>
int main()
{
   
    unsigned int b = -10; //存
    //原码 1000 0000 0000  0000 0000 0000 0000 1010
    //反码 1111 1111 1111  1111 1111 1111 1111 0101
    //补码 1111 1111 1111  1111 1111 1111 1111 0110
    printf("%d\n", b);//取
    //有符号数 转回原码
    //原码 1000 0000 0000  0000 0000 0000 0000 1010
    printf("%u\n", b);//取 

    //补码1111 1111 1111  1111 1111 1111 1111 0110
    //无符号数原反补相同
    return 0;
}
 

 取 看“对应”数据类型,首先看被什么修饰的,再看输出的格式说明符

85d45693d48942f78e0984128e1feed2.png

大小端存储 

数据在内存,存储的方向是从小到大,我们可以通过计算机,得出负10的补码,因为我们的补码是存储在计算机中的。

-10 原码 1000 0000 0000 0000 0000 0000 0000 1010

      反码 1111 1111 1111  1111 1111 1111 1111 0101

      补码 1111 1111 1111  1111 1111 1111 1111 0110

我们再把补码以16进制的形式表示出来 0XFF FF FF F6 ,但是我们在VS2022查看地址的时候,发现地址是从小到大来存储的,为什么是不一样的呢?

bb02b742e521435985ba29c21b05e2ef.png

数据按照字节为单位是有高权值位和低权值位之分的,内存按照空间划分按照字节为单位也有高地址和低地址之别。

由此产生两种存储方案:
1.大端 按照字节为单位,低权值位数据存储在高地址处,就叫做大端

2.小端按照字节为单位,低权值位数据存储在低地址处,就叫做小端

所以我们VS2022存储方式是小端存储。

#include<stdio.h>
int main()
{
   
    unsigned int b = -10; //存
    //原码 1000 0000 0000  0000 0000 0000 0000 1010
    //反码 1111 1111 1111  1111 1111 1111 1111 0101
    //补码 1111 1111 1111  1111 1111 1111 1111 0110
    printf("%d\n", b);//取
    //有符号数 转回原码
    //原码 1000 0000 0000  0000 0000 0000 0000 1010
    printf("%u\n", b);//取 看“对应”数据类型,首先看被什么修饰的,再看输出的格式说明符

    //补码1111 1111 1111  1111 1111 1111 1111 0110
    //无符号数原反补相同
    return 0;
}
 

43e43376200041b2958084fbf86e8a63.png

那数据是如何取的呢?

按照我们存入的先后顺便,依次取出即可。具体就是先看数据的大小端,再看自身类型。

数据截断

当发生数据截断如何存数据?

#include <stdio.h>
int main()
{
	char c = -128;
	// 存储
	// 原码 1 1000 0000
	// 反码 1 0111 1111 +1
	// 补码 1 1000 0000
	printf("%d\n", c);
	return 0;
}

因为我们CPU中的寄存器参与了运算,出现了第九个比特位。当我们存储的时候只有八个比特位,我们 就会发生截断问题,发生截断后在内存中存储的就是1000 0000

 存储
 原码 1 1000 0000
 反码 1 0111 1111 +1
 补码 1 1000 0000

所以我们存进内存的补码是1000 0000,当我们取出的时候是取的1000 0000先转乘

当发生数据截断如何取数据?

这边演示一个截断的小例子,给大家说一下发生截断的情况。

9ec627aec0d0478fbcb5dd4c9bf2be62.png

当我们发生截断后舍弃高位在例子中舍弃的是符号位。所以我们的取出是原码1000 0000,我们先减1,符号位是参与运算的,我们得到的结果就是0111 1111,再对该二进制数列取反,符号位是0不变得到的结果就是0000 0000,但是我们实际输出的值是-128,由此可以得出结论,我们取出的过程是半计算半规定的一种方式。   

无符号数的取值范围非常简单,如果是八个比特位,那么就是0-255,如果是16比特位,那就是0-2^16次方减1.        

有符号数的取值范围

short是[-2^7 - 2^7-1]

short [-2^15 - 2^15-1]

int [ -2^31 - 2^31-1]

有符号整数(通常默认为 signed)

  1. 符号位:有符号整数使用最高位(最左边的位)作为符号位。在8位的情况下,第8位是符号位。其余七位都是数值位。

    • 如果符号位是0,整数是正数。
    • 如果符号位是1,整数是负数。
  2. 表示范围:因为有符号整数使用一个位来表示正负,所以剩下的位用来表示数值。在8位的情况下:

    • 最小值:-2^(位数-1)(即-128,因为2^(8-1) = 128)
    • 最大值:2^(位数-1) - 1(即127)
  3. 表示方法:有符号整数通常使用二进制补码(two’s complement)表示法来表示负数。这意味着:

    • 正数的表示与无符号整数相同。
    • 负数通过取其绝对值的二进制表示,然后取反(0变1,1变0),最后加1来表示。

无符号整数(unsigned)

  1. 无符号位:无符号整数没有符号位,所有的位都用来表示数值。无符号数的原码->补码->反码是相等的并且没有符号位,直接存。

  2. 表示范围:在8位的情况下,所有的256个可能的组合都用来表示非负数:

    • 最小值:0
    • 最大值:2^位数 - 1(即255)
  3. 表示方法:无符号整数的二进制表示直接对应于其数值,没有符号位的解释。

在计算机中为什么都是补码?

在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加法器)。此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。所有的计算全部转化成加法,因为所有的计算都在CPU内计算,CPU都用加法那我们的硬件电路设计一套就足够了。

C语言中什么是语句?

在C语言中凡是是分号结尾的就是语句

第七个关键字if else(条件判断语句)

 if与else双分支语句

1.先执行if()中的表达式,得到返回值 
2.对返回值进行判断 
 3.为真走if语句,为假走else语句,判断真假口诀非零非空即为真

#include <stdio.h>
int main()
{
	int flag = 1;
	//1.先执行()中的表达式,得到返回值 
	// 2.对返回值进行判断 
	// 3.为真走if语句,为假走else语句
	if (flag == 1)
	{
		printf("hello world\n");
	}
	else
	{
		printf("hello else\n");
	}
	return 0;
}

 我们了解这个之后,有什么用呢?

我们可以写一个自定义函数,在main函数中定义一个变量接受该函数的返回值,用if对该变量进行判断就可以知道,返回值是真是假,是不是空。因为是0是空都是假,口诀就是非空非空即为真。

#include <stdio.h>
int fun()
{
	printf("无数据\n");
	return 1;
}
int main()
{
	int ret = fun();//将fun函数返回值,赋值给ret

	if (ret == 1)
	{
		printf("yes\n");
	}

	return 0;
}

3fe2c7ac4b5d44fd94e79e326410bc02.png

我们可对该代码做进一步优化,将fun函数返回值进行判断

#include <stdio.h>
int fun()
{
	printf("无数据\n");
	return 1;
}
int main()
{


	if (fun() == 1)//将fun函数返回值进行判断
	{
		printf("yes\n");
	}

	return 0;
}

e933fb91476f44ae922ca4d0226a8b8d.png

来个随堂小练习

#include <stdio.h>
int main()
{
	int a = 1;
	int b = 2;
	int c = 3;
	if (a == 1 && b == 2||c==4)
	{
		printf("if表达式为真\n");

	}
	else
	{
		printf("if表达式没有成立\n");
	}
	return 0;
}

 if表达式中我们先对&&左右两边进行判断u后,&&表达式的返回值为1,此时的||左边为真的时候就会发生短路,因此if表达式的结果过为真。

4f26de0e9758411fac352e8b6ad56250.png

if语句嵌套使用

#include <stdio.h>
int main()
{
	int flag = 2;
	if (1 == flag)
	{
		printf("hello bit\n");
	}
	else if (2 == flag)
	{
		if (1)
		{
			printf("学好C语言\n");;
		}
		printf("hello C语言\n");
	}
	else
	{
		printf("hello world\n");
	}
	return 0;
}

bfce65798a2c456b8a660797cfe3726a.png

C语言中有bool(布尔)类型吗?

c89,c90是没有的,C99引入新的关键字是_Bool布尔类型。以宏的方式形成一个布尔类型,有true有false,目前大部分书都是c90,都是认为没有的。因为书,一般都要落后于行业。
但是c99引入了_Bool类型(你没有看错,_Bool就是一个类型,不过在新增头文件stdbool.h中,被重新用宏写成了bool,为了保证C/C ++兼容性)。

下面用代码给大家演示一下,bool类型的变量在C语言中是占1个字节

#include <stdio.h>
#include <stdbool.h>//c99
int main()
{
	bool x = true;
	x = false;
	printf("%d\n", sizeof(x));
	return 0;;
}

fa54cfedef744bff9644c0e4b3f4c3ff.png

我们来看一下在最新的C99编译标准中,我们是如何定义_Bool类型的,双金选中转定义。

f614a36f46714380968897c9a3d75004.png

8b822ae6efa948bd8122dd73a7129052.png

C语言_Bool类型,c89,c90是没有的,C99引入新的关键字是_Bool布尔类型。以宏的方式形成一个布尔类型,有true有false

全部大写的布尔类型,很多编译器已经删除了宏定义。我们不推荐,得了解不同的平台的特性,可移植性较差!

030baf2586e147eb8fd85932367b18ed.png

Bool类型用法推荐 

只有第三种写法是推荐的,第一种在语义的层面上不容易上编译器理解,因为0是整数用双等号跟0进行比较,是特别不友好的,因为它可能会误以为是整数比较。第二种用头文件去掉,用C90就会编译不过

因为第三种是先执行()中的表达式or函数,得到真假结果(true, false, 逻辑结果),然后条件判定功能,if只能判定逻辑结果,而我们的flag本身就是逻辑结果,进行分支功能(!flag)直观的反应出来了,flag是"bool"布尔类型!

#include <stdio.h>
#include <stdbool.h>//c99
int main()
{
	int flag = 0;
	if (flag == 0)
	{
		//不推荐
		printf("1\n");
	}
	if (flag == false)
	{
		//不推荐
		printf("2\n");
	}
	//1.先执行()中的表达式or函数,得到真假结果(true, false, 逻辑结果)
	//2.条件 判定功能
	//3.进行 分支功能
	//if(!flag)
	//直观的反应出来了,flag是"bool
	//直观的反应出来了,flag是"bool"
	
	if (!flag)
	{
		//推荐
		printf("3\n");
	}
	return 0;;
} 
#include <stdio.h>
#include <stdbool.h>
int main()
{
	bool x = true;
	if (x)//推荐
	{
		;
	}
	return 0;;
} 

if判断浮点数

精度损失不光可以让数据变小,也可以让数据变大

在C语言中,我们实数常量它默认是double类型的,当我把一个小数复制到float类型的变量里面,我们存储的时候时候和数据本身是没有关系的,所以3.14是八个字节的数据,放进float四个字节大小的空间,就会发生截断!编译器也会识别到!

#include <stdio.h>
int main()
{
	float a = 3.14;
	return 0;
}

8df53d8eed4c49b1be83fcf9803b594a.png

为了让他不再出现这种告警,我们可以在数据的右边加f结尾,以为数据是float类型。

#include <stdio.h>
int main()
{
	float a = 3.14f;
	return 0;
}

精度损失不光可以让数据变小,还可以让数据变大c82731a32307496d9edd325f68884192.png

我在通过一个小例子看一下精度损失

正常来说我们1.0-0.9它的结果应该是0.1但是我们打印的结果都是0.1

#include <stdio.h>
int main()
{
	double a = 1.0;
	double b = 0.1;
	printf("%.50f\n", a-0.9);
	printf("%.50f\n",b);
	return 0;
}

8383b20c08494cd687677ae600537e94.png

我们尝试,用if判断一下a-0.9是不是等于0.1?

#include <stdio.h>
int main()
{
	double a = 1.0;
	double b = 0.1;
	printf("%.50f\n", a-0.9);
	printf("%.50f\n",b);
	if ((a - 0.9) == 0.1)
	{
		printf("you can see me!\n");
	}
	else
	{
		printf("oops\n");
	}
	return 0;
}

2807562423f64c40882a328f3024c933.png

运行之后的结果很明显,它又发生了精度丢失!所以浮点数在进行比较的时候,绝对不能用==等于号来进行比较!

浮点数的精度丢失是由误差范围,我们认为在该范围内,就算是正常的,由此我们可以用这个误差范围来做一个判断.编译系统已经把最小的误差范围给了我们,我们可以用DBL_EPSILON来判断,记得引入<float.h>头文件。fabs函数是求绝对值的函数,需要包含头文件<math.h>,我们想判断两个浮点数是否相等,只要将两个浮点数相减在合法的误差内。

#include <stdio.h>
#include <math.h>
#include <float.h>
int main()
{
	double a = 1.0;
	double b = 0.1;
	printf("%.50f\n", a-0.9);
	printf("%.50f\n",b);
	if (fabs((a - 0.9) - b) < DBL_EPSILON)
	{
		printf("you can see me!\n");
	}
	else
	{
		printf("oops\n");
	}

	return 0;
}

2030708f9dea4a9fbbbfeace85a37b03.png

浮点数和0比较:
1. 浮点数存储的是时候,是有精度损失的(实验验证了)

2. 浮点数是不能进行==比较的
3. if(fabs(a-b)< DBL_EPSILON){}
4. fabs函数的返回值不要写<=,直接写<就好,是正确的

指针变量和零值比较

  其实在存储的层面上,也就是原码的层面上,0,\0,NULL本质上都是归0,我们通过打印可以看出来,但是我们要知道一点是内存的数据取出来是对应类型的,不同的类型数据的解释不一样。

#include <stdio.h>
int main()
{

	printf("%d\n",0 );
	printf("%d\n", '\0');
	printf("%d\n", NULL);
	return 0;
}

5f32e47a02e941b2b22597ff7ad80e51.png

我可以看一下NULL的定义


8d85b4304ccc498b8405cefa5cc41788.png

NULL其实就算0值,强转成void*类型,我们强制类型转化,本质上改变的是类型,对内存所存放的数据是不改变的。也就是说只是同一个数据对应不同的类型解释。那我们为什么要改变它的类型呢?原因是出于编译器识别代码的考虑,如果编译器阅读的时候,发现指针变量是NULL它就会知道是空,因为等号的左右两边要同类型!对于程序员来讲只是对我们写代码的时候方便。

我们单纯的用指针变量p去跟0去做比较,对我们来说阅读的体验并不是很好,因为我们会认为p是一个变量,而忽略是指针变量

#include <stdio.h>
int main()
{
	int* p=NULL;
	if (p==0) {//不推荐这种
		;
	}
	return 0;
}

如果我们直接在if判断中,判断一个指针变量的时候,即使我们知道,指针变量指向的空间是NULL并且这个NULL是0的强值类型转化为void*的,那我们也不能直接用指针变量做判断。

#include <stdio.h>
int main()
{
	int* p=NULL;
	if (p) {//不推荐这种
		;
	}
	return 0;
}

我们写的时候要让指针变量与NULL进行比较,并且要写成指针等于NULL这样来比较,这样就算我们少写一个=号的时候,我们也会语法报错提醒我们

#include <stdio.h>
int main()
{
	int* p=NULL;
	if (NULL==p) {//推荐
		;
	}
	return 0;
}

if与else匹配问题

在C语言中if与else的匹配问题是遵循就近原则的,else跟距离自己最近的if进行匹配!

#include <stdio.h>
int main()
{
	int x = 10;
	int y = 20;
	if (x == 19)
		if (y == 20)
			printf("Hello World\n");
		else
			printf("NO NO NO\n");
	return 0;
}

06c248f271d84f6cba90fdba58d6f6a1.png

我们为了养成良好的代码规范,在使用if与else语句的时候,一定要加括号,让代码块的作用域明确!

#include <stdio.h>
int main()
{
	int x = 10;
	int y = 20;
	if (x == 19) {
		if (y == 20) {
			printf("Hello World\n");
		}
	}
	else
	{
		printf("NO NO NO\n");
	}
	
	return 0;
}

44fc966c809e422ba1db9a923328628b.png

if与分号问题

因为我们if只能跟一条语句,如果后面跟的是;,分号也算一条语句是空语句,它就会认为是跟的那条语句,当条件成立的时候我们就会执行;空语句。

代码示例:

#include <stdio.h>
int main()
{
	int flag = 0;
	if (flag);
	{
		printf("Hello world");
	}
	return 0;
}

2564caad894a4c4bb39d1d100d2a48a6.png

第八个关键字switch case(多条件匹配)

switch匹配的机制是整型匹配,简单的理解switch与case所匹配的值只能是整数.

switch基本语法结构

switch(整型变量/常量/整型表达式)
{
    case var1:
        break;
    case var2:
        break;
    case var3:
        break;
    default:
        break;
}

其实switch与if语句的作用是雷同的,但是我们if语句能够判断的种类和条件是更丰富的,switch却只能进行整数判断。

如何使用switch和if

第一种情况:如果我们写的代码多条件的匹配和分支用switch搞不定,那我们只能用if。

第二种情况:如果判断的种类并不是特别多的时候, 我们推荐直接用if。

第三种情况:如果我们发现判断分支就是swittch而且是整型的判断,判断条件特别多,那我们就用switch。

其实switch关键字本身并没有判断能力,更没有任何分支能力!switch的本质其实是拿着右边圆括号的(整型变量/常量/整型表达式)依次进行匹配。switch当中的case执行的判断能力,如果我们csae语句的后面没有break,我们发现想匹配的case语句会从上而下一直执行,直到遇见break跳出!

#include <stdio.h>
int main() {
	int choice = 2;
	switch (choice) {
	case 1:
		printf("选择了第一项\n");
	case 2:
		printf("选择了第二项\n");
	case 3:
		printf("选择了第三项\n");
	case 4:
		printf("选择了第四项\n");
		break;//遇到break跳出
	case 5:
		printf("选择了第五项\n");
	}
	return 0;
}

983fdb48563248f5990664623a73313e.png

我们通过代码可以发现,从case匹配上后它就会依次往下执行,直到遇见break跳出,所以break的功能相当于分支功能,让我们明确了解每个case的语句分支。case完成的就是我们的判断的功能。

任何具有判定功能的语法结构,都必须具备:判断+分支,case本身是用来判定的,break用来进行分支功能。

switch有什么作用呢?

switch我们可以理解为对多条件判定的笼统的语法结构,它拿到圆括号中的(整型变量/常量/整型表达式)值,依次与case进行匹配.switch本身没有判断能力,需要借助case与break进行判定功能与分支功能。

注意:每个case语句的结尾绝对不要忘了加break,否则将导致多,个分支重叠(除非有意使多个分支重叠)。

default语句

如果我们一旦switch与case匹配不上,就会不执行代码就会什么都没有,什么都没有对我们的软件使用者来讲并不是一个很好的体验,因为我们并不知道,我们是执行正确了,还是执行错误了,就算报错了我们也要知道是什么原因报错的。所以在我们多条件判定情况没有匹配成功,我们switch代码块,也就是case最后建议加上default!y,意味着以上条件都没有匹配成功,就执行default

#include <stdio.h>
int main() {
	int choice = 9;
	switch (choice) {
	case 1:
		printf("选择了第一项\n");
		break;
	case 2:
		printf("选择了第二项\n");
		break;
	case 3:
		printf("选择了第四项\n");
		break;
	case 4:
		printf("选择了第五项\n");
		break;
	case 5:
		printf("选择了第六项\n");
		break;
	default:
		printf("没有匹配!!!\n");
		break;
	}
	return 0;
}

4a8b53f3c3cb4b8eac822ebf842b8559.png

最后必须使用default分支。即使程序真的不需要default处理,也应该保留以下语句。

	default:
	{
		break;
	}
	

case语句

switch与case匹配成功后,执行对应的case语句,case与break之间是可以执行多行代码的

#include <stdio.h>
int main() {
	int choice = 1;
	switch (choice) {
	case 1:
		printf("选择了第一项\n");
		printf("选择了第一项\n");
		printf("选择了第一项\n");
		printf("选择了第一项\n");
		break;
	case 2:
		printf("选择了第二项\n");
		break;
	case 3:
		printf("选择了第四项\n");
		break;
	case 4:
		printf("选择了第五项\n");
		break;
	case 5:
		printf("选择了第六项\n");
		break;
	default:
		printf("数据有误,请重新赋值!");
		break;
	}
	return 0;
}

95ce46cd3b764cfe8fa6cbd2fe227ea4.png

但是case与brak之间不能定义变量,语法上会报错

e5e8f74640ae4565b18273d33cca7a17.png我们要在case与break之间的代码加上花括号,以证明它是一个完整的代码块,就可以了。

a26d4c6c799e4d43ac194ef58a120218.png

其实我们写代码,并不建议在case中定义变量,如果真的想这样来写,我们可以把case封装成函数,我们封装完函数后,在case匹配的时候,我可以对函数进行调用

#include <stdio.h>
void fun()
{
	int a = 10;
	printf("选择了第一项\n");
	printf("选择了第一项\n");
	printf("选择了第一项\n");
	printf("选择了第一项\n");
	printf("%d\n", a);
}
int main() {
	int choice = 1;
	switch (choice) {
	case 1:
	{
		fun();//封装函数,调用

	}
	break;
	case 2:
		printf("选择了第二项\n");
		break;
	case 3:
		printf("选择了第四项\n");
		break;
	case 4:
		printf("选择了第五项\n");
		break;
	case 5:
		printf("选择了第六项\n");
		break;
	default:
		printf("数据有误,请重新赋值!");
        break;

	}
	return 0;
}

439a310ca9cc41ea9039e2b7de7ad155.png

这样写case语句会表得更加清楚,所以在case语句和break之间不能定义变量,定义变量就会报错,决定方案就算在case和break之间加花括号,明确作用域范围。或者直接封装函数进行调用。

多条case执行同一条语句

推荐写法:

#include <stdio.h>
int main() {
	int choice = 1;
	switch (choice) {
	case 1:
	case 2:
	case 3:
		printf("第1项-第3项\n");
		break;
	case 4:
	case 5:
		printf("第4项-第5项\n");
		break;
	default:
		printf("数据有误,请重新赋值!");
        break;
	}
	return 0;
}

478855d810d64e7084f2b0bb873a4964.png

default语法上是不是只能放在最后?

我们放在中间看一下,当case都没有匹配上的时候就会走default

#include <stdio.h>
int main() {
	int choice = 10;
	switch (choice) {
	case 1:
		printf("选择了第一项\n");
		break;
	case 2:
		printf("选择了第二项\n");
		break;
	case 3:
		printf("选择了第三项\n");
		break;
	default:
		printf("数据有误,请重新赋值!");
		break;//一定要加break;
	case 4:
		printf("选择了第四项\n");
		break;
	case 5:
		printf("选择了第五项\n");
		break;
	}
	return 0;
}

3c3d34f871ef408dae8e2b244fe474f2.png

我们放在开头的位置看一下,当case都没有匹配上的时候就会走default

#include <stdio.h>
int main() {
	int choice = 1;
	switch (choice) {
	default:
		printf("数据有误,请重新赋值!");
		break;
	case 1:
		printf("选择了第一项\n");
		break;
	case 2:
		printf("选择了第二项\n");
		break;
	case 3:
		printf("选择了第四项\n");
		break;
	case 4:
		printf("选择了第五项\n");
		break;
	case 5:
		printf("选择了第六项\n");
		break;
	}
	return 0;
}

36f3c73cf8884862b8854519ee693b15.png

所以default放在siwtch的语句中任何位置都是可以的,只不过我们习惯性的把default放在最后。因为这样更符合语义表述。

switch如何实现多组输入?

我们可以使用while循环包括switch语句,定义一个变量让他初始化为0,我们while语句的判断条件设置成取反该变量,我们再给想退出的case语句中让该变量初始化为1,这样我们再次进入判断的时候它就会由原本的1,取反为0实现退出while循环。

#include <stdio.h>
int main() {
	int choice = 1;
	int quit = 0;
	while (!quit)
	{
		scanf("%d", &choice);
		switch (choice) {
		default:
			printf("数据有误,请重新赋值!");
			break;
		case 1:
			printf("选择了第一项\n");
			break;
		case 2:
			printf("选择了第二项\n");
			quit = 1;//取反后 退出
			break;
		case 3:
			printf("选择了第四项\n");
			break;
		case 4:
			printf("选择了第五项\n");
			break;
		case 5:
			printf("选择了第六项\n");
			break;
		}
	}
	
	return 0;
}

950e1be46e1045f5b0f71600b7f0cf35.png

case后面的值有什么要求吗?

如果我们用const修饰变量,让该变量成为常变量后可以在switch圆括号内与case进行匹配吗?

#include <stdio.h>
int main()
{
	const a = 10;
	switch (a)
	{
	case 10:
		printf("Hello world\n");
		break;
	default:
			break;
	}
	return 0;
}

很显然是可以的,但是我们不可以csae使用常变量与switch圆括内进行匹配

case可以用宏定义常量

宏定义的常量本身就算常量的一种,是可以用在case进行匹配的。

#define A 10
#include <stdio.h>
int main()
{
	switch (A)
	{
	case A:
		printf("Hello world\n");
		break;
	default:
		break;
	}
	return 0;
}

而且define又被曾为宏替换,它实际上用A替换了10,所以我们switch圆括号中如果是变量与常量进行匹配也是可以的。我们要注意的是switch圆括号内对常量或者变量没有要求,case匹配的时候只能用常量!

#define A 10
#include <stdio.h>
int main()
{
	int a = 10;
	switch (a)
	{
	case A:
		printf("Hello world\n");
		break;
	default:
		break;
	}
	return 0;
}

case语句排列顺序

1. 按字母或数字顺序排列各条case语。如果所有的case语句没有明显的重要性差别,那就按A-B-C或1-2-3等顺序排列case语。这样做的话,你可以很容易地找到某条case语句。

比如:

#include <stdio.h>

int main() {
    char grade = 'B';

    switch (grade) {
    case 'A':
        printf("优秀\n");
        break;
    case 'B':
        printf("良好\n");
        break;
    case 'C':
        printf("中等\n");
        break;
    case 'D':
        printf("及格\n");
        break;
    case 'F':
        printf("不及格\n");
        break;
    default:
        printf("无效等级\n");
        break;
    }

    return 0;
}

2. 在C语言中,编写switch语句时,将正常情况放在前面,异常情况放在后面,是一种良好的编程习惯。这样做可以提高代码的可读性,并帮助其他开发者或未来的你理解哪些是预期内的分支,哪些是处理意外情况的分支。以下是一个示例:

#include <stdio.h>

int main() {
    int input = 3; // 假设这是用户输入的值

    switch (input) {
    case 1:
        // 正常情况:处理用户输入1的情况
        printf("处理选项1\n");
        break;
    case 2:
        // 正常情况:处理用户输入2的情况
        printf("处理选项2\n");
        break;
    case 3:
        // 正常情况:处理用户输入3的情况
        printf("处理选项3\n");
        break;
        // ... 可以继续添加更多正常情况的case

        // 异常情况处理
    case -1:
        printf("异常。。。。");
        break;
    default:
        // 异常情况:处理非预期的输入
        printf("无效的输入,请输入1到3之间的数字。\n");
        break;
    }

    return 0;
}

3. 在C语言中,将switch语句中的case语句按照执行频率排列是一种优化代码执行效率的做法。将最常执行的情况放在前面可以减少程序在switch语句中的平均查找时间,尤其是在case语句较多时,这种优化可以带来明显的性能提升。以下是按照执行频率排列case语句的示例:

#include <stdio.h>

int main() {
    int option = 2;/* 用户输入或程序逻辑决定的值 */;

    switch (option) {
    case 2:
        // 最常执行的情况
        printf("处理最常见的情况2\n");
        break;
    case 1:
        // 较常执行的情况
        printf("处理较常见的情况1\n");
        break;
    case 3:
        // 较少执行的情况
        printf("处理较少见的情况3\n");
        break;
    case 4:
        // 最不常执行的情况
        printf("处理最不常见的情况4\n");
        break;
    default:
        // 异常情况处理
        printf("无效的选项\n");
        break;
    }

    return 0;
}

第九个关键 - while循环

在C语言循环中要遵循循环三要素,循环条件初始化,循环条件判定,循环条件更新。while循环中的循环判定条件和循环条件更新是分离的,如果代码块相隔较大如果忘记循环条件更新。

while循环语法结构:

循环条件初始化
while( 循环条件判定)
{
	代码块
	循环条件更新
}

 代码示例:

#include <stdio.h>
int main()
{
	int i = 0;//循环条件初始化
	while (i < 10)//循环条件判定
	{
		printf("%d\n", i);
		i++;//循环条件更新
	}
	return 0;
}

while循环死循环实现

如果while循环中没有循环条件初始化和循环条件更新的话,while判定条件为真,就会变成死循环。

#include <stdio.h>
int main()
{
	while (1)//循环条件判定
	{
		printf("%d\n", 666);
	}
	return 0;
}

第十个关键字 - for循环

在C语言循环中要遵循循环三要素,循环条件初始化,循环条件判定!循环条件更新,for的循环条件初始化,循环条件判定,循环条件更新是在一套结构中来完成的,这种语法形式对我们来讲语法形式是比较紧凑的,所以循环三要素我们可以一眼看到。推荐使用for循环

for循环语法结构:

for (循环条件初始化; 循环条件判定; 循环条件更新)
{
	代码块
}

 代码示例:

#include <stdio.h>
int main()
{
	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", i);
	}
	return 0;
}

for循环实现死循环

for循环实现死循环的话我们可以循环条件初始化,循环条件判定!循环条件更新都省略掉,但是要保留两个分号。

#include <stdio.h>
int main()
{
	for(;;)
	{
		printf("%d\n", 666);
	}
	return 0;
}

当然我们循环条件为真,把循环条件初始化和循环条件更新省略也是可以实现死循环

#include <stdio.h>
int main()
{
	for(;1;)
	{
		printf("%d\n", 666);
	}
	return 0;
}

第十一个关键字 - do while循环

在C语言循环中要遵循循环三要素,循环条件初始化,循环条件判定,循环条件更新!do while循环的语法结构是先执行代码块,执行完代码块再进行判定。

do while循环的语法结构:

循环条件初始化
do
{
	代码块
	循环条件更新
} while(循环条件判定);//分号不能少

do while循环代码示例:

#include <stdio.h>
int main()
{
	int i = 10;
	do {
		printf("%d\n", i);
		printf("%d\n", i);
		printf("%d\n", i);
	} while (0);
	return 0;
}

我们通过代码执行的结果可以分析得出,do while循环是先执行代码块,执行完后再进入while判断,因为我们的while循环的条件是0,按理说是假不执行的,但do while的特性就是先执行后判断。所以至少执行一次!

什么场景下要使用do while循环?

比如说我们玩游戏,只要登录上某款游戏,理论上我们先玩一局,玩完之后再进行条件更新。简单的说就是先做一次,尝试做一次后,再进行条件判定,这样的场景都适合do while语法结构。

do while实现死循环

do while想要实现死循环直接在循环条件判定为真,就可以实现。

#include <stdio.h>
int main()
{
	int i = 10;
	do {
		printf("%d\n", i);
		printf("%d\n", i);
		printf("%d\n", i);
	} while (1);
	return 0;
}

什么是默认三流?

任何C程序,在默认编译运行后,运行的时候,都会打开三个输入输出流(C++,python,java都会默认打开)

流:C/C++高级语言中对于输入输出一种抽象化描述,其实可以简单的理解成往一个文件当中或者往某种结构当中写入对应的数据,对应的数据就会以某种方式保存起来要么显示出来。那默认打开哪几个呢?

stdin:标准输入,FILE* stdin,键盘

stdout:标准输出 FILE* stdout, 显示器

stderr:标准错误  FILE* stderr,显示器

所以我们文件打开就会默认打开stdin:标准输入,stdout:标准输出,stderr:标准错误,他们的类型都是FILE*文件指针,我们在读写文件之前都需要打开文件,我们程序执行的时候,我们的系统就已经默认打开了三个输入输出文件stdin:标准输入,stdout:标准输出,stderr:标准错误!

getchar函数获取字符 

getchar使用细节1:

在使用getchar函数的时候,一定不要忽略了按回车的\n,像下面这个代码我们通过getcahr()获取一个字符的同时我们按下回车,相当于电脑读取了一个\n,加上本身printf中有\n换行,所以会呈现出换了两行.

我们通过调试可以发现一个规律,就是我们使用getcahr的时候是从键盘上把输入到缓存区,我们每一次执行while循环的时候,都会让getchar从缓存区获取一个字符。

#include <stdio.h>
int main()
{
	while (1)
	{
		int c = getchar();
		if (c == '#')
		{
			break;
		}
		printf("%c\n", c);
	}
	printf("while end...\n");
	return 0;
}

想要他实现,每次换行都是一行的话,我们直接把printf函数中的\n删除掉就好

#include <stdio.h>
int main()
{
	while (1)
	{
		int c = getchar();
		if (c == '#')
		{
			break;
		}
		printf("%c", c);
	}
	printf("while end...\n");
	return 0;
}

getchar使用细节2:

getchar返回值是一个整型,它如果是一个char类型它能表示的就是0-255这些字符在 ASCLL码表中都是合法的ASCLL码值,ASCLL表有两个表,一个是基本表,一个是扩展表.所以getchar获取成功必定是一个有效字符,但是如果getchar获取的值,获取失败它是无法表示出来,所以getchar函数返回值是int类型.

getchar使用细节3:

通过键盘输入显示的所有内容或者往显示器中打印的内容全部都是字符!!!

拿printf函数举个例子,printf函数的返回值就是输出在屏幕上字符的个数.printf函数的返回值也是int类型.

#include <stdio.h>
int main()
{
	int ret = printf("Hello");
	printf("\n");
	printf("%d", ret);
	return 0;
}

我们使用scanf输入字符就是输入每个字符,它通过格式化转化成对应的数据,这也是为什么scanf叫做格式化输入函数,printf叫做格式化输出函数,它在把对应的字符,或者字符串格式化对应的整型或者浮点数.就像我们通过sncaf 函数输入一个3.14这样的数字其实是3字符点字符1字符4字符,当按下回车之后这个值才被格式化输入到a当中

#include <stdio.h>
int main()
{
	double a;
	scanf("%lf", &a);
	printf("%lf", a);
	return 0;
}

换句话说,我们今天输入的内容全部都是字符,既然全部都是字符那我们的格式化输出便具备了一个格式化的含义,它的作用就是把我们的字符转化成数据.键盘显示器这类设备我们都称为字符设备.在我们C语言当中进行"格式化"的转化.我们把视线回到getchar,此时下面的这个代码当我们输入1234的时候,输出的结果也是1234但是它并不是一个数字,而是一个字符.

#include <stdio.h>
int main()
{
	while (1)
	{
		int c = getchar();
		if (c == '#')
		{
			break;
		}
		printf("%c", c);
	}
	printf("while end...\n");
	return 0;
}

我们为了打印出来比较容易查看,给每个字符输出的时候,都加个空格,所以我们输入的数据是字符,打印出来的数据依旧是字符.它没有像scanf进行"格式化"这种操作.

#include <stdio.h>
int main()
{
	while (1)
	{
		int c = getchar();
		if (c == '#')
		{
			break;
		}
		printf("%c ", c);
	}
	printf("while end...\n");
	return 0;
}

第十二个关键字break-跳出循环

我们通过代码可以发现,我们输入在缓冲区的数据,getcahr会在每次while的时候获取一个字符,当获取到#的时候,while循环体中的if判断成立,执行break直接跳出循环。

#include <stdio.h>
int main()
{
	while (1)
	{
		int c = getchar();
		if ('#' == c)
		{
			break;//跳出循环,结束循环
		}
		putchar(c);
	}
	printf("\nbreak跳出循环\n");
	return 0;
}

如果if判断成立,getchar在缓冲区内获取到一个#字符,If判断就会成立,执行break,结束当前循环,break也是跳出循环的意思。

#include <stdio.h>
int main()
{
	while (1)
	{
		int c = getchar();
		if ('#' == c)
		{
			break;//跳出循环,结束循环
		}
		putchar(c);
	}
	printf("\nbreak跳出循环\n");
	return 0;
}

第十三个关键字-continue(结束当前循环)

continue跳出当前循环并且再次进入循环,所谓的再次循环就是再次执行条件判定,条件成立则再次进入循环语句。

while中出现continue

下面代码if成立的情况下,执行里面的continue,从continue直接再次进入whlie判定。

do while中出现continue

 当continue出现在do while循环中,我们执行了contine后,程序会跳出当前do while循环体,再次进入do while的判定中。

 代码演示一下:

#include <stdio.h>
int main()
{
	int i = 0;
	do
	{
		i++;
		if (i == 3) {
			continue;
		}
		printf("%d\n", i);
	} while (i<5);
	return 0;
}

for循环中出现continue

注意我们for循环中执行了continue程序会执行for循环的条件更新!

 用代码演示给大家看一看

#include <stdio.h>
int main()
{
	for (int i = 0; i <= 5; i++)
	{
		if (i == 3)
		{
			continue;
		}
		printf("%d\n", i);
	}
	return 0;
}

for循环“半闭半开”写法

在C语言的for循环中,提到“半闭半开”通常是指循环的迭代范围,也可以叫做前闭后开。具体来说,它意味着循环的起始条件是包含在内的(闭),而结束条件是不包含在内的(开)。

for (int i = 0; i < n; ++i) {
    // 循环体
}

这里的“半闭半开”体现在:

  • “闭”部分:循环开始时,i等于0,这个值是包含在循环体内的,即循环体至少会执行一次,除非n小于或等于0。
  • “开”部分:循环的条件是i < n,这意味着当i等于n时,循环不会执行。因此,n这个值是不包含在循环体内的。

因此,如果n是正整数,循环将执行n次,但循环变量i的值将是从0到n-1,这是一个典型的半闭半开区间 [0, n)

第十四关键字goto(跳转)

在C语言中,goto 语句提供了一种无条件跳转到程序中指定标签的能力。goto修饰的自定义标识符被称为标签,当程序执行到goto修饰的标签后,会执行跳转相同的标签去执行代码块,goto语句不能跳转函数和文件!

goto语句向下跳转

#include <stdio.h>
int main()
{
	goto hello;
	printf("星期一\n");
	printf("星期二\n");
	printf("星期三\n");
	hello:
	printf("星期四\n");
	printf("星期五\n");
	printf("星期六\n");
	printf("星期日\n");
	return 0;
}

goto语句向上跳转

#include <stdio.h>
int main()
{
	hello:
	printf("星期一\n");
	printf("星期二\n");
	printf("星期三\n");
	goto hello;
	printf("星期四\n");
	printf("星期五\n");
	printf("星期六\n");
	printf("星期日\n");
	return 0;
}

第十五关键字void(空)

void可以修饰变量吗?void大小是多少?

Dev C++中的void大小

vs2022中void大小 

不可以,定义变量的本事是开辟空间,而void作为空类型,理论上不应该开辟空间,即使开辟了空间,也仅仅作为一个占位符看待,所以,既然无法开辟空间,那么也就无法作为正常变量使用,既然无法使用,编译器干脆不让他定义变量!

void是空类型,那么在内存当中开辟多大的空间都是不确定的,void本身就被编译器解释为空类型,强制的不允许定义变量,虽然可能有大小,但是不能用来定义变量。所以vs2022编译器当前是0字节没有办法开辟空间是可以理解的,但是跨平台演示的时候void是1大小已经确定了,void对应的变量也可以开辟编译器会直接报错,在Dev C++当中void是明确大小的可以理解成是编译器级别的约定!void本身就被编译器解释为空,强制的不允许定义变量!

void最大的价值是什么?

告诉编译器我是一种空类型,既然是空类型,我们一定是可以有一些特殊情况去使用。

强制类型转换为void

把10强转为void之后,一定是把值复制到变量a的空间里面,而void本身也是作用占位符去使用的,也就是void不能用来定义变量,同事也不能用来解释数据,所以我对我们来讲void不能强转。

void修饰函数返回值和参数

大概分为五种情况,情况五比较推荐!

情况一: 如果自定义函数的返回类型使用void类型,但是函数有使用return进行返回时,我们在调用处接收此函数的返回值,代码会语法上报错,因为我们自定义函数的返回值是void

#include <stdio.h>
void fun()
{
	printf("hello\n");
	return 1;
}
int main()
{

	int a =fun();
	printf("%d\n", a);
	return 0;
}

情况二:但是如果我们在调用时不使用变量来接收函数的返回值的话,那语法上是不会报错的

情况三:C语言函数可以不带返回值

情况四:自定义函数无返回值,通过return返回一个整数,调用处用变量接受函数的返回值,并输出

#include <stdio.h>
fun()
{
	printf("hello\n");
	return 5;
}
int main()
{

	int a =fun();
	printf("%d\n", a);
	return 0; 
}

情况五:void修饰函数返回值让用户明确不需要返回值,告知编译器无法接收。

#include <stdio.h>
void fun()
{
	printf("hello\n");
	return 5;
}
int main()
{

	int a =fun();
	printf("%d\n", a);
	return 0; 
}

void 作为函数参数

void充当函数的形参列表:告知用户或者编译器,该函数不需要传参。需要注意是不同的编译器当中编译器语法审核可能不同。

#include <stdio.h>
int fun(void)
{

	return 5;
}
int main()
{
	fun(1,2,3,4);
	return 0; 
}

void指针        

void*可以被任何类型的指针接收,void*也可以接收任意指针类型(常用)

#include <stdio.h>
int main()
{
	void* p = NULL;
	int* a = NULL;
	double* b = NULL;
	a = p;
	b = p;

	p = a;
	p = b;
	return 0;
}

void * 定义的指针变量可以进行运算操作吗

#include <stdio.h>
int main()
{
	void* p = NULL;
	p++;
	p--;
	return 0;
}

在vs2022编译器中void是内存是0,本质上是以占位符的作用去使用的,让指针向前移动或者向后移动若干字节,就要明确出来向前向后是几个字节,void*大小是不能确,所以说就不能进行因为void*的大小是0的时候加减是没有意义的,编译器会直接终止。

但是在Dev C++中,编译是可以通过的。因为void*的大小是0的时候加减是没有意义的。但是Dev C++ 所以向前和向后的小大对应的结果是明确的。

void*指针可以直接解引用吗?

C语言是不能对void*的指针进行解引用

函数内定义的数组是在栈上开辟的空间,它本质上其实是一份临时空间,函数调用的时候,其实就是在栈上开辟空间,函数调用完毕栈的空间就会被释放掉,包括临时变量这些数据。所以输出的时候是一个乱码

#include <stdio.h>
char* show()
{
	char str[] = "hello ";
	return str;
}

int main()
{
	char* s = show();
	printf("%s", s);	
	return 0;
}

C语言有没有字符串类型?

C语言没有字符串类型,但是有字符串!字符串的值叫做字面值,不能直接使用的原因是不能定义名字,因为对应的类型。这字符串类型和字符串两个概念不一样,java,C++都有字符串类型,C语言是有字符串,但是没有字符串类型。

C语言中,字符串通常是通过字符数组(char数组)来实现的,并以空字符('\0')作为字符串的结束标志。字符串中的\0并不是字符串内容的部分,只是字符串结束标记位。

字符数组:字符串可以存储在字符数组中。        

char str[] = "Hello, World!";

指针:字符串也可以通过字符指针来引用。

char *str = "Hello, World!";

第十六个关键字return

我们在学习return关键字之前呢,需要先学习一些预备知识点,来拓展一下知识面,从而让我们更好的理解return关键字

计算机中释放空间是将数据全部清0吗?

情况数据并不是把数据全部清0,而是通过某种方式设置该数据无效即可。举个例子,比方说小明是一个开发商,建造了一个楼房,当需要拆除的时候,只需要写一个拆字,就可以拆。

  1. 建造楼房:当小明建造楼房时,这相当于在计算机系统中分配内存并存储数据。

  2. 写一个拆字:在楼房上写一个“拆”字,相当于在内存中设置一个标记或标志,表明这个数据块不再有效或不再使用。在计算机术语中,这通常是通过修改元数据来实现的,比如在文件系统中设置文件的删除标记。

  3. 拆除楼房:实际的拆除动作,相当于计算机系统中释放内存的过程。在设置“拆”字之后,楼房(数据块)并没有立即被拆除(清空),而是等到实际执行拆除(内存清理)时,才会物理上移除楼房(数据)。

  4. 标记与实际清除:在计算机中,设置数据无效通常是通过修改位字段或状态标志来完成的,这是一个非常快速的操作。而实际清除数据,即物理上擦除数据,则是一个更慢的过程,可能涉及到多次写入操作来确保数据无法恢复。

  5. 重用:在小明的例子中,拆除楼房后,这块地皮可以用来建造新的楼房。在计算机中,释放并标记为无效的内存块可以被重新分配并用于存储新的数据。

函数的栈帧是什么?

C语言定义的临时变量,以及函数或者代码块都是在栈区开辟空间。栈区向下增长,地址减少的方向增长。堆区是向上增长,地址增大方向。

调用函数,形成栈帧。函数返回,释放栈帧。所以的释放栈帧仅仅表明该空间是无效的,无效的意思是空间是可被覆盖的

#include <stdio.h>
char* show()
{
	char str[] = "hello ";
	return str;
}

int main()
{
	char* s = show();
	printf("%s", s);	
	return 0;
}

函数栈帧被释放

在调用printf函数之前之前的字符串字面值还在的原因是计算机并没有情况数据,虽然数据是无效的但是我们依然还是可以看到。

函数栈帧被覆盖

因为pintf也是函数,printf也要循序调用printf就要形成栈帧,返回printf就要释放栈帧。它就会覆盖之前无效的空间。也就是覆盖无效的栈帧结构,所以我们字符串也就不复存在了。

printf函数调用后会重新形参栈帧,覆盖show留下的栈帧空间!因为show函数返回后,该空间标记无效!

函数调用时如何形成足够的栈帧空间?

实际上自定义函数,虽然并没有被真正的调用,但实际上编译器在编译这段代码的时候,尤其是遇到函数时,它其实通过核算关键字,来预估出这个函数将来要用多大的空间.这也是我们可以用sizeof求变量的内存大小,因为编译器认识,所以关键字就是给编译器预估空间与开辟空间用的

为什么临时变量具有临时性?

栈帧结构在函数调用的完毕,需要被释放.释放栈帧仅仅表明该空间是无效的,无效的意思是空间是可被覆盖的.临时变量又是在栈帧空间开辟的,所以具有临时性.

return语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。

reruen关键字的最终奥秘

函数的默认返回值,虽然函数内定义变量默认返回的时候,这个变量是临时变量,临时变量就具有临时性,函数如何把临时变量返回到外部调用处呢?本质是通过寄存器来进行返回的.当我们实际调用return但是外部没有接收时,照样会生成同样的汇编.,对接收方来讲有对应的接收语句会形成对应的接收汇编,如果没有的话程序则继续往下执行.

1.函数返回值的基本概念

当你调用一个函数并使用 return 语句返回某个值时,函数会把值传递给调用者。

如果返回值是一个临时变量(比如在函数内定义并返回的变量),C 语言的调用约定通常是通过寄存器或栈将该值传递出去

2.返回值如何传递?

返回值类型:对于简单的数据类型(如 intfloat 等),大多数平台和编译器约定使用 寄存器 来传递返回值。比如,在 x86 架构中,通常使用 EAX 寄存器来返回 int 类型的值。

临时变量:当你在函数中创建临时变量并通过 return 返回时,编译器会将该变量的值存放在合适的寄存器中(如果它的类型适合寄存器的大小)。即使这个变量是临时的,函数的返回机制依然会保证它的值能够从函数传递到调用者。

返回语句return 语句会把变量的值(或表达式的计算结果)直接通过寄存器返回给调用者。临时变量的生命周期在函数返回后结束,但是它的值已经成功地传递给了调用者。

3.临时变量的生命周期

临时变量是 "临时的",这指的是这些变量在函数调用期间存在,它们的作用域在函数结束时就结束。临时变量的值会被返回,但它本身在栈上分配,函数返回后该栈帧会被销毁。问题是:如果临时变量是 "临时的",它如何确保能返回给调用者?

通过寄存器:通常情况下,临时变量的值是直接放入寄存器的,而寄存器的值在函数退出时依然可以传递给调用者。这就是为什么我们在汇编代码中看到 mov eax, value 这样的操作,其中 eax 寄存器被用来存放返回值。

4.返回值没有接收时的行为

如果函数的返回值没有被接收(比如调用时没有将返回值赋给一个变量),那么函数依然会返回一个值,且它会通过寄存器或栈传递。但由于没有接收这个返回值,程序就会继续执行下去。

即使没有接收返回值,return 语句依然会生成与接收有返回值的情况相同的汇编代码。换句话说,函数调用的返回值存储在寄存器中,而不管接收方是否存在,它都会按同样的方式执行。

5. 汇编代码的例子

 假设你有如下 C 代码:

int add(int a, int b) {
    int result = a + b;
    return result;
}

编译器会将 add 函数的返回值存放在寄存器中。假设我们在 x86 架构上,编译器可能生成类似这样的汇编代码:

add:
    ; 函数体
    mov eax, [esp+4]  ; 获取参数 a
    add eax, [esp+8]   ; 将 b 加到 a 上
    ; eax 现在存储了 result 的值
    ret                 ; 返回,并通过 eax 传递返回值

在上面的代码中,返回值是通过 eax 寄存器传递的。无论调用者是否接收它,eax 都会持有返回值。

如果没有接收返回值,调用方的汇编代码可能看起来像这样:

call add
; 之后的代码继续执行,不会使用返回值

在这种情况下,返回值仍然通过 eax 寄存器返回,只是调用方没有做任何处理。

6. 接收返回值的情况

如果你将返回值接收在一个变量中,编译器会为此生成代码来将寄存器中的返回值存储到该变量的内存位置。例如:

int result = add(3, 4);

对应的汇编代码可能是:

call add
mov [esp+4], eax  ; 将 eax 中的返回值存入 result

 esp eax mov call ret这些是什么?

1. esp(Extended Stack Pointer)

  • esp 是 扩展栈指针(Extended Stack Pointer)的缩写。它是一个寄存器,指向当前栈顶的地址。在 x86 架构中,栈是用来存储局部变量、函数参数、返回地址等数据的内存区域。
  • 每当函数调用时,栈指针 esp 会发生变化,通常会向下移动(栈是向下生长的)。esp 指向当前栈顶,也就是最近被压入栈的内容的位置。
  • 栈是计算机系统中非常重要的部分,esp 是用来管理栈的寄存器。

例如:

mov eax, [esp+4]  ; 从栈中加载参数

mov eax, [esp+4]  ; 从栈中加载参数

2. eax(Extended Accumulator Register)

  • eax 是 扩展累加寄存器(Extended Accumulator Register),是 x86 架构中的一个 32 位寄存器。eax 在汇编中通常用于算术操作、存储函数返回值等。
  • 在许多系统调用或函数返回中,eax 被用来存放返回值。比如,在你调用一个函数后,返回的值通常会存储在 eax 中。

例如:

mov eax, 5  ; 将 5 存入 eax 寄存器

3. mov(Move)

  • mov 是汇编中的 数据传送指令,用于将数据从一个地方复制到另一个地方。
  • mov 不会修改数据,只是把一个值传递到另一个寄存器或内存地址。它是汇编语言中最常用的指令之一。

例如:

mov eax, 10     ; 将 10 移动到 eax 寄存器
mov [ebx], eax  ; 将 eax 寄存器中的值移动到内存地址 [ebx] 指向的地方

4. call(Call)

  • call 是汇编中的 调用函数指令,它用于跳转到另一个函数(或子程序)并保存当前指令的返回地址,以便函数执行完后能够返回到调用位置。
  • 当执行 call 时,CPU 会先把当前指令的下一条指令地址压入栈中,然后跳转到目标函数的地址去执行。

例如:

call some_function  ; 调用名为 some_function 的函数

5. ret(Return)

  • ret 是 返回指令,它用于从函数返回到调用函数的地方。ret 指令会从栈中弹出保存的返回地址,并跳转到这个地址。
  • ret 是与 call 配对使用的指令,它确保程序在执行完函数后能够返回到调用位置继续执行。

例如:

ret  ; 从当前函数返回

当执行 ret 时,CPU 会从栈中弹出一个地址,这个地址是通过 call 指令保存的,指向函数调用结束后应该继续执行的代码。

总结:

  • esp:栈指针寄存器,指向栈顶。
  • eax:累加寄存器,用来存储数据,通常用来存储函数的返回值。
  • mov:数据传送指令,将数据从一个位置传递到另一个位置。
  • call:函数调用指令,跳转到指定地址并保存返回地址。
  • ret:函数返回指令,从栈中弹出返回地址并跳转到该地址。

这些指令和寄存器是 x86 汇编语言的核心部分,它们负责控制程序的流动、数据的存取以及函数调用的管理。

第十七个关键字const

const修饰的变量,不可以“直接”被修改

const可以放在类型之前,也可以放在类型之后

#include <stdio.h>
int main()
{
	//const可以放在类型之前,也可以放在类型之后
	const int a = 10;
	int const b = 20;
	return 0;
}

coust修改的变量可以间接被修改

#include <stdio.h>
int main()
{
	//const可以放在类型之前,也可以放在类型之后
	const int a = 10;
	int* p = (int*) & a;//(int*)使左右两边类型一致
	*p = 20;
	printf("%d", *p);
	return 0;
}

那count修饰变量,有什么意义?

有两点,第一点是:const修改变量是给编译器去看的,让编译器在编译代码时对用const修饰的变量进行语法检查,对直接修改const修饰变量,有修改行为的话编译器会直接报错,能将错误提前发现,证明代码是一个优质的代码。第二点是:告诉其他程序员(正在改你代码或者阅读你代码的)这个变量后面不要改哦。也属于一种“自描述”含义。

字符串常量真正意义上不可被修改

字符串常量真正意义上的不可被修改并不是C语言给我们提供的,而是操作系统级别上为我们的代码做的保护,我们想要深入学习,需要了解操作系统关于这部分的知识点。

#include <stdio.h>
int main()
{
	//p指向字符串hello的首地址
	char* p = "hello";//字符串保存在字符串常量区并非栈帧空间
	*p = 'H';
	return 0;
}

 数组的长度可以用const修饰的变量表示吗?

#include <stdio.h>
int main()
{
	const a = 10;
	int arr[a] = { 0 };
	return 0;
}

数组空间开辟的时候他的元素个数,也就是数组的长度必须的常量,要么就是一些字面值,10,2030这样的,要么就是宏定义,像consr修饰的变量我们就不能真正意义上称他为常量,编译器也必须的在开辟空间的时候才能确定它的大小,所以默认的情况下编译是不通过的。如Linux操作系统是支持的

不同的编译器对于C语言的标准是不太一样的,有的是支持标C,并且能够在特定的平台做扩展,一份C语言代码在不同编译可能有不同的样子,我们尽可能使用标C,因为标C是所有的编译器都支持的

关于标准C的补充:

  • C89/C90:是标准C的最早版本,定义了C语言的基本规范。C90是对C89的一个小的补充,包含了一些小的修正。
  • C99:是对C89/C90的更新,增加了新特性,如对变长数组(VLA)、long long 数据类型、stdbool.h 布尔类型等的支持。
  • C11:是进一步的更新,主要增加了对多线程支持(_Thread_local<threads.h>)以及改进了代码的并发执行等方面的特性。
  • C17/C18:这些版本对C11做了一些小的修订,增强了语言的一些细节,但没有引入大的新特性。

const修饰数组

 1.普通数组我们是通过索引下标来给数组进行赋值的

#include <stdio.h>
int main()
{
	int arr[5] = { 1,2,3,4,5 };
	arr[0] = 10;
	arr[1] = 10;
	arr[2] = 10;
	for (int i = 0; i < 5; i++)
		printf("%d\n", arr[i]);
	return 0;
}

2.但是被const修饰的数组,索引下标的时候是不能被修改的

#include <stdio.h>
int main()
{
	const int arr[5] = { 1,2,3,4,5 };
	arr[0] = 10;
	arr[1] = 10;
	arr[2] = 10;
	for (int i = 0; i < 5; i++)
		printf("%d\n", arr[i]);
	return 0;
}

cinst修饰指针

指针和指针变量是两种概念,一般意义上我们在C语言使用的大部分内容是指针变量,其次指针就是地址,就是一串数字。指针变量是一个变量,该变量是用来保存地址。指针提高查找定位效率。

同样是变量x代表的含义是不同

第一个是x的空间变量的属性,只用空间称为左值

第二个是x的内容数据的属性  使用内容称为右值

#include <stdio.h>
int main()
{
	int x;
	x = 100;//x的空间 变量的属性,只用空间称为左值
	int y = x;//x的内容	数据的属性  使用内容称为右值
	printf("%d\n",y);

	return 0;
}

指针变量做左值的时候是空间变量的属性,只用空间称为左值。

指针变量做右值时是内容数据的属性(因为是地址 )使用内容称为右值。

#include <stdio.h>
int main()
{
	int a = 10;
	int* p = &a, * q = NULL;
	p = &a;
	q = p;
	printf("%d", *q);
	return 0;
}

C语言中,任何变量&地址都是最低地址开始

在 C 语言中,当分配一个变量的内存时,是从最低地址开始分配的,并且是按照字节序列来分配的。这意味着,不考虑变量的类型,第一个字节分配给变量的最低地址,后续的字节依次分配给更高的地址。

这里有一个重要的概念是内存地址的增长方向。在大多数现代计算机体系结构中,内存地址是从低到高增长的。例如,假设有一个变量 int a;,并且它被分配在地址 0x1000,那么这个 int 类型的变量将占用 4 个字节(假设 int 是 4 字节大小),这 4 个字节的地址将是:

  • 0x1000 (最低地址)
  • 0x1001
  • 0x1002
  • 0x1003 (最高地址)

以下是一个简单的例子,演示了在 C 语言中不考虑类型的情况下,变量的地址是如何从最低地址开始的

#include <stdio.h>

int main() {

    int i=10;    

    return 0;
}

指针指向的空间

用一个代码演示给大家看一下。

#include <stdio.h>
int main() {

    int i=10;    
    int* p = &i;
    return 0;
}

指针指向的空间也是地址中地址最低的那个

 我们输入&i,回车后发现依旧是指向地址最低的那个

指针解引用

在 C 语言中,(类型相同)对指针进行解引用就是指针所指向的目标。解引用操作符 * 用于访问指针所指向的内存位置。解引用后的指针可以作为左值或右值,这取决于它在表达式中的使用。

右值:当解引用的指针用于获取它所指向的值时,它充当右值。在这种情况下,解引用操作符 * 用于读取指针指向的内存位置中的值。

例如:

int a = 10;
int *p = &a;
int b = *p; // *p 作为右值,读取 p 指向的值(a 的值),并将其赋值给 b

左值:当解引用的指针用于修改它所指向的值时,它充当左值。在这种情况下,解引用操作符 * 用于指定一个可修改的内存位置。

例如:

int a = 10;
int *p = &a;
*p = 20; // *p 作为左值,修改 p 指向的内存位置中的值(a 的值)

以下是一些具体的例子,说明 *p 如何在不同的语句中充当左值或右值:

作为右值

int a = *p; // *p 读取 p 指向的值,并赋值给 a
if (*p > 0) { ... } // *p 读取 p 指向的值,用于条件判断

作为左值

*p = 10; // *p 修改 p 指向的内存位置中的值
*p += 5; // *p 修改 p 指向的内存位置中的值,进行加法赋值操作

总结来说,解引用的指针 *p 在表达式中的角色(左值或右值)取决于它是用于读取值还是修改值。如果用于读取值,它是右值;如果用于修改值,它是左值。

第一种情况:const int* p

const修饰*号,在语义上来说如果对这个p进行解引用,那么这个值是不能被直接修改的,换言之p执行的内容不可改、因为我们const修饰*,p无法解引用之后充当左值,所以就是指向的变量不可被修改。p等于100是不会报错的,因为const并没有修饰p。注意一点:关键字是不能用来修饰关键字的

#include <stdio.h>
int main() {
    int a = 10;
    const int* p = &a;//p指向的变量不可以直接被修改
    *p = 100;
    p = 100;
    return 0;
}

第二种情况: int const * p

第二种写法跟第一种写法语意上是一样的没有差别,都是修饰*

#include <stdio.h>
int main() {
 
    //两种写法一样的
    int const * p = &a;//const修饰的是*
    const int * p = &a;//const修饰的是*
    *p = 100;
    p = 100;
    return 0;
}

第三种情况:int* const p

p的内容不可以直接被修改,但是*p可以,因为*p对应的就是我们所指向的目标就是a,a是可以被修改的

#include <stdio.h>
int main() 
{
    int a =10;
    int* const p = &a;
    *p = 100;
    return 0;
}

但是我们p的内容是不可以被修改的,p的内容是a的地址,是p指向a

#include <stdio.h>
int main() 
{
    int a =10;
    int* const p = &a;
    p = 100;
    return 0;
}

第四种情况:const int* const p

p的指向不能改,永远都指向a,p指向a不能直接通过p去修改a

#include <stdio.h>
int main() 
{
    int a =10;
    const int* const p = &a;
    p = 100;
    *p = 100;
    return 0;
}

如果把一个类型限定不严格的变量,赋值给另外一个类型限定非常严格的变量,其中编译器是不报错的,但是要把一个类型限定比较严格的变量,赋值给类型限定不怎么严格的变量,其中的编译器就会报错。


关系表达式与逻辑运算符

#include <stdio.h>
int main()
{
	int a, b, c;
	a = b = c = 0;
	a++ && ++b||c++;
	printf("a=%d,b=%d,c=%d", a, b, c);
	return 0;
}
  1. a++:这是一个后缀自增运算符,它首先返回 a 的当前值(0),然后将 a 的值增加 1。因此,a 变为 1。

  2. &&:由于 a++ 返回 0,根据逻辑与的短路行为,++b 不会被计算,因此 b 保持为 0。

  3. ||:由于 a++ && ++b 的结果是 0,c++ 会被计算。c++ 是一个后缀自增运算符,它首先返回 c 的当前值(0),然后将 c 的值增加 1。因此,c 变为 1。

最终
printf 语句将输出 ab 和 c 的值,分别为 1、0 和 1。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值