ここまでの冒険を記録しますか?

長岡技大ソフトウェア開発サークルの開発ブログ

C++で覚えるプログラミングのイロハ【ヘ】

書き手:Hidano

今回はプログラミングをするにあたって非常に重要となる、コンピュータの仕組み、具体的にはメモリの概念について解説します。

「メモリってあれでしょ? いろんなファイルを保存しておく場所」と思った方は、必ずこの章の内容を理解してください。プログラミングをするにあたってその認識は非常に危険です。

C/C++は数あるプログラミング言語の中でもメモリ空間を特に強く意識する必要のある言語で、逆にC/C++でメモリの概念を理解できていると、あらゆるプログラミングで効率化を図ったり、不具合を特定しやすくなります。

ちょっと脅すような前振りになってしまいましたが、モノを理解するのは心身がリラックスした状態でないと難しいですし、ここは肩の力を抜いてゆったりと読みすすめてもらえればと思います。

まず大前提として、ほとんどのコンピュータで共通する要素を確認します。

1.CPU

コンピュータの心臓部分である演算装置です。与えられた命令(私たちの書くプログラムも)を解釈し、計算結果を吐き出します。規模の大きい電卓と表現するべきでしょうか。

多くのパソコンにはCPUの種類を表すシールが貼ってあるかと思います。「intel inside」などと表記されているでしょう。

なおCPUは流れてくる命令をひたすら実行するだけで、データを永続的に蓄積する能力はありません。

2.主記憶装置

いわゆるメモリであり、今回扱う装置です。実行中のプログラムや、そこで宣言されたデータがここに保存されます。

コンピュータの基盤(マザーボード)に直接突き刺さっており、伝送速度が速いのが特徴です。

ただし、常に電気を流していないと情報を保存できないので、コンピュータの電源を切るとデータは消えてしまいます。

容量は最近だと4GBから16GB程度が主流です。PCゲームのサイトなどでは「RAM:8GB以上」などと書かれていて、これがメモリの必要容量を指します(RAMはランダムアクセスメモリの略)。

3.補助記憶装置

こちらは「ハードディスクドライブ(HDD)」と呼べば分かりやすいかと思います。最近はHDDだけでなくSSDもシェアを伸ばしていますね。

物理的にデータを書き込むため、電源を落としても情報を保存できることが特徴です。

一方でメモリに比べると伝送速度は大幅に遅く、連続で読み書きを行うファイルはメモリ上に一度移動してから処理しています。

容量は数百GBから1TBを超えるものも珍しくなくなってきましたね。ちなみにCDやDVD、USBメモリなどの外部記憶装置もこの補助記憶装置に分類される場合があります。

4.入力装置

キーボードやマウスなどです。詳しい説明は割愛。

5.出力装置

モニタやスピーカー等です。こちらも説明は省略します。

これらの構成を揃えたものが「コンピュータ」と定義され、パソコンに限らずプリンターやゲーム機などもコンピュータと呼ぶことができます。

ゲーム機の記憶装置

ファミコンをはじめとした初期のゲーム機は、カセットに内蔵された「ROM」という補助記憶装置にゲームのプログラムを保存していました。

これはリードオンリーメモリの略で、HDDなどとは違い書き込みができません。つまりゲームのセーブデータは保存できないのです(ゲームのプログラムはROM製造時に工場で記録されます)。

スーパーファミコンの頃からカセットに電池が内蔵されて、常にメモリへ通電することでセーブデータの保存ができるようになりました。

当時のゲームソフトは今ほとんど電池切れを起こしているので、こうしたカセットはセーブデータの保存ができなくなっています。

それでは、メモリの構造を大雑把に見ていきましょう。

コンピュータはよく「0と1ですべてが表現される」なんて言われますが、この0か1の2パターンの情報を1ビットと表現します。これが8つ集まると1バイになります。1バイトは2×2×2×2×2×2×2×2 = 28 = 256通りの情報を保存できます。

なお、ビットは小文字の「b」で、バイトは大文字の「B」で表現されます。

メモリは全体で4GBなり8GBなり一定の容量を持っていますが、それらは1バイトずつ番地が割り振られていて、例えば一番端の1バイトは0番地、その隣の1バイト分は1番地……といった具合です。

f:id:NUT_SoftwareDevelopper:20161122233934j:plain

なぜ4000000000番地ではなく = 4294967295番地になる?

1バイトの1000倍は1KB(キロバイト)、その1000倍は1MB(メガバイト)と覚えている方も多いのではないでしょうか。

実はこれは不正確な表現で、1KBは厳密には1024バイトなんです。一見半端な数に思えますが、実は2の10乗が1024で、根底の部分で2進数を扱うコンピュータにとっては、1000よりも1024の方が区切りの良い数なんです。

そのため4GBは1024×1024×1024×4バイトなので、電卓で弾くと4294967296となります。

さらに、画像の番地は1からではなく0から始まっているため、終端の番地は1マイナスした4294967295番地ということになります。

では、プログラム内で宣言された変数の番地を確認する簡単なプログラムを書いてみましょう。

#include<stdio.h>

int main() {
    int num = 0;
    printf("変数numのアドレスは%pです\n", &num);
    getchar();
    return 0;
}

たったこれだけです。

printfの引数numの頭についている「&」は「アドレス演算子」と呼び、変数の前に付けることでその変数のアドレスを参照することができるようになります。

表示するテキスト内にある「%p」は、よく使う「%d」のアドレス版です。「%d」を使うと10進数で表示されますが、メモリの番地は通常16進数で表記されるので、こちらを採用しました(もっとも、ゲームソフトなんか作っているとアドレスを表示したくなる場面は希ですが……)。

getchar()は何か入力があるまで待機する関数です。通常のビルドだと、最後まで処理が終わった時に自動でウィンドウが閉じてしまい出力結果が確認できない場合があり、そういう時にこの関数を挟んでやると出力を読むことができるようになります。

私の環境ではこのようになりました。

f:id:NUT_SoftwareDevelopper:20161215070717j:plain

変数が保存される番地はシステムの状態によって変わるので、値は一致しないと思います。

この画像では00CAFE08番地であることが分かりました。これは16進数なので、プログラマ電卓(Windows標準搭載の電卓の機能)で10進数に直してやると、13303304であることが分かりました。

ここまでで、変数が宣言されるとメモリのどこかに値が保存されることが分かりました。

単品で宣言したときはどこかいい感じのところに自動で割り振られるのですが、配列の場合はメモリ上に連続して配置されます。何となくイメージが付きますでしょうか。確認してみましょう。

#include<stdio.h>

int main() {
    int arg[3] = {100,200,250};
    printf("arg[0]のアドレスは%pです\n", &arg[0]);
    printf("arg[1]のアドレスは%pです\n", &arg[1]);
    getchar();
    return 0;
}

f:id:NUT_SoftwareDevelopper:20161216135931j:plain

おや、連続になるかと思ったら間が空きましたね。00AFF834と00AFF838の間の3バイトには何が入っているのでしょうか。

ここで考えるべきがデータの量です。多くの環境ではint型は4バイトで構成されており、~834から~838の4バイト分にその値が格納されています。

この仕組みがちょっと混乱を招きやすいのですが、今表示された「00AFF834」というのは、arg[0]の4バイト分の内、最初の1バイトが格納されているメモリの番地です。あとの3バイトにもarg[0]の値の一部が入っていますが、アドレス(=番地)から変数を指定する時はその変数が格納されているメモリの先頭のアドレスを指定する必要があり、逆に変数からアドレスを呼び出した場合は、その先頭の番地が表示されたというわけです。

ちなみに、int型が本当に4バイトなのか確認もしておきましょう。

#include<stdio.h>

int main() {
    int num = 0;
    printf("int型の変数のサイズは%dです\n", sizeof(num));
    getchar();
    return 0;
}

f:id:NUT_SoftwareDevelopper:20161216134409j:plain

変数のサイズはsizeof()で調べることができ、結果はバイト数で返ってきます(ビット数ではないので注意)。この例では変数名「num」を引数としていますが、直接「int」を書いても同じ結果を返してくれます。

int型のサイズについて

今回はint型のサイズは4であると説明しましたが、例外もあります。具体的にはCPUのビット数や、それに伴うコンパイラの仕様によって変動してきた歴史があったのです。

現在のCPUは32bitと64bitが混在していますが、過去には16bitや8bitが主流の時代もありまして、その頃にはint型も2バイトや1バイトでした。

ただ、int型のサイズがCPUのビット数に必ずしも等しいわけではなく、最新の64bit環境では32bitと同様に4バイトのケースがほとんどです。

もっとも、8バイトは10進数で20桁、92概(「億」「兆」「京」のさらに1つ上の桁)もの整数値を管理するケースはごくごく希でしょうし、int型が4バイトであることを前提に動作するプログラムが動かせなくなる(互換性が無くなる)デメリットを考慮すると、4バイトのままでいいのかなという気もしますね。

さて、変数や配列がメモリ上でどのように管理されているかの概要は理解できましたでしょうか。

しかしこれだけだと「ふーん、そうなんだ」で終わってしまいます。メモリの概念を理解する本当の意味は、「構造体での利用」と「動的確保」を使いこなすためにあると言っても過言ではないでしょう。

それについては長くなってしまうので次回にしたいと思います。