一、起源与发展背景
20 世纪 70 年代初期,计算机行业正经历着一场技术变革。当时,贝尔实验室的肯・汤普森(Ken Thompson)已经开发出了 UNIX 操作系统的雏形,但它是用汇编语言编写的。汇编语言依赖于特定的硬件架构,可移植性极差,这使得 UNIX 系统在不同计算机之间的移植变得异常困难。
为了解决这一问题,肯・汤普森在 1970 年基于 BCPL 语言开发出了 B 语言。B 语言是一种解释型语言,它在一定程度上提高了编程效率,也增强了程序的可移植性。然而,B 语言存在诸多局限性。它是无类型语言,不支持结构体,类型检查机制薄弱,在处理复杂数据结构和大型程序时力不从心。
在这样的背景下,丹尼斯・里奇(Dennis Ritchie)在 B 语言的基础上进行了大刀阔斧的改进。他保留了 B 语言简洁、灵活的特点,同时引入了数据类型的概念,增加了 int、char 等基本数据类型,以及结构体(struct)这一重要的构造数据类型,使得 C 语言能够更精确地描述数据。1972 年,C 语言正式诞生,并被用于重写 UNIX 操作系统。
C 语言的出现,使得 UNIX 系统的移植工作变得轻松许多。到 1973 年,UNIX 系统的大部分代码都用 C 语言重写完成,这不仅大大提高了 UNIX 的可移植性,也让 C 语言随着 UNIX 的广泛传播而逐渐被人们所熟知和接受。
20 世纪 80 年代,随着微型计算机的兴起,C 语言凭借其高效、可移植等特性,迅速在微机领域得到推广。许多操作系统、编程语言编译器等都采用 C 语言开发,C 语言逐渐成为当时最流行的编程语言之一。
二、显著特点
1. 简洁紧凑、灵活方便
C 语言的关键字仅有 32 个(C89 标准),如 auto、break、case 等,数量远少于许多其他高级语言。这使得 C 语言的语法体系相对简洁,容易被程序员掌握。
其程序书写格式非常自由,没有严格的缩进要求(虽然良好的缩进是编程规范所倡导的),程序员可以根据自己的编程习惯安排代码的布局。例如,一条语句可以分多行书写,也可以多条语句写在同一行(用分号分隔)。
这种灵活性还体现在对变量的使用上,变量可以在需要时才定义(C99 标准开始支持),而不必像某些语言那样必须在函数开头集中定义。如:
for (int i = 0; i < 10; i++) { // 在循环中定义变量i
printf("%d ", i);
}
2. 运算符丰富
C 语言的运算符多达 34 种,涵盖了各种运算需求。
- 算术运算符中的自增(++)和自减(--)运算符非常有特色,它们可以使代码更加简洁。如i++表示先使用 i 的值,再将 i 加 1;++i则表示先将 i 加 1,再使用 i 的值。在循环控制中经常用到,如for (i = 0; i < 10; i++)。
- 位运算符在底层编程中不可或缺。例如,要将一个整数的某一位置为 1,可以使用按位或(|)运算:a |= (1 << n),其中 n 为要置 1 的位的位置。要清除某一位,则可以使用按位与(&)运算:a &= ~(1 << n)。
- 条件运算符(?:)是一种三目运算符,能够简化简单的条件判断语句。如max = (a > b) ? a : b,等价于:
if (a > b) {
max = a;
} else {
max = b;
}
3. 数据类型丰富
- 基本数据类型除了常见的 int、char、float、double 外,还有各种修饰符,如 short、long、signed、unsigned 等,用于表示不同范围和精度的数据。例如,short int 表示短整数,其取值范围通常比 int 小;long long int 则可以表示更大的整数。
- 构造数据类型中的共用体(union)也很有特点,它的所有成员共享同一段内存空间,这使得它可以在不同时刻存储不同类型的数据,节省内存空间。如:
union Data {
int i;
float f;
char c;
};
union Data data;
data.i = 10; // 存储整数
printf("%d", data.i);
data.f = 3.14f; // 存储浮点数,会覆盖之前的整数
printf("%f", data.f);
- 枚举类型(enum)可以为一组整数常量命名,增加代码的可读性。如:
enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
};
enum Weekday today = Monday;
默认情况下,Monday 的值为 0,Tuesday 为 1,依此类推。
4. 具有结构化的控制语句
- if-else 语句可以嵌套使用,实现多分支判断。但嵌套层次不宜过多,否则会降低代码的可读性。例如:
if (score >= 90) {
printf("优秀");
} else if (score >= 80) {
printf("良好");
} else if (score >= 60) {
printf("及格");
} else {
printf("不及格");
}
- switch 语句适用于多分支判断,且判断条件只能是整数类型或字符类型。case 后面的常量表达式必须是不同的值。default 子句用于处理所有未被 case 匹配的情况。如:
switch (grade) {
case 'A':
printf("90-100");
break;
case 'B':
printf("80-89");
break;
default:
printf("其他");
}
break 语句的作用是跳出 switch 结构,若没有 break,则会继续执行下一个 case 的语句。
- for 循环适合于已知循环次数的情况,其三个表达式(初始化、循环条件、更新)可以灵活设置。例如,遍历数组时:
int arr[5] = {1, 2, 3, 4, 5};
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
- while 循环和 do-while 循环适用于循环次数未知的情况,区别在于 do-while 循环至少会执行一次循环体。如用 while 循环计算 1 到 100 的和:
int sum = 0, i = 1;
while (i <= 100) {
sum += i;
i++;
}
5. 语法限制不太严格,程序设计自由度大
C 语言允许程序员直接操作内存地址,这虽然带来了灵活性,但也增加了出错的风险。例如,程序员可能会不小心访问到非法的内存地址,导致程序崩溃。
在类型转换方面,C 语言允许隐式类型转换,但有时会造成意想不到的结果。如将浮点型数据赋给整型变量时,会自动截断小数部分:
int a = 3.14; // a的值为3
程序员需要自己注意类型转换可能带来的问题。
6. 允许直接访问物理地址,能进行位操作,可以直接对硬件进行操作
在嵌入式系统开发中,经常需要操作硬件寄存器,这些寄存器都有特定的物理地址。C 语言可以通过指针来访问这些地址。例如,要向地址为 0x12345678 的寄存器写入数据 0x55:
*(volatile unsigned int *)0x12345678 = 0x55;
其中 volatile 关键字用于告诉编译器该变量的值可能会被意外修改,防止编译器进行不必要的优化。
位操作可以直接对硬件的控制位进行设置。如某一硬件设备的控制寄存器的第 3 位用于启动设备,那么可以通过*control_reg |= (1 << 3)来启动设备。
7. 生成目标代码质量高,程序执行效率高
C 语言编译后的目标代码与汇编语言编写的程序在效率上相差无几,通常只低 10%~20%。这是因为 C 语言的设计贴近硬件,编译器能够生成高效的机器指令。
在对性能要求极高的领域,如实时控制系统,C 语言是首选语言。例如,在工业自动化控制中,需要实时响应传感器的信号并做出处理,C 语言的高效性能保证系统的实时性。
8. 可移植性好
C 语言的标准库提供了一组统一的函数接口,如输入输出函数 printf、scanf,字符串处理函数 strcpy、strcat 等。这些函数在不同的平台上具有相同的功能,只是底层实现可能不同。
只要遵循 C 语言的标准,编写的程序可以在不同的操作系统(如 Windows、Linux、macOS)和不同的硬件架构(如 x86、ARM)上通过重新编译而运行。例如,一个简单的计算两数之和的 C 程序,在 Windows 下用 Visual Studio 编译,在 Linux 下用 GCC 编译,都能正常运行。
9. 表达力强
C 语言可以用较少的代码实现复杂的功能。例如,利用指针和结构体可以实现链表、树等复杂的数据结构。下面是一个简单的单链表节点定义和插入操作:
struct Node {
int data;
struct Node *next;
};
struct Node* insert(struct Node *head, int value) {
struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));
newNode->data = value;
newNode->next = head;
return newNode;
}
通过这样简洁的代码,就实现了链表的插入功能。
三、语法结构
- 数据类型
- 基本数据类型:
-
- int:通常占 4 个字节(32 位),取值范围为 - 2147483648 到 2147483647。可以用 short、long 修饰,如 short int(2 字节)、long int(4 或 8 字节)。
-
- char:占 1 个字节,既可以表示字符(如 'A'、'b'),也可以表示小范围的整数(-128 到 127,signed char)或 0 到 255(unsigned char)。字符在内存中以 ASCII 码形式存储,如 'A' 的 ASCII 码是 65。
-
- float:单精度浮点数,占 4 个字节,精度约为 6-7 位有效数字。
-
- double:双精度浮点数,占 8 个字节,精度约为 15-17 位有效数字。
- 构造数据类型:
-
- 数组:数组的大小在定义时可以指定,也可以通过初始化列表自动确定(C99 支持变长数组)。例如,int arr[] = {1, 2, 3, 4};定义了一个包含 4 个整数的数组。数组元素通过下标访问,下标从 0 开始,如 arr [0] 表示第一个元素。
-
- 结构体:结构体成员可以是不同的数据类型,访问结构体成员可以使用点运算符(.)或箭头运算符(->,用于结构体指针)。如:
struct Student {
char name[20];
int age;
float score;
};
struct Student stu = {"Tom", 18, 90.5};
printf("%s %d %f", stu.name, stu.age, stu.score);
struct Student *p = &stu;
printf("%s", p->name);
- 共用体:如前面所述,共用体成员共享内存空间,其大小等于最大成员的大小。
- 枚举类型:枚举常量的值可以手动指定,如enum Color {RED=2, GREEN=4, BLUE=6};。
- 派生数据类型:
-
- 指针类型:指针变量存储的是另一个变量的地址。指针的类型决定了它所指向变量的类型。如 int *p 表示 p 指向 int 类型的变量,char *q 表示 q 指向 char 类型的变量。指针可以进行加减运算,其步长取决于所指向类型的大小。例如,int *p,p++ 表示 p 指向的地址增加 4 个字节(假设 int 占 4 字节)。
-
- 函数类型:函数名代表函数的入口地址,函数指针可以指向函数,从而实现函数的动态调用。如:
int add(int a, int b) {
return a + b;
}
int (*funcPtr)(int, int) = add; // 函数指针指向add函数
int result = funcPtr(3, 5); // 调用add函数,结果为8
- 数组类型:数组的类型由元素类型和数组大小共同决定,如 int [5] 是一个包含 5 个 int 元素的数组类型。
2. 变量和常量
- 变量的存储类别包括 auto、static、register、extern。
-
- auto 变量:在函数内部定义的变量默认是 auto 类型,其作用域是所在的函数或复合语句,生命周期是从定义到函数或复合语句结束。
-
- static 变量:在函数内部定义的 static 变量,其生命周期是整个程序运行期间,只会初始化一次;在函数外部定义的 static 变量,其作用域是所在的文件,其他文件不能访问。
-
- register 变量:建议编译器将变量存储在寄存器中,以提高访问速度。但编译器可能会忽略该建议,且 register 变量不能取地址。
-
- extern 变量:用于声明在其他文件中定义的全局变量,表明该变量已经在别处定义,可以在当前文件中使用。
- 常量除了 const 修饰的常量外,还有字面常量,如整数常量(123)、浮点数常量(3.14)、字符常量('a')、字符串常量("hello")。字符串常量在内存中以字符数组的形式存储,末尾会自动添加一个 '\0' 作为结束标志。
3. 运算符和表达式
- 运算符的优先级和结合性非常重要,决定了表达式的求值顺序。例如,乘法运算符的优先级高于加法运算符,所以 a + b * c 先算乘法。赋值运算符的结合性是从右向左,所以 a = b = c 等价于 a = (b = c)。
- 逗号表达式由多个表达式用逗号连接而成,其值为最后一个表达式的值。如int x = (a = 3, b = 5, a + b);,x 的值为 8。
4. 语句
- 控制语句中的 break 语句可以用于跳出 switch 结构和循环结构;continue 语句用于结束本次循环,开始下一次循环。
- goto 语句可以无条件跳转到函数内的标号处,但过度使用会使程序结构混乱,不利于维护,一般不建议使用。
5. 函数
- 函数可以分为库函数和用户自定义函数。库函数是 C 语言标准库提供的,如 printf、scanf 等,使用时需要包含相应的头文件(如 stdio.h)。
- 函数的参数传递方式是值传递,即函数接收的是实参的副本,对形参的修改不会影响实参。但可以通过指针传递地址,实现对实参的间接修改。如:
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int x = 3, y = 5;
swap(&x, &y); // 调用后x=5,y=3
- 函数可以递归调用,即函数自己调用自己。递归调用需要有终止条件,否则会导致栈溢出。例如,计算 n 的阶乘:
int factorial(int n) {
if (n == 0 || n == 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
四、应用领域
- 系统软件
- 操作系统:Linux 内核几乎全部用 C 语言编写,它的许多核心模块,如进程管理、内存管理、文件系统等,都依赖于 C 语言的高效性和对硬件的直接操作能力。Windows 内核的大部分代码也是用 C 语言编写的,部分使用了 C++。
- 编译器:许多编程语言的编译器是用 C 语言开发的,如 GCC(GNU Compiler Collection),它可以编译 C、C++、Java 等多种语言。编译器需要对源代码进行词法分析、语法分析、代码生成等复杂处理,C 语言的强大表达力和高效性使其能够胜任这一工作。
- 数据库管理系统:MySQL 的核心部分是用 C 和 C++ 编写的,C 语言负责底层的数据存储和访问操作,保证了数据库的高效运行。
2. 应用软件
- 办公软件:早期的一些办公软件,如 Lotus 1-2-3,部分模块使用 C 语言开发。C 语言的可移植性使得这些软件能够在不同的平台上运行。
- 图形图像处理软件:一些专业的图形图像处理软件,如 GIMP(GNU Image Manipulation Program),其底层的图像处理算法很多是用 C 语言实现的,因为 C 语言能够高效地处理大量的像素数据。
3. 嵌入式系统
- 智能家电:如智能冰箱、智能电视的控制系统,需要对温度传感器、显示屏等硬件进行控制,C 语言能够直接操作这些硬件的接口,实现各种功能。
- 汽车电子:汽车的发动机控制系统、防抱死制动系统(ABS)等,需要实时处理各种传感器数据并做出快速响应,C 语言的高效性和实时性能够满足这些要求。
4. 游戏开发
早期的许多游戏,如《DOOM》《Quake》,其引擎部分是用 C 语言开发的。游戏引擎需要处理复杂的物理计算、图形渲染等任务,C 语言的高性能能够保证游戏的流畅运行。即使在现在,一些游戏的底层模块仍然使用 C 语言开发。
5. 科学计算
在一些对性能要求较高的科学计算领域,如天气预报、流体力学模拟等,C 语言可以用于编写核心的计算程序。虽然 Python 等语言在科学计算中更易用,但对于大规模的数值计算,C 语言的效率优势明显。
五、标准演变
- K&R C
1978 年,Brian Kernighan 和 Dennis Ritchie 合著的《The C Programming Language》一书出版,书中定义了 C 语言的语法和特性,被称为 K&R C。它没有正式的标准文档,只是一种事实标准。
K&R C 缺少一些现代 C 语言的特性,如函数原型、const 关键字等。在函数定义时,参数的类型需要在函数体内部声明,如:
int add(a, b)
int a, b; {
return a + b;
}
2. ANSI C(C89/C90)
1989 年,ANSI 发布了第一个 C 语言标准 X3.159-1989,即 C89。1990 年,ISO 采纳该标准,命名为 ISO/IEC 9899:1990,即 C90。两者内容基本一致,通常统称为 C89/C90。
C89 增加了函数原型,要求在调用函数前声明函数的参数类型和返回值类型,提高了程序的安全性。引入了 const 关键字,用于定义常量。还增加了 void 类型,用于表示无返回值的函数或无类型指针(void *)。
3. C99
1999 年,ISO 发布了 C99 标准(ISO/IEC 9899:1999)。
- 增加了 long long int、_Bool(布尔类型,需包含 stdbool.h 头文件,可使用 bool、true、false)等新的数据类型。
- 支持可变长度数组(VLA),即数组的大小可以是变量,如int n = 5; int arr[n];。
- 允许在 for 循环的初始化部分定义变量,如for (int i = 0; i < 10; i++)。
- 增加了 inline 关键字,用于声明内联函数,减少函数调用的开销。
- 引入了复数类型(_Complex)和虚数类型(_Imaginary)。
然而,C99 标准推出后,一些编译器的支持并不及时和完全,如微软的 Visual Studio 对 C99 的支持就比较有限。
4. C11
2011 年,ISO 发布了 C11 标准(ISO/IEC 9899:2011)。
- 增加了对多线程编程的支持,引入了 thread.h 头文件,提供了创建线程、互斥锁等函数。
- 增加了泛型宏(_Generic),可以根据参数类型选择不同的宏定义。
- 引入了原子操作(stdatomic.h),用于多线程环境下的数据同步。
- 增强了对 Unicode 的支持,增加了 char16_t、char32_t 等类型。
5. C17/C18
C17(ISO/IEC 9899:2018)是 C 语言的一个小更新标准,主要是修复了 C11 中的一些缺陷,没有引入新的语言特性。它有时也被称为 C18。
目前,主流的编译器如 GCC、Clang 对 C11 和 C17 标准都有较好的支持,而 Visual Studio 在较新的版本中也逐渐增加了对这些标准的支持。
不同的标准反映了 C 语言随着时代发展的适应性,每个标准都在不断完善语言的功能,使其能够更好地满足不同领域的编程需求。程序员在开发时,需要根据目标平台和编译器的支持情况选择合适的标准。