C 语言标准定义的 32 个关键字有什么,分别怎么用,用的时候需要注意到什么,便是这篇文章所要告诉的一切。内容较长,先上本篇文章结构思维导图。文章结尾处有 32 个关键字附录。
- 修饰类 -
- 1. extern -- 最会带帽子的关键字 -
- 定义创建了对象并为这个对象分配了内存,声明没有分配内存
- 相同的定义只能出现一次,相同的声明可以出现多次
int i; // 变量的定义
extern int i; // 变量的声明
void fun(int i, char c); // 函数的声明
void fun(int i, char c) {} // 函数的定义
- 2. auto -
- 编译器在默认的缺省情况下,所有变量都是用 auto 修饰的。
- 3. register -- 最快的关键字 -
- 这个关键字请求编译器尽可能的将变量存在 CPU 内部寄存器中而不是通过内存寻址访问以高效率。注意是尽可能,不是绝对。
3.1 寄存器
- 数据从内存里拿出来先放到寄存器,然后 CPU 再从寄存器里读取数据来处理,处理完后同样把数据通过寄存器存放到内存里,CPU 不直接和内存打交道。
CPU Registers 即为寄存器
3.2 使用 register 修饰符的注意点
- register 变量必须是能被 CPU 寄存器所接受的类型
- register 变量必须是一个单个的值,并且其长度应小于或等于整型的长度
- register 变量可能不存放在内存中,所以不能用取址运算符“&”来获取 register 变量的地址
- 4. static -- 最名不符实的关键字 -
关键字 static 有着不寻常的历史。起初,在 C 中引入关键字 static 是为了表示退出一个块后仍然存在的局部变量。随后,static 在 C 中有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。为了避免引入新的关键字,所以仍使用 static 关键字来表示这第二种含义。
4.1 修饰变量
- 变量又分为局部和全局变量,但它们都存在内存的静态区
- 静态全局变量,作用域仅限于变量被定义的文件中
- 静态局部变量,在函数体里面定义且只能在这个函数里用
- 静态变量的值不会被销毁,函数下次使用时仍然能用到这个值
4.2 修饰函数
- 函数前加 static 使得函数成为静态函数,指对函数的作用域仅局限于本文件(所以又称内部函数)
- 5. const 关键字 -
- const 是 constant 的缩写,并不是英文直译的用来修饰“常量”,更精确的说是修饰“只读变量”,其值在编译时不能被使用,因为编译器在编译时不知道其存储的内容。
- const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点。
5.1 const 修饰的只读变量
- 定义 const 只读变量,具有不可变性。该变量具有只读属性。
5.2 节省空间,避免不必要的内存分配,同时 高效率
- 编译器通常不为普通 const 只读变量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的值,没有了存储与读内存的操作,使得它的效率也很高。
- const 定义的只读变量从汇编的角度来看,只是给出了对应的内存地址。
5.3 修饰一般变量
- 只读变量在定义时,修饰符 const 可以用在类型说明符前,也可以用在类型说明符后。例如:
int const i=2; // 正确
const int j=2; // 正确
5.4 修饰数组
- 定义或说明一个只读数组可采用如下格式:
int const a[5]={1, 2, 3, 4, 5}; // 正确
const int b[5]={1, 2, 3, 4, 5}; // 正确
5.5 修饰指针
- 先忽略类型名(编译器解析的时候也是忽略类型名),我们看 const 离哪个近。“近水楼台先得月”,离谁近就修饰谁。
const int *p; // p 可变,p 指向的对象不可变
int const *p; // p 可变,p 指向的对象不可变
int *const p; // p 不可变, p 指向的对象可变
const int *const p; // 指针 p 和 p 指向的对象都不可变
5.6 修饰函数的参数
- 当不希望这个参数值被函数体内意外改变时,const 修饰符也可以修饰函数的参数,从而防止了使用者的一些无意的或错误的修改。
void Fun(const int i);
5.7 修饰函数的返回值
- const 修饰符也可以修饰函数的返回值,返回值不可被改变。例如:
const int Fun (void);
- 在另一连接文件中引用 const 只读变量
extern const int i; // 正确的声明
extern const int j=10; // 错误!只读变量的值不能改变。
- 6. signed、unsigned 关键字 -
- 既然计算机底层只认识 0 和 1 的二进制数字,那么正数和负数是怎么存储的呢?signed 和 unsigned 修饰的变量各有什么特点?
- 答案很简单。在 32 位计算机的内部,一个 int 型的整数由 4 字节 32 位 0/1 数字组成,32 个数字中最高位(第一个数字)表示符号位并约定最高位如果是 1 则表示负数,如果是 0 则表示正数,这样一来,一个 32 位有符号的 int 型变量便能表示 -2147483648~2147483647 之间的数字,而无符号的 int 型变量便能表示 0~4294967295 之间的数字。
- 其中有符号的 int 型变量由 signed 修饰,一般可以省去;无符号的 int 型变量由 unsigned 修饰,不能省去。
signed int i;
unsigned int love;
signed int u;
- 然而用普通 01 码(原码)表示的数字中 +0 和 -0 各有一种表示方法,且原码无法,具体的计算机中负数其实是以补码的形式存储的。补码由反码 +1 而来,而反码由原码的每一位 0 变成 1,1 变成 0而来。同时反码和补码的出现使得计算机为辨别"符号位"而设计的电路不再复杂。具体的有关机器语言中原码、反码和补码的知识都在我的博客园里写过,期待之后整理到简书中来。
- 数据类型 -
- 7. 数据类型 -
- short、int、long、char、float、double 这六个关键字代表 C 语言里的六种基本数据类型。
7.1 数据类型与“模子”
- 在每一个拥有数据类型的变量的声明时,都会分配给该变量特定的内存大小空间。放出基本数据类型在 32 位操作系统下特定的内存空间大小:
- 具体有关各个数据类型的特点和区别及其混合运算与类型转换等可以参考我在博客园写的这篇文章,毕竟本文的重心在“关键字”上。
7.2 变量的命名规则
- 命名和缓存失效是计算机科学面临的最难的两件事。其中符合规范的变量命名可以直观地看出代码的逻辑性,具体命名规则也同样加入自己的博客园文章地址,期待之后整理到简书中来。
- 8. void 关键字 -
- “色即是空,空即是色”,void 也有它的妙用 —— void 的字面意思是“空类型”,void *则为“空类型指针”,void *可以指向任何类型的数据。
- void 真正发挥的作用在于:
1 -> 对函数返回的限定;
2 -> 对函数参数的限定。 - 任何类型的指针都可以直接赋值给 void ,无需进行强制类型转换。
void *p1;
int *p2;
p1 = p2;
8.1 void 修饰函数返回值和参数
- 如果函数没有返回值,那么应声明为 void 类型
在给函数不加返回值说明时默认为 int 型,所以对于没有返回值的函数推荐用 void 声明。另外,加上 void 类型声明后,也可以发挥代码的“自注释”作用。所谓的代码的“自注释”即代码能自己注释自己。 - 如果函数无参数,那么应声明其参数为 void
在 C 语言中,可以给无参数的函数传送任意类型的参数,但是在 C++编译器中编译同样的代码则会出错。所以,无论在 C 还是 C++中,若函数不接受任何参数,一定要指明参数为 void。
8.2 void 指针
- 千万小心又小心使用 void 指针类型。
按照 ANSI(American National Standards Institute)标准,不能对 void 指针进行算法操作,即下列操作都是不合法的:
void * pvoid;pvoid++; // ANSI:错误
pvoid += 1; // ANSI:错误
但是大名鼎鼎的 GNU(GNU's Not Unix 的递归缩写)则不这么认定,它指定 void *的算法操作与 char *一致。因此下列语句在 GNU 编译器中皆正确:
pvoid++; // GNU:正确
pvoid += 1; // GNU:正确
因此,为了兼容两大标准, 高程序的可移植性,我们可以这样编写实现同样功能的代码:
void * pvoid;(char *)pvoid++; // ANSI:正确;GNU:正确(char *)
pvoid += 1; // ANSI:错误;GNU:正确
- 如果函数的参数可以是任意类型指针,那么应声明其参数为 void *。
典型的如内存操作函数 memcpy 和 memset 的函数原型分别为:
void * memcpy(void *dest, const void *src, size_t len);
void * memset ( void * buffer, int c, size_t num );
8.3 void 不能代表一个真实的变量
- void 不能代表一个真实的变量。
void a; // 错误
function(void a); // 错误
- 9. struct 关键字 -
- 常用在网络协议、通信协议、嵌入式系统、驱动开发等地方的 struct 关键字将一些相关联的数据打包成一个整体(结构体),方便使用。同时,可以用结构体压缩参数个数。
9.1 空结构体有多大
- 结构体所占的内存大小是其成员所占内存之和。但空结构体呢?
struct student{
} stu;
- 答案不是 0 而是 1。因为编译器认为任何一种数据类型都有其大小,空结构体的大小又不能大于只有 char 类型的结构体大小,所以 0 < sizeof(stu) <= 1 的情况下,空结构体的大小就定为 1 个 byte。
9.2 柔性数组
- C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员。
typedef struct st_type {
int i;
int a[];
} type_a;
- 即使这时 sizeof(type_a) 得到的只有 sizeof(int) 的值 —— 4,但这是我们可以进行边长操作,通过以下表达式给结构体分配内存:
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
- 这时候 p->item[n] 就能简单的访问可变长元素。而 sizeof(*p) 依旧是 4。
9.3 struct 和 class 的区别
- 在 C++里 struct 关键字与 class 关键字一般可以通用,只有一个很小的区别。struct 的成员默认情况下属性是 public 的,而 class 成员却是 private 的。很多人觉得不好记,其实很容易。你平时用结构体时用 public 修饰它的成员了吗?既然 struct 关键字与 class 关键字可以通用,你也不要认为结构体内不能放函数了。
- 10. union 关键字 -
- union 关键字的用法与 struct 的用法非常类似,用来维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间。例子如下:
union StateMachine
{
char character;
int number;
char *str;
double exp;
};
- 一个 union 只配置一个足够大的空间以来容纳最大长度的数据成员,以上例而言,最大长度是 double 型态,所以 StateMachine 的空间大小就是 double 数据类型的大小。
- 在 C++里,union 的成员默认属性页为 public。union 主要用来压缩空间。如果一些数据不可能在同一时间同时被用到,则可以使用 union。
10.1 大小端模式对 union 类型数据的影响
union {
int i;
char a[2];
} *p, u;
p = &u;
p->a[0] = 0x39;
p->a[1] = 0x38;
- 上面的例子中 p.i 的值为多少呢?这就要考虑存储模式 —— 大端模式和小端模式了。
- 大端模式(Big_endian):字数据的高字节存储在地地址中,而字数据的低字节存放在高地址中。
- 小端模式(Little_endian),字数据的高字节存储在高地址中,而字数据的低字节则存放在低地址中。
- 11. enum 关键字 -
- 初学者一般用不到的 enum 数据类型其实很有用处。
11.1 枚举类型的使用方法
- 一般的定义方式如下:
enum enum_type_name
{
ENUM_CONST_1,
ENUM_CONST_2,
...
ENUM_CONST_n
} enum_variable_name;
- 其中 enum_type_name 是自定义的一种数据类型名,而 emun_varaiable_name 类型是对一个变量取值范围的限定,而花括号内是它的取值范围,及 enum_type_name 类型的变量 emun_varaiable_name 只能取值为花括号内的任何一个值,如果赋给该类型变量的值不在列表中,则会报错或警告。
11.2 枚举与 #define 宏的区别
-
define 宏常量是在预编译阶段进行简单替换,枚举常量则是在编译的时候确定其值。
- 一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
- 枚举可以一次定义大量相关的常量,但是 #define 宏一次只能定义一个。
- 枚举可以一次定义大量相关的常量,而 #define 宏一次只能定义一个。
- 选择分支 -
- 12. If else 组合 -
12.1 bool 变量与“零值”进行比较
bool bTestFlag = FALSE; // 一般要初始化为 FALSE
if(bTestFlag == 0); // 不好。容易被误解为整型变量
if(bTestFlag == TRUE); // 不好。TRUE的值不都为1。
if(bTestFlag); // 推荐
12.2 float 变量与“零值”进行比较
float fTestVal = 0.0;
if(fTestVal == 0.0); // 不好。
if((fTestVal >= -EPSINON) && (fTestVal <= EPSINON)); // 推荐。EPSINON 为定义好的精度。在该范围内则认为为零值
12.3 指针变量与“零值”进行比较
if(p == 0); // 尽管 NULL 值为0,容易引起 p 是整型的误会。
if(p); // 容易引起 p 是 bool 型的误会
if(NULL == p); // 推荐。且 NULL 要写在左值避免 p = NULL; 的错误赋值。
12.4 else 到底与哪个 if 配对呢?
if(0 == x)
if(0 == y) error();
else {
//program code
} // 这段代码容易引起误会
- C 语言有这样的规定:else始终与同一括号内最近的未匹配的 if 语句结合。但是任何时候都不要偷懒。该写花括号都要写上。
for (initialization; condition; update)
{
// 推荐风格
}
for(initialization; condition; update){
// 不推荐风格
}
12.5 if 语句后面的分号
- 如果出现以下代码,fun() 函数无论如何都会被调用:
if(NULL != p);
fun();
- 建议在真正需要用空语句时写成这样:
NULL;
- 13. switch case 关键字 -
- if、else 一般表示两个分支或是嵌套表示少量的分支,如果分支很多的话,用 switch 和 case 组合更好
- 每个 case 语句的结尾绝对不要忘了加 break,否则将导致多个分支重叠(除非有意使多个分支重叠)。
- 最后必须使用 default 分支。即使程序真的不需要 default 处理,也应该保留语句。这样做并非画蛇添足,可以避免让人误以为你忘了 default 处理。
13.1 case 关键字后面的值的要求
- case 后面只能是整型或字符型的常量或常量表达式
13.2 case语句的排列顺序
- 按字母或数字顺序排列各条 case 语句。
- 把正常情况放在前面,而把异常情况放在后面。
- 按执行频率排列 case 语句。
13.3 使用 case 语句的其他注意事项
- 简化每种情况对应的操作。case 语句后面的代码越精炼,case 语句的结
果就会越清晰。 - 不要为了使用 case 语句而刻意制造一个变量。
- 把 default 子句只用于检查真正的默认情况。
- 14. goto 关键字 -
- 一般来说,编码的水平与 goto 语句使用的次数成反比。很多 C 语言编程书及其作者都主张禁用 goto 语句
struct student *p = NULL;
...
goto state;
p = (struct student *)malloc(...); // 被 goto 跳过,没有初始化
...
state: // 使用 p 指向的内存里的值的代码⋯
- 循环分支 -
- 15. do while for 关键字 -
- C 语言中循环语句有三种:while 循环、do-while 循环、for 循环。
- 死循环当然有用,操作系统的整个进程就是死循环,直到遇到关机指令。
15.1 break 与 continue 的区别
- break 用来终止本层循环。
- continue 用来终止本次(本轮)循环。
15.2 循环语句的注意点
- 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。
- 建议 for 语句的循环控制变量的取值采用“半开半闭区间”写法。
- 不能在 for 循环体内修改循环变量,防止循环失控。
- 循环要尽可能的短,要使代码清晰,一目了然。
- 把循环嵌套控制在 3 层以内。
- 其他 -
- 16. sizeof -- 最冤枉的关键字 -
很多人都认为 sizeof 是函数,至少我第一次学 C 语言的时候是这么认为的。然而事实呢? sizeof 是关键字,常年被人误认为函数的关键字。
sizeof 在计算变量所占空间大小时,括号可以省略,而计算类型(模子)大小时不能省略。
一般情况下,sizeof 关键字后尽可能加上括号,继续装作函数。
sizeof(int); // 正确
sizeof int; // 错误
int a[10];
sizeof a; // 正确
sizeof(a); // 正确
- 17. typedef -- 伟大的缝纫师 -
17.1 历史的误会----也许应该是 typerename
- type 是数据类型的意思,def(ine)是定义的意思,合起来就是定义数据类型。然而事实并不是这样,typedef 的真正意思是给一个已经存在的数据类型(注意:是类型不是变量)取一个别名,而非定义一个新的数据类型。
typedef struct student {
//code
} Stu_st, *Stu_pst;
- 这时候,下方的定义中(1)和(2)没有区别,(3)、(4)和(5)没有区别。
struct student stu1; // (1)
Stu_st stu1; // (2)
struct student *stu2; // (3)
Stu_pst stu2; // (4)
Stu_st *stu2; // (5)
17.2 typedef 与#define 的区别
#define INT32 int
unsigned INT32 i = 10; // 正确
typedef int INT32;
unsigned INT32 j = 10; // 错误
从这段代码可以看出 typedef 取的别名不支持这种类型扩展。
#define PCHAR char*
PCHAR p3,p4; // 等价于 char *p3;char p4;
typedef char* pchar;
pchar p1,p2; // 等价于 char *p1;char *p2;
从这段代码又可以看出 #define 仅仅是简单的替换,并无法代表一个数据类型。
- 18. volatile -- 最易变的关键字 -
- volatile 是易变的、不稳定的意思,和 const 一样是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以 供对特殊地址的稳定访问。
18.1 编译器优化
int i=10;
int j = i; // (1)语句
int k = i; // (2)语句
- 编译器在编译时会对代码进行优化。而因为在(1)、(2)两条语句中 i 没有被用作左值,这时候编译器认为 i 的值没有发生改变,所以在(1)语句时从内存中取出 i 的值赋给 j 之后,这个值并没有被丢掉,而是在(2)语句时继续用这个值给 k 赋值。编译器不会生成出汇编代码重新从内存里取 i 的值,这样提高了效率。但要注意:(1)、(2)语句之间 i 没有被用作左值才行。
18.2 volatile 关键字防止编译器优化
volatile int i=10;
int j = i; // (3)语句
int k = i; // (4)语句
- 如果将之前的代码改成上面的样子的话,volatile 关键字会告诉编译器 i 是随时可能发生变化的,每次使用它的时候必须从内存中取出 i的值,因而编译器生成的汇编代码会重新从 i 的地址处读取数据放在 k 中。
- 19. return 关键字 -
- return 用来终止一个函数并返回其后面跟着的值。
- return 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。
- 关于 int main 中该不该出现 return 0; 又为什么是这样,我在我的博客园中如是写道:
附录:C 语言标准定义的 32 个关键字
关键字 | 意 义 |
---|---|
auto | 声明自动变量,缺省时编译器一般默认为 auto |
int | 声明整型变量 |
double | 声明双精度变量 |
long | 声明长整型变量 |
char | 声明字符型变量 |
float | 声明浮点型变量 |
short | 声明短整型变量 |
signed | 声明有符号类型变量 |
unsigned | 声明无符号类型变量 |
struct | 声明结构体变量 |
union | 声明联合数据类型 |
enum | 声明枚举类型 |
static | 声明静态变量 |
switch | 用于开关语句 |
case | 开关语句分支 |
default | 开关语句中的“其他”分支 |
break | 跳出当前循环 |
register | 声明寄存器变量 |
const | 声明只读变量 |
volatile | 说明变量在程序执行中可被隐含地改变 |
typedef | 用以给数据类型取别名(当然还有其他作用) |
extern | 声明变量是在其他文件正声明(也可以看做是引用变量) |
return | 子程序返回语句(可以带参数,也可不带参数) |
void | 声明函数无返回值或无参数,声明空类型指针 |
continue | 结束当前循环,开始下一轮循环 |
do | 循环语句的循环体 |
while | 循环语句的循环条件 |
if | 条件语句 |
else | 条件语句否定分支(与 if 连用) |
for | 一种循环语句(可意会不可言传) |
goto | 无条件跳转语句 |
sizeof | 计算对象所占内存空间大小 |
写在最后
这篇文章灵感和素材均来源自《C 语言深度解剖》这本书,感谢作者对 C 语言的独特见解,如有需要,欢迎购买实体书籍支持作者。
- Hello,我是韩亦乐,现任本科软工男一枚。软件工程专业的一路学习中,我有很多感悟,也享受持续分享的过程。如果想了解更多或能及时收到我的最新文章,欢迎订阅我的个人微信号:韩亦乐。我的简书个人主页中,有我的订阅号二维码和 Github 主页地址;我的知乎主页 中也会坚持产出,欢迎关注。
- 本文内部编号经由我的 Github 相关仓库统一管理;本文可能发布在多个平台但仅在上述仓库中长期维护;本文同时采用【知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议】进行许可。