プログラム書いたり、もの作ったり。あと育児とか。

プログラム書いたり、もの作ったりすることについて書いていく予定です。あと育児とかも書くかもしれません。

ポインタとメモリ その2

前回:ポインタとメモリ その1 - プログラム書いたり、もの作ったり。あと育児とか。

値渡しと参照渡し

C言語では、色んな所で実は同じように値のコピーが行われます。

ここに、ポインタの有用性が出てきます。

一番わかりやすいのは関数の引数における「値渡しと参照渡し」でしょう。

// 値渡し
void func_val(int fa, int fb)
{
    fb = fa;
    return;
}

// 参照渡し
void func_ref(int *pa, int *pb)
{
    *fb = *fa;
    return;
}

int a = 0x10;
int b = 0x20;

func_val(a, b);
printf("a=%d, b=%d", a, b);
// a=16, b=32となり、func_valではbの値は更新されない

func_ref(&a, &b);
printf("a=%d, b=%d", a, b);
// a=16, b=16となり、func_refではbの値は更新される

値渡しの場合は、関数の中でfb=faとやっても、もともとの変数には影響を与えません。これは関数が呼ばれるときに引数の値はスタック領域と呼ばれるメモリ上の別な場所にコピーされるため、関数の中の引数(faとかpa)と関数を呼ぶ前の引数(aとかb)の実体は別々のものになります。

つまり、アドレスが異なると思ってもらって良いです。

例:もとのaとbのアドレスが0x2000, 0x2001、関数引数のfaとfbのアドレスが0x2100と0x2101、などそんな感じ

アドレスが異なっていれば、そりゃあ関数の中で書き換えを行ってももとの変数には影響与えませんよね。

メモリのイメージで説明するとこんな感じです。


func_val()呼ぶ前

0x2000 | 10 20 xx xx xx xx xx xx 
...
0x2100 | xx xx xx xx xx xx xx xx

func_val()に入った直後

0x2000 | 10 20 xx xx xx xx xx xx 
...
0x2100 | 10 20 xx xx xx xx xx xx

b=aの直後

0x2000 | 10 20 xx xx xx xx xx xx 
...
0x2100 | 10 10 xx xx xx xx xx xx

func_val()を抜けたあと

0x2000 | 10 20 xx xx xx xx xx xx 
...
0x2100 | xx xx xx xx xx xx xx xx

じゃあ、参照渡しの場合はどうなるんでしょう。


func_ref()呼ぶ前

0x2000 | 10 20 xx xx xx xx xx xx 
...
0x2100 | xx xx xx xx xx xx xx xx

func_ref()に入った直後

0x2000 | 10 20 xx xx xx xx xx xx 
...
0x2100 | 00 20 01 20 xx xx xx xx

pb=paの直後

0x2000 | 10 10 xx xx xx xx xx xx 
...
0x2100 | 00 20 01 20 xx xx xx xx

func_ref()を抜けたあと

0x2000 | 10 10 xx xx xx xx xx xx 
...
0x2100 | xx xx xx xx xx xx xx xx

func_refに入った直後、 0x2100 | 00 20 01 20 xx xx xx xx

こうなってますけど、まさにこれこそがポインタなのです。

(仮定として、16ビット系、リトルエンディアンとして書いてます。)

つまりpa(0x2100) = 0x2000, pb(0x2102) = 0x2001が代入されており、paがポインタなので*paと書くと、もともとの変数(0x2000の変数)に対して操作することができるわけです。

あくまで、関数がコールされるときにスタックに値がコピーされることは同じです。ですが、そのコピーされるものが値ではなくアドレスであり、さらにポインタという仕組みがあるため、そのアドレスの値にアクセスすることができるのです。

これによって関数の外側の変数を変更することができるというわけです。

ポインタの利点

これがわかると、ポインタがなぜ利用されるのかがわかってきます。

例えば、構造体。

構造体を関数の引数に渡すとき、そのまま渡したら構造体の中身がすべてスタックにコピーされる事になります。構造体の大きさがそこまで大きくなければ影響も少ないかもしれませんが、大きくなってくるとスタックにコピーされる量も大きくなり、メモリ的にも動作的にも無駄が大きくなってきます。

ところが、これをポインタで渡すようにすると、構造体の先頭アドレスをコピーするだけ済みます。

typedef struct sample_s
{
    int num1;  // 2byte
    int num2;  // 2byte
    char str[16];  // 16byte
} sample_t;

void proc_sample_val(sample_t sample)
{
    // コピーされたsampleにアクセスする
    // スタックへ20byte分すべてコピーされる
    // 元の変数sample1は変更されない
    sample.num2 = sample.num1;
    sprintf(sample.str, "val");
}

void proc_sample_ref(sample_t *sample)
{
    // sample1のアドレスにアクセスする
    // スタックはsampleのアドレスサイズ分(今回は2byte)だけ消費される
    // 元の変数sample1も変更される
    sample->num2 = sample->num1;
    sprintf(sample->str, "ref");
}

sample_t sample1;

sample1.num1 = 10;
sample1.num2 = 20;

proc_sample_val(sample1);
printf("val: %d, %d", sample1.num1, sample1.num2);
// val: 10, 20

proc_sample_ref(&sample1);
printf("ref: %d, %d", sample1.num1, sample1.num2);
// ref: 10, 10