【C语言深度剖析】第一篇:关键字


前言

相信对于刚刚学完C语言的同学来说都有着或多或少的疑问,而在学习课程中我我们却很少涉及到这些盲区,本章讲的就是对C语言的相关拓展,相信看完这篇文章的同学对C语言又会有更深的认识。本章将要探讨的是C语言当中的关键字,C语言一共多少个关键字呢?一般的书上,都是32个(包括本书),但是这个都是 C90(C89) 的标准。其实 C99 后又新增了5个关键字。不过,目前主流的编译器,对 C99 支持的并不好,我们后面默认情况,使用 C90 ,即,认为32个。


一、关键字分类

1.数据类型关键字(12个)

char :声明字符型变量或函数
short :声明短整型变量或函数
int : 声明整型变量或函数
long :声明长整型变量或函数
signed :声明有符号类型变量或函数
unsigned :声明无符号类型变量或函数
float :声明浮点型变量或函数
double :声明双精度变量或函数
struct :声明结构体变量或函数
union :声明共用体(联合)数据类型
enum :声明枚举类型
void :声明函数无返回值或无参数,声明无类型指针

2.控制语句关键字(12个)

2.1循环控制(5个)

for :一种循环语句
do :循环语句的循环体
while :循环语句的循环条件
break :跳出当前循环
continue :结束当前循环,开始下一轮循环

2.2条件语句(3个)

if : 条件语句
else :条件语句否定分支
goto :无条件跳转语句

2.3开关语句 (3个)

switch :用于开关语句
case :开关语句分支
default :开关语句中的“其他”分支

2.4返回语句(1个)

return :函数返回语句(可以带参数,也看不带参数)

3.存储类型关键字(5个)

auto :声明自动变量,一般不使用
extern :声明变量是在其他文件中声明
register :声明寄存器变量
static :声明静态变量
typedef :用以给数据类型取别名(但是该关键字被分到存储关键字分类中,虽然看起来没什么相关性)

4.其他关键字(3个)

const :声明只读变量
sizeof :计算数据类型长度
volatile :说明变量在程序执行中可被隐含地改变

二、第一个C程序

在这里插入图片描述

运行程序的方式,当然可以用vs直接启动
当然,也可以在vs项目中,找到代码生成的二进制可执行程序(exe),双击即可。
所以:我们的角色是写代码,编译器的角色是把文本代码变成二进制可执行程序。

在Windows当中,双击就是打开程序并运行,那么启动程序的本质是什么呢?将程序数据,加载到内存中,让计算机运行!我们用一张图片表示:
在这里插入图片描述

三.定义与声明

3.1什么是变量?(是什么)

本质上是在内存中开辟特定大小的空间,用来保存数据

3.2如何定义变量(怎么用)

类型 变量名 = 默认值

int x = 10char c = 'a'; 
double d = 3.14;

3.3为什么要定义变量(为什么)

计算机是为了解决人计算能力不足的问题而诞生的。即,计算机是为了进行计算的。 而计算,就需要数据。 而要计算,任何一个时刻,不是所有的数据都要立马被计算。 如同:要吃饭,不是所有的饭菜都要立马被你吃掉。饭要一口一口吃,那么你还没有吃到的饭菜,就需要暂时放在盘子里。 这里的盘子,就如同变量,饭菜如同变量里面的数据。 换句话说,为何需要变量?因为有数据需要暂时被保存起来,等待后续处理。 那么,为什么吃饭要盘子?我想吃一口菜了,直接去锅里找不行吗?当然行,但是效率低。 因为我们吃饭的地方,和做饭的地方,是比较"远"的。

3.4变量定义与声明的本质

我们现在已知: 1. 程序运行,需要加载到内存中 2. 程序计算,需要使用变量
那么,定义变量的本质:在内存中开辟一块空间,用来保存数据。(为何一定是内存:因为定义变量,也是程序逻辑的一部 分,程序已经被加载到内存),变量只能定义一次
声明的意思就是告知计算机我们存在一个变量:一个变量可以声明多次

四.关键字解析

4.1 auto

4.1.1变量的分类

局部变量:包含在代码块中的变量叫做局部变量。局部变量具有临时性。进入代码块,自动形成局部变量,退出代码块自动 释放。[网上很多说函数中的变量是局部变量,不能说错,但说法是不准确的]
全局变量:在所有函数外定义的变量,叫做全局变量。全局变量具有全局性。
代码块:用{}括起来的区域,就叫做代码块

#include<stdio.h>
#include<windows.h>

int g_val = 0;//全局变量

int main()
{
	int a = 0;//局部变量
	printf("hello world\n");
	system("pause");//停屏
	return 0;
}

4.1.2变量的作用域

在这里插入图片描述
局部变量:只在本代码块内有效
全局变量:整个程序运行期间,都有效
那么大家看下面这个代码:

#include<stdio.h>

int x = 100;

int main()
{
	int x = 10;
	printf("%d\n",x);
	return 0;
}

你们认为答案是什么呢?运行程序:
在这里插入图片描述
当全局变量和局部变量发生冲突时,优先使用局部变量。

4.1.3变量的生命周期

生命周期概念:指的是该变量从定义到被释放的时间范围,所谓的释放,指的是曾经开辟的空间”被释放“
局部变量: 进入代码块,形成局部变量[开辟空间],退出代码块,"释放"局部变量
全局变量: 定义完成之后,程序运行的整个生命周期内,该变量一直都有效

4.1.4变量的生命周期作用域 vs 生命周期

很多同学在学C语言的时候就分不清什么是作用域而什么又是生命周期,原因就是我们对它们的概念很模糊,让我们误以为作用域就是生命周期。
作用域:改变量的有效区域。
生命周期:时间的概念,什么时候被开辟,什么时候被释放。
举个简单的例子:伟人从出生到去世就是他们的生命周期,而他们在他们的领域做出的贡献就是他的作用域。相信这个例大家都能够更好的理解了它们的含义了吧。

4.1.5 auto 相关

如何使用:一般在代码块中定义的变量,即局部变量,默认都是auto修饰的,不过一般省略
默认的所有变量都是auto吗?不是,一般用来修饰局部变量
后面我们所到的,局部变量,自动变量,临时变量,都是一回事。我们统称局部变量
这是一个比较老的关键字,所以写代码的时候基本上不会用。

#include<stdio.h>
#include<windows.h>

int main() {
	for (int i = 0; i < 10; i++)
	{
		printf("i=%d\n", i); if (1)
		{
			auto int j = 0;	//自动变量 
			printf("before: j=%d\n", j);
			j += 1; printf("after : j=%d\n", j);
		}
	}system("pause");
	return 0;
}

4.2 register

其实,CPU主要是负责进行计算的硬件单元,但是为了方便运算,一般第一步需要先把数据从内存读取到CPU内,那么也就需要CPU具有一定的数据临时存储能力。注意:CPU并不是当前要计算了,才把特定数据读到CPU里面,那样太慢了。
所以现代CPU内,都集成了一组叫做寄存器的硬件,用来做临时数据的保存。

4.2.1存储金字塔

在这里插入图片描述
距离CPU越近的存储硬件,速度越快。

寄存器存在的本质在硬件层面上,提高计算机的运算效率。因为不需要从内存里读取数据啦。

4.2.2register 修饰变量

尽量将所修饰变量,放入CPU寄存区中,从而达到提高效率的目的.
那么什么样的变量,可以采用register呢?

  1. 局部的(全局会导致CPU寄存器被长时间占用)
  2. 不会被写入的(写入就需要写回内存,后续还要读取检测的话,register的意义在哪呢?)
  3. 高频被读取的(提高效率所在)
  4. 如果要使用,请不要大量使用,因为寄存器数量有限
#include<stdio.h>

int main()
{
	register int a = 10;
	printf("&a = %p \n", &a);
	return 0;
}

这里的编译器可能会报错。也可能不会报错,取决于编译器。

总结:该关键字,不用管,因为现在的编译器,已经很智能了,能够进行比人更好的代码优化。 早期编译器需要人为指定register,来进行手动优化,现在不需要了。

4.3 static

4.3.1 认识多文件

在这里插入图片描述

.h:我们称之为头文件,一般包含函数声明,变量声明,宏定义,头文件等内容(header)
.c: 我们称之为源文件,一般包含函数实现,变量定义等 (.c:c语言) test.h

4.3.2全局变量和函数的两个结论

我们在test.c中定义Show函数,而不在test.h中声明
在这里插入图片描述
在main.c中去调用它
在这里插入图片描述
那么可以吗?
在这里插入图片描述
答案是可以的。那么在test.c中定义全局变量g_val
在这里插入图片描述
然后再main.c中打印可以吗?
在这里插入图片描述
答案是不行的。
然后在test.c中声明g_val
在这里插入图片描述
此时我们就可以使用g_val了

注意:声明不开辟空间。
变量声明可以多次,变量定义只能定义一次
所有的全局变量声明的时候,不能设置初始值。

两个结论

  1. 全局变量,是可以跨文件,被访问的。
  2. 全局函数,是可以跨文件,被访问的。

4.3.3static 的作用

  1. 修饰全局变量,该全局变量只能在本文件内被使用。
  2. 修饰函数,该函数只能在本文件内被使用。
  3. 局部变量,更改局部变量的声明周期。
    此时我们在test.c中加上static
    在这里插入图片描述
    再main.c中使用:
    在这里插入图片描述
    此时就会报错。g_val和Show函数的作用域就是在test.c中。而不能在其他文件中被使用。
void fun1()
{
	int i = 1;
	i++;
	printf("no static: i = %d \n", i);
}

void fun2()
{
	static int i = 1;
	i++;
	printf("has static: i = %d \n", i);
}

int main()
{
	for (int i = 0; i < 10; i++)
	{
		fun1();
		fun2();
	}
	/*Show();
	printf("%d\n", g_val);
	return 0;*/
}

大家看这个代码输出的会是什么呢?
在这里插入图片描述
结论:static修饰局部变量,变量的生命周期变成全局周期。将数据放在堆区(作用域不变) 此时我们大家应该就能明白了吧。

4. 4基本数据类型

用一张图片直观表示:
在这里插入图片描述
C常见内置类型
在这里插入图片描述

前面已经说过,定义变量的本质:在内存中开辟一块空间,用来保存数据。
而定义一个变量,是需要类型的,这个是基本语法决定的。那么,类型决定了:变量开辟空间的大小。

4.5sizeof 理解

用来计算变量和类型的大小,用法比较简单。那么我们用一个例子:
在这里插入图片描述

4.6signed、unsigned 与整形在内存的存储

4.6.1有符号整数 vs 无符号整数

char
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]

4.6.2原反补

我们之前讲过一个变量的创建是要在内存中开辟空间的,空间的大小是根据不同的类型而决定的。那么,数据在所开辟内存中到底是如何存储的呢?

我们知道,编译器为 a 分配四个字节的空间。那如何存储呢?
首先,对于有符号数,一定要能表示该数据是正数还是负数。所以我们一般用最高比特位来进行充当符号位。
原码、反码、补码 计算机中的有符号数有三种表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位三种表示方法各不相同。
如果一个数据是负数,那么就要遵守下面规则进行转化:
原码:直接将二进制按照正负数的形式翻译成二进制就可以。
反码:将原码的符号位不变,其他位依次按位取反就可以得到了。
补码:反码+1就得到补码。 如果一个数据是正数,那么它的原反补都相同。
在这里插入图片描述

那么我们再看一另一个让人费解的代码:

int main()
{
	unsigned int a = -10;
	printf("%d\n", a);
	return 0;
}

这个变量定义会有错误码?如果没有错误,那么结果会是对少呢?
数据存储的过程:

在这里插入图片描述
那么unsigned int 什么时候其作用呢?是在读取数据的时候
在这里插入图片描述

4.6.3十进制二进制快速转化

口诀:1后面跟n个0,就是2的n次方
在这里插入图片描述

4.6.4大小端补充

CPU访存的基本单位是字节 数据按照字节,是有高权值位低权值为之分的。
内存按照字节是有高地址,低地址之别的。
在这里插入图片描述
大小端是如何影响数据存储的?本质上是数据和空间按照字节为单位的一种映射关系。
大小端的存储方式是不会影响用户的使用的。
为什么都是补码

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

4.6.5整形取值范围

以signed char为例:
0 000 0000
最高位为符号位,意味着只有7个数值位。
1 111 1111 ->-127
0 111 1111 ->127
那么signed char 的取值范围是[-127~127]吗?
当我们在计算一个二进制的取值范围的时候,应该是2^n次方个(n为比特位个数)。例如:
如果有两个比特位 那么可以表示的数就是: 00 01 10 11
如果有三个比特位 那么可以表示的数就是: 000 001 010 011 100 101 110 111
所以根据推论,那我们计算signed char的取值范围的时候,是不是应该是2^8的个排列组合,但是上面的取值是 -127~127 才255个数,那么应该怎么办呢?
而0只能有一种标识方案,必然是 0000 0000 (+0)
那么 1000 0000 (-0) 用来标识谁呢?根据推论,那必然就是-128了。
怎么证明呢?

在这里插入图片描述
因为-128 在char 取值范围内,所以就可以输出-128。用张图片直观表示:
在这里插入图片描述

总结规律:
整数的取值范围 无符号:[0,2^n-1]
有符号:[-2^(n-1), 2^(n-1)-1]

例题:

int main()
{
	char a[1000];
	for (int i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d\n", strlen(a));
	return 0;
}

大家看这个输出的答案是多少:
在这里插入图片描述
这是为什么呢?
在这里插入图片描述

int main()
{
	int i = -20;
	unsigned int j = 10;
	printf("%d\n", i + j);
	return 0;
}

这个题又会输出什么呢?
在这里插入图片描述
在这里插入图片描述
在看一道题:

int main()
{
	unsigned int i;
	for (i = 0; i >= 0; i++)
	{
		printf("%u\n", i);//%u 以无符号整形打印
	}
	return 0;
}

这个会是什么呢?
答案是死循环。因为 i 是unsigned int 类型 取值始终是大于0 的所以这个程序一直会打印下去。

4.7 if else 语法

什么是语句
C语言中由一个分号;隔开的就是一条语句。
比如: printf(“hehe”); 1+2;
什么是表达式
C语言中,用各种操作符把变量连起来,形成有意义的式子,就叫做表达式。 操作符:+,-,*,/,%,>,<,=,==…
基本语法

1
if(表达式)
语句;
2
if(表达式)
语句1;
else
语句2;
3
if(表达式1)
语句1;
else if(表达式2)
语句2;
else
语句3;

4.7.1bool 与0比较

C语言有没有bool类型?
c99之前,主要是c90是没有的,目前大部分书,都是认为没有的。因为书,一般都要落后于行业。
但是c99引入了_Bool类型(你没有看错,_Bool就是一个类型,不过在新增头文件stdbool.h中,被重新用宏写成了 bool,为了保证C/C++兼容性)。

在VS2019上true和false是1和0

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
那么BOOL又是什么呢?
在这里插入图片描述
这个微软上提供的一套BOOL标准,只能在Windows上使用,不具有跨平台性。
我们在linux上运行代码:
在这里插入图片描述
此时我们就会发现编译出错
在这里插入图片描述
而使用bool可以吗?
在这里插入图片描述
在这里插入图片描述

此时运行我们就会发现可以编译成功。运行结果还是0 和 1.
所以bool 具有跨平台性,而BOOL不具有跨平台性。
总结:

  1. 优先使用c90,就是我们之前以及后面一直用的方式
  2. 万一非得使用bool,推荐c99标准,不推荐MS自定义。

那么,C中如何进行 bool 值与0比较呢?

#include<stdio.h>
#include<string.h>
#include<stdbool.h>
#include<windows.h>


int main()
{
	int flag = 0;
	if (flag == 0)
	{
		//不推荐
		printf("1\n");
	}
	if (flag == false)//c99引用false
	{
		//不推荐
		printf("2\n");
	}
	if (flag)
	{
		//推荐
		printf("3\n");
	}
	return 0;
}

所以在以后我们将false赋值给flag的时候就用推荐的。

4.7.2float 变量与0比较

浮点数在内存中存储,并不想我们想的,是完整存储的,在十进制转化成为二进制,是有可能有精度损失的。
注意这里的损失,不是一味的减少了,还有可能增多。浮点数本身存储的时候,在计算不尽的时候,会“四舍五入"

在这里插入图片描述
这里可以看出精度损失不仅仅是字面上数值减少的意思,而有可能增大。
我们再看一个例子:
在这里插入图片描述
此时也会发生精度损失。
那么浮点数可以进行比较吗?
在这里插入图片描述

所以浮点数不能用== 直接进行比较。而浮点数和0同样也不能用== 进行比较
那么两个浮点数该如何比较呢?
应该进行范围精度比较

//伪代码
 if((x-y) > -精度 && (x-y) < 精度)
 {
 	 //TODO
 }
 //伪代码-简洁版 
 if(fabs(x-y) < 精度)//fabs是浮点数求绝对值
 {  
    //TODO 
 }

在这里插入图片描述

精度: 自己设置?后面如果有需要,可以试试,通常是宏定义。
使用系统精度?暂时推荐 #include<float.h> //使用下面两个精度,需要包含该头文件
DBL_EPSILON //double 最小精度
FLT_EPSILON //float 最小精度

在这里插入图片描述
在这里插入图片描述
所以我们也可以使用系统给的精度范围
在这里插入图片描述
同理用这个也可以与0比较
在这里插入图片描述

//x > -DBL_EPSILON && x < DBL_EPSILON: 为何不是>= && <= 呢?
//个人看法:XXX_EPSILON是最小误差,是:XXX_EPSILON+n不等于n的最小的正数。
//XXX_EPSILON+n不等于n的最小的正数: 有很多数字+n都可以不等于n,但是XXX_EPSILON是最小的,but, XXX_EPSILON依旧是引起不等的一员。
//换句话说:fabs(x) <= DBL_EPSILON(确认x是否是0的逻辑),如果=,就说明x本身,已经能够引起其他和他±的数据 本身的变化了,这个不符合0的概念。

4.7.3指针与0比较

这里大概了解一下,因为要深入了解还要用到指针知识。
0 ‘\0’ NULL
在这里插入图片描述
可见其实0 、’\0’和NULL其实都是0。怎么证明呢?
在这里插入图片描述

NULL其实是将0强制类型转化为(void*)类型,如何理解强制类型转化呢?
强制类型转化是不会改变内存中存储的数据的,只会改变对应的类型。
比如所0在内存中存储的就是0,而我们用指针的角度去看就是NULL,用字符的角度去看就是’\0’

int* p = NULL;
A: if(p == 0)//不推荐
B: if(p)//不推荐
C: if(p == NULL)//推荐

4.7.4else 匹配

大家看这个代码运行会是什么呢?

int main()
{
	int x = 1;
	int y = 2;
	if (x == 10)
		if (y == 20)
			printf("hehe\n");
	else
		printf("haha\n");
	return 0;
}

在这里插入图片描述
运行后发现不会输出任何内容,这是为什么呢?因为else 匹配采取就近原则这里的else匹配的是第二个if。以后在判断的时候应该加上{},看起比较规范。
else常见错误:

int main()
{
	int x = 0;
	if (x);
	{
		printf("hello\n");
	}
	return 0;
}

这个代码会输出hello吗?
在这里插入图片描述
答案是会输出hello,因为if后面加了;if就会被看出一个语句,执行下一个语句的时候就会printf直接就会输出hello。

4.8switch case

基本语法结构:

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

已经有if else为何还要switch case
switch语句也是一种分支语句,常常用于多分支的情况。这种多分支,一般指的是很多多分支,而且判定条件主要以整型为 主,如:
输入1,输出星期一
输入2,输出星期二
输入3,输出星期三
输入4,输出星期四
输入5,输出星期五
输入6,输出星期六
输入7,输出星期日
如果写成 if else 当然是可以的,不过比较麻烦
case 比较简单,就不多讲了,需要注意的就是break;如果不加break可能就会得到不一样的结果。

在这里插入图片描述

4.9do、while、for

三种循环各自语法

//while 
条件初始化 
while(条件判定)
{ 
	//业务更新 
	条件更新 
}
//for
for(条件初始化; 条件判定; 条件更新)
{ 
	//业务代码 
}
//do while 
条件初始化 
do
{ 
	条件更新 
}while(条件判定);

while
在这里插入图片描述
for
在这里插入图片描述
do whlle
在这里插入图片描述
三种循环对应的死循环写法

while(1)
{
	;
}
for( ; ; )
{
	;
}
do
{
	;
}while(1)

break和continue;
break退出当前循环,continue退出本轮循环;
举个例子:
在这里插入图片描述
在这里插入图片描述
相信看完这两个例子大家都懂了吧。
补充

任何一个c程序,在默认编译好之后,运行时,都会打开上三个输入流:
stdin:标准输入,FILE* stdin,键盘。
stdout:标准输出,FILE* stdout,显示器。
stderr:标准错误,FILE* stderr;显示器。

键盘输入的内容,或者往显示器打印的内容都是字符:怎么证明呢?
在这里插入图片描述

4.10goto

基本使用
在这里插入图片描述

很多公司确实禁止使用goto,不过,这个问题我们还是灵活对待,goto在解决很多问题是有奇效的。
我们可以认为goto使用场景较少,一般不使用。但是必须得知道goto,需要的时候,也必须会用

有人用吗?
在这里插入图片描述
Linux内核源代码中充满了大量的goto,只能说我们目前,或者很多公司的业务逻辑不是那么复杂
现在基本用不到。

4.11void

void 是否可以定义变量
在这里插入图片描述
答案是不能的,那我们在linux平台下定义可以吗?
在这里插入图片描述
我们编译运行
在这里插入图片描述

发现linux上也是不行的。所以void不能用来定义变量。
定义变量的本质:开辟空间 而void作为空类型,理论上是不应该开辟空间的,即使开了空间,也仅仅作为一个占位符看待 所以,既然无法开辟空间,那么也就无法作为正常变量使用,既然无法使用,编译器干脆不让他定义变量。因为它是空数据类型,那么它的大小是0吗?

在这里插入图片描述
显然它在VS上是0,我们在linux上看一下:
在这里插入图片描述
在这里插入图片描述
此时我们发现它在linux上是1,所以对void不要以为就是0,在不同平台上的编译器大小是不一样的。
void不能定义变量,那么void呢?
在这里插入图片描述
此时我们发现编译器并没有报错,为什么void
可以呢?那又怎么解释呢?因为void*是指针,是指针,空间大小就能明确出来,32位就是4个字节,64位就是8个字节。
再看下面这个例子:

在这里插入图片描述
同样也没有报错,void的作用是用来接受任意指针类型的。这块在后面如果想设计出通用接口,很有用。以memset为例:
在这里插入图片描述
此时我们发现接口就是void
,里面的变量也是void*,就可以接受任意类型的数据了。
void * 定义的指针变量可以进行运算操作吗?
我们在VS2019上演示:
在这里插入图片描述
很显然在VS上会报错,同样的代码我们在linux上会报错吗?
在这里插入图片描述
在这里插入图片描述

此时我们运行发现生成了a.out这样的文件,那么说明就没有报错,输出了1和0.为什么在不同的平台下,编译器会表现出不同的现象呢? 根本原因是因为使用的C标准扩展的问题。

对指针++具体是怎么++? 为何gcc中,输出结果是1, 和0 呢? 本质和不同平台,看待void空间大小相关。
补充:

GNU计划,又称革奴计划,是由Richard Stallman(理查德·斯托曼)在1983年9月27日公开发起的。它的目标是创建一套完 全自由的操作系统。它在编写linux的时候自己制作了一个标准成为 GNU C标准。ANSI 美国国家标准协会,它对C做的标准 ANSI C标准后来被国际标准协会接收成为 标准C 所以 ANSI C 和标准C是一个概念,总体来说现在linux也支持标准C,以 后标准C可以跨平台,而GUN c 一般只在linux c下应用。–来自百度 Linux 上可用的 C 编译器是 GNU C 编译器,它建立在自由软件基金会的编程许可证的基础上,因此可以自由发布。 GNU C 对标准 C 进行一系列扩展,以增强标准 C 的功能。–来自百度 一句话,大部分编译器是标准C,而Linux下是扩展C,Linux平台也能保证标准C的运行

4.11return

首先来看一段代码

#include<stdio.h>

char* Show()
{
	char str[] = "hello world";
	return str;
}
int main()
{
	char* s = Show();
	printf("%s\n", s);
	return 0;
}

结果会输出什么呢?
在这里插入图片描述

这里很容易理解,因为Show函数里面定义的str在Show函数结束后就会被释放,我们在去访问它就会出错,那么就是非法访问内存。但是此时str被指向的空间被清零了吗?

在这里插入图片描述
此时我们调试起来,发现当调试已经走到13行了,但是str指向的空间还是hello world,所以释放空间不是将空间清零,而是将空间置成无效。
在这里插入图片描述
但是当我们走到14行的时候,hello world就没有了,这是为什么呢?
这里就不得不谈到函数栈帧了。用一张图片表示:
在这里插入图片描述

每次函数调用都会开辟一个函数栈帧:当调用结束的时候,栈帧销毁。所以当Show函数调用结束的时候栈帧销毁,而printf函数也是函数,自然也要开辟函数栈帧,从而直接在main函数栈帧下面开辟,所以Show函数就被覆盖了,而里面的内容自然也不在了。

在这里插入图片描述
大家看上面这段代码,结果输出的是10,这很容易理解,但是我们刚刚不是才说函数调用完毕,空间释放吗?里面的a不会被覆盖吗?那么这个a又是如何返回来的呢?此时我们转到反汇编
在这里插入图片描述
大家注意eax(寄存器)我们是将函数的返回值放入eax的寄存器当中的,寄存器随着程序运行,会一直存在,所以我们就能够拿到a的值

4.12const

const修饰变量,该变量为常属性。比说说:
在这里插入图片描述
那么a可以被改变吗?
在这里插入图片描述

此时我们会发现a的值被改为了20,结论:const修饰的变量并非是真的不可被修改的常量。
那const修饰变量,意义何在?

  1. 让编译器进行直接修改式检查
  2. 告诉其他程序员(正在改你代码或者阅读你代码的)这个变量后面不要改哦。也属于一种“自描述”含义

const修饰指针:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.13volatile

volatile 是易变的,不稳定的意思。很多人根本就没见过这个关键字,不知
道它的存在。也有很多程序员知道它的存在,但从来没用过它。我对它有种“杨
家有女初长成,养在深闺人未识”的感觉。
volatile 关键字和 const 一样是一种类型修饰符,用它修饰的变量表示可以
被某些编译器未知的因素更改,比如操作系统、硬件或者其他线程等。遇到这个
关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供
对特殊地址的稳定访问。
接下来,我在汇编角度,在Linux平台给大家对比演示一下加还是不加volatile的作用,让大家看明白

在这里插入图片描述
这个代码是死循环。但是从汇编的角度是可以优化的。
在这里插入图片描述
加上volatile
在这里插入图片描述
在这里插入图片描述

4.14struct union enum 和typedef

struct

union
在这里插入图片描述

enum
在这里插入图片描述
typedef
对类型进行重命名
在这里插入图片描述
typedef 与 #define 的区别
typedef和define容易引起混淆,但是在类型重定义还是有很大区别的,比如下面这个例子:
在这里插入图片描述

总结

本章就是关于C语言所有的关键字,其中大多数都是C语言常见的关键字,小部分作为补充,希望看完本章,你对C语言的关键字又有了更深的拓展。

  • 45
    点赞
  • 117
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小唐学渣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值