ProgrammingEmbedded C Developer

C言語におけるreturn演算子の動作メカニズムについて説明してください。その構文と意味論の詳細、関数から値を正しく返す方法、値無しのreturnと式を伴うreturnの違い、さらに返される構造体、ポインタ、局所変数に関連する落とし穴について教えてください。

Hintsage AIアシスタントで面接を突破

答え。

問題の歴史

return演算子は、関数の作業を明示的に終了し、呼び出しコードに結果を渡すためにCで登場しました。初期のプログラミング言語では値を返す機能が常に存在したわけではなく、returnメカニズムによって計算結果を明示的に指定できるようになりました。これによりプログラムの表現力と安全性が向上しました。

問題

主な課題は、関数を正しく終了させ、必要に応じて特定の型に対応する値を返すことです。間違った型の値を返したり、存在しないポインタや局所変数を返したり、呼び出し側が返された値を無視したりすることでエラーが多く発生します。

解決策

  • return; はvoid型の関数(何も返さない)にのみ使用されます。
  • return expression; はvoid以外の型の関数に使用され、指定された値を返すことで関数を終了します。
  • 返される値の型は、関数のプロトタイプで宣言された型と正確に一致する必要があります。
  • 構造体を返す際は構造体のコピーが返されます。ポインタを返す際はアドレスのコピーが返されます。
  • 局所変数へのポインタを返すことは危険です(関数を出ると消去されます)。

コード例:

#include <stdio.h> struct Point { int x, y; }; struct Point make_point(int x, int y) { // 構造体を返す(コピー) struct Point p = {x, y}; return p; } int* dangerous() { int num = 42; return &num; // 危険:局所変数のアドレスを返す! } void do_nothing() { return; // void型の関数に対しては正しい } int main() { struct Point p = make_point(3, 4); printf("%d %d\n", p.x, p.y); int* ptr = dangerous(); // UB: ptrは解放された領域を指す }

主な特徴:

  • returnは関数の実行を即座に終了させます。
  • 返される値の型は宣言された型と一致しなければなりません。
  • 構造体/オブジェクトを返す際はコピーが発生し、参照は返されません。

騙し質問。

値を持たない関数(void)でreturnを使用できますか?

答え:はい、「return;」を書くことはvoid関数で可能ですが、式(return x;)をvoid関数で指定することはできません。

関数から配列を返すとどうなりますか?

答え:Cでは配列を直接返すことはできません。ポインタ(例:静的配列へのポインタ)を返すことはできますが、より一般的にはポインタとサイズを返すか、動的に割り当てた配列を使用すべきです。

int* make_arr() { static int arr[5] = {1,2,3,4,5}; return arr; // 静的配列は関数を出た後も存在し続ける }

局所変数へのポインタを返すことが危険な理由は何ですか?

答え:関数を出ると局所変数に対するメモリは解放されます(スタック領域)。返されたポインタを使うことは未定義の動作を引き起こします。

一般的なエラーとアンチパターン

  • スタック上の変数に対するポインタの返却
  • 返される式の型と関数の型の不一致
  • 値を返すと宣言された関数でreturnパスをスキップすること

実生活の例

ネガティブケース

関数が局所変数へのポインタを返し、呼び出し側が「ゴミ」を受け取り、予測できない動作やまれなバグが発生します。

利点:

  • 実装が迅速

欠点:

  • ランダムなクラッシュ、関数終了後のスタックの変更によってデータが破損する可能性。

ポジティブケース

返される構造体を使用する(値でコピー)または静的/動的メモリへのポインタを返す:

利点:

  • 挙動が予測可能
  • 「ぶら下がり」ポインタなし

欠点:

  • 大きな構造体をコピーするのは高額になることがある、または明示的なメモリ解放を覚えておく必要があります。