【配列とポインタ】第7回 そもそも変数とは何か?(1)

今まで、さんざんポインタと配列をいじり倒しながら試行錯誤してきましたが、そろそろ「そもそも変数とは何か?」という本質論について考える時が来たようです。

プログラミングの入門書によくある説明で、変数とは「番号のついた箱」と言われます。データ格納領域と位置情報の比喩だと思いますが、変数のキモはそれだけじゃないです。

変数のキモ、それは…

  1. データの場所
  2. データのサイズ
  3. データを解釈する方法

この3つです。

データの場所

これは早い話、変数にアドレス演算子&をつけると表示される、その変数のアドレスのことです。

変数という「箱」には、1バイトごとにアドレス番号が割り振られているわけですが、そのアドレス番号を確認するには、アドレス演算子&の戻り値を確認する方法と、nmコマンド(GNU binutils)で確認する、二つのやり方があります。

まず、アドレス演算子&で、変数のアドレスを確認します。

  1 /* sample.c */
  2 #include <stdio.h>
  3 
  4 int         gl_val;
  5 const char  cn_val = -128;
  6 
  7 void func(void)
  8 {
  9     return;
 10 }
 11 
 12 int main(void)
 13 {
 14     static int  st_val;
 15     static int  st_val_ini = -1;
 16     auto int    at_val;
 17 
 18     printf("%p : gl_val\n", &gl_val);
 19     printf("%p : cn_val\n", &cn_val);
 20     printf("%p : st_val\n", &st_val);
 21     printf("%p : st_val_int\n", &st_val_ini);
 22     printf("%p : at_val\n", &at_val);
 23     printf("%p : func\n", func);
 24     printf("%p : main\n", main);
 25 
 26     return 0;
 27 }

# 実行結果

$ ./sample
0x804a02c : gl_val
0x8048550 : cn_val
0x804a028 : st_val
0x804a020 : st_val_int
0xbf894a8c : at_val
0x804841c : func
0x8048422 : main

つぎに、nmコマンドで各変数と関数のアドレスを確認します。

$ nm sample
08048550 R cn_val
0804841c T func
0804a02c B gl_val
08048422 T main
0804a028 b st_val.1819
0804a020 d st_val_ini.1820

nmコマンドによって確認できる、変数とアドレスの紐付け表のことを、一般的に「シンボルテーブル」と呼びます。

シンボルテーブルのアドレスと、アドレス演算子で表示されたアドレスが、一致していますね。また、func()やmain()などの関数のアドレスも表示されています。関数にも、その実行コードが格納される領域があるからです。なお、at_valの領域は、実行時に動的にスタックに確保されるため、シンボルテーブルには出てきません。

アドレス タイプ シンボル名 型修飾子 初期値指定の有無
08048550 R cn_val const あり
0804841c T func (なし) -
0804a02c B gl_val (なし) なし
08048422 T main (なし) -
0804a028 b st_val static なし
0804a020 d st_val_ini static あり

各タイプの意味は、以下の通りです。大文字の場合グローバルスコープ、小文字の場合はローカルスコープのシンボルになります。

シンボル 領域の割り当て先 割り当て先の意味
R/r 読取専用領域 定数が配置される書き込み不可領域
T/t .textセクション 命令コードが配置される領域
D/d .dataセクション 初期化指定ありの変数が配置される、読み書き可能領域
B/b .bssセクション 初期化指定なしの変数が配置される、読み書き可能領域

アドレス演算子&やnmコマンドによって、生のアドレスを示すことは
・シンボルの正体をさらす
と言っていいでしょう。

よくIPアドレスの説明で、「生のIPアドレスは人間が覚えにくいので、人間が覚えやすい名前にひもづけられている」という説明がありますが、あれと同じことです。例えば、「203.216.247.225」は「www.yahoo.co.jp」に紐づいているように、メモリ上のアドレス「08048550」は「cn_val」に紐づいています。言い換ええれば、アドレス演算子とは名前解決の演算子である…といってもいいでしょう。手計算でメモリアドレスを割り出して、変数名を使わずに生のアドレスをポインタ型にキャストして任意の領域にアクセスすることもできますが、それは手間なので、人間に分かりやすい「変数名」に換えて、プログラマーに扱いやすくしているわけです。

これは、main()などの関数についても同じことが言えます。関数や変数の生アドレスにひもづく名前を総称して、「シンボル」と呼びます。

だから、なぜ「num」とかの変数名を使うのか?というと、それはメモリ上の特定領域にアクセスするため…です。アセンブリコードを読んだりすると感じますが、大切なのは「アドレス」で、シンボルはプログラミングの生産性を上げるために、一時的につけたラベルみたいなものです。コンパイル済みのオブジェクトファイルを逆アセンブルすると分かりますが、コンパイルしてオブジェクトファイルを生成した時点で、シンボルはすべて生のアドレスに置換されています(リンクされていない外部シンボルは除く)。

次回につづく…