
很多人在 Visual Studio 里写 C 语言第一次碰到“C4996”都是同一种表情代码明明是按教材敲的怎么一编译就冒出一行红字说什么 scanf 不安全建议换 scanf_s。我当年第一次见到这个报错也是一头雾水还以为是自己的 IDE 配错了后来折腾了好几次才明白这根本不是什么棘手的语法错误而是 VS 编译器基于安全策略给经典 C 函数套上的“紧箍咒”。这篇内容我会把 C4996 的来龙去脉、几种主流处理方式、实操过程中的坑都讲清楚无论你是刚入门的新手还是被这个报错折磨到想直接换编译器的老同学照着下面的思路处理基本都能顺利过关。1. 认识 C4996这不是“错误”而是编译器在跟你说“别用旧函数”1.1 C4996 到底是什么身份在 Visual Studio 的编译输出窗口里你会看到类似下面的信息error C4996: scanf: This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.注意开头是error C4996但 C4996 的更准确身份是“弃用警告deprecation warning”。它跟真正的语法错误、类型不匹配错误不一样代码本身是可以编译的逻辑也不一定有问题。VS 之所以把它显示成 error一部分原因是新版项目模板默认开启了“将警告视为错误Treat Warnings As Errors”以及安全开发生命周期检查SDL checks这种配置下警告会升级成错误导致编译直接失败。打个比方C4996 就像你在一条老路上开车导航提醒“前方道路已废弃建议绕行新路”。导航并没有说你超速、没说你违章只是告诉你这条路虽然还能走但有风险换一条更安全的路更好。如果你非要走也不是不行但导航会反复提醒你甚至在某些强制设置下直接不让你导航过去。C4996 并不只针对 scanf像 strcpy、strcat、fopen、gets 这类大家非常熟悉的经典函数在 VS 里都会触发同样的提示。共同点在于这些函数在历史上都曾因为缺少边界检查或输入校验被广泛用于制造缓冲区溢出攻击所以编译器把它们列入了“安全黑名单”。1.2 为什么 scanf 会成为“重点盯防对象”scanf 的问题并不是它不能工作而是它工作得太“信任用户”了。它从标准输入读取数据按格式化方式写入对应变量的内存地址但 scanf 本身不检查目标缓冲区有多大也不检查用户实际输入的内容是否会超出预期范围。你声明了一个char buf[10]用户输入 30 个字符scanf 照样往那块内存里塞多出来的部分直接越过数组边界写进相邻内存区域这在 C 语言里叫缓冲区溢出轻则导致程序行为异常、崩溃重则可能被恶意利用来执行攻击代码。VS 团队为这类函数提供了一批带_s后缀的安全替代版本比如scanf_s、strcpy_s、fopen_s。核心改进是要求调用者额外提供缓冲区的大小让函数在写入前能够做一次长度校验从而抑制溢出风险。这就是为什么 VS 见到 scanf 就要跳出来“拦一道”它不希望你在不知情的情况下把潜在的安全漏洞写进程序里。1.3 关于 scanf_s 不是标准函数的常见争议很多同学会疑惑我用的明明是教材里的 C 语言标准函数为什么 VS 非要说它不安全这里补一句背景scanf 是 C 标准库函数几乎是所有平台都支持的而 scanf_s 是 Visual Studio 提供的扩展函数并不是标准 C 的一部分。你在 Windows 上写scanf_s可以编译但把代码放到 Linux 或者 macOS 的 GCC 环境里编译器很可能直接报“undefined reference to scanf_s”。这一点很重要因为很多学校的作业、在线判题系统用的是 GCC/Clang 环境你如果为了在 VS 里消灭 C4996 而把所有 scanf 改成 scanf_s交上去反而可能编译失败。所以选择哪种方案不能只看“哪个不报错”还得看你平时运行代码的目标环境。这个问题没有绝对正确答案只有“当前场景下的合适方案”。2. 解决方案全景四条路各有各的适用场景2.1 方案一把 scanf 改成 scanf_s最直观、也最贴合编译器提示的做法是使用安全版本。对于普通整数的读入改动几乎无感scanf(%d, num);改成scanf_s(%d, num);表面上只多了个_s大多数数值类型读入时参数不用变。但一旦读入字符串事情就不一样了char name[20]; scanf(%s, name);改成char name[20]; scanf_s(%s, name, (unsigned)sizeof(name));这里多出来的第三个参数就是缓冲区大小函数需要用这个值来判断最多能写多少字符。漏掉这个参数编译器仍然会报错误填错了比如填成比实际缓冲区更大的数字同样可以造成溢出安全版本也就名存实亡了。我见过不少人改完scanf_s之后高高兴兴编译通过结果一到别的环境就凉了。所以如果你主要是用 VS 做练习用 scanf_s 没问题如果以后要走算法竞赛、在线判题或者需要跨平台开发那就得谨慎考虑别让自己被某个编译器的扩展函数绑住手脚。2.2 方案二在代码顶部定义宏把警告关掉VS 自己提示了“To disable deprecation, use _CRT_SECURE_NO_WARNINGS”也就是说可以通过定义一个宏来告诉编译器“我知道这些函数旧但我就是要用你别再拦我了。”最简单的方式是在每个源文件的最顶部也就是所有头文件之前写上#define _CRT_SECURE_NO_WARNINGS #include stdio.h注意顺序很关键。必须放在任何预处理指令之前尤其是要在标准头文件包含之前完成定义否则编译器在处理头文件里的函数声明时不知道你已经关闭了警告后面还是会报错。很多人在文件第一行写了宏定义却仍然看到 C4996多半就是因为头文件的#include出现在宏定义之前。如果你有一整个项目几十个源文件一个个文件去加宏显然太蠢。这时候直接在工程属性里统一设置更合适右键项目 → 属性 → C/C → 预处理器 → 预处理器定义点下拉框选择“编辑”添加一行_CRT_SECURE_NO_WARNINGS确认后项目内所有源文件编译时都会携带该宏定义一次性解决。2.3 方案三调整项目级的安全检查选项前面提过C4996 在有些工程里显示为 error不是因为它本身是错误而是工程开启了“将警告视为错误”。如果你用的还是新版 VS 默认创建的控制台项目很可能是开启了 SDL 检查导致告警升级为错误。要关闭这个升级机制可以打开项目属性 → C/C → 常规 → 将“安全开发生命周期检查SDL checks”设为“否”。这样 C4996 会降级回普通警告编译可以继续程序能正常运行但输出窗口里还是会看到那条黄色 warnings看着不太舒服却不影响执行。我对这个方案的态度比较保守。把 SDL 关掉确实能快速跑通但它相当于把整个项目的安全检查级别调低了不只是 scanf 的告警被弱化其他更多有意义的警告也可能被一同抑制。降低警钟音量不等于问题不存在作为学习过程中的临时手段可以但不能养成习惯。2.4 方案四换一种更值得推荐的输入方式与其纠结 scanf 和 scanf_s 哪个好不如回头看看有没有更通用的替代方案。C 语言里读字符串fgets 就是一个远比 scanf 可靠的选择char line[100]; fgets(line, sizeof(line), stdin);fgets 的第二个参数就是缓冲区最大长度它从输入流中最多读取 n-1 个字符并自动补上字符串结束符\0。这从根本上杜绝了溢出。缺点是它会把换行符也读进来你需要自己处理一下行尾的\nline[strcspn(line, \n)] \0;如果你要读整数不想用 scanf 家族可以先用 fgets 把一行读进来再用sscanf或者strtol去解析。这样你还能对输入格式做更多控制比如检查用户输入是否真的是数字。这个路子写起来比 scanf 啰嗦但安全性和跨平台兼容性都更好长远来看很值得掌握。3. 实操演示从报错到解决完整走一遍3.1 先复现同一个报错现场我在 VS 中新建一个 C 语言控制台项目空项目然后把下面这段代码写进去#define _CRT_SECURE_NO_WARNINGS #include stdio.h int main() { int age; char name[20]; printf(请输入年龄); scanf(%d, age); printf(请输入姓名); scanf(%s, name); printf(年龄%d姓名%s\n, age, name); return 0; }如果直接把宏定义去掉重新编译会看到错误列表里明确列出 C4996。不过请注意我这里的演示保留了宏之后为什么不报错因为我把宏写在了#include stdio.h的上方这个顺序是关键。如果你像下面这样把宏写到 include 后面则即使宏存在也可能会看到报错#include stdio.h #define _CRT_SECURE_NO_WARNINGS原因前面说了scanf 的函数声明在头文件阶段就已经被编译器“标记”为不安全你后面才定义宏编译器在处理头文件的时候并不知道你要关掉告警。3.2 实际动手宏定义为什么比想象中敏感我在自己电脑上测试时有意把宏定义放在了#include stdio.h之前、之后各试了一次结果确实像上面所说宏位置不对警告照旧。有些同学觉得“我明明写了 #define怎么还报错”大概率就是位置顺序的问题。还有一点值得注意宏定义不要写在函数内部哪怕在函数顶部第一行也不行。_CRT_SECURE_NO_WARNINGS必须在文件作用域、预处理阶段就生效放在main函数里是晚了实际效果基本等于没写。另外如果你是在同一个源文件的多个头文件之后才加上宏定义也会失效。我习惯把宏定义和#include一起放在文件最顶端永远放在第一个而不是仅仅放在#include stdio.h前面。这样无论后面包含多少个头文件宏都能优先生效。3.3 实际动手用 scanf_s 替换时的现场记录为了对比我保留了一段原来的 scanf 代码另存一个新源文件把输入部分改成 scanf_s#include stdio.h int main() { int age; char name[20]; printf(请输入年龄); scanf_s(%d, age); printf(请输入姓名); scanf_s(%s, name, (unsigned)sizeof(name)); printf(年龄%d姓名%s\n, age, name); return 0; }这次编译结果没有 C4996。需要注意的是字符串那一行的(unsigned)sizeof(name)我专门做了强制类型转换因为 scanf_s 期望的这个长度参数类型是unsigned不同平台下sizeof返回类型可能与之不完全一致加上强制转换能避免出现关于“有符号/无符号不匹配”的新警告。对于多个字符串读取比如一次读两个名字逻辑也一样每个字符串都要单独传自己的缓冲区长度char first[10], last[10]; scanf_s(%s, first, (unsigned)sizeof(first)); scanf_s(%s, last, (unsigned)sizeof(last));3.4 实际动手通过项目属性全局设置最快如果不想改代码里的任何内容只想让现有项目里的告警消失我推荐把宏加到项目属性里。操作路径如下在解决方案资源管理器中右键项目名称选择“属性”。在左侧选择“C/C” → “预处理器”。点击预处理器定义右侧的下拉箭头选择“编辑”。在打开的窗口中添加_CRT_SECURE_NO_WARNINGS每一行一个宏。点击确定。建议把顶部配置从“Debug”切换为“所有配置”平台从“Win32”切换为“所有平台”这样以后切换 Debug/Release 或者 32/64 位编译时不需要再设置一遍。重新编译。有个小坑如果你在属性管理器里改但没有选“所有配置”那 Debug 下生效了 Release 下编译可能仍然报错。初学者很容易漏掉这一点看到 Debug 能跑就觉得万事大吉等以后切换 Release 编译时又被同样的问题卡住。3.5 验证是否真的解决改完之后重新生成解决方案错误列表窗口里如果清空了说明问题已解决。如果你想确认不是缓存导致的假象可以执行“生成” →“清理解决方案”再“重新生成解决方案”保证整个项目从头编译一遍。输出窗口不再出现error C4996程序也能正常接收输入并打印结果这一步就算完成了。4. 理解背后的“安全焦虑”才能做出更理智的选择4.1 缓冲区溢出不是危言耸听为什么 VS 对 scanf 如此“反感”因为你写的程序一旦发布接收的就是来自外部世界的真实输入用户可能不小心输入超长内容也可能有恶意使用者故意构造数据来攻击你的程序。缓冲区溢出是最经典的内存破坏型漏洞之一。经典网络攻击里攻击者通过向程序输入超长字符串覆盖返回地址从而劫持程序执行流程这种操作在 C 语言里时有发生。把 scanf 和缓冲区的关系类比成“用手接水”scanf 就是用两只手去接水管里喷出来的水但你的手能捧多少水scanf 不管也不看水多了就流到地上、渗进地板。scanf_s 则先递给你一个固定容量的水桶告诉水管“最多只能注这么多”想多注管子会自动停。对编译器开发者来说在源头就要求开发者提供这个容量信息比事后靠操作系统防御更可靠。4.2 标准之争 Boxscanf_s 到底该不该用用 scanf_s 还牵扯到一个“标准洁癖”问题。C 语言标准是 ISO/IEC 9899scanf 是标准库的一部分scanf_s 并不是标准要求提供的函数它属于“带边界检查”的扩展做法。因此很多人认为在教学中坚持用 scanf 是维护代码的可移植性换 scanf_s 反而是把编译器个性带进了“标准 C”。这个争论本身没有非黑即白的答案。我的实际经验是要看你的下一站去哪。如果你写的是学校作业、课程设计、VS 内置练习用 scanf_s 完全无妨如果你的代码需要交到在线判题系统、跨平台编译那就老老实实用 scanf并结合宏定义来处理编译告警。比函数选型更重要的是你是否真正理解输入边界检查这件事。理解了它将来无论在哪个平台写代码你都会自觉做防御性编程。4.3 学习阶段到底该不该“屏蔽警告”我给初学者的建议是不要一开始就把所有警告当成“烦人的弹窗”直接关掉。C4996 是一次很好的安全启蒙它逼着你去了解缓冲区、指针、字符串存储这些 C 语言核心概念。先用_CRT_SECURE_NO_WARNINGS让程序跑通这没问题但跑通之后务必再去查一查“为什么这个函数不安全”“scanf_s 多出的参数是什么含义”“缓冲区溢出到底能造成多大危害”。带着这些思考写完几个练习项目基础就会比当年那些一关了之的同学扎实很多。5. 常见问题速查与踩坑实录5.1 为什么我加了宏定义还是报错最典型的原因就是我反复强调的那两个宏定义位置不在文件最顶部或者宏定义只加了一个源文件、其他文件没加。还有一个隐蔽原因是文件编码问题——比如源文件被保存为带 BOM 的 UTF-8 或 GB 编码如果 BOM 影响了第一个字符预处理可能不能识别首行宏。这种情形较少见如果你的代码是从某些网页直接复制粘贴的建议删掉前几行重新敲一遍。5.2 使用 scanf_s 时提示“未找到重载函数”或“参数不足”这说明你调用 scanf_s 的方式不对。数值类型读取时scanf_s(%d, num)和 scanf 一致但读取字符串时必须传入缓冲区长度参数。有些教程里写scanf_s(%s, str, sizeof(str))不够严谨在 x64 平台可能产生“从 size_t 转换到 unsigned 的警告”。建议写成(unsigned)sizeof(str)既保证编译安静也让意图更明确。5.3 关闭了 SDL 检查但错误并没有消失SDL 检查影响的是“警告是否被当成错误”这件事它本身并不禁止 scanf。如果你没加_CRT_SECURE_NO_WARNINGS关闭 SDL 后C4996 会从 error 降级为 warning错误列表可能不再显示红色 error但仍会有黄色警告。想在输出里彻底看不见它还是要配合宏定义或者换用 scanf_s。5.4 项目属性里找不到“预处理器定义”出现这种情况多半是因为你没有选择 C/C或者当前项目类型不是原生 C 项目。如果你创建的是 C# 项目那当然没有这个选项如果是 Visual Studio 里新建的“空项目”“控制台应用”则在项目属性的 C/C → 预处理器下一定能找到。另外提醒一下在属性页里搜索框输入“预处理器”也可以快速定位。5.5 常见方案优劣势对比速查方案改动成本可移植性安全性适用场景使用 scanf_s低但要改调用参数差gcc/clang 不支持较高仅在 VS 里做练习宏定义 _CRT_SECURE_NO_WARNINGS极低不影响代码本身不提升也不降低快速跑通、在线判题关闭 SDL 检查极低不影响代码本身整体告警被弱化临时性绕过使用 fgets/strtol 等替代较高好高长期工程、跨平台5.6 我的一个实际小经验如果是在 VS 里学习数据结构或者 C 语言基础我个人的习惯是保留_CRT_SECURE_NO_WARNINGS宏继续使用 scanf原因是很多教材和伪代码都用 scanf这样做复现和对照更省事但如果我想把某个功能封装成模块给其他人用或者去写一个可能会被真实用户输入“攻击”的软件我绝对会优先选择带边界检查的输入方式而不是依赖宏来掩盖警告。最后再分享一个小技巧如果你使用的编辑器支持“代码片段”功能可以把下面这段存成控制台项目的新文件模板以后每次新建文件都会自动带上宏省得反复添加#define _CRT_SECURE_NO_WARNINGS #include stdio.h #include stdlib.h #include string.h记住这个宏的适用范围仅限于当前 TU翻译单元即当前源文件以及它所包含的头文件它并不会影响链接器和其他源文件。想要一个项目统一关闭这些警告一定要回到项目属性里去配置而不是在每个文件里各自为政。搞清楚这一点C4996 这个故事就算彻底翻篇了。