必读:
- C语言关键字是一个非常重要的话题,因为它能在相当的程度上将C语言的核心内容串联起来,起到一种提纲挈领的效果
- 下面的内容重点提及的是相应关键字特别值得注意的地方,这些地方是我们经常忽略的,而且考试也会经常涉及到
- 讲解这些关键字时默认大家都有C语言的基础,因此不会从0开始谈起
- 一:auto关键字
- 二:register关键字
- (1)存储器分级
- (2)register修饰变量
- 三:static关键字
- (1)修饰全局变量和函数
- (2)修饰局部变量
- 四:sizeof关键字
- 五:signed、unsigned关键字
- 六:if、else
- (1)关于C语言中bool类型
- (2)float与“零值”的比较
- (3)if和else的匹配问题
- 七:switch-case组合
- 八:do 、while 、for关键字
- 九:goto关键字
- 十:void关键字
- 十一:return关键字
- 十二:const关键字
- 十三:volatile关键字
- 十四:extern关键字
- 十五:struct关键字
- 十六:Union关键字
- 十七:enum关键字
- 十八:typedef关键字
- 总结
- (1)关键字分类
一般来讲,C语言一共有32个关键字(C90标准),当然C99后又新增了5个关键字,不过我们还是重点讨论这32个关键字
| 关键字 | 说明 |
|---|---|
| auto | 声明自动变量 |
| short | 声明短整型变量或函数 |
| int | 声明整形变量或函数 |
| long | 声明长整形变量或函数 |
| float | 声明浮点型变量或函数 |
| double | 声明双精度变量或函数 |
| char | 声明字符型变量或函数 |
| struct | 声明结构体变量或函数 |
| union | 声明共用数据类型 |
| enum | 声明枚举类型 |
| typedef | 用以给数据类型取别名 |
| const | 声明只读变量 |
| unsigned | 声明无符号类型变量或函数 |
| signed | 声明有符号类型变量或函数 |
| extern | 声明变量是在其它文件中正声明 |
| register | 声明寄存器变量 |
| static | 声明静态变量 |
| volatile | 说明变量在程序执行过程中可以被隐含地改变 |
| void | 声明函数无返回值或无参数,声明无类型指针 |
| if | 条件语句 |
| else | 条件语句否定分支(与if连用) |
| switch | 用于开关语句 |
| case | 开关语句分支 |
| for | 一种循环语句 |
| do | 循环语句的循环体 |
| while | 循环语句的循环条件 |
| goto | 无条件跳转语句 |
| continue | 结束当前循环,开始下一轮循环 |
| break | 跳出当前循环 |
| default | 开关语句中的“其它”分支 |
| sizeof | 计算数据类型长度 |
| return | 子程序返回语句,循环条件 |
一般来说,在代码块中定义的变量(也即局部变量),默认都是auto修饰的,不过会省略。但是一定要注意:不是说默认的所有变量都是auto的,它只是一般用来修饰局部变量
当然在C语言中,我们已经不再使用auto了,或者称其为过时了,但是在C++中却赋予了auto新的功能,它变得更加强大了。有兴趣请点击2-6:C++快速入门之内联函数,auto关键字,C++11基于范围的for循环和nullptr
二:register关键字register意味寄存器
(1)存储器分级这个概念我们在计算机组成原理中讲得已经非常详细了,请点击:(计算机组成原理)第三章存储系统-第一节:存储器分类、多级存储系统和存储器性能指标
(2)register修饰变量可以看出,如果将变量放到寄存器中,那么效率就会提高。可以用register修饰的变量有以下几种
- 局部的(全局变量会导致CPU寄存器长时间被占用)
- 不会被写入的(写入的话就需要被写回内存,要是这样的话register就没有意义的)
- 高频需要被读取的
如果要使用,不要大量使用,因为寄存器的数量有限。
另外还需要注意的一点是:被register修饰的变量,是不能取地址的,因为它已经放在了寄存器中,地址会涉及到内存,但是可以被写入
当然这个register关键字现在也基本不会用了,因为如今的编译器优化已经很智能了,不需要你自己手动优化
我们知道全局变量(加入关键字extern声明)和函数都可以跨文件使用的
但是有一些应用场景中,我们不想让全局变量或函数跨文件访问应该怎么办呢?那么就可以使用static关键字
static int g_value=100;//修饰staic后全局变量将不能跨文件使用
可以看出被static修饰的全局变量是不能被外部其他文件直接访问的,而只能在本文件内使用
- 需要注意这里说的是直接访问,那意味着可以间接访问,比如通过函数的方式实现
同样,被static修饰的函数只能在本文件内访问,而不能在外部其它文件中直接访问
- 还是需要注意,这里是不能直接访问,并不是不能访问,比如可以通过函数嵌套的方式
static这种功能本质为了封装,因为我们可以把一些不需要或者不想要暴露的细节保护起来了,只提供一个功能函数个,该函数在内部调用它们即可,这样的话代码安全性也比较高
(2)修饰局部变量我们知道全局变量仅在当前代码块内有效,代码块结束之后局部变量会自动释放空间,因此下面代码的结果就会是这样
如果使用static修饰局部变量,会更改其生命周期,但其作用域不变,如下当用static修饰后,变量i地址不变,且结果累加
static为什么可以更改局部变量的生命周期呢?因为被static修饰的变量会将其从栈区移动到数据段,当然这就涉及到了C/C++地址空间的问题了
查看实际地址
#include四:sizeof关键字#include int gobal_val=100;//全局变量已经初始化 int gobal_unval;//全局变量未初始化 int main(int argc,char* argv[],char* env[]) { printf("main函数处于代码段,地址为:%p,十进制为:%dn",main,main); printf("n"); printf("全局变量gobal_val,地址为:%p,十进制为:%dn",&gobal_val,&gobal_val); printf("n"); printf("全局变量未初始化gobal_unval,地址为:%p,十进制为:%dn",&gobal_unval,&gobal_unval); printf("n"); char* mem=(char*)malloc(10); printf("mem开辟的堆空间,mem是堆的起始地址,是%p,十进制为:%dn",mem,mem); printf("n"); printf("mem是指针变量,指针变量在栈上开采,其地址为%p,十进制为:%dn",&mem,&mem); printf("n"); printf("命令行参数起始地址:%p,十进制为:%dn",argv[0],argv[0]); printf("n"); printf("命令行参数结束地址:%p,十进制为:%dn",argv[argc-1],argv[argc-1]); printf("n"); printf("第一个环境变量的地址:%p,十进制为:%dn",env[0],env[0]); printf("n"); }
sizeof用于确定一种类型对应在开辟空间的时候的大小,注意它是关键字而不是函数
它的基本用法就是下面这样,这我就不再多说了(注意Windows32位平台)
int main()
{
cout <<"char:" <
特别注意,sizeof求一种类型大小的写法共有三种,特别第三种很多人认为是错误的,而考试就爱给你整这些犄角旮旯的东西
int main()
{
int a = 10;
第一种:cout << sizeof(a) << endl;
第二种:cout << sizeof(int) << endl;
第三种:cout << sizeof a << endl;//这种写法其实也证明了sizeof不是函数
cout << sizeof int << endl;//注意这种写法是错误的
}
五:signed、unsigned关键字
这一部分需要涉及数据存储及原码反码等基础概念,请参照以下章节
- (计算机组成原理)第二章数据的表示和运算-第二节1:定点数的表示(原码、反码、补码和移码)
- (计算机组成原理)第二章数据的表示和运算-第二节2:原码、反码、补码和移码的作用
第一点: 需要深刻理解signed和unsigned只是对数据的一种解读方式,其中signed会把首位数据解读为符号位,符号位用于标识其正负,unsigned的首位也算作数据位,也就是说类型决定了其读写的时候的解释方式
因此像下面的这样一句代码,看似不合适,但是它是没有问题的,因为存储时对于变量a它只关心我所开辟的空间上的二进制数据放进了没有,并不关心你之前是怎么样的
unsigned int b=-10;
-10的原码:1000 0000 0000 0000 0000 0000 0000 1010
-10的反码:1111 1111 1111 1111 1111 1111 1111 0101
-10的补码:1111 1111 1111 1111 1111 1111 1111 0110
也就是说b里面的存储的内容会按照不同的解释方式而变化
第二点: signed和unsigned也是相关C语言考试的重点,下面代码可以帮助你很好的理解
#include
#include
#include
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%un", i);
Sleep(100);
}
}
由于变量i是无符号整形,因此在与0比较的时候,不会小于0,所以会死循环,并且打印时从9开始减小到0,然后接着是42亿多,然后依次减小,最后再到0
第三点:使用unsigned时初始化变量时,建议带上u,也即
unsigned int b=10u;
六:if、else
if和else如果简单点学其实也很简单,主要就是以下内容
- 0为表示假,非0表示真
- if语句执行时,必然是先执行“()”里面的表达式或者是函数,得到真假后,然后进行判定,再进行分支功能
(1)关于C语言中bool类型
在C99之前C语言是没有bool类型的,在C00之后引入了_Bool类型,它处于头文件stdbool.h中
#include
#include
#include
int main()
{
bool ret = false;
ret = true;
printf("%dn", sizeof(ret));//在vs中为1
return 0;
}
源码中显示就是一个宏定义
//
// stdbool.h
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The C Standard Library header.
//
#ifndef _STDBOOL
#define _STDBOOL
#define __bool_true_false_are_defined 1
#ifndef __cplusplus
#define bool _Bool
#define false 0
#define true 1
#endif
#endif
(2)float与“零值”的比较
使用if进行浮点数比较时,下面的代码正确吗?按照道理1.0-0.9=0.1,应该是正确的
int main()
{
double x = 1.0;
double y = 0.1;
if ((x - 0.9) == y)
{
printf("correctn");
}
else
{
printf("wrongn");
}
return 0;
}
但实际结果却是:
为什么会这样呢,其实这涉及到的的浮点数如何在计算机中存储的问题,详细细节请移步:
- (计算机组成原理)第二章数据的表示和运算-第三节1:浮点数的表示
其实如果你打印出来后,你会发现两者根本不相等,精度丢失
既然浮点数比较时不能直接使用“==”,那么应该怎么办呢?
比较时,有点像高等数学中的取极限,
δ
delta
δ可以被视为一个误差范围,这个
δ
delta
δ需要你自己定义,当两者的绝对值之差小于该范围时,C语言就认定他们相等,否则不相等
int main()
{
double x = 1.0;
double y = 0.1;
if (fabs((1.0 - 0.9)-0.1) < CMP)
{
printf("correctn");
}
else
{
printf("wrongn");
}
return 0;
}
这里的
δ
delta
δ其实C语言已经帮我们定义好了,处在float.h头文件之下
#define DBL_EPSILON 2.2204460492503131e-016
#define FLT_EPSILON 1.192092896e-07F
回归到主题,如果0被定义为了浮点数,我们要判断某个数是否是0的话可以这样写
int main()
{
double x = 0;
if (fabs(x) < DBL_EPSILON)//注意不要写成<=
{
printf("x是0n");
}
else
{
printf("x不是0n");
}
return 0;
}
(3)if和else的匹配问题
这是一个老生常谈的话题。下面代码看似会输出“2”,但实际什么都不会输出
int main()
{
int x = 0;
int y = 1;
if (10 == x)
if (11 == y)
printf("1n");
else
printf("2n");
return 0;
}
这属于代码风格问题,else匹配采用的是就近原则
七:switch-case组合
第一: switch case的基本语法结构
switch(整型变量/常量/整型表达式)//注意只能这三种
{
case var1://判断在这里
break;
case var2:
break;
case var3:
break;
default:
break;
}
其中case完成的判断功能,break完成的是分支功能,所以如果忘记写break,就会导致击穿现象
第二: 注意一个语法细节,就是case里面如果要定义变量的话,必须加花括号
int main()
{
int num = 0;
scanf("%d", &num);
switch (num)
{
case 1:
{
int a = 1;//注意花括号
printf("firstn");
break;
}
case 2:
printf("secondn");
break;
case 3:
printf("thirdn");
break;
default:
printf("othern");
break;
}
}
第三: 多条件匹配时可以这样写
int main()
{
int num = 0;
scanf("%d", &num);
switch (num)
{
case 1:
case 2:
case 3:
printf("firstn");
break;
case 4:
case 5:
printf("secondn");
break;
default:
printf("othern");
break;
}
}
第四: 注意default可以放在任意位置
第五: switch中可以使用return语句,但不建议使用
八:do 、while 、for关键字
第一: 这三种循环基本语法如下
//while
条件初始化
while(条件判定){
//业务更新
条件更新
}
//for
for(条件初始化; 条件判定; 条件更新){
//业务代码
}
//do while
条件初始化
do{
条件更新
}while(条件判定)
第二: 三种循环对应的死循环写法如下
while(1){
}
for(;;){
}
do{
}while(1);
第三: break是跳出该循环,continue是结束一次循环
int main()
{
while (1)
{
int c = getchar();
if (c == '#')
{
break;//表示接受到“#”就结束
}
putchar(c);
}
}
int main()
{
while (1)
{
int c = getchar();
if (c == '#')
{
continue;//表示接受到“#”略过
}
putchar(c);
}
}
这里需要注意for循环的continue,经常爱考察。for循环在continue时是跳到循环更新处
int main()
{
int i = 0;
for (; i < 10; i++)
{
printf("continue before:%dn", i);
if (i == 5) {
printf("continue语句之前n");
continue;
printf("continue语句之后n");
}
printf("continue after:%dn", i);
}
}
第四: for循环区间建议是前闭后开
for(int i=0;i<10;i++)
{
//循环10次
}
for(int i=6;i<10;i++)
{
//循环10-6=4次
}
九:goto关键字
第一: goto基本控制逻辑或者基本语法如下
int main()
{
int i = 0;
START:
printf("[%d]goto running ... n", i);
Sleep(1000);
++i;
if (i < 10){
goto START;
}
printf("goto end ... n");
return 0;
}
十:void关键字
第一: void是不能用来定义变量的。因为定义变量的本质就是开辟内存空间,而void作为空类型,理论上将是不应该开辟空间的,即使开辟了空间,也仅仅作为一个占位符来看待,所以这种行为直接就会被编译器禁止
第二: 首先先说明一点,在C语言中函数是可以不带返回值的,返回类型为整型
还在有些场景中我们时不需要函数的返回值的,如果采用上面的那种方式书写,很容易产生阅读上的歧义,因此如果函数不想让其返回,可以用void,这里一定要将其理解为一种占位符,它是告知用户和编译器的
第二: 在如下情形中,编译器是不会报错的,因此会有很大的安全隐患
而如果限制void后,编译器将会报警。因此void可以充当函数的形参列表,用于告知编译器和用户该函数不需要传入参数
第四: void的确不可以定义变量,但是void*可以,因为指针变量的大小是明确的(Windows32位下为4个字节大小)
void* p=nullptr;
第五: void*可以被任何类型的指针接受,void*也可以接受任意指针类型
int main()
{
void* p = NULL;
int* x = NULL;
double* y = NULL;
p = x;//void*接受int*
p = y;//void*接受double*
}
- 尤其注意 void*也可以接受任意指针类型,这一点通常用作一些通用接口的设计
第六: 我们知道,普通类型的指针可以进行位运算
int* p=NULL;
p++;
p--;
而对于void*呢?它要是视平台而定,一般VS下不可以,Linux下可以(Linux认为void是1)
十一:return关键字
第一:return不可以返回指向“栈内存”,因为在函数体结束时会被自动销毁
因此下面的语句会出现乱码
char* show()
{
char str[] = "hello world";
return str;
}
int main()
{
char* s = show();
printf("%sn", s);
return 0;
}
第二: 函数的返回值,通过寄存器的方式,返回给函数的调用方(注意区别上面,上面不能那样做,因为那是指向栈的指针)
int GetData()
{
int x = 0x11223344;
printf("runningn");
return x;
}
int main()
{
int y = GetData();
printf("return value:%xn", y);
return 0;
}
return x对应的汇编代码为:
十二:const关键字
第一: const修饰的变量不可以直接被修改
const int a=10;
a=20;//错误
但间接可以修改
int main()
{
const int a = 10;
int* p = &a;
printf("change before:%dn", a);
*p = 20;
printf("change after:%dn", a);
return 0;
}
那么既然这样其意义何在呢?其实const修饰变量主要有下面两个目的
- 让编译器进行直接修改式检查
- 告诉其他人这个变量不要改动,属于“自描述”含义
真正意义上的不可修改如C语言中的常量字符串
int main()
{
char* str = "hello worldn";//常量字符串
*str ='E';
return 0;
}
第二: const int i 和int const i是等价的
第三: const修饰的变量同样不能作为数组定义的一部分(标准C不可以,但是在Linux可以)
int main()
{
const int n = 100;
int arr[n];//错误
return 0;
}
第四: const在定义时必须初始化
第五: 建立只读数组可以这样写
int const a[5]={1,2,3,4,5};
或
const int a[5]={1,2,3,4,5};
第六 :const放在谁后面就修饰谁,因此它与指针的关系如下
①:const int* i 与int const* i等价
其中i是指针,const修饰了int,表示指针可以变化,但是指针指向内容不能被修改
②:int* const i
const修饰的是指针,所以指针不可变,但是指向的内容可变
③:const int* const i=&a
表示指针不可以变,指向的内容也不可以变
第七: const 也可以用来修饰函数参数,表明不可更改
void show(const int* _p)//防止指针指向内容被修改
{
printf("value:%dn", *_p);
*_p = 20;//非法操作
}
int main()
{
int a = 10;
int* p = &a;
show(p);
}
十三:volatile关键字
有关volatile关键字的作用在下面这篇文章中欧诺个有详细介绍,请移步
- Linux系统编程34:进程信号之可重入函数,volatile关键字的作用和SIGHLD
volatile关键字的作用:volatile将保持内存的关键字,一个变量一旦被volatile修饰,那么系统总是会从内存中读取数据,而不是从寄存器
需要注意const和volatile的区别,两者并不矛盾
- const要求你不要进行写入
- volatile意思是你读的时候每次要从内存读
十四:extern关键字
extern关键字这里就多说了,非常简单
十五:struct关键字
第一: struct基本介绍
定义
初始化(不能初始化后整体赋值)
成员访问
结构体传参
第二: 在Linux中空结构体的大小为0
第三: 柔性数组
我们知道C语言中是不能有这样的操作的,就是用变量对数组进行初始化
int main()
{
int i=0;
scanf("%d",&i);
int arr[i];
}
在C语言中如果要完成动态数组,可以借助柔性数组。使用柔性数组时,我们采用结构体的方式,将一个数组作为结构体成员放置于其中,但注意该数组不初始化,什么都不写
在上述结构体中,有两个结构体变量,数组似乎不占空间,但其实不然。实则,该结构体将其所占空间划分为两部分,一部分就是那个整形,一部分用于动态开辟,以此满足数组的动态变化
既然是柔性,那就可以修改,使用realloc修改
十六:Union关键字
第一: Union是什么
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间,联合体内所有成员的起始地址都是一样的,每个成员都认为它是联合体的第一个成员
第二: 根据内存地址分布,如下,b永远定义在相对于a的低地址处
union Un
{
int a;
int b;
};
根据这一性质我们可以利用联合体来判断机器是大端机还是小端机,如下
union Un
{
int a;
char b;
};
int main()
{
union Un u;
u.a = 1;
if (u.b == 1){
printf("小端机n");
}
else {
printf("大端机n");
}
return 0;
}
这是因为
十七:enum关键字
enum用于枚举一堆常量,就像Excel中的数据有效性,它规定了整个数据它只能是这几个取值,比如说男性它只有男或女
定义
enum color是枚举类型,括号中的内容是枚举类型的可能取值,也叫做枚举常量。这些可能取值实际上是有值的,默认是从0开始的
当然是可以修改的
枚举的这样的写法其实和宏的写法在代码的逻辑上是相似的
十八:typedef关键字
第一: typedef的作用就是为类型重新命名
typedef unsigned int u_int;
int main()
{
u_int a = 10;
return 0;
}
typedef 经常会在结构体重命名里
typedef struct stu
{
int a;
int b;
}Student;
int main()
{
Student student1;
}
第二: 大家一定要对typedef理解到位,如下
int main()
{
int* a, b;
//a是指针类型
//b是整形
}
typedef从某种方面可以理解一种全新的类型,因此下面的*就不存在和谁结合的问题了
typedef int* int_p;
int main()
{
int_p a, b;
//a是指针类型
//b也是指针类型
}
而对于#define而言它就是一种文本替换了,因此
#define int_p int*
int main()
{
int_p a, b;
//a是指针类型
//b是整形
}
第三:使用typedef定义后的新类型,不能配合其他关键字使用
#define INT_DE int
typedef int INT_TY;
int main()
{
unsigned INT_DE a;//正确
unsigned INT_TY b;//错误
}
总结
(1)关键字分类
数据类型关键字 :
- char
- short
- int
- long
- signed
- unsigned
- float
- double
- struct
- union
- enum
- void
控制语句关键字 :
1:循环控制
- for
- do
- while
- break
- continue
2:条件语句
- if
- else
- goto
3:开关语句
- switch
- case
- default
4:返回语句
- return
存储类型关键字 :
- auto
- extern
- register
- static
- typedef
这里需要补充一点:使用typedef时不能同时出现多个存储关键字
typedef static int//错误
typedef register int//错误
其他关键字 :
- const
- sizeof
- volatile



