C语言是一种面向过程的
计算机编程语言,属于
高级语言,其经典著作是1978年布莱恩·克尼汉(Brian W. Kernighan)和
丹尼斯·里奇出版的《The C Programming Language》。
发展简史
20世纪70年代初,贝尔实验室的丹尼斯·里奇致力于开发
UNIX操作系统(操作系统是能够管理计算机资源、处理计算机与用户之间交互的一组程序),因而需要一种编程语言,它必须简洁、高效,并且能够有效地控制硬件。
传统上,系统软件主要是用汇编语言编写的。
汇编语言属于低级语言,其程序依赖于计算机硬件,其可移植性和可读性都很差,而一般的高级语言又难以实现对计算机硬件的直接操作,所以人们希望有一种兼具汇编语言的效率、硬件访问能力和高级语言的通用性、可移植性的新语言。于是,丹尼斯·里奇在当时已有的B语言的基础上设计出了一种新语言,并取名为C语言。
1978年,布莱恩·克尼汉和丹尼斯·里奇出版了经典著作《The C Programming Language》,从而使C语言成为了在世界上广泛流传的高级程序设计语言。C语言早期主要用于UNIX系统,但其各方面的优点逐渐被人们认识,于是在20世纪80年代开始进入其它操作系统,并很快在各类计算机上得到广泛应用,成为当代最优秀的程序设计语言之一。
在计算机的普及过程中,C语言出现了很多版本,但由于没有统一的标准,这些C语言存在很多不一致的地方。为此,美国国家标准学会(ANSI)于1989年为C语言制定了一套
ANSI标准,称为C89,此后C语言标准在相当长一段时间内都没有变化;直到1999年,ANSI又通过了C99标准,它相对于C89做了很多修改,增加了基本数据类型、关键字和一些系统函数等。进入21世纪后,又出现了C11等标准,截止2023年,最新的C语言标准是C23。
C语言不仅得到了广泛应用,还影响了后续产生的诸多高级语言。20世纪80年代,贝尔实验室的本贾尼·斯特劳斯特卢普(
Bjarne Stroustrup)基于C语言开发了一种面向对象的程序设计语言并取名为C++,该名称来自C语言中的递增运算符“++”,表明它是C语言的扩充版本。后来出现的
Java、
C#等
面向对象的编程语言,也都吸收了C或C++的许多特性。可以说,C语言在计算机编程语言中具有举足轻重的地位,它的诞生和发展对整个计算机编程领域产生了广泛而深远的影响。
主要特性
基本数据类型
C语言提供的基本数据类型有:
char: 字符型,可以存放本地字符集中的一个字符;
int: 整型,通常反映了所用机器中整数的最自然长度;
double: 双精度浮点型。
在基础数据类型之前可以添加限定符:
short: 用于限定整型,即short int,称为短整型;
long: 用于限定整型,即long int,称为长整型;
signed: 用于限定字符型或任何整型,表示其为有符号类型;
unsigned: 用于限定字符型或任何整型,表示其为无符号类型。
其中,short和long的引入是为了提供满足实际需求的不同长度的整型数,通常的机器上short为16位,long为32位,而int可以为16或32位。它们的长度取决于具体实现(编译器、操作系统、硬件环境等),但C语言要求short int类型不长于int,long int不短于int。整型默认是有符号的,但是不带signed/unsigned限定符的字符型(char)是否有符号也取决于具体实现。
长度与符号的类型限定符可以同时使用,例如unsigned long int(或long unsigned int)表示无符号长整型,signed short int表示有符号短整型。
复合数据类型
C语言提供了数组、指针、结构、联合等复合数据类型,用于支持针对更复杂的数据的编程。
数组
数组是一种能够存储多个同类型的值的数据格式,它的各个元素依次存储在内存中。例如,下面的语句声明了一个由10个整型数构成的数组:
int digits[10];
在C语言中,数组下标总是从零开始,所以该数组的10个元素分别是digits[0], digits[1], …, digits[9]。用于访问数组的下标可以是任何整型表达式,包括整型变量和整型常量。
数组可以用列表来初始化,但只能在定义时使用:
int cmd_code[5] = {7, 5, 0, 3, 8};
C语言提供了类似矩阵的多维数组,例如下面的语句声明了一个二维数组:
double darray[12][30];
这样的二维数组按行优先存储,当按存储顺序访问数组时,最右边的下标(即列)变化得最快。
指针
指针是一种保存变量地址的变量,是C语言具有的重要特性之一。一元运算符*可用于声明指针和对指针解引用,一元运算符&可用于取一个对象的地址,例如下面的程序
char c;
char *pc = &c;
*pc = ‘Z’;
声明了一个指针pc并把变量c的地址赋给它(此时称pc是“指向”c的指针),然后访问pc指向的对象(也就是c)并给它赋值为字符“Z”。
在C语言中,指针与数组的关系十分密切,通过数组下标完成的操作都可以通过指针来实现,且通常效率更高。如果有声明和赋值语句
int a[10];
int *pa;
pa = &a[0];
那么pa指向数组a的0号元素。一般而言,当pa指向数组a的某个元素时,指针运算pa+1将指向下一个元素,而pa+i将指向pa所指元素之后的第i个元素,因而当pa指向a[0]时,*(pa+i)与a[i]是等价的。
指针可能导致程序难以理解。如果使用者粗心,指针很容易就会指向错误的地方;但如果谨慎地使用指针,则通常可以利用它写出简单、清晰、高效的程序。
结构
结构是一种复杂的数据类型,它将一至多个变量组合起来,并且这些变量可以具有不同的类型。结构使得用户可以定义自己的数据类型并为之赋予新的名字,例如为了用横纵坐标表示平面直角坐标系中的一个点,可以定义下面的结构:
struct point {
double x;
double y;
};
这个新的数据类型是struct point,它包含两个成员x和y分别表示横纵坐标。然后,就可以使用声明普通变量的语法来声明相应的变量了:
struct point pA, pB;
这样就声明了pA、pB两个struct point类型的变量,它们的成员可以用“.”运算符来访问,例如“pA.x”“pB.y”。
此外,结构类型的变量可以像普通变量那样进行“=”赋值运算,也可以作为函数参数或返回值,还可以定义由结构组成的数组和指向结构的指针,但不能直接使用“<”“>=”等进行比较运算。指向结构的指针常被用于实现二叉树的数据结构,例如下面的struct tnode即可表示一个结点:
struct tnode {
const char *name;
struct tnode *leftchild, *rightchild;
};
联合
联合与结构相似,都可以组合多个不同类型的变量,但它们的区别在于联合的各个成员占据同一块内存空间。假设有下面的联合定义:
union utag {
int iv;
float fv;
};
那么对于一个union utag类型的变量,改变其iv成员的值将导致fv的值改变。这种复合类型适合于编译器的符号表等应用场景,一个变量可能是多种类型之一,但通常需要另外一个变量来指示该联合变量是什么类型。
关键字
C语言将下列标识符保留作关键字,不能用于其它用途(例如变量、函数命名):
auto, break, case, char, const, continue, default, do, double, else, enum, extern, float, for, goto, if, int, long, register, return, short, signed, sizeof, static, struct, switch, typedef, union, unsigned, void, volatile, while.
某些编译器还可能有额外的关键字,例如“fortran”“asm”等。关键字在不同标准的C语言中也可能略有区别。
运算符
C语言提供了丰富的运算符,并且规定了运算符的结合性和优先级。
算术运算符
二元算术运算符有“+”“-”“*”“/”“%”,分别用于加、减、乘、除、取模;此外,“+”“-”也可以作为一元运算符,表示正负号。二元算术运算符从左到右结合,且“*”“/”“%”的优先级高于“+”“-”,但“+”“-”作为一元算术运算符时优先级更高。
关系和逻辑运算符
关系运算符有“>”“>=”“<”“<=”,它们具有相同的优先级;相等性运算符“==”“!=”分别表示等于和不等于,它们的优先级仅次于前面几个关系运算符;优先级更低的是逻辑运算符“&&”“||”,分别表示逻辑与、或,由它们连接的表达式将按从左到右的顺序求值,并且在已确定结果为真或假时立即停止计算。
值得注意的是,C语言用整型来表示逻辑,认为非零整数为真,0为假。一元逻辑运算符“!”表示“非”的含义,将0转换为1,而将非零整数转换为0。
自增、自减运算符
“++”“--”分别是自增、自减运算符,作用是将变量递增或递减1。它们的特殊之处在于既可以作为前缀运算符又可以作为后缀运算符,都可以对变量做递增、递减,但区别在于前者先递增再取值,后者先取值再递增。具体来说,假设变量m、n的值都是5,那么执行下面的语句后
x = m++;
y = ++n;
变量m、n的值都变成6,但是x、y的值分别被赋为5、6。
位运算符
。
赋值运算符
“=”是最基本的赋值运算符,它将右边的表达式赋值给左边的变量。但当左边的变量重复出现在右边的表达式中时,可以使用相应的复合运算符,例如:
i = i + 2
可以使用复合运算符“+=”改写为
i += 2
除了“+”,其它二元运算符(“-”“*”“/”“%”“<<”“>>”“&”“^”“|”)也有相应的复合运算符。也就是说,如果expr1、expr2是两个表达式,op是上述二元运算符之一,则
expr1 op= expr2
相当于
expr1 = (expr1) op (expr2)
它们的区别在于,expr1在前一种形式中只被计算一次,而在后一种形式中将被计算两次,所以使用复合运算符不仅可以使代码更易于理解也有利于编译器生成更高效的目标代码。
三元运算符
C语言支持一种使用三元运算符“?:”的条件表达式,具体来说,如果有3个表达式expr1, expr2, expr3,那么在表达式
expr1 ? expr2 : expr3
中,首先计算expr1,如果其值为真(不等于0),那么计算expr2并以其值作为整个条件表达式的值,否则计算expr3并以其值作为整个条件表达式的值。这种条件表达式常用于替换if语句,以编写出更简洁的代码。
运算符的优先级与结合性
如图是C语言中所有运算符的优先级和结合性,同一行中的各运算符具有相同的优先级和结合性,各行从上向下优先级降低,其中有一些规则上文未讲述(例如“->”用于访问结构成员,“(type)”用于类型转换,“sizeof”用于取对象长度)。
如果不能准确掌握某些优先级或结合性,那么在编写程序时应当多使用圆括号。例如在位测试表达式中常用的“&”与“==”运算符,当不明确二者哪个更高时,请使用圆括号将“&”子表达式括起来
if ((x & MASK) == 0) …
事实上,“==”比“&”优先级更高,漏掉圆括号将导致意想不到的错误。
控制流
程序设计语言中的控制流语句用于控制各计算操作执行的次序,C语言提供了常见的if-else、for、while等多种常见的控制流语句。在本节中,expr表示表达式,stmt表示语句。
语句与复合语句
从语法上讲,表达式加上分号“;”构成了语句,类似于“int a;”的声明也是语句。用花括号“{”“}”把多条语句括在一起就构成了复合语句,它在语法上等价于单条语句。
if-else分支语句
if-else语句用于条件判定,其语法为
if (expr)
stmt1
else
stmt2
其中else部分是可选的。该语句执行时先计算expr的值,若其值为真则执行stmt1,否则执行stmt2(如果有else部分)。if语句可以嵌套,但为了避免编译器的解释与程序员的意图不符的错误,通常建议给存在嵌套的if语句使用花括号。
在C语言中,下面的结构常用于多路判定:
if (expr1)
stmt1
else if (expr2)
stmt2
else if …
…
else
stmt
其中各表达式expr1、expr2等将被依次求值,一旦其中某个为真,则执行与之相关的语句并终止整个语句序列的执行;最后的else处理“上述条件均不成立”的情况,也是可选的。
while和for循环语句
在while循环语句
while (expr)
stmt
中,首先计算expr的值,如果为真则执行stmt,然后重复上述过程直到expr为假时终止整个循环语句。
for循环语句
for (expr1; expr2; expr3)
stmt
在不包含continue语句的条件下等价于下面的while循环语句
expr1;
while (expr2) {
stmt
expr3;
}
此外,在for循环中expr1、expr2、expr3都可以省略,当expr2省略时将被认为永远为真,即“无限循环”。
do-while循环语句
另一种常见的循环语句是do-while语句,其用法为
do
stmt
while (expr);
在此语句中,stmt将先被执行,然后计算expr的值,如果为真则重复上述过程,直到expr为假时循环终止。
break、continue和goto语句
break语句用于从for、while、do-while循环中提前退出,也可以用于更复杂的switch语句。continue则用于在循环中立即开始下一次的循环,对于while、do-while循环它将直接跳转到对expr的测试,对于for循环则是使控制转移到expr3(常用于循环变量的更新)的部分。
C语言也提供了goto语句,它用于直接跳转到指定的标号处。在某些情形中,使用goto语句可以带来方便,例如在某些复杂的程序结构中发现了错误需要立即跳转到错误处理部分,如果错误可能出现在多处,使用goto语句会比较方便:
for (…)
for (…) {
…
if (disaster)
goto error;
}
…
error:
…/* Error handler */
然而在绝大多数情况下,goto这一看似简单的语句常常导致程序难以理解和维护,因此通常建议尽可能少使用它。
函数
函数为计算的封装提供了一种简便的方法,一个设计良好的函数可以在使用时无需考虑它是如何实现的,只要知道它具有哪些功能就够了。
函数的定义一般由返回值类型、函数名、参数列表、函数体(语句序列)组成,例如下面是一个名为“strlen”的函数的定义,它接受一个const char*类型的参数s,计算该字符串的长度,返回值类型为unsigned int:
unsigned int strlen(const char* s)
{
unsigned int i = 0;
++i;
return i;
}
其中,return语句表示返回,它结束函数的执行并返回调用者。返回值类型不为空(void)的函数应当包含return语句。函数定义完成后,可以使用下面的语句来调用它,例如
将调用刚定义的strlen函数,计算该字符串的长度,并将返回值赋值给变量len。
函数的定义和调用可以不在同一个源文件,但是在调用前应当有声明,指出函数的返回值类型、名称以及参数类型,例如上面的函数应当声明为
unsigned int strlen(const char* s);
这种声明称为函数原型,它必须与原函数的定义和用法一致,否则将出错(但参数的名称s可以与定义不同,也可以省略)。
一般而言,任何C程序都应当包含一个名称为“main”的函数,它是整个程序的入口——程序从这里开始执行。通常main函数的返回值类型为int,可以包含形式参数(常称为argc、argv)以支持程序启动时命令行参数的获取。main函数的返回意味着程序结束。
标准库
ANSI标准定义了一些标准库,它们并不是C语言本身的构成部分,但支持标准C的实现会提供这些库中的函数声明、类型以及宏定义,可以通过访问标准头文件来使用这些函数、类型以及宏:
assert.h, ctype.h, errno.h, float.h, limits.h, locale.h, math.h, setjmp.h, signal.h, stdarg.h, stddef.h, stdio.h, stdlib.h, string.h, time.h.
访问头文件的方法为使用“#include”预处理指令,例如
#include
常用的头文件有输入输出stdio.h、字符串处理string.h、实用函数stdlib.h、数学计算math.h、日期时间time.h等。这里仅作简单介绍,各个函数、类型以及宏的详细用法请参见C语言相关的参考资料或手册。
输入输出stdio.h
该头文件声明了与输入输出有关的函数、类型以及宏。
fopen、fclose、fread、fwrite等函数用于文件操作,通常用于打开和读写磁盘上的文件。此外,当程序开始执行时,有三个文件已经处于打开状态,它们所对应的文件指针是stdin、stdout、stderr,称为标准输入、标准输出、标准错误。其中,stdin通常默认指向键盘输入,而stdout和stderr则默认指向屏幕显示输出。
fprintf、fscanf等函数用于格式化输出、输入,它们需要接受字符串常量作为格式串,前者按照格式串将用户提供的数据进行转换并输出到指定文件中,后者从指定文件读取输入并按照格式串转换后赋值给用户提供的变量。与之对应的printf、scanf函数,则是分别默认了目标文件是stdout、stdin,常用于向屏幕上打印文字和从键盘读取数据。
此外还有getchar、putchar、getc、putc等函数(或宏),它们也常用于输入和输出,但通常一次操作一个字符。
字符串处理string.h
该头文件声明了两组字符串函数,分别以“str”开头和以“mem”开头,主要用于字符串处理和按照字符数组的方式操作对象。
以“mem”开头的memcpy、memmove、memcmp、memset等函数则用于按字符数组的方式操作对象,对应的操作分别是复制、移动、比较、设置。
实用函数stdlib.h
该头文件声明了一些执行数值转换、内存分配以及其它相关工作的函数。
atoi、atol、atof等转换函数分别用于将字符串转换为整型数(int)、长整型数(long int)和浮点数(double),它们按十进制来解释输入的字符串。更一般的strtol、strtoul可由参数来指定基地,包括但不限于二进制、八进制、十六进制等,然后将字符串转换为长整型或无符号长整型数(unsigned long int)。
malloc、calloc、realloc这三个函数用于动态分配和释放内存空间。malloc分配指定大小的内存空间并返回相应的指针,如果由其管理的内存空间不足,它将通过系统调用向操作系统申请;calloc、recalloc则分别用于为指定长的对象分配内存空间和修改已分配内存空间的长度。free函数可以释放指定的内存空间,但指定的内存空间必须是先前由malloc、calloc或realloc分配的。
abort、exit、system等则是与系统有关的函数。abort和exit分别用于程序的非正常和正常终止,system将一个字符串传递给系统执行环境。它们的具体细节通常与操作系统有关。
数学计算math.h
该头文件声明了一系列数学函数与宏。
sin、cos、tan计算正弦、余弦、正切三角函数,asin、acos、atan计算相应的反三角函数,sinh、cosh、tanh计算相应的双曲函数。
exp、log用于计算指数、对数,它们的底数均为e;pow计算任意底数的幂,log10计算以10为底的对数。
此外还有计算算术平方根的sqrt,将浮点数取整的floor、ceil,以及计算绝对值的fabs等用于完成各种数学处理的函数。
数学函数的返回值通常为double。当出现定义域或值域错误时,它们将设置全局变量errno,但返回值取决于具体实现。
日期时间time.h
该头文件声明了一些与日期时间处理相关的类型和函数。
与时间相关的类型有clock_t、time_t、struct tm等,前两者是表示时间的算术类型,最后一者保存了日历时间的各个构成部分,例如秒数、分钟数、小时数等。
与时间相关的函数有clock、time等,它们分别用于获取程序开始执行后占用处理器的时间和当前系统日历的时间。此外还有用于对时间做运算或转换的difftime、ctime、gmtime、localtime等函数。
优势与缺点
主要优势
相比于大多数程序设计语言,C语言存在很多优势,使得它在各个领域中得到了广泛应用。
简单紧凑
C语言一共只有30余种关键字和9种控制流语句,结构、特性简单并且结合了高级语言的基本结构和低级语言的实用性,精心设计的C语言程序可以非常紧凑又清晰明确。
图:使用C语言可以编写出简洁紧凑的程序
运算符灵活
。充分利用运算符的特性可以用很短的语句实现复杂的功能。
数据结构丰富
C语言提供了数组、指针、结构、联合等复合数据类型,可以满足用户在各种应用场景下对自定义数据结构的需要,使得它具有非常好的通用性,可以用于编写不同领域的大多数程序。
可操作硬件
C语言兼具高级语言和低级语言的特性,可以像汇编语言一样对位、字节、地址等硬件层面的工作单元进行操作,因而成为了编写操作系统、驱动程序等底层软件最常用的语言之一。
效率较高
C语言的“低级”特性使得精心设计的程序具有非常高的执行效率,通常只比汇编语言低10%~20%。且随着编译技术的发展,现代编译器可以为之生成更高质量的目标代码,以充分发挥计算机硬件的性能。
适用范围广
C语言最初是为UNIX而设计,但其适用范围早已不限于UNIX。C语言的可移植性使得同一份程序可以较方便地在Linux、Windows等多种操作系统下运行,还可用于MCU等嵌入式场景。
存在缺点
尽管C语言具有简单高效等优势,但也存在很多缺点,对使用它的程序员提出了较高的要求。
错误隐蔽
C语言的灵活性使得一些错误难以在编译时发现,增大了调试的难度。例如初学者常把相等运算符“==”误写为赋值运算符“=”,由于“=”也可以构成表达式,所以诸如“if (x = 5) …”的语句不会被编译器认为是语法错误,但会导致不符合程序员意图的运行效果,且找出这种隐蔽的错误往往不太容易。错用某些运算符的优先级(例如“&”与“==”)也可能导致灾难性的后果。不过,现代编译器常常能识别出部分语法正确但可能不符合程序员意图的写法并报出警告,关注这些警告可以尽早发现潜在的错误,因此盲目无视警告通常被认为是不好的编程习惯。
不易维护
实际应用中的大型程序常常需要“类”等面向对象的概念,对数据结构的封装、重用等特性提出了较高的需求,这些特性可以将程序分解成更加易于管理的模块。然而,C语言缺乏这些特性,使得大型程序维护起来比较困难。
安全性问题
C语言的数组等数据类型设计简单,在提高了效率的同时也带来了安全性问题。缓冲区溢出就是一种被黑客广为利用的漏洞,对计算机系统的安全造成了严重威胁。具体来说,当保存在函数调用栈上的局部数组在写入时发生越界时,就会侵入其它内存单元并覆盖其数据,从而导致程序出错。下面是一个存在这种漏洞的程序的例子:
int getstationname()
{
char buf[20];
gets(buf);
… /* other processes */
return 0;
}
这段程序使用gets函数从标准输入读取一行字符串并保存到缓冲区buf中进行后续处理,但缓冲区buf的长度仅有20个字节,且C语言默认不会对数组访问进行越界检查。当用户输入“shihudang”时,程序可以正常运行;但如果用户输入了较长的“zhengzhouhangkonggang”,就会导致对buf的写入超过了边界从而溢出,覆盖栈上的关键数据导致程序运行时出错。这种漏洞不仅可以使程序崩溃,还可以被精心构造的输入字符串通过覆盖返回地址使程序转移到攻击者期望的程序段,从而获得更大的权限和对系统的控制。
避免这种风险的方法就是在进行数组访问等操作时用额外的程序进行严格的检查,否则稍有不慎就可能写出致命的漏洞。因此,使用C语言设计程序尤其是操作系统等底层软件时,需要格外细致。
常见标准
C89
C89是美国国家标准学会(ANSI)于1989年为C语言制定的一套标准,即C语言标准ANSI X3.159-1989。C89对函数的定义与声明、指针的操作与类型以及一些编译限制(条件语句的嵌套层数、结构或联合中的成员个数等)做了规定,改变了自C语言诞生以来多套版本不一致的局面。本词条所介绍的特性即属C89的标准。
在随后的1990年,国际标准化组织ISO也制定了同样的标准ISO 9899-1990,该标准被称为C90。但由于C90与C89只有细微的差别,因此它们一般被认为是同一套标准。
C99
在C89标准出现后,C语言标准在相当长一段时间内都没有变化。直到1999年,ANSI又通过了C99标准,它引入了很多新的特性,下面是相比于C89的部分主要变化:
(1) 提高了最小编译限制,例如将条件语句的嵌套层数从8提到了63,将外部符号的有效字符个数从6延长到了31,将函数调用中的参数限制从31增加到了127,等。
(2) 引入了以“//”开头的单行注释。
(3) 增加了对内联(inline)函数的支持。
(4) 允许变量定义在代码中间,而不限于复合语句开头。
(5) 允许for语句内的变量声明。
(6) 允许可变长数组。
(7) 增加了新的数据类型,包括逻辑类型、复数等,以及相应的头文件stdbool.h、complex.h。
C99的部分属性是可选的,即编译器可自行决定是否支持它们。其中的一些新特性一定程度上来自C++,例如(2)(3)(5),它们显著提高了C语言编程的灵活性。
C11
进入21世纪,C语言得到进一步的发展和完善,其中C11就是在2011年出现的一套新标准,它增强C语言的安全性、并发和国际化支持。下面是相对于C99的部分变化:
(1) 增加了数据对齐方式的说明。
(2) 引入threads.h,支持多线程。
(3) 改进了对Unicode的支持。
(4) 使用关键字_Generic的泛型表达式。
(5) 提供了边界检查接口。
与C99类似,C11的部分特性也是可选的,其中包括边界检查接口、多线程,以及在C99中必须支持的复数类型和可变长数组。
应用举例
操作系统开发
C语言最初即为开发UNIX操作系统而设计的,用C语言改写的第3版UNIX具有非常好的可读性和可移植性,为推广普及奠定了基础,也说明了C语言在系统开发领域的优势。
1991年,芬兰赫尔辛基大学的林纳斯·托瓦兹(Linus B. Torvalds)开发了一个操作系统内核,后来经过无数志同道合的程序员们的共同参与,成为了一种得到广泛应用的多用户多任务的操作系统,即现在的GNU/Linux。GNU/Linux的源代码开放,能在互联网上自由下载,与Windows等一起成为了操作系统的主流市场。如今的Linux内核的源代码已经有数千万行,其中主要都是C语言。
另一个大规模使用C语言的例子是Windows,它是微软公司于1983年开始研发的操作系统,凭借易学易用、图形界面友好等特性,逐步占领了微型计算机的操作系统市场。据统计,截至2018年底,在桌面计算机操作系统领域,Windows 10/8/7/XP等版本的市场占有率合计为86.2%,成为了人们使用最多的操作系统。
嵌入式开发
C语言简单高效等优势使其在物联网、嵌入式领域也得到了广泛应用,其中比较重要的单片机程序开发和项目设计也越来越多地使用C语言。例如常见的51单片机编程所用的C语言,在Keil集成开发环境中称为Keil C51,简称C51,即用于51单片机开发的C程序。Keil C51在兼容ANSI C的基础上,又增加了很多与51单片机硬件资源相关的关键字、库函数和编译特性,使得51系列单片机程序的开发变得更为方便快捷。其它系列的单片机,如STM、AVR等,也常常使用C语言进行开发。