Skip to content

LV005-有哪些关键字

C 语言关键字是 编译器保留、具有特殊含义的标识符,不能作为变量名或函数名使用。

截止到 2024 年末,C17 是最新的 C 语言标准,至此,C 语言的关键字总数已经达到了 44 个。32 个传统 C 语言关键字 + 5 个 C99 新增关键字 + 7 个 C11 新增关键字 = 44 个关键字。

一、C89(ANSI C)标准

1. C89 简介

C89(ANSI C)标准是 C 语言的第一个标准,1989 年由美国国家标准委员会(American National Standards Institute,简称 ANSI)制定,所以被称为 ANSI C。又由于这个版本是 89 年完成制定的,因此也被称为 C89。

后来 ANSI 把这个标准提交到 ISO(国际化标准组织),1990 年被 ISO 采纳为国际标准,称为 ISO C。又因为这个版本是 1990 年发布的,因此也被称为 C90。

ANSI C(C89)与 ISO C(C90)内容基本相同,主要是格式组织不一样,所以它们实际上是同一套标准。

2. C89 标准关键字(32 个)

C89 标准关键字(32 个)
类别 关键字 说明
数据类型 int 用于声明整型变量,通常占用 4 字节内存
float 用于声明单精度浮点数,通常占用 4 字节内存
double 用于声明双精度浮点数,通常占用 8 字节内存
char 用于声明字符类型,占用 1 字节内存
类型修饰符 short 用于修饰整型,缩小其范围和内存占用
long 用于修饰整型或双精度浮点型,扩大其范围
signed 表示数据类型可以存储正负值(默认情况)
unsigned 表示数据类型只能存储非负值
void 表示无类型或空类型,常用于函数返回值。void 类型的数据默认占有一个字节空间(需要注意的是并没有这种说法,只是它在 C 语言中就是这样的)。
存储类型 auto 默认的存储类型,变量在块内自动分配和释放
register register 修饰表示相应的变量将被频繁使用,建议编译器将变量存储在寄存器中以提高访问速度。
static 使局部变量在函数调用之间保持其值,或限制全局变量的作用域。
外部链接 extern 声明一个在其他文件中定义的全局变量或函数
typedef 用于创建自定义类型名,简化复杂的类型声明
控制流 if 条件语句,根据条件执行不同的代码块
else 与 if 配合使用,指定条件不满足时执行的代码块
switch 多路分支语句,根据表达式的值选择执行不同的代码块
case 在 switch 语句中指定一个待匹配的值
default 在 switch 语句中指定当没有 case 匹配时执行的代码块
循环结构 for 用于创建一个具有初始化、条件和更新表达式的循环
while 当条件为真时重复执行代码块的循环
do 与 while 配合使用,创建一个至少执行一次的循环
break 用于提前退出循环或 switch 语句
continue 跳过循环的当前迭代,直接进入下一次迭代
跳转语句 goto 无条件跳转到程序中的标记位置
return 从函数中返回值并结束函数的执行。它不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。
常量定义 const 声明一个不可修改的变量(常量)
联合体 union 定义一个共享内存空间的数据结构
结构体 struct 定义一个包含多个不同数据类型成员的复合数据类型
enum 定义一组命名的整型常量
位操作 volatile 告诉编译器变量可能会被意外改变,防止过度优化
大小计算 sizeof 返回数据类型或表达式的字节大小, 它其实是一个函数,但是也被称之为关键字(在 64 位平台下,它的返回值是 long unsigned int 类型,打印的时候 print 格式字符应该为 %ld )。在获取字符串的大小的时候它会计算字符串结束符 '\0' 在内

3. 几个常见关键字

3.1 static

3.1.1 有什么用?

static 本质就是延长变量或函数的生命周期,同时限制其作用域 。主要有以下作用

  • (1)在修饰变量的时候,static 修饰的静态局部变量只执行一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
  • (2)static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。
  • (3)static 修饰一个函数,函数就被定义成为静态函数,这个函数的只能在本文件中调用,不能被其他文件调用。所以其他文件中可以定义相同名字的函数,不会发生冲突。
  • (4)static 修饰的局部变量存放在全局数据区的静态变量区。初始化的时候自动初始化为 0;
3.1.2 什么情况使用

以下情况应该考虑使用 static:

(1)不想被释放的时候,可以使用 static 修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰

(2)考虑到数据安全性(当程想要使用全局变量的时候应该先考虑使用 static)

3.1.3 几个问题
  • static 全局变量与普通的全局变量有什么区别?

存储方式 上:static 全局变量和普通全局变量都是静态存储方式;

作用域 上:普通全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,普通全局变量在各个源文件中都是有效的。 而 static 全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于 static 全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此可以避免在其它源文件中引起错误。

  • static 局部变量和普通局部变量有什么区别?

存储方式 上:普通局部变量是动态存储方式,static 局部变量是静态存储方式

作用域 上:都是在块或者函数内部有效

  • static 函数与普通函数有什么区别?

(1)用 static 修饰的函数,本限定在本源码文件中,不能被本源码文件以外的代码文件调用。而普通的函数,默认是 extern 的,也就是说,可以被其它代码文件调用该函数。

(2)static 函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。

3.2 register

register 修饰表示相应的变量将被频繁使用,尽可能(注意是尽可能,不是绝对,因为一个 CPU 的寄存器就那么几个)的将这个变量保存在 CPU 内部寄存器中,而不是通过内存寻址来访问,这是为了提升程序的运行效率。

使用限制

(1)register 变量必须是能被 CPU 所接受的类型,这通常意味着 register 变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。但是,有些机器的寄存器也能存放浮点数。

(2)因为 register 变量可能不存放在内存中,所以不能用 “&” 来获取 register 变量的地址。

(3)只有局部自动变量和形式参数可以作为寄存器变量,其它(如全局变量)不行。在调用一个函数时占用一些寄存器以存放寄存器变量的值,函数调用结束后释放寄存器。此后,在调用另外一个函数时又可以利用这些寄存器来存放该函数的寄存器变量。

(4)局部静态变量不能定义为寄存器变量。不能写成:register static int a, b, c;

(5)由于寄存器的数量有限(不同的 CPU 寄存器数目不一),不能定义任意多个寄存器变量,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的 register 修饰符的数目和类型都依赖于运行程序的机器,而任何多余的 register 修饰符都将被编译程序所忽略。

3.3 const

3.3.1 const 简介

只要一个变量前用 const 来修饰,就意味着该变量里的数据只能被访问,而不能被修改,也就是意味着 const“只读”(readonly)。它的规则就是:const 离谁近,谁就不能被修改。

const 修饰一个变量时,一定要给这个变量初始化,若不初始化,在后面也不能初始化。

const 的作用如下:

  • (1)可以用来 定义常量修饰函数参数修饰函数返回值 ,且被 const 修饰的东西,都受到强制保护,可以预防其它代码无意识的进行修改,从而提高了程序的健壮性(是指系统对于规范要求以外的输入能够判断这个输入不符合规范要求,并能有合理的处理方式。
  • (2)使编译器保护那些不希望被修改的参数,防止无意代码的修改,减少 bug;
  • (3)给读程序的人传递有用的信息,声明一个参数,只是为了告诉用户这个参数的应用目的;
3.3.2 优点?
  • (1)编译器可以对 const 进行类型安全检查(所谓的类型安全检查,能将程序集间彼此隔离开来,这种隔离能确保程序集彼此间不会产生负面影响,提高程序的可读性);
  • (2)有些集成化的调试工具可以对 const 常量进行调试,使编译器对处理内容有了更多的了解,消除了一些隐患。例如
c
void func(const int i) {
	// ... 
}

编译器就会知道 i 是一个不允许被修改的常量。

  • (3)可以节省空间,避免不必要的内存分配,因为编译器通常不为 const 常量分配内存空间,而是将它保存在符号表中,这样就没有了存储于读内存的操作,使效率也得以提高;
  • (4)可以很方便的进行参数的修改和调整,同时避免意义模糊的数字出现。
3.3.3 修饰指针
c
const int * a;  /* 意味着 a 是一个指向常整型数的指针(也就是,指针指向的整型数是不可修改的,但指针可以被修改)*/
int * const a;  /* 意味着 a 是一个指向一个整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)*/
const int const * a; /* 意味着 a 是一个指向常整型数的常指针(也就说说,指针指向的整型数是不可以修改的,同时指针也是不可修改的) */

记法:左数右指

  • 当 const 出现在 * 号左边时,指针指向的数据不可以变,但指针可以。

  • 当 const 出现在 * 号右边时,指针本身不可以改变,但指针指向的数据可以。

3.4 volatile

3.4.1 volatile 简介

volatile 是易变的,不稳定的; 这是一个编译器警告提示字,防止编译器优化用该修饰符修饰的变量。意思就是一个定义为 volatile 的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。

如果一个变量 a ,使用了 volatile 修饰,那么可以保证每次取 a 的值都不是从缓存中取,而是从 a 所真正对应的内存地址中取。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。这是因为访问寄存器的速度要快过 RAM,所以编译器一般都会作减少存取外部 RAM 的优化。

注意

(1)一个参数既可以是 const,也可以是 volatile,它可以同时由这两者进行修饰,例如只读的状态寄存器,它是 volatile 表示它可能被意想不到地改变,它还是 const 表示程序不应该试图去修改它。

(2)一个指针可以是 volatile ,当一个中服务子程序修改一个指向一个 buffer 的指针时,这个指针就可以是一个 volatile。

3.4.2 可以在哪些地方使用

(1)中断,中断服务程序中修改的供其它程序检测的变量需要加 volatile ;

(2)多任务共享资源,多任务环境下各任务间共享的标志应该加 volatile ;

(3)mmu 映射的寄存器,存储器映射的硬件寄存器通常也要加 volatile 说明,因为每次对它的读写都可能由不同意义;

3.4.3 使用实例 1
c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define MACRO 0

void *thread_func(void *arg)
{
	sleep(2);
	*(int *)arg = 0;
}

int main(int argc, const char *argv[])
{
	pthread_t tid;
#if MACRO == 1
	volatile int a = 1;
#else
	int a = 1;
#endif
	pthread_create(&tid, NULL, thread_func, &a);

	while (a);
	printf("----------------------\n");
	return 0;
}

我们使用下边的命令编译:

shell
gcc main.c -lpthread       # 不进行优化
gcc main.c -lpthread  -O1  # 进行优化, 或者用 -O3

不进行优化的时候,MACRO 若等于 1 ,那么整个程序编译会有警告,但是可以正常运行完毕,若 MACRO 若等于 0,程序也可以正常运行完毕。

进行优化的时候,MACRO 若等于 1 ,那么整个程序编译会有警告,但是可以正常运行完毕,若 MACRO 若等于 0,程序就不会。结束了。不会打印出最后的一句话。

3.4.4 使用实例 2

如下程序,想要打印出传入参数的平方:

c
#include <stdio.h>

int func(volatile int *p)
{
	return *p * *p;
}

int main(int argc, const char *argv[])
{
	int a = 10;
	printf("a^2 = %d\n", func(&a));
	return 0;
}

但是这样其实是可能会发生问题的,因为 *p 指向一个 volatile 类型参数,它的值可能会被意想不到地改变,最后返回的用于相乘的两个数可能是不同的,正确的写法应该是这样的:

c
#include <stdio.h>

int func(volatile int *p)
{
    int temp = *p
	return temp * temp;
}

int main(int argc, const char *argv[])
{
	int a = 10;
	printf("a^2 = %d\n", func(&a));
	return 0;
}

二、C99 标准

1995 年,C 程序设计语言工作组对 C 语言进行了一些修改,增加了新的关键字和数据类型,编写了新的库,取消了原有的限制,并于 1999 年形成新的标准——ISO/IEC 9899:1999 标准,通常被称为 C99。

C99 标准是 C 语言的一个重要里程碑,为这门经典的编程语言带来了许多新特性和改进。C99 标准新增了以下 5 个关键字。

关键字说明
inline用于函数定义,建议编译器将函数代码直接插入到调用处,以提高程序执行效率。这个关键字主要用于优化小型、频繁调用的函数。
restrict用于指针声明,告诉编译器该指针是访问某一内存区域的唯一途径。这使得编译器可以进行更激进的优化,因为它可以确信不会通过其他途径修改该内存区域。
_Bool引入了布尔类型,可以存储 0(假)或 1(真)。这使得逻辑运算和条件判断更加清晰和直观。在使用时,通常会包含 头文件,这样就可以直接使用 bool 而不是 _Bool。
_Complex用于声明复数类型。C99 标准引入了对复数的原生支持,使得科学计算和信号处理等领域的编程变得更加方便。
_Imaginary用于声明纯虚数类型。与 _Complex 配合使用,可以完整地表示复数系统中的所有数。

三、C11 标准

C11 标准由国际标准化组织(ISO)和国际电工委员会(IEC) 旗下的 C 语言标准委员会于 2011 年底正式发布,主要增加了安全函数,以及对多线程的支持。

关键字说明
_Alignas用于指定变量的对齐方式,可以提高内存访问效率。
_Alignof用于获取类型的对齐要求,并并返回一个 size_t 类型的值,表示指定类型的对齐字节数。
_Atomic用于声明原子类型,确保在多线程环境中对该类型的操作是原子的,避免数据竞争。
_Generic用于实现类型泛型编程,根据表达式的类型选择不同的代码路径,它允许我们根据参数类型编写更灵活的函数。
_Noreturn用于指示函数不会返回到调用者。这通常用于终止程序的函数,如 exit()。
_Static_assert用于在编译时进行断言检查,如果断言失败,编译器会报错。这有助于捕获一些潜在的编程错误。
_Thread_local用于声明线程局部存储的变量。每个线程都会有该变量的独立副本,这在多线程编程中非常有用。

值得注意的是,这些关键字都以下划线开头,这是为了避免与现有的用户定义标识符发生冲突。

四、C17 标准

2018 年,ISO/IEC 又发布了 C11 标准的修正版,称为 C17 或者 C18 标准。和 C11 标准相比,C17 并没有添加新的功能和语法特性,仅仅修正了 C11 标准中已知的一些缺陷,所以 C17 标准并没有新增任何关键字。

五、下划线

留意一下就可以发现,C99、C11、C17 中的很多关键字都以下划线 _ 开头,这种命名约定并非偶然,而是经过深思熟虑的设计决策。

C 语言的设计者们面临着一个棘手的问题:如何在保持向后兼容性的同时引入新的语言特性,他们需要确保新增的关键字不会与现有的代码产生冲突。为了解决这个问题,他们采用了一个巧妙的策略——使用以下划线开头的标识符。

根据 C 语言标识符的命名规则,以下划线开头的标识符通常被保留给实现使用,这意味着普通的程序员不应该在他们的代码中使用这样的标识符。通过这种方式,C 语言标准委员会为自己预留了一个安全的命名空间,可以在其中引入新的关键字,而不必担心与现有代码发生冲突。

这种命名策略不仅解决了兼容性问题,还为程序员提供了一个视觉提示。当你在代码中看到以下划线开头的关键字时,你可以立即意识到这是一个相对较新的语言特性,可能需要更现代的编译器才能支持。