プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

C++を頑張る(5)

今日は久しぶりにC++をやります。
メモリ等についてのお話のあたりを勉強します。

メモリとアドレス

メモリとは、プログラムを実行する際の作業領域だそうです。
ディスク上にある実行ファイルに多少手を加えたものをメモリ上に読み出して、
そのメモリ上のデータを元にプログラムが実行されるという感じ。
ディスクはデータをずっと保存しておくためのもので、メモリは一時保存する場所。
メモリに置かれるのはファイルの中身だけでなく、プログラムの変数なども置かれるそうです。

というわけで関数も変数もメモリ上の「どこか」にそれぞれ置かれるはずで、
その「どこか」のことを「アドレス」と呼ぶそう。
アドレスは基本的にバイト単位の通し番号になっているのですが、そう単純でない場合もあると。

アドレスを知る方法ですが、変数ならば変数名の前に&をつけるだけで、
配列や関数ならその名前を書くだけでいいそうです。

#include <iostream>
using namespace std;

void Foo(){
    int a;
    char b[10];
    cout << "a    : " << (size_t)&a << endl;
    cout << "b    : " << (size_t)b << endl;
    cout << "Foo  : " << (size_t)Foo << endl;
}

int main(){
    Foo();
}

(size_t)によるキャストは、結果を符号なしの10進数で表示するためだそう。
結果はこうなりました。

a    : 140732920755460
b    : 140732920755518
Foo  : 4294971568
Program ended with exit code: 0

ふむ。bは配列なので、140732920755518というのはb[0]のアドレスですね。
b[9]は140732920755529になるのだと思います。
aはint型なので、140732920755460から140732920755463を占領しているはず。

Fooだけ全然違うところにいますね。どれぐらいの場所を使っているのかは想像できません。
ただ、恐らく関数と変数には割り当てられるアドレスが違うだろうことがわかります。
全然違ってたら恥ずかしいですが。

ポインタ

プログラミング初心者は皆ポインタで挫折すると聞いたことがあります。
挫折したらLispに行きます。

ポインタとは、先程出てきたアドレスを格納するための変数だそうです。楽勝やんけ!
実際に見てみます。

int main(){
    char a;
    char* p = &a;
    
    cout << "aのアドレスは " << (size_t)&a << " です。" << endl;
    cout << "pの値は      " << (size_t)p << " です。" << endl;
}
aのアドレスは 140732920755519 です。
pの値は      140732920755519 です。
Program ended with exit code: 0

代入しているから当たり前っちゃ当たり前ですね。

さて、このポインタを使って何をすればいいのでしょうか…。
参考書によるとポインタの役目はその名の通り指し示すことにあって、
そのアドレスにある変数の代わりになるそうです。

#include <iostream>
using namespace std;

int main(){
    int a;
    int* p = &a;
    
    a = 0;
    cout << "a  = " << a << endl;
    cout << "*p = " << *p << endl;
    
    *p = 5;
    cout << "a  = " << a << endl;
    cout << "*p = " << *p << endl;
}

実行結果です。

a  = 0
*p = 0
a  = 5
*p = 5
Program ended with exit code: 0

aと*pの値が連動していますね。普通の変数は&をつけるとアドレスを指し、
ポインタ変数は*をつけると値を指すようです。

このような値が連動するような動きは、参照に似ています。
参照とポインタの大きな違いは、参照は一度参照先を決めたらもう変えられないのに対して、
ポインタは再代入すれば別の変数に対して使うことができる点だそう。
そして使い分けは人それぞれだということです。

配列とポインタ

配列を引数に取る関数を書きます。

#include <iostream>
using namespace std;

void Init(int array[]);
void Show(int array[]);

int main(){
    int n[5];
    
    Init(n);
    Show(n);
}

void Init(int array[]){
    for(int i = 0; i < 5; i++){
        array[i] = i * 5;
    }
}

void Show(int array[]){
    for(int i = 0; i < 5; i++){
        cout << array[i] << " ";
    }
    cout << endl;
}
0 5 10 15 20 
Program ended with exit code: 0

配列を関数に渡す場合は、関数宣言は大きさの不定な配列変数の形にし、
使用する方は配列変数の名前を渡すようにすれば良いようです。

仮引数は、引数に渡した変数の値をコピーしたものであるというのが今までの話でした。
しかし配列変数を引数とした時は話が違うそうです。
配列をもし通常の変数と同じように値渡ししたら、配列のサイズによってはコピーが大変になります。
元の配列を使ったほうが良さそうです。
そもそも配列変数の名前配列変数の先頭要素のアドレスを示すそうです。
つまり、上のプログラムはポインタを利用して関数に配列を渡しています。

[]という演算子は、アドレス[インデックス]とした時に、
指定したアドレスから型の大きさ*インデックスだけ進んだところにある変数を利用するものだそう。

#include <iostream>
using namespace std;

void Init(int* array);
void Show(int* array);

int main(){
    int n[5];
    
    Init(n);
    Show(n);
}

void Init(int* array){
    for(int i = 0; i < 5; i++){
        array[i] = i * 5;
    }
}

void Show(int* array){
    for(int i = 0; i < 5; i++){
        cout << array[i] << " ";
    }
    cout << endl;
}

ふーん、と書き換えてみましたが動きました。結果も一緒です。

0 5 10 15 20 
Program ended with exit code: 0

更に配列とポインタ

配列が本当にアドレスの上で連続して存在しているのか確認します。

#include <iostream>
using namespace std;

int main(){
    char array[10];
    
    cout << "array    : " << (size_t)array << endl;
    for(int i = 0; i < 10; i++){
        cout << "&array[" << i << "]: " << (size_t)&array[i] << endl;
    }
}

結果はこうでした。

array    : 140732920755534
&array[0]: 140732920755534
&array[1]: 140732920755535
&array[2]: 140732920755536
&array[3]: 140732920755537
&array[4]: 140732920755538
&array[5]: 140732920755539
&array[6]: 140732920755540
&array[7]: 140732920755541
&array[8]: 140732920755542
&array[9]: 140732920755543
Program ended with exit code: 0

配列変数の名前と[0]のアドレスは一緒で、char型の配列なので1ずつ進んで行きます。
アドレス上連続しているのは間違いないようです。

さらに本当に他の関数の引数にした時にアドレスが一緒なのかです。

#include <iostream>
using namespace std;

void Show(char* pointer);

int main(){
    char array[10];
    
    cout << "array    : " << (size_t)array << endl;
    for(int i = 0; i < 10; i++){
        cout << "&array[" << i << "]: " << (size_t)&array[i] << endl;
    }
    Show(array);
}

void Show(char* pointer){
    cout << "pointer    : " << (size_t)pointer << endl;
    for(int i = 0; i < 10; i++){
        cout << "&pointer[" << i << "]: " << (size_t)&pointer[i] << endl;
    }
}
array    : 140732920755534
&array[0]: 140732920755534
&array[1]: 140732920755535
&array[2]: 140732920755536
&array[3]: 140732920755537
&array[4]: 140732920755538
&array[5]: 140732920755539
&array[6]: 140732920755540
&array[7]: 140732920755541
&array[8]: 140732920755542
&array[9]: 140732920755543
pointer    : 140732920755534
&pointer[0]: 140732920755534
&pointer[1]: 140732920755535
&pointer[2]: 140732920755536
&pointer[3]: 140732920755537
&pointer[4]: 140732920755538
&pointer[5]: 140732920755539
&pointer[6]: 140732920755540
&pointer[7]: 140732920755541
&pointer[8]: 140732920755542
&pointer[9]: 140732920755543
Program ended with exit code: 0

アドレスが間違いなく一緒ですね。

上の例ではarrayとpointerが同じもののように見えるのですが、本当は違うから注意という話が続きます。
まず、pointerはポインタなので他のアドレスを代入できて、arrayはできないということ。
そしてsizeof演算子の挙動が違うという話です。

#include <iostream>
using namespace std;

void Show(char* pointer);

int main(){
    char array[10];
    
    cout << "array    : " << (size_t)array << endl;
    for(int i = 0; i < 10; i++){
        cout << "&array[" << i << "]: " << (size_t)&array[i] << endl;
    }
    cout << "array size : " << sizeof array / sizeof array[0] << endl;
    Show(array);
}

void Show(char* pointer){
    cout << "pointer    : " << (size_t)pointer << endl;
    for(int i = 0; i < 10; i++){
        cout << "&pointer[" << i << "]: " << (size_t)&pointer[i] << endl;
    }
    cout << "pointer size : " << sizeof pointer / sizeof pointer[0] << endl;
}
array    : 140732920755534
&array[0]: 140732920755534
&array[1]: 140732920755535
&array[2]: 140732920755536
&array[3]: 140732920755537
&array[4]: 140732920755538
&array[5]: 140732920755539
&array[6]: 140732920755540
&array[7]: 140732920755541
&array[8]: 140732920755542
&array[9]: 140732920755543
array size : 10
pointer    : 140732920755534
&pointer[0]: 140732920755534
&pointer[1]: 140732920755535
&pointer[2]: 140732920755536
&pointer[3]: 140732920755537
&pointer[4]: 140732920755538
&pointer[5]: 140732920755539
&pointer[6]: 140732920755540
&pointer[7]: 140732920755541
&pointer[8]: 140732920755542
&pointer[9]: 140732920755543
pointer size : 8
Program ended with exit code: 0

配列の要素数は、配列全体のサイズを1要素のサイズで割ったものとして算出できるそうです。
上で言えば、sizeof array / sizeof array[0]の部分ですね。
arrayのサイズはchar型10個分なので10、array[0]は1個分なので1、配列全体は10。
1バイトのchar型だからなんか間抜けな感じがしますが、
環境に左右されない有用な配列の要素数の計算方法ですね。

ところが、pointerはchar型の配列ではなく、char型のポインタであり、サイズが8なんですね。
でもpointer[0]はchar型を指しているのでサイズは1です。
結果として算出される配列の要素数は欲しかった10ではなく8です。
これは初心者がハマる落とし穴らしいです。気をつけます。

とりあえずこんなもので。おやすみなさい。