☰
VS中scanf报错C4996不安全?三种方案彻底解决
2026/10/1 14:15:07 网站建设 项目流程

1. 报错原因:为什么VS非要对scanf下狠手

很多C语言初学者刚装上Visual Studio,照着教材敲了第一行scanf("%d", &a);,一编译,直接红波浪线+错误列表弹出一句“'scanf': This function or variable may be unsafe. Consider using scanf_s instead.”。这时候整个人都是懵的,教材上明明这么写,怎么到自己电脑上就成"unsafe"了?

1.1 报错现象截图式描述

打开VS新建一个控制台应用,输入:

#include <stdio.h> int main() { int a; printf("请输入一个整数:"); scanf("%d", &a); printf("你输入的是:%d\n", a); return 0; }

按Ctrl+F7编译或者按F5运行,VS会直接提示编译错误。仔细看输出窗口的内容,通常长这样:

错误 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.

这里有个很关键的细节:C4996这个错误编号。C4996不是语法错误,你在其它编译器(比如Linux下的GCC、Dev-C++自带的MinGW)里编译同样代码,完全不会报错,程序照常运行。唯独在VS里,微软会把这个当成错误拦截下来。这就让不少人误以为是自己代码写错了。

1.2 VS为什么这么“多管闲事”

事情要从C语言的缓冲区溢出漏洞说起。scanf是个不安全的函数,它只管往你指定的内存地址里写数据,完全不检查你有没有给足够大的空间。比如你声明了一个char name[10],然后用scanf("%s", name);去读用户输入,用户要是输进去20个字符,这20个字符就会一股脑塞进10字节的空间里,后面的数据直接覆盖到内存中的其它位置。轻则程序崩溃,重则被恶意利用改写程序执行流程,这就是臭名昭著的缓冲区溢出攻击。

微软作为操作系统厂商,对这些安全漏洞最敏感,所以在MSVC编译器的运行库(CRT,C Runtime Library)里把strcpy、strcat、scanf、gets这类不安全函数全部标记为“弃用”(deprecated),同时提供了带_s后缀的“安全版本”来替代。VS编译时遇到这些被标记的函数,就默认当成错误抛出来,目的就是逼着你用安全版本。

1.3 scanf_s到底比scanf安全在哪

scanf_s的核心思路很简单:如果你输入的数据有固定长度上限,那就在调用时显式告诉函数“这个缓冲区最多能装多少字节”,函数内部就会做边界检查,一旦发现装不下了就拒绝写入,而不是硬着头皮往里塞。

以最常见的整数输入为例,scanf("%d", &a);改成scanf_s("%d", &a);,因为int是固定4字节(在Windows平台上),编译器知道自己需要的缓冲区大小,不需要你额外传。但如果读字符串:

char name[20]; scanf_s("%s", name, 20);

第三个参数20就是在告诉scanf_s:这个数组的容量只有20个字符,最多只能写19个字符加一个字符串结束符\0。如果用户输入的字符串超过19个字符串它不会写入,从而保障了内存安全。

注意:从这就能看出,_s系列函数本质是微软在编译器层面上做的内存安全加固,它要求程序员显式提供缓冲区大小信息,从而避免缓冲区溢出风险。这也就是为什么scanf_s在非微软编译器(比如GCC)上根本不存在——它不是一个标准的C库函数,而是微软的扩展。

2. 方案一:向微软“屈服”——改用scanf_s

既然VS强烈建议用scanf_s,最简单直接的思路就是跟着编译器走,把代码里的scanf全部改成scanf_s。

2.1 基础类型怎么改

对于整数、浮点数、字符等固定大小的数据类型,改动其实非常无脑,直接把名字换掉就行:

int a; float f; char c; scanf_s("%d", &a); // 整数 scanf_s("%f", &f); // 浮点数 scanf_s("%c", &c, 1); // 字符比较特殊,后面细说

为什么整数和浮点数不需要传缓冲区大小?因为它们在内存中的长度是固定的(int 4字节、double 8字节),编译器完全知道该写入多少数据,不需要额外指定。但char类型虽然只有1字节,按照_s系列函数的设计规范,传入单个字符变量时最好还是带上大小参数1,因为%c既可以读单个字符,也可以读取指定长度的字符序列,编译器没法自动判断你想读几个字符,所以需要显式注明。

2.2 字符串输入是重灾区

字符串输入是初学者最容易栽跟头的地方。先看一段网课上常见的写法:

char name[20]; scanf_s("%s", name);

这么写,编译能通过,但运行到这一行就直接崩溃或者出现诡异行为——因为%s对应的参数必须提供缓冲区大小,而你没给。正确的写法:

char name[20]; scanf_s("%s", name, 20);

这里sizeof(name)也是20,所以你也可以写scanf_s("%s", name, sizeof(name));。用sizeof的好处是,万一后面你改数组大小,比如从char name[20]改成char name[50],sizeof(name)会自动变成50,你不需要再手动同步第二个参数,避免改一处漏一处的问题。这是我在实际编码里比较推荐的习惯。

另外需要特别提醒:scanf_s的第三个参数类型是unsigned int(在Windows的MSVC里),如果你写sizeof(name),它的返回类型是size_t,在64位编译模式下是8字节无符号整数,理论上能隐式转换,基本不会出问题。但如果你用自己定义的变量做大小参数,注意别传负数进去,否则在安全检查时可能引发意想不到的行为。

2.3 多个变量的混合输入

一次读多个值的情况也很常见,比如一次读一个整数和一个浮点数:

int age; float score; scanf_s("%d%f", &age, &score);

这种写法不需要额外传大小参数,因为%d和%f对应的都是固定大小的变量。如果是字符串与整数混读:

char name[20]; int age; scanf_s("%s %d", name, 20, &age);

这个格式串参数的顺序就有点讲究了:每个需要缓冲区大小的占位符,其对应的缓冲区大小参数必须紧跟在被赋值的变量后面。也就是说第一个%s对应name和20,第二个%d对应&age,顺序错乱会导致数据读错甚至编译报错。我第一次用scanf_s时就在这里失误过,把20写到了最后,结果编译直接报参数不匹配的错误。

2.4 直接用scanf_s方案的隐患

用scanf_s确实是最省事的方法,但有一个很现实的问题:如果你以后要把代码迁移到Linux上,或者用GCC、Clang这类编译器,scanf_s根本不存在。到时候满屏的“implicit declaration of function 'scanf_s'”会让你欲哭无泪。

另外,网上很多教材、题库、ACM刷题模板,用的都是标准语法scanf。如果你从第一天学C语言就只用scanf_s,后面做题、看网课、抄例题时,往往需要来回转换,脑子的负担其实更重。所以我的建议是:如果是自学、只需要在自己电脑的VS上跑通,那用scanf_s完全可行;但如果你是大学生、以后要考试或参加竞赛,建议改走下面第二种方案,让代码保持标准C语法的写法。

3. 方案二:保留scanf——用_CRT_SECURE_NO_WARNINGS“封印”警告

如果你不想改代码、想彻底保留标准C的scanf写法,VS也给了一条官方认可的出路。看错误提示的后半句:“To disable deprecation, use _CRT_SECURE_NO_WARNINGS.”——意思是只要在预处理器里定义_CRT_SECURE_NO_WARNINGS这个宏,编译器就会把这个弃用警告当空气,代码里继续用scanf也不会有任何提示。

3.1 一行宏命令直接写在代码顶部

最暴力的做法,在源文件最开头加上这行:

#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { int a; scanf("%d", &a); return 0; }

注意#define必须写在所有#include之前。这个宏的原理是:MSVC的stdio.h头文件内部就是靠_CRT_SECURE_NO_WARNINGS这个宏来“屏蔽”弃用警告的。你提前定义好它,头文件展开时就走“不警告”的逻辑分支。

3.2 三种常见的使用方式对比

除了直接写在源文件里,还有两种常见方式:

**第一种:写在属性页的预处理器定义里。**这是最推荐的做法,因为不用动每一份源码,对所有源文件都生效。后面第4章会专门细讲完整操作步骤。

**第二种:写在项目属性里但是单独针对某个文件。**在“解决方案资源管理器”里右键某个.c文件 → 属性 → C/C++ → 预处理器 → 预处理器定义 → 编辑,添加_CRT_SECURE_NO_WARNINGS。这种做法的好处是只对这一个文件生效,其它文件仍然保持原警告状态。适合只有一部分旧代码需要兼容的情况。

3.3 宏定义方案的一个重要大坑

如果你在源文件顶部写了#define _CRT_SECURE_NO_WARNINGS,然后编译通过,没有任何警告了,这时候有一个非常容易踩的坑:这个宏只对这个源文件(.c文件)生效。如果你的项目里有多个.c文件都用了scanf,你需要把#define加到每一个文件里,或者干脆一步到位改在项目属性里。

另外,如果你用的是预编译头(比如MFC项目、或者勾选了“预编译头”控制台项目),由于预编译头文件会被所有源文件包含,而且预编译头的展开顺序在源文件的所有内容之前,那么你在源文件里写的#define有可能不生效。这时候必须把宏加到“预处理器定义”里,或者加到预编译头文件(比如pch.h)的最顶部。这是我踩过的坑,VS自动生成的“启用预编译头”项目,我就是因为没有关掉预编译头,结果在源文件里写#define完全无效,最后还是在属性面板里解决的。

提示:_CRT_SECURE_NO_WARNINGS的作用范围是“屏蔽警告”,不是“把scanf变成安全函数”。你依旧用着不安全的scanf,只是编译器不提醒你了。调试的时候,故意输入超长字符串,照样能让你程序崩溃。这属于眼不见心不烦,实际风险还是存在的。

4. 方案三:修改项目属性,让整个项目告别C4996

这个方法是我平时最推荐的,也是最长效的方案。它其实就是把_CRT_SECURE_NO_WARNINGS写到项目的预处理器定义里,让它对整个项目的所有源文件生效。好处显而易见:一劳永逸,改完之后整个项目都用标准C语法,不用在每个源文件里手动#define。

4.1 完整操作步骤(VS2022为例)

不同VS版本菜单名称基本一致,我以VS2022中文版为例:

  1. 在“解决方案资源管理器”里,右键点击你的项目名称(注意不是解决方案,是下面那个项目,图标是个“应用程序”样式的那个)。
  2. 选择最底部的“属性”,打开项目属性页。
  3. 左边的配置属性树形菜单里,选择“C/C++” → “预处理器”。
  4. 右侧找到“预处理器定义”,点击下拉箭头,选择“编辑”。
  5. 在弹出的对话框里,把_CRT_SECURE_NO_WARNINGS加进去。原有内容不要去动它,在新一行的位置输入这串字符,点确定。
  6. 把“配置”切到“所有配置”,“平台”切到“所有平台”,确保Debug和Release、x86和x64都生效。然后点“确定”保存。

改完之后,重新编译。刚才那一片C4996错误全部消失,scanf用起来就跟在Linux终端下一样丝滑。

4.2 为什么必须改“所有配置、所有平台”

属性页左上角有“配置”和“平台”两个下拉框,默认是“活动(Debug)”和“活动(Win32)”。如果你只是在Debug下加了宏,切到Release再编译,可能又报错。如果你后面把项目从Win32改成x64,又可能报错。所以无论以什么配置打开的属性页,添加宏的时候都把“所有配置”和“所有平台”拉出来,一次性搞定,省得以后再折腾。

4.3 最简单粗暴的方式:右键源文件就是干

其实还有一种相对“偷懒”的方式——在源文件第一行加:

#pragma warning(disable:4996)

注意这个#pragma warning(disable:4996)和前面讲#define的区别。宏是在预处理阶段屏蔽头文件的弃用判定逻辑,而#pragma是编译阶段直接关闭编号为4996的警告。两者最终效果看起来一样,但原理不同。相比而言#define _CRT_SECURE_NO_WARNINGS更加“正统”,因为它把Off时提示的“use _CRT_SECURE_NO_WARNINGS”真正落实了;而#pragma只是针对当前文件、当前编译单元封闭警告,如果你有多个.c文件就得在每个文件都加。

4.4 两种方式混用时的注意

如果你既在源文件里写了#define _CRT_SECURE_NO_WARNINGS,又在项目属性里加了同样的宏,编译器会给出一个“宏重定义”的警告。其实这个警告并不影响编译,但你的输出窗口会多出来一条莫名其妙的C4005提示。为了保持干净,二选一即可,不要叠着来。这也是我见过不少人把代码发出来求问“为什么我定义了宏还提示重定义”的原因。

5. 常见问题与排查技巧实录

5.1 加了宏依然报错是什么原因

排在最前面的情况就是致命错误C1189(Windows SDK版本冲突),老版本Windows SDK的crtdefs.h里在_CRT_SECURE_NO_WARNINGS被定义时会通过#pragma做某些处理,如果同时引用了Windows.h等系统头,偶尔会冒出一句“Cannot use _CRT_SECURE_NO_WARNINGS with /WX”。这个其实不常见,但遇到时说明你同时开启了“把警告视为错误”的选项——项目属性 → C/C++ → 常规 → “将警告视为错误”改成了“是”。要么关掉WX,要么就别用这个宏了,直接用scanf_s反而省事。

排在第二的情况就是宏位置不对。我前面说过,#define必须写在#include <stdio.h>之前。有人会把#define写在整个文件的第5行、#include在第2行、然后第3行还有别的#include,这样也能生效,只要你定义出现在头文件展开之前就行。但一旦你把#define写在#include <stdio.h>之后,头文件已经处理完了,警告逻辑已经生成,你再定义就晚了。

第三是源文件编码问题。某些教材的代码是从网页复制来的,源文件里带着BOM或者奇怪的空白字符,导致预处理器根本识别不到宏名。这种情况你直接看代码行号附近有没有红色波浪线就知道是编码问题了。个人建议这种问题直接手动重敲那几行,别复制粘贴了。

5.2 scanf_s编译通过但运行崩溃怎么办

如果你已经换成了scanf_s,编译一点问题没有,但一运行就崩溃,那先检查两件事:

**第一,字符串输入有没有传够缓冲区大小。**最常见的是:

char s[10]; scanf_s("%s", s);

这种写法在VS2022里甚至会直接编译报错,告诉你参数太少。但有些早期版本VS编译能过,运行才崩。%s后面的第三参数必须传字符串数组的元素个数,让编译器知道边界。

第二,%s和%c混用时的空白字符问题。

char name[20]; char gender; scanf_s("%s", name, 20); scanf_s("%c", &gender, 1);

这里第二个scanf_s会直接把缓冲区内残留的回车符\n读给gender,表现就是你输入名字后按回车,回车字符被当成了性别。这种问题本质上是C标准输入流的通病,不是scanf_s专有的。解决办法是在%c前面加空格:

scanf_s(" %c", &gender, 1);

格式串里第一个字符是空格,它会跳过输入流里的所有空白字符(包括换行、空格、制表符),再读取下一个非空白字符。这个小技巧在scanf里同样适用,属于C语言输入处理的必备知识点。

5.3 VS Code里用了C/C++插件还是报C4996

这里额外说一句,很多同学看到热词里有一堆“VS Code配置C/C++环境”,就在VS Code里写C语言,然后发现同样报这个错。VS Code本身不是编译器,它只是调用你电脑里的编译工具链。如果你在VS Code里用的是MinGW-w64(GCC),那scanf根本不报warning,因为GCC没有微软这套_s函数设计。如果你在VS Code里调用的是MSVC编译器,那就照着上面说的,用同样的方法处理。

如果用的是MinGW但代码里写了scanf_s,反而会报“隐式声明”的错,因为GCC的库函数里没有scanf_s。这也从侧面印证了我前面说过的:长期写scanf_s会让你的代码绑定在微软平台上,可移植性变差。

6. 个人实操建议与踩坑后的最终选择

6.1 不同人群的差异化建议

直接做个总结,大家根据自己的情况对号入座:

  • 纯自学、不打算换平台、只想快速跑通教材代码:直接用scanf_s。最省心,也不用理解宏和预处理器的概念。但建议装一个Dev-C++或Code::Blocks辅助验证标准C语法,防止自己学到的是“微软方言”。
  • 高校学生、要应对考试、以后可能要刷题:强烈建议用方案二或方案三,保留scanf标准写法。平时练习环境统一,考试换到其它环境也不用改代码。我见过太多只写scanf_s的人,到考试用VS以外的环境做题就傻眼。
  • 工作后的项目开发:按项目规范来。老项目统一scanf_s就续用,新项目在预处理器定义里统一加_CRT_SECURE_NO_WARNINGS。关键是“整个项目的写法保持一致”,别一半一半。

6.2 我个人的习惯做法

说实话,我最早入门时也用过scanf_s,但后面转Linux开发就彻底不用了。现在我在Windows上用VS练手时,默认的操作流程是:新建项目之后,第一步就打开项目属性,在预处理器定义里加上_CRT_SECURE_NO_WARNINGS,然后全程用标准C语法写代码。这样做的好处是,代码拿到任何平台都能编译,甚至拿到网页OJ、在线评测系统里也不会有兼容性问题。

另外建议把“学习使用的项目”和“练手写的测试代码”区分开来。学习时新建一个专门的“测试项目”,改好属性、写代码只管跑;平时随手写个几行验证某个函数用法时,也可以直接右键源文件加上#pragma warning(disable:4996)快速实验。保持一个干净、可复现、可迁移的学习环境,比“背下来怎么消除报错”要重要得多。

6.3 再分享一个小技巧

如果你经常被困扰,VS里还能直接把C4996从“错误”降级为“警告”,操作是在工具 → 选项 → 文本编辑器 → C/C++ → 高级 → 禁用特定警告里填4996。但说实话,我不太推荐这么做,因为VS初始设置C4996为错误是为了安全,你自己关闭这个提醒之后,等于把整个项目的安全检查放松了,万一以后代码里真有别的安全问题的警告也被一同屏蔽,反而不利于培养良好的编码习惯。

回到最初的问题——scanf函数报unsafe,本质上不是代码写错了,而是编译环境的安全策略更严格。理解了这一点,解决方案无非就是“用更安全的函数”或者“关掉安全提醒”两条路。我的建议是:了解scanf_s的用法,但平时练习坚持用scanf标准语法,同时在VS里配置好_CRT_SECURE_NO_WARNINGS。这样既能拿VS最强的调试功能,又不会把自己锁死在微软生态里。学会和编译器交朋友,搞懂每个报错背后的逻辑,比背下十种消错方法都有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询