上周帮一个做工业上位机的朋友看构建日志,整个输出窗口被 error C4996 刷了整整两屏,清一色指着 strncpy,末尾还挂着那句每次看到都想叹气的提示——To disable deprecation, use _CRT_SECURE_NO_WARNINGS。他的第一反应是把警告全关了,第二反应是把函数全换成带 _s 的版本,结果这两种做法在后面的几天里都还了债:前者把真正有价值的安全警告一起埋了,后者在一个边界条件下让程序直接闪退。这类问题说大不大,但真要处理干净,得先搞清楚 VS 为什么盯着这几个老函数不放,再决定用哪种姿势和解。这篇就把这个问题从头到尾捋一遍,从报错本身的含义、三条主流解决路线的取舍,到属性表批量配置、CMake 工程处理、代码跨平台兼容,再到我自己踩过的几个坑,都摊开来说。刚接触 C++ 的朋友能照着抄作业,手上已经有好几个老工程要维护的朋友,也能找到一条一劳永逸的工程化路子。
1. 先把这行报错读明白,别急着关警告
1.1 一条 C4996 里其实塞了三层信息
MSVC 的这条诊断信息写得其实挺厚道,只是大多数人被红色吓到,直接跳过去看最后一句了。完整的形态大概是这样:
error C4996: 'strncpy': This function or variable may be unsafe. Consider using strncpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.第一层是编号 C4996,它属于编译器诊断里"弃用声明"这一大类,意思是"你用到的东西被标记为不建议继续使用了"。注意它描述的是"弃用",不是"语法错误",也不是"这个函数现在不能用了"。这个区别很关键,因为它决定了你有一堆合法的应对手段,而不是只能改代码。
第二层是具体对象 strncpy。微软从很早就开始把一批 C 运行时函数归入"可能不安全"的名单,理由是这些函数在参数不当时会安静地写坏内存,不给你任何提示。strncpy 的经典毛病是当源字符串长度不小于指定的拷贝长度时,它不会在目标缓冲区末尾补结束符,调用方如果忘了手动补,后面任何一次字符串操作都可能越过缓冲区尾部。同类被点名的还有 strcpy、strcat、sprintf、gets、scanf 等等。
第三层才是解决方案提示,也就是那句 To disable deprecation, use _CRT_SECURE_NO_WARNINGS。它同时给了一个替代建议 strncpy_s。所以这一行报错本质上是在问你:你是想关掉这条提示继续用老函数,还是换成我建议的新函数?两条路微软都认,选择权在你。
顺带说一句,C4996 默认是警告级别而不是错误级别,但很多团队在工程配置里开了"将警告视为错误",或者在较新的 VS 里因为启用了 SDL 检查而被提升为 error,这才会出现编译直接失败、生成不了目标文件的情况。搞清楚当前工程到底处于哪种状态,是后面选方案的前提。
1.2 为什么偏偏是微软的编译器在挑刺
很多人第一次遇到这个提示会觉得莫名其妙:同一份代码在 GCC 和 Clang 下编译得好好的,一个警告都不给,怎么到了 MSVC 这里就变成拦路虎了。原因在于各家编译器的安全策略不同,微软在 CRT 层面做了额外的加固设计,把"不安全函数"的检测做进了头文件里,用 #pragma deprecated 这类机制主动发出提示,并且提供了一套 _s 后缀的安全版本作为替代。
这套设计诞生于 2005 年前后,当时内存安全问题在 Windows 平台上造成的漏洞占比很高,微软的思路是在源头掐断:只要你在代码里写了这些老函数,编译阶段就提醒你一次,让你有机会换成带长度校验的版本。对安全要求高的项目来说,这个提醒是有价值的;但对大量移植过来的老代码、跨平台代码、教学示例来说,它就成了一种噪声。
还有一个容易被忽略的点:Visual Studio 项目模板在创建时默认开启了 SDL 检查(在项目属性的 C/C++ 常规页里能看到这一项)。SDL 是微软的一套安全检查开关集合,开启后一部分原本只是警告的诊断会被提升成错误,其中就包括与安全函数相关的 C4996。所以有时候你明明在预处理器定义里加了 _CRT_SECURE_NO_WARNINGS,编译还是失败,八成是 SDL 检查在起作用。这一点我在第 4 章会单独展开,因为它是排查时最容易绕进去的弯。
理解了这层背景,你就能明白:C4996 不是编译器在刁难你,而是两套设计哲学撞在了一起。你的任务不是"打败"这个警告,而是根据自己的项目情况,选一条成本最低、后患最小的路。
2. 解决思路选型:三条路各自适合什么场景
2.1 路线一:用宏定义关掉弃用提示
最直接的做法就是在编译单元里定义 _CRT_SECURE_NO_WARNINGS 这个宏。它的作用非常明确:让 CRT 头文件里那段主动触发弃用警告的代码失效,于是 C4996 就不再针对这批函数报出来。注意它的作用范围是"这批不安全函数相关的弃用提示",不是"所有 C4996",这个边界要清楚。
这个宏有三个落地点,各有各的适用场合。写在源文件顶部属于最小改动,适合临时验证或者只有一两个文件的老代码;写进项目属性的预处理器定义里属于工程级配置,适合一个项目内大面积使用老函数的情况;写进属性表或者更高层的构建配置里,则适合一个解决方案下几十个项目都要统一的场景。
为什么这个方案值得优先考虑?因为它对代码零侵入。你不需要动任何一行业务逻辑,不需要重新评估每个 strncpy 调用的语义,风险最低、耗时最短。尤其是那种几万行、上游还在持续合并的老代码库,改函数的成本高得离谱,一个宏就能把编译打通,把精力留给真正重要的事情。
但它的代价也很明显:你把安全提示一起关掉了。原本编译器会提醒你"这里有潜在的内存风险",现在它闭嘴了。如果你的项目里有大量用户输入、网络报文、文件路径参与字符串拼接,关掉提示等于放弃了一道免费的防线。所以我一般建议团队在使用这个宏的同时,配套做一轮代码审查,把真正危险的调用点单独排查一遍,而不是简单地"关掉了事"。
2.2 路线二:换成带 _s 后缀的安全版本
微软给出的替代方案是 strncpy_s,它有几个明确的改进:参数里多了目标缓冲区的容量,函数内部会检查容量是否够用;在容量足够的情况下保证结果以结束符结尾;参数非法时会调用无效参数处理程序而不是安静地写坏内存。函数原型大致是这样:
errno_t strncpy_s( char *strDest, size_t numberOfElements, const char *strSource, size_t count );第三个参数 numberOfElements 是目标缓冲区的总大小,第四个参数 count 是要拷贝的最大字符数。这里有个非常实用的取值叫 _TRUNCATE:当你把它传给 count 时,函数会尽可能多地拷贝字符,并在目标缓冲区末尾自动补结束符,超出部分直接截断,不会触发错误。日常做字符串拷贝,绝大多数场景直接传 _TRUNCATE 就够了。
比如原来的代码是这样:
char name[32]; strncpy(name, src, sizeof(name)); name[sizeof(name) - 1] = '\0';换成安全版本之后可以写成:
char name[32]; strncpy_s(name, sizeof(name), src, _TRUNCATE);明显更省心,也不用再手动补那行结束符。同族的还有 strcpy_s、strcat_s、sprintf_s、memcpy_s 等等,命名规律一致。
选它的理由也很充分:不只是消掉警告,而是真的把风险点修掉了。如果你的项目是新建的,或者要过安全审计,那这条路线基本是唯一选择。缺点有两个:一是要动代码,调用点多的话工作量不小;二是这套函数是微软 CRT 特有的,代码一旦要拿到 Linux 上编译,就会找不到符号。后面第 3 章会专门讲怎么处理这个兼容问题。
2.3 路线三:调整工程配置里的检查开关
第三条路不走代码,也不走宏,而是改工程属性里的开关。主要有两个位置值得关注。一个是禁用特定警告:项目属性 → C/C++ → 高级 → 禁用特定警告,填上 4996,相当于给编译器加了 /wd4996,直接把这条编号的警告屏蔽掉。另一个是 SDL 检查:项目属性 → C/C++ → 常规 → SDL 检查,改成"否"。
先说 /wd4996 和 _CRT_SECURE_NO_WARNINGS 的区别。前者按警告编号屏蔽,属于比较粗的操作,会把所有 C4996 都挡掉,包括那些跟字符串函数无关的弃用提示;后者只针对 CRT 安全函数这一块,更精准。能用宏解决的时候,我不太推荐用 /wd4996。
再说 SDL 检查。关掉它确实能让编译通过,但这个开关同时还管着一批别的安全检查,比如整数溢出、缓冲区溢出的额外告警。一关了之,等于把整套加固措施都拆了。除非你的项目有非常明确的理由(比如引入了第三方库,那些库本身不符合 SDL 要求,导致大量噪音),否则我不建议动这一项。真要关,也应该先用属性表把它限制在特定项目上,而不是在整个解决方案层面全局关闭。
三种路线做个横向对比,看起来更清楚:
| 方案 | 作用范围 | 改动量 | 是否保留安全提示 | 推荐场景 |
|---|---|---|---|---|
| 定义 _CRT_SECURE_NO_WARNINGS | 当前编译单元或项目 | 极小 | 否 | 老代码维护、快速打通编译 |
| 改用 strncpy_s 等安全函数 | 每个调用点 | 中等偏大 | 是 | 新项目、需过安全审计 |
| 禁用警告 4996 / 关闭 SDL | 项目或解决方案 | 小 | 否 | 引入第三方库造成大量噪音 |
我个人在实际工程里的做法通常是分层的:核心业务模块、涉及外部输入的模块,优先换成安全函数;边缘的、只处理内部固定字符串的老模块,用宏定义把提示压下去,但有计划地逐步重构。一刀切看着痛快,长期看往往要返工。
3. 手把手实操,从单文件改到整个解决方案
3.1 单文件最小改动:宏必须写在所有 include 之前
这是最容易翻车的一步,因为它太简单了,简单到没人会去想它为什么失效。看下面这段代码:
#include <string.h> #define _CRT_SECURE_NO_WARNINGS int main(void) { char buf[16]; strncpy(buf, "hello", sizeof(buf)); return 0; }这段代码照样会报 C4996。原因在于这个宏是在 CRT 头文件被展开的时候起作用的,头文件已经处理完了,你再定义它就晚了。正确写法是把定义挪到文件最顶端,任何 include 之前:
#define _CRT_SECURE_NO_WARNINGS #include <string.h> int main(void) { char buf[16]; strncpy(buf, "hello", sizeof(buf)); return 0; }如果项目用了预编译头(VS 默认模板里是 pch.h 或者更老版本的 stdafx.h),那这个宏要写在预编译头文件的第一行,并且在所有其他 include 之前。因为预处理头会把当时的状态固化下来,后面再用这个头文件的翻译单元都继承那个状态,只有放在最前面才有效。我见过太多次"宏明明写了却不生效",追下去十有八九是位置放错了,或者项目压根没启用预编译头、文件顶部那个定义只对当前文件有效。这里还有个细节值得提醒:如果你的源文件里 include 顺序是#include "pch.h"再#include <string.h>,那把宏写在 pch.h 的第一行就一定生效;但如果你写成#include <string.h>再#include "pch.h",那 pch.h 里的宏就来不及了。所以项目里最好统一 include 顺序,把预编译头永远放第一个。
3.2 项目属性里做全局配置:预处理器定义的正确填法
单文件加宏适合验证,真要落到工程上,还是得进项目属性。步骤如下:
- 右键项目 → 属性(或者按 Alt+Enter)。
- 左侧依次展开"配置属性 → C/C++ → 预处理器"。
- 在右侧"预处理器定义"这一行里,点下拉箭头 → 编辑。
- 在列表里新增一行 _CRT_SECURE_NO_WARNINGS,不要写成
#define _CRT_SECURE_NO_WARNINGS,只写宏名本身。 - 确认后,注意左上角的"配置"和"平台"下拉框。默认只改了 Debug|x64 这一组。
第五步是很多人忽略的地方。VS 的项目属性是分配置存储的,Debug 和 Release 是两套独立的配置数据,Win32 和 x64 也是。你只改了 Debug|x64,切到 Release 一编译,C4996 又冒出来了。稳妥的做法是把"配置"下拉切到"所有配置",把"平台"切到"所有平台",再改一次属性。检查方法很简单:改完之后,把两个下拉框分别切到不同组合,看预处理器定义那一栏里是不是都有这个宏。
如果你要管理的是一个包含几十个 C++ 工程的解决方案,一个个点属性太慢了,正确做法是使用属性表。打开"视图 → 其他窗口 → 属性管理器",能看到每个项目下面分了 Debug|x64、Release|x64 等节点。右键其中一个节点 → 添加新项目属性表,存成一个 .props 文件,比如 common.props,然后在里面设置好预处理器定义。之后其他项目只需要右键 → 添加现有属性表,把这个文件挂上去就行。属性表的好处是集中管理:哪天要调整,改一个文件,所有挂载的项目一起生效。已挂载的属性表会在属性管理器的节点下显示出来,双击就能编辑,比在项目属性窗口里翻来翻去高效得多。
3.3 CMake 工程与批量处理的写法
现在越来越多的 C++ 项目用 CMake 管理,这类工程的解决方式和 VS 属性窗口完全不同,改错了地方等于白改。标准的做法是在 CMakeLists.txt 里给目标加编译定义:
add_executable(myapp main.c util.c) if(MSVC) target_compile_definitions(myapp PRIVATE _CRT_SECURE_NO_WARNINGS) endif()用 PRIVATE 还是 PUBLIC 取决于这个宏是否需要传递给依赖方。对于这种纯粹压制本模块警告的宏,用 PRIVATE 更合适,避免污染下游。如果整个目录下的目标都要加,也可以在 CMakeLists.txt 里统一写:
if(MSVC) add_compile_definitions(_CRT_SECURE_NO_WARNINGS) endif()注意 add_compile_definitions 会影响这个 CMakeLists.txt 及其子目录里之后定义的所有目标,位置放得太早可能会把它带到不该带的地方,所以我一般还是建议按目标逐个添加,可控性更强。
改完 CMakeLists.txt 之后如果没生效,八成是 CMake 缓存的问题。CMake 会把上次配置的结果存在 CMakeCache.txt 里,有时候改了文件但没触发重新配置,构建依然用旧参数。处理方式是在构建目录下删除 CMakeCache.txt 和 CMakeFiles 目录,然后重新执行配置和生成。用 VS 的"打开文件夹"方式加载 CMake 工程时,可以右键 CMakeLists.txt → 删除缓存并重新配置,效果一样。
顺便提一个跨编译器都通用的写法。把宏定义和编译器判断结合起来,能让同一份 CMakeLists 在 Windows 和 Linux 下都正常工作:
if(MSVC) target_compile_definitions(myapp PRIVATE _CRT_SECURE_NO_WARNINGS) endif()GCC 和 Clang 那边不认识这个宏,但因为被 if(MSVC) 包住了,也不会造成任何影响。
3.4 代码要上 Linux 的话,_s 系列不能硬写
很多工程存在"Windows 上开发、Linux 上部署"的情况,这时候路线二就要小心了。strncpy_s、strcpy_s、sprintf_s 这些都是微软 CRT 提供的东西,glibc 里没有对应的实现,直接拿到 Linux 上编译会提示找不到符号。有三种处理方式,我按推荐程度排序。
第一种是封装一层自己的安全拷贝函数,内部用条件编译区分平台:
#include <string.h> static void copy_str(char *dst, size_t dstsz, const char *src) { #ifdef _MSC_VER strncpy_s(dst, dstsz, src, _TRUNCATE); #else if (dstsz == 0) { return; } size_t n = strlen(src); if (n >= dstsz) { n = dstsz - 1; } memcpy(dst, src, n); dst[n] = '\0'; #endif }这样业务代码里统一调 copy_str,两边的行为都保证以结束符结尾,也保证不会越界。缺点是每个项目都要维护这么一份小工具,最好做成公共头文件放进基础库。
第二种是干脆不用 _s 系列,统一改用标准 C 里跨平台都有的 snprintf。字符串拷贝场景其实用 snprintf 就能覆盖大部分需求:
char name[32]; snprintf(name, sizeof(name), "%s", src);snprintf 保证不会写超过缓冲区大小,并且总是以结束符结尾(只要大小大于 0),GCC、Clang、MSVC 全都支持。唯一的小坑是部分老版本 MSVC 对 snprintf 的实现和 C99 标准有差异,但 VS2015 以后已经没有这个问题了。
第三种是用第三方库,比如某些基础库会提供跨平台的安全字符串封装。如果项目已经引入了这类库,直接用现成的就行,没必要再造轮子。我个人的倾向是:新代码优先 snprintf,需要严格控制截断行为的地方用自己封装的 copy_str,只有确定永远不出 Windows 的模块才直接用 _s 系列。
4. 踩过的坑与问题排查实录
4.1 宏明明定义了却不生效的几种典型原因
这一类问题我遇到过太多次,归纳下来无非是几个位置问题,按出现频率从高到低排一下。
最常见的是宏写在了 include 之后。前面 3.1 已经说过了,这里再强调一遍判断方法:如果只有某一个 .c 或 .cpp 文件报错,其他文件正常,那基本就是这个文件里宏的位置不对。
第二常见的是配置和平台没选全。只改了 Debug|x64,Release 编译的时候没生效。判断方法是在项目属性的预处理器定义栏里,把配置和平台下拉切到各个组合看看是否都有这个宏。
第三种是用了预编译头但定义在 .cpp 里。预编译头会把它展开时的宏状态固化,后续所有包含它的翻译单元都继承那个状态。如果你把宏写在 .cpp 里而 .h 是靠预编译头进来的,那宏就没机会在头文件展开前生效。解决办法就是把宏提到预编译头文件的第一行。
第四种是项目引用了静态库或动态库,你改了主工程但没改库工程。库在编译时也会触发 C4996,而且库的警告不会因为主工程的设置而消失。这时候要用属性表把配置铺开到所有工程,或者干脆在库工程里单独加。
第五种比较隐蔽:工程里存在多个同名宏的冲突定义。比如某个第三方头文件里写了#undef _CRT_SECURE_NO_WARNINGS,或者某个 .props 里把它定义成了 0。宏一旦被定义成 0,在预处理阶段#ifdef判断依然成立,但 CRT 头文件里通常会检查它的值,这种情况下就不生效了。排查手段是在预处理器定义里临时加一个测试宏,配合#ifdef打印一段信息出来,确认它到底有没有被正确带上。
4.2 strncpy 本身的坑:不补结束符这件事
就算你不打算换函数,也值得花两分钟看看 strncpy 到底危险在哪,因为它和 C4996 的因果关系是绑在一起的。看这段代码:
char buf[8]; strncpy(buf, "hello world", sizeof(buf)); printf("%s\n", buf);strncpy 的规则是:拷贝最多 n 个字符,如果源字符串长度小于 n,则在剩余位置补零;如果源字符串长度大于等于 n,则拷满 n 个字符,并且不补结束符。上面这段代码里源字符串长度 11 大于 n=8,所以 buf 里 8 个字节全是字符,没有结束符。后面 printf 会一直往后读,直到在内存某处碰到一个零字节才停,读到的内容是什么完全不可控,运气不好就是崩溃,运气好则是打印出一串乱码。
这种问题在测试环境里往往看不出来,因为栈上刚好有零字节,跑得好好的;一到生产环境,函数调用层次变了、栈布局变了,就突然炸了。这也是为什么这类函数被列入"可能不安全"名单的核心原因:它的失败是安静的、延迟的、和环境相关的。
如果暂时不换函数,最低限度的自保手段是手动补结束符:
char buf[8]; strncpy(buf, "hello world", sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0';注意这里有两个改动:长度参数减了 1,给结束符预留位置;拷贝完之后手动写一个零字节。两件事缺一不可,只做前者的话,当源字符串刚好等于 sizeof(buf)-1 时,结尾依然没有结束符。
4.3 strncpy_s 用错反而更容易崩
换到安全函数并不意味着万事大吉,用错了它比原来的函数更容易出问题,因为它的错误处理方式是"终止程序"。看这个写法:
char buf[8]; strncpy_s(buf, sizeof(buf), "hello world", sizeof(buf));这里第四个参数传的是目标缓冲区大小 8,而源字符串长度 11,超出了 8-1 的可容纳范围。这种参数组合下,strncpy_s 会认为发生了缓冲区不足的严重错误,调用无效参数处理程序。默认行为在 Debug 下会弹出断言对话框,Release 下会直接终止进程。也就是说,一个原本只是"字符串被截断"的小问题,被放大成了程序崩溃。
正确写法是传 _TRUNCATE:
char buf[8]; strncpy_s(buf, sizeof(buf), "hello world", _TRUNCATE);这样函数会拷贝 7 个字符加一个结束符,静默截断,不报错。我个人的经验是:除非业务上明确规定"源字符串超长必须报错",否则一律用 _TRUNCATE。真要精确控制,也得先判断源长度,再决定行为,而不是把目标缓冲区大小当成拷贝长度传进去。这个参数混淆是我在 code review 里见得最多的错误之一。
另外注意 strncpy_s 的参数顺序是"目标、目标容量、源、拷贝长度",别和某些第三方库的封装搞混。函数返回类型是 errno_t,虽然大部分情况下可以忽略返回值,但如果你的项目对可靠性要求高,检查一下返回值是个好习惯:返回 0 表示成功,返回其他值表示出现了截断或者参数问题。
4.4 常见问题速查表
把实际排查中遇到的情况整理成一张表,遇到问题可以直接对照:
| 现象 | 最可能的原因 | 处理方式 |
|---|---|---|
| 加了宏还是报 C4996 | 宏写在 include 之后 | 移到文件或预编译头最顶端 |
| 只有 Release 报错 | 只改了 Debug 配置 | 配置切"所有配置"重设一次 |
| 主工程好了,库工程还报 | 库工程没同步配置 | 用属性表挂到所有工程 |
| 加了宏依然编译失败 | SDL 检查把警告提升为错误 | 检查 SDL 检查开关,或改用安全函数 |
| 换成 strncpy_s 后程序退出 | 第四参数误传目标缓冲区大小 | 改用 _TRUNCATE |
| 字符串打印出乱码 | strncpy 未补结束符 | 手动补 '\0' 或换安全函数 |
| CMake 改了不生效 | 构建缓存未刷新 | 删除 CMakeCache.txt 重新配置 |
| Linux 上找不到 strncpy_s | 平台相关函数 | 用条件编译封装或改用 snprintf |
提示:排查顺序建议从"配置是否选全"开始,再到"宏位置是否正确",最后才怀疑 SDL 检查之类的深层开关。按这个顺序走,基本十分钟内能定位。
4.5 一个被低估的进阶做法:C++ 下自动重载
如果你的工程是 C++,还有一个不太为人知的开关值得了解一下:_CRT_SECURE_CPP_OVERLOAD_STANDARD_NAMES。把它定义为 1,并保证在包含 CRT 头文件之前生效,那么在 C++ 代码里调用 strncpy 时,编译器会自动把它替换成对应的安全版本重载。也就是说,你不需要挨个改调用点,老代码照样能获得安全版的实现。
这个机制的原理是在 string.h 里提供了一组模板重载,当参数个数和类型匹配时会优先选中它们,从而转发到 _s 版本。它对代码是零侵入的,但门槛在于宏的定义位置要求很严格,必须在所有 CRT 头文件之前,通常只能放在预编译头或者编译选项里。另外它只对 C++ 有效,纯 C 工程享受不到。所以这个做法适合那种"代码量巨大、改不动、又是 C++ 项目、想顺手提升一点安全性"的场景。用了之后记得做一轮回归测试,因为字符串处理的行为细节可能会发生变化,尤其是截断场景下的结果长度。
5. 我在实际项目里的取舍
做了这么些年维护和重构的工作,我对这类问题的态度一直在变。最开始是能关就关,一个宏解决一切;后来被线上问题教育过几次之后,变得谨慎起来,倾向于全部替换成安全函数;再往后又发现,无差别替换的成本实在太高,尤其是在大型遗留代码里,很多 strncpy 的调用点其实是安全的,源字符串长度可控,替换的意义有限,改动反而引入了新的风险。现在的做法是有选择地处理。
判断标准主要看三个维度:数据来源、执行频率、出错后果。数据来源如果是外部输入,比如配置文件、网络报文、用户输入,那必须换,而且不只是换个函数名那么简单,还要检查长度校验逻辑是否完整。数据来源如果是编译期常量或者内部固定字符串,源长度明确可控,那用宏压掉警告是可以接受的,但我会在代码旁边留个注释说明为什么可以这么做,方便后来人复查。执行频率高的路径,比如每帧都在跑的渲染循环里的字符串拼接,安全性收益更高,优先处理。出错后果这一维度最直观:如果这段代码崩了只会让日志难看一点,和如果会导致界面卡死、数据丢失,处理优先级完全不同。
另外有个习惯值得分享:无论用哪种方案,我都会在项目根目录放一份 README 或者注释说明,写清楚这个项目为什么用了 _CRT_SECURE_NO_WARNINGS,哪些模块已经替换成安全函数,哪些还没动。这事儿看着很小,但当下一个人接手、或者半年后的自己回头看时,能省下大量考古时间。我接手过的一个项目就是因为没留这份说明,团队里三个人分别用三种方式处理同一个警告,最后配置互相打架,排查了整整一天才弄明白。
工具层面还有一个我觉得挺顺手的小技巧。VS 的"输出"窗口里如果只显示错误,看不到上下文,可以把"显示输出来源"切到"生成"并开启详细级别,或者在"错误列表"里按代码筛选 C4996,一次性看到所有触发点,方便评估工作量。对于调用点特别多的项目,可以先用这个方法统计一下总数,再决定是走宏定义路线还是逐点替换路线,别凭感觉拍脑袋。数字摆出来之后,取舍就简单多了。