 与 scanf( ))
1. printf( ) 函数printf ( ) 函数的作用是将格式化的文本输出到屏幕标准输出名字中的 f 代表 format 格式化表示我们可以定制输出文本的格式。它的原型定义在标准库头文件stdio.h中使用前必须在源码头部引入这个头文件。1.1 基本用法#include stdio.h int main() { // 基础输出 printf(Hello C Language\n); // 换行说明\n 是换行符让光标移动到下一行的开头 printf(第一行内容\n第二行内容\n); return 0; }注意printf( ) 不会在行尾自动添加换行符运行结束后光标会停留在输出结束的位置需要手动添加 \n 实现换行。1.2 占位符printf( ) 最核心的能力就是通过占位符实现动态内容的输出。所谓 “占位符”就是输出文本中的这个位置可以用其他数值动态替换。#include stdio.h int main() { // 单个占位符%d 表示整数占位符 printf(There are %d apples\n, 3); // 多个占位符按顺序一一对应 printf(%s says it is %d oclock\n, zhangsan, 21); return 0; }• 占位符的第一个字符一律为 % 第二个字符表示占位符的类型比如 %d 表示整数%s 表示字符串• 占位符的数量必须和后面的参数数量一一对应否则会出现不可预期的错误。常用占位符举例占位符对应类型说明%cchar字符类型%d / %iint十进制整数%hdshort短整型%ldlong长整型%lldlong long长长整型%uunsigned int无符号整数%ffloat / double浮点数默认保留 6 位小数%lfdouble双精度浮点数%s字符串字符数组 / 字符串%p指针用来打印内存地址%zdsize_t对应 sizeof 运算符的返回值类型%%无输出一个百分号本身其他占位符占位符对应类型说明%a十六进制浮点数十六进制浮点数字母输出为小写%A十六进制浮点数十六进制浮点数字母输出为大写%e浮点数科学计数法使用科学计数法的浮点数指数部分的 e 为小写%E浮点数科学计数法使用科学计数法的浮点数指数部分的 E 为大写%g浮点数6 个有效数字的浮点数。整数部分一旦超过 6 位就会自动转为科学计数法指数部分的 e 为小写%G浮点数等同于 % g唯一的区别是指数部分的 E 为大写%ho八进制 short int 类型\%hx十六进制 short int 类型\%huunsigned short int 类型无符号短整型 unsigned short int 类型%lo八进制 long int 类型\%lx十六进制 long int 类型\%luunsigned long int 类型无符号长整型 unsigned long int 类型%llo八进制 long long int 类型\%llx十六进制 long long int 类型\%lluunsigned long long int 类型无符号长长整型 unsigned long long int 类型%Lelong double 类型浮点数科学计数法表示的 long double 类型浮点数%Lflong double 类型浮点数\%n整数输出字符统计已输出的字符串数量。该占位符本身不输出只将值存储在指定变量之中%o八进制整数\%x十六进制整数\1.3 定制输出格式printf( ) 支持通过占位符定制输出格式满足不同的显示需求。1.3.1 限定输出宽度通过 % [数字] d 的格式可以限定占位符的最小输出宽度若内容不足宽度默认在前面补空格右对齐。#include stdio.h int main() { // 最小宽度5位不足补空格输出 123 printf(%5d\n, 123); // 加-号改为左对齐输出 123 printf(%-5d\n, 123); // 浮点数限定宽度输出 123.450000 printf(%12f\n, 123.45); return 0; }运行结果1.3.2 总是显示正负号默认情况下printf( ) 仅会对负数显示 - 号不会为正数显式添加 号。如果需要让正数也输出正号保证正负数值的格式统一只需在占位符的 % 后添加一个 号即可。#include stdio.h int main() { printf(%d\n, 12); // 输出 12 printf(%d\n, -12); // 输出 -12 return 0; }1.3.3 限定小数位数通过 % . [数字] f 的格式可以限定浮点数输出的小数位数。#include stdio.h int main() { // 保留2位小数输出 0.50 printf(%.2f\n, 0.5); // 结合宽度限定总宽度6位保留2位小数输出 0.50 printf(%6.2f\n, 0.5); return 0; }运行结果1.3.4 动态指定输出宽度与小数位数除了在占位符中硬编码固定的宽度和小数位数printf( ) 还支持用 * 号代替这两个限定值数值可以通过 printf( ) 的后续参数动态传入让输出格式的控制更加灵活无需修改占位符本身就能调整输出样式。#include stdio.h int main() { // 用*代替宽度和小数位数通过后续参数6、2动态传入 printf(%*.*f\n, 6, 2, 0.5); // 这行代码完全等价于 printf(%6.2f\n, 0.5); return 0; }上述示例中%*.*f 里的两个 * 号会依次读取后续的两个参数 6 和 2 分别作为输出的最小宽度和保留的小数位数。1.3.5 输出部分字符串通过 % . [数字] s 的格式可以限定只输出字符串的前 N 个字符。#include stdio.h int main() { // 只输出前5个字符输出 hello printf(%.5s\n, hello world); return 0; }运行结果2. scanf( ) 函数scanf( ) 函数用于读取用户从键盘标准输入输入的内容程序运行到这个语句时会停下来等待用户输入用户按下回车键后scanf( ) 会按照格式解析输入内容并存入对应的变量中。它的原型同样定义在 stdio.h 头文件中。使用 scanf( ) 函数前需在代码的第一行必须在包含任何头文件之前添加如下宏定义#define _CRT_SECURE_NO_WARNINGS 1否则会出现如下报错2.1 基本用法#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { int score 0; printf(请输入成绩); // 读取用户输入的整数存入score变量 scanf(%d, score); printf(你输入的成绩是%d\n, score); return 0; }运行结果1. 第一个参数是格式字符串里面的占位符告诉编译器如何解析用户的输入比如 %d 表示读取整数2. 后续参数是用来存放输入内容的变量变量前面必须加上 取地址运算符字符串 / 指针变量除外因为 scanf( ) 需要通过变量的内存地址将用户输入的值存入变量中3. 占位符的数量必须和变量的数量一一对应。2.2 输入缓冲区1. 什么是输入缓冲区简单来说输入缓冲区是内存中开辟的一块临时存储区域。当你在键盘上输入内容时数据并不会直接传给程序中的变量而是先被存入这块临时区域只有当你按下回车键Enter后scanf( ) 才会从这块区域中“读取”数据。2. 为什么需要输入缓冲区设计输入缓冲区的核心目的有两个都是为了提升程序的运行效率和使用体验1协调硬件速度差异提升程序运行效率键盘输入的速度远慢于 CPU 的处理速度。如果程序每输入一个字符就要中断、处理一次会造成极大的性能浪费。有了缓冲区就可以等你输完一整行内容按下回车再一次性把数据交给程序处理大幅降低 CPU 的中断频率。2支持输入回退修改优化使用体验我们输入内容时经常会输错字符、按退格键修改。正是因为输入的内容先存在缓冲区里还没有传给程序我们才能在按下回车前自由修改输入的内容如果没有缓冲区字符会直接传给程序根本无法回退修改。3. scanf( ) 处理用户输入的完整流程我们通过一个具体场景拆解 scanf( ) 的处理逻辑场景程序先执行 scanf(%d,num); 用户输入 10 并按下回车键。完整流程如下1. 用户输入数据暂存缓冲区你在键盘上依次按下1、0、Enter回车键这些输入会被按顺序存入输入缓冲区。此时缓冲区里的内容是1、0、\n \n 是回车键对应的换行符。2. scanf( ) 按规则读取数据scanf(%d,num) 开始工作它会严格按照 %d整型的格式要求从缓冲区的第一个字符开始读取• 先跳过开头的空白字符空格、换行符、制表符等如果有的话• 读取连续的、符合整数格式 1 和 0 将其转换为整数 10 存入变量 num 中• 读到不属于整数的字符这里是换行符 \n 时立刻停止读取。3. 关键特性不符合要求的字符会残留在缓冲区中scanf( ) 只会读取符合格式要求的内容停止读取时那个 “不符合要求” 的字符会被留在缓冲区里不会被清除。上面的场景中换行符 \n 就被留在了缓冲区里 —— 这正是后续使用 %c 占位符时程序会 “自动跳过输入、直接读取换行” 的核心根源。2.3 使用规则2.3.1 空白字符自动过滤scanf( ) 处理数值占位符时会自动过滤用户输入中的空格、制表符、换行符等空白字符用户分行输入也能正常解析。#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { int i, j; float x, y; // 支持用户分行、加空格输入都能正常解析 scanf(%d%d%f%f, i, j, x, y); printf(i%d, j%d, x%f, y%f\n, i, j, x, y); return 0; }在上面代码中输入时可以用空格 / 制表符隔开数字也可以用回车隔开数字2.3.2 %c 的特殊读取规则在 scanf( ) 的所有常用占位符中除了 %c 以外其余数值类、字符串类占位符都会自动忽略用户开头输入的空白字符包括空格、制表符 Tab 、换行符 \n 如下所示但 %c 有完全不同的规则它不会自动跳过任何空白字符会严格读取输入缓冲区中的第一个字符无论这个字符是空格、换行符还是用户输入的有效字符。下面是一个常见的错误案例#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { int num 0; char ch 0; printf(请输入一个整数); scanf(%d, num); printf(请输入一个字符); scanf(%c, ch); // 错误预期等待用户输入字符实际会直接“跳过” // 打印变量的数值和ch的ASCII码验证读取结果 printf(num %d\n, num); printf(ch的ASCII码值%d\n, ch); return 0; }运行并输入整数 10运行结果运行上述代码当我们输入整数 10 并按下回车键后程序不会等待我们输入第二个字符会直接执行完毕。打印结果会显示 ch 的 ASCII 码值10 —— 这正是换行符 \n 的 ASCII 码值。核心原因用户输入 10 并按下回车后输入缓冲区中会存入 10\n scanf(%d, num) 会读取数字 10 把换行符 \n 留在了缓冲区中。后续的 scanf(%c, ch) 不会跳过空白字符直接把缓冲区里残留的换行符 \n 读走了自然不会等待用户的新输入。解决方案如果希望 %c 跳过所有前置的空白字符只读取用户输入的有效非空白字符只需在 %c 前面添加一个空格写成 scanf( %c,ch) 即可。这个前置空格会告诉 scanf( )先跳过输入中零个或多个连续的空白字符再读取后续的第一个非空白字符完美解决缓冲区残留空白字符的问题。修正后的正确代码#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { int num 0; char ch 0; printf(请输入一个整数); scanf(%d, num); printf(请输入一个字符); scanf( %c, ch); // %c前加空格跳过所有前置空白字符 printf(num %d\n, num); printf(你输入的字符是%c\n, ch); return 0; }运行结果2.3.3 %s 字符串读取规则%s 是 scanf( ) 中用于读取字符串的占位符。2.3.3.1 基础读取逻辑%s 的读取规则可以总结为从输入缓冲区的第一个非空白字符开始读取直到遇到任意空白字符空格、换行符 \n 、制表符 Tab 时立即停止读取。这里有两个关键细节需要重点注意1. 自动跳过开头空白和 %c 不同%s 会自动忽略输入开头所有连续的空白字符只会从第一个有效非空白字符开始读取2. 空白字符是终止符读取过程中只要遇到空格、换行、制表符中的任意一个就会立刻停止读取这个终止用的空白字符会被留在输入缓冲区中不会被读入字符串。也正因为这个规则%s无法用来读取包含空格的完整字符串比如书名、歌曲名、带空格的人名等。代码示例#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { char book_name[50]; printf(请输入书名); scanf(%s, book_name); printf(读取到的内容%s\n, book_name); return 0; }运行结果• 如果你输入 C语言入门无空格程序会正常读取完整内容输出读取到的内容C 语言入门• 如果你输入 C语言 入门中间带空格程序只会读取到空格前的内容输出读取到的内容C 语言而空格后的 “入门” 会被留在输入缓冲区中不会被读取。如果需要读取带空格的一整行内容需要多个 %s 配合使用或者使用后续会学到的 fgets( ) 函数。2.3.3.2 字符串结束符自动补充规则C 语言中的字符串必须以空字符 \0ASCII 码值为 0作为结束标志这是 C 语言字符串的核心底层规则。有两种最常见的场景编译器会自动为字符串补充 \0 结束符1. 所有双引号包裹的字符串字面量编译器会在编译阶段自动在其末尾补充一个 \0 2. scanf( ) 在使用 %s 占位符成功读取内容后会自动在读取到的字符串末尾补充一个 \0 结束符存入字符数组中。这个特性带来了一个硬性要求用来存储字符串的字符数组长度必须比你要存储的最大有效字符数多 1多出来的 1 个位置就是专门用来存放自动补充的 \0 结束符。示例需要最多存储 10 个有效字符字符数组长度至少要定义为 11存储 4 个有效字符的字符串数组长度至少要定义为 5。一个错误案例#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { // 错误写法数组长度不足无法同时容纳有效字符和结束符 // 字符串abcd完整存储需要5字节空间4个有效字符1个\0数组仅分配了4字节 char arr[4] { abcd }; // 因数组中无合法\0结束符输出会出现乱码 printf(%s\n, arr); return 0; }运行结果错误原因1. 字符串本身自带结束符数组容量不足导致内容被截断abcd 是 C 语言的字符串字面量它除了 a、b、c、d 这 4 个可见的有效字符编译器会在编译阶段自动在末尾补充一个 \0 结束符因此这个字符串完整存储需要占用5个字节的内存空间。而我们定义的 char arr[4]数组长度仅为 4最多只能存储 4 个字节的内容。在数组初始化时编译器只会把字符串的前 4 个字节a、b、c、d填入数组多出来的 \0 结束符会因为数组容量不足被直接截断丢弃最终数组里没有合法的字符串结束标志。2. 乱码的根源缺少 \0 结束符导致的内存越界读取printf(%s\n, arr); 的本质是按字符串格式输出内容它的执行规则是从你传入的数组起始内存地址开始逐个读取字符并输出直到读取到一个 \0 结束符才会停止输出。而上面的数组里仅存储了 a、b、c、d 没有合法的 \0 结束符。因此 printf( ) 不会在数组末尾停下会继续读取数组边界之外、不属于这个数组的内存数据直到在内存中碰巧遇到一个值为 0 的字节\0才会停止。3. 为什么会出现 “烫烫烫” 乱码在 Windows 的 Visual Studio Debug 环境下栈内存的未使用区域会被编译器默认填充为 0xcc这个十六进制值对应的 GBK 编码字符就是 “烫”。printf( ) 越界读取到的就是这些填充了 0xcc 的内存区域因此会输出大量的 “烫烫烫” 乱码。正确写法为结束符预留存储空间#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { // 正确写法1手动指定数组长度严格遵循 数组长度有效字符数1 // abcd有4个有效字符数组长度至少设为5给\0预留1个字节的位置 char arr1[5] { abcd }; printf(arr1的内容%s\n, arr1); // 正确写法2不手动指定数组长度让编译器自动计算所需空间 // 编译器会根据字符串字面量的完整长度含\0自动分配刚好5字节的空间 // 完全避免手动计算长度出错的问题是更稳妥的开发写法 char arr2[] { abcd }; printf(arr2的内容%s\n, arr2); // 打印数组占用的总字节数验证编译器自动包含了\0的空间 printf(arr2占用的总字节数%zd\n, sizeof(arr2)); return 0; }运行结果2.3.3.3 数组溢出风险理解了字符串结束符的规则我们就必须关注 %s 读取时一个非常危险的特性 —— 也是 Visual Studio 会对 scanf( ) 报安全警告的核心原因之一数组溢出风险。风险原理scanf( ) 使用 %s 读取字符串时不会自动检测用户输入的字符串长度是否超过了目标字符数组的最大容量。如果用户输入的有效字符数量超过了「数组长度 - 1」我们为 \0 预留的空间不仅会导致 \0 结束符没有合法的存放位置超出的字符还会直接越过数组的内存边界写入到数组后面、不属于它的内存空间中。这种行为被称为缓冲区溢出数组越界轻则篡改程序中其他变量的数值重则直接导致程序崩溃甚至被恶意利用执行非法代码是 C 语言开发中非常经典的安全隐患。错误案例未限制长度导致的数组溢出#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { // 数组长度为5最多只能存储4个有效字符1个\0结束符 char name[5]; printf(请输入你的名字); // 错误写法未限制读取长度用户输入过长就会触发数组溢出 scanf(%s, name); printf(你输入的名字是%s\n, name); return 0; }运行结果 1输入长度合规运行结果 2输入长度超出限制此时用户输入了 8 个有效字符远超数组能容纳的 4 个已经发生了数组溢出。数组溢出最危险的点在于它不一定会立刻触发程序崩溃或报错却会隐蔽地破坏内存数据留下难以排查的 bug甚至引发安全问题。安全解决方案显式限定读取的最大长度为了从根源上避免数组溢出的风险使用 %s 占位符时必须显式指定本次读取的最大有效字符长度标准写法为 %[m]s• 其中 [m] 是一个整数表示本次读取最多接收的有效字符个数• 超出这个长度的字符会被直接丢弃不会被读入字符数组中彻底杜绝数组越界的可能。重点提醒这里指定的长度 m必须小于等于「字符数组长度 - 1」必须预留 1 个位置给 scanf( ) 自动补充的 \0 结束符和前序的字符串结束符规则完全对应。比如数组长度为 5最多只能存储 4 个有效字符那么限定长度就应该写为 %4s 。正确安全的代码示例#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { // 数组长度为5最多存储4个有效字符1个\0结束符 char name[5]; printf(请输入你的名字); // 正确写法%4s 限定最多读取4个有效字符从根源避免溢出 scanf(%4s, name); printf(你输入的名字是%s\n, name); return 0; }运行结果此时哪怕用户输入了 8 个字符scanf( ) 也只会读取前 4 个有效字符 Zhan并自动补充 \0 存入数组超出的 gSan 会被丢弃完全不会出现数组溢出的问题。另一个正确安全的代码示例#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { // 字符数组长度为11最多存储10个字符1个结束符 char name[11]; // 限定最多读取10个字符避免溢出 scanf(%10s, name); printf(你输入的名字是%s\n, name); return 0; }2.4 返回值scanf( ) 的返回值是一个整数表示成功读取的变量个数• 读取成功返回成功读取的变量数量• 匹配失败、没有读取到任何项返回 0 • 读取过程中遇到错误或文件结尾返回常量 EOF 值为 -1 。代码示例#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { int a 0, b 0; float f 0.0f; // 接收返回值 int r scanf(%d %d %f, a, b, f); printf(成功读取的个数%d\n, r); printf(a%d, b%d, f%f\n, a, b, f); return 0; }运行结果2.5 赋值忽略符用户的输入格式可能不符合我们的预定格式比如我们希望用户输入 2025-03-20但用户输入了 2025/03/20此时 scanf( ) 会解析失败。问题场景比如我们开发一个日期录入功能希望用户按照「年 - 月 - 日」的格式输入日期例如2025-03-20一般的思路如下#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { int year 0, month 0, day 0; printf(请输入年-月-日); // 预定格式用 - 作为固定分隔符 scanf(%d-%d-%d, year, month, day); // 打印读取结果验证是否解析成功 printf(解析结果年%d月%d日%d\n, year, month, day); return 0; }异常现象• 如果用户严格按照格式输入 2025-03-20程序能正常解析输出解析结果年 2025月 3日 20• 但如果用户输入了其他分隔格式比如2025/03/20、2025.03.20、2025 03 20程序就会解析失败只有 year 变量能正确读到 2025moth 和 day 都会保持初始值 0完全没有读取到用户输入的内容。失败原因scanf( ) 会严格匹配格式字符串里的非占位符内容。上面的代码中格式字符串里的 - 是固定匹配字符scanf( ) 读取完第一个整数 2025 后会要求输入缓冲区的下一个字符必须是 - 如果用户输入的是 /、. 等其他字符匹配直接失败scanf( ) 会立刻停止后续的解析工作month 和 day 自然无法读取到数据。解决方案C 语言提供了赋值忽略符 *只要把 * 加在占位符的 % 和类型字符之间该占位符就只会解析对应格式的内容解析完成后直接将内容丢弃不会存入任何变量也不需要为它传递对应的变量地址。回到上面的日期场景我们可以用 %*c 来匹配并丢弃用户输入的任意分隔符无需限定用户必须输入 -兼容所有单字符分隔的日期格式。代码示例#define _CRT_SECURE_NO_WARNINGS 1 #include stdio.h int main() { int year 0, month 0, day 0; // %*c 会忽略任意一个分隔符无论用户输入- / 还是其他字符都能正常解析 scanf(%d%*c%d%*c%d, year, month, day); printf(%d年%d月%d日\n, year, month, day); return 0; }• 格式字符串 %d%*c%d%*c%d 的完整逻辑先读取一个整数年→ 读取并丢弃任意一个字符分隔符→ 读取一个整数月→ 读取并丢弃任意一个字符分隔符→ 读取一个整数日• 无论用户输入的分隔符是 - 、/ 、. 还是空格都能被 %*c 匹配并丢弃程序可以正常解析出年、月、日大幅提升了输入的兼容性。补充拓展赋值忽略符不仅可以用 %*c 匹配单个字符也可以用%*d —— 匹配并丢弃一个整数%*s —— 匹配并丢弃一个字符串