第1章 C语言入门
Hello World的故事
一、导读
1.1 本章学习目标
第1章是整本《C程序设计语言》的开篇之作,由C语言的创造者 Dennis Ritchie 亲自执笔。本章以一个经典的 hello, world 程序为起点,带领读者走进C语言的世界。这不是一段普通的代码——它是理解C语言程序结构、编译模型和开发流程的钥匙。
本章的核心学习目标包括:
- 理解C程序的基本结构:
main函数、预处理指令、语句和表达式如何组成一个完整的程序。 - 掌握编译与运行的基本流程:从源文件(
.c)到可执行文件,经历了哪些阶段? - 熟悉 `printf` 函数:格式化输出的基本用法,以及转义字符的含义。
- 理解 `#include` 的作用:预处理指令如何将标准库引入程序。
- 认识C语言的简洁性:为什么C语言只用少量关键字就能表达复杂的逻辑?
- 建立开发环境的基本认知:编译器、源文件、可执行文件之间的关系。
1.2 为什么第一章如此重要
很多初学者急于跳入具体的语法细节,觉得第一章"太简单"。这种想法是极其危险的。正如学习任何一门语言都需要先掌握字母表和最基本的问候语,学习C语言也需要从最基础的结构开始。第一章虽然简短,但它展示了C程序的全部骨架——后续所有章节的内容,最终都要回到这个骨架上来。
K&R 在第一章就展示了C语言最核心的设计哲学:程序由函数组成,函数由语句组成,语句操作数据和值。理解了这个基本框架,后续学习类型、控制流、指针等概念时,你就知道它们在这个框架中处于什么位置。
更重要的是,hello, world 程序是每一个C程序员的起点。从Brian Kernighan在1972年第一次写下这个程序至今,它已经成为全世界程序员的共同语言。当你写下 printf("hello, world\n"); 的那一刻,你正在延续半个世纪的传统。
1.3 核心问题引导
在阅读本章时,请带着以下问题思考:
一个最小的C程序需要哪些组成部分?缺少其中任何一个会怎样?
#include <stdio.h> 到底做了什么?如果不写这行会怎样?
main 函数的返回值有什么意义?为什么需要 return 0?
\n 和直接写换行有什么区别?为什么需要转义字符?
C程序是如何从源代码变成可执行文件的?
为什么说C语言是"小而强大"的?它的关键字为什么这么少?
二、核心概念详解
2.1 第一个C程序:hello, world
让我们从C语言最经典的程序开始:
#include <stdio.h>
int main() {
printf("hello, world\n");
return 0;
}这个程序虽然只有四行有效代码,但包含了C程序的几乎所有基本要素。让我们逐行分析。
2.1.1 预处理指令 #include
#include <stdio.h>#include 是一条预处理指令,它在编译之前由预处理器处理。<stdio.h> 是C标准库中的标准输入输出头文件(Standard Input/Output Header),它声明了 printf、scanf、getchar、putchar 等函数的原型。
预处理指令以 # 开头,它们不是C语句,不需要分号结尾。预处理器在处理 #include 时,会将指定头文件的内容原封不动地插入到当前源文件中。这意味着,经过预处理后,你的源文件会变大——包含了 stdio.h 中所有的函数声明和宏定义。
头文件有两种包含方式:
#include <stdio.h> // 尖括号:从系统目录查找
#include "myheader.h" // 双引号:先从当前目录查找,再到系统目录常见误区:很多初学者认为 #include 是"导入"了一个库。实际上,它只是将头文件的文本复制到了源文件中。真正的库代码是在链接阶段才被引入的。
2.1.2 main 函数
int main() {
...
return 0;
}main 函数是每个C程序的入口点。当操作系统运行一个C程序时,它实际上调用的是 main 函数。无论你的程序有多少个函数,执行总是从 main 开始。
main 前面的 int 表示这个函数返回一个整数值。括号中的参数列表为空(在ANSI C中也可以写成 int main(void)),表示 main 函数不接受参数。实际上,main 函数可以接受命令行参数,这将在后面讨论。
return 0 表示程序正常退出。返回值会被操作系统接收:在UNIX系统中,返回值0表示成功,非零值表示某种错误。你可以通过 $? 变量查看上一个程序的退出状态:
./hello
echo $? # 输出 0,表示成功2.1.3 printf 函数
printf("hello, world\n");printf 是C标准库中最常用的函数之一,用于格式化输出到标准输出(通常是终端屏幕)。它接受一个格式字符串作为第一个参数,后面可以跟若干个要输出的值。
在这个简单的例子中,格式字符串是 "hello, world\n"。其中 \n 是一个转义字符,代表换行符(newline)。C语言中的转义字符都以反斜杠 \ 开头,常见的包括:
| 转义字符 | 含义 | ASCII码 |
|---|---|---|
\n | 换行 | 0x0A |
\t | 水平制表符 | 0x09 |
\\ | 反斜杠本身 | 0x5C |
\" | 双引号 | 0x22 |
\0 | 空字符(字符串终止符) | 0x00 |
\a | 响铃 | 0x07 |
为什么需要 `\n`? 在终端中,输出是行缓冲的。如果没有 \n,输出可能会停留在缓冲区中,不会立即显示在屏幕上。加上 \n 后,缓冲区会被刷新,内容立即显示。
2.2 C程序的基本组成
通过 hello, world 程序,我们可以总结出C程序的基本组成:
C程序
├── 预处理指令(#include, #define 等)
├── 函数定义
│ ├── main 函数(必须,且只有一个)
│ └── 其他函数(可选)
├── 变量声明(全局变量)
└── 类型定义(typedef, struct 等)一个C程序可以由一个或多个源文件组成。每个源文件(.c 文件)独立编译成目标文件(.o 文件),然后由链接器将所有目标文件和库文件合并成最终的可执行文件。
2.2.1 语句与表达式
C程序的基本执行单位是语句(statement)。每条语句以分号 ; 结尾。printf("hello, world\n"); 就是一条函数调用语句。
表达式(expression) 是能产生值的代码片段。2 + 3 是一个表达式,值为 5;printf(...) 也是一个表达式,它的值是成功输出的字符数。
int x = 2 + 3; // 表达式 2+3 的值为5
int n = printf("hi"); // printf的返回值为输出的字符数,n=22.2.2 注释
C语言支持两种注释方式:
/* 这是C89风格的块注释
可以跨越多行 */
// 这是C99风格的行注释(K&R第二版基于C89,但现代编译器都支持)好的注释习惯:注释应该解释"为什么"而不是"做什么"。代码本身应该足够清晰地表达"做什么"。
/* 不好的注释 */
i++; // i加1
/* 好的注释 */
i++; // 跳过表头行,数据从第二行开始2.3 编译过程详解
当你输入以下命令时:
gcc hello.c -o hello编译器实际上执行了四个阶段的工作:
2.3.1 预处理(Preprocessing)
gcc -E hello.c -o hello.i预处理器处理所有以 # 开头的指令:
#include:将头文件内容插入源文件#define:展开宏定义#ifdef/#ifndef/#endif:条件编译
预处理后的 hello.i 是一个纯文本文件,包含了所有展开后的代码。你可以用 head 命令查看它的前几行,会发现文件比原始源文件大得多。
2.3.2 编译(Compilation)
gcc -S hello.c -o hello.s编译器将C源代码翻译成汇编语言。生成的 hello.s 文件是人类可读的文本,但已经是面向特定处理器架构的低级指令了。
.file "hello.c"
.section .rodata
.LC0:
.string "hello, world"
.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
leaq .LC0(%rip), %rdi
call printf@PLT
movl $0, %eax
popq %rbp
ret2.3.3 汇编(Assembly)
gcc -c hello.s -o hello.o汇编器将汇编语言翻译成机器指令,生成目标文件 hello.o。这是一个二进制文件,包含了 main 函数的机器编码,但 printf 函数的地址还没有解析——它需要链接器来完成。
2.3.4 链接(Linking)
gcc hello.o -o hello链接器将目标文件与标准库(libc)合并,解析所有外部符号引用(如 printf 的地址),生成最终的可执行文件 hello。
链接分为两种:
- 静态链接:将库代码直接复制到可执行文件中,文件体积大但自包含
- 动态链接:运行时从系统加载共享库(
.so文件),文件体积小但依赖系统环境
2.4 变量与算术
让我们看一个稍微复杂一点的程序:
#include <stdio.h>
int main() {
int fahr, celsius;
int lower, upper, step;
lower = 0;
upper = 300;
step = 20;
fahr = lower;
while (fahr <= upper) {
celsius = 5 * (fahr - 32) / 9;
printf("%d\t%d\n", fahr, celsius);
fahr = fahr + step;
}
return 0;
}这个程序打印华氏温度与摄氏温度的对照表。它引入了几个新概念:
2.4.1 变量声明
int fahr, celsius;
int lower, upper, step;在C语言中,所有变量必须先声明后使用。int 表示这些变量是整型(integer)。声明告诉编译器:这些名字代表什么类型的数据,需要分配多少内存空间。
C语言的基本数据类型包括:
| 类型 | 关键字 | 典型大小 | 取值范围 |
|---|---|---|---|
| 字符 | char | 1字节 | -128 ~ 127 |
| 整型 | int | 4字节 | -2^31 ~ 2^31-1 |
| 短整型 | short | 2字节 | -2^15 ~ 2^15-1 |
| 长整型 | long | 4/8字节 | 取决于平台 |
| 单精度浮点 | float | 4字节 | ~7位有效数字 |
| 双精度浮点 | double | 8字节 | ~15位有效数字 |
2.4.2 赋值语句
lower = 0;
upper = 300;赋值运算符 = 将右边的值赋给左边的变量。注意,C语言中的 = 是赋值,不是相等判断(相等判断用 ==)。这是C语言中最常见的错误来源之一。
2.4.3 while 循环
while (fahr <= upper) {
...
fahr = fahr + step;
}while 循环先判断条件,再执行循环体。只要条件为真(非零),循环就会继续执行。
2.4.4 printf 的格式化输出
printf("%d\t%d\n", fahr, celsius);printf 的格式字符串中,%d 是格式说明符,表示输出一个十进制整数。\t 是制表符。printf 会按照格式字符串中的说明符,依次用后面的参数替换。
常用的格式说明符:
| 说明符 | 含义 |
|---|---|
%d | 十进制整数 |
%f | 浮点数(小数形式) |
%e | 浮点数(科学计数法) |
%c | 单个字符 |
%s | 字符串 |
%x | 十六进制整数 |
%o | 八进制整数 |
%% | 百分号本身 |
2.5 浮点数版本的温度转换
上面的温度转换程序有一个问题:使用整数运算会导致精度损失。例如,华氏0度对应的摄氏温度应该是 -17.78°C,但整数运算给出的结果是 -17。
让我们用浮点数来改进:
#include <stdio.h>
int main() {
float fahr, celsius;
float lower, upper, step;
lower = 0;
upper = 300;
step = 20;
fahr = lower;
while (fahr <= upper) {
celsius = (5.0 / 9.0) * (fahr - 32.0);
printf("%3.0f %6.1f\n", fahr, celsius);
fahr = fahr + step;
}
return 0;
}注意几个变化:
- 变量类型从
int改为float - 常量
5.0 / 9.0使用浮点除法,而不是整数除法5 / 9(后者结果为0) - 格式说明符
%3.0f表示至少3个字符宽度、0位小数的浮点数
整数除法的陷阱:在C语言中,两个整数相除的结果仍然是整数(向零取整)。5 / 9 的结果是 0,而不是 0.555...。这是初学者最常犯的错误之一。要得到浮点数结果,至少有一个操作数必须是浮点数:5.0 / 9.0 或 (float)5 / 9。
2.6 for 语句
上面的 while 循环可以用更简洁的 for 语句来重写:
#include <stdio.h>
int main() {
int fahr;
for (fahr = 0; fahr <= 300; fahr += 20) {
printf("%3d %6.1f\n", fahr, 5.0 * (fahr - 32) / 9.0);
}
return 0;
}for 语句将循环的三个要素集中在了一起:
- 初始化:
fahr = 0 - 条件判断:
fahr <= 300 - 步进:
fahr += 20
这种写法更加紧凑,也更不容易出错(因为不容易忘记更新循环变量)。for 语句是C语言中最常用的循环结构,在后续章节中我们会看到更多复杂的用法。
2.7 符号常量
在上面的程序中,300、20 等数字直接出现在代码中,这被称为魔术数字(magic number)。更好的做法是使用符号常量:
#include <stdio.h>
#define LOWER 0
#define UPPER 300
#define STEP 20
int main() {
int fahr;
for (fahr = LOWER; fahr <= UPPER; fahr += STEP) {
printf("%3d %6.1f\n", fahr, 5.0 * (fahr - 32) / 9.0);
}
return 0;
}#define 是预处理指令,它在编译之前将所有的 LOWER 替换为 0,UPPER 替换为 300,STEP 替换为 20。这种简单的文本替换称为宏替换。
使用符号常量的好处:
- 可读性:
UPPER比300更有意义 - 可维护性:修改上限只需要改一处
- 一致性:避免同一个数字在代码中出现多次但含义不同
按照惯例,符号常量的名字使用大写字母。
2.8 字符输入/输出
C语言的标准库提供了一组简单的字符输入/输出函数:getchar() 和 putchar()。
#include <stdio.h>
int main() {
int c;
while ((c = getchar()) != EOF) {
putchar(c);
}
return 0;
}这个程序将输入原封不动地复制到输出——它是一个最简单的文件复制程序。
几个关键点:
c 的类型是 int 而不是 char。这是因为 getchar() 的返回值不仅仅是字符,还包括 EOF(End Of File,文件结束标志)。EOF 通常定义为 -1,它不是一个有效的字符值,所以 c 必须用 int 类型才能区分字符和 EOF。
while 条件中的括号是必须的。因为 != 的优先级高于 =,如果不加括号:
```c
while (c = getchar() != EOF) // 错误!等价于 while (c = (getchar() != EOF))
```
这会将比较结果(0或1)赋给 c,而不是将 getchar() 的返回值赋给 c。
EOF 是一个符号常量,定义在 <stdio.h> 中。在UNIX系统中,你可以通过 Ctrl+D 发送 EOF 信号。
2.9 字符数组与字符串
C语言没有内置的字符串类型。字符串是用字符数组来表示的:
#include <stdio.h>
int main() {
char message[] = "Hello, C!";
printf("%s\n", message);
int i = 0;
while (message[i] != '\0') {
printf("%c", message[i]);
i++;
}
printf("\n");
return 0;
}C语言中的字符串以空字符 `\0` 结尾。"Hello, C!" 在内存中实际存储为 'H', 'e', 'l', 'l', 'o', ',', ' ', 'C', '!', '\0'——共10个字节,最后一个 \0 是字符串终止标志。
这个看似简单的设计决定了C语言字符串处理的所有特性:
- 计算字符串长度需要遍历到
\0(strlen函数的时间复杂度为 O(n)) - 字符串拼接需要确保目标数组有足够的空间容纳
\0 - 忘记分配
\0的空间是缓冲区溢出的常见原因
三、实战案例
3.1 案例一:多功能计算器
#include <stdio.h>
int main() {
double num1, num2;
char op;
printf("Simple Calculator\n");
printf("Enter expression (e.g., 3.5 + 2.1): ");
scanf("%lf %c %lf", &num1, &op, &num2);
printf("Result: ");
switch (op) {
case '+':
printf("%.2f\n", num1 + num2);
break;
case '-':
printf("%.2f\n", num1 - num2);
break;
case '*':
printf("%.2f\n", num1 * num2);
break;
case '/':
if (num2 == 0) {
printf("Error: Division by zero\n");
} else {
printf("%.2f\n", num1 / num2);
}
break;
default:
printf("Error: Unknown operator '%c'\n", op);
break;
}
return 0;
}运行示例:
Simple Calculator
Enter expression (e.g., 3.5 + 2.1): 3.5 + 2.1
Result: 5.60代码解析:
scanf用于格式化输入,%lf表示读取double类型switch语句根据运算符选择不同的计算分支- 除法运算需要检查除数是否为零
break语句用于跳出switch,防止"穿透"到下一个分支
3.2 案例二:文本统计工具
#include <stdio.h>
int main() {
int c;
long num_chars = 0;
long num_words = 0;
long num_lines = 0;
int in_word = 0;
while ((c = getchar()) != EOF) {
num_chars++;
if (c == '\n') {
num_lines++;
}
if (c == ' ' || c == '\n' || c == '\t') {
in_word = 0;
} else if (!in_word) {
in_word = 1;
num_words++;
}
}
printf("Lines: %ld\n", num_lines);
printf("Words: %ld\n", num_words);
printf("Chars: %ld\n", num_chars);
return 0;
}运行示例(通过管道输入):
echo "Hello World\nThis is C" | ./wordcount
Lines: 2
Words: 4
Chars: 22代码解析:
- 使用状态机思想:
in_word变量记录当前是否在单词内部 - 遇到空白字符时切换到"单词外"状态
- 遇到非空白字符且当前在"单词外"状态时,发现了一个新单词
- 这与UNIX的
wc命令功能类似
3.3 案例三:带表头的温度转换表
#include <stdio.h>
#define LOWER 0
#define UPPER 300
#define STEP 20
int main() {
printf("============= Temperature Conversion =============\n");
printf("%5s | %10s | %10s\n", "Fahrenheit", "Celsius", "Kelvin");
printf("------+------------+----------\n");
for (int fahr = LOWER; fahr <= UPPER; fahr += STEP) {
float celsius = 5.0 * (fahr - 32) / 9.0;
float kelvin = celsius + 273.15;
printf("%5d | %10.2f | %10.2f\n", fahr, celsius, kelvin);
}
printf("==================================================\n");
return 0;
}运行结果:
============= Temperature Conversion =============
Fahrenheit | Celsius | Kelvin
------+------------+----------
0 | -17.78 | 255.37
20 | -6.67 | 266.48
40 | 4.44 | 277.59
60 | 15.56 | 288.71
80 | 26.67 | 299.82
100 | 37.78 | 310.93
...代码解析:
%5s表示至少5个字符宽度的字符串,右对齐%10.2f表示至少10个字符宽度、2位小数的浮点数- 使用
|和-字符绘制简单的表格边框 - 同时输出摄氏度和华氏度,增加实用性
四、本章小结
第1章通过经典的 hello, world 程序,展示了C语言的基本面貌:
程序结构:C程序由函数组成,每个程序必须有且只有一个 main 函数。函数由语句组成,语句以分号结尾。
预处理指令:#include 引入头文件声明,#define 定义符号常量。它们在编译之前由预处理器处理。
编译流程:源代码经过预处理、编译、汇编、链接四个阶段,最终生成可执行文件。
数据类型:C语言提供了 char、int、float、double 等基本数据类型,所有变量必须先声明后使用。
控制流:while、for 循环和 if-else 条件语句是程序逻辑控制的基础。
标准I/O:printf 和 scanf 提供格式化输入输出,getchar 和 putchar 提供字符级I/O。
C语言的设计哲学是简洁、高效、接近硬件。它的关键字很少(C89标准只有32个关键字),但通过组合这些基本元素,可以表达出任意复杂的逻辑。这种"小而强大"的设计,正是C语言历经半个世纪仍然充满生命力的原因。
下一章将深入讲解C语言的类型系统——这是编写正确、高效C程序的基础。
五、思考与练习
思考题
如果去掉 #include <stdio.h>,程序还能编译通过吗?为什么?
main 函数的返回值如果不写 return 0,而是直接省略,会发生什么?(提示:C89和C99的行为不同)
为什么 getchar() 的返回值要赋给 int 类型的变量,而不是 char 类型?
5 / 9 和 5.0 / 9.0 的结果有什么不同?为什么?
编程练习
修改温度转换程序:将温度转换表倒序输出(从300度到0度)。
实现一个简单的回显程序:读取用户输入的每一行,在前面加上行号后输出。
编写一个程序:统计输入中空格、制表符和换行符的个数。
编写一个程序:将输入中的大写字母转换为小写,小写字母转换为大写(提示:ASCII码的规律)。