無限再帰を試行した後、スタックが枯渇するために両方の関数がクラッシュすることを理解していると思います。あなたが求めているのは、cout の例が「スタック オーバーフロー」でもクラッシュしないのはなぜですか?
答えは、コンパイラによる末尾再帰の検出と関係があるとは思いません。コンパイラが再帰を最適化した場合、どちらの例もクラッシュしません。
私は何が起こっているかについて推測しています。「スタック オーバーフロー」例外は、場合によっては (Windows など)、スタックの最後に割り当てられた単一の仮想メモリ「ガード ページ」で実装されます。スタック アクセスがこのガード ページにヒットすると、特殊な例外タイプが生成されます。
Intel の小粒度ページの長さは 4096 バイトであるため、ガード ページはそのサイズのメモリ範囲をガードします。関数呼び出しが 4096 バイトを超えるローカル変数を割り当てる場合、そこからの最初のスタック アクセスが実際にはガード ページを超えて拡張される可能性があります。次のページは予約されていないメモリであることが予想されるため、その場合はアクセス違反が発生します。
もちろん、例ではローカル変数を明示的に宣言していません。operator<<() メソッドの 1 つが 1 ページ以上のローカル変数を割り当てると仮定します。つまり、operator<<() メソッドまたは cout 実装のその他の部分 (一時オブジェクト コンストラクターなど) の開始付近でアクセス違反が発生するということです。
また、あなたが書いた関数であっても、 operator<<() の実装は、中間結果用のストレージを作成する必要があります。そのストレージは、おそらくコンパイラによってローカル ストレージとして割り当てられます。ただし、あなたの例では合計で 4k になるとは思えません。
本当に理解する唯一の方法は、アクセス違反のスタック トレースを見て、どの命令がそれを引き起こしているかを確認することです。
アクセス違反のスタック トレースと、障害のあるオペコードの領域周辺の逆アセンブリを取得しましたか?
Microsoft C コンパイラを使用している場合は、別の可能性として、printf() と独自の関数が /Ge でコンパイルされ、operator<<() がコンパイルされていないか、関数のみが /Ge でコンパイルされ、記述されているものと同様の要因が含まれている可能性があります。上記の偶然の一致により、表示される動作が発生します。これは、printf() の例では、関数が呼び出されているときにクラッシュが発生し、operator<<() の場合はライブラリを呼び出しているときに発生するためです。