C语言关键字再谈上
前言
本文主要是针对C 语言中各个关键字的整理总结,当然了,关键字在各个语言中大多通用,其他语言也可参考。
适合刚进行语言学习的小白,或是在学习完一门语言(C语言)之后进行基础巩固和提高,文章中设计的关键字是以C语言为标准的,主要参考C90 标准,C99 标准也有涉及,相关代码使用VS2019 编译运行,部分知识参考自网络,书籍参考自《C语言深度解剖(第2版)》
这只是第一部分,我会陆续更新之后的内容的。
关键字分类
一般以C90标准,认为C语言关键字为32个,虽然后来C99 又增加了5个关键字,但是影响不大,任然以C90 标准为主。下面会主要介绍几个重要的关键字。
程序运行
编写程序,就是由 文本文件 -> 可执行程序(二进制文件),运行的就是可以双击运行的 .exe 文件
- 在win 中,双击的本质是运行程序,将程序(从硬盘)加载到内存中
- 任何程序在运行前都必须被加载到内存中
- 程序没有被加载的时候,在硬盘中
- 至于要加载到内存中是为例匹配CPU的速度
所有变量都要在内存的某个位置开辟变量
即变量就是在内存中开辟空间,以供程序使用
为什么要定义变量
在程序运行时,并不是所有数据都要立刻被计算执行,而是要暂时保存一下,在需要的时候再处理
定义与声明
定义:开辟空间使用,只能定义一次
声明:告知程序使用,声明可以多次
最宽宏大量的关键字—auto
auto 在缺省的情况下,编译器默认所有变量都是auto的
一般认为在代码块内(即 { } 范围内定义的为局部变量)定义的变量为局部变量,在内存的 栈 区保存
在代码块外定义的变量为全局变量,在内存的全局静态区保存
下面这个例子可以理解变量的作用域
#include <stdio.h>
int main()
{
int x = 10;
if (10 == x)
{
int y = 20;
printf("code1: %d, %d\n", x, y);
}
//编译会报错,显示y 未定义,y只在代码块内有效
printf("code2: %d, %d\n", x, y);
return 0;
}
变量的生命周期
生命周期的定义:该变量从定义到被释放的时间范围,释放就是指曾经开辟的空间被释放
局部变量:进人代码块,形成局部变量【开辟空间】,退出代码块,【释放空间】
全局变量:定义完成之后,程序运行的整个生命周期内,该变量一直有效
如果全局变量与局部变量出现命名冲突,则优先使用局部变量的定义信息
#include <stdio.h>
int g_val = 100; //定义一个全局变量
int main()
{
int g_val = 200;
printf(" %d \n", g_val);
return 0;
}
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 = 1; //默认隐藏的auto 关键字
printf("before: %d\n", j);
j++;
printf("after: %d\n", j);
}
}
return 0;
}
最快的关键字—register
可以使用register 修饰的变量:
- 局部的(全局变量会导致CPU寄存器被长时间占用)
- 不会被写入的(写入需要些内存,就失去了register 的意义)
- 高频被读取的(为了提高效率)
- 如果要使用,请不要大量使用,因为寄存器数量有限
register修饰的变量,不能取地址,因为已经在寄存器区了
#include <stdio.h>
int main()
{
register int pass = 10; //定义一个存放在寄存器区的局部变量
pass = 200;
printf("%d\n", pass);
return 0;
}
最名不副实的关键字—static
头文件的必要性
-
单纯的使用源文件,组织项目结构的时候,项目越大越复杂的时候,维护成本就会变高,所以在实际开发的时候,就会使用头文件,减少大型项目的维护成本,所以可以多使用头文件来集中管理各个变量和函数
-
一般头文件与实现文件同名,头文件要被多个源文件包含,可能会有被重复包含的问题,在VS中可以使用 #program once 来防止被重复包含
-
主要包含系统的头文件,所有变量的声明,所有函数的声明,自定义的数据类型(结构体)等
-
函数的定义和声明必须完全一致,方便后期查看阅读
test.h
#pragma once
#include <stdio.h>
extern int g_val = 100;
//extern 表示仅仅只是声明,而不是定义,可以多次声明,但是不可以多次定义,所以在变量的声明时最好使用extern 修饰
void show();
test.c
#include "test.h"
void show()
{
printf("hello world!\n");
}
main.c
#include "test.h"
//int g_val = 100; //定义一个全局变量
//声明并没有开辟空间,没有办法赋值,所有变量在声明变量时,不能设置初始值
extern int g_val = 100;
int main()
{
printf("%d\n", g_val);
show();
return 0;
}
多文件下,变量声明必须使用 extern 修饰,函数声明建议使用 extern(函数本身就有extern 属性)
static的好处
函数和全局变量都可以跨文件访问 在具体的应用场景中,我们是不想让某个变量/函数实现跨文件访问的
static 关键字修饰的全局变量,只能在本文件内被访问,不能被外部其他文件直接访问
static关键字修饰的函数,只能在本文件内被访问,不能在外部被其他文件直接访问
使用static关键字可以实现函数访问的加密,就像C++ 的封装,可以增加安全性
使用static 关键字可以很好的提供安全保证和项目维护
static修改的是什么?
static关键字限制的是作用域,而不是生命周期
static 修饰局部变量
使用static修饰的局部变量,具有 局部临时性,由函数调用开辟空间并初始化,函数结束释放空间
#include "test.h"
static void fun()
{
int i = 0;
i++;
printf("i= %d , [ %p ] \n", i,&i);
}
int main()
{
for (int i = 0; i < 10; i++)
{
fun();
}
return 0;
}
下面是两次的运行结果:
不同的运行结果是不一样的,可见其每次的地址空间是不同的,只是这里VS 在申请空间的时候,每次都把相同的地址空间都分配给了 i ,实际上是每次调用 fun 函数都在重新调用新的地址空间。
static 修饰局部变量
如果将上面例子的 i 变为static修饰的局部变量,观察运行结果:
#include "test.h"
static void fun()
{
static int i = 0;
i++;
printf("i= %d , [ %p ] \n", i,&i);
}
int main()
{
for (int i = 0; i < 10; i++)
{
fun();
}
return 0;
}
运行结果:
![static修饰局部变量的运行结果](https://img-blog.csdnimg.cn/219b10d918f74cf1a7d14ee9c9377338.png#pic_center
可见,static修饰局部变量,更改局部变量的声明周期,将临时变量变为全局的生命周期,可以用下面这个例子来加深对生命周期和作用域的理解:
#include "test.h"
int* p = NULL;
static void fun()
{
static int a = 100;
p = &a;
}
int main()
{
printf("%d\n", a); //会报错,a 是未定义的标识符,应为它只是有 全局的声明周期,而没有全局的作用域
fun(); //如果是局部变量,那么在函数调用结束时就会被释放,但是结果并没有
printf("%d\n", *p); //获取指针的解引用,如果该函数的局部变量已经被释放,得到的应该是随机值,而不是100
return 0;
}
下面是运行结果:
内存布局
基本数据类型
sizeof 关键字:编译时确定一种类型在开辟空间时的大小
printf("size: %d\n", sizeof(char)); //1
printf("size: %d\n", sizeof(short)); //2
printf("size: %d\n", sizeof(int)); //4
printf("size: %d\n", sizeof(long)); //4
printf("size: %d\n", sizeof(long long)); //8
printf("size: %d\n", sizeof(float)); //4
printf("size: %d\n", sizeof(double)); //8
确定大小的方法:
int a = 10;
printf("%d\n", sizeof(a));
printf("%d\n", sizeof(int));
printf("%d\n", sizeof a);
C语言中类型的使用
C语言要使用内存,本质是对内存进行合理划分,按需索取
就是为了满足不同的计算需求,要使用合适的变量来保存,由于计算机硬件资源有限,为了要满足不同的应用场景,解决不同的应用场景的计算方法也不同,需要的空间也是不同的,本质就是使用最小的成本,解决各种多样化的场景问题
变量命名规则
变量命名直观可以阅读,要做到 见名知意,推荐使用大小驼峰命名法,使用数字、字母、下划线的组合命名
- 推荐使用数字、字母、下划线的组合进行命名
- 命名长度不宜过长,较长的单词可以选择去掉“元音”进行缩写,英文词尽量不缩写,特别是专业名词,在同一系统中对同一个单词必须使用相同的表达方法,并且注明其意思
- 大驼峰命名法:标识符由多个词组成时,每个词的第一个字母大写,其余全部小写:int CurretnVal;
- 尽量避免名字中出现数字编号,除非逻辑上确实需要编号,比如驱动开发时的 引脚 命名
- 对在多个文件之间共同使用的全局变量或函数要加范围限定符(可以使用模块名(缩写)作为范围限定符)
- 变量命名前缀可以参考 [ 匈牙利命名法 ],但是现在不是很推荐,仅使用 g 前缀来表示全局变量就可以了
- 程序中不得出现以大小写区分的变量名
- 所有宏定义、枚举常量、只读变量 全部用大写字母命名,用下划线分割单词 : #define INT_MAX 100
- 对于习惯问题,局部变量可以采用通用的命名方式,但是仅限于 n ,j ,k 等作为循环变量使用
一般习惯上使用n ,m ,i ,j ,k 等表示 int 类型的变量;
c ,ch 等表示字符型变量;
a 表示数组;
p 表示指针;
仅仅作为习惯,除了 i ,j ,k 等可以作为表示循环变量之外,别的字符变量名尽量不要使用 - 结构体等被定义时,必须要有明确的结构体名,所有的 结构 和 联合 的类型在转换单元的结尾应该是完整的,结构或联合的完整声明应该包含在任何涉及结构的转换单元之内
- 定义变量的同时一定要进行初始化,定义变量时编译器并不一定清空了该块内存,它的值可能是无效数据或随机值,VS编译器甚至不能通过编译,gcc 编辑器可以通过编译
- 不同类型数据之间的运算要注意精度扩展问题,一般 低精度 向 高精度 数据扩展
- 禁止使用八进制的常数(0 除外,因为严格意义上来讲 0 也是八进制数 )和八进制的转义字符
在计算机中,任何以 0 开头的数字都认为是 八进制格式的数字(十六进制的 0x 不算 ) ,所以我们写固定长度的数字的时候是有一定风险的。
最冤枉的关键字sizeof
sizeof 可以用来求一些数据类型的大小
int* p = NULL;
int arr[10];
int* test[3];
printf("%d\n", sizeof(p)); //指针变量的大小 4
printf("%d\n", sizeof(arr)); //数组的大小,不是数组的首地址的大小, 40
printf("%d\n", sizeof(test)); //指针数组的大小,数组内有三个指针,每个指针大小为4, 12
数据存储的概念
数据在计算机内部已二进制形式存储,在计算机中任何数据都被转化为二进制存储
存储的本质
unsigned int b=-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
但是整形在存储时,空间是不关心内容的,在将数据保存在空间内时,数据已经被转化为二进制,所以是可以放到 unsigned int 申请到的空间中的,并且不会有任何问题,而 unsigned int 在读取时才会起限制作用,即如何解释空间内部的信息时,进行表示
unsigned int a = -10;
printf("整形输出:a = %d\n", a);
printf("无符号输出:a = %u\n", a);
//输出结果:
//整形输出:a = -10
//无符号输出:a = 4294967286
//这里的 a 是无符号数,在 以 %d 进行输出使,可以正常解释为 int 型进行输出,就得到了 -10,但是如果是以无符号数进行输出时,%u,存储在计算机内部的二进制补码,就被认为是其原码了,因为是无符号数,原码、反码、补码都一样,所以将其补码作为原码转换为十进制就进行输出了,就得到了 4294967286
变量存储过程:字面数据必须 先转换为补码,再放入空间中 ,所以,所谓的符号位,完全看数据本身是否携带 + - 号,和变量是否有符号无关
取得变量的过程:取数据一定要先看变量本身的类型,然后才决定要不要看最高符号位,如果不需要,直接二进制转换为十进制,如果需要则需要转换为原码,然后才可以识别(要识别最高符号位在哪里,还需要明确大小端)
大小端问题
什么是大小端
在内存中,每个字节在内存中都有不同的存储地址,所有地址都是不相同的,有大小之分,即地址高低之分
数据在存储到内存中时,也是以字节为单位,分成若干块,即分为高权值位和低权值位
而如何将数据存储到内存中,就是大小端问题要研究的地方了
大端存储:
按照字节为单位,低权值位数据存储在高地址处
小端存储
按照字节为单位,高权值位数据存储在低地址处
如果 记忆的话,可以:小端存储就是:小小小 --> 小端:安装字节为单位,低权值(小)数据存储在低地址(小)
大小端如何影响数据存储:
大小端存储本质上是数据和空间 按照字节为单位的一种隐射关系
如果以大端存储来存储数据,就需要以大端存储来取出数据
以小端存储(32位机器)为例:
存储:将数据转换为32位二进制数(左边为高权值,右边为低权值) --> 在内存中开辟一块对应大小的空间 --> 将高权值 位 存储到高地址空间,低权值位 存储到低地址空间 --> 存储完成
取值:先看自身类型,确定是否需要符号位等 --> 判断是大端存储还是小端存储,从内存中取出数据,由符号位判断是否需要符号 --> 得到二进制数 --> 转换为对应进制数据,显示出来
几道编程题,来加深对于以上知识的理解
1.字符数组的长度
下面的用来计算长度:
printf("%d\n", '\0');
char a[1000];
for (int i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d\n", strlen(a)); //返回字符串的长度
/*
运行结果:
0
255
*/
**这里要注意的是,数组a 是字符型的,每个元素的大小为 一个字节,一个字节能够存储的十进制数的范围是 -128~127 , 所以在 for 循环中,当 i = 127时,该元素的值为-128,还可以存储,当i = 128时,在进行计算存储时,就是 -1 和 -128 做加法运算,转换为补码进行计算,就是 1111 1111 和 1000 0000 做和,得到的二进制数为 1 0111 1111 由于只有一个字节(8位),所以就要发生截断,存储的是 0111 1111 , 所以转化为十进制就是127,下一个当 i = 129时,该元素的十进制值就是 126了,==后面随着 i 的增大,a[i] 的值就有逐渐变小,一直到 a[255] = 0 , 由于a[i] 是字符型变量,所以它就被解释为 ASCII 码了,而十进制数字0对应的 ASCII码值就是 \0 所以该字符数字在输出长度的时候,就只到 \0 到结束, 得到的输出结果就是 255 **
2.不同类型的差异
int i = -20;
unsigned int j = 10;
printf("以有符号数输出结果:%d\n", i + j);
printf("以无符号数输出结果:%u\n", i + j);
/*
输出结果:
以有符号数输出结果:-10
以无符号数输出结果:4294967286
*/
这里如果以有符号数数进行输出的话,就是简单的十进制数字的计算问题,就很好理解了,结果就是 -10了,但是,如果是以无符号数进行输出,就需要考虑数字运算在计算机内部的计算过程了,首先将 -20 和 10 在计算机内部的存储的二进制数据表示出来,即将十进制转化为32位二进制数,然后将其转化为补码,准备进行计算,分别是: 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 00000 00000 1010,如果是以有符号来进行转换的就是十进制的 -10 ,如果是以无符号数进行解释的话,就是直接将这个二进制数解释为十进制数,就是十进制的 4294967286。所以数据的存储形式都是一样的,只是不同类型的解释不同,结果就不同。
所以在实际编程中,可以将无符号数在定义时,在其后面添加 u 后缀
unsigned int a = 23u;
printf("a=%d", a);
//运行结果:a=23
计算机内部的存储的二进制数据表示出来,即将十进制转化为32位二进制数,然后将其转化为补码,准备进行计算,分别是: 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 00000 00000 1010,如果是以有符号来进行转换的就是十进制的 -10 ,如果是以无符号数进行解释的话,就是直接将这个二进制数解释为十进制数,就是十进制的 4294967286。所以数据的存储形式都是一样的,只是不同类型的解释不同,结果就不同。
所以在实际编程中,可以将无符号数在定义时,在其后面添加 u 后缀
unsigned int a = 23u;
printf("a=%d", a);
//运行结果:a=23
这只是第一部分,我会陆续更新之后的内容的。
感谢观赏,慢慢提高