C言語入門|関数活用の落とし穴

ここまで、本当にお疲れさまでした。
第8章では、関数についてじっくり学んできましたね。

  • 引数と戻り値の役割
  • 値の受け渡しの正体
  • 構造体や配列を扱う関数

「もう関数は使いこなせている気がする!」
そんな手応えを感じている人も多いと思います。

ですが―
ちょうど今が、最も危険なタイミングでもあるのです。

String型っぽいものを定義してみると…

これまで自然に使ってきた「文字列」について、改めて考えてみましょう。

typedef char String[1024];

一見すると、
「String型=便利な文字列型」
のように見えます。

しかし、これはあくまで char型の配列 です。
ここを誤解した瞬間から、落とし穴が始まります。

String型に課せられた厳しいルール

この String型っぽいものには、次のようなルールがあります。

ルール内容
配列サイズ1024を小さくしてはいけない。
1024文字すべて使えると思ってはいけない。
初期化以外で = 代入してはいけない。
計算や連結に使ってはいけない。
== で比較してはいけない。
関数の引数や戻り値に使ってはいけない。
この章のラストで理由を整理します。

「えっ!?
配列や文字列を引数で渡す話、さっきまでしてましたよね?」

はい、そのとおりです。
今までは学習を進めるために、あえて踏み込んでいただけなのです。

なぜ、そこまで慎重になる必要があるのか

正直に言います。

C言語において、
関数の引数や戻り値として
文字列や配列を扱うことは、

非常に高いリスクを伴います

これは初心者だけの話ではありません。

  • メモリの破壊
  • 想定外のデータ書き換え
  • 原因不明の暴走
  • システム全体への影響

こうした事故は、
熟練エンジニアであっても実際に起こしています。

これまでのサンプルは「安全な例」だけだった

これまで紹介してきたコードは、

  • サイズが合っている。
  • 寿命の問題が出ない。
  • 偶然トラブルが起きない。

という条件を満たしたものだけでした。

つまり私たちは、

事故が起きないルートだけを歩いていた

という状態だったのです。

関数と配列が組み合わさると何が起きる?

次のような関数を見てみましょう。

// 指定点以上のスコアを持つ人数を数える関数
// 引数:点数配列、基準点
// 戻り値:人数
int countQualified(int scores[5], int border)
{
    int count = 0;

    for (int i = 0; i < 5; i++) {
        if (scores[i] >= border) {
            count++;
        }
    }

    return count;
}

呼び出し側です。

int scores[5] = {72, 88, 91, 60, 79};
int result = countQualified(scores, 80);

printf("条件を満たした人数は %d 人です\n", result);

実行結果

条件を満たした人数は 2 人です

正常に動いています。
しかし、ここに大きな前提があります。

配列は「値渡し」ではない

関数に渡されているのは、

  • 配列の中身
    ではなく
  • 配列の先頭アドレス

です。

つまり関数は、

呼び出し元の配列を
直接操作できる立場

にあります。

うっかり書き換えれば、
呼び出し元のデータは壊れます。

これが、関数活用に潜む最大の落とし穴です。

構造体なら安全なのか?

構造体の場合は、少し事情が異なります。

// ステータスを更新する関数
// 引数:プレイヤー構造体
// 戻り値:更新後のプレイヤー
Player updateStatus(Player p)
{
    p.level += 1;
    return p;
}

Player hero = {"Knight", 5};
hero = updateStatus(hero);

printf("レベルは %d になりました\n", hero.level);

表示結果

レベルは 6 になりました

構造体は基本的に 値としてコピー されます。
そのため、配列よりは安全です。

ただし、
構造体の中に配列やポインタを含めた瞬間、
再び危険領域に入ります。

「使える」ことと「使っていい」は違う

ここまでの話をまとめると、こうなります。

  • 書けるから安全とは限らない。
  • 動くから正しいとは限らない。
  • 便利だから使うのは危険

だからこそ、実務や本格開発では、

  • グローバル変数を避ける。
  • 配列や文字列を直接渡さない。
  • 管理できる形に変換する。

といったルールが生まれました。

複合リテラル:その場で集成体型の実体を生み出す

とはいえ、

「一度作って、すぐ関数に渡すのは面倒…」

という場面もあります。

Student s = {"Bob", 76};
printStudent(s);

そんなときに使えるのが 複合リテラル です。
近年の C 言語では、複合リテラル(compound literal)という構文を使って、関数呼び出しの式の中で集成体型の実体を生み出し、即時利用できるようになっています。

書式

(型名){初期化指定}

構造体の例

printStudent( (Student){"Bob", 76} );

配列の例

int total = sumValues( (int[4]){10, 20, 30, 40} );

その場で生成し、
使い終わったら消える。

この書き方は、
安全に使える場面がはっきりしています。


新しい「最後のルール」

ルール⑦
C言語は「仕組みを理解して初めて安全に使える言語」である。
仕組みを知らないまま便利さだけを使ってはいけない。

このルールを意識できたとき、
C言語は「怖い言語」から
「信頼できる道具」に変わります。