今回は変数について、深堀してくので見てってください。
低レイヤーのために。
メモリアドレスとは?
まず、メモリは連続した大量の箱が並んでいます。その箱は1バイトごとに区切られてその箱の位置を表す アドレス が定義されています。
下の表では1段目がアドレス、2段目が1バイト分のデータです。
| 0x0000 | 0x0001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 | 0x0006 | 0x0007 |
|---|---|---|---|---|---|---|---|
| 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 | 0x00 |
PCなどでは、メモリ16GBであれば、16ギガ個のアドレスがある感じです。
例えばint型であれば4バイトなので、メモリの箱を4つ使って値を保持していますね。
試しに 0x0002番~0x0005番の4バイトに1億の値を保存した場合 どうなるか...
| 0x0000 | 0x0001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 | 0x0006 | 0x0007 |
|---|---|---|---|---|---|---|---|
| 0x00 | 0x00 | 0x05 | 0xF5 | 0xE1 | 0x00 | 0x00 | 0x00 |
変数は箱ではない
先ほどのメモリについてを踏まえたうえで、変数はこの メモリアドレス に名前を付けたものプログラマーがわかりやすく名前を付けたものです。
それも使っているすべてのメモリアドレスではなく、開始地点のメモリアドレスに名前を付ける感じです。
先ほどの1億のint型、このようにコードがある場合、変数numは0x0002のことを指します。
int num = 100000000;
| 0x0000 | 0x0001 | 変数num | 0x0003 | 0x0004 | 0x0005 | 0x0006 | 0x0007 |
|---|---|---|---|---|---|---|---|
| 0x00 | 0x00 | 0x05 | 0xF5 | 0xE1 | 0x00 | 0x00 | 0x00 |
変数は箱ではなく、メモリアドレスに名前を付けたもの
型は "読み方のルール"
ここまでで「変数はメモリアドレスに付けた名前」という理解ができたと思います。
では 型(int, char, float など) は何をしているのでしょうか?
結論から言うと、 型は「そのアドレスから何バイトを、どう解釈するか」 を示しています。
先ほどのメモリ状態をもう一度見ます。
| 0x0000 | 0x0001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 | 0x0006 | 0x0007 |
|---|---|---|---|---|---|---|---|
| 0x00 | 0x00 | 0x05 | 0xF5 | 0xE1 | 0x00 | 0x00 | 0x00 |
この0x0002をint型で読むと 0x0002~0x0006 の4バイトをまとめて読み、
int型0x0002番 → 100000000
ですが、もし、0x0002をchar型で読むと 0x0002 の1バイトを読むことで、
char型0x0002番 → "5"
といった感じで値が読み取れます。
大事なのは メモリの中身は同じでも、型が違うと「見える値」が変わる ということ。
配列は変数の並び
配列は先ほどの変数が並びます。
メモリアドレスが長くなるので、4バイトでまとめたら下記
| 0x0000 | 0x0004 | 0x0008 | 0x000C | 0x0010 | 0x0014 | 0x0018 | 0x001C |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
このコードのように配列を定義した場合、numの指すアドレスが0x0004であれば...
int num[4] = {10, 20, 30, 40};
| 0x0000 | 0x0004 | 0x0008 | 0x000C | 0x0010 | 0x0014 | 0x0018 | 0x001C |
|---|---|---|---|---|---|---|---|
| 0 | 10 | 20 | 30 | 40 | 0 | 0 | 0 |
言語ごとの違い
共通の事実
- どの言語でも最終的には、
- メモリは アドレス付きの連続領域
- CPUは アドレスを指定して読み書き
という点は同じです。
違うのは「プログラマがどこまで直接触れるか」 です。
C言語:ほぼ制限なし
特徴
- メモリアドレスをそのまま扱える
- 型変換(キャスト)が自由
- 境界チェックなし
低レイヤー部分 - 「配列」「変数」という概念は ほぼ存在しない
- あるのは アドレス + 読み方(型)
👉 OS・組み込み・ドライバ向き
👉 人間が責任を負う前提の言語
C++:自由だが「自己防衛用の仕組み」が増えた
特徴
- Cと同じことができる
- しかし std::array, std::vector など安全な抽象がある
低レイヤー部分
- 危険な操作は可能
- だが「安全に書く道」も用意されている
👉 パフォーマンスと安全性の折衷
Rust:メモリは触れるが「条件付き」
特徴
- 配列境界チェックあり
- 生ポインタは unsafe が必要
- 所有権・借用で書き換えを制御
低レイヤー部分
- 安全な世界と危険な世界が明確に分離
- 「壊すなら、ここから先は自己責任」と明示される
👉 現代的な低レイヤー言語
Java / C#:メモリを「概念として」扱う
特徴
- アドレスを直接扱えない
- 配列は必ず境界チェック
- GCがメモリ管理
低レイヤー部分
- プログラマは アドレスの存在を忘れてよい
- 代わりに、細かい制御はできない
👉 業務・安全・生産性重視
Python / JavaScript:変数は「箱ですらない」
特徴
- 変数は参照
- 配列(list)は実体ではなくオブジェクト
- メモリ配置は言語処理系依存
低レイヤー部分
- 「連続メモリ」という保証すらない
- CPUキャッシュ効率などは 完全にブラックボックス
👉 低レイヤー理解は概念止まり