C言語によるプログラミングに関する世界観4(配列とポインタ)です。
ツール関係はC/C++ツールに移動しました。
システムに依存する部分はLinux(システムコール・API)やWindowsプログラミングも参照のこと。
配列とは、連続した領域に確保される、複数の変数(要素)の集合となる変数のこと。
以下のように、[]を伴って変数を宣言・定義すると、配列となる。
int val[5];
x[3]とした時は3つの要素を持つ配列が出来る。これにx[0], x[1], x[2]のようにアクセスすることで、要素の中身を参照・変更できる。
配列の初期化は以下のように記述する。
int x[5] = { 3, 6, 9, 12, 15 };
文字列を表現するためには、char型の配列を使ってchar str[256]とする。あるいはchar型のポインタを使ってchar* str = "Hoge";などとする。
後日注記:配列とポインタは結びつきが非常に強く、a[i]という添字表記は*(a+i)とコンパイラによって解釈される。異なるのは、ポインタは変数であるためインクリメントできる。たとえば、p++など。だが、配列名は変数でないためインクリメントできない。ポインタを使うことで、配列を順次にアクセスするために使うことができる。
後日注記:基本的に、C言語では、ポインタと配列はほとんど同じものとして扱われる。配列を関数に渡した時は値渡しではなく参照渡しとなる。また、コピーした時は複製されずアドレスだけのコピーとなる。そのため、配列をコピーする際には別々の変数を作ったかのように見えて同じ場所を指していることがあるため注意が必要。また文字列を表現するためには、基本的に文字の配列へのポインタとして扱われる。そのため、文字列を複製するためにはstrcpy_s()という専用のAPIを使う。
配列の初期化はmemset()、配列のコピーはmemcpy()でできる。
一般に、配列はポインタとして扱われるため、そのまま代入したのではアドレスがコピーされるだけで内容はコピーされない(浅いコピー)。浅いコピーの場合、二つの名前はメモリ上の同じ配列を指す別々の参照となる。
内容をすべてコピーする(深いコピー)には、memcpy()関数を使う。深いコピーの場合、配列はメモリ上の別々の場所に複製される。
配列の要素数を取得することはC言語では方法が用意されていないが、sizeofを使って配列のメモリ上のサイズを知ることはできるため、普通はそこから要素数を計算する。
ポインタ:
ポインタは変数のメモリアドレスの入った変数であり、*pはポインタの先の変数の値、&xは変数のアドレスを指している。
int x; int *p; p = &x; *p = 40; printf("%d",x);
この例では、40と表示される。xに40がポインタを介して代入されている。
後日注記:ポインタを考える時に、単にメモリアドレスを格納しポイントする型であると理解するだけではなく、特定の変数を「ポイント」するというC言語における独自の概念であると考えると理解しやすいかもしれない。
普通、変数は宣言されるごとに、新しい領域に変数のデータが確保されます。
x, y, zを順に次々に宣言すると、xとyとzはそれぞれ、別々の領域に変数が作られます。
これは、それぞれの場所に値を確保したいような、「値型の変数」では問題ありません。
ですが、「参照型の変数」とも呼ばれますが、ポインタを使うことで、「同じ場所(アドレス)にある別の名前の変数」を参照できます。
これは、たとえばデータ構造のように、「プログラムの中で、別の名前でも同じアドレスの変数を参照したい」時に使えます。特に、リストのようなデータ構造では、「リストに連結された次のデータ」を参照するために、ポインタを使います。
ポインタを応用することで、同じアドレスを関数の内部(呼び出し先)と外部(呼び出し元)で共有することもできます。こうした「データの共有」という考え方が、ポインタによって行えます。
つまり、「変数のアドレス」という考え方で、同じ場所にあるデータを「共有」し、「指し示す」という考え方が、ポインタです。
先の連結リストの例のように、ポインタを上手く使うことで、「データ構造の中に別の変数のアドレスを含める」といったようなことができます。ある場所にある変数を、別の場所から操作できるのです。
こうしたことができるのは、変数が単なる数値ではなく、「メモリアドレス上のデータ領域」であるためです。C言語では変数にその変数が位置するメモリアドレスでアクセスすることができます。
構造体のポインタは->でアクセスできる。
struct user schwarz; struct user *up; up = &schwarz; up->name = name;
構造体のポインタを上手く使うことで、関数への引数から同じ構造体を操作することができる。これにより、簡単な変数の保持と共有ができる。
どの関数からも同じデータを使いたい時は、構造体のポインタを使う。C++/Javaのオブジェクト指向は、構造体に関数ポインタを関連付けて、どの関数からも同じ構造体のデータ(構造体のポインタ)にアクセスできるようにしたもの、だと考えることができる。
C言語では、ファイルポインタのように、構造体へのポインタを関数に渡す、という手法を多用する。
特にWindowsなどで、ポインタを操作する関数というのはたくさん存在し、ポインタを返す関数もたくさんある。特に、ファイルなどシステムのデータを参照する時に、ポインタを返す関数を使い、ハンドラなどを関数に渡すことが多い。
構造体のポインタを関数に渡すのは半ば慣例化していて、Linuxカーネルなどでも、ポインタを関数に渡すことが通例になっている。関数の内部でポインタのデータを操作することができる。
後日注記:関数に構造体データを与える時、単に引数として与えると「複製」になる。これはサイズの大きなデータではオーバーヘッドが大きく、また関数の中から変更することができないため、関数の引数として構造体を与える時はポインタを与えることが多い。この時、呼び出し側では&をつけて構造体変数のアドレスを与える。(与えるのが配列や文字列の場合は既にポインタであるため&は必要ない。)
後日注記:ポインタの引数を純粋にオーバーヘッドの問題のみから使う場合、引数が変更されないことを示すためにconstをつける(たとえばconst char*)こともある。しかしながら、Cの基本型(intなど)を使う場合、参照渡しよりも値渡しを使った方が普通は高速である。
高度なプログラミングとして、ポインタをインクリメント(++で繰り返し値を増やしていく)して関数ポインタを操作したり、構造体などで関数ポインタのようなものを列挙したりすることで、汎用的なプログラミングを行うことが出来る。
C言語で出てくる「ポインタ」は、さまざまな関数処理を行うための、「データの在り処」だと考えましょう。
Cでは、ファイルポインタFILE*など、とにかくポインタがたくさん出てきますが、基本的にそれらは、ファイルの読み書き位置やデータへのアクセスを許すものであり、要は「データの在り処」です。
実際のプログラミングでは、ポインタを返す関数というのは良く使われる。
たとえば、このようなコードがよく出てくる。
struct block_object *obj = next_block_object();
こうしたポインタと関数の使い方をすることで、open()のようにシステムで確保したデータ領域を保持したり、何らかの処理を行った上で確保されたデータ領域へのアクセス手段を(別の関数でアクセスするために)保持し続けることができる。
ポインタを返す関数を作る時は、変数の寿命に注意すること。関数内で宣言されたローカル変数の「値」を返すのは問題ないが、「アドレス」を返すのは駄目。ローカル変数として宣言された変数は、関数が終わってしまえば削除されてしまうため、ローカル変数へのアドレスは関数が終わった時点でアクセスできなくなる。値の場合はコピーされて渡されるためこういう問題は無い。もし関数内で作った変数のアドレスを返したい時は、malloc()などを使うことが考えられる。
ポインタにconstをつける場合は、constをどの位置に記述するかで文法的な意味が変わってくる。
const char* str = "Hoge";
のように、*よりも前にconstをつけた場合、ポインタが参照する値が変更できなくなる。この例ではポインタの参照する文字列が変更できなくなる。
int* const ptr = &n;
のように、*よりも後にconstをつけた場合、ポインタそのもの(メモリアドレス)が変更できなくなる。この例ではポインタに別のアドレスを代入することができなくなる。
詳しくは以下を参照のこと。
2023.01.20編集
自分の書いた「ニュース - 2021-05-第二週」2021/05/06より。
C言語のポインタについて言うと、パソコンやメモリをハードウェア的に見た時に、あるのは「名前」ではなく、「場所」である。
変数において、名前は識別するためのシンボルに過ぎず、真にハードウェアを見た時、あるのは名前ではなく、アドレス空間における場所、すなわちメモリアドレスである。
ポインタは、このような「メモリアドレスを指し示す」変数である。
通常の配列は、要素の数が固定されており、後々になってサイズを変更することができない。
これに対して、malloc()を使って動的に確保された配列は、要素の数を動的に決めたり、realloc()によってサイズを後になって変更することができる。
malloc()で確保したメモリ領域は、使用されなくなった段階で必ずfree()で解放しなければならない。
2023.01.18編集
malloc()の使い方は、
int *mem; mem = (int *)malloc(sizeof(int) * 20);
のように、配列のサイズを指定したmalloc()関数を実行し、その返り値を型キャストして配列のポインタに代入する。
後日注記:malloc()で確保した配列は、通常の配列と同様に[]を使ってmem[0]のように要素にアクセスできる。
2023.01.19編集
malloc()とrealloc()の使い方として、注意すべきなのはエラーチェック。malloc()は、システムのメモリが使い果たされた場合など、正常にメモリを確保できなかった場合にNULLを返す。このNULLを必ずチェックするようにしよう。
また、realloc()では、一時的にtmpなどの一時変数に格納し、再確保に失敗した場合はその時点で解放し(そのまま使い続けても問題はない)、成功した場合に一時変数を代入するようにしなければならない。
#include <stdio.h> int main(void){ int i; int *mem; int *tmp; if ((mem = (int *)malloc(sizeof(int) * 20)) == NULL) { fprintf(stderr, "メモリの確保に失敗しました。\n"); exit(1); } printf("最初の配列:\n"); for (i = 0; i < 20; i++) { mem[i] = i; printf("mem[%d] : %d\n", i, mem[i]); } if ((tmp = (int *)realloc(mem, sizeof(int) * 40)) == NULL) { fprintf(stderr, "メモリの再確保に失敗しました。\n"); free(mem); exit(1); } else { mem = tmp; } printf("サイズを変更した配列:\n"); for (i = 0; i < 40; i++) { mem[i] = i; printf("mem[%d] : %d\n", i, mem[i]); } free(mem); return 0; }
実行結果:
最初の配列: mem[0] : 0 mem[1] : 1 mem[2] : 2 mem[3] : 3 mem[4] : 4 mem[5] : 5 mem[6] : 6 mem[7] : 7 mem[8] : 8 mem[9] : 9 mem[10] : 10 mem[11] : 11 mem[12] : 12 mem[13] : 13 mem[14] : 14 mem[15] : 15 mem[16] : 16 mem[17] : 17 mem[18] : 18 mem[19] : 19 サイズを変更した配列: mem[0] : 0 mem[1] : 1 mem[2] : 2 mem[3] : 3 mem[4] : 4 mem[5] : 5 mem[6] : 6 mem[7] : 7 mem[8] : 8 mem[9] : 9 mem[10] : 10 mem[11] : 11 mem[12] : 12 mem[13] : 13 mem[14] : 14 mem[15] : 15 mem[16] : 16 mem[17] : 17 mem[18] : 18 mem[19] : 19 mem[20] : 20 mem[21] : 21 mem[22] : 22 mem[23] : 23 mem[24] : 24 mem[25] : 25 mem[26] : 26 mem[27] : 27 mem[28] : 28 mem[29] : 29 mem[30] : 30 mem[31] : 31 mem[32] : 32 mem[33] : 33 mem[34] : 34 mem[35] : 35 mem[36] : 36 mem[37] : 37 mem[38] : 38 mem[39] : 39
詳しくは以下のページが参考になる。
2023.01.18
2023.02.17編集
2023.11.17編集
malloc()で動的に確保した配列は、ポインタに代入して操作し、実行時に配列のサイズを動的に決められる。このため、「どれくらいのサイズになるか分からない」変数を扱うために使用できる。
途中で配列のサイズを変更したい場合はrealloc()を使う。また、メモリ領域を0で初期化した上で確保するcalloc()という関数がある。
malloc()で確保した変数はヒープ領域に保管され、スコープを脱出しても削除されない。このため、さまざまな変数から共通してアクセスするような「共有データ」のために使用できる。
使い終わった後はfree()で解放しなければ、メモリリークが起きる。
後日注記:システムのメモリが使い果たされた時、malloc()はNULLを返す。これを必ずチェックすること。Cでシステムやライブラリの関数を使う時は、関数の戻り値をエラーチェックに使うように注意しよう。一連の処理を自作のメモリアロケータ関数にすることもある。また、入出力データなどで、動的にメモリを確保する場合、足りなくなったらrealloc()を用いて調整することもある。
自分の書いたブログ「未来のわたしの心より今のあなたへ」2021/03/27より。
並列処理などで動的なメモリ確保と解放が複雑になる時に、参照カウント方式のガーベッジコレクションを使うことがある。
その変数の参照が増えるごとに参照カウントをインクリメントし、参照が減るごとにデクリメントする。
全ての参照が無くなってゼロになった時に、その変数は不要になるため、メモリ領域を解放する。
参照カウント方式の他にも、マーク・スイープやコピーGCなどの方式がある。
以下は参考文献。
Java(2C.ガーベッジコレクションと例外)も参照のこと。
関数ポインタを使うことで、関数をポインタとして変数に格納できる。構造体や配列に格納したり、引数として関数ポインタを使うことも可能。
後日注記:関数ポインタは(*func_p)(int, int)のように定義する。intの部分は引数の型を表す。この関数ポインタの見た目は「醜い」といって批判されがちである。関数を引数にとることもできるので、どのような関数が来るかは分からないがその関数を実行したい時などに使える。
以下は上記サイトを参考に自分でコードを記述。
関数ポインタを引数にとる関数:
#include <stdio.h> int func_p(int x, int (*func1)(int), int (*func2)(int)){ /* 引数に関数ポインタを指定 */ return (func1(x) * func2(x)); } int func_f(int x) { return (x * 2); } int func_g(int x) { return (x + 100); } int main(){ x = func_p(100, func_f, func_g); printf("%d\n", x); return 0; }
関数や関数型プログラミングやクロージャ・無名関数・関数オブジェクトも参照のこと。
2023.01.20編集
ポインタを理解するためには、「同じ箱にアクセスできるか否か」ということが重要です。
ポインタとは、メモリアドレスを格納できる変数のことで、そのアドレスが指し示す「箱」の中にある値を参照したり、書き換えたりすることができます。
ですが、ポインタという言葉を聞くと、「普通の変数ではない特殊な変数」だと思われる方が多いでしょう。
ですが、ローカル変数をローカルスコープで扱う際には、ポインタと同じように「同じ箱にアクセスする」ということを普通にやっています。
たとえば、以下のようなコードがあります。
int x = 0; x = 10; x = 15;
ここで、この3行は、同じxという箱に変数名を使ってアクセスしています。決して、10の格納されたxと、15の格納されたxが、別々に確保されることはなく、「同じxという変数の箱」に10や15を格納しています。
ですが、関数への値渡しの時はそうではなく、関数の中では別の変数の箱が用意され、値はコピーされるだけです。
void plus2(int x) { x = x + 2; } int main(){ int x = 0; x = 10; plus2(x); }
このようにしても、xが12になることはありません。値が別の箱にコピーされて渡されているからです。
ポインタとは、この後者の例ではなく、前者の例のように、変数の箱のアドレスそのものを渡して、「同じ箱にアクセスできる」ようにしたものです。
void plus2(int* x) { *x = *x + 2; } int main(){ int x = 0; x = 10; plus2(&x); }
この例では、plus2(&x)によって、x = x + 2を実行するのと同じように、xに2を加算することができます。
Javaのような多くのオブジェクト指向言語にはポインタがありません。その理由は、同じ変数の箱に関数からアクセスしたいのであれば、クラスのメンバ変数をそれぞれのメソッドから操作すればよいからです。メンバ変数をメソッドから操作することで、ポインタがなくても、各関数から同じ変数の箱に共通かつ効率的にアクセスできます。
また、多くのオブジェクト指向言語で、クラスのオブジェクトは参照型として扱われるため、ある関数やメソッドにオブジェクトを渡して、その関数でオブジェクトを操作することも、ポインタがなくてもできる言語が多いです。
あるいは、Perlのように、myを使ってローカル変数であることを明示しない限り、すべての変数をグローバル変数として扱うような言語も、手軽と言えば手軽なのかもしれません。
ポインタは、グローバル変数とよく似ています。グローバル変数も、それぞれの関数から共通してアクセスできます。違う点は、アドレスを渡すだけで、名前空間を汚染することがないということです。グローバル変数は、グローバルな名前でアクセスするために、グローバルな名前空間を汚染してしまいます。ポインタは、名前空間を汚染せず、必要な関数に必要な時点で変数のアドレスを渡すことができます。
ポインタの使いどころとは、大きくいって、「大きなデータを渡す場合」や、「さまざまな場所で変数へアクセスする場合」などです。
大きなデータを渡す場合、たとえばファイルやソケットのバッファを渡す場合、ポインタを使うことで、値をコピーせず、箱そのもののアドレスを渡すことができ、オーバーヘッドが少なくてすむため、効率的になります。
また、さまざまな場所で変数にアクセスしたい場合にも、ポインタを使うことで、同じ変数にさまざまな場所からアクセスできます。
また、変数のサイズがどれくらいの大きさになるのか分からない時、静的な配列ではコードを書けないことがあります。このような時に、動的に確保したメモリ領域をポインタを使って保持することができます。
ほかにも、データ構造を作る際に、連結リストやツリーのようなデータ構造では、要素が別の要素へと連結されて続いていく場合があります。このような場合にもポインタを使うことが多いです。
また、C言語のハッカーの常套手段として、配列のような連続したデータ領域で、要素にひとつひとつ順番にアクセスするために、ポインタの値をインクリメント(1ずつ加算)して、配列の各要素を指し示すための「カーソル」として使うこともあります。数値の配列だけではなく、文字列などにおいても使えます。ただし連続したデータ領域に対してしかこの手法は使えません。同様の手法を連続していない反復データ構造に使いたい場合、C++のSTLコンテナなど多くの言語では、イテレータ(反復子)を使うこともできます。
また、カーネルやシステムのリソースをユーザーランドのプログラムが使う際に、カーネルとユーザーの間でリソースをやり取りするためにポインタが使われることがあります。たとえば、UNIXやstdioにおけるオープンしたファイル、ソケットなどのバッファ、Windowsのデバイスコンテキストやウィンドウインスタンスなどのシステムオブジェクトは、ファイルディスクリプタやウィンドウハンドラ・インスタンスハンドラのようなシステムによって指定される整数値の識別子か、あるいはファイルポインタのような特定の型やオブジェクトへのポインタを使ってユーザープログラムからアクセスします。また、カーネルの内部でも、たとえばソケットバッファのような場合、バッファをコピーするためのオーバーヘッドを避けるために、ポインタを使って各ネットワーク層でデータの受け渡しがされています。
プログラミング作法(3C.ポインタとメモリ管理)も参照のこと。
メモリと仮想メモリについては、メモリやLinuxカーネル(メモリ管理)を参照のこと。
アセンブラについては、アセンブリ言語を参照のこと。
メモリAPIについては、Linux API(プロセス・メモリ)を参照のこと。
C++のSTLコンテナについては、C++(STL・ライブラリ)を参照のこと。
Javaについては、JavaやJava(オブジェクト指向)を参照のこと。
ラベルを同じ箱を指す別の名前として使用する。
変数名ではなく、メモリアドレスによって、絶対的位置から変数を操作する。
C言語でリストなどを実装したり、カーネル内部のバッファにアクセスするなどに必要。