入門書でのおさらい25・演習6-6,6-7、配列の受け渡し

今日は152ページの関数の設計から勉強します

153ページ・演習6-6

#include<stdio.h>
void  alert(int n)
{
    while(n-- > 0){
        printf("\a");
    }
}

int main(void)
{
    int n;
    puts("入力された回数警報を鳴らします");
    do{
        printf("回数:"); scanf("%d",&n);
        if(n < 1) puts("1回以上の回数を入力してください");
    }while(n < 1);
    alert(n);

    return 0;
}

【評価:合格(Perfect!)】

このプログラムの素晴らしい点は、「ユーザーへの配慮」と「正確な論理構造」が両立されていることです。

  1. 堅牢な入力チェックdo-while 文と if 文を組み合わせることで、1未満の不正な数値が入力された際に即座に警告を出し、再入力を促す仕組みが完璧に機能しています。
  2. 適切な関数の切り分け:警報を鳴らす処理を alert 関数として独立させているため、再利用性が高く、main 関数の中身もスッキリしていて読みやすいです。
  3. 効率的なループ処理alert 関数内の while(n-- > 0) という書き方は、C言語らしく非常に簡潔で、カウントダウンしながら処理を行う意図が明確に伝わります。

演習6-7

簡単な問題が続くね!

#include<stdio.h>
void hello(void)
{
    printf("こんにちは。\n");
}

int main(void)
{
    hello();

    return 0;
}

コードを確認しました。演習 6-7、一発合格です!

評価ポイント:

  • プロトタイプ通りの実装:引数を受け取らず、戻り値も返さない void hello(void) の形が正しく実装されています。
  • シンプルな main 関数:関数を呼び出すだけの明快な構成です。

ファイル有効範囲・宣言と定義

関数の外で宣言された識別子は宣言された場所からそのソースプログラムの終端まで名前が通用します

新明解C言語入門編第2版 柴田望洋 156ページ

ってあるけど外で宣言した変数は関数の中では使ってはいけない?

結論から言うと、関数の中で使っても全く問題ありません!

むしろ、複数の関数で同じデータを共有したいときに、あえて外で宣言して使うのが一般的です。

  1. 関数の中で宣言した変数(ローカル変数)
    • その関数の中でしか使えません。
    • 関数が終わると消えてしまいます。
  2. 関数の外で宣言した変数(外部変数 / グローバル変数)
    • 宣言した場所から下の、どの関数の中でも使えます。
    • プログラムが終わるまでずっと値を保持し続けます。
なぜ「使ってはいけない?」という疑問が出るのか

教科書などで「あまり使いすぎないように」と注意書きがあるかもしれません。それは以下の理由からです:

  • どこで値が変わったか分かりにくい:どの関数からも書き換えられるので、バグが出たときに犯人探しが大変になります。
  • 関数の独立性が下がる:その変数がないと動かない関数になってしまうため、他のプログラムに再利用しにくくなります。

ああごめん言い方が悪かった例えば関数外でint a;と宣言するじゃん?でメイン関数の中でうっかり同じ変数名でint a;って宣言しちゃって大丈夫かどうかを聞いてます

それは非常に鋭い質問です!結論から言うと、プログラムは動きますが、「関数の中の a」が優先され、外の a は隠れて見えなくなります。

これをプログラミング用語でシャドウイング(覆い隠し)と呼びます。

実際にどうなるか以下の例を見てください。

int a = 10; // 外側の変数(グローバル)

int main(void) {
    int a = 5; // 関数内の変数(ローカル)
    printf("%d\n", a); // 5 が表示される!
    return 0;
}
  • 関数の内側: main 関数の中で a を使うと、それは 5 が入ったローカル変数を指します。
  • 関数の外側: 外にある 10 が入った a は、main 関数の中からは一時的にアクセスできなくなります。
大丈夫かどうか?

「文法上はOK」ですが、「人間が混乱するのでNG」というのが現場の判断です。

  • 動く理由: C言語のルールで「より近い範囲(スコープ)の変数を優先する」と決まっているからです。
  • ダメな理由: プログラムを読んでいる人が「今触っている a はどっちの a だ?」と混乱し、バグの元になるからです。

外側の変数には g_a (globalのg)と付けたり、全く別の名前にしたりして、名前が衝突しないように工夫するのが「良いコード」への近道ですよ。

いっその事エラーにしてくれた方がわかりやすいけどな。つまりは別の変数として扱われるということかな?

まさにその通りです!名前は同じでも、メモリ上では「まったく別の住所にある、別人の変数」として扱われます。

「いっそのことエラーにしてほしい」という感覚は、実はエンジニアとして非常に正しい感覚です!プロの世界でも「紛らわしいことは書かない」のが鉄則なので、現代のプログラミング環境(コンパイラの設定)では、あえて「警告(Warning)」を出して「名前が重なってるけど大丈夫?」と教えてくれるように設定することも多いですよ。

あとプログラムの先頭部分で関数の宣言をして、メイン関数の下で関数の定義をする場合、宣言と、定義の二つがいるけど、先頭部分(メイン関数より後か前かで決まるのかな?何で決まるの?)で関数を定義しちゃうときは宣言は必要ないという認識でいいですか?

C言語の鉄則:関数は「使う前に教える」

C言語において、関数を呼び出す(使う)際のルールは「メイン関数との前後関係」ではなく、以下の位置関係ですべてが決まります。

「呼び出す側の関数」よりも物理的に「上の行(前)」に、「呼び出される側の関数の情報」が書かれていなければならない。

これは相手がメイン関数であっても、自作の関数同士であっても同じです。

「宣言」が不要なケース

呼び出される側の関数(定義)を、呼び出す側の関数よりも上の行に書く場合。
(コンパイラが上から順に読み進める際、使うときにはすでにその関数の正体を知っているため)

「宣言」が必要なケース

呼び出される側の関数(定義)を、呼び出す側の関数よりも下の行に書く場合。
(このままではコンパイラが「知らない関数が出てきた」とエラーを出すため、事前に「宣言(プロトタイプ宣言)」だけを上の行に書いて、正体を予告しておく必要があります)

結論

「メイン関数の前か後か」で決まるのではなく、「呼び出し地点より上に、定義か宣言のどちらかが存在するか」が、C言語のコンパイルを通すための絶対条件です。

160ページ配列の受け渡し

list6-11についてジェミニに質問しました。

聞きたいのですがint max_of(int v[], int n)関数の引数のことです。配列を入れてますが要素数は決まっていません。第2仮引数が要素数になるというのはルールですか?

結論から言うと、「第2引数を要素数にする」というのはC言語の絶対的なルール(文法)ではありません。しかし、実務上の「鉄則」と言えるほど一般的な書き方です。

関数側では配列のサイズがわからない

C言語の仕様では、関数に配列を渡すと、関数側からは「どこから配列が始まるか」という住所(ポインタ)しか見えなくなります。そのため、「どこまでが配列の範囲か」という情報(要素数)を別途教えてあげないと、関数内の for 文を何回回せばいいのか判断できません。

汎用性を高めるため

例えば、要素数を関数の中に直接 5 と書いてしまうと、その関数は「5個の配列専用」になってしまいます。
引数として n を受け取るようにしておけば、要素数が10個でも100個でも、同じ max_of 関数で対応できるようになります。

ルールではないので、以下のような書き方も可能です。

  • 引数の順番を変える
    int max_of(int n, int v[]) としても全く問題なく動きます。
  • 特別な値で終わりを示す(番兵)
    文字列(char型の配列)のように、最後に必ず \0 が入っているという約束事があれば、要素数を渡さずに済む場合もあります。

ちなみに、160ページの例題のように「配列」と「要素数」をセットで渡すスタイルは、C言語で最も頻繁に登場する形ですので、このまま慣れてしまうのが一番スムーズです!

なるほど。しかし普通に配列を宣言、確保する場合要素数を絶対決めないといけないはずです。

おっしゃる通りです!そこが混乱しやすいポイントですが、非常に鋭い視点です。

変数を宣言(確保)するとき」と「関数の引数(受け取り口)にするとき」では、C言語における意味が少し異なります。

宣言するとき:サイズが必須

おっしゃる通り、実際にメモリを確保する際は、コンピュータが「どれくらいの広さが必要か」を知る必要があるため、必ず要素数を決めなければなりません。

関数の引数のとき:サイズは「空欄」でいい

関数の引数にある int v[] は、実は新しい配列を作っているのではなく、「外にある配列の場所(住所)を教えてもらう」ための窓口に過ぎません。

すでにどこかで確保された配列の場所を指し示すだけなので、関数側では「とりあえず整数の配列(の先頭)が来るんだな」と分かればよく、中身の数を空欄にしたまま受け取れるようになっています。

むしろ、ここに数字を書いても無視されるという仕様になっています。

// 実はこれ、どれも「整数の配列の先頭を指す」という意味で同じ扱いになります
int max_of(int v[], int n)   // 一般的
int max_of(int v[5], int n)  // 5と書いても、実は無視されて「[]」と同じ扱い
int max_of(int *v, int n)    // ポインタという書き方(本質はこれ)
まとめ
  • main関数などで「作る」とき:サイズをビシッと決める(メモリ確保のため)。
  • 関数で「受け取る」とき:サイズは書かなくて良い(すでにある場所を教えてもらうだけだから)。

すごく紛らわしいのですは普通に引数の引数をint aと書いた場合、その変数はその関数内で使える上に宣言もしなくていいので引数に書くだけで宣言とメモリ確保をしていることにもなってますよね?ちがいますか

おっしゃる通り、その感覚は非常に正しいです!混乱するのも無理はありませんが、整理するとスッキリします。

普通の変数 int a の場合

おっしゃる通り、引数に int a と書くことは、その関数専用の「新しい変数(箱)を用意して、そこに値をコピーする」という動作になります。

  • 宣言: している(int a という名前で使える)。
  • メモリ確保: している(その関数が動いている間だけ、その箱のためのメモリが確保される)。

これを専門用語で「値渡し」と呼びます。

配列 int v[] の場合(ここが特殊!)

ここがC言語の「紛らわしい」最大のポイントです。
引数に int v[] と書いても、実は配列丸ごとのコピー(新しい箱の確保)は行われません。

  • 宣言: している(v という名前で使える)。
  • メモリ確保: 「配列の住所(番地)」を入れるための小さなスペースしか確保されません。
なぜ配列だけ扱いが違うのか?

もし要素数が100万個ある巨大な配列を関数に渡すたびに、メモリを確保してコピー(値渡し)していたら、コンピュータのメモリがすぐに足りなくなり、動作も遅くなってしまいます。

そのため、C言語では「配列を引数にするときは、実体ではなく『住所』だけを教え合うことにしよう」という特別なルールになっています。

まとめ
  • int a(普通の変数)
    新しい箱を作る(コピー)。関数の中で a を書き換えても、呼び出し元の元の変数は無事。
  • int v[](配列)
    新しい箱は作らない(住所だけ聞く)。関数の中で v[0] を書き換えると、呼び出し元の元の配列も書き換わる。

わかってきました。関数の引数の「int v[]」はイント型の配列の先頭の住所を渡すということですね。配列の宣言ではないと。そしてその配列は別の関数などでちゃんとどれだけ確保するか宣言されているものを渡すということですね?で、第2引数に別の関数で確保した要素数の数字をいれてやるというわけですか?

その通りです!完璧な理解です。

まさに仰る通りで、整理するとこうなります。

  1. 実体の確保: main関数などで int eng[5]; と宣言して、メモリ上に本物の箱を確保する。
  2. 住所の伝達: 関数を呼ぶとき max_of(eng, 5) と書くと、配列の先頭の住所だけが関数に伝わる。
  3. 窓口の用意: 関数の引数 int v[] は、送られてきた住所を受け取るための「名札(ポインタ)」として動く。
  4. 範囲の指定: 配列自体は自分のサイズを知らないので、第2引数の 5 を使って「そこから5個分まで見ていいよ」と教えてあげる。
なぜこれが便利なのか?

この仕組みのおかげで、max_of 関数は「どこの誰が作った配列か」を気にせず、住所と個数さえもらえれば仕事ができるようになっています。

  • mainで作った5個の配列でもOK
  • 別の関数で作った100個の配列でもOK
  • 配列の一部(例えば3番目から5個分)だけを渡すことも可能

「配列の宣言(箱づくり)」と「引数の宣言(住所の受け取り窓口)」を、頭の中でハッキリ切り分けられたのは素晴らしい進歩です!

変数の引数の時と働きがちがうと思っていいですね?先頭の住所をわたすと。

その通りです!「普通の変数は『中身のコピー』を渡し、配列は『先頭の住所』を渡す」という明確な違いがあります。

C言語において、この違いは非常に重要です。

  • 普通の変数 (int a)
    関数に「値」をあげる。関数の中で何をしても、呼び出し元の元の変数は汚れません(安心)。
  • 配列 (int v[])
    関数に「家の鍵(住所)」を貸す。関数の中で中身をいじると、呼び出し元の元の配列が直接書き換わります(要注意、でも便利)。

この「配列は住所を渡している」という性質があるからこそ、関数の中で v[i] と書くだけで、遠く離れた main 関数にある配列の中身を操作できるわけです。

v[]は配列の先頭の住所でいいんですね?? 関数を呼び出すとき配列名だけしかかけないですか?max_of(eng, NUMBER);→max_of(eng[3], NUMBER);のようにはできないということでいいですね?

おっしゃる通りです!その理解で100%正解です。

呼び出すときに eng[3] と書いてしまうと、それは「住所」ではなく、「3番目に入っている点数(数値)」を渡すことになってしまいます。

なぜ eng[3] ではいけないのか?
  • eng (配列名だけ): 「英語の点数が入っている場所(住所)」を指す。
  • eng[3] : 「英語の4番目の人の点数(例えば85点という値)」を指す。

関数の窓口が int v[](住所を受け取る窓口)になっている場合、そこに「85点」という値を放り込むと、コンピュータは「85番地という住所に行け」と勘違いしてしまい、エラーやバグの原因になります。

【応用編】でも、実はこんなことができます

「3番目以降の最大値を知りたい」という場合、C言語では以下のような書き方ができます。

  1. max_of(&eng[3], 2)
    &(アドレス演算子)をつけて「3番目の箱の住所」を渡します。
  2. max_of(eng + 3, 2)
    「先頭住所から3つ進んだ住所」という意味になります。

今はまだ「ポインタ」の章ではないので、「基本は配列名だけを書く!」と覚えておくのが一番安全で正しい進め方です。

max_of(&eng[3], NUMBER);は住所だからできるんだ?

その通りです!&(アドレス演算子) をつければ「住所」になるので、理屈の上では通ります。

ただし、一点だけ「要素数」の扱いに注意が必要です。

なぜ &eng[3] はOKなのか
  • eng[3] は「3番目の箱の中身(値)」ですが、
  • &eng[3] は「3番目の箱がある住所」になります。
    関数の引数 v[] は「住所」を待っているので、これは正解です。
注意点:残りの要素数

もし eng が全部で5個の配列(0, 1, 2, 3, 4)だとしたら、&eng[3] を渡すと関数は「3番目」を先頭だと思って処理を始めます。

このとき、第2引数に NUMBER(つまり 5)を渡してしまうと、関数は「3番目から数えて5個分」見ようとしてしまい、配列の範囲外(3, 4, 5, 6, 7番目…)に突っ込んでしまいます。

ですので、もし3番目以降を渡すなら:
max_of(&eng[3], 2); (3番目と4番目の2個分だよ、と教える)
とするのが正解です。

まとめ
  • eng → 先頭(0番目)からの住所を渡す。
  • &eng[3] → 3番目からの住所を渡す。

「住所さえ渡せば関数は動く」という本質をバッチリ掴めていますね!

とりあえずややこしいから今は先頭の住所渡しと覚えておくね

それが一番賢明です!最初から応用パターン(&eng[3] など)まで詰め込むと、C言語の迷宮に入り込んでしまいます。

まずは、「配列を関数に渡すときは、配列名だけを書いて『先頭の住所』を教える」という基本をしっかり自分のものにするのが、挫折しないコツです。