見出し画像

私がC言語を好きな理由

私がC言語を好きな理由。

ポインタ変数があるから

これにつきる。
C言語しか使っていなかった当初はわからなかったが、いくつかの言語に触れる都度、ポインタ変数が欲しくなる。やりたいことができない。もどかしいことこの上ない。どうも私は、何か制約があるのが苦手なようである。


ポインタを持たない言語たち

javaにも、VBにも、C#にも、Pythonにもポインタ変数はない。ないのではあるが、ポインタ変数を全く使っていないかというとそうではない。プログラマーの見えないところでポインタ変数は使っているのである。正確に確認したわけではないが使っているはずだ。これらの言語ではライブラリが豊富であるが、C言語と比べてこれだけ多いのには「ポインタ変数がないから」ということも理由の一つである。すなわち、ポインタが必要になりそうなデータ構造をライブラリ化し、プログラマーに提供している。「プログラマーからポインタを隠蔽している」と言い換えてもいい。ポインタが必要なアルゴリズムやデータ構造をライブラリ化するわけだ。従って、ある程度ライブラリを把握していないと使えるアルゴリズムが制約されるということでもある。もちろん、ポインタなしでも実現できるアルゴリズムもある。但し、処理時間が大幅に遅くなる。

ポインタは不具合を生みやすい。加えてその不具合の内容は致命的である。ポインタを排除した言語にとっては、そういった危険極まりないポインタを有象無象魑魅魍魎が跋扈するプログラマー達の自由にするなどまかりならん、というわけである。プログラマーはどんどんCPUから引き離されていく。もやは蚊帳の外である。


CPU

CPUはレジスタとメモリを駆使して動作する。
ハードディスクをアクセスしたり、モニタに文字を表示したり、あるいはグラフィックで高度な画像を表示したり、カメラから映像を入力したり、マイクから音声を入力したり、ネットワークを通して遥か遠くのサーバーやなアクセスしたりとコンピューターの入出力は多岐に渡るが、CPUはそんなことは知らない。

レジスタとメモリ

たったそれだけのデータをあっちに待って行ったりこっちに持って来たり、あるいは足したり引いたりと、その程度のことしかしていない。

加減乗除の四則演算、論理和、論理積、排他的論理和
そして条件分岐

CPUができる計算はたったそれだけなんである。
これらの演算に展開できないとCPUは何もできない。微分も知らなければ積分も知らない。方程式さえ知らない。いろいろな言語に数学のライブラリが用意されているが、数学のいろいろを上記の演算子に展開したものである。方程式を解くのは、解くというよりも解を探すと言った方が的確である。探す方法はいろいろあってそれらをアルゴリズムという。結構、大変なんである。


レジスタ

レジスタはCPUによって異なるが、CPUが決まればどのようなレジスタがあるのかが決まる。昔は8個程度しかなかったが最近はレジスタの数も多い。だから、C言語のコンパイラもレジスタを使うことが多くなった。アセンブラでプログラムするときはレジスタをどう使うかが重要になる。メモリからレジスタにデータを取り出し、演算し、その後再びメモリに戻す。ほとんどこれらの繰り返しである。


メモリ

では、メモリはどうやってアクセスするのか。
全メモリに次のようにアドレスが割り振られているのである。

0x00000000~0xFFFFFFFF

これは32ビットの場合である。この16進数表記を10進数に直すと次のようになる。

0~4,294,967,295

これは 4GB になる。昔は40MBのハードディスクさえ大海を得たような気分だったが、最近はメモリでさえ4GBを超えてきた。とすると、32ビットでは全メモリをアドレシングできないことになる。そこで、ポインタ変数のサイズが64ビットに拡張された。そうすると、ポインタ変数の範囲はこのようになる。

0x0000000000000000~0xFFFFFFFFFFFFFFFF

10進数だとこんな感じだ。

・・・。

と思ったが、電卓でもExelでも計算できない。4G✕4Gというのはとにかくとんでもない数字だ。1Gで0が9つなので、1G✕1Gは0が18個ということになる。

  • k キロ 0 が3つ

  • M メガ 0 が6つ

  • G ギガ 0 が9つ

  • T テラ 0 が12個

  • P ペタ 0 が15個

  • E エクサ 0 が18個

であるので、
4GB ✕ 4GB = 16 EB
ということになる。大きさがよくわからん。

とにかくも、CPUはこのアドレスで指定されるメモリの内容を、足したり引いたり、あるいは別のアドレスへコピーしたり移動したりをしているわけだ。たったそれだけのことをせっせと繰り返す。

例えば、次のようなコードがあったとする。

        x = 5;
        y = 6;
        z = x + y;

このような場合、例えば次のようにメモリに展開される。

x、y、zのアドレスを表示すると次のようになる。

ソースコード
#include <stdio.h>
int main()
{
    int x = 0;
    int y = 0;
    int z = 0;
    
    x = 5;
    y = 6;
    z = x + y;
    
    printf("x : %p %d\n", &x, x);
    printf("y : %p %d\n", &y, y);
    printf("z : %p %d\n", &z, z);
}

実行結果
x : 0x7fd80d6a4c 5
y : 0x7fd80d6a48 6
z : 0x7fd80d6a44 11

さっきの絵とアドレスが違っているがご容赦願いたい。実行する度に違うアドレスに割り当てられるんである。次に実行した時にどこに割り当てられるのかは、さっぱりわからない。OS次第である。なんで「x」の方がメモリの後ろの方にあるのかというと、オート変数をスタックに積んでいるからだ。スタックについてはこちらでも書いた。

さて、C言語の場合、変数「x」のアドレスは「&x」と書いて知ることができる。

この例の場合、変数「x」のアドレスは「0x7fd80d6a4c」である。「&x」と書けば「0x7fd80d6a4c」という数値を得ることができる。

任意の変数のアドレスを知ることができるのはわかった。その逆はどうだろうか。
任意のアドレスの変数にアクセスできるのであろうか。そう。C言語の場合はそれも可能である。例えば、これら、x, y, z はそれぞれ 4byte のサイズであるが、これを1byte単位でアクセスしてみる。先のコードにこんなコードを入れてみる。

unsigned char* p = (unsigned char*)(&z);
p -= 0x10;
int i;
for (i = 0; i < 0x30; i++)
{
    if ((i & 0x0f) == 0x00) {printf("%p:", &p[i]);}
    printf("%02x ", p[i]);
    if ((i & 0x0f) == 0x0f) {printf("\n");}
}

x, y, z が並んでいる(であろうと思われる)場所から16byte遡ってそこから 0x30byte を表示している。

実行結果はこうなる。

0x7fdde5c7d0:00 00 00 00 04 00 00 00
0x7fdde5c7d8:d0 c7 e5 dd 7f 00 00 00
0x7fdde5c7e0:0b 00 00 00 06 00 00 00
0x7fdde5c7e8:05 00 00 00 00 00 00 00
0x7fdde5c7f0:00 c8 e5 dd 7f 00 00 00
0x7fdde5c7f8:f8 00 58 b9 78 00 00 00

ほらほら、見えるでしょう?
x と y と z が。
ちょっと色を付けてみましょう。

x と y と z の位置

x が 5
y が 6
z が 11 (16進数で0b)
となってます。みんな仲良く並んでいる。

ちなみに、 x がなんで
00 00 00 05
じゃあなくって
05 00 00 00
なんだというのは、私が使っているスマホのCPUがARM だからであって、こういう風にメモリでの並びが逆順になるタイプを「リトルエンディアン」という。逆順ではなく「00 00 00 05」のように昇順に配置するCPUもあるわけで、昇順の場合は「ビッグエンディアン」という。

ともかくも。
ポインタというのはメモリのアドレスであって、ポインタを使うということは、アドレスをランダムにアクセスできるということである。

変数 x は、「x」と書く以外の方法でもアクセスできるというのがポインタの成せる技なのである。

では、ポインタを使った方がいいデータ構造とは、どんなものだろうか。

なんだか長くなってきたので、今回はここまで。

この記事が気に入ったらサポートをしてみませんか?