Visual Studioで、昔のサンプルコードを写して動かしたら、ビルド結果に黄色い警告がずらりと並びました。「fopenは安全でない可能性があります」。コードは、教科書どおりのはずなのに、です。
調べると、検索結果の大半は「_CRT_SECURE_NO_WARNINGSを書けば消える」でした。確かに消えます。ただ、書く位置を間違えると、書いたのに消えないこともあります。そもそも、この警告は消してしまって、本当によいのでしょうか。
そこで、警告の実物と、消し方、消したあとで何が変わるのかを、MSVC(19.44、x64)で実際に動かして確かめました。
C4996とは?
C4996とは、
・「この関数は、使わないほうがよいとされています」という、コンパイラからの通知。関数ごとに、理由の書かれた文面が付く
です。名前の「4996」は、廃止予定・非推奨の機能に使われる、警告番号です。C++の文法違反ではないので、ビルドは通ります。ただし、設定によっては、エラーとして扱われて、ビルドが止まります(後述)。
実験①:警告の実物を見る
fopen・strcpy・sprintfを使った、小さなプログラムを用意しました。
▼c1_warn.cpp
#include <cstdio>
#include <cstring>
int main()
{
FILE* fp = fopen("test.txt", "w"); // 警告の対象①
char buf[16];
strcpy(buf, "hello"); // 警告の対象②
sprintf(buf, "%d", 123); // 警告の対象③
if (fp)
{
fputs(buf, fp);
fclose(fp);
}
std::printf("書き込み完了: %s\n", buf);
return 0;
}これを、Visual Studioの既定と同じ警告レベル(/W3)で、ビルドしました。
c1_warn.cpp
c1_warn.cpp(6): warning C4996: 'fopen': This function or variable may be unsafe. Consider using fopen_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
c1_warn.cpp(8): warning C4996: 'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
c1_warn.cpp(9): warning C4996: 'sprintf': This function or variable may be unsafe. Consider using sprintf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.3つの関数それぞれに、警告が出ました。文面を読むと、「fopen_sのほうを使いなさい」「警告を消すなら、_CRT_SECURE_NO_WARNINGSを使いなさい」と、対処法がそのまま書いてあります。ビルドは成功しており、実行もできます。
書き込み完了: 123実験②:エラーになる設定がある
同じコードを、/sdl(SDLチェック)を付けてビルドしました。
c1_warn.cpp
c1_warn.cpp(6): error C4996: 'fopen': This function or variable may be unsafe. Consider using fopen_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
c1_warn.cpp(8): error C4996: 'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
c1_warn.cpp(9): error C4996: 'sprintf': This function or variable may be unsafe. Consider using sprintf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.警告(warning)だったものが、エラー(error)になり、ビルドが止まりました。Visual Studioのプロジェクトのプロパティには、「C/C++ → 全般 → SDLチェック」という項目があり、これが有効だと、同じ現象になります。新しく作ったプロジェクトでは、この項目が有効になっていることが多く、「前のプロジェクトでは警告だったのに、今回はエラーで止まる」の原因になります。
警告を消す方法と、【重要】書く位置の罠
警告を消す方法として、いちばん知られているのが、_CRT_SECURE_NO_WARNINGSを定義することです。まず、コードの先頭で定義した版を試しました。
▼c2_define_before.cpp(先頭で定義)
#define _CRT_SECURE_NO_WARNINGS // 必ず、最初のincludeより前に書く
#include <cstdio>
#include <cstring>
int main()
{
FILE* fp = fopen("test.txt", "w");
char buf[16];
strcpy(buf, "hello");
sprintf(buf, "%d", 123);
if (fp)
{
fputs(buf, fp);
fclose(fp);
}
std::printf("書き込み完了: %s\n", buf);
return 0;
}c2_define_before.cppファイル名が出ているだけで、警告は1つも出ませんでした。次に、#includeのあとに書いた版です。
▼c3_define_after.cpp(#includeのあとで定義)
#include <cstdio>
#include <cstring>
#define _CRT_SECURE_NO_WARNINGS // includeより「後」に書いた(効かない)
int main()
{
FILE* fp = fopen("test.txt", "w");
char buf[16];
strcpy(buf, "hello");
sprintf(buf, "%d", 123);
if (fp)
{
fputs(buf, fp);
fclose(fp);
}
std::printf("書き込み完了: %s\n", buf);
return 0;
}c3_define_after.cpp
c3_define_after.cpp(7): warning C4996: 'fopen': This function or variable may be unsafe. Consider using fopen_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
c3_define_after.cpp(9): warning C4996: 'strcpy': This function or variable may be unsafe. Consider using strcpy_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.
c3_define_after.cpp(10): warning C4996: 'sprintf': This function or variable may be unsafe. Consider using sprintf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.書いてあるのに、警告は、消えませんでした。#defineは、書かれた位置より後ろにしか効きません。警告の元になる宣言は、#include <cstdio>の中にあるため、それより前に定義しておく必要があるのです。
ヘッダーをたくさん読み込むプロジェクトでは、この順番を守り切るのが大変です。そこで、ビルドの設定で定義する方法のほうが、確実です。コマンドラインでは/Dで定義できます。
cl /W3 /D_CRT_SECURE_NO_WARNINGS c1_warn.cppc1_warn.cpp同じコード(c1_warn.cpp)でも、警告は出ませんでした。Visual Studioでは、プロジェクトのプロパティの「C/C++ → プリプロセッサ → プリプロセッサの定義」に、_CRT_SECURE_NO_WARNINGSを足すのが、これと同じ設定です。
警告を消すだけで、問題ないのか?
消し方は分かりました。ただ、「安全でない可能性」という文面は、本当なのでしょうか。確かめるために、入りきらない文字列をコピーする実験をしました。まず、strcpyです。
▼c6_overflow_cpy.cpp(8バイトの箱に、44バイトをコピー)
#include <cstdio>
#include <cstring>
int main()
{
char buf[8];
std::printf("コピー前\n");
std::fflush(stdout);
strcpy(buf, "this string is much longer than eight bytes"); // 8バイトに入らない
std::printf("コピー後: %s\n", buf);
std::fflush(stdout);
return 0;
}Releaseビルドでは、コンパイルの時点で、別の警告(C4789)が出ました。
c6_overflow_cpy.cpp
C:\blogtest\c4996\c6_overflow_cpy.cpp(9) : warning C4789: 関数 'main' で バッファー 'buf' (サイズが 8 バイト) はオーバーランされます; 44 バイトがオフセット 0 から書き込まれます実行すると、次のようになりました(DebugビルドもReleaseビルドも、同じ結果でした)。
終了コード: 0xC0000409
--- 標準出力 ---
コピー前
コピー後: this string is much longer than eight bytesコピーは、実行されて、画面に表示までされました。8バイトの箱から44バイトがあふれて、その先のスタックを壊したのに、その場では何も止められていません。壊れたことに気づいたのは、プログラムが終わる段階で、終了コードは0xC0000409(スタックのバッファーオーバーラン)でした。
では、同じことをstrcpy_sでやると、どうなるでしょうか。
▼c7_overflow_cpy_s.cpp
#include <cstdio>
#include <cstring>
int main()
{
char buf[8];
std::printf("コピー前\n");
std::fflush(stdout);
strcpy_s(buf, sizeof(buf), "this string is much longer than eight bytes"); // 8バイトに入らない
std::printf("コピー後: %s\n", buf);
std::fflush(stdout);
return 0;
}終了コード: 0xC0000409
--- 標準出力 ---
コピー前Releaseでは、「コピー後」の表示が出る前に、プログラムが止まりました。コピーをする前に、サイズが足りないことを検出して、止めたということです。Debugビルドで動かすと、原因を表示してくれました。

Debug Assertion Failed!
Program: C:\blogtest\c4996\ov_strcpys_dbg.exe
File: minkernel\crts\ucrt\inc\corecrt_internal_string_templates.h
Line: 81
Expression: (L"Buffer is too small" && 0)
For information on how your program can cause an assertion
failure, see the Visual C++ documentation on asserts.
(Press Retry to debug the application)警告が言っていた「安全でない可能性」は、大げさではありませんでした。strcpyは、壊してから、あとで気づく。strcpy_sは、壊す前に止める。これが、実測で見えた違いです。警告を黙らせるだけだと、この違いを、手放してしまうことになります。
直し方:_s版の関数か、C++の標準機能に置き換える
①MSVCの_s版に置き換える
fopenはfopen_s、strcpyはstrcpy_s、sprintfはsprintf_sに変えます。書き込み先のサイズを引数で渡す形になり、fopen_sは戻り値で、成功か失敗かが分かります。
▼c4_safe.cpp
#include <cstdio>
#include <cstring>
int main()
{
FILE* fp = nullptr;
if (fopen_s(&fp, "test.txt", "w") != 0) // 戻り値(エラー番号)で成否が分かる
{
std::printf("ファイルを開けませんでした\n");
return 1;
}
char buf[16];
strcpy_s(buf, sizeof(buf), "hello"); // 書き込み先のサイズを渡す
sprintf_s(buf, sizeof(buf), "%d", 123);
fputs(buf, fp);
fclose(fp);
std::printf("書き込み完了: %s\n", buf);
return 0;
}c4_safe.cpp書き込み完了: 123警告は出ず、実行もできました。ただし、_s版の関数は、C言語の標準規格の付録に載っている扱いで、MSVC以外のコンパイラでは、使えない環境が多いです。他のコンパイラでも動かしたいコードでは、注意が必要です。
②C++の標準機能に置き換える
C++で書いているなら、そもそもfopenやstrcpyを使わない選択もあります。ファイルはstd::ofstream、文字列はstd::stringを使います。
▼c5_cpp.cpp
#include <fstream>
#include <iostream>
#include <string>
int main()
{
std::ofstream ofs("test.txt");
if (!ofs)
{
std::cout << "ファイルを開けませんでした\n";
return 1;
}
std::string s = "hello";
s = std::to_string(123);
ofs << s;
std::cout << "書き込み完了: " << s << "\n";
return 0;
}c5_cpp.cpp書き込み完了: 123こちらは、警告が出ず、他のコンパイラでも同じコードが動きます。バッファーのサイズを自分で管理しなくてよい点も、安全です。
別のC4996:C++の非推奨機能でも出る
C4996は、CRTの関数だけでなく、C++の標準ライブラリで非推奨になった機能でも出ます。例として、文字コードの変換でよく使われるstd::wstring_convertを、C++の規格(/std:)を変えて、ビルドしました。
▼c8_codecvt.cpp
#include <codecvt>
#include <locale>
#include <string>
int main()
{
std::wstring_convert<std::codecvt_utf8<wchar_t>> conv; // C++17で非推奨
std::string s = conv.to_bytes(L"hello");
return (int)s.size();
}| ビルドの規格 | 警告 |
|---|---|
/std:c++14 | 出ない |
/std:c++17 | 出る(C4996 / STL4017の4行) |
/std:c++20 | 出る(同じ4行) |
c8_codecvt.cpp(7): warning C4996: 'std::codecvt_utf8<wchar_t,1114111,(std::codecvt_mode)0>': warning STL4017: std::wbuffer_convert, std::wstring_convert, and the <codecvt> header (containing std::codecvt_mode, std::codecvt_utf8, std::codecvt_utf16, and std::codecvt_utf8_utf16) are deprecated in C++17. (中略) consider using MultiByteToWideChar() and WideCharToMultiByte() from <Windows.h> instead. You can define _SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING or _SILENCE_ALL_CXX17_DEPRECATION_WARNINGS to suppress this warning.
(同じ警告が、ほかに3行出た)文面の最後に、「MultiByteToWideChar()とWideCharToMultiByte()を使いなさい」と、置き換え先が書かれています。_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNINGを定義すれば消せますが、「規格が非推奨にした機能」なので、この場合は、置き換えるほうが長持ちします。
C4996が出たときの、よくある失敗例と対処法
①#defineを、#includeのあとに書く
実験で見たとおり、効きません。先頭に書くか、ビルド設定の「プリプロセッサの定義」で指定します。
②「前は警告だったのに」と、プロジェクトを疑う
警告がエラーになるのは、/sdlなどの設定の違いです。コードではなく、プロジェクトのプロパティを、前のプロジェクトと見比べます。
③警告を消したあと、サイズの確認を忘れる
_CRT_SECURE_NO_WARNINGSは、警告を消すだけで、strcpyの危険を減らしてはくれません。実験のとおり、あふれても、その場では止まりません。コピー先のサイズは、自分で確認します。
④置き換えるときに、strcpy_sの第2引数をsizeof以外にする
strcpy_sは、書き込み先のサイズを渡す関数です。実際より大きな値を渡すと、検出の意味がなくなります。配列なら、sizeof(buf)を使います。
注意点
- C4996は廃止予定・非推奨の通知。ビルドは通る。ただし
/sdlが有効だとエラーになる _CRT_SECURE_NO_WARNINGSは、#includeより前に書くか、ビルド設定の「プリプロセッサの定義」で指定する- 警告を消しても、
strcpyの危険は、そのまま。今回の実験では、8バイトの箱に44バイトが書き込まれた strcpy_sは、コピーの前に、サイズ不足を検出して止める(Debugでは原因を表示する)_s版は、MSVC以外では使えないことが多い。C++なら、std::stringやstd::ofstreamに置き換えるwstring_convertのように、規格で非推奨になった機能にも、C4996は出る- 今回の検証は、MSVC 19.44(Visual Studio 2022付属)で行った。警告の文面は、版によって変わりうる
結局、どうすればいい?
- 黄色い警告は「消す」より先に「読む」。文面に、置き換え先が書いてある
- 消す場合は、
/D_CRT_SECURE_NO_WARNINGSをビルド設定に入れる(位置の罠を避けられる) - 新しく書くコードは、
_s版か、C++の標準機能を使う - 規格で非推奨になった機能は、置き換え先に移るのが長持ちする
警告は、消せば静かになりますが、そのぶん、気づく機会も減ります。「出たら、一度は文面を読む」。それだけでも、strcpyが静かにスタックを壊すようなバグは、ずいぶん減らせます。


