今日は172ページの有効範囲と記憶域期間を勉強します
172ページ・有効範囲と記憶域期間
List6-17で関数print_xだけ75になるのがよくわかりませんでした
その理由は、C言語の「スコープ(有効範囲)」と「名前の隠蔽(シャドウイング)」という仕組みにあります。
一言で言うと、「関数やブロックの中で宣言された変数は、その外側にある同じ名前の変数を隠してしまう」からです。
なぜ print_x だけが 75 なのか?
print_xの場合- この関数の内部には
xという変数が宣言されていません。 - そのため、関数の外(ファイル全体)で定義されている グローバル変数の
int x = 75;を探しに行って表示します。
- この関数の内部には
main関数の場合mainの冒頭でint x = 999;と宣言しています。- これ以降、
mainの中ではグローバルな75ではなく、自分に近い999の方が優先的に使われます。
forループの中の場合- さらに内側の
{ }でint x = i * 100;と宣言しています。 - ここでは
999よりもさらに自分に近い、このループ内限定のxが優先されます。
- さらに内側の
実行結果のイメージ
print_x()→ 自分の手元にxがないので、外の 75 を見る。printf("x = %d\n", x);(main内) → 自分のすぐ上にある 999 を見る。printf("x = %d\n", x);(for内) → ループの中で作った 0, 100… を見る。- 最後の
printf(main内) → forループが終わってx = i * 100は消滅したので、再び 999 を見る。
「より内側(近く)で宣言された変数が優先される」と覚えると分かりやすいですよ。
もし print_x で 999 を表示させたい場合は、引数を使って print_x(x); のように値を渡す必要があります。
「print_x() → 自分の手元に x がないので、外の 75 を見る。」ここだけがわかりにくかったのですがこの関数のすぐ上でx=999;してるでしょ?でこの関数の場所もmainの中にあるしどうして外まで見に行ったのかなって
なるほど、そこが混乱のポイントですね!
実は、C言語には「関数は、他の関数の中身(ローカル変数)を覗き見ることはできない」という鉄のルールがあるんです。
なぜ main の x = 999 が見えないのか?
- 関数の独立性:
mainも一つの関数、print_xも一つの関数です。たとえmainの中でprint_x()を呼び出していたとしても、print_xからすると、mainの中で何が起きているか(どんな変数が作られているか)は一切分かりません。 - 住所の違い:
mainの中のx = 999は、mainという家の中だけで有効な「ローカル変数」です。print_xは自分の家(関数)にxがないので、外(街全体)を見て、誰でもアクセスできる 「グローバル変数の 75」 を見つけました。
「関数が main の中にある」という誤解
コードの見た目上、main の中で print_x() と書いてあるので、「main の中に入っている」ように見えますが、これは「main が print_x を呼び出した(電話をかけた)」だけなんです。
main「print_xさん、実行して!」print_x「了解。えーと、xを表示するんだな。自分の手元には無いし、mainさんの家の中は勝手に見ちゃいけないルールだから……お、外(グローバル)に75があるな。これを使おう」
という動きをしています。
おおーー!なるほど「「関数が main の中にある」という誤解」をしてました!実際にはprint_xは記述してある場所(メイン関数外、すぐ上)にあって電話をかけて通信してるだけというイメージかな?
そのイメージ、100点満点の正解です!
まさに「電話をかけて通信している」状態です。
イメージを整理すると:
- 配置:
mainもprint_xも、同じファイルという「街」の中に建っている、別々の 「独立した家」 です。 - 通信:
mainの中でprint_x()と書くのは、mainの中からprint_xの家に 「電話をかけて呼び出している」 だけです。 - プライバシー: 電話越しに「実行して!」とは命令できますが、電話口の相手(
print_x)は、かけてきた相手(main)の家の中にある家具(ローカル変数x = 999)を見ることはできません。
なぜ上に書いてあるの?
C言語では、電話をかける前に「あ、その電話番号(関数名)は実在するな」と知っておく必要があるため、基本的には main より 上(先) に書くルールになっています。
記述場所が main の「外」にある以上、print_x にとっての「すぐ外」は、さらに広い 「街全体(グローバル領域)」 しかありません。だから 75 を見に行った、というわけです。
これでスコープの仕組みは完璧に攻略しましたね!
ブロック有効範囲が一番強力で優先されると思っていいかな
C言語のスコープ(有効範囲)には、「より狭い範囲(内側)で宣言されたものが、広い範囲(外側)のものを上書き(隠蔽)して優先される」という大原則があります。
優先順位を強い順に並べるとこうなります:
- 最強:ブロックスコープ(
{ }の中。forやifの中で宣言した変数) - 強い:関数スコープ(関数の引数や、関数の中で宣言したローカル変数)
- 標準:ファイルスコープ(関数の外に書いたグローバル変数)
今回のコードのfor文の中が良い例です。
「今自分がいる場所から一番近い(内側の){ }にある変数」が常に優先されます。
この「ブロックが優先される」仕組みがあるおかげで、大きなプログラムを作るときに「外側でどんな名前の変数が使われているか」をいちいち気にせず、その場限りの変数(ループ用のiなど)を安心して作れるようになっているんです。
この「内側優先」のルール、バッチリ掴めましたね!
int x =999;の部分をint x = x;としてもxは75にはならないんだって
そうなんです、そこがまたC言語の面白い(そして少し意地悪な)ところです!
int x = x; と書いた瞬間、コンパイラはこう判断します。
「よし、今ここで新しく x という箱を作るぞ。で、その中身には……今作ったばかりのこの x の値を入れよう!」
つまり、外側の 75 を見に行く前に、「自分自身(中身が空っぽでゴミデータが入った状態)」 を参照してしまうんです。
なぜ 75 にならないのか?
- 名前の予約:
int xと書いた時点で、そのブロック内ではグローバルのx(75)への道が遮断されます。 - 初期化のタイミング: 右側の
xを評価するとき、すでに「新しく作られたx」が優先されているため、まだ値が決まっていない自分の値を自分に入れようとしてしまいます。
結果として、75にはならず、デタラメな数値(ゴミ値)が入ったり、コンパイラに「それ怪しいよ!」と怒られたりします。
もし「グローバルの 75 を使って 999 の方を初期化したい」なら、変数の名前を int my_x = x; のように変えるか、特殊な書き方(externなど)が必要になります。
int x = x;では、左辺を作った瞬間に入ってた、左辺xのゴミデータを入れるんですね
その通りです!「左辺で名前を決めた瞬間、もう右辺の x は自分自身を指してしまう」という、ちょっとした落とし穴なんです。
これを専門用語で「未定義の動作」と呼びます。
- 箱の名前を
xと決める。 - その箱にはまだ何も入っていない(ゴミデータの状態)。
- そのゴミデータを、自分自身(
x)に入れ直す。
結果、中身は謎の数字のまま……ということになります。
ちなみに、もしどうしても「グローバル変数の x(75)」を、同じ名前のローカル変数 x の初期化に使いたい場合は、C言語(特にC++などで有名ですが)ではちょっと工夫が必要です。ただ、基本的には「名前を被らせない」のが一番の安全策ですね。
記憶域期間
C言語の記憶域期間(Storage Duration)ですね!プログラム中で変数が「いつまでメモリ上に存在し続けるか」を決める重要なルールです。
大きく分けて4つの種類がありますが、まずは基本の3つを押さえましょう。
1. 自動記憶域期間 (auto)
いわゆる「ローカル変数」です。
- 寿命:
{で宣言された場所から、そのブロック}を抜けるまで。 - 初期化: 自動ではされません(ゴミデータが入っています)。
- 例:
int x;(関数の内部で宣言)
2. 静的記憶域期間 (static)
プログラムの開始から終了までずっと居座るタイプです。
- 寿命: プログラムが動いている間ずっと。
- 初期化: 明示しない場合、
0で初期化されます。 - 例:
- 関数の外で書く(グローバル変数)
- 関数内で
static int count = 0;と書く(関数を抜けても値を保持する)
3. 割り当て記憶域期間 (allocated)
いわゆる「動的メモリ」です。
- 寿命:
mallocで確保してから、freeで解放するまで。 - 管理: プログラマが責任を持って消さない限り、メモリに残ります(放置するとメモリリークの原因に)。
静的記憶域期間がわからなかったのであれこれ聞く
今日は1と2を学習するだけです。1は今まで使ってきた変数のことなのでとても分かりやすいけど2はもう少し教えてほしい。
staticをつけなくても関数の外で宣言されたものは静的記憶域期間が与えられるの?
はい、その通りです!関数の外(ファイルスコープ)で宣言した変数は、static を付けなくても「静的記憶域期間」になります。
プログラムが始まった瞬間にメモリが確保され、終わるまで消えません。
では、なぜわざわざ static と書く場合があるのか、その違いを整理しましょう。
関数の「外」で static をつける場合
これは寿命(期間)のためではなく、「見える範囲(スコープ)」を制限するために使います。
staticなし(グローバル変数): 他のファイルからも中身が見えるし、書き換えられる。staticあり: そのファイル内だけでしか使えない「ファイル専用の変数」になる。
関数の「中」で static をつける場合
これは「寿命」を延ばすために使います。
staticなし: 関数が終わると消える(自動記憶域期間)。staticあり: 関数が終わっても消えず、次に呼び出したときに前回の値を覚えている(静的記憶域期間)。
「静的記憶域期間」になる条件は以下の2パターンです:
- 関数の外で宣言する(
staticの有無に関わらず期間は一生)。 - 関数の中で
staticを付けて宣言する。
関数の外でstaticをつけなくても静的記憶域期間が与えられた変数は0で初期化されるの?
はい、0で初期化されます。
C言語のルールでは、「静的記憶域期間」を持つ変数は、明示的に値を書かなかった場合、コンパイル時に自動的に「0」で初期化されると決まっています
具体的には以下の2つが対象です:
- 関数の外で宣言した変数(
staticの有無を問わず) - 関数の中で
staticを付けて宣言した変数
関数の中で static を付けて宣言した変数の初期化のタイミングはメイン関数の実行が開始される前なんだって。
正確には、プログラムがメモリ上にロードされた際、main関数の一番最初の行が実行されるよりも前に、静的変数の領域が確保され、初期化も完了しています。
これには面白い性質が2つあります。
- 初期化は「一生に一度」だけ
関数の中にstatic int count = 0;と書いてあっても、その関数が呼ばれるたびに 0 にリセットされるわけではありません。「実行前に 0 にセット済み」なので、関数の中ではその行は「2回目以降は無視」されるイメージです。 - 定数でしか初期化できない
実行前に初期化し終えていないといけないので、static int n = get_number();のように、実行してみないと値がわからない関数などは初期化に使えません。(※C++などの後続言語ではできるようになっていますが、C言語ではNGです)
static int count = 0;のような宣言がメイン関数内に書いてあってもメイン関数より前に初期化されるの?
はい、その通りです!たとえ main 関数の中で static を使って宣言したとしても、初期化されるタイミングは main 関数が動き出す前になります。
「関数の中に書いてあるから、実行がそこにたどり着いた時に初期化されるのかな?」と思いがちですが、コンパイラは static という文字を見た瞬間に「あ、これは実行前に準備しておく一生モノの変数だな」と判断して、特別なメモリ領域に 0 をセットして用意してしまいます。
ですので、以下のような挙動になります。
#include <stdio.h>
int main() {
// mainの開始より前に、すでにメモリ上に 0 として存在している
static int count = 10;
printf("%d\n", count); // 10 が表示される
return 0;
}ここがポイント:
もしこれが static のない普通の変数(int count = 10;)なら、main の実行がその行に到達した瞬間に「10を代入する」という命令が動きますが、static の場合は「最初から10が入った状態でスタート」します。
「書かれている場所」はスコープ(使える範囲)を決めているだけで、「初期化のタイミング」は static というキーワードで決まる、と考えるとスッキリするかもしれません。
ちなみに静的のほうは0以外でも初期化出来て何にもしなければ0にしてくれるってことだよね?
その通りです!完璧な理解です。
まとめるとこうなります:
- 値を指定した場合:その値で初期化される。(例:
static int n = 10;なら 10 からスタート) - 何も書かなかった場合:自動的に 0 で初期化される。(例:
static int n;なら 0 からスタート)
一方、普通の変数(自動記憶域期間)は「何も書かないとゴミデータが入る」ので、この「何もしなくても0にしてくれる」というのは静的記憶域期間ならではの、ちょっとした優遇措置(仕様)ですね。
破棄されるのもメイン関数の後なの?
はい、その通りです!main 関数が終了(return)した後、プログラム自体が終了するタイミングで破棄されます。
「静的記憶域期間」という名前の通り、その変数の寿命はプログラムの実行期間(プログラムの開始から終了まで)と完全に一致しています。
イメージとしてはこんな感じです:
- プログラム起動:
mainが始まる前に、静的変数のメモリが確保され、初期化される。 main実行中: ずっとメモリ上に居座り続ける。main終了: OSにメモリを返却する(ここで破棄される)。
ちなみに、関数の中で宣言した static 変数も、関数を抜けたからといって消えることはありません。見えなくなるだけで、メモリ上には「次に呼ばれるまで」じっと値を保持したまま待機しています。
メイン関数の閉じ括弧のあとなの?
厳密に言うと、メイン関数の閉じ括弧(})を通り過ぎて、プログラムが完全に終了する時です。
流れを細かく分解するとこうなります:
mainの最後でreturn 0;する(または閉じ括弧に到達する)。main関数という「場所」から脱出する。- C言語の実行環境(スタートアップルーチンといいます)が、「後片付け」を始める。
- その後片付けの中で、メモリが解放され、静的変数が消滅する。
つまり、main の閉じ括弧の「直後」に行われるOSへの返却作業の中で破棄される、というイメージです。
これで、生まれるのも main の前、消えるのも main の後という、まさに「プログラムの全人生」を共にする変数だということがわかりますね!
静的記憶域期間のことはよくわかったよ。メイン関数で宣言された自動記憶域期間の変数は閉じ括弧の時に破棄?
その通りです!大正解です。
main 関数の中で宣言された普通の変数(自動記憶域期間)は、main 関数の閉じ括弧 } に到達した瞬間に役割を終え、メモリから破棄されます。
今日のまとめとして、両者の「人生」を比較するとこうなります:
- 自動変数(いつもの):
mainの実行中に生まれ、mainの}で死ぬ。 - 静的変数(static):
mainの実行前に生まれ、mainの}の後(プログラム終了時)に死ぬ。
autoとregister
ここでautoとregisterの話が出てきましたが詳しくわかりやすく教えて
auto と register は、どちらも「自動記憶域期間」の仲間ですが、現代のプログラミングではほとんど見かけない「レアキャラ」です。
わかりやすく解説しますね。
1. auto(自動)
実はこれ、私たちが普段使っているローカル変数の正体です。
- 役割: 「この変数は自動記憶域期間ですよ」と明示するキーワード。
- 現状: C言語では、関数の内部で宣言した変数は書かなくても勝手に
auto扱いになります。 - 例:
int x;とauto int x;は全く同じ意味です。 - なぜ使わない?: 書かなくてもいいので、誰も書かなくなりました。
2. register(レジスタ)
これは「この変数はよく使うから、メモリじゃなくて、CPUの中の爆速な場所(レジスタ)に置いて!」とコンピュータに頼むキーワードです。
- 役割: 処理を高速化するためのリクエスト。
- 制限:
- メモリ上にないので、住所(アドレス
&)を取得できません。 - レジスタの数には限りがあるので、必ずしも採用されるとは限りません。
- メモリ上にないので、住所(アドレス
- 現状: 最近のコンパイラは非常に賢いので、人間が指定しなくても「あ、この変数はよく使うからレジスタに入れよう」と自動で最適化してくれます。そのため、今では人間がわざわざ書く必要はほぼありません。
今日の「記憶域クラス」まとめ
| キーワード | どこに置かれる? | 寿命 | 特徴 |
|---|---|---|---|
| (auto) | メモリ(スタック) | { } の中だけ | 普段使っている変数。 |
| register | CPU(レジスタ) | { } の中だけ | 高速化のお願い。今はほぼ使わない。 |
| static | メモリ(静的領域) | プログラム終了まで | 今日学習した「一生モノ」の変数。 |
結論:auto と register は「昔の名残」に近い存在です。
今のC言語学習では、「普通のローカル変数は auto と呼ばれるものなんだな」、「register という高速化のおまじないがあったんだな」 くらいの理解で十分ですよ!
これで記憶域期間の分類はバッチリ見えましたか?それとも、「レジスタ」という言葉が少し引っかかりますか?
ひっかかります。この表のメモリ(静的領域)とメモリ(スタック)ってなにがちがうの
メモリの「静的領域」と「スタック」の違いですね。これは、「整理整頓された本棚」と「机の上に積み上げられた書類」の違いに例えるとわかりやすいです。
静的領域(本棚)
プログラムが始まる前から終わりまで、ずっと場所が確保されている領域です。
- 住人:
static変数やグローバル変数。 - 特徴:
- 場所が固定: プログラムが動いている間、その変数の「住所」はずっと変わりません。
- 寿命が長い: プログラムが始まった瞬間に用意され、終わるまで片付けられません。
- 例: 辞書や図鑑のように、ずっとそこにあって、いつでも参照できるイメージです。
スタック(机の上の積み上げ書類)
関数が呼び出されるたびに、一時的に「使い捨ての作業スペース」として積み上げられる領域です。
- 住人: 普通のローカル変数(自動変数)。
- 特徴:
- 出し入れが激しい: 関数が呼ばれるとメモリが「積まれ」、関数が終わるとその分が「バサッと捨てられ」ます。
- 寿命が短い: その関数が実行されている間だけ存在します。
- 例: 今やっている作業のメモ用紙。終わったらゴミ箱に捨てるので、机の上(スタック)はいつも次のお仕事のために空けられます。
なぜ分けているの?
もし全部を「静的領域」に置いてしまうと、使っていない関数の変数までずっとメモリを占領してしまい、メモリが足りなくなってしまいます。
- 静的領域があるおかげで、「ずっと覚えておきたい大事なデータ」を保存できる。
- スタックがあるおかげで、「その場限りの作業」を効率よく、メモリを節約しながら行える。
この2つがあることで、コンピュータは賢くメモリを使い分けているんです
もう一つひっかかります。registerに入れる変数ってどんな変数でどんな処理の時なのか
「レジスタ(register)」という言葉、いかにも機械っぽくてイメージしづらいですよね。
簡単に言うと、「CPUのすぐ隣にある、超高速だけど超狭い作業机」のことです。
昔のプログラマーが register を使って「これ、レジスタに入れて!」と頼んでいたのは、以下のようなケースです。
どんな変数?
- ループのカウンター(
iやjなど)- 何万回、何億回と読み書きされるので、いちいち遠くのメモリ(倉庫)に取りに行くより、手元の机(レジスタ)に置いておいたほうが速いからです。
- 計算の途中の値
- 一時的に何度も足し引きするような変数です。
どんな処理のとき?
- 巨大なループ処理
register int i;
for (i = 0; i < 100000000; i++) {
// ここで爆速で計算したい!
}- リアルタイム性が求められる処理
- 音声処理や画像処理など、1ミリ秒の遅れも許されないような計算で使われていました。
今は使われない
実は、今のコンピュータとコンパイラ(翻訳ソフト)は人間より圧倒的に賢いです。
- 自動で判断する: コンパイラがコードを読んで「この
iはめちゃくちゃ使うから、勝手にレジスタに入れておこう」と自動でやってくれます。 - 人間のお願いを無視する: 人間が
registerと書いても、コンパイラが「いや、こっちの変数のほうが大事だから、人間のお願いは無視してこっちをレジスタに入れよう」と判断することもあります。
唯一の注意点
register と指定した変数は、CPUの中に置かれる(=メモリ上に住所がない)という扱いになるので、&i のようにアドレス(住所)を取り出そうとするとエラーになります。 これが最大のルール上の違いです。
「昔は『これ、よく使うから手元の速い机に置いて!』と指示するために register を使っていたけど、今はコンピュータにお任せでOK」という感じです。
演習6-14
これはList6-19をそのまま使えばいいから簡単です
#include<stdio.h>
int main(void)
{
static double sa[5];
for(int i = 0; i < 5; i++){
printf("sa[%d] = %f\n", i, sa[i]);
}
return 0;
}完璧です!正解です。
static を付けて宣言したことで、初期化を省略しても全要素がしっかり 0.000000 と表示されたはずです。
ポイントは以下の2点を見事に押さえているところです:
static double sa[5];とすることで、main関数の外に出さなくても「静的記憶域期間」を持たせている。%fを使って、double型の初期値が0.0であることを視覚的に証明している。
もし static を消して実行すると、デタラメな数字(ゴミデータ)が表示されるので、その違いこそが今日の学習の核心ですね。
演習6-15
#include<stdio.h>
void put_count()
{
static int cnt;
cnt++;
printf("put_count:%d回目\n", cnt);
}
int main(void)
{
int n;
puts("関数を呼び出す回数を入力してください");
do{
printf("回数:");
scanf("%d",&n);
if(n < 0) puts("正の整数を入力してください");
}while(n < 0);
for(int i = 0; i < n; i++){
put_count();
}
return 0;
}素晴らしい!100点満点、完璧なプログラムです。
このコードの特に素晴らしいポイントは以下の3点です。
static int cnt;の初期化省略
先ほど学習した「静的変数は何も書かなければ0になる」という性質を活かして、あえて初期値を書かずにスマートに記述できていますね。cnt++の位置
呼び出されるたびにカウントを増やし、それを表示する。まさにstaticの「値を保持し続ける」という特徴を正しく使いこなしています。- 入力バリデーション(
do-while)
負の数が入力された時のエラーチェックまで入っていて、プログラムとしての完成度が非常に高いです。
もし cnt が static ではなく普通の自動変数だったら、何度呼び出しても毎回 1回目 と表示されてしまいます。それがちゃんと 1, 2, 3... と増えていく様子を見て、static のパワーを実感できたのではないでしょうか。
cnt++;
printf(“put_count:%d回目\n”, cnt);
ここ表示させた後にカウントさせるか迷いました
その迷い、プログラミングにおいて非常に健全で素晴らしい感覚です!
今回の演習が「何回目」という言葉だったため、以下の2パターンのどちらが自然かで迷われたのだと思います。
- 先に足す(あなたのコード)
- 初期値が 0 なので、1回目に呼ばれた時に
0+1=1となり、「1回目」と表示される。 - 直感的で、今回の問題にはこの書き方が最もスマートです。
- 初期値が 0 なので、1回目に呼ばれた時に
- 表示した後に足す
printf("put_count:%d回目\n", cnt + 1); // 表示だけ1増やす
cnt++;または static int cnt = 1; と初期化して、表示後に cnt++ する。
これでも動きますが、コードが少し複雑になります。
結果として、あなたの「まずカウントを最新の状態にしてから、その数字を出す」というやり方が、最もバグが少なく、読みやすい正解の書き方です!
