一.存储类型关键字
前言
补充1:内存
内存:用于暂时存放CPU中的运算数据,以及与硬盘等外部存储器交换的数据。它是外存与CPU进行沟通的桥梁,计算机中所有程序的运行都在内存中进行,内存性能的强弱影响计算机整体发挥的水平。
图解:
补充:外存:又称为辅存,是指除计算机内存及CPU缓存以外的存储器,此类存储器断电后仍能保存数据。常见的外存有硬盘、光盘、U盘等。CPU如果想访问外存中的数据,必须先把外存的数据保存到内存中,CPU再去读取内存中的数据。
思考:
一般我们点击图标打开程序,这一步计算机做了什么?
答:将程序加载到内存中
那为什么要将程序加载到内存中呢?
答:为了提高程序运行的速度。
为什么不用寄存器做为内存的主要部分,这样不是更快吗?
答:寄存器的成本过高,为了使计算机的成本更加亲民,使用了分布式缓存技术,降低了电脑的造价成本的同时,还使电脑的整体效率得到提升。
补充2:变量与内存的关系
变量的类型决定了此变量开辟的空间大小,开辟空间的大小又决定了所存数据的范围,而变量的地址决定了变量的唯一性,在内存中此变量的地址为变量的寻找提高了极大的效率。
举例:如同:要吃饭,不是所有的饭菜都要立马被你吃掉。饭要一口一口吃,那么你还没有吃到的饭菜,就需要暂时放在盘子里。这里的盘子,就如同变量,饭菜如同变量里面的数据
补充3:变量的分类
1.全局变量(具有外部链接属性,也就是其它文件能使用)
2.静态的全局变量(不具有外部链接属性,同理)
3.局部变量(在代码段中的变量)——进大括号创建,出大括号销毁
说明:代码块:用{}括起来的区域,就叫做代码块
说明:临时变量,自动变量 也称为局部变量
4.静态的局部变量(在代码段中被static修饰的变量)
关键字static具体可看:初始C语言第三篇:常用操作符与关键字
变量的具体内容请看:【初始C语言】变量与字符
int main()
{
int a = 0;
//这里的a变量在开辟的时候,会看其类型为int(4个字节),在内存中寻找4的字节的空间,并为其变量分配地址
//然后将0值放在a变量的空间中,使CPU能通过地址,准确地对变量a进行操作。
return 0;
}
补充4:存储类
1.静态存储区的变量:全局变量,static修饰的局部变量
2.动态存储区的变量:局部变量(前不加static)
静态存储区的变量的关键字:exern static
动态存储区的变量的关键字:register auto
补充5:删除数据是怎么删除的?
安装下载的时候,那么慢,但是删除只需要几秒这是为啥?
答:删除数据并不是真正的删除了,而是将数据设置为无效数据,这样新的数据就可以将原来的数据进行覆盖。
1.auto
auto 自动存储变量的关键字,实际用处不大。
1.仅在语句块内部使用。
2.不可修饰全局变量
3.修饰对象的生命周期和作用域都只在本代码块。
说明:不加修饰的局部变量的生命周期和作用域
生命周期:进大括号创建,出大括号销毁。
作用域:仅在本代码块中有效。
4.对象是具有自动存储期的变量:进入大括号创建出去大括号销毁。
错误提示:
auto int a = 1;//这是错误的,虽然能在VS2019上运行
int main()
{
printf("%d", a);
return 0;
}
2.register
1.修饰的对象为具有自动存储期的变量。(全局变量尽量不用,因为会长期占用寄存器)
2.register修饰的变量不能取地址,因为变量有可能呗放在寄存器里面,而寄存器没有地址。
3.register修饰的变量的值可以被修改
4.register修饰的变量 ,是被尽量放在寄存器里面,最终决定是否被放在寄存器里面是由编译器所决定的。
使用建议:
1.被高频使用的变量
2.尽量不要大量使用register
3.不用写回内存的变量
4.对象尽可能为局部变量
补充:如果寄存器中已满,则将由计算机硬件将寄存器中最久未使用的变量调入到高速缓冲存储器(Cache)中,同时放入你的变量。(这是由编译器的实现原理来决定的)
3.static
对象:函数,全局变量,局部变量。
1. 对于函数和全局变量来说,加上static将作用范围锁定在了本文件中,将外部链接属性改为内部链接属性,也就是说只能在本文件中使用,而在其他文件则不能使用,使变量和函数的使用更加安全。
2.对于局部变量来说,static将局部变量的生命周期改为全局,本质上是将局部变量由栈区移至堆区,但作用域并不会发生变化。
具体可看:常用操作符与关键字
4.extern
基本用法:
1.变量:extern +数据类型+变量名+; 注意:声明的时候不可对变量进行赋值。
2.extern是对外部变量进行的声明
基本功能
1.跨文件使用
对于全局变量(不被static修饰),和函数(不被static修饰),因为static会改变变量或者函数的外部链接属性,而导致声明无法使用。
说明:具体例子可看:常用操作符与关键字
2.逆顺序使用
当要在函数的定义之前使用此变量,(编译器是从上往下扫描的)要告诉编译器有这个函数,所以我们要声明一下(没有这个声明,函数也能进行运行,但是会提醒你未定义)。
例:
//extern int add(int, int); 这最好加上
int main()
{
int ret = add(1, 2);
printf("%d", ret);
return 0;
}
int add(int x, int y)
{
return x + y;
}
图解:
5.typedef
基本用法: typedef +已有类型+命名+; 注意:这里的;不可以省去
1.对不加修饰的类型进行重命名
举例:
typedef unsigned int u_int;
int main()
{
u_int j = 0;
return 0;
}
2.对结构体重命名
举例:
typedef struct Student
{
char name[10];
int age;
int sex;
char numbers[32];
}Student;
3.对数组类型重命名
typedef int arr[4];
int main()
{
arr x = {0};
//跟 int x[4]={0};等效
return 0;
}
4.对数组指针类型重命名
typedef int (*ptr)[4];
int main()
{
ptr a = NULL;
//我们可以把这里的ptr a理解为把a替换到int (*ptr)[4]的ptr中
//也就是int(*a)[4];
return 0;
}
5.函数指针重命名
举例:
typedef int(*ptr1)(int, int);
int add(int x, int y)
{
return x + y;
}
int main()
{
int (*p)(int, int) = NULL;
p = add;
int ret = (*p)(1, 2);
printf("%d\n", ret);
ptr1 p2 = add;
//同理我们可以把这里的ptr1 p2理解为把a替换到int(*ptr1)(int, int)的ptr1中
//也就是int (*p2)(int, int)
ret = (*p2)(3, 3);
printf("%d\n", ret);
return 0;
}
typedef与define的对比
typedef:是对类型进行封装,使之成为一个真正的类型。
define:定义类型是对类型进行替换
代码:
#define ptr int*
typedef int* ptr1;
typedef static int ptr1;//这个定义是错误的,因为这个算是对本身变量的修饰而改变了改变量的存储形式,
//typedef只能是封装一个存储类型,而int已经是一个存储类型了,所以加上static不能修饰。
int main()
{
int* p1, p2, * p3;//p1,p3为指针,p2为整形变量
ptr p4, p5, * p6;//p4,p6为指针,p5为整形变量
ptr1 p7, p8, p9;//p7,p8,p9都为指针
return 0;
}
二.数据类型关键字
数据类型关键字
char :声明字符型变量或函数
short :声明短整型变量或函数
int : 声明整型变量或函数
long :声明长整型变量或函数
signed :声明有符号类型变量或函数
unsigned :声明无符号类型变量或函数
float :声明浮点型变量或函数
double :声明双精度变量或函数
struct :声明结构体变量或函数
union :声明共用体(联合)数据类型
enum :声明枚举类型
void :声明函数无返回值或无参数,声明无类型指针
1.为什么要有那么多的数据类型?
数据类型决定开辟空间的大小和内存的存储方式,不同的数据根据需求需要放在不同的数据类型当中以便于能够准确使用。
图解:
说明:这是月饼的模具,你想要什么样的月饼,就有什么样的模具,对应到数据类型中是不是跟形象?你想存什么样的类型的数据,就用什么"模具"——数据类型
int main()
{
int x = 0;
//这里的意思是在空间中开辟4个字节,把0的补码放在内存中。
return 0;
}
2.数据类型的大小?数据类型的范围?
大小用sizeof关键字进行求解,范围根据类型能够存储的数据的空间和是否是有符号位和无符号位有关,无符号位的数据类型的范围是大于等于0的。
代码:求解基本类型的大小
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(char)); //1(单位字节)
//signed char (-2的八次方,2的八次方减一),这是闭区间
printf("%d\n", sizeof(short)); //2
//short (-2的16次方,2的16次方减一),这是闭区间
printf("%d\n", sizeof(int)); //4
//int (-2的32次方,2的32次方减一),这是闭区间
printf("%d\n", sizeof(long)); //32位时4,64位可能是8(取决于编译器)
//long(-2的32次方,2的32次方减一),这是闭区间
printf("%d\n", sizeof(long long)); //8
//long long(-2的64次方,2的64次方减一),这是闭区间
printf("%d\n", sizeof(float)); //4
float(-2的32次方,2的32次方减一),这是闭区间
printf("%d\n", sizeof(double)); //8
double(-2的64次方,2的64次方减一),这是闭区间
printf("%d\n", sizeof(void));//vs下是0,Linux下是1
return 0;
}
说明:如果数据类型为有符号位,则符号位为1其它数字为0存的是此类型的最小负数(这是半计算——是负数,规定——表示最小负数),编译器是直接识别此数据的补码而转换成最小数据的,可不要使用原码反码进行计算
3.数据在内存中是如何存储的?
数据类型
我们需要判断数据的大致类型,有整形家族,浮点型家族,构造类型(数组,结构体,枚举类型),void类型。
说明:void类型的变量是不一定不能够定义的;
1.在Linux系统下void类型的大小是1个字节,说明void类型有大小.
2.在VS2019下,void的大小是0,也就是开辟空间为0,也就是没开辟。既然没开辟空间那数据也无法放进去,所以vs会自动识别void是不能够进行定义变量的。
内存数据的存储
1.左值和右值的类型没有太大的冲突(在一个数据的大类)。
举例:比如说int和unsigned int都在整形家族中,没有太大的类型冲突,所以可以将右值数据在内存中的存储形式,直接放在左值的空间当中,但在存入时需要考虑截断(数据类型的大小)和大小端(内存存储的方式)的问题。
说明:
1.截断:
截断数据类型空间大的数据放在数据类型空间小的数据类型,需要将空间大的在内存中的数据进行从低位到高位开始数到目标变量的大小能存的最大位数为止.
2.大小端:
int main()
{
int a = 0x11223344;
return 0;
}
内存:
说明:前面的0x00EFF860是此变量的地址
为什么不是:11 22 33 44呢?
这里要引出大小端的问题:
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址
中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中
简单来说:大异小同。
4.数据到内存中的转换?
1.整型
正数
说明:源码,反码,补码相同。
举例1:
int a =1;
源码:00000000000000000000000000000001
反码:00000000000000000000000000000001
补码:00000000000000000000000000000001(在内存中实际存的数据)
注意:计算机并不是直接把正数的源码存进去了,而是把源码转换为补码存进去了。
负数
说明:遵循着运算逻辑——反码等于源码按位取反(符号位,也就是最高位不变,把源码中的0变成1,1变成0),补码等于反码加上1。
图解:
特殊:int的取值范围:-2147483648 ~2147483647
负数的最小值的存储为:10000000000000000000000000000000(这是补码,并且这是语法规定的)
因此我们可以这样记住负数与正数的存储形式:
我们可以看出,这是一个轮回,是不是很神奇?
2.浮点型
浮点数的在内存中的存储方式
可看:数据的存储形式
float类型的比较
1.浮点数的计算存在误差:一个数存到浮点数当中会发生一定的误差,这个误差很小。
2.浮点数进行计算两数相差是否等于0并不能直接与0进行比较, 应该与规定的最小的精度进行比较,如果比规定的最小精度小那就判断两值相等;反之不等。
说明:这个精度可以自己定义,因为有些时候可能相减的结果比最小的精度大,但我们看上去确实是相等的。
3.如果两数相差等于最小的精度,这时我们最好判断两值不相等,因为最小精度是规定与0不相等相等的最小精度,也就是只有比最小精度小才可以算做相等。
错误示范:
#include <sdtio.h>
int main()
{
float x = 1.0;
float y = 0.9;
float z = x - y-0.1f;
printf("%.23f\n", z);
if (fabs(z) == 0.0f)
{
printf("与0相等");
}
else
{
printf("与0不相等");
}
return 0;
}
正确示范:
int main()
{
float x = 1.0;
float y = 0.9;
float z = x - y-0.1;
printf("%.23f\n", z);
printf("%.23f\n", FLT_EPSILON);
if (fabs(z) < FLT_EPSILON)
{
printf("与0相等");
}
else
{
printf("与0不相等");
}
return 0;
}
5.强制类型转化与真类型转换
0的实际意义
1.字符0
2.字符换行符
3.空指针
#include <sdtio.h>
int main()
{
void* p = NULL;// ((void *)0)
char str = '0';// 48
char str1 = '\0';// 0
printf("%d %d %d", p, str, str1);
return 0;
}
真的类型转换比如把字符串“123456”和整形123456互相转换。
1.数字转字符
#include <sdtio.h>
void num_to_strings(int x)
{
if (x > 9)
{
num_to_strings(x / 10);
}
printf("%c", "0123456789"[x%10]);
//如果想要保存,则需要再传进去一个字符数组。
}
int main()
{
num_to_strings(123456);
return 0;
}
1.字符串转数字
说明:此代码只适用于全为数字的字符串中。
//字符数字转换为整形数字
void strings_to_num(char* p)
{
int len = strlen(p);
int num = 0;
for (int i = len; i > 0; i--)
{
int tmp1 = i;
int tmp2 = 1;
while (tmp1 > 1)
{
tmp2 = tmp2 * 10;
tmp1--;
}
num = num +((*p++) - 48) * tmp2;
//这一步是将字符转换为数字,再将数字乘以10的位数减一次方。
}
printf("%d", num);
}
int main()
{
strings_to_num("123456");
return 0;
}
6.自定义类型
三.条件判断关键字
if : 条件语句
else :条件语句否定分支
goto :无条件跳转语句
开关语句 (3个)
switch :用于开关语句
case :开关语句分支
default :开关语句中的“其他”分支
1.if语句与bool值
1.if语句执行的具体逻辑
1.对表达式进行求值
2.对表达式的结果(逻辑结果或者说bool结果)进行判断(0为假,非0为真)
3.如果为真执行if的下面的语句,这是分支功能。
说明:条件结构的语法的基本功能:
1.分支功能
2.条件判断功能
else与最近的if进行匹配
例如:
int main()
{
int i = 0;
int j = 1;
if (1 == i)
if (1 == j)
printf("hello world\n");
else
printf("i与1不相等");//else与最近的一个if进行匹配
//代码运行的结果为啥也不打印
return 0;
}
3.bool值
1.bool值到底有没有?
C89,C90没有,C99在stdbool.h中引入bool
说明:c99中用define定义bool类型,bool类型的大小为1个字节
注意:VS2019中有微软定义bool 是用int进行typedef的,但是不推荐,因为这样只能在VS的编译器上能跑过,其他平台上不一定能跑过。
4.switch(开关语句关键字)
- switch()中只能有整形家族的变量,常量,整形表达式
2.case 后边的情况取值只能用常量
3.switch()和case的情况的取值,共同实现判断功能,
4. case后面的代码块如果不加大括号是不能定义变量的。
5. break 将每个case分割开而实现分支功能
建议:
1.case里面的代码不建议带return
2.default处理默认情况也就是除了case之外的情况,建议放在最末尾
3.case情况排列按顺序或者按字母排序
除此之外:有一个比较有意思的代码
int main()
{
int i = 0;
scanf("%d", &i);
switch (i)
{
int j = 0;//这句代码,实际上并不会被执行,但是却被定义了
//这是因为在编译的时候,会将变量进行处理,导致在运行的时候,我们会
//看见这句代码实际上只起了定义变量的作用
case 1:
j = 1;
printf("%d\n", j);
printf("你好!\n");
break;
case 2:
printf("再见!\n");
break;
case 3:
printf("找个对你好的!\n");
break;
default:
printf("我emo了!\n");
break;
}
return 0;
}
5.goto
说明:go to实际上并不是不用,而是用的场景我们目前还接触不到。
具体语法:分支与循环(下)
四.循环语句关键字
循环操作符的基本结构
循环都包括三部分
1.条件初始化
2.条件判断
3.条件调整
说明:根据实际情况可以适当的少用上面的部分。
while
int main()
{
int i = 0;// 条件初始化
while (i < 9)// 条件判断
{
i++;//条件调整
printf("i=%d\n", i);
}
return 0;
}
do while
int main()
{
int i = 0;// 条件初始化
do
{
i++; //条件调整
printf("i=%d\n", i);
} while (i < 9);// 条件判断
return 0;
}
for
#include<stdio.h>
int main()
{
for (int i = 0; i < 9; i++)
//第一个部分为条件初始化
//第二部分为条件判断
//第三个部分为条件调整
{
printf("%d", i);
}
return 0;
}
break 的使用
break 只能用循环吗?答案是no,也可以用于switch分支语句中。
如果有多层循环/switch:
break跳过最进的一层循环。
break终止最近的一层switch
int main()
{
for (int i = 0; i < 9; i++)
{
printf("i==%d\n", i);
for (int j = 0; j < 9; j++)
{
printf("j==%d\n", j);
if (5 == j)
{
break;//当j等于5时,只会跳出离for循环最近的一层。
}
}
}
return 0;
}
continue
while
int main()
{
int i = 0;// 条件初始化
while (i < 9)// 条件判断
{
continue;//跳到条件判断部分
i++;//条件调整
printf("i=%d\n", i);
}
return 0;
}
do while
int main()
{
int i = 0;// 条件初始化
do
{
i++; //条件调整
continue;//跳到条件判断部分
printf("i=%d\n", i);
} while (i < 9);// 条件判断
return 0;
}
for
#include<stdio.h>
int main()
{
for (int i = 0; i < 9; i++)
//第一个部分为条件初始化
//第二部分为条件判断
//第三个部分为条件调整
{
continue;//跳到条件调整部分
printf("%d", i);
}
return 0;
}
死循环
do while
#include<stdio.h>
int main()
{
do
{
}while(1);//判断部分为非0,一直为真————这里只是举个例子,像2也可以
return 0;
}
while
#include<stdio.h>
int main()
{
while(1)//判断部分为非0,一直为真————这里只是举个例子,像2也可以
{
}
return 0;
}
for
#include<stdio.h>
int main()
{
int i = 0;
for( ; ; )
//这里可证明for循环内的三个部分都可省略,这没有判断进行的语句因此可一直进行。
{
}
for( i=1 ; ; )//这里将1赋给i一直为真,一直进行循环
{
}
for( ; 1; )//这里1为真,一直进行循环
{
}
return 0;
}
说明:只要判断部分一直为真就是死循环。
五 .返回与其余关键字
const :声明只读变量
sizeof :计算数据类型长度(以字节为单位)
volatile :说明变量在程序执行中可被隐含地改变
return :函数返回语句(可以带参数,也看不带参数)
1.const
代码1:
const int i = 0;//这称之为常变量,这本质上是变量,就像披着羊皮的狼
//常变量是不能直接被修改的!
int *p =(int*)&i;
*p = 1;
//可以通过地址来进行间接修改
代码2:
int main()
{
//这是真正意义上的不可修改的变量
const char* p = "hello world\n";//加const与否都不能修改*p
return 0;
}
代码3:
const 修饰多级指针,具有就近原则。
int main()
{
char* ptr = NULL;
char*const* p = &ptr;
//说明:*p不能修改
const char** p1 = &ptr;
//说明:**p1不能进行修改
return 0;
}
说明:const修饰的变量并不是常量!
2.sizeof
说明:
1.sizeof是关键字也是操作符,但不是函数。
2.sizeof的值为unsigned int
#include<stdio.h>
int main()
{
printf("%d\n", sizeof(char)); //1(单位字节)
printf("%d\n", sizeof(short)); //2
printf("%d\n", sizeof(int)); //4
printf("%d\n", sizeof(long)); //32位时4,64位可能是8(取决于编译器)
printf("%d\n", sizeof(long long)); //8
printf("%d\n", sizeof(float)); //4
printf("%d\n", sizeof(double)); //8
printf("%d\n", sizeof(void));//vs下是0,Linux下是1
return 0;
}
3.return
1.既然函数在返回时会销毁,那返回值是如何进行保存的?
答:是通过寄存器eax进行保存的,然后返回的。
2.return能返回空值吗?
答:函数的返回类型为void能返回空值。
具体可见:函数栈帧
4.volatile
说明:
1. 表明变量可能被修改
2.告诉编译器不要直接进行优化,也就是把变量直接加载到寄存器中,就直接在CPU进行判断,而不考虑变量在内存中是否被改变,就好像是背水一战的感觉。
举例:
int main()
{
int flag = 0;
while (flag);
return 0;
}
说明:
我们在判断条件时,通常是把内存的值放在寄存器中,然后再进行判断,然后重复此循环,但是为了进行优化编译器会直接判断是否需要直接放在寄存器中,死循环会,然后直接放在寄存器中进行判断,之后的每一次循环都会直接从寄存器中进行拿数据,而不再从编译器中再拿数据了。
五 总结
C语言一共多少个关键字呢?一般的书上,都是32个,但是这个都是C90(C89)的标准。其实C99后又新增了5个关键字。不过,目前主流的编译器,对C99支持的并不好,我们后面默认情况,使用C90,即,认为32个。
数据类型关键字
char :声明字符型变量或函数
short :声明短整型变量或函数
int : 声明整型变量或函数
long :声明长整型变量或函数
signed :声明有符号类型变量或函数
unsigned :声明无符号类型变量或函数
float :声明浮点型变量或函数
double :声明双精度变量或函数
struct :声明结构体变量或函数
union :声明共用体(联合)数据类型
enum :声明枚举类型
void :声明函数无返回值或无参数,声明无类型指针
控制语句关键字(12个)
1 循环控制(5个)
for :一种循环语句
do :循环语句的循环体
while :循环语句的循环条件
break :跳出当前循环
continue :结束当前循环,开始下一轮循环
- 条件语句(3个)
if : 条件语句
else :条件语句否定分支
goto :无条件跳转语句
- 开关语句 (3个)
switch :用于开关语句
case :开关语句分支
default :开关语句中的“其他”分支
- 返回语句(1个)
return :函数返回语句(可以带参数,也看不带参数)
存储类型关键字(5个)
auto :声明自动变量,一般不使用
extern :声明变量是在其他文件中声明
register :声明寄存器变量
static :声明静态变量
typedef :用以给数据类型取别名(但是该关键字被分到存储关键字分类中,虽然看起来没什么相关性)
其他关键字(3个)
const :声明只读变量
sizeof :计算数据类型长度
volatile :说明变量在程序执行中可被隐含地改变