第一章 C语言核心基础语法
本章要点
本章是 C 语言语法的初步概览,目的是让读者对 C 语言的核心语法建立一个整体轮廓认知,而不是要求一次性全部掌握。整章以猜数游戏这个完整程序为核心线索——从游戏所需的功能出发,逐一拆解和讲解其中涉及到的语法点:程序的基本骨架、注释、变量与数据类型、运算符、判断语句、循环语句、函数、头文件、标准输入输出,最后将所有知识点串联回猜数游戏。具体涵盖以下内容:
- C 语言程序的基本骨架:
main函数、花括号、分号以及预处理指令#include - 注释的写法与用途
- 变量与基本数据类型:
int、float、double、char - 双引号与字符串初识(为第四章做铺垫)
- 算术运算符、自增自减运算符、复合赋值运算符、逻辑运算符与比较运算符,以及运算符优先级
- 判断语句:
if、if-else、if-else if-else,以及等值多分支switch - 循环语句:
while、do-while、for,以及break与continue - 函数的定义、调用与声明
- 标准输入输出:
printf与scanf - 综合练习:编写一个完整的猜数游戏
这些内容是 C 语言的根基。后续所有章节——无论是数组、指针、结构体,还是动态内存分配与文件操作——都建立在它们之上。初学阶段看不懂某些概念完全正常,建议以最后的猜数游戏代码为锚点反复阅读,遇到不懂的语法就翻回对应小节查阅,多敲几遍自然就通了。
一、C语言程序的基本结构
1.1 C语言的源文件
在正式编写程序之前,有一个看似简单但必须首先明确的问题:编程这件事,到底是在“写什么“?
答案很朴素——写的是代码文件。编程的本质,就是按照编程语言的语法规则,将你的意图写成纯文本内容,保存为一个带有特定后缀的文件。这个文件称为源代码文件(Source Code File),简称源文件。编译器读取这个源文件,将其中的代码翻译为计算机能够执行的机器指令。
不同的编程语言使用不同的文件后缀:
- Python →
.py - Java →
.java - C 语言 →
.c
当你看到 hello.c、main.c、game.c 这样的文件名时,就可以立刻知道——它们都是 C 语言的源代码文件。
后缀 .c 有两个作用:给开发者看(一眼就知道这是什么语言的代码)和给工具看(VSCode 根据后缀启用语法高亮和代码提示,编译器根据后缀判断如何处理这个文件)。
因此,编写一个 C 程序的第一步就很清晰了:创建一个以 .c 为后缀的新文件,在文件中按照 C 语言的语法编写代码,然后交给编译器去处理。下面,我们就从创建一个名为 hello.c 的文件开始,完整地走一遍这条路。
1.2 第一个程序:Hello World
学习任何一门编程语言,几乎都从“Hello World“开始。这个传统并非偶然——它用最小的篇幅展示了程序从源代码到运行的完整路径,让学习者对“写程序“这件事建立一个直观的全局印象。
下面准备了你的第一个 C 程序,它的功能很简单:在终端上打印出 “Hello, World!” 这行文字。打开 VSCode,创建一个名为 hello.c 的文件,把以下代码原样输入进去:
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
对这段代码的简单讲解: 你现在看到的是一个完整的、可以运行的 C 程序。其中 printf 的作用是在屏幕上输出文字——你可以把它理解为 C 语言提供的“打印“功能,小括号里双引号内的文字就是它要输出的内容。至于 #include <stdio.h>、int main(void)、return 0 各是什么意思,下一节(1.3 程序骨架解析)会逐一拆解;printf 的更多用法会在第九节(标准输入与输出)中系统讲解。现在你只需要知道:照抄这段代码,它就能在屏幕上打印出一行字。
写好代码之后,我们还需要一步才能让它真正运行起来。回忆一下第零章的内容:计算机的“母语“是由 0 和 1 组成的机器指令,它并不认识 printf,也不认识 int 和 return。因此,我们需要一个“翻译“,把人类能读懂的 C 语言代码,转换成计算机能够执行的机器指令——这个翻译的过程叫作编译(Compile),负责做这件事的工具叫作编译器(Compiler)。
根据第零章的配置,我们使用的编译器是 Clang。打开终端,进入 hello.c 所在的目录,输入以下命令:
# Windows
clang hello.c -o hello.exe
# macOS / Linux
clang hello.c -o hello
这条命令的含义是:调用 Clang 编译器,编译 hello.c 这个源文件,将生成的可执行文件命名为 hello.exe(或 hello)。拆开来看:clang 是编译器的名字,hello.c 是要编译的源文件,-o 选项用来指定输出文件的名字(o 是 output 的缩写),后面紧跟的 hello.exe 就是你给可执行文件起的名字。
编译成功后,运行生成的可执行文件:
# Windows
hello.exe
# macOS / Linux
./hello
如果一切顺利,屏幕上会显示:
Hello, World!
这就是从源代码到程序运行的全过程——你写好了 .c 源文件,用 clang 命令把它编译成可执行文件,然后运行它。今后你写的每一个 C 程序,都要经过同样的“编写 → 编译 → 运行“三步流程。关于 clang 命令和 -o 选项的更多细节,可以翻到本章末尾的小贴士查阅。
1.3 程序骨架解析
上面这个只有六行的程序,包含了 C 语言程序的全部基本元素。让我们将其拆解开来看:
#include <stdio.h> // 预处理指令
int main(void) // 主函数头部:void 明确表示不接收参数
{ // 函数体的开始
printf("Hello, World!\n"); // 语句
return 0; // 语句
} // 函数体的结束
下面逐一解释这三个核心要点。
要点一:main 函数是程序的入口。 int main(void) { ... } 定义了一个名为 main 的主函数,其中 void 明确表示不接收参数。宿主环境中的普通 C 程序从 main 函数开始执行,直到 main 返回或程序以其他方式终止。
类比:一本书的目录指定了从哪里开始读——
main就是 C 程序的“起始页“。写在main外面的函数不会自动执行,只有被main(或其他函数)显式调用时才运行。
必须记住的规则:普通的 C 可执行程序必须有且只有一个 main 函数。没有它,链接器就找不到程序的入口。后续在编写库文件或嵌入式程序时会遇到例外,初学阶段先记住这条规则即可。
💡 提示:什么是“函数“?
如果你现在对“函数“这个词感到陌生,完全没关系。现阶段你只需要把
int main(void) { ... }这个结构照抄下来,知道“程序要写在这对花括号里面“就够了。函数的概念会在第七章(函数)中详细讲解,到那时你会彻底明白它是什么、怎么用。
要点二:花括号 { } 标记代码的范围。 在 C 语言中,花括号总是成对出现,用来标明一段代码的起止边界。main 后面的 { 表示“从这里开始是 main 函数的内容“,对应的 } 表示“main 函数的内容到此结束“。可以把一对花括号视为一个包裹——左花括号打开包裹,中间装入需要执行的语句,右花括号封口。
要点三:分号 ; 是语句的结束标志。 C 语言中,绝大多数语句必须以分号结尾。分号的作用是告诉编译器:“这句话到此结束”。上面的程序中,printf(...) 和 return 0 都以分号结尾。如果遗漏分号,编译器会报错,程序无法通过编译。
💡 提示:为什么有些行不需要分号?
你可能已经发现了:
#include <stdio.h>没分号,int main(void)没分号,{和}也没分号——但printf(...)和return 0却有。现阶段不用深究原因,记住两条硬规矩就行:(1) 以#开头的行,一律不加分号;(2) 函数定义的头部(比如int main(void)),不加分号,函数体紧跟其后。背后的原理——什么是预处理指令、什么是函数定义——在学习了第七章(函数)和第八章(#include 与头文件)之后自然就懂了。
1.4 基本框架与规则
C 语言最基本的骨架如下:
int main(void) {
return 0;
}
短短三行代码,包含了 C 程序最核心的五条规则:
- 普通的 C 可执行程序必须有且只有一个
main函数——int main(void)就是程序的入口 - 程序从
main函数的第一条语句开始执行——这里是return 0; - 每条语句以分号
;结尾——return 0;末尾的分号 - 花括号
{ }必须成对出现,用于包裹函数体——main的函数体就在这对花括号里 int main(void)是函数定义头部,后面紧跟函数体,不加分号
1.5 动手练习
请创建一个名为 first.c 的文件,编写以下程序并运行:
#include <stdio.h>
int main(void)
{
printf("这是我的第一个C程序\n");
printf("我可以写出多行文字\n");
return 0;
}
编译运行:
# Windows
clang first.c -o first.exe
first.exe
# macOS / Linux
clang first.c -o first
./first
预期输出:
这是我的第一个C程序
我可以写出多行文字
请尝试以下修改:
- 将其中一个
\n去掉,观察输出有什么变化 - 将双引号中的文字改成任意内容
- 再添加一行
printf,输出你的名字
二、注释
2.1 什么是注释
注释是写在代码中的说明文字。它不会被编译,也不会对程序的运行产生任何影响。编译器在处理源代码时,会完全忽略注释的内容。
注释的唯一读者是人——包括写代码的你自己,以及将来可能阅读这份代码的其他开发者。在课本空白处写笔记,是为了帮助自己理解课本内容;在代码中写注释,是为了帮助人理解代码的意图和逻辑。
2.2 为什么需要注释
编程中的一个常见困境是:你花了一下午写了一段复杂的逻辑,三个月后再打开这个文件,却发现自己已经读不懂当初写的是什么了。注释正是为了解决这个问题而存在的。它的主要用途:
- 解释代码的作用:说明这一段代码是干什么的
- 记录复杂逻辑的思路:为什么这样写,当时的考虑是什么
- 调试时临时禁用代码:不删除,只注释掉,方便随时恢复
养成写注释的习惯,是成为合格程序员的第一步。这并非夸张——在一个团队协作的项目中,没有注释的代码会大幅增加他人的理解成本,也容易在不经意间引入 bug。
2.3 C语言中的两种注释
C 语言提供了两种注释方式。
单行注释以 // 开头,从 // 开始到该行结束的所有内容都是注释:
// 这是单行注释,只对这一行有效
printf("Hello\n"); // 也可以写在代码后面
多行注释以 /* 开头,以 */ 结尾,中间的所有内容(可以跨越多行)都是注释:
/* 这是多行注释的开始
这一行也是注释
这一行还是注释
这是多行注释的结束 */
需要特别注意一点:多行注释不能嵌套。也就是说,在一个多行注释的内部不能再写另一个多行注释。编译器遇到第一个 */ 就会认为注释结束了,后面的内容会被当作普通代码处理,这通常会导致编译错误。
2.4 动手练习
#include <stdio.h>
int main(void)
{
// 下面这行代码在屏幕上输出 Hello
printf("Hello\n");
/*
* 作者:我自己
* 日期:2026年
* 功能:练习使用注释
*/
printf("World\n"); // 输出 World
return 0;
}
请尝试:
- 在
printf("Hello\n");前面加上//,把它变成注释,观察输出发生了什么变化 - 用多行注释把
printf("World\n");整行注释掉,观察输出变化 - 在代码中添加你自己的注释,说明每一行的作用
三、变量与数据类型
3.1 变量的概念
在数学中,我们常用字母表示数——设 x = 5,那么 x + 3 = 8。这里的 x 就是一个变量,它的值可以随赋值而改变。
在 C 语言中,变量的含义与此类似,但更加具体:变量是内存中的一块命名存储空间,用于存放数据。你可以给这块空间起一个名字(比如 age),把数据放进去(比如 18),之后就可以通过这个名字来读取或修改里面的数据。
💡 提示:什么是“内存“?
如果你对“内存“这个概念感到陌生,可以先这样理解:内存就像电脑运行时的一张巨大的“草稿纸“,程序运行过程中需要用到的所有数据都临时记在这张纸上。变量就是在这张草稿纸上划定一个小格子,给它起个名字,往里面填数据。至于内存的物理本质、地址、以及它和硬盘的区别,会在后续学习第七章(指针)和动态内存分配时逐步深入。目前只需要记住:变量 = 内存中的一块有名字的存储空间,用类型来指定这块空间的大小。
3.2 变量的声明与初始化
在 C 语言中,使用变量之前必须先声明它,即告诉编译器“我需要一块存储空间,它的名字叫 xxx,用来存放 xxx 类型的数据“。声明的基本格式为:
类型 变量名;
例如:
int age; // 声明一个名为 age 的整数变量
也可以在声明的同时给它一个初始值,称为初始化:
int age = 18; // 声明变量 age,同时初始化为 18
如果声明变量时没有初始化,变量的值是不确定的——它取决于那块内存中此前存了什么数据。在后续章节学习指针和内存模型之后,你会对这个机制有更透彻的理解。
3.3 基本数据类型
不同类型的数据需要分配不同大小的存储空间。C 语言提供了以下四种基本数据类型,先通过一张表快速浏览:
| 类型名 | 含义 | 存储内容 | 占用空间 | 示例 |
|---|---|---|---|---|
int | 整数型 | 整数(正负整数和零) | 通常 4 字节 | 5, -3, 0, 100 |
float | 单精度浮点型 | 小数(有效数字约6-7位) | 通常 4 字节 | 3.14, -0.5, 2.0 |
double | 双精度浮点型 | 小数(有效数字约15-16位) | 通常 8 字节 | 3.1415926535 |
char | 字符型 | 单个字符 | 1 字节 | 'A', 'b', '#', '7' |
除 char 在 C 标准中固定为 1 字节外,其他类型的实际大小会受平台和编译器的影响。在多数现代桌面环境中,int 为 4 字节、double 为 8 字节。如果需要确认当前环境下的真实大小,可以使用 sizeof 运算符。
下面逐一认识这四种类型。
3.3.1 int 类型——整数
int(integer 的缩写)是 C 语言中最常用的数据类型,专门用来存储整数——也就是没有小数点的数字,包括正整数、负整数和零。
int age = 18; // 年龄——正整数
int count = 0; // 计数——零也是整数
int temperature = -5; // 温度——负整数
在大多数现代桌面环境中,一个 int 占用 4 个字节(32 位),能表示的整数范围大约是 -21 亿到 21 亿(精确值是 -2,147,483,648 到 2,147,483,647)。对于日常的计数、年龄、序号等场景,这个范围绰绰有余。
使用 int 时需要特别注意整数除法的行为:两个 int 相除,结果仍然是 int,小数部分会被直接丢弃(不是四舍五入):
int a = 7 / 3; // 结果是 2,不是 2.333...
int b = 5 / 2; // 结果是 2,不是 2.5
这个行为在初学时经常让人困惑。如果需要得到小数结果,需要把至少一个操作数写成浮点数(如 7.0 / 3),这个细节在第四节 运算符中会详细说明。
3.3.2 float 类型——单精度浮点数
float(floating-point number 的缩写)用于存储小数——也就是带有小数点的数字。
float height = 1.75; // 身高(米)
float weight = 65.5; // 体重(公斤)
float score = 98.5; // 成绩
一个 float 占用 4 个字节,有效数字大约是 6 到 7 位。这意味着 float 适合日常场景(身高、体重、成绩),但不适合需要高精度的计算——比如你把 1.23456789 存进 float,它可能只能精确记住前 6~7 位,后面的数字会产生微小误差。这是浮点数本身的特性,不是 C 语言的 bug,任何编程语言的浮点数都有这个问题。
3.3.3 double 类型——双精度浮点数
double(double-precision floating-point number 的缩写)和 float 一样用于存储小数,但精度翻倍——占用 8 个字节,有效数字约 15 到 16 位。
double pi = 3.141592653589793;
double distance = 1.496e11; // 科学记数法:1.496 × 10¹¹ 米(地球到太阳的距离)
选择 float 还是 double 的简单判断:日常场景(身高体重)用 float 就够;科学计算、需要高精度的场景用 double。如果不确定用哪个,直接用 double 是最稳妥的选择。在 printf / scanf 中,两者输出时都用 %f,不会增加额外的学习负担。
3.3.4 char 类型——字符
char(character 的缩写)用于存储单个字符,固定占用 1 个字节。
char grade = 'A'; // 成绩等级
char symbol = '#'; // 符号
char digit = '7'; // 字符 '7'
字符必须用单引号包裹。这里有一个初学者极其容易混淆的地方:数字 7 和字符 '7' 是完全不同的东西。
7是整数,可以参与加减乘除(7 + 1 = 8)'7'是一个字符符号,它的本质是一个编码值——在 ASCII 编码中,'7'对应的编码值是 55
可以这样理解:7 是数学意义上“七“这个数量,而 '7' 是写在纸上的“7“这个符号——就像键盘上印着的那个数字键。关于字符编码的更多细节,会在后续学习 ASCII 编码时进一步展开。
💡 提示:C 语言的布尔类型
很多语言(比如 Python、Java)都有专门的布尔类型(
bool)来表示true/false。C 语言的处理方式则有所不同:C 语言内置了“真“和“假“的概念——0 表示假,非 0 表示真(前面比较运算符中,5 == 5的结果是1即真,5 == 3的结果是0即假,就是这个道理)。但是,C 语言没有内置bool这个类型名来专门存放真假值。从 C99 标准开始,C 语言新增了
_Bool类型(注意前面有个下划线)。而我们平时代码里写的bool、true、false,都来自<stdbool.h>这个头文件——它用宏做了三件事:把bool定义为_Bool的别名,把true定义为1,把false定义为0。所以在实际开发中,如果你想用更直观的
bool/true/false写法,只需在文件开头加上:#include <stdbool.h>之后就可以放心使用了。它们本质上还是整数(
true= 1,false= 0),但代码读起来清晰很多。
3.3.5 双引号与字符串初识
在 printf("Hello, World!\n") 这行代码中,"Hello, World!\n" 用双引号包裹了一串文字。那么双引号到底是什么意思?它和单引号有什么区别?
简单来说:
- 单引号
' '包裹的是一个字符,类型是char,占 1 个字节。例如'A'、'7'。 - 双引号
" "包裹的是一串字符,称为字符串。例如"Hello"、"你好"。
当你写下 "Hello" 时,C 语言会在内存中依次存放字符 H、e、l、l、o,并在最后自动追加一个隐藏的结束标志 '\0'(空字符),用来标记“字符串到这里结束“。因此 "Hello" 实际上占用了 6 个字节——5 个可见字符 + 1 个看不见的 '\0'。
💡 提示:先有个印象即可
这里只需要记住两件事:(1) 单引号是字符,双引号是字符串;(2) 双引号创建的字符串末尾会自动带一个
'\0'。关于字符串的声明、输入输出、标准库函数以及'\0'的关键作用,将在第四章 字符串中专门展开。目前你在printf中直接写"Hello"这样的双引号文字就够了,暂时不用深究。
单引号 vs 双引号速查:
单引号 ' ' | 双引号 " " | |
|---|---|---|
| 包含内容 | 单个字符 | 一串字符 |
| 类型 | char | 字符串(字符数组) |
| 示例 | 'A'、'#'、'7' | "Hello"、"C语言" |
| 占用空间 | 1 字节 | 可见字符数 + 1(末尾有 '\0') |
能否赋给 char 变量 | 可以:char c = 'A'; | 不可以:char c = "A"; 会报错 |
3.4 变量的命名规则
C 语言对变量命名有三条硬性规定:
- 只能使用英文字母(a-z, A-Z)、数字(0-9)和下划线(
_) - 不能以数字开头(
2nd不合法,second2合法) - 不能与 C 语言的关键字重名(如
int、return、if等) - 区分大小写:
Age和age是两个不同的变量
💡 提示:什么是 C 语言的关键字?
关键字(Keywords)是 C 语言预先保留的一批单词,每个关键字在语言中有固定的含义和用途——比如
int表示整数类型,if表示条件判断,return表示函数返回。这些单词已经被语言本身“征用“了,你不能把它们拿来做变量名或函数名,否则编译器会分不清你到底是想使用语言内置的功能,还是定义了一个新东西,从而导致编译错误。截止 C11 标准,C 语言共有 44 个关键字(部分编译器可能有少量扩展)。以下是完整列表,现阶段不需要全部记住,大致浏览一遍有个印象即可:
类别 关键字 数据类型相关 char,short,int,long,float,double,void,signed,unsigned,_Bool,_Complex,_Imaginary存储类型相关 auto,register,static,extern,typedef,_Thread_local结构相关 struct,union,enum,const,volatile,restrict,_Atomic控制流程 if,else,switch,case,default,for,while,do,break,continue,goto,return其他 sizeof,inline,_Alignas,_Alignof,_Noreturn,_Static_assert,_Generic其中以
_开头后跟大写字母的(如_Bool、_Thread_local)是 C11 新增的底层关键字,初学阶段接触不到,不用在意。日常编程中只要记住:变量名不要取int、float、if、for、return这类常见单词即可。
除了硬性规定,还有两条值得从一开始就遵守的习惯:
- 使用有意义的英文单词命名:
age(年龄)、score(分数)、student_count(学生数量) - 多个单词用下划线连接:
math_score、total_price - 避免使用
a、b、c、x这类无意义的名字,除非在极短的示例中
3.5 动手练习
#include <stdio.h>
int main(void)
{
// 声明整数变量
int age = 18;
int year = 2026;
// 声明小数变量
float height = 1.75;
double pi = 3.1415926;
// 声明字符变量
char grade = 'A';
// 使用 printf 输出变量的值
// %d 用于输出整数, %f 用于输出小数, %c 用于输出字符
printf("年龄:%d\n", age);
printf("年份:%d\n", year);
printf("身高:%f 米\n", height);
printf("圆周率:%f\n", pi);
printf("成绩等级:%c\n", grade);
return 0;
}
编译运行后输出:
年龄:18
年份:2026
身高:1.750000 米
圆周率:3.141593
成绩等级:A
printf中的%d、%f、%c称为格式占位符,作用是告诉printf“在这个位置插入对应变量的值”。本章第九节将系统讲解这些占位符的用法。目前只需记住:%d对应整数,%f对应小数,%c对应字符。注意:在printf中,float会自动提升为double,二者都用%f输出;scanf读取double时才使用%lf。
请尝试:
- 把
age的值改成你自己的年龄 - 添加一个
float类型的变量weight(体重),并输出它 - 声明一个变量但不赋初始值,直接输出它,观察会发生什么(这是未定义行为,可能输出一个随机的数值)
四、运算符
4.1 算术运算符
C 语言提供了五种基本算术运算符,与数学中的运算一一对应:
| 运算符 | 名称 | 示例 | 结果 | 说明 |
|---|---|---|---|---|
+ | 加法 | 5 + 3 | 8 | 两数相加 |
- | 减法 | 5 - 3 | 2 | 两数相减 |
* | 乘法 | 5 * 3 | 15 | 两数相乘(注意不是数学中的 ×) |
/ | 除法 | 6 / 3 | 2 | 两数相除(注意不是数学中的 ÷) |
% | 取余 | 7 % 3 | 1 | 求整数除法的余数 |
其中取余运算符 % 仅对整数有效。a % b 的结果是 a 除以 b 的余数——例如 7 % 3 = 1,因为 7 除以 3 商 2 余 1。
整数除法的注意事项。 在 C 语言中,两个整数相除的结果仍然是整数,小数部分会被直接丢弃(截断),而非四舍五入:
int a = 7 / 3; // a 的值是 2,不是 2.333...
int b = 5 / 2; // b 的值是 2,不是 2.5
如果要得到小数结果,需要将至少一个操作数写为浮点数形式:
float c = 7.0 / 3; // c 的值约等于 2.333
float d = 5.0 / 2; // d 的值是 2.5
这个行为的根源在于,编译器会根据操作数的类型来决定使用整数除法还是浮点除法。当两个操作数都是整数时,执行的是整数除法;只要其中一个是浮点数,编译器就会将另一个也转为浮点数,然后执行浮点除法。
4.2 比较运算符
比较运算符用于判断两个值之间的大小关系。比较的结果是一个逻辑值——在 C 语言中,真用整数 1 表示,假用整数 0 表示。
| 运算符 | 名称 | 示例 | 结果 | 说明 |
|---|---|---|---|---|
== | 等于 | 5 == 5 | 1(真) | 判断两边是否相等 |
!= | 不等于 | 5 != 3 | 1(真) | 判断两边是否不相等 |
> | 大于 | 5 > 3 | 1(真) | 左边是否大于右边 |
< | 小于 | 5 < 3 | 0(假) | 左边是否小于右边 |
>= | 大于等于 | 5 >= 5 | 1(真) | 左边是否大于或等于右边 |
<= | 小于等于 | 5 <= 3 | 0(假) | 左边是否小于或等于右边 |
初学者最容易犯的一个错误是:判断“等于“使用两个等号 ==,而一个等号 = 是赋值操作。如果在 if 条件中误写成 if (x = 5),编译器不会报错(语法上是合法的),但含义变成了“把 5 赋给 x,然后判断 x 是否为 0“,这与“判断 x 是否等于 5“截然不同。
4.3 自增与自减运算符
C 语言提供了两个极其常用的单目运算符:++(自增)和 --(自减)。它们的作用分别是对变量的值加 1 或减 1,常用于循环计数和指针移动等场景。
基本用法
int a = 5;
a++; // a 变为 6(等价于 a = a + 1)
a--; // a 变回 5(等价于 a = a - 1)
这两种写法的效果看起来和 a = a + 1 完全一样,初学者自然会问:为什么要专门引入 ++ 和 -- 运算符?直接写 a = a + 1 不行吗?
答案是:++ 和 -- 不仅仅是“短一点“的写法,它们有一个 a = a + 1 做不到的特性——可以作为表达式的一部分使用,并且根据写在变量前还是变量后,产生不同的求值顺序。
前缀与后缀的区别——这是重点
++ 和 -- 都可以写在变量的前面(前缀)或后面(后缀),两种写法对变量自身的最终效果相同(加 1 或减 1),但表达式的求值结果不同:
| 写法 | 名称 | 规则 | 等价逻辑 |
|---|---|---|---|
++i | 前缀自增 | 先加 1,再使用 i 的值 | i = i + 1; 使用 i; |
i++ | 后缀自增 | 先使用 i 的值,再加 1 | 使用 i; i = i + 1; |
--i | 前缀自减 | 先减 1,再使用 i 的值 | i = i - 1; 使用 i; |
i-- | 后缀自减 | 先使用 i 的值,再减 1 | 使用 i; i = i - 1; |
“使用“是什么意思?就是取这个表达式的值,把它赋给别人、拿去运算、或作为函数参数传递。
看一个具体例子就清楚了:
#include <stdio.h>
int main(void)
{
int a = 5, b = 5;
int x = ++a; // 前缀:a 先加 1 变成 6,然后把 6 赋给 x
int y = b++; // 后缀:先把 b 的当前值 5 赋给 y,然后 b 再加 1 变成 6
printf("a = %d, x = %d\n", a, x); // a = 6, x = 6
printf("b = %d, y = %d\n", b, y); // b = 6, y = 5
return 0;
}
++a 和 b++ 都让变量加到了 6,但 ++a 表达式的值是加完之后的 6,b++ 表达式的值是加之前的 5。这就是前缀和后缀唯一的区别——表达式的值不同,变量本身最终一样。
单独使用时没有区别
如果你只是想让变量加 1,不把结果赋给别人,也不参与别的运算,那么 i++、++i、i = i + 1 三者完全等价:
for (int i = 0; i < 5; i++) // 用 i++,常见写法
for (int i = 0; i < 5; ++i) // 用 ++i,效果完全相同
这里 i++ 和 ++i 没有区别,因为 for 的“更新“部分只看变量最终变了没有,不关心表达式的值。所以挑你喜欢的写法即可——i++ 更常见,++i 在 C++ 中某些场景效率略高(C 语言中内置类型无所谓)。
初学阶段最常见的误解
int i = 3;
printf("%d\n", i++); // 输出 3(先用后加)
printf("%d\n", i); // 输出 4(i 已经加过了)
printf("%d\n", ++i); // 输出 5(先加后用)
如果你预期 i++ 会输出 4,说明还没掌握“先使用、后加 1“这条规则。下次看到 i++,在心里默念:先用后加。
⚠️ 警告:不要在同一个表达式里对同一个变量多次使用 ++/–
int i = 5; int x = i++ + ++i; // 未定义行为!结果不可预测这种“炫技“写法在任何正经代码里都不应该出现。C 标准没有规定这种表达式的求值顺序,不同编译器可能算出不同结果。一条语句里只用一次自增/自减,让它干干净净。
4.4 运算符优先级
现在你已经学过了算术运算符、比较运算符、自增自减运算符,再加上之前接触过的赋值运算符 =,种类已经不少了。当多个运算符同时出现在一个表达式中时,哪个先算、哪个后算?这就需要运算符优先级来回答。
优先级类似于小学数学中的“先乘除、后加减“——3 + 4 × 5 等于 23 而不是 35,因为乘法优先级高于加法。C 语言继承了这套直觉,并扩展到了更多运算符上。
常用运算符优先级表(从高到低)
下表列出了初学阶段会遇到的运算符,优先级从高到低排列。越往上越先算,同一行的优先级相同:
| 优先级 | 运算符 | 名称 | 结合方向 |
|---|---|---|---|
| 1(最高) | () | 圆括号 | 从左到右 |
| 2 | ++ --(后缀) | 后缀自增、自减 | 从左到右 |
| 3 | ++ --(前缀)、+ -(单目) | 前缀自增自减、正负号 | 从右到左 |
| 4 | * / % | 乘、除、取余 | 从左到右 |
| 5 | + - | 加、减 | 从左到右 |
| 6 | < <= > >= | 比较运算符 | 从左到右 |
| 7 | == != | 等于、不等于 | 从左到右 |
| 8 | = | 赋值 | 从右到左 |
几点关键直觉:
- 圆括号无敌。 优先级最高,任何表达式只要用
()包起来,里面的内容一定先算。不确定优先级时,加括号是最稳妥的做法——既不会出错,又能让代码意图一目了然。 - 乘除优先于加减。 和数学完全一致。
- 比较运算符在算术之后。
a + b > c等价于(a + b) > c,而不是a + (b > c)。这符合直觉:先算出结果,再做比较。 - 赋值几乎最晚。
=的优先级很低,保证等号右边的整个表达式先算完,再把结果赋给左边。 - 单目运算符(正负号、前缀自增自减)优先级高于乘除。
-a * b等价于(-a) * b。后缀自增自减的优先级比前缀还高,但初学阶段用括号规避即可。
结合方向是什么?
当优先级相同时,结合方向决定从左往右算还是从右往左算:
// 算术运算符是"从左到右"
10 - 3 - 2; // (10 - 3) - 2 = 5,而不是 10 - (3 - 2) = 9
// 赋值运算符是"从右到左"
int a, b, c;
a = b = c = 0; // 等价于 a = (b = (c = 0)),三个变量全部赋为 0
实用建议:不需要背,用括号
运算符优先级表不需要死记硬背。初学阶段记住两条规则就够用:
- 不确定谁先谁后,就加括号。
(a + b) * c比a + b * c更不会让人想岔。 - 赋值右边先算完。
x = a + b * c不需要写x = (a + b * c),因为=优先级最低。
4.5 其他常用运算符速览
除了上面介绍的几类运算符,C 语言还有几个在初学阶段就会频繁遇到的运算符。这里快速过一遍,建立印象即可,后续章节用到时会反复见到它们。
复合赋值运算符
在 C 语言中,a = a + 3 这种“先计算、再赋回给自己“的模式非常常见,C 语言提供了更简洁的复合写法:
a += 3; // 等价于 a = a + 3
b -= 5; // 等价于 b = b - 5
c *= 2; // 等价于 c = c * 2
d /= 4; // 等价于 d = d / 4
e %= 3; // 等价于 e = e % 3
规则很简单:x op= y 就是 x = x op y。op 可以是 +、-、*、/、% 等。这种写法不仅更短,而且在某些场景下更清晰地表达了“在原有值上累加/累减“的语义。循环中最常见的 i += 1,还有更短的 i++(见第 4.3 节)。
逻辑运算符
当你需要组合多个判断条件时——比如“年龄在 18 到 60 之间“、或者“不是周末“——就需要用到逻辑运算符:
| 运算符 | 名称 | 含义 | 示例 |
|---|---|---|---|
&& | 逻辑与 | 两边都为真,结果才为真 | age >= 18 && age <= 60 |
| ` | ` | 逻辑或 | |
! | 逻辑非 | 取反:真变假,假变真 | !(score < 60) 等价于 score >= 60 |
一个典型的实际例子:
// 判断一个年份是不是闰年
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
{
printf("%d 是闰年\n", year);
}
逻辑运算符也遵从我们在第 4.4 节讲过的优先级规则:! > && > ||。记不住也没关系,用括号把每个条件包起来就一目了然。
💡 提示:短路求值
C 语言的
&&和||有一个高效的设计叫短路求值。对于a && b,如果a已经为假,b根本不会被计算——因为不管b是什么,整个表达式都一定是假。同理,对于a || b,如果a已经为真,b也不会被计算。这个特性可以用来写出简洁安全的代码,例如:if (p != NULL && *p == 5)——如果p是NULL,第二个条件就不会执行,避免了通过空指针解引用导致的程序崩溃。
条件运算符(三目运算符)? :
这是 C 语言中唯一一个需要三个操作数的运算符,可以看作 if-else 的表达式版本:
int max = (a > b) ? a : b; // 如果 a > b,取 a;否则取 b
条件 ? 值1 : 值2 ——条件为真时整个表达式的结果是值1,条件为假时结果是值2。适合在需要根据条件取不同值的场景,比写一个 if-else 更紧凑。
位运算符(初步了解)
C 语言还提供了对二进制位直接操作的运算符:&(按位与)、|(按位或)、^(按位异或)、~(按位取反)、<<(左移位)、>>(右移位)。它们在底层编程、嵌入式系统和性能优化中有重要用途。初学阶段只需知道它们的存在即可,第七章学习指针时会结合具体场景补充。
💡 重要区分:
&vs&&,|vs||这是初学者非常容易混淆的两对符号:
&&/||是逻辑运算符,用于组合判断条件(真/假),有短路求值&/|是位运算符,用于操作二进制位,两个操作数都会完整求值举个例子:
if (x > 0 && y > 0)是“x 和 y 都大于 0“——用&&。而z = x & y是把 x 和 y 的每一个二进制位做与运算——用&。目前不需要记住位运算的细节,只需注意:写条件判断时别忘了打两个符号——&&和||,不是&和|。
4.6 动手练习
#include <stdio.h>
int main(void)
{
int a = 10;
int b = 3;
// 算术运算
printf("a + b = %d\n", a + b); // 13
printf("a - b = %d\n", a - b); // 7
printf("a * b = %d\n", a * b); // 30
printf("a / b = %d\n", a / b); // 3(整数除法,丢弃小数)
printf("a %% b = %d\n", a % b); // 1(10除以3余1)
printf("\n");
// 比较运算
printf("a == b: %d\n", a == b); // 0(假)
printf("a != b: %d\n", a != b); // 1(真)
printf("a > b: %d\n", a > b); // 1(真)
printf("a <= b: %d\n", a <= b); // 0(假)
return 0;
}
注意 printf("a %% b = %d\n", a % b); 中的 %%——因为 % 在 printf 的格式字符串中有特殊含义,要输出一个百分号本身,需要写成 %%。
请尝试:
- 修改
a和b的值,观察计算结果的变化 - 试试浮点除法:
float c = 10.0 / 3; printf("%f\n", c); - 用
==比较两个相等的数,验证结果是否为1
五、判断语句
前几节所写的程序都是从上到下顺序执行的,每一行都会被执行,没有例外。然而在实际问题中,程序往往需要根据不同的条件做出不同的响应:如果考试成绩大于等于 60 分,显示“及格“;否则显示“不及格“。如果用户猜的数字比答案大,提示“太大了“;比答案小,提示“太小了“。这种“如果…就…否则…“的逻辑,在 C 语言中由判断语句来实现。
5.1 if 语句
if 语句的含义是:如果某个条件成立,就执行一段指定的代码。其基本结构为:
if (条件)
{
条件成立时要执行的语句;
}
一个简单的例子:
#include <stdio.h>
int main(void)
{
int score = 85;
if (score >= 60)
{
printf("及格了。\n");
}
return 0;
}
程序执行到这里时,会先计算 score >= 60 这个条件。85 >= 60 为真,因此执行花括号中的 printf。如果 score 的值为 50,条件不成立,花括号中的代码就会被跳过。
5.2 if-else 语句
只判断“如果“是不够的——更多时候我们需要同时处理“条件成立“和“条件不成立“两种情况。if-else 语句满足了这一需求:
if (条件)
{
条件成立时执行的语句;
}
else
{
条件不成立时执行的语句;
}
示例:
#include <stdio.h>
int main(void)
{
int score = 55;
if (score >= 60)
{
printf("及格了。\n");
}
else
{
printf("不及格,需要补考。\n");
}
return 0;
}
程序的逻辑流程可以用下图表示:
开始
|
score >= 60 ?
/ \
是 否
| |
输出"及格" 输出"不及格"
| |
结束
关键在于:两个分支中有且仅有一个会被执行。条件为真走 if 分支,条件为假走 else 分支,不存在两条都走或都不走的情况。
5.3 多分支判断:if-else if-else
当需要判断的条件超过两个时,可以使用 if-else if-else 结构。其基本结构如下:
if (条件1)
{
条件1成立时执行的语句;
}
else if (条件2)
{
条件2成立时执行的语句;
}
else if (条件3)
{
条件3成立时执行的语句;
}
else
{
以上条件都不成立时执行的语句;
}
注意:else if 可以有任意多个,末尾的 else 是可选的——如果省略,当所有条件都不满足时,什么也不会执行。
下面是一个根据考试成绩评定等级的示例:
#include <stdio.h>
int main(void)
{
int score = 85;
if (score >= 90)
{
printf("等级:优秀\n");
}
else if (score >= 80)
{
printf("等级:良好\n");
}
else if (score >= 60)
{
printf("等级:及格\n");
}
else
{
printf("等级:不及格\n");
}
return 0;
}
执行过程是自上而下的:程序先检查第一个条件 score >= 90,不满足则检查第二个条件 score >= 80,依此类推。一旦某个条件成立,它对应的分支被执行,后续所有条件都会被跳过。在上面的例子中,score 为 85,第一个条件不满足,第二个条件 score >= 80 成立,于是程序输出“等级:良好“;第三个条件 score >= 60 虽然也成立,但不会再被检查。这种“匹配即退出“的机制,是多分支判断的核心规则。
else if 的数量没有限制,可以根据需要添加任意多个。末尾的 else 是可选的——如果省略,那么当所有条件都不满足时,什么也不会执行。
5.4 嵌套判断
在一个 if 语句的内部可以再写 if 语句,称为嵌套判断:
#include <stdio.h>
int main(void)
{
int score = 75;
if (score >= 60)
{
printf("及格了\n");
if (score >= 90)
{
printf("而且非常优秀!\n");
}
}
else
{
printf("不及格\n");
}
return 0;
}
嵌套判断本身没有新的语法规则,但需要注意花括号的配对——内层的 if 必须完整地包含在外层 if 的某个分支内部。缩进是保持嵌套结构可读性的关键手段。
5.5 switch 语句——等值多分支
if-else if-else 适合处理范围判断(大于、小于、区间),但有一种常见场景它处理起来比较啰嗦:对同一个变量的不同取值分别执行不同的操作。比如根据用户输入的编号选择功能:
if (choice == 1)
{
printf("新建文件\n");
}
else if (choice == 2)
{
printf("打开文件\n");
}
else if (choice == 3)
{
printf("保存文件\n");
}
else if (choice == 4)
{
printf("退出\n");
}
else
{
printf("无效选项\n");
}
choice == 某值 这个模式重复了四遍,眼花缭乱。C 语言为此提供了 switch 语句,专门处理“一个变量等于不同值时走不同分支“的场景。其基本结构为:
switch (表达式)
{
case 常量1:
匹配常量1时执行的语句;
break;
case 常量2:
匹配常量2时执行的语句;
break;
case 常量3:
匹配常量3时执行的语句;
break;
default:
以上常量都不匹配时执行的语句;
}
💡 提示:注意
case后面没有小括号。 这是初学switch时很容易写错的地方——if的条件要写在()里(if (x == 1)),但case后面直接跟常量(case 1:),不需要也不能加括号。写成case (1):虽然在某些编译器下可能碰巧通过,但这是非标准的写法,不要这样做。
上面的菜单选择用 switch 改写如下:
switch (choice)
{
case 1:
printf("新建文件\n");
break;
case 2:
printf("打开文件\n");
break;
case 3:
printf("保存文件\n");
break;
case 4:
printf("退出\n");
break;
default:
printf("无效选项\n");
}
switch 的执行流程:先计算括号内表达式的值,然后从上到下依次与每个 case 后面的常量比较。匹配到哪个 case,就从那里开始执行,直到遇到 break 或者 switch 结束的花括号 }。末尾的 default 是可选的——当所有 case 都不匹配时执行 default 分支,作用类似 if-else 链末尾的 else。
break 是关键。 如果漏写了 break,程序会“穿透“到下一个 case 继续执行,这通常不是你想要的行为:
switch (n)
{
case 1:
printf("一\n"); // n==1 时会输出 一、二、三 三行!
case 2:
printf("二\n");
case 3:
printf("三\n");
}
但有时刻意利用穿透也能写出紧凑的代码——比如多个 case 共享同一段处理逻辑:
switch (day)
{
case 1:
case 2:
case 3:
case 4:
case 5:
printf("工作日\n");
break;
case 6:
case 7:
printf("周末\n");
break;
}
switch 有两条使用限制需要记住:第一,括号内的表达式必须是整数类型(int、char、枚举等),float 和 double 不能用于 switch;第二,每个 case 后面必须是常量,不能是变量或范围表达式。
5.6 语法规则总结
if-else 系列:
// 形式一:单分支
if (条件) { 语句; }
// 形式二:双分支
if (条件) { 语句; } else { 语句; }
// 形式三:多分支
if (条件1) { 语句; }
else if (条件2) { 语句; }
else if (条件3) { 语句; }
else { 语句; }
核心规则:
if后面必须跟一对圆括号(),条件写在括号内- 条件为真(非 0)时执行
if对应的花括号中的代码 else不能单独使用,必须与一个if配对else if可以有多个,else至多一个- 如果某个分支内部只有一条语句,花括号可以省略,但初学者不建议这样做——省略花括号后容易在修改代码时引入逻辑错误
switch:
switch (表达式)
{
case 常量1: 语句; break;
case 常量2: 语句; break;
default: 语句;
}
核心规则:
- 表达式必须是整数类型,
case后必须是常量 - 匹配到
case后一直执行到break才跳出 - 漏写
break会导致穿透到下一个case default是可选的,所有case都不匹配时执行
什么时候用 if-else,什么时候用 switch? 范围判断(大于、小于、区间)用 if-else;对同一个变量的不同取值做等值判断,且分支较多时,用 switch 更清晰。
5.7 动手练习
#include <stdio.h>
int main(void)
{
int number = 15;
// 判断一个数是正数、负数还是零
if (number > 0)
{
printf("%d 是正数\n", number);
}
else if (number < 0)
{
printf("%d 是负数\n", number);
}
else
{
printf("这是零\n");
}
// 判断奇偶性
if (number % 2 == 0)
{
printf("%d 是偶数\n", number);
}
else
{
printf("%d 是奇数\n", number);
}
return 0;
}
请尝试:
- 把
number改成负数,观察输出变化 - 把
number改成 0,观察输出变化 - 编写代码判断一个年份是否为闰年(条件:能被 4 整除但不能被 100 整除,或者能被 400 整除)
六、循环语句
编程中有一类非常常见的需求:重复执行某些操作。计算 1 到 100 的累加和,需要做 99 次加法;将“Hello“打印 10 次,如果每次都写一行 printf,程序会变得冗长且难以修改。循环语句的作用正是——在满足条件的前提下,让一段代码被反复执行。
C 语言提供了三种循环结构:while、for 和 do-while。它们的核心逻辑是相同的(重复执行),区别在于判断条件的位置和时机不同。下面按这个顺序逐一讲解,掌握它们并能够根据场景选择最合适的一种,是写好 C 程序的基本功。
6.1 while 循环
while 循环的逻辑是:先判断条件,条件成立才执行循环体。执行完一遍循环体之后,回到开头重新判断条件,如此反复。其语法为:
while (条件)
{
循环体语句;
}
示例——从 1 数到 5:
#include <stdio.h>
int main(void)
{
int count = 1;
while (count <= 5)
{
printf("第 %d 次循环\n", count);
count = count + 1; // 每次循环让 count 增加 1
}
printf("循环结束。\n");
return 0;
}
输出:
第 1 次循环
第 2 次循环
第 3 次循环
第 4 次循环
第 5 次循环
循环结束。
将执行过程按轮次展开,可以清楚地看到条件判断和变量变化的节奏:
| 轮次 | count 的值 | count <= 5? | 执行的操作 |
|---|---|---|---|
| 第1次 | 1 | 真 | 输出“第 1 次循环“,count 变为 2 |
| 第2次 | 2 | 真 | 输出“第 2 次循环“,count 变为 3 |
| 第3次 | 3 | 真 | 输出“第 3 次循环“,count 变为 4 |
| 第4次 | 4 | 真 | 输出“第 4 次循环“,count 变为 5 |
| 第5次 | 5 | 真 | 输出“第 5 次循环“,count 变为 6 |
| 第6次 | 6 | 假 | 条件不满足,退出循环 |
这里有一个极其重要的原则:循环体内必须有能让条件最终变为假的代码——本例中是 count = count + 1。如果缺失了这一步,count 永远是 1,条件 count <= 5 永远为真,循环便永不终止,形成所谓的死循环。
6.2 for 循环
当你明确知道需要循环多少次时(比如“从 1 数到 5“、“重复 10 次”),for 循环是最常用也最简洁的选择。它把循环变量的初始化、条件判断和更新全部集中在一行中,使循环的结构一目了然。其语法为:
for (初始化; 条件; 更新)
{
循环体语句;
}
for 后面小括号里的三个部分,分别负责一件事:
| 部分 | 执行时机 | 作用 |
|---|---|---|
| 初始化 | 循环开始前,仅执行一次 | 设置循环计数变量的初始值 |
| 条件 | 每次循环开始前判断 | 为真则进入循环体,为假则退出 |
| 更新 | 每次循环体执行完毕后 | 更新循环计数变量 |
示例——从 1 数到 5:
#include <stdio.h>
int main(void)
{
for (int i = 1; i <= 5; i = i + 1)
{
printf("第 %d 次循环\n", i);
}
printf("循环结束。\n");
return 0;
}
输出:
第 1 次循环
第 2 次循环
第 3 次循环
第 4 次循环
第 5 次循环
循环结束。
逐轮手动推理——跟着程序走一遍:
下面我们用纸笔推演的方式,逐行跟踪程序的执行过程。这是理解循环最有效的方法——不要只“看“,跟着手动推一遍。
| 轮次 | 代码执行顺序 | i 的值 | i <= 5? | 执行的操作 |
|---|---|---|---|---|
| 准备 | int i = 1(初始化,只执行一次) | 1 | — | i 被设为 1 |
| 第1轮 | 判断 i <= 5 → 真 → 执行循环体 → i = i + 1 | 1 → 2 | 真 | 输出“第 1 次循环“,i 从 1 变为 2 |
| 第2轮 | 判断 i <= 5 → 真 → 执行循环体 → i = i + 1 | 2 → 3 | 真 | 输出“第 2 次循环“,i 从 2 变为 3 |
| 第3轮 | 判断 i <= 5 → 真 → 执行循环体 → i = i + 1 | 3 → 4 | 真 | 输出“第 3 次循环“,i 从 3 变为 4 |
| 第4轮 | 判断 i <= 5 → 真 → 执行循环体 → i = i + 1 | 4 → 5 | 真 | 输出“第 4 次循环“,i 从 4 变为 5 |
| 第5轮 | 判断 i <= 5 → 真 → 执行循环体 → i = i + 1 | 5 → 6 | 真 | 输出“第 5 次循环“,i 从 5 变为 6 |
| 第6轮 | 判断 i <= 5 → 假 → 退出循环 | 6 | 假 | printf("循环结束。\n") 被执行 |
关键观察:i 从 1 开始,每轮加 1。第 5 轮结束时 i 变成 6,第 6 轮判断时 6 > 5,条件不成立,循环结束。循环体的执行发生在“条件为真“的时候——条件一旦为假,循环体就被跳过了。
对于 i = i + 1 这种让变量加 1 的操作,C 语言提供了一个更简洁的写法:i++(称为自增运算符),效果和 i = i + 1 完全相同。因此上面的 for 循环通常写成:
for (int i = 1; i <= 5; i++)
{
printf("第 %d 次循环\n", i);
}
对初学者而言,建议先用
i = i + 1把逻辑想清楚,熟练之后再改用i++。i++和++i的区别已经在第四节 运算符中详细讲过,忘了可以回去翻看。
6.3 do-while 循环
while 和 for 都是“先判断,再执行“——条件不成立的话,循环体一次都不会执行。但有些时候,你希望不管条件是否成立,先执行一次再说。do-while 就是为这种场景设计的。其语法为:
do
{
循环体语句;
} while (条件);
注意
while (条件)后面有一个分号;,这是do-while在语法上区别于while的重要标志——漏掉这个分号会导致编译错误。
示例——从 1 数到 5:
#include <stdio.h>
int main(void)
{
int count = 1;
do
{
printf("第 %d 次循环\n", count);
count = count + 1;
} while (count <= 5);
printf("循环结束。\n");
return 0;
}
输出与 while 版本完全相同:
第 1 次循环
第 2 次循环
第 3 次循环
第 4 次循环
第 5 次循环
循环结束。
逐轮手动推理——跟着程序走一遍:
| 轮次 | 代码执行顺序 | count 的值 | count <= 5? | 执行的操作 |
|---|---|---|---|---|
| 第1轮 | 先执行循环体 → count = count + 1 → 再判断 | 1 → 2 | 真 | 输出“第 1 次循环“,count 从 1 变为 2,判断 2<=5 真 |
| 第2轮 | 执行循环体 → count = count + 1 → 判断 | 2 → 3 | 真 | 输出“第 2 次循环“,count 从 2 变为 3,判断 3<=5 真 |
| 第3轮 | 执行循环体 → count = count + 1 → 判断 | 3 → 4 | 真 | 输出“第 3 次循环“,count 从 3 变为 4,判断 4<=5 真 |
| 第4轮 | 执行循环体 → count = count + 1 → 判断 | 4 → 5 | 真 | 输出“第 4 次循环“,count 从 4 变为 5,判断 5<=5 真 |
| 第5轮 | 执行循环体 → count = count + 1 → 判断 | 5 → 6 | 真 → 假 | 输出“第 5 次循环“,count 从 5 变为 6,判断 6<=5 假 |
注意第 1 轮:count 的初始值是 1,程序不先判断,直接进入循环体执行 printf,然后才做 count = count + 1,最后判断 count <= 5。这就是 do-while 与 while 的核心差异——先斩后奏。
while 和 do-while 的关键区别: 当条件一开始就不成立时,两者的差异最明显:
// while 版本:循环体一次都不会执行
int count = 10;
while (count <= 5)
{
printf("这句话不会输出\n"); // 永远不会执行
}
// do-while 版本:循环体至少执行一次
int count = 10;
do
{
printf("这句话会输出一次\n"); // 执行一次后,判断 10 <= 5 为假,退出
} while (count <= 5);
总结两者的核心区别:
| 特性 | while / for | do-while |
|---|---|---|
| 判断时机 | 先判断,后执行 | 先执行,后判断 |
| 最少执行次数 | 0 次 | 1 次 |
选择哪一种取决于业务逻辑:如果“先检查再行动“是合理的(比如检查文件是否还存在再读取),用 while 或 for;如果“先行动再检查“是合理的(比如先让用户输入,再检查输入是否合法),用 do-while。
6.4 循环的选择
三种循环在功能上是等价的——任何一个循环都可以用另外两种形式改写。但根据场景选择最自然的形式,能让代码的可读性大为提升:
| 场景 | 推荐选择 |
|---|---|
| 循环次数已知(如“重复10次“、“从1到100”) | for |
| 循环次数未知,但条件明确(如“直到用户输入正确“) | while |
| 循环体必须先执行一次,再判断是否继续 | do-while |
6.5 break 与 continue
循环体内还可以使用两个特殊的关键字来控制循环的执行流程。
break 的作用是立即终止整个循环,程序跳到循环后面的第一条语句继续执行:
#include <stdio.h>
int main(void)
{
for (int i = 1; i <= 10; i++)
{
if (i == 5)
{
printf("遇到 5,提前结束循环\n");
break; // 跳出整个 for 循环
}
printf("当前数字:%d\n", i);
}
printf("循环外的代码\n");
return 0;
}
输出:
当前数字:1
当前数字:2
当前数字:3
当前数字:4
遇到 5,提前结束循环
循环外的代码
continue 的作用是跳过本次循环的剩余部分,直接进入下一次循环的条件判断:
#include <stdio.h>
int main(void)
{
for (int i = 1; i <= 5; i++)
{
if (i == 3)
{
printf("跳过 3\n");
continue; // 跳过本次循环剩余代码
}
printf("当前数字:%d\n", i);
}
return 0;
}
输出:
当前数字:1
当前数字:2
跳过 3
当前数字:4
当前数字:5
两者的区别值得牢记:break 是彻底离开循环,continue 只是跳过当前这一轮。另外,break 和 continue 只对它们所在的最内层循环有效——如果嵌套了多层循环,它们无法影响外层循环。
6.6 动手练习
#include <stdio.h>
int main(void)
{
// 练习1:计算 1 到 100 的和
int sum = 0;
for (int i = 1; i <= 100; i++)
{
sum = sum + i;
}
printf("1 到 100 的和是:%d\n", sum);
// 练习2:打印 1 到 20 中的偶数
printf("1 到 20 的偶数:");
for (int i = 2; i <= 20; i = i + 2)
{
printf("%d ", i);
}
printf("\n");
// 练习3:用 while 实现倒计时
int count = 5;
while (count > 0)
{
printf("倒计时:%d\n", count);
count--;
}
printf("发射!\n");
return 0;
}
请尝试:
- 修改练习1,计算 1 到 100 中所有奇数的和
- 用
for循环打印九九乘法表的前五行 - 用
do-while写一个模拟输入验证的程序:不断生成随机数,直到生成的数大于 100 为止
七、函数
7.1 从数学中的函数说起
函数(function)这个概念并非编程所独有——你在数学课上已经见过它了。
数学中的函数 f(x) = x + 5 表达了一种“输入—处理—输出“的关系:给它一个输入值 x,它经过计算 x + 5,返回一个结果。当 x = 3 时,f(3) = 8;当 x = 10 时,f(10) = 15。这个模型包含三个要素:输入(参数 x)、处理(运算 x + 5)、输出(返回值)。
C 语言中的函数在概念上与数学函数高度一致,只是用代码来描述“处理“这一步。这种一致性是刻意为之——函数式思维是结构化编程的基石。
那么,为什么需要自己定义函数呢?假设程序中有多处需要计算“一个数的平方再加 5“,如果不使用函数,代码会写成这样:
int a = 3;
int b = 7;
int c = 10;
int result1 = a * a + 5;
int result2 = b * b + 5;
int result3 = c * c + 5;
同一个计算逻辑 x * x + 5 重复出现了三次。如果逻辑本身更复杂,或者需要对公式做修改(比如改为加 10),每一处都需要单独修改,这既低效又容易出错。函数的解决方案是:把这段逻辑封装起来,起一个名字,之后每次用到它时只需调用这个名字即可:
#include <stdio.h>
// 自定义函数:计算 x 的平方加 5
int square_plus_five(int x)
{
int result = x * x + 5;
return result;
}
int main(void)
{
int result1 = square_plus_five(3); // 14
int result2 = square_plus_five(7); // 54
int result3 = square_plus_five(10); // 105
printf("f(3) = %d\n", result1);
printf("f(7) = %d\n", result2);
printf("f(10) = %d\n", result3);
return 0;
}
使用函数带来了三个明确的收益:避免重复代码(写一次,多次调用);使程序结构更清晰(把复杂问题拆分为小块);便于维护(修改函数内部一处,所有调用处自动生效)。
7.2 函数的定义
定义一个函数需要指定以下五个要素:
返回值类型 函数名(参数类型 参数名1, 参数类型 参数名2, ...)
{
函数体语句;
return 返回值;
}
| 部分 | 作用 | 示例 |
|---|---|---|
| 返回值类型 | 函数返回结果的数据类型,如果不返回任何值则写 void | int, float, void |
| 函数名 | 调用函数时使用的名字 | square_plus_five, get_max |
| 参数列表 | 函数的输入,可以为空(写 void 或留空) | int x, int a, int b |
| 函数体 | 函数执行的代码 | { ... } 中的内容 |
| return 语句 | 将计算结果返回给调用者 | return result; |
根据有无参数和有无返回值,函数可以归纳为四种形式:
形式一:有参数,有返回值
int add(int a, int b)
{
int sum = a + b;
return sum;
}
形式二:无参数,有返回值
int get_hundred(void)
{
return 100;
}
形式三:有参数,无返回值
void greet(char name[])
{
printf("你好,%s!\n", name);
// 没有 return 语句,或写 return;(不带任何值)
}
形式四:无参数,无返回值
void print_line(void)
{
printf("--------------------\n");
}
void 关键字表示“空“、“没有”。用在返回值类型的位置表示函数不返回任何值,用在参数列表的位置表示函数不接受任何参数。
7.2.1 函数不能重名——C 语言不支持函数重载
在 C 语言中有一条铁律:同一个程序中,不能存在两个名字相同的函数。 无论它们的参数类型或参数个数是否不同,只要函数名相同,编译器就会报错。
下面的代码在 C 语言中是不合法的:
// 错误:两个函数都叫 add,编译器无法区分
int add(int a, int b)
{
return a + b;
}
double add(double a, double b) // 编译错误:函数 add 重复定义!
{
return a + b;
}
如果你接触过 C++、Java 或 Python 等语言,你可能听说过函数重载(Function Overloading)——允许同一个函数名定义多个版本,根据调用时传入的参数类型和个数自动匹配对应的版本。这是这些语言提供的一项便利特性。
但 C 语言不支持函数重载。原因在于 C 语言的编译模型:编译器在生成目标代码时,函数在内部符号表中的名字就是你在代码中写的名字。如果你定义了两个同名的函数,编译器在链接阶段会发现同一个名字对应了两段不同的代码——它不知道该选哪一个,于是直接报错。
这实际上是 C 和 C++ 的一个关键差异点。C++ 支持函数重载,因为它采用了名字修饰(Name Mangling)技术——编译器在内部会根据函数的参数类型和个数,为每个重载版本生成一个独一无二的内部名字(例如
add_int_int和add_double_double),从而在底层区分它们。而 C 语言不做这种修饰,函数名是什么,内部符号名就是什么。
因此,如果你需要处理不同类型的数据,就需要给函数起不同的名字来区分:
int add_int(int a, int b) // 整数加法
{
return a + b;
}
double add_double(double a, double b) // 浮点加法
{
return a + b;
}
7.3 函数的调用
定义函数之后,需要通过调用来使用它。调用时写出函数名,并在圆括号中提供具体的参数值(称为实参,以区别于函数定义中的形参):
#include <stdio.h>
// 函数定义:求两个数中的较大值
int get_max(int a, int b)
{
if (a > b)
{
return a;
}
else
{
return b;
}
}
int main(void)
{
int max1 = get_max(5, 3); // max1 = 5
int max2 = get_max(10, 20); // max2 = 20
int max3 = get_max(-1, -5); // max3 = -1
printf("5 和 3 中的较大值:%d\n", max1);
printf("10 和 20 中的较大值:%d\n", max2);
printf("-1 和 -5 中的较大值:%d\n", max3);
return 0;
}
函数调用的执行过程可以分为四步:程序执行到调用处,将实参的值传递给形参;跳转到函数定义处;执行函数体内的代码;遇到 return 后带着返回值跳回调用处,继续执行后续代码。
7.4 函数的声明与定义
在 C 语言中,声明(Declaration)和定义(Definition)是函数相关的两个重要概念:
- 函数声明 = 告诉编译器“有这么一个函数存在,它叫什么名字、接受什么参数、返回什么类型“。声明不包含函数体,只给出函数的“签名“。
- 函数定义 = 不仅告诉编译器函数的名字和类型签名,还给出了函数的完整实现——即花括号
{ }中的函数体代码。
两者在代码层面很容易区分:
// 这是函数声明——只有函数头部,末尾是分号,没有函数体
int add(int a, int b);
// 这是函数定义——有完整的函数头部 + 花括号中的函数体
int add(int a, int b)
{
return a + b;
}
C 语言的编译器是从上到下逐行阅读源代码的。如果 main 函数中调用了一个尚未被编译器“见过“的函数,编译器会报错。解决这个问题有两种方式:一是将函数定义写在 main 前面(前面大多数例子采用的都是这种方式);二是在 main 前面写上函数声明,将函数的具体定义放在 main 后面:
#include <stdio.h>
// 函数声明:提前告诉编译器 add 函数的存在
int add(int a, int b);
int main(void)
{
int result = add(3, 5);
printf("3 + 5 = %d\n", result);
return 0;
}
// 函数定义:实际的实现
int add(int a, int b)
{
return a + b;
}
函数声明的格式与函数定义的第一行相同,只是末尾要加上分号。它的作用就是提前告知编译器:“后面会有一个叫 add 的函数,它的参数类型和返回值类型是下面这样的,你先记下来,别急着报错。”
💡 提示:声明与定义的关系
- 声明可以出现多次(你可以在多个地方声明同一个函数),但定义只能有一份——因为函数的具体实现代码当然只需要写一次。
- 声明不分配内存、不生成可执行代码;定义会实际编译成机器指令。
- 现阶段你写的函数大多直接把定义写在
main前面,这既是声明也是定义,不需要刻意区分。但在大型项目中,函数声明通常被集中放在头文件(.h文件)中,而定义放在源文件(.c文件)中,二者分离,供多个源文件共享。这个主题将在第十六章详细展开。
7.5 动手练习
#include <stdio.h>
// 函数声明
int square(int x);
int is_even(int n);
void print_stars(int count);
int main(void)
{
printf("4 的平方 = %d\n", square(4));
printf("7 的平方 = %d\n", square(7));
printf("\n");
if (is_even(10) == 1)
{
printf("10 是偶数\n");
}
if (is_even(7) == 0)
{
printf("7 是奇数\n");
}
printf("\n");
printf("打印 5 个星号:");
print_stars(5);
return 0;
}
// 函数定义:计算平方
int square(int x)
{
return x * x;
}
// 函数定义:判断是否为偶数(1 表示是,0 表示否)
int is_even(int n)
{
if (n % 2 == 0)
{
return 1;
}
else
{
return 0;
}
}
// 函数定义:打印 count 个星号
void print_stars(int count)
{
for (int i = 0; i < count; i++)
{
printf("*");
}
printf("\n");
}
请尝试:
- 编写一个
get_min函数,返回两个整数中的较小值 - 编写一个
factorial函数,计算正整数 n 的阶乘(n! = 1 × 2 × 3 × … × n) - 编写一个
is_prime函数,判断一个数是否为素数
八、#include 与头文件
8.1 什么是 #include 和头文件
#include 是 C 语言的预处理指令(Preprocessor Directive)之一。以 # 开头的行都属于预处理指令,它们在实际编译开始之前,由预处理器(Preprocessor)先行处理。
头文件(Header File)是以 .h 为后缀的文件,内部通常包含函数声明、宏定义、类型定义等内容。它的作用是把一些公共的声明集中存放,供多个源文件引用。
#include 这条预处理指令的工作方式是:在编译之前,将指定头文件的全部内容原样复制、插入到 #include 所在的位置。换句话说,#include <stdio.h> 的效果就相当于把 stdio.h 这个文件的所有文字直接粘贴进你的源代码。
8.2 语法形式
#include 有两种写法:
#include <stdio.h> // 尖括号形式:用于标准系统头文件
#include "myheader.h" // 双引号形式:用于用户自定义头文件
| 形式 | 搜索方式 | 适用场景 |
|---|---|---|
尖括号 < > | 编译器在系统预设的目录中搜索 | C 语言官方提供的标准头文件 |
双引号 " " | 先在当前源文件所在目录搜索,找不到再去系统目录 | 你自己编写的头文件,或第三方库的头文件 |
初学阶段记住一个简单原则即可:官方提供的头文件用 < >,自己写的头文件用 " "。
8.3 通俗理解
如果觉得上面的概念比较抽象,可以先这样理解:
编程就像写文章。你写文章时,有些“素材“不会自己从头编——比如你要引用一句名人名言,或者引用一条公式,你会直接去查资料、抄过来用。C 语言也是同样的道理:很多基础功能(打印文字、做数学运算、获取随机数)已经有人帮你写好了,存在一个个叫“头文件“的文档里。你想用哪个功能,就在文件开头用 #include 把对应的文档“拿过来“。编译器看到 #include,就会在编译之前先去把那篇文档的内容拷过来,这样后续的代码就能顺利使用这些现成的功能了。
你可以暂时把 #include 理解为“导入工具包“——就像你要用螺丝刀,得先把工具箱打开;你要用 printf 输出文字,得先 #include <stdio.h> 把标准输入输出工具包导入。至于预处理器具体怎么处理、头文件和源文件的深层关系,后续章节会展开讲,现阶段先按这个理解来写代码就够了。
8.4 常用标准头文件速览
C 语言提供了一组标准头文件,各有不同的分工。以下列出初学阶段最常接触的几个,现阶段只需混个眼熟,用到时再回来查:
| 头文件 | 主要功能 | 常用函数/内容 |
|---|---|---|
stdio.h | 标准输入输出 | printf, scanf, puts, fgets |
stdlib.h | 标准库函数 | rand, srand, exit, malloc, free |
math.h | 数学函数 | sqrt, pow, sin, cos, fabs |
string.h | 字符串处理 | strlen, strcpy, strcmp, strcat |
time.h | 时间和日期 | time, clock |
使用某个标准库函数之前,需要先包含对应的头文件——查阅函数文档时,文档总会注明需要哪个头文件。
8.5 代码示例
下面是一个同时使用多个头文件的简单示例,帮助建立直观印象:
#include <stdio.h> // 为了使用 printf
#include <math.h> // 为了使用 sqrt(平方根)
#include <stdlib.h> // 为了使用 rand(随机数)
int main(void)
{
printf("4 的平方根是 %f\n", sqrt(4));
printf("一个随机数是 %d\n", rand());
return 0;
}
这个程序同时“导入“了三个工具包:stdio.h 负责屏幕输出,math.h 提供数学计算,stdlib.h 提供随机数功能。每个 #include 各司其职,需要什么功能就包含对应的头文件——这个模式将贯穿你今后写的每一个 C 程序。
💡 提示:关于自定义头文件
本章只涉及 C 语言官方提供的标准头文件。在大型项目中,程序员也会编写自己的头文件(例如
myutils.h),将自定义的函数声明集中管理。关于如何组织和编写自定义头文件,将在第十六章详细展开。
九、标准输入与输出
9.1 输出:printf 函数
前面各节中已经大量使用了 printf,本节对它做一个系统性的梳理。
printf 函数的作用是向屏幕输出格式化的文字。它的第一个参数是一个格式字符串,其中可以包含两种内容:普通字符(原样输出)和格式占位符(以 % 开头,会被后续参数的值替换)。
什么是占位符?为什么需要占位符?
设想你要输出一句话:“我叫张三,今年 18 岁”。其中“张三“和“18“这两个值是存放在变量中的,每次运行程序时可能不同。你当然不能把变量名直接写进双引号——printf("我叫 name,今年 age 岁") 会在屏幕上原样输出 我叫 name,今年 age 岁,而不是变量的值。
用比喻来理解:占位符就像在句子中先挖一个坑(
%d),标记好“这里要填一个整数“,然后printf按照顺序,从后面的参数列表中取出对应的值填进去。坑挖在哪里、填什么类型,由你通过占位符精确控制。
占位符以 % 开头,常见的有 %d(整数)、%f(小数)、%c(字符)、%s(字符串)。
基本的调用格式为:
printf("格式字符串", 参数1, 参数2, ...);
常用格式占位符如下:
| 占位符 | 对应的数据类型 | 示例 | 输出结果 |
|---|---|---|---|
%d | int | printf("%d", 42); | 42 |
%f | float / double | printf("%f", 3.14); | 3.140000 |
%c | char | printf("%c", 'A'); | A |
%s | 字符串(char[]) | printf("%s", "Hello"); | Hello |
%u | unsigned int | printf("%u", 100); | 100 |
%% | 输出 % 本身 | printf("%%"); | % |
注意:
scanf和printf对double的格式要求不同。printf输出double使用%f;scanf读取double使用%lf。
控制小数位数——在 %f 的 % 和 f 之间加入 .n(n 为整数),可以指定保留 n 位小数:
float pi = 3.14159;
printf("默认:%f\n", pi); // 3.141590
printf("保留2位:%.2f\n", pi); // 3.14
printf("保留4位:%.4f\n", pi); // 3.1416(四舍五入)
控制输出宽度——在 % 和格式字符之间写一个数字,可以指定最小输出宽度:
int num = 42;
printf("[%d]\n", num); // [42]
printf("[%5d]\n", num); // [ 42](右对齐,占5个字符宽度)
printf("[%-5d]\n", num); // [42 ](左对齐,占5个字符宽度)
转义字符——格式字符串中,以下反斜杠组合有特殊含义:
| 转义字符 | 含义 |
|---|---|
\n | 换行 |
\t | 制表符(Tab) |
\\ | 反斜杠本身 |
\" | 双引号 |
9.2 输入:scanf 函数
scanf 函数用于从键盘读取用户输入的数据,并将读到的值存入指定的变量。其调用格式为:
scanf("格式字符串", &变量1, &变量2, ...);
最关键的注意事项是:变量名前必须加 & 符号。& 是取地址运算符(Address-of Operator),它的作用是获取变量在内存中的存储地址。关于地址的概念,这里先做一个直观的类比:
变量就像你在银行租的一个保险柜——保险柜本身有编号(地址),柜子里存放着你的物品(变量的值)。scanf 要做的事情是“把你从键盘输入的数据放进保险柜里“,所以它需要的不是柜子里的东西(变量的值),而是柜子的编号(变量的地址)。& 的作用就是告诉 scanf:“这个柜子的编号是多少,你把数据存到那个编号对应的柜子里去。”
如果你忘了写 &,scanf 就拿不到正确的地址,它会把数据存到一个错误的位置——这通常会导致程序崩溃。遗忘 & 是初学者在使用 scanf 时最常见的错误。
💡 提示:不理解没关系
地址和指针是 C 语言中比较抽象的概念,如果你现在对
&取地址的理解还比较模糊,完全正常。目前只需要记住一条规则:使用scanf读取普通变量时,变量名前必须加&。至于“地址“到底是什么、&的深层原理,将在第七章指针中详细展开。先会用,再理解。
下面的程序演示了 scanf 读取三种不同类型的数据:
#include <stdio.h>
int main(void)
{
int age;
float height;
char grade;
printf("请输入你的年龄:");
if (scanf("%d", &age) != 1) return 1;
printf("请输入你的身高(米):");
if (scanf("%f", &height) != 1) return 1;
// %c 前加空格,跳过之前遗留的换行符
printf("请输入你的成绩等级(A/B/C/D):");
if (scanf(" %c", &grade) != 1) return 1;
printf("\n--- 你的信息 ---\n");
printf("年龄:%d 岁\n", age);
printf("身高:%.2f 米\n", height);
printf("成绩等级:%c\n", grade);
return 0;
}
运行示例:
请输入你的年龄:18
请输入你的身高(米):1.75
请输入你的成绩等级(A/B/C/D):A
--- 你的信息 ---
年龄:18 岁
身高:1.75 米
成绩等级:A
%c 的特殊问题。 当用户输入完一个数字后按回车时,回车符(\n)会残留在输入缓冲区中。如果紧接着用 %c 读取字符,scanf 会把这个回车符当作一个“字符“读进去,导致用户还没来得及输入自己的字符,程序就已经“读“完了。解决方法是在 %c 前加一个空格:scanf(" %c", &grade);,这个空格会指示 scanf 跳过所有空白字符(空格、回车、Tab),直到遇到真正的非空白字符。
字符串输入(初步了解)。 用 scanf 读取字符串时,变量名前面不需要 &,因为数组名本身就代表地址:
char name[20];
printf("请输入你的名字:");
if (scanf("%19s", name) != 1) return 1; // 数组名不加 &,并检查读取结果
printf("你好,%s!\n", name);
这里的 19 是为了给数组最后的 '\0' 留出一个位置,避免用户输入过长时写出数组边界。第四章学习字符串时会更系统地讨论这个问题。
关于数组和字符串的详细内容,将在第二章和第三章中专门讨论。
9.3 动手练习:简易计算器
#include <stdio.h>
int main(void)
{
int num1, num2;
printf("=== 简易加法计算器 ===\n\n");
printf("请输入第一个整数:");
if (scanf("%d", &num1) != 1) return 1;
printf("请输入第二个整数:");
if (scanf("%d", &num2) != 1) return 1;
// 提升后再运算,避免 int 加减乘以及 INT_MIN / -1 发生有符号溢出
long long left = num1;
long long right = num2;
printf("\n%d + %d = %lld\n", num1, num2, left + right);
printf("%d - %d = %lld\n", num1, num2, left - right);
printf("%d * %d = %lld\n", num1, num2, left * right);
if (num2 != 0)
{
printf("%d / %d = %lld\n", num1, num2, left / right);
printf("%d %% %d = %lld\n", num1, num2, left % right);
}
else
{
printf("除数不能为 0!\n");
}
return 0;
}
请尝试:
- 将程序改为支持
float类型的运算(对应的占位符为%f) - 编写一个程序,让用户输入圆的半径,计算并输出面积(面积 = π × 半径²)
- 编写一个程序,让用户输入三个整数,输出其中的最大值
十、综合练习:猜数游戏
本章覆盖了大量基础语法,逐一练习之后,本节用一个完整的程序将它们串联起来。这个猜数游戏的规则很直观:程序在 1 到 100 之间随机选定一个数,玩家反复猜测,每次猜测后程序给出“猜大了“或“猜小了“的提示,直到猜中为止。程序还会记录猜测次数并给出评价,并支持再来一局。
这个程序虽然不长,但几乎用到了本章介绍的所有知识点:变量与数据类型、运算符、判断语句、循环语句、函数、头文件、标准输入输出。
10.1 完整代码
将以下代码保存为 guess_game.c,编译并运行。
/* =============================================
* 猜数游戏
* 功能:程序随机生成 1-100 的整数,玩家猜数字
* 知识点:变量、类型、运算符、判断、循环、函数、
* 头文件、标准输入输出
* ============================================= */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/* ---------- 函数声明 ---------- */
int generate_random_number(int min, int max);
int get_valid_guess(void);
int check_guess(int guess, int target);
/* ---------- 主函数 ---------- */
int main(void)
{
int target_number;
int user_guess;
int guess_count;
int game_count = 0;
int is_correct;
char play_again;
srand((unsigned int)time(NULL));
printf("=================================\n");
printf(" 欢迎来到猜数游戏!\n");
printf("=================================\n\n");
do
{
game_count++;
target_number = generate_random_number(1, 100);
guess_count = 0;
is_correct = 0;
printf("【第 %d 局】我已经想好了一个 1 到 100 之间的数字。\n", game_count);
printf("请你来猜一猜吧!\n\n");
while (!is_correct)
{
user_guess = get_valid_guess();
guess_count++;
is_correct = check_guess(user_guess, target_number);
}
printf("\n恭喜你猜对了!你一共猜了 %d 次。\n", guess_count);
if (guess_count <= 5)
{
printf("评价:太厉害了,简直是猜数天才!\n");
}
else if (guess_count <= 10)
{
printf("评价:不错不错,继续加油!\n");
}
else
{
printf("评价:还可以更棒,多练习几次吧!\n");
}
printf("\n想再来一局吗?(y/n):");
if (scanf(" %c", &play_again) != 1) play_again = 'n';
printf("\n");
} while (play_again == 'y' || play_again == 'Y');
printf("感谢游玩,再见!\n");
return 0;
}
/* ---------- 函数定义 ---------- */
/*
* 函数:generate_random_number
* 功能:生成 [min, max] 范围内的随机整数
*/
int generate_random_number(int min, int max)
{
int random_num = rand() % (max - min + 1) + min;
return random_num;
}
/*
* 函数:get_valid_guess
* 功能:获取用户输入的有效猜测(1-100 之间的整数),无效输入会提示重新输入
*/
int get_valid_guess(void)
{
int guess;
int is_valid;
do
{
printf("请输入你的猜测(1-100):");
is_valid = scanf("%d", &guess);
if (is_valid == EOF)
{
printf("\n输入已结束,游戏退出。\n");
exit(EXIT_SUCCESS);
}
// 清除输入缓冲区中的无效字符
int ch;
while ((ch = getchar()) != '\n' && ch != EOF)
{
// 空循环体:逐字符读取直到遇到换行符
}
if (is_valid != 1 || guess < 1 || guess > 100)
{
printf("输入无效!请输入 1 到 100 之间的整数。\n\n");
is_valid = 0;
}
} while (!is_valid);
return guess;
}
/*
* 函数:check_guess
* 功能:比较猜测值和目标值,给出提示
* 返回值:1 表示猜中,0 表示未猜中
*/
int check_guess(int guess, int target)
{
if (guess > target)
{
printf(" → 猜大了!再小一点试试。\n\n");
return 0;
}
else if (guess < target)
{
printf(" → 猜小了!再大一点试试。\n\n");
return 0;
}
else
{
printf(" → 恭喜你,猜对了!\n");
return 1;
}
}
10.2 分步讲解
第一步:头文件包含
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
这三个 #include 行各自引入一个“工具箱“:
<stdio.h>→ 提供printf(屏幕输出)和scanf(键盘输入)<stdlib.h>→ 提供rand(生成随机数)和srand(设置随机种子)<time.h>→ 提供time(获取系统时间,用作随机种子)
需要哪个功能,就包含哪个头文件——这是 C 语言组织代码的基本方式。
第二步:函数声明
int generate_random_number(int min, int max);
int get_valid_guess(void);
int check_guess(int guess, int target);
这三行是函数声明——它们告诉编译器:“后面会定义这三个函数,它们的名字、参数类型和返回值类型长这样”。因为这三个函数的定义写在 main 后面,而 main 中要调用它们,所以需要提前“打个招呼“。
每行末尾的分号告诉编译器“这里只是声明,不是定义“。
第三步:变量声明与随机种子初始化
int main(void)
{
int target_number; // 存储本局的目标数字
int user_guess; // 存储用户每次的猜测
int guess_count; // 记录猜测次数
int is_correct; // 0 = 未猜中,1 = 猜中
char play_again; // 存储用户的重玩意愿 (y/n)
int game_count = 0; // 记录已玩局数(初始为 0)
srand((unsigned int)time(NULL));
进入 main 函数后,第一件事是声明需要用到的变量。每个变量都有明确的职责(见注释),声明类型后编译器才知道要为它们分配多少内存。
紧接着,程序把 time(NULL) 的结果转换成 unsigned int 后交给 srand,用当前日历时间初始化伪随机数生成器。不同秒启动时通常会得到不同种子;同一秒内多次启动仍可能得到相同序列,而且 rand() 不适合密码学或安全用途。这里把它用于入门猜数游戏已经足够。
第四步:外层 do-while 循环——控制多局游戏
do
{
game_count++; // 局数 +1
target_number = generate_random_number(1, 100); // 生成 1~100 的随机目标
guess_count = 0; // 重置猜测次数
is_correct = 0; // 重置猜中状态
printf("【第 %d 局】我已经想好了一个 1 到 100 之间的数字。\n", game_count);
printf("请你来猜一猜吧!\n\n");
// ← 内层循环在这里,见第五步
// ← 成绩评价与重玩在这里,见第八步
} while (play_again == 'y' || play_again == 'Y');
整个游戏逻辑放在一个 do-while 循环中。选择 do-while 的理由很简单:游戏至少需要运行一局,而 do-while 保证“先执行,后判断“,正好符合这个需求。
每局开始前做了三件事:
- 调用
generate_random_number(1, 100)生成新目标 - 把
guess_count和is_correct复位(否则上一局的数据会干扰本局) - 打印本局的欢迎提示
循环末尾检查 play_again == 'y' || play_again == 'Y',用户输入 y 或 Y 就再来一局,否则退出。
第五步:内层 while 循环——单局猜测逻辑
while (!is_correct) // is_correct 为 0 时继续循环
{
user_guess = get_valid_guess(); // ① 获取一个合法输入
guess_count++; // ② 猜测次数 +1
is_correct = check_guess(user_guess, target_number); // ③ 判断结果
}
这个 while 循环是单局游戏的心脏。!is_correct 的含义是“还没猜中“(is_correct 为 0 时,!is_correct 为 1,循环继续;猜中后 is_correct 变为 1,!is_correct 为 0,循环退出)。
每轮循环做三件事,一行对应一个操作:
| 行 | 代码 | 做了什么 |
|---|---|---|
| ① | user_guess = get_valid_guess() | 等待用户输入一个 1~100 的合法整数 |
| ② | guess_count++ | 猜了一次,计数加 1 |
| ③ | is_correct = check_guess(...) | 判断猜得对不对,返回 0 或 1 |
这三行代码完美对应“输入 → 计数 → 判断“的游戏流程。直到猜中,循环才退出。
第六步:get_valid_guess 函数——输入验证
int get_valid_guess(void)
{
int guess;
int is_valid;
do
{
printf("请输入你的猜测(1-100):");
is_valid = scanf("%d", &guess); // 尝试读取一个整数
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) // 清理本行剩余输入
{
// 空循环体:逐字符吃掉剩余内容直到换行符
}
if (is_valid != 1 || guess < 1 || guess > 100)
{
printf("输入无效!请输入 1 到 100 之间的整数。\n\n");
is_valid = 0; // 标记无效,让外层 do-while 再来一轮
}
} while (!is_valid); // 输入不合法就重新来
return guess;
}
这个函数用 do-while 循环死磕到底——用户不输入合法数字,就一直提示重新输入。有两个关键细节:
scanf("%d", &guess) 的返回值等于“成功读取的项目数“。如果用户输入了 abc 之类的非数字内容,scanf 读不到整数,返回的不是 1,函数就知道输入无效。
这个空循环负责丢弃当前输入行中尚未读取的字符,直到遇到换行符或 EOF。必须判断 EOF,否则当输入流关闭且没有换行时,getchar() 会持续返回 EOF,循环将永远无法结束。
第七步:check_guess 函数——比较与提示
int check_guess(int guess, int target)
{
if (guess > target)
{
printf(" → 猜大了!再小一点试试。\n\n");
return 0; // 未猜中,返回 0
}
else if (guess < target)
{
printf(" → 猜小了!再大一点试试。\n\n");
return 0; // 未猜中,返回 0
}
else
{
printf(" → 恭喜你,猜对了!\n");
return 1; // 猜中,返回 1
}
}
这个函数是游戏规则的直接体现,只有三种可能:
- 猜大了 → 提示“再小一点“,返回
0 - 猜小了 → 提示“再大一点“,返回
0 - 猜对了 → 祝贺,返回
1
返回值 0 或 1 被赋给 is_correct,从而控制第五步的内层循环是继续还是退出。
第八步:成绩评价与重玩
printf("\n恭喜你猜对了!你一共猜了 %d 次。\n", guess_count);
if (guess_count <= 5)
printf("评价:太厉害了,简直是猜数天才!\n");
else if (guess_count <= 10)
printf("评价:不错不错,继续加油!\n");
else
printf("评价:还可以更棒,多练习几次吧!\n");
printf("\n想再来一局吗?(y/n):");
if (scanf(" %c", &play_again) != 1) play_again = 'n';
猜中之后,用 if-else if-else 按猜测次数分档评价。三条路径只会走一条:≤5 次是天才,6~10 次是不错,>10 次鼓励继续练习。
最后询问是否重玩。注意 scanf(" %c", &play_again) 中 %c 前面有一个空格——这个空格不是笔误,它的作用是跳过缓冲区中残留的空白字符(换行符、空格等)。如果省略这个空格,残留的换行符会被当成用户输入直接吃掉,导致程序跳过“是否重玩“的询问。
10.3 编译与运行
# Windows
clang guess_game.c -o guess_game.exe
guess_game.exe
# macOS / Linux
clang guess_game.c -o guess_game
./guess_game
运行示例:
=================================
欢迎来到猜数游戏!
=================================
【第 1 局】我已经想好了一个 1 到 100 之间的数字。
请你来猜一猜吧!
请输入你的猜测(1-100):50
→ 猜小了!再大一点试试。
请输入你的猜测(1-100):75
→ 猜大了!再小一点试试。
请输入你的猜测(1-100):60
→ 猜小了!再大一点试试。
请输入你的猜测(1-100):68
→ 恭喜你,猜对了!
恭喜你猜对了!你一共猜了 4 次。
评价:太厉害了,简直是猜数天才!
想再来一局吗?(y/n):n
感谢游玩,再见!
本章小结
本章以猜数游戏这个完整程序为核心,在逐步拆解游戏所需语法的过程中,初步介绍了 C 语言的核心基础语法:从 hello.c 的程序骨架,到注释、变量与数据类型、运算符、判断语句、循环语句、函数、头文件、标准输入输出,最后用猜数游戏将所有知识点串联在一起。
💡 提示:第一遍看不懂很正常
如果你是零基础初学者,读完这一章后感到有些概念还比较模糊——这是完全正常的。编程不是靠“看会“的,而是靠“练会“的。本章的内容不是让你一次性全部背下来,而是让你对 C 语言的语法有一个初步的轮廓认知。建议的学习方式是:以最后的猜数游戏为核心,反复阅读它的每一行代码,遇到不懂的语法点就翻回前面对应的小节去查。看不懂没关系,先跟着代码敲一遍,跑起来,再回头理解。多敲几遍,慢慢就通了。
以下是本章关键知识点的回顾:
- C 程序从
main函数开始执行,花括号定义代码范围,分号终结语句,#include引入外部代码 - 注释不会被编译,是写给人类阅读的说明文字
- 变量使用前必须声明类型;
int、float、double、char是四种基本数据类型 - 算术运算符、自增自减运算符(
++/--的前缀与后缀形式)、复合赋值运算符(+=、-=等)、逻辑运算符(&&、||、!)与比较运算符,以及运算符优先级 if、if-else、if-else if-else实现条件分支,程序根据条件的真假选择不同的执行路径while、do-while、for三种循环各有适用的场景,break终止循环,continue跳过本轮- 函数封装可复用的代码逻辑,接收参数、返回结果;声明与定义可以分离
printf和scanf分别负责屏幕输出和键盘输入,格式占位符是二者通用的核心机制
⚠️ 初学者常见错误提醒
在进入下一章之前,请务必注意以下两个最容易犯的错误——它们虽然简单,但几乎每个初学者都会踩坑:
一、所有符号必须是英文符号。 C 语言代码中的每一个标点符号——分号 ;、逗号 ,、双引号 "、单引号 '、小括号 ()、花括号 {}、小于号 <、大于号 >——都必须使用英文输入法输入。中文输入法下打出的符号(如 ;、,、"、'、()、《》)在编译器眼里全是非法字符,会导致编译失败。这是初学阶段最高频的问题,没有之一。当你看到编译器报出一大串看不懂的错误时,先检查有没有打成中文标点,往往问题就出在这里。
二、关键字和函数名的拼写要准确。 C 语言的关键字和标准库函数名都是固定的英文单词,一个字母都不能错。常见的拼写错误包括:
prnitf/print→ 正确写法是printfscanf→ 容易写成scanmain→ 容易写成mianinclude→ 容易写成inlcude或incluedfloat→ 容易写成flaotdouble→ 容易写成doubel
编译器对拼写错误零容忍,差一个字母就完全不认识。遇到“未定义的标识符“(undeclared identifier)这类报错时,检查拼写是第一步。
💡 小贴士:clang 命令行的 -o 选项
在安装好 Clang 之后,你将在终端中通过 clang 命令来编译你的 C 程序。截止目前,Clang 已经积累了上百个命令行选项,但对初学者而言,你真正需要立刻掌握的只有一个:-o。
最基本的编译命令:
打开终端,进入你的 .c 文件所在目录,输入:
clang hello.c
编译成功后,Clang 会默认在当前目录下生成一个名为 a.out(在 Windows 上是 a.exe)的可执行文件。这个默认文件名 a.out 源自 Unix 的传统——“a” 代表 “assembler output”(汇编器输出),这个命名习惯从 1970 年代一直沿用至今。
-o:指定输出文件名:
显然,每次编译完都得到一个叫 a.out 的文件并不方便——你很快就会分不清它到底是谁编译出来的。-o 选项(字母 o,代表 output)就是用来解决这个问题的:
clang hello.c -o hello
这条命令的含义是:编译 hello.c,并将输出的可执行文件命名为 hello(Windows 上会自动添加 .exe 后缀,即 hello.exe)。
-o 后面跟的,就是你希望生成的文件叫什么名字。你可以随便取:
clang hello.c -o my_first_program
clang hello.c -o hello_world
clang hello.c -o test
编译完成后,在 Linux/macOS 上用 ./hello 运行,在 Windows 上用 hello.exe(或直接 hello)运行。
-o 的位置:
-o 可以放在源文件的前面,也可以放在后面——两种写法都是合法的:
clang -o hello hello.c # -o 在前面
clang hello.c -o hello # -o 在后面
两种写法效果完全相同。本教程统一采用源文件在前、-o 在后的写法,因为读起来更自然:“编译 hello.c,输出为 hello”。