C语言常见关键字解析(一)

本章主要介绍C语言的常见关键字,在介绍关键字之前,我们对第一个C程序 - “hello world!”进行补充!!!

第一个C语言程序补充

//在vs2019中创建项目
//编写第一个C语言程序"hello world"
#include <stdio.h>
int main()
{
	printf("hello world!\n");
	return 0;
}

对于这段代码,运行程序的方式,(1)可以用vs直接启动,(2)也可以在vs项目中,找到代码生成的二进制可执行程序(.exe文件),双击即可。
所以:我们的角色是写代码,编译器的角色是把文本代码变成二进制可执行程序。
那么双击这个动作做了什么?不就是windows下启动程序的做法吗?
那么启动程序的本质是什么呢?将程序数据,加载到内存中,让计算机运行!
那么为什么要加载到内存中呢?因为内存速度比外设快,任何程序在运行之前都必须加载到内存当中。

冯诺依曼体系结构,硬件决定,输入设备将数据放入内存中,然后cpu从内存取走数据,cpu处理之后再放入内存中,然后输出到输出设备。
在这里插入图片描述
没加载到内存之前,程序在哪里?在硬盘中(外设)。

定义和声明

下面再介绍一下定义和声明,什么是定义?什么是声明?他们的区别是什么?

什么是变量?
变量就是在内存中开辟的一块特定大小的空间,用来保存数据。

我们如何定义变量?

int x = 10char c = 'a';
double d = 3.14;
//类型 变量名 = 默认值

为什么要定义变量?
计算机是为了进行计算,要计算就必须要有数据,但是很多数据,并不是一下子就使用,有些数据要暂时保存起来,等待以后使用。

什么是声明变量?
有两种含义:1.告知编译器存在这样一个变量,其他的地方不能用它来作为变量名或者对象名;2.告诉编译器,这个名字已经匹配到一块内存上了,后面的代码用到这个名字,是在别的地方已经定义了的。

定义变量的本质:在内存中开辟一块空间,用来保存数据。
声明变量的本质:广而告之。

女朋友只能有一个,但是可以告诉很多人你有女朋友了,女朋友是谁。

定义只能有一次,声明可以有多次!!!

1.auto

在介绍auto关键字之前,重新来回顾一下局部变量和全局变量的概念。

局部变量:在代码块内部定义的变量叫局部变量,局部变量具有临时性,进入代码块自动创建,出代码块自动销毁,网上很多说函数中的变量是局部变量,不能说错,但说法是不准确的。

全局变量:在所有函数外定义的变量,叫做全局变量。全局变量具有全局性。

代码块:用{}括起来的区域,就叫做代码块。

#include <stdio.h>
int g_x = 100; //全局变量
int main()
{
	int x = 10; //局部变量,main函数也是函数,也有代码块{}
	printf("x:%d\n", x);

	return 0;
}

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

#include <stdio.h>
int main()
{
	int x = 10;
	if (x == 10)
	{
		int y = 20;
		printf("局部: x: %d, y: %d\n", x, y);//y只能在本代码块内有效
	} 
	printf("局部: x: %d, y: %d\n", x, y); //报错,y不能被访问
	return 0;
}

结论:
局部变量:只在本代码块内有效
全局变量:整个程序运行期间,都有效

#include <stdio.h>
int g_x = 100; //全局变量

void show()
{
	printf("show: 全局: %d\n", g_x); //在任何代码块中都可以被访问
} 
int main()
{
	show();
	printf("main: 全局: %d\n", g_x); //在任何代码块中都可以被访问,甚至被修改

	return 0;
}

局部变量和全局变量同名时:

#include <stdio.h>
int g_x = 100; //全局变量

int main()
{
	int g_x = 10; //局部变量,与全局同名

	printf("g_x:%d\n", g_x); //输出的是局部,也就是局部和全部同名的时候,优先局部。所以,强烈不建议这样干。

	return 0;
}

声明周期指的是:该变量从定义到释放的时间范围,所谓释放是指释放曾经开辟的空间。

局部变量: 进入代码块,形成局部变量【开辟空间】,退出代码块,"释放"局部变量

全局变量: 定义完成之后,程序运行的整个生命周期内,该变量一直都有效
释放后,内存仍然存在,只是没有了使用权限,权限归还给操作系统。

作用域 vs 生命周期
作用域:该变量的有效区域
生命周期:时间的概念,什么时候被开辟,什么时候被释放。

下面来介绍一下auto关键字
auto关键字的使用场景:一般在代码块中定义的变量,即局部变量,默认都是auto修饰的,不过一般省略;(auto不能修饰全局变量!!!
以后我们所学的,局部变量,自动变量,临时变量,都是一回事。我们统称局部变量。

#include <stdio.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);
		}
	} 

	return 0;
}

i用auto修饰可以吗?去掉j的auto可以吗?
答案是:都可以。但是auto关键字已经很老了,不再使用了。

2.register

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

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

对于寄存器,我们可以不关系硬件细节,只要知道CPU内集成了一组存储硬件即可,这组硬件叫做寄存器。

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

接下来我们来聊聊register这个关键字。

register 修饰变量:尽量将所修饰变量,放入CPU寄存区中,从而达到提高效率的目的,但是能否成功放入寄存器并不确定,由编译器决定。

那么什么样的变量,可以采用register呢?

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

另外需要格外注意的一点是:寄存器变量是不能进行取地址操作的,因为变量已经放在寄存区中了,而地址是内存相关的概念。

#include <stdio.h>
int main()
{
	register int a = 0;
	printf("&a = %p\n", &a);
	//编译器报错:错误 1 error C2103: 寄存器变量上的“&”
	//注意,这里不是所有的编译器都报错,目前我们的vs2019是报错的。
	
	return 0;
}

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

多文件介绍

在介绍static关键字之前,我们先来学习一下多文件中,关于全局变量和函数的一些结论。

创建项目,添加两个源文件test.c和main.c,如下:

在这里插入图片描述

在test.c中定义全局变量g_val和函数show,在main.c中使用全局变量和函数,如果有test1.c,test2.c,test3.c,。。。都要使用test.c中的全局变量和函数,怎么办?在每个源文件中,都使用extern关键字声明变量吗?单纯的使用源文件,组织项目结构的时候,项目越大越复杂的时候,维护成本变得越来越高!(如果test.c中变量名改变,在使用它的源文件中都需要改变,这叫维护成本高!)那么应该怎么办呢?由此出现了头文件 - .h文件,放所有的声明。

使用头文件,组织项目结构的时候,减少大型项目的维护成本问题。一般地,头文件的个数比源文件个数少一个,源文件多出一个main.c文件。

在多文件结构中,头文件是一定会被多个源文件包含的,那么就可能会有一个问题,头文件被重复包含的问题。怎么保证头文件不被重复包含?方法1:在头文件开头位置写入:#pragma once ; 方法2:条件编译,后续再学习。

哪些内容在头文件中定义:

1.包含c头文件
2.所有变量的声明
3.所有函数的声明
4.#define,类型typedef,struct

以后在源文件中(test.c),只包含与自己同名的头文件即可(tets.h),不再包含其他头文件了,对于其他头文件的包含,全部放到头文件中进行维护(tets.h).

在这里插入图片描述
注意:在这里,对于函数和变量的声明,不带extern也不会报错,但是建议,对于变量的声明,一定要把extern带上,否则看不出来到底是定义还是声明变量。函数声明建议带上extern,因为函数是声明还是定义,主要在于有没有函数体,带与不带效果相同,但是建议带上extern,表示这是声明。

对于函数声明,形参名可以省略,类型不能省略,但是建议形参名不要省略,函数声明和定义建议形式完全一致。

对于没有形参的函数,若在调用时传参,编译器不会报错。

3.static

问题:

1.全局变量可以跨文件访问吗?
可以。在一个源文件中可以访问其他源文件定义的全局变量,全局变量默认具有外部链接属性。

2.函数可以跨文件访问吗?
可以。在一个源文件中可以访问其他源文件定义的函数,函数默认具有外部链接属性。

3.在具体的应用场景中,有没有可能我们不想让全局变量或者函数跨文件访问,只想在本文件内部被访问?可以。

关于static有如下结论:

结论1:static修饰全局变量,该变量只在本文件内部访问,不能被外部其他文件直接访问,但是可以间接访问,可以通过调用该文件中的函数来访问(该函数中访问了该static变量)。不改变生命周期,改变了变量的作用域。

结论2:static修饰函数,该函数只能在本文件内被访问,不能在外部其他文件直接访问,但是可以间接访问,在该文件对外提供的其他接口函数中访问该函数。

static体现了封装的思想,项目维护,提供安全保证。

结论3:static修饰局部变量,更改该变量的作用域还是生命周期?更改生命周期。**临时变量生命周期变为全局的生命周期。**作用域没有改变。变量存放在内存的静态区的全局数据区。

有一定规模的项目,一定是多文件的,多文件是为了模块化,自己写的代码可以供别人使用,就一定要进行数据的“交互”(#include "test.h",main.c使用test.c中的函数), 如果不能跨文件,“交互”成本比较高。所以C语言默认将全局变量、函数设置为可以跨文件访问(外部链接属性)。

4.sizeof

C语言包含数据类型如下:
在这里插入图片描述
定义变量的本质:在内存中开辟一块空间,用来保存数据。而定义一个变量,是需要类型的,这个是基本语法决定的。那么,类型决定了:变量开辟空间的大小。

为什么要根据类型,开辟一块空间,直接将内存整体使用不好吗?不好。
任何时刻,都不是你一个程序在运行,还有很多其他程序也在运行。你整块用了,让别人怎么办?另外,你全都用了,一定需要在任何时刻,全部都用完吗?对于暂时不用的空间,但是给你了,对计算机来讲,就是浪费。

sizeof关键字经常被误认为是函数,其实是操作符

sizeof:确定一种类型对应在开辟空间的时候所占内存的大小。

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;
}
int main()
{
	int a = 1;
	printf("%d\n",sizeof(a));//正确
	printf("%d\n",sizeof(int));//正确
	printf("%d\n",sizeof a);//正确
	//printf("%d\n",sizeof int);//错误

	return 0;
}

sizeof在计算变量所占空间大小时,括号可省略,而计算类型(模子)大小时不能省略。sizeof操作符中不要有其他的运算,否则不会达到期望的效果。

5.unsigned、signed

原码 反码 补码

任何数据在计算机中,都必须转化成为二进制,为什么?因为计算机只认识二进制,对于有符号数,一定要能表示该数据是正数还是负数。计算机中的有符号数有三种表示方法,即原码、反码和补码,三种表示方法均有符号位和数值位两部分,一般用最高比特位来进行充当符号位,符号位都是用0表示“正”,用1表示“负”,而数值位三种表示方法各不相同。

对于有符号数且是正数, 原码 = 反码 = 补码;
对于有符号数且是负数:
原码:直接将二进制按照正负数的形式翻译成二进制就可以。
反码:将原码的符号位不变,其他位依次按位取反就可以得到了。
补码:反码+1就得到补码

  • 无符号数:不需要转化,也不需要符号位,原反补相同。
  • 对于整形来说:数据存放内存中其实存放的是补码

在做运算时符号位是否参与运算?答案是需要参与运算的。做运算时,使用的是补码。

大小端

如何理解大小端

地址有大小,数据有高权重位、低权重位,那么高权重位可以存放在低地址,也可以存放在高地址,这样就有两种方案。
在这里插入图片描述
在这里插入图片描述
大小端基本概念

CPU访存的基本单位是字节
数据按照字节,是有高权值位低权值为之分的。内存按照字节是有高地址,低地址之别的。
大端 按照字节为单位,低权值位数据存储在高地址处,叫大端存储
小端 按照字节为单位,低权值位数据存储在低地址处,就小端存储(小小小)

1字节数据(signed char,unsigned char)不存在大小端。

大小端是如何影响数据存储的?

大小端存储方案:本质是数据和空间按照字节为单位的一种映射关系。
存、取时要使用相同的方案,存、取是系统来完成的,用户不需要关心,直接使用。

在这里插入图片描述
举例:

//如何存储?
unsigned int a = -10;
//1111 1111 1111 1111 1111 1111 1111 0110  - 补码

在这里插入图片描述

深入理解变量内容的存入和取出

unsigned int a = 10;
unsigned int b = -10;//不会报错

在这里插入图片描述
结论:

:字面数据必须先转成二进制补码,再根据大小端放入空间当中。所以, 所谓符号位,完全看数据本身是否携带±号。和变量是否有符号无关!(存和变量数据类型无关)

:取数据,根据大小端取出二进制数据,然后一定要先看变量本身类型,然后才决定要不要看最高符号位。如果不需要,直接二进制转成十进制。如果需要,则需要转成原码,然后才能识别。(当然,最高符号位在哪里,又要明确大小端)

为什么都是补码?

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

int main()
{
	unsigned int  a = -10;
	//存:和目标变量的空间是没有关系的
	//存:与变量类型无关,目标空间不够,则发生截断
	//-10的补码
	//10000000 00000000 00000000 00001010 
	//11111111 11111111 11111111 11110101
	//11111111 11111111 11111111 11110110  - 补码
	//ff ff ff f6

	//取:看对应的数据变量的类型
	printf("%d\n",a);//-10
	printf("%u\n",a);//4294967286
	return 0;
}

在这里插入图片描述

int main()
{
	//-128在合法范围内
	//char范围:-128~127
	//共8bit,有2^8种组合
	char a = -128;
	printf("%d\n",a);//-128
	//原码:  1 1000 0000
	//反码:  1 0111 1111
	//补码: 1 1000 0000 
	//存入时,与变量的数据类型无关,因为目标空间不够,发生截断,符号位被丢弃
	//所以存入的是1000 0000
	//截断是不是发生了“错误”?这里是的!
	//取:因为a是有符号数,看符号位,符号位为1,所以是负数
	//补码转原码:无法正确转换,所以不用原反补转化,直接规定1000 0000 就是-128
	return 0;
}
//char:-128 ~ 127, -2^7 ~ 2^7-1
//short:-2^15 ~ 2^15-1

特定数据类型,能表示的数据取值范围(范围由多个连续数据构成),本质是多位比特位形成的排列组合的的个数。

看下面的例题:

int main()
{
	char a[1000];//char:-128~127
	for (int i = 0; i < 1000; i++)
	{
		a[i] = -1 - i;
	}
	printf("%d\n",strlen(a));//255
	
	//a[0] = -1
	//a[1] = -2
	//-1-1 = (-1) + (-1)
	//原码:1000 0000 0000 0000 0000 0000 0000 0001
	//反码:1111 1111 1111 1111 1111 1111 1111 1110
	//补码:1111 1111 1111 1111 1111 1111 1111 1111
	//-1+(-1)
	//   1 1111 1111 1111 1111 1111 1111 1111 1110
	//1111 1111 1111 1111 1111 1111 1111 1110 - 补码
	//1000 0000 0000 0000 0000 0000 0000 0010 - 原码 :-2
	//最高位溢出了 - 发生截断 -1-1 = -2
	//...
	//a[126] = -127
	//a[127] = -128
	//a[128] = -1-128 越界,变成正数  = 127
	//-1 +(-128)
 	//1000 0000 0000 0000 0000 0000 1000 0000 -- -128的原码
 	//1111 1111 1111 1111 1111 1111 0111 1111   --- -128反码
 	//1111 1111 1111 1111 1111 1111 1000 0000    --  -128补码
 	//   +
 	//1111 1111 1111 1111 1111 1111 1111 1111  --- -1的补码
 	//1 0000 0000 0000 0000 0000 0000 0111 1111 - 截断,最高位丢弃
	//0000 0000 0000 0000 0000 0000 0111 1111 - 符号位为0,表示是正数
	//值为127
	//a[129] = -1-129 = 126
	//...
	//a[255] = 0
	return 0;
}

在这里插入图片描述

#include <stdio.h>
#include <windows.h>
int main()
{
	int i= -20;
	//原码:1000 0000 0000 0000 0000 0000 0001 0100
	//反码:1111 1111 1111 1111 1111 1111 1110 1011
	//补码:1111 1111 1111 1111 1111 1111 1110 1100
	
	unsigned int j = 10;//字面常量10默认是int - 有符号
	//原码:0000 0000 0000 0000 0000 0000 0000 1010

	//1111 1111 1111 1111 1111 1111 1110 1100
	//   +
	//0000 0000 0000 0000 0000 0000 0000 1010
	//1111 1111 1111 1111 1111 1111 1111 0110 - 补码
	//1000 0000 0000 0000 0000 0000 0000 1001 - 反码
	//1000 0000 0000 0000 0000 0000 0000 1010 - 原码
	//-10
	printf("%d\n", i+j);
	return 0;
}
unsigned int j = 10u;//建议10后面加上u,表示10是unsignend int
#include <stdio.h>
#include <windows.h>
int main()
{
	unsigned int i;//i >= 0
	for (i = 0; i >= 0; i++)//判断条件恒成立
	{
		printf("%u\n", i);
	} 
	system("pause");
	return 0;
}

程序运行是死循环。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值