ポインタを考える
ポインタはメモリアドレスを表すマクロ機能みたいなもので、中身はただの整数値なんですが
その事実に納得するために様々な視点が必要かと思います
今回の記事はその様々な視点の中の一つの視点だと思ってください
ポインタが何者であるか/アドレスが何者であるか
架空のお話をします。
ここに1KBのメモリがあります
このメモリは1~100の値を1024個格納できます
値は連続して格納されます
[10][20][30][40][50]... 1024個までこの連続です。
初期状態の箱は全て0が設定されているとします(実際は不定、電圧等の物理現象に左右される)
[0][0][0][0][0]....
次の動作を連続で行います。
1番目の箱に11と記録する
[11][0][0][0][0]....
2番目の箱に22と記録する
[11][22][0][0][0]....
3番目の箱に33と記録する
[11][22][33][0][0]....
続けて次の動作を連続で行います。
1番目の箱から値を読み出します
11 が得られる
2番目の箱から値を読み出します
22 が得られる
3番目の箱から値を読み出します
33 が得られる
アドレスとはn番目の値を指す数字です
ポインタとはアドレスです
ポインタとはメモリのn番目を表す数値です
C言語のポインタ加算からポインタを考える
架空のお話をします。
1KBのメモリがあります
このメモリは1文字を1024個格納できます
次のように文字列を格納します
[W][e][l][c][o][m][e][ ][_][_][_][_][_][_][_][_]
[t][o][ ][_][_][_][_][_][_][_][_][_][_][_][_][_]
[t][h][e][ ][_][_][_][_][_][_][_][_][_][_][_][_]
[w][o][r][l][d][ ][_][_][_][_][_][_][_][_][_][_]
[o][f][ ][_][_][_][_][_][_][_][_][_][_][_][_][_]
[p][r][o][g][r][a][m][m][i][n][g][.][_][_][_][_]
# 本来のメモリに改行は存在しませんが、見やすくするために改行をして表現しています。
# 改行1行は16文字です
ポインタ算出の式は次の式を使います。
counter = N
pointer = counter * 16
counter = 0 の時 pointer = 0
counter = 1 の時 pointer = 16
counter = 2 の時 pointer = 32
となります。
次に、indexの値をメモリのアドレスとして捉えます
その際に人間の感覚でいう「1番目」をコンピュータに伝える際は「0番目」と表現するルールがあります。
人間は1から物を数えますが、コンピュータは0から物を数え始めます。
では、counter値に対応するメモリの中身を見ていきます
counter = 0 の時 pointer = 0 = [W] ([W][e][l][c][o]... の先頭とも言える)
counter = 1 の時 pointer = 16 = [t]([t][o][ ][][]... の先頭とも言える)
counter = 2 の時 pointer = 32 = [t]([t][h][e][ ][_]... の先頭とも言える)
次の架空のコードで pointer を参照し続けた場合を考えます。
for(counter = 0; counter < 6; counter += 1) {
pointer = counter * 16
print_pointer_reference(pointer) # メモリアドレスの pointer 番目から連続して文字列を読み出す命令
print_char('\n') # 改行を出力
}
print_pointer_reference は指定されたメモリのアドレスから文字列を読み込んで表示する命令です。架空の命令です。
[_] を発見したら表示を中止する仕様とします。
次のように表示されます。
Welcome
to
the
world
of
programming.
16個という区切りを決めて、その感覚に従い表示したい文字列をメモリ内に予め準備し
表示するロジックでは16個置きに参照していくことで準備した文字列を表示することができました。
C言語のポインタ加算の挙動
C言語を使って前項の内容を合理的に処理します。
次の構造体を作成します。
struct Text {
char value[16];
};
次のデータが 0番目から書き込まれている物とします。
[W][e][l][c][o][m][e][ ][_][_][_][_][_][_][_][_]
[t][o][ ][_][_][_][_][_][_][_][_][_][_][_][_][_]
[t][h][e][ ][_][_][_][_][_][_][_][_][_][_][_][_]
[w][o][r][l][d][ ][_][_][_][_][_][_][_][_][_][_]
[o][f][ ][_][_][_][_][_][_][_][_][_][_][_][_][_]
[p][r][o][g][r][a][m][m][i][n][g][.][_][_][_][_]
# 本来のメモリに改行は存在しませんが、見やすくするために改行をして表現しています。
# 改行1行は16文字です
次のコードを実行します。
struct Text* pointer = 0;
for(counter = 0; counter < 6; counter += 1) {
print_pointer_reference(pointer) # メモリアドレスの pointer 番目から連続して文字列を読み出す命令
print_char('\n') # 改行を出力
pointer += 1 # 1 しか加算していない
}
print_pointer_reference は指定されたメモリのアドレスから文字列を読み込んで表示する命令です。架空の命令です。
[_] を発見したら表示を中止する仕様とします。
次のように表示されます。
Welcome
to
the
world
of
programming.
pointer += 1 # 1 しか加算していない
1つずつしか加算していないのにおかしいと感じると思います。
原因は次の箇所にあります
struct Text* pointer = 0;
ここでなにをやっているかというと
pointer は Text構造体型のアドレス変数です
-> Text構造体型は文字16個分の大きさがあります
-> 1加算されたら16加算します
という便利機能の宣言、準備をしています。
実際のコード
前項の内容を実際のC言語コードにします
#include <stdio.h>
struct Text {
char value[16];
};
/*
* Text構造体が持つ文字を先頭から表示する。
* 終端文字 '\0' または16文字目に到達したら終了する。
*/
void print_pointer_reference(const struct Text *pointer)
{
int index;
for (index = 0; index < 16; index += 1) {
if (pointer->value[index] == '\0' || pointer->value[index] == '_') {
break;
}
putchar(pointer->value[index]);
}
}
/*
* 指定された1文字を表示する。
*/
void print_char(char value)
{
putchar(value);
}
int main(void)
{
struct Text texts[6] = {
{{
'W', 'e', 'l', 'c', 'o', 'm', 'e', ' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'
}},
{{
't', 'o', ' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'
}},
{{
't', 'h', 'e', ' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'
}},
{{
'w', 'o', 'r', 'l', 'd', ' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'
}},
{{
'o', 'f', ' ', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0', '\0'
}},
{{
'p', 'r', 'o', 'g', 'r', 'a', 'm', 'm', 'i', 'n', 'g', '.', '\0', '\0', '\0', '\0'
}}
};
struct Text *pointer = &texts[0];
int counter;
for (counter = 0; counter < 6; counter += 1) {
print_pointer_reference(pointer);
print_char('\n');
pointer = texts + counter; // 直感的には pointer には 0, 1, 2, 3... と入るはずなので逐次 W e l c o m e と表示されるように思うかもしれない
// 又は pointer += 1; とする
}
return 0;
}
実際のC言語ではポインタ(アドレスn番目の指定)に0を指定できないので、変数を宣言しその変数のアドレスを与えています。
まとめ
ポインタはアドレスのこと
アドレスの数値はn番目のメモリを参照する、書き込むという時に利用する
変数型、構造体型をポインタに使用すると、加算時の挙動が素直に+1ではなく、その変数型のサイズ分加算という挙動になる
ポインタが何故難しいか?
- メモリの物理配置を想像できない
- 変数型に合わせて挙動が変わるというコンパイラが勝手に便利にしてくれる仕様を理解していない
だいたいこの辺だと思います。
面倒かとは思いますが、マイコンプログラムをgccで作成し、アセンブラファイルやリンカファイルを自分で編集、参照して確認するという作業をする覚えざるを得ないので、そういう作業をどこかでやると良いかと思います。
PICとかH8マイコンなんかの学習キットでできるんじゃないかと思います。