本章主要介绍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 = 10;
char 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呢?
- 局部的(全局会导致CPU寄存器被长时间占用);
- 不会被写入的(写入就需要写回内存,后续还要读取检测的话,register的意义在哪呢?);
- 高频被读取的(提高效率所在);
- 如果要使用,请不要大量使用,因为寄存器数量有限。
另外需要格外注意的一点是:寄存器变量是不能进行取地址操作的,因为变量已经放在寄存区中了,而地址是内存相关的概念。
#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;
}
程序运行是死循环。