けんつの煽られ駆動開発記

何か作るときは大抵誰かに煽られた時です。

C言語 ポインタと配列

5章 ポインタと配列

ポインタは、他の変数のアドレスを格納する変数でありC言語では頻繁に利用される

理由は大きく分けて2つある
一つ目としてポインタを使うことが処理を行う唯一の方法である場合があるから
二つ目はポインタを用いて記述したほうが、それを使用しないで記述されたコードよりコンパクトで効率的になる場合があるから

そしてポインタと配列には密接な関係がある

しかしポインタは理解不能なプログラムをつくってしまうものとしてgotoとどっこいの扱いを受けてきた
確かに不用意にポインタを使うと理解不能なプログラムになってしまうが十分に注意することでポインタを活用し
よりよいコードを書くことも出来る。これはさっき述べた理由の二番目に合致する

ANSI Cではポインタの扱いが明確に定義され型void *(voidへのポインタ)をchar *の代わりに一般化されたポインタとして利用出来るようになった

5.1 ポインタとアドレス

これはポインタを扱う上で基礎的な知識になる

まずメモリ構成について、普通コンピュータでは個々にあるいは連続したグループとして扱うことの出来る
連続番号付きのつまりはアドレス付きのメモリセルという形の配列を持つ、これは配列とポインタの関係を説明する上で重要な点になる

そして、ポインタとはアドレスを格納することができるので
以下のように書ける

int *p;
int n = 1;

p = &n;

まず一行目でint型のポインタを宣言し四行目でint型変数のアドレスを代入している
ここで使用した&演算子はオブジェクトのアドレスを示すものであり、変数か配列の要素にのみ使用できる
逆に式や定数、レジスタ変数には適用できない

次にポインタを示す*演算子は間接演算子、逆参照演算子と言われる
これをポインタに適用にするとそのポインタが示すオブジェクトにアクセスできる

具体的には以下のように使える

int *p;
int x = 1, y = 2;

p = &x;//変数xのアドレスを代入
y = *p;//アドレスが示すオブジェクトつまり1を代入

printf("%d\n",y);//print -> 1

また以下のような文があったとしよう

double *dp,atof(char *);

この場合*dpとatof(char *)がdouble型をもちatofの引数charへのポインタであることを示すと同時に
ポインタがある特定の種類のオブジェクト指すように制約されている
そのため各ポインタは特定のデータ型を指す

ただし例外がありvoidへのポインタは任意の型のポインタを保持できるがそれ自身を逆参照することは出来ない

それではさっきの例に戻って*pがxを表すなら*pはxの代わりにどの文脈でも使用できる
つまり

//*p,x=1,p=&xを仮定
*p+=2;//これでxとpは3を表す

*p = *p + 2;
++*ip;

//これだけ演算子の関係でかっこが必要
(*p)++;

このように自由に計算できるが*pはxのアドレス、つまりxが存在する場所を参照できるので演算結果がxにも影響する
さらに最後の場合で単行演算子は右から左へ評価されるためipだけがインクリメントされてしまうなど注意が必要

さらに*qを同じくintへのポインタだとすると

q = p;

とすることでpがqにコピーされqはpを示すものとなる

5.2 ポインタと関数引数

以下のような関数を考える

void swap(int x,int y){
    int tmp = 0;
    tmp = x;
    x = y;
    y = tmp;
}

これは何ら問題なくコンパイルできるし一見正しく動作するように思えるが
関数内部の処理をどう変更してもこのままでは外部に全く影響しないのでswapしてるようでswapしてない
一種のバグを生み出している

それもそのはず、関数の引数はポインタやアドレスが関係しない限り渡した値のコピーでしかないからだ

ただポインタを使えば、アドレスを参照することができるので変更が外部にも適用される
なので以下のように変更すると想定どおりの動作をするだろう

void swap(int *x,int *y){
    int tmp = 0;
    tmp = *x;
    *x = *y;
    *y = tmp;
}

このようにポインタを使うと簡単に実装できたりポインタがなければ実装できない処理もある
またわかりやすい例を上げるならscanf関数である、scanf関数は入力を格納したい変数のアドレスを渡しているため
scanf関数外にある変数に値を代入できる

5.3 ポインタと配列

ポインタと配列には強い関係がある

というのも配列でインデックスを指定して実行出来る操作がポインタでも出来るためである
ポインタを使った操作のほうが一般に高速だとされているが初心者には理解しにくい

まず例を上げる

int a[10];

という宣言があるとき、配列はa[0],a[1],a[2]...と連続するオブジェクトからなるブロックである
a[i]といった記法は先頭からi番目の要素を参照する

また以下のintへのポインタ

int *p;

が宣言されているとすると、以下の代入によって

p = &a[0];

aの0番目の要素のアドレスがpにセットされる

さらにintへのポインタpに対して配列aの特定の要素が定義されている場合
p+1で次の要素を指す
一般にp+iはi番目の要素を指す

したがって、pがa[0]を指しているなら

*(p+1)

はa[1]の要素を参照する
またp+iはa[i]のアドレスであり*(p+i)はその要素を指す

ここまでで説明したものは配列aの変数の型やサイズに関わらず当てはまる


インデックシングとポインタ演算の間の対応は非常に良く、定義により配列型の変数あるいは式の値は配列の先頭の要素のアドレスであるから

p = &a[0];

のあとでpとaは同じ値を持つ。つまり配列の名前はその先頭の要素の位置と同義であるから
以下のようにも書ける

p = a;

この前にもさらっと書いたがa[i]への参照は*(a+i)とかけることからそれもまた同等であると言える
またその等式に&演算子を適用すると&a[i]とa+iは同じということになる

ただしここで一つだけ注意しないといけないことがある

p = a;

でおこなわれた定義に対して

p++;

は意味のある演算であるが

a = p;
a++;

のような演算は正しくない
配列名が関数などに渡されるとき渡されるのは配列の先頭のアドレスであって
呼び出された関数内では局所的な変数として扱われる
したがって配列名のパラメータはポインタはである

これらを使って文字列の長さを求めるstrlen関数を新しく作ってみる

int strlen(char *str){
	int lencount;
	for(lencount = 0; *str != '\0'; str++,lencount++);
	return lencount;
}

このコードにおいてstr++というのはstrがポインタなので許される
それらは関数内の文字列に何ら影響しない
これはポインタの単にstrlen内でのプライベートなコピーを演算しているだけに過ぎない

ここまでまとめたことにより

char str[]

char *str

は関数の仮引数として同一であると言える

そして以下のように配列をポインタに渡した場合

//int *p;int a[n];を仮定
p = a;

以下のような操作も出来る

p[i]

5.4 アドレス演算

今pが配列のどれかの要素へのポインタだと仮定すると、p++でインクリメントされると次の要素を指すようになる
同様にp+=iならi要素分だけ先を指すようになる

このような形はポインタ、アドレス演算の典型的な形である

Cではアドレス演算のアプローチが首尾一貫してて規則的になっている
ポインタ、配列、アドレス計算を統合したのはこの言語の主な強力さの一部になっている

まずmallocなどの関数を使ってメモリをメモリを確保し
使い終わったらfree関数などを使って確保したメモリを解放する2つのルーチンがある

これらはいままとめた順序で行う必要があり
順番が違っても、どちらかが抜けてもエラーやバグの原因となるので注意

そして特に配列が絡む場合ポインタは+,-などの演算子を使って計算することができる
というのも結果的にどの要素を指しているかということなので==,<=,!=,<,>,>=といったものも使える

特に2つの配列のポインタp,qを仮定したとき

p < q

などとするとpとqの場所が比較される

また演算子で計算できることから
さっき書いたstrlen関数を以下のように書きなおすこともできる

int strlen(char *str){
    char *p = str;
    while(*p != '\n')
        p++;
    return p - s;
}

5.5 文字ポインタと関数

以下のように記述された文字列定数は文字の配列である

"hello,world"

別の章でも言ったように実際はこれの末尾にnull文字があるので文字列のサイズは
ダブルクォートの中身の文字の数より1多い数値になる

おそらく最も文字列定数が現れるのは以下のような関数への引数として現れるときだろう

printf("hello,world");

このようなものが実際にコードの中にあるとき、それへのアクセスは文字ポインタを通して行われる
ただし実際にprintf関数が受け取るのは文字列定数の初めのアドレスである

話は変わって、ここで文字列定数が以下ように

char *pmsg = "hello,world";

char amsg[] = "hello,world";

と宣言、定義された場合
pmsgとamsgでは明確な違いがある

この単元の最初でも言ったように文字列定数は配列である
それを配列に代入した場合、文字数とnull文字を含めたサイズ分だけの大きさを持つ
この配列の中身の文字は変わるかもしれないが、amsgは常に同じアドレスをさす

一方pmsgのようにポインタを使った場合、pmsgには文字列のポインタのみが代入される
したがってこのポインタをあとで別の場所を参照するように変更することもできる
しかし文字列の内容を変えようとするとその結果は不定となるので注意が必要

5.6 ポインタのポインタ

ポインタとはそれ自身がアドレスを格納する特殊な変数なので
それ自身も変数のアドレスをメモリのどこかに確保していることになる
なのでポインタ自身もアドレスをもつということがわかる

C言語ではポインタのアドレスを更にポインタに格納することが可能である
例えばintへのポインタのポインタを作るなら

int **p;

のように宣言できる

ポインタの連鎖自体は何個でも作ることができるが自分が管理できる範囲にしておくのがいい

またこの考えはポインタ配列というものにも適用できて以下のような関数が書ける
複数個の文字列の配列に対して

void swap(char *str[],int i,int j){
     char *tmp;
     
     tmp = str[i];
     str[i] = str[j];
     str[j] = tmp;
}

このようなことも出来る