C语言(英语:C Language)是一种通用的、过程式编程编程语言,支持结构化编程、词法作用域和递归,使用静态类型系统,并且广泛用于系统软件与应用软件的开发。
所谓类型,在计算机科学中,类型系统(英语:type system)用于定义如何将编程语言中的数值和表达式归类为许多不同的类型,如何操作这些类型,这些类型如何互相作用。类型可以确认一个值或者一组值具有特定的意义和目的(虽然某些类型,如抽象类型和函数类型,在程序运行中,可能不表示为值)。类型系统在各种语言之间有非常大的不同,也许,最主要的差异存在于编译时期的语法,以及运行时期的操作实现方式。
类型检查
类型检查所进行的检验处理以及实行类型的约束,可发生在编译时期(静态检查)或运行时期(动态检查)。静态类型检查是在编译器所进行语义分析中进行的。如果一个语言强制实行类型规则(即通常只允许以不丢失信息为前提的自动类型转换)就称此处理为强类型,反之称为弱类型。
静态和动态检查
如果一个编程语言的类型检查,可在不测试运行时期表达式的等价性的情况下进行,该语言即为静态类型的。一个静态类型的编程语言,是在运行时期和编译时期之间的处理阶段下重视这些区别的。如果程序的独立模块,可进行各自的类型检查(独立编译),而无须所有会在运行时出现的模块的那些信息,该语言即具有一个编译时期阶段。如果一个编程语言支持运行时期(动态)调度已标记的资料,该语言即为动态类型的。如果一个编程语言破坏了阶段的区别,因而类型检查需要测试运行时期的表达式的等价性,该语言即为依存类型的。[1]
在动态类型中,经常在运行时期进行类型标记的检查,因为变量所约束的值,可经由运行路径获得不同的标记。在静态类型编程语言中,类型标记使用识别联合类型表示。
动态类型经常出现于脚本语言和RAD语言中。动态类型在解释型语言中极为普遍,编译语言则偏好无须运行时期标记的静态类型。对于类型和隐式类型语言较完整的列表参见类型和隐式类型语言。
术语推断类型(鸭子类型,duck typing)指的是动态类型在语言中的应用方式,它会“推断”一个数值的类型。
看看类型标记检查是如何运作的,考虑下列假码示例:
var x; //(1) x := 5; //(2) x := "hi"; //(3)
在这个示例中,(1)宣告x;(2)将整数值5代给x;(3)将字符串值"hi"代给x。在主要的静态系统中,这个代码片断将会违反规则,因为(2)和(3)对 x所约束的类型相矛盾。
相较之下,一个纯粹的动态类型系统允许上述程序的运行,因为类型标记附到数值上(不是变量)。在处理错误语句或表达式的时候,以动态类型实现的语言会捕捉程序的错误,而不是误用错误类型的数值。换句话说,动态类型捕捉在程序运行时的错误。
典型的动态类型实现,会以类型标记维持程序所有数值的“标记”,并在运算任何数值之前检查标记。例如:
var x := 5; //(1) var y := "hi"; //(2) var z := x + y; //(3)
在这个程序片断中,(1)将数值5约束给x;(2)将数值"hi"约束给y;以及(3)尝试将x加到y。在动态类型语言中,约束给x的值会是一对(整数, 5),且约束给y的值会是一对(字符串, "hi")。当这个程序尝试运行第3行时,语言对类型标记整数和字符串进行检查,如果这两个类型的+(加法)运算尚未定义,就会发出一个错误。
某些静态语言有一个“后门”,在这些编程语言中,能够编写一些不被静态类型所检查的代码。例如,Java和C-风格的语言有“转型”可用。在静态类型的编程语言中,不必然意味着缺乏动态类型机制。例如Java使用静态类型,但某些运算需要支持运行时期的类型测试,这就是动态类型的一种形式。更多静态和动态类型的讨论,请参阅编程语言。
继续讲回C语言
C语言于1969年至1973年间,为了移植与开发UNIX操作系统,由丹尼斯·里奇与肯·汤普逊,以B语言为基础,在贝尔实验室设计、开发出来。二十世纪八十年代,C语言应用日渐广泛。为了避免各开发厂商用的C语言的语法产生差异,美国国家标准局为C语言订定了一套完整的国际标准语法,称为ANSI C,作为C语言的标准。与此同时,国际标准化组织也接受该标准为国际标准。因此,ANSI C也同时被称为ISO C。二十世纪八十年代至今的有关程序开发工具,一般都支持符合ANSI C的语法。
C语言具有高效、灵活、功能丰富、表达力强和较高的可移植性等特点,在程序设计中备受青睐,成为最近25年使用最为广泛的编程语言[4]。目前,C语言编译器普遍存在于各种不同的操作系统中,例如Microsoft Windows、macOS、Linux、Unix等。C语言的设计影响了众多后来的编程语言,例如C++、Objective-C、Java、C#等。现行的许多软件都是由C语言或者其影响和派生的编程语言开发出来的。
与ALGOL一族的大多数过程式编程语言类似,C语言是一个有结构化程序设计、具有变量作用域(variable scope)以及递归功能的过程式语言。其采用的静态类型系统可以防止无意的程序设计操作。C语言中所有的可执行代码都被包含在子程序(函数)里。其传递参数均是以值传递(pass by value)[5],另外也可以传递指针(a pointer passed by value)。C语言是自由形式语言,即其源代码的缩进并不影响程序的功能,而是使用分号作为语句的结尾,花括号来表示代码块。
由于C语言的语言规模较小,若干高层的机制需要使用定义的函数来提供。比如,C语言并没有直接处理复合对象(例如字符串、集合、列表、数组等)的操作,也没有对于存储器分配工具和内存回收工具的直接定义,同时也本身不具有输入和输出以及文件访问的方法。然而,用户定义的函数和C语言标准库中的函数为这些高层的机制提供了可能性。
C语言也具有以下的特性:[6]
- 基本数据类型包括字符、整型和浮点数等。另外也有派生的各种数据类型,如指针、数组、结构和联合。
- 部分的变量类型可以转换,例如整数型和字符型变量。
- 透过指针(pointer),C语言可以容易的对存储器进行低端控制。
- 不同的变量类型可以用结构体(struct)组合在一起。
- 具有基本的控制流:语句组、条件判断、多路选择、循环等。
- 函数可以返回各种数据类型的值,并且都可以递归调用。每次调用函数会重新创建变量。
- C语言只有32个保留字(reserved keywords),使变量、函数命名有更多弹性。
- 编译预处理(preprocessor)让C语言的编译更具有弹性。
语法
C语言的语法相对简洁而直接。C语言的形式文法由国际标准化组织所制定。[8]简单来说,C语言包括如下文法:
- 作为一种指令式编程语言,C语言使用语句执行操作。最常见的语句是表达式语句,由一个表达式后加一个分号组成,可以令系统调用函数和为变量赋值;
- 注释: C语言支持单行注释(以
//开头)和多行注释(以/*开始,以*/结束); - 数据类型: 基本的数据类型包括整数(
int)、浮点数(float和double)、字符(char)、枚举enum等; - 数组: 数组是一组相同类型的数据元素的集合。使用以下方法初始化一个五个元素的整数数组:
int numbers[5] = {1, 2, 3, 4, 5}; - 封装结构:结构(
struct)、联合(union); - 结构化编程和控制结构: C语言包括条件语句(
if、else)、循环语句(for、while、do-while)等; - 跳转语句:C语言允许使用跳转关键字
goto、break和continue来实现程序块之间的跳转,这和汇编语言的jmp关键字有一定相似处; - 函数: C语言中的函数是程序的基本模块,可以自定义函数并在程序中调用;
- 灵活且靠近底层的内存控制机制:C程序员可以自由选择分配何种内存,以及分配多大的内存,如如下代码所示:
int *array = (int *)malloc(5 * sizeof(int)); // 分配一個包含五個整數的數組 free(array); // 釋放使用malloc分配的內存
现在广泛被编程初学者使用的"hello, world"程序实例最初就是出现在《C程序设计语言》第一版中。下面是一个在标准输出设备(stdout)上打印出 "Hello, world!" 字符串的简单程序。类似的程序,通常作为初学编程语言时的第一个程序:
#include <stdio.h>
int main(void) {
printf("Hello, world!\n");
return 0;
}
其中只有int,void,return为C语言的关键字,预处理器会将#include <stdio.h>替换为stdio.h文件的内容。
main函数是C语言程序的入口点。
"Hello, world!\n"中的\n是一个转义字符,形式为\加上一个字符。所起的作用在ASCII码中规定。
printf是声明于stdio.h的函数,关于printf的更多细节,参见printf;
关于格式化字符串的更多信息,参见格式化字符串。
内存管理
C语言的特色之一是:程序员必须亲自处理内存的分配细节。语言不负责内存边界检查,这是因为在运行时进行内存边界检查会造成性能问题,与UNIX哲学不符。此特性容易导致缓冲区溢出问题。然而,部分编译器(如英特尔编译器)会出于安全性的考量,提供方法以进行运行时内存边界检查[9]。
大多数C语言实现使用栈(Stack)来保存函数返回地址/栈帧基址、完成函数的参数传递和函数局部变量的存储。然而,在部分极特殊的平台上,使用栈并不能获得最大效率。此时的实现由编译器决定[10]。 如果程序需要在运行的过程中动态分配内存,可以利用堆(Heap)来实现。
基本上C程序的元素存储在内存的时候有3种分配策略:
- 静态分配
如果一个变量声明为全局变量或者是函数的静态变量,这个变量的存储将使用静态分配方式。静态分配的内存一般会被编译器放在数据段或代码段来存储,具体取决于实现。这样做的前提是,在编译时就必须确定变量的大小。 以IA32的x86平台及gcc编译器为例,全局及静态变量放在数据段的低端;全局及静态常量放在代码段的高端。
- 自动分配
函数的自动局部变量应该随着函数的返回会自动释放(失效),这个要求在一般的体系中都是利用栈(Stack)来满足的。相比于静态分配,这时候,就不必绝对要求这个变量在编译时就必须确定变量的大小,运行时才决定也不迟,但是C89仍然要求在编译时就要确定,而C99放松了这个限制。但无论是C89还是C99,都不允许一个已经分配的自动变量运行时改变大小。
所以说C函数永远不应该返回一个局部变量的地址。
要指出的是,自动分配也属于动态分配,甚至可以用alloca函数来像分配堆(Heap)一样进行分配,而且释放是自动的。
- 动态分配
还有一种更加特殊的情况,变量的大小在运行时有可能改变,或者虽然单个变量大小不变,变量的数目却有很大弹性,不能静态分配或者自动分配,这时候可以使用堆(Heap)来满足要求。ANSI C定义的堆操作函数是malloc、calloc、realloc和free。
使用堆(Heap)内存将带来额外的开销和风险。
库
C语言的标准文档要求了一个平台移植C语言的时候至少要实现的一些功能和封装的集合,称为“标准库”,标准库的声明头部通过预处理器命令#include进行引用。
在C89标准中:
| 文件 | 简介说明 |
|---|---|
| <assert.h> | 断言相关 |
| <ctype.h> | 字符类型判断 |
| <errno.h> | 标准报错机制 |
| <float.h> | 浮点运算 |
| <limits.h> | 各种体系结构限制 |
| <locale.h> | 本地化接口 |
| <math.h> | 数学函数 |
| <setjmp.h> | 跨函数跳转 |
| <signal.h> | 信号(类似UNIX的信号定义,但是差很远) |
| <stdarg.h> | 可变参处理 |
| <stddef.h> | 一些标准宏定义 |
| <stdio.h> | 标准I/O库 |
| <stdlib.h> | 标准工具库函数 |
| <string.h> | ASCII字符串及任意内存处理函数 |
| <time.h> | 时间相关 |
在94年的修正版中
- <iso646.h>
- <wchar.h>
- <wctype.h>
在C99中增加了六个函数库
- <complex.h>
- <fenv.h>
- <inttypes.h>
- <stdbool.h>
- <stdint.h>
- <tgmath.h>
以上是C语言的标准。各个系统各自又对C库函数进行的各种扩充,就浩如烟海了。如POSIX C、GNU C等。
经典错误
void main() 的用法并不是任何标准制定的。 C语言标准语法是 int main(),任何实现都必须支持int main(void) { /* ... */ }和int main(int argc, char* argv[]) { /* ... */ }。 在 C++ 标准中,main的标准类型应是int,否则类型是由实现定义的。任何实现都必须支持int main() { /* ... */ }和int main(int argc, char* argv[]) { /* ... */ }。
int main(int argc, char* argv[])
允许程序接收命令行参数,其中:
argc是参数个数(包括程序名称)。argv是一个字符串数组,存储了命令行参数。
意思就是,实现还可以支持其他形式的 main(比如增加更多的参数或不同的返回类型),但这些都是“实现定义”的,也就是说,它们是特定编译器或系统支持的,而非标准定义的。
1万+

被折叠的 条评论
为什么被折叠?



