C言語基礎|オブジェクトとアドレス

ポインタの前にここ!C言語は「値」より先に「場所(アドレス)」を理解すると一気にラクになる。

ポインタって聞くと、いきなり * やら & やらが出てきて「うっ…」となりがちですよね。
でも大丈夫。ポインタの正体は、実はすごく素朴で、

  • オブジェクト(変数や配列の要素など)はメモリ上に置かれていて
  • それぞれに 住所(アドレス) がある
  • その住所を扱う仕組みがポインタ

…という流れなんです。

なのでまずは、ポインタの前段として「オブジェクトって何?」「アドレスって何?」を、ゆっくり丁寧に固めていきましょう。

オブジェクトとは(C言語でいう「置き場所つきのデータ」)

C言語でいう オブジェクト(object) は、ざっくり言うと

値を入れておける“実体”があって、メモリ上のどこかに置かれているもの

です。

オブジェクトに含まれる代表的な性質

性質何のこと?
どう解釈するかint, double など
大きさ何バイト使うかsizeof(int), sizeof(double)
アドレスメモリ上の場所&n, &x
記憶域期間いつからいつまで存在するか自動/静的/動的 など
今入っている内容n = 10 など

この中でも、ポインタに直結するのが アドレス です。

「バラバラな箱」から「メモリの一部」へ(図でイメージ)

これまで変数を「箱」として見てきたのは正しい理解です。ただし実際の世界では、その箱は メモリという大きな空間のどこかに並んで置かれています。

図:2つの見方(同じものを別の角度から)

図A:バラバラの箱として見る(直感)

図B:メモリ空間の一部として見る(実態)

※番地やサイズは環境で変わります。ここでは説明用の例です。

アドレスとは(オブジェクトの住所)

アドレス(address) は、

オブジェクトがメモリ上のどこに置かれているかを表す番地

のことです。

さっきの図Bだと、n の先頭が 1000 なら「n のアドレスは 1000」という感じです。
この「先頭」の意味も大事で、複数バイトにまたがる場合でも 代表として先頭アドレス をアドレスとして扱います。

sizeofで分かる「大きさ」とアドレスの関係

オブジェクトには大きさがあります。大きさがあるということは、メモリ上で 領域を占有します。

型の大きさの例(代表例)

大きさの例備考
char1バイトほぼ固定
int4バイト多くの環境
double8バイト多くの環境

※正確な値は処理系で変わるので、必ず sizeof で確認できます。

アドレス演算子 &(オブジェクトの住所を取り出す)

オブジェクトのアドレスを取り出すのが 単項の &(アドレス演算子) です。

単項 & 演算子(アドレス演算子)

書き方意味
&aa のアドレス(a が置かれている場所)

ここでの & は、ビットAND(2項演算子の &)とは別物です。
1つの値に対して使っていたらアドレス演算子2つの値を並べていたらビットAND、という見分けが基本になります。

サンプルプログラム

「変数と配列要素のアドレス」を表示して、配列が連続して並ぶ雰囲気もつかむシンプルな例です。

プロジェクト名:chap10-2-1 ソースファイル名:chap10-2-1.c

#include <stdio.h>

int main(void)
{
    int    score = 10;
    double rate  = 1.25;
    int    data[3] = {100, 200, 300};

    puts("メモリ上の住所(アドレス)を確認します。");

    printf("score のアドレス  : %p\n", (void *)&score);
    printf("rate  のアドレス  : %p\n", (void *)&rate);

    printf("data[0]のアドレス : %p\n", (void *)&data[0]);
    printf("data[1]のアドレス : %p\n", (void *)&data[1]);
    printf("data[2]のアドレス : %p\n", (void *)&data[2]);

    return 0;
}

実行結果のイメージ(例)

メモリ上の住所(アドレス)を確認します。
score のアドレス  : 0x7ffd2c9a1a3c
rate  のアドレス  : 0x7ffd2c9a1a30
data[0]のアドレス : 0x7ffd2c9a1a20
data[1]のアドレス : 0x7ffd2c9a1a24
data[2]のアドレス : 0x7ffd2c9a1a28

アドレスの値や桁数、並び方は環境で変わります。
ただし配列は多くの場合、要素が連続して並び、int なら 4バイト刻みっぽく増えるのが見えてきます。

表示に使った命令(関数)と書式、何をする?

ここで出てきた命令(関数)は puts と printf です。

puts

  • 書式:puts(文字列);
  • 何をする?:文字列を表示し、最後に改行も出します
  • 特徴:シンプルに1行出すのが得意

printf

  • 書式:printf(書式文字列, 引数1, 引数2, ...);
  • 何をする?:書式に合わせて値を表示します
  • 例:printf("score のアドレス : %p\n", (void *)&score);

%p と (void *) の意味(アドレス表示の作法)

アドレスを表示するときの変換指定は %p です。p は pointer 由来です。

なぜ (void *) にキャストしているの?

printf の %p は、「ポインタ値(アドレス)を表示する」ための指定です。
Cでは %p に渡す値は void * 型 に合わせるのが作法なので、(void *)&score のように書くと安全で読みやすいです。

%p のポイント

項目内容
変換指定%p
渡す値(void *) にしたポインタ(例:(void *)&score)
表示形式多くの環境で16進数っぽい見た目

配列要素のアドレスが「規則的」に増える理由

配列は基本的に 同じ型の要素が連続して並ぶので、要素のアドレスはだいたい次の関係になります。

  • &data[1] は &data[0] より int 1個分 だけ後ろ
  • &data[2] は &data[1] より int 1個分 だけ後ろ

図:配列は横並び(連続領域)

この「連続して並ぶ」が、次に学ぶ ポインタ演算配列とポインタの関係 に効いてきます。

注意:register 変数は & できない(古典ルール)

古いCの考え方では、register 指定された変数は「レジスタに置くかも」なので、メモリ上の住所が取れない扱いになり、& を付けるとエラーになり得ます。

ただ、最近のコンパイラでは最適化の都合もあり、register 自体あまり使われません。
試験や文法として「&できないことがある」を知っておく、くらいでOKです。

まとめ(ここが腹落ちするとポインタが楽)

  • オブジェクトはメモリ上の実体で、性質として 型・大きさ・アドレス を持つ
  • アドレスは「置かれている場所(番地)」
  • アドレスは & で取り出せる。
  • 表示は %p を使い、(void *) にキャストして渡すのが作法
  • 配列要素は連続して並ぶので、アドレスが規則的に増える。