最近在整理C语言基础教程有不少读者问我同一类问题老师上课讲printf和scanf的时候都听懂了代码也能抄对但只要一换题目、一换场景马上就懵。比如格式化字符串里到底该写%d还是%f为什么输入浮点数用%lf输出却用%f还有人问getchar()读到的到底是字符还是整数。其实这不怪大家我当年学到这里也卡了很久。输入输出函数这个主题看起来是最基础的语法点但恰恰因为太基础很多教材都是一笔带过只告诉你函数签名和几个示例从不解释里面的门道。等你真正开始写项目、处理用户输入、对接文件数据时那些被省略的细节全都会变成bug找上门。这篇文章我打算把自己这些年用C语言做实际项目时积累的关于输入输出函数的经验完整梳理一遍。不会只讲函数怎么用而是把每个关键选择背后的原因、容易踩的坑、以及调试技巧都讲透。为什么scanf一定要取地址符为什么混合读取数字和字符时会有诡异行为为什么有时候程序明明没报错但结果就是不对——这些问题都会在这篇文章里找到答案。适合刚学完语法想进阶的初学者也适合写过一段时间但一直对输入输出细节含糊其辞的开发者。1. 为什么说输入输出函数是整个程序的地基很多人觉得输入输出函数简单不过是几个现成的API调用一下就行。但恰恰是这种轻视让无数程序死在了最简单的环节上。我见过不少项目核心算法写得天衣无缝结果在读取输入数据时因为缓冲区问题多读了一行或者因为格式串不匹配直接读到乱码整个程序直接崩溃。1.1 从更宏观的角度重新认识I/O操作输入输出函数表面上是让你和终端打交道实际上它们承担了程序与外界交互的全部通道用户从键盘敲入的数据文件里存储的记录网络连接传来的字节流最终都要通过输入输出接口进入程序内存而程序产生的结果也要通过这些接口送达屏幕、文件或者传输到别的地方。在C语言里输入输出函数家族非常庞大。最常用的是标准输入输出stdio那一套包括格式化输入输出、字符读写、字符串读写。这些函数最大的特点是有缓冲区机制数据不会立刻写入设备而是先在内存缓冲区内攒着等条件满足后再一次性搬运。这就解释了为什么很多程序在运行过程中看不到输出直到程序退出或缓冲区满了之后内容才一次性出现——很多人第一次遇到这种情况都会以为程序死掉了。理解了缓冲区你就理解了输入输出函数的第一个核心概念数据不总是即时的你要知道什么时候数据真正落地。1.2 输入输出函数与其他语言I/O的对比如果你学过Python或者Java再回来看C的输入输出体感会非常明显。Python的input()和print()牺牲了一部分性能换来了类型自动推断的便利你不需要告诉解释器要读入的是整数还是浮点数。但C语言的scanf和printf走的是另一条路你必须在格式串里明确声明类型这意味着编译器没有帮你兜底类型写错了程序不会报编译错误而是在运行时产生难以察觉的逻辑错误。打个比方Python的输入输出像自动挡汽车挂上D挡踩油门走人C语言像手动挡每一个挡位都要你自己挂挂错了发动机就轰鸣但车子不走。自动挡当然方便但你想精确控制发动机转速想好好理解汽车原理手动挡是绕不开的学习过程。C语言输入输出函数的价值就在于它逼着你搞清楚数据在内存里到底是什么形态是几个字节怎么解读。2. 格式化输出函数printf的完整拆解printf恐怕是C语言里学习曲线最平缓但技术含量最高的函数。平缓在于你会写printf(hello)就会用了技术含量高在于它的格式化系统极其复杂年深日久很多奇怪的字符串处理需求你都能在格式串里找到答案。2.1 printf的基本工作流程和参数本质printf的第一个参数是格式串后面跟着可变参数列表。所谓可变参数列表是说同一个函数可以接受任意数量、任意类型的参数。编译器只知道格式串里写了什么不会检查你后面实际传的参数是否匹配——这是危险的来源。格式串中的转换说明以%开头后面跟着可选的标志、宽度、精度、长度修饰符最后是一个转换字符。比如%08.2f这个写法里的每个字符都有意义0表示左边补零8是最小宽度为8个字符.2是小数点后保留两位f表示浮点数。少了任何一个部分格式就变了。我看过很多初学者写的printf(%d, 3.14)输出结果是个莫名其妙的巨大整数。这是因为%d告诉printf要从参数列表中提取一个int类型的值但实际传入的是double类型两者在内存中的表示方式完全不同printf按int的前几个字节重新解释后得到的就是垃圾值。编译器看到类型不匹配时通常只给warning甚至什么都不给程序照样编译通过跑起来结果也不报错这种bug极具迷惑性。2.2 格式串的完整语法要点一个完整的转换说明可以拆成六个部分%[flags][width][.precision][length_modifier]conversion。单独记忆这六个部分很抽象但如果拆开逐个看每个部分实际上都有一个具体问题要回答。width指定的是输出占用的最小字符宽度如果内容不足宽度就会填充空格或零。比如printf([%10d], 42)输出[ 42]42靠右对齐占了两位左边补了8个空格凑够10位宽。加了-标志变成%-10d就变成靠左对齐右边补空格。precision的含义取决于转换字符类型对整数来说它指定了最少输出的数字个数比如%.5d配合值42会输出00042对浮点数来说是小数点后的位数%.2f就是经典的两位小数对字符串来说是最大输出字符数%.5s配合hello world只输出hello。这些规则经常让人混淆但我发现用一个小例子去记就很清晰precision的基本含义是最少或最多数字/小数位数。长度修饰符h、l、ll用来指定参数的类型尺寸。对整数hd表示shortld表示longlld表示long long对浮点数lf是一个很容易让初学者困惑的地方在printf里float类型的参数会被自动提升为double所以%f和%lf在输出时效果完全相同。但在scanf里恰恰相反%f读入float*%lf读入double*混用就会出大问题。这是日常开发中最高频的类型错误之一。2.3 printf返回值与其他冷门特性很多教材不会讲printf有返回值但实际在写程序时这个返回值很有用。它返回的是成功输出的字符总数可以用来排查输出是否被截断。比如你想往文件里写内容写完之后检查一下返回值是否等于你预期的长度能立刻发现问题。另外printf是线程不安全的多个线程同时往同一个文件流输出时会出现交错内容在多线程程序里要么加锁要么用fprintf配合单独的FILE*。还有个冷门技巧printf支持动态宽度和精度用*占位符代替数字实际值从参数列表中获取。比如printf(%*.*f, width, precision, value)运行时你再决定宽度和精度这在做报表对齐时非常灵活。但如果参数位置搞错了结果会乱套——格式串里的*从左到右依次取参数这个顺序很多人不知道。3. 格式化输入函数scanf的深层机制相比之下scanf的坑比printf多得多。它涉及缓冲区、指针、类型提升和空白符处理等多个复杂的机制。我辅导过的初学者里十个有八个在scanf上出过问题而且很多人出了问题之后不知道怎么排查只能反复修改格式串碰运气。搞清楚scanf的工作方式这些坑其实是可以完全避开的。3.1 scanf为什么必须传地址scanf的设计初衷是通过参数把读取结果传回去这和普通函数的传值模式完全不同。普通函数如int add(int a, int b)参数在函数内部只是局部拷贝改了不影响外面的变量。但scanf要改变你定义的变量的值因此必须拿到变量在内存中的地址也就是指针才能通过指针修改变量的内容。所以scanf(%d, num)里的是不可省略的。漏掉它程序会把num的值当作地址往一个未知的内存位置写入数据。这种行为是未定义的可能导致段错误直接崩溃也可能静默地覆盖了别的变量的内存让程序在很久之后出现奇怪行为。没有任何编译错误提示运行时的表现还不固定这非常难排查。如果你用的是数组名来读取字符串是另一个特例数组名本身就是地址不需要再加。在调试此类bug时gdb能帮大忙在scanf调用处设置断点用print num查看变量地址再用x命令查看内存内容你会直观看到数据写入了和预期不同的位置。3.2 scanf的匹配规则与空白符处理scanf的读取并不是直接读取键盘输入的它的工作方式是从输入缓冲区读取字符然后根据格式串尝试匹配。格式串里的普通字符不是%开头的要求输入中必须存在对应的字符否则匹配就失败。空格、换行、制表符这些空白符比较特殊在格式串中出现意味着跳过零个或多个空白符。更让人困惑的是即使格式串里没有写任何空白符某些转换说明也会自动跳过输入中的前导空白符。数字类转换%d、%f会自动跳过空白%c则不会%s会自动跳过前导空白但遇到空白就停止读取%[集合匹配也有自己的规则。记住这样一个要点只有%c和%[不会跳过前导空白其他绝大多数转换说明都会自动处理。这解释了为什么scanf(%d, num)之后紧接着scanf(%c, ch)会读到换行符而不是你期望的字符。如果你希望忽略输入中某个特定字符可以在格式串里显式跳过。比如要跳过冒号可以写scanf(%d:%d, a, b)输入10:20时两个值都能正确读取如果写成scanf(%d %d)输入10:20只能成功读取到1020会因为冒号不匹配而留在缓冲区。3.3 输入数据和格式不匹配时的处理当输入的数据与格式串要求不匹配时scanf会停止读取并且将第一个不匹配的字符留在缓冲区。这是很多循环读取代码的致命伤while(scanf(%d, n) ! EOF)如果用户输入了字母a循环第一次失败a留在缓冲区第二次循环继续用%d去读还是读到a再次失败……无限循环程序卡死。解决这个问题有两个思路。第一是检查scanf的返回值它返回成功匹配的参数个数如果返回值小于预期说明有数据没被正确读取第二是主动丢弃缓冲区中多余的内容。常见的清空缓冲方法有while(getchar() ! \n);这行代码会不断读取字符直到遇到换行把上次遗留的所有字符都吞掉然后再继续新的读入。虽然性能上有点浪费但在交互式程序中非常皮实。另外还要注意scanf的返回值只表示成功匹配的参数个数不等于实际读取的条目数。比如scanf(%d %d, a, b)输入10 abc返回值是1因为第二个%d匹配失败了abc映射到整数失败。很多老手会根据返回值判断后续流程但新手往往直接忽略返回值——安全编码的习惯是每次都检查。4. 字符与字符串输入输出getchar、putchar和gets家族的细节格式化函数负责处理有类型的数据字符和字符串则常常用另外一组函数。这组函数数量多命名相似彼此之间的细微差别很容易让人记混。4.1 getchar的返回值为什么是intgetchar()在教材里被定义为读取一个字符但它的返回值类型却是int函数原型是int getchar(void)。不少初学者就困惑了读字符返回int什么意思关键原因和文件结束符相关。当到达文件末尾或发生读取错误时getchar()返回一个特殊值EOF这个值通常定义为-1。char类型可能是有符号的也可能是无符号的如果用char ch getchar()来存放返回值某些平台上char能表示的最大值不足以覆盖-1。虽然当前的char处理了-1的情况但作为习惯和标准做法用int接收getchar()的返回值更为稳妥否则到处都可能出现判断EOF异常的边界bug。另外getchar()从缓冲区读取字符时如果缓冲区里没有内容它会阻塞等待用户输入。在交互式的命令行程序里这是正常流程但在需要非阻塞输入的场景比如游戏循环中监听按键就需要额外的手段处理了。4.2 gets的隐患与fgets的正确姿势gets()在C11标准里已经被正式移除因为它完全没有边界检查读取任意长度的行到固定大小的缓冲区里字符串超出缓冲区长度就会造成缓冲区溢出。经典的黑客攻击手段里就有利用gets溢出改写返回地址的例子。即使你现在的代码只是自己练习用也建议从一开始就别碰它。安全的替代品是fgets(char *s, int size, FILE *stream)。它最多读取size-1个字符然后自动添加字符串结束符\0因此永远不会越界。第三个参数传入stdin即可从标准输入读取。需要注意如果fgets读到了一行不足size-1个字符时会把换行符\n也读进缓冲区并作为字符串内容后续如果你用strlen计算长度会发现多算了一个字符。处理办法很简单读取后检查最后一个字符是不是换行符是的话就把它替换为\0。这个处理经常被人忽略尤其是做字符串比较时最坑输入的内容和你预期的字符串总是差一个看不见的换行符怎么比较都不相等。排查半天会发现是这个原因而且不止一次遇到。4.3 puts与printf的选择策略puts和printf都能输出字符串但差异其实不小。puts在输出字符串后会附加一个换行符这个行为对逐行输出很方便但它不接受格式串无法做格式化拼串。还有一个容易忽略的事情是puts遇到字符串内部的\0就会停止因此无法输出包含嵌入式空字符的数据。在处理纯文本逐行输出时puts足够且更简洁在需要拼合多个变量成一行输出时只能用printf。实际项目中这两种选择带来的性能差异可以忽略不计代码的可读性和意图表达才是更重要的衡量标准。我个人的习惯是固定字符串整行输出用puts带变量或需要对齐时用printf。不要频繁在两种风格间切换保持一致性能让代码更好维护。5. 缓冲区的秘密行缓冲、全缓冲与fflush前面反复提到缓冲区但一直没有系统讲。输入输出函数的很多诡异行为根源都在缓冲区工作机制上。搞明白它很多玄学bug就能变成可预期的行为。5.1 标准流与缓冲类型对照C语言里每个打开的文件流都有自己的缓冲区标准输入stdin、标准输出stdout、标准错误stderr三种流是程序启动时自动打开的。它们的缓冲策略并不相同具体情况取决于平台、连接的是不是终端设备。一般而言标准输出连接到终端时是行缓冲缓冲区遇到换行符就把内容刷出去如果输出重定向到文件就变成全缓冲攒满一个缓冲区的大小才写盘一次。标准错误则通常是全缓冲的因为错误信息需要立刻显示不能等缓冲区满了才输出——程序如果已经崩了缓冲区里的内容就永远刷不出来了。搞清楚这三者差异你就理解了为什么程序崩溃时最后一条printf的内容经常看不到。5.2 缓冲区在用户视角的体验差异从用户角度缓冲机制最直观的体验就是为什么程序运行中没有及时输出内容。比如一个倒计时程序每秒钟循环打印一个数字如果不用换行符printf之后没有换行也没有显式刷新终端上你什么都看不到数字被憋在缓冲区里等到循环结束时一次性输出全部数字看起来就像程序卡了很久突然蹦出一堆内容。这种问题最常见的解决办法是在输出后加换行符或者调用fflush(stdout)强制刷新缓冲区。fflush()用来立即将缓冲区的内容发送出去。对于输出流调用fflush(stdout)可以立刻清空缓冲区对输入流比如stdin在部分实现上fflush可以清空输入缓冲区但这在标准上属于未定义行为。想安全地从输入缓冲区丢弃残留字符还是用前面说的while(getchar() ! \n);循环。不少人在平台相关的代码里见过fflush(stdin)能工作只是特定编译器的扩展行为换一个平台很可能就失效所以代码里不应该有这种写法。5.3 缓冲区刷新的完整触发条件缓冲区内容什么时候真正到达目的地新手总结不出清单我把常见触发条件列一下缓冲区满时自动刷新遇到换行符仅限行缓冲模式显式调用fflush程序正常退出return或exit标准库内部在某些场景自动刷新。需要特别注意的是程序因段错误、未捕获信号等原因异常终止时缓冲区里的内容可能直接丢失这就是为什么在排查崩溃问题时你最后打印的那条调试信息经常看不到。一个很实用的经验是在调试可疑代码段的关键位置加上fflush(stdout)往往能帮你准确定位最后一次正常执行的边界。6. 从终端到文件fprintf、fscanf与freopen的进阶用法前面的内容都围绕标准输入输出但实际项目中大量数据来自文件。C语言的文件输入输出函数和标准输入输出的用法几乎一样只是把操作对象从终端换成了FILE*指针。这种统一设计是C语言实用性的体现。6.1 文件流的打开模式与选择fopen的第二个参数是模式字符串常见的有r只读、w只写会截断已有文件、a追加、r读写、w读写并截断、a读和追加。选错模式会导致严重的数据丢失用w打开一个已有文件会立刻把文件清空这一步是不可恢复的没有后悔药。我见过有人在清理临时文件的代码里因为模式拼写错误把真正重要的数据文件给截断成零字节这种事故非常惨痛。养成习惯只读数据永远用r要新建或覆盖时用w要保留原有内容时用a。另外fopen成功后要检查返回值是否为NULL。文件不存在、权限不足、磁盘满都会导致返回NULL但很多初学者拿到NULL还继续往下用直接导致段错误。写文件代码的第一步一定是对FILE*判空这一步不能省。6.2 fprintf和fscanf的参数对应关系fprintf(FILE *stream, const char *format, ...)与printf的差别仅在于多了第一个FILE*参数。fscanf(FILE *stream, const char *format, ...)同理。这种设计使得代码可以在标准输入输出和文件流之间无缝切换——想从文件读取时把第一个参数换成fp想输出到文件时同理。也正因为函数形态一致你在printf和scanf上积累的格式串经验可以直接平移到文件场景。有一个细节值得注意fscanf从文件读取失败时它读取的部分数据不会推送回文件下次再读取时位置已经前移了。如果你在解析一个被损坏的文件读取到一半的字段会让后面的解析全部错位。应对方法很简单每次都检查fscanf的返回值如果返回值不等于你期望的匹配数立即停止解析并报告错误不要贸然用半截数据继续计算。6.3 freopen重定向标准流有时候你希望程序的其他部分继续使用printf和scanf但把实际的输入输出对象换成文件。freopen就是为此设计的把stdin或stdout重新绑定到文件。比如在调试一个大批量数据处理程序时你不想每次都在终端敲一堆数据可以先用freopen(input.txt, r, stdin)把输入重定向到文件代码里照旧用scanf读取完全不用改逻辑。这个方法在做在线评测、批量测试时尤其方便程序和输入数据解耦让同一套命令可以反复用不同数据跑。freopen的返回值也需要检查失败时它会返回NULL且原来的流仍保持打开状态此时继续读写会产生诡异结果最好直接跳过文件处理分支并把错误报告给用户。7. 输入输出实战案例一个完整的命令行交互程序理论讲得差不多了直接看一个综合案例。这个程序要做的事很简单从终端循环读取学生姓名和成绩存入数组最后输出所有记录并计算平均分。但我会把之前提到的各种知识点都编织进去重点展示在真实代码里这些细节是怎么落地的。7.1 功能设计程序的基本流程提示用户输入学生人数循环读取每个学生的姓名和成绩读取完成后展示所有记录并计算平均分。这个流程涵盖了scanf读取整数、fgets读取字符串、printf格式化输出、缓冲区残留问题的处理以及返回值检查等所有关键点。7.2 第一版代码及问题演示先写一个有问题的版本这个版本非常典型几乎是初学者错误的大集合#include stdio.h #define MAX_NAME_LEN 50 #define MAX_STUDENTS 100 int main() { int count, i; char name[MAX_NAME_LEN]; float score; printf(请输入学生人数: ); scanf(%d, count); for (i 0; i count; i) { printf(请输入学生姓名: ); fgets(name, MAX_NAME_LEN, stdin); printf(请输入成绩: ); scanf(%f, score); } return 0; }运行一下就会发现两个明显问题输入完人数后第一次fgets读到的内容为空循环第二次时fgets似乎跳过了输入直接读取了上一次残留的换行符。这两个问题的根源都是缓冲区残留scanf(%d, count)在读取整数时会在输入缓冲区里留下一个换行符这个换行符随后被fgets读走了。7.3 修复版完整代码修复思路和最终代码就是下面的版本每一处修改我都会在注释里解释清楚#include stdio.h #include string.h #define MAX_NAME_LEN 50 #define MAX_STUDENTS 100 typedef struct { char name[MAX_NAME_LEN]; float score; } Student; static void clear_input_buffer() { int c; while ((c getchar()) ! \n c ! EOF) { // 丢弃缓冲区中剩余的字符 } } int main() { int count, i; float total 0.0f; Student students[MAX_STUDENTS]; printf(请输入学生人数: ); if (scanf(%d, count) ! 1) { printf(输入格式错误程序退出。\n); return 1; } if (count 0 || count MAX_STUDENTS) { printf(人数超出有效范围。\n); return 1; } clear_input_buffer(); for (i 0; i count; i) { printf(请输入学生姓名: ); if (fgets(students[i].name, MAX_NAME_LEN, stdin) NULL) { printf(读取姓名失败。\n); return 1; } size_t len strlen(students[i].name); if (len 0 students[i].name[len - 1] \n) { students[i].name[len - 1] \0; } printf(请输入成绩: ); if (scanf(%f, students[i].score) ! 1) { printf(成绩格式错误。\n); return 1; } clear_input_buffer(); total students[i].score; } printf(\n 成绩单 \n); for (i 0; i count; i) { printf(%-20s %6.2f\n, students[i].name, students[i].score); } printf(\n); printf(平均成绩: %.2f\n, total / count); return 0; }这里每一处关键设计都有原因。读取人数后调用clear_input_buffer()把scanf留在缓冲区的换行符清掉否则后面fgets会读到空行。fgets读取姓名后手动去掉末尾换行符不然输出时会出现奇怪的换行错位。每次读取成绩后再次清理缓冲区保证下一轮循环的fgets不会读到残留字符。scanf的返回值全部做了检查防止用户输入非数字时程序继续用垃圾值运行。姓名用结构体存储不再用单数组这样后续扩展会更方便。实际运行这个版本输入输出交互就很干净了无论你手速多快多慢程序都不会出现吞掉一行的怪问题。8. 输入输出调试的实用套路面对输入输出相关的bug老手和新手的差异很大程度体现在排查路线上。老手心里有一套固定的检查清单按顺序排查很快就能锁定问题。新手则是看哪不对就改哪儿改来改去碰运气。下面分享几个我常用的调试套路。8.1 最常被忽视的三个调试手段第一个手段是查看scanf/printf的返回值。在关键输入后加一行printf(read count %d\n, ret)你立刻能知道是否成功读取、读取了几个字段。这个信息在交互式程序调试时极其有用远比猜数据是否进对了变量更直接。第二个手段是打印变量的地址和值。用printf(address of num %p, value %d\n, (void*)num, num)确认数据是否真的写入、地址是否合理。这个习惯在检查指针相关问题时救过我很多次。第三个手段是直接输出缓冲区剩余内容。在现象诡异时用printf(remaining: [%c]\n, getchar())看看缓冲区里滞留了什么字符。我见过不少卡住的交互程序最后都是发现缓冲区里堆了一大堆换行符和空格程序在无限循环里不断读到它们。8.2 常见输入输出问题速查最近几年我在回答问题和review代码时积累了一些高频问题整理成速查表形式现象可能原因解决思路scanf读多个值时程序跳过输入前一次输入留下了换行符或空格在需要的地方清理缓冲区fgets读到空白行scanf残留换行符被fgets读取在scanf后清空输入缓冲区字符串比较老是失败fgets带入了末尾换行符读取后去除末尾\nprintf输出顺序错乱多线程共享文件流无锁保护使用fprintf并加锁程序正常退出前看不到部分输出缓冲区未刷新换行输出或用fflushprintf(%f, double)结果不对格式串与参数类型不匹配严格按类型对应格式串输入文件最后一行读不到最后一行没有换行符用feof和fgets配合判断8.3 用gdb和strace辅助排查当基础检查解决不了问题时就该上调试工具了。gdb里设置断点在scanf处用next单步执行再print变量看值是否变化这是最直观的做法。strace则能从系统调用层面查看程序的读写行为strace -e read,write ./program能看到每一次read和write系统调用的字节数和内容判断问题的根源在用户态还是内核态。这两个工具一起用几乎所有输入输出问题都能被定位到行级别。9. 关于输入输出函数的一些个人经验和体会写了这么多年C程序我对输入输出函数最大的体会是看似最基础的函数恰恰是数据正确性能否保证的关键。很多项目后期暴露的诡异问题回溯到最后都是早期代码里对输入数据的处理过于草率——要么没检查返回值要么没有处理缓冲区残留。还有一个常被忽略的点是输入输出函数的相互作用。比如你用setvbuf调整了缓冲区大小或者在不同函数之间混合使用printf与底层write调用都会引发预想不到的顺序变化。这种问题往往只在特定平台和特定数据量下复现排查成本极高。所以我的建议是在项目里统一输入输出的使用风格不要在同一个程序里一会儿用printf一会儿用fwrite输出同一路数据除非你非常清楚两者缓冲区机制的区别。最后想分享一句我经常对读者说的话处理输入输出时不要把用户当作用户要把用户当做一个不可靠的输入源。你程序里的每个scanf、每个fgets都要预设用户可能输入任何内容、可能少输入、可能多输入、可能输入非法字符。一旦抱着这个心态写代码你就会自然而然地检查返回值、清理缓冲区、处理边界情况——这些习惯积累起来程序的健壮性就有了保障。输入输出函数在不同的程序里表现差异很大但只要你把这些底层机制理解透了无论换什么平台和环境都能很快适应和排查问题。