ポインタに関すること
これは自分用学習メモです。まだ途中です。
C言語のポインタに関する断片的な覚え書き
C言語をベースとしているライブラリは、C言語である以上、ポインタや構造体が絡んできます。
確かに ライブラリは、プログラムの面倒くさい部分を隠したり、肩代わりしてくれます。
しかし、その他様々なライブラリを調べていくうちに、その肩代わりする機能を上手に使いこなすためには、結局ポインタを知らなければ難しいということがわかってきました。
そこで覚え書きつけることにしました。
(※C言語ライブラリを、他言語(スクリプト言語等)から利用できるようにしたラッパーライブラリについては分かりません。)
《注意》
この文書はまだ最適化や整理が必要で、100%正しいとは限りません。至らない所もあります。間違いがあったら教えて頂けると有り難いです。
万が一参考にするにしても、市販解説書や他の解説サイトを合わせ読まれ、プログラムを書いて検証するなどして判断されることをお勧めします。
この覚え書きでは問題を複雑化させないように、ポインタの作用においてmallocによるメモリ管理・変数型を同時に扱いません。
最初は参照の渡し方と受け取り方中心に考えていきます。
また、「関数」と「ポインタ」が関わる場合限定で考えていきます。
変数型についても、基本ははint(整数)型の場合だけ扱い、他の変数型(double ,charなど)は応用として考えることにします。
|
【目次】
- ポインタとは?
- 総まとめ(関数に様々なポインタを渡す)
- ポインタの宣言・初期化(関数外での宣言、関数内での宣言)
- 「値渡し」と「アドレス渡し」の比較(変数同士の値の交換、その違い)
- ポインタの演算(多重間接参照 の前知識)
- 「配列」と「ポインタの配列」の違い(多重間接参照の前知識)
- 多重間接参照(ダブルポインタ)
- 「配列へのポインタ」と「関数へのポインタ」 (配列への、関数へのポインタ)
- ポインタを含む型定義 (アドレスを返す関数へのポインタ、複雑な型定義)
- 構造体
- 記憶クラス,修飾子,変数型,変数名 (書込み禁止する const 修飾子)
|
.
- 用語や概念の説明。関数と密接なつながりがあります。
- ポインタにはこれだけの種類があり、共通した型でないと渡せません。
- ポインタが使われる(参照する/上書きされる)時、このように作用します。
- その具体的な例と、その作用を説明します。
- ポインタの集合体に対して、相対的に参照することができます。
- ポインタの集合体について、その違いと種類を紹介します。
- 以上の2つを踏まえた、多重参照の方法。
- 配列や関数を指すポインタを一緒に考えましょう。
- 複雑なポインタの参照関係も、洗い出して単純にできます。
- ポインタと参照は、構造体にも利用できます。
- const 識別子の付いたポインタは、関数の動作を把握する手がかり。
|
配列の宣言時の注意
a[n] は「n個用意したい」なのか、それとも「n番目に入れたい」のかを、見分ける。
図表の読み方。
この文書に限らず、図を読むときは、言語による思考以外に感覚的なイメージも併用すると理解が進むかもしれない。
プログラムは止まっているものではなく「動きのある」ものだから……。
例えば、紙の上に右向きの矢印が書かれているのを見ると、たいていの人は無意識のうちに、「左から右へほにゃららする」 と感じると思います。この法則を利用できるかも。
抽象イメージの例:地上、地下、右から左、左から右、飛び出す。着地する。入る、出る。放物線、箱の中、外、潜る、浮かぶ、上書き、コピー。
図から引き出した概念を頭の中に瞬間的な映像として、コードに一致するようなイメージを膨らませます。
(※できるだけ瞬間的、抽象的、単純なものを組み合わせる使います。)
●この手法に近いもの:
NHKで放映されてた番組新感覚☆キーワードで英会話
【1.ポインタとは?】
ポインタは、メモリ上のアドレス(番地)を指し示す変数で、変数の参照に使われます。
PCでは、メモリ上にOSや数多くのプログラムが配置され実行しています。その中の一つのプログラムを見ると下の図のようになっています。
プログラムは、
どのアドレスに配置されるかは実行される順番によって変化するので
分かりません。しかし中のセグメントの順序は、ほぼ一緒です。
関数は、プログラム領域、 ポインタや変数、定数リテラルはデータ領域にそれぞれ設置されます。
【ポインタと変数の違い、渡し方の違い】
この文書では、変数やポインタを値とメモリが重なった
二層構造から見ていきます。
変数には値、ポインタにはアドレスを入れて使います。
|
【型が一致しないとアドレスを渡せない】
C言語では:
・値 の 型(int,double,char等)
・データの種類(変数、配列、関数、ポインタ、構造体等)
の両特徴が複雑に組み合っています。
そのため関数や変数に値を代入する場合、型が一致しないと渡せず、エラーになります。
(関数の場合は、引数の数や型の特徴も一致しなければいけません)
注意
誰かが作ったライブラリには、もとのC言語には存在しない独自のデータ「型」も存在します。
これらは、構造体やtypedef によって定義され、例えば BITMAP *bitmap や SAMPLE *sample 等がそれです。
見慣れない型を見つけたら、そのライブラリのドキュメントや、ヘッダファイルを見ると正体が分かることがあります。
|
【アドレスとは?】
どんな変数(関数、配列)でも、メモリ上のアドレス(番地)
に、置かれています。
ポインタには、
他の変数のある場所(=アドレス)
を入れることができます。
これを、ポインタの語源から、
「他のアドレスを指している」
と表現することができます。
ただし、値の型、データの種類を持つ変数のアドレスは、
同じ型のポインタに入れなければ、エラーになります、
【ポインタのデータの大きさ】
ポインタの1単位あたりのメモリサイズは、変数型によらず一定(4バイト)。
しかし、普通の変数は変数型(char, int ,double 等)ごとにサイズが違うため、ポインタ演算でポインタを1単位進ませても、アドレス値の進み具合も一定とは限りません。
(厳密にいえば、ポインタは、それぞれの変数の先頭アドレスを指すことになる。)
|
|
|
|
ポインタには、まずアドレスを入れ、
次に、間接参照子と一緒に用いてはじめて「参照」ができます。
ポインタに参照子「*」を付けることで、この場に無い"配列"や"関数"等を参照し、
こちら(ポインタ側)から参照先を読み書きする手段を確立します。
「*」は、間接参照子の他に、逆参照演算子という呼び方もされます。
以下、混乱を防ぐため「*」を間接参照子と統一して呼ぶことにします。
|
【たとえ】
ケータイに消防署のアドレスを登録しておけば、直接消防署に出向けない場合も助けを呼べます。
(C言語の世界では番号を長々と押さなくても、「&消防署」とするだけで自動的に番号が登録される)
ケータイに消防署の番号を登録
(ポインタ初期化)
↓
登録した番号を押す
(参照する)
↓
救急隊員が来る。
【その他】
宣言しただけのポインタには、ランダムなゴミ値が入るので、必ず初期化しましょう。
【ポインタやアドレス渡しの使い道】
- ポインタ演算:指すアドレスを基準に相対的にデータを扱う。(型サイズの違い)
- データの流通
- 関数や配列を名前で管理し動的に参照する。
- 関数内から外部配列を読書きする手段を確立
- データ同士を動的に結び付け、データに構造を作り出す
(連結リスト、ツリ−構造)
- データを測るための物差しや目印を作る。
- C言語の文字列操作関数を使う時。
- C言語用ライブラリの関数を組み込む時。
|
【2.総まとめ:関数に色んなポインタを渡す】
C言語のデータは、機能を3種類のグループに分けられます
- 変数、 配列、構造体:(様々な荷物 )
- ポインタ:(遠くにあるデータを運ぶためのパイプ )
- 関数:(AとBを管理・流通する集配センター)
そして、ポインタをうまく機能させるのに必要なのが住所を書いた伝票→アドレス情報です 。
関数は、受け入れ口として仮引数がポインタである場合、最初に対象のアドレスを入れると、間接演算子を使って代入したり、参照したりすることで、データ(グループAとB)を管理し。
複数の値を行き来させたり、関数の外にある離れた所のデータを間接的に操作することができます。
C言語の性質(制限)
- 関数へ引数はいくらでも渡せるが、returnによる戻り値は1つだけ。
- 引数1つに配列全体(含まれる全部の変数)を一度に渡す事はできない(※)。
- 多次元配列は存在しない(が、1次元配列を束ねたものが相当する)。
- 「型」に厳しい。型が合わなければエラーになる。
(※ポインタを使えば、関数から2つ以上の値を(間接的に)返すことができるようになります)
(※配列変数全員が来れないなら、配列の代表者を頼って会いに行きそこで解決してもらう)
左の図は、取り扱うデータの種類と変数の記述を対応をまとめたものです。(クリック拡大)
※まず、変数には、データの種類(変数、配列、構造体、関数)があって、
ポインタにも同じく対応する型(変数への〜、配列への〜、関数への〜)
があるという考えです。それが一致しないと、アドレスがうまく渡せない。
図
どうしても配列全体を値渡ししたい場合は、構造体のメンバ変数に配列を用意する。
【3.ポインタ変数の宣言・初期化】
ここでは、ポインタがどのように作用するのかを順に追って表示します。
ポインタ変数は、アドレスを指し示したい変数と同じ型で宣言する必要があります。
右のコマのようにメモリ空間を横から見ていきましょう。
ここでは、アドレス(地面)の上に、変数という入れ物があるという二層構造として捉えていきます。
今回は比較のため、引数1つ、戻り値1つの場合に限定して説明します。
ポインタ宣言時に指しているアドレスはゴミ値が入っているので、アドレスを入れて初期化して下さい。
関数内の変数は、関数が終了するとなくなります。そのため関数内の変数のアドレスを外に持っていくことはできません。
【4.値渡しとアドレス渡しの比較( 変数の交換)】
「値渡し」と、「ポインタへのアドレス渡し+参照」の違いを説明する例で有名なものに「2つの変数の値を交換する」があります。
普通に値を交換するだけなら、別にポインタを使っても使わなくてもできます。
しかし、別の関数に変数の交換を行なわせる場合、そうはいきません。 これこそポインタが活躍する場面です。
【四コマ漫画1】(下図)
次元を上げて、2つ以上の引数と戻り値を考えてみましょう。
今、関数(箱の中)で作業をしているとします。この関数は引数に2つ、アドレスを渡すことができるとします。
- (1)関数にアドレスが引数として渡されました。
- (2)ポインタP, Q は、変数a,bのアドレス指し示します。先に変数を行き交わせる道を開通させておこうという訳です。
- (3)向こうにある値を、ポインタ+間接参照子を通して関数の中に引き入れました。
- (4)値を戻すときに、ポインタ+間接参照子に入れると、指した先の値を書き換えます。
それぞれ引き入れてきたのとは別のポインタに入れるとさてどうなるでしょう?
関数の終了とともに箱は崩壊します。ローカル変数もなくなりますが、無事、関数から値を戻すことが出来ました。
【四コマ漫画2】(下図)
もし、ポインタやアドレス渡しを使わず、 関数の引数に値渡しだけを使って2つの変数を交換すると、次の結果になります。
ポインタと概要。
ポインタを使う時
- 例外として、配列や関数の先頭アドレスは、&をつけなくても代入できる。
- ポインタの宣言時の変数の型と、関連づける対象の変数の型(int, double ,char 等)は同じにする。
- 同じくポインタのデータの種類と、対象となるデータの型(変数、配列、関数(引数/戻値)、構造体の特徴)も同じにする。→ typedef
- 式の中では配列はポインタに読み替えることができる
でも、実際書かれたコードを見ると、参照のためのアドレスも仮引数で宣言される関係上、
どれが本当に関数から戻される引数なのかが、一見してわかりません。
// この関数は結局、どのポインタを引数にとって、どのポインタ変数で上書き参照して返すんだろう?
int function(char *a , double *b, const char *c, const int *d){ }
こういう時は、const修飾子を見ることで判断します。
ポインタと間接参照(読み書き)による機能を制限する
int function(char *a , double *b, const char *c, const int *d){ }
関数functionの仮引数のうち、ポインタ c 、 d は、const修飾子がついているので、参照可能だけど、上書き参照することはできません。
このことから、なにも修飾子が付いてない、ポインタ a 、b の値こそ、関数内で処理が加えられ、上書き参照して返されると予想できます。
これで、関数functionの動作をある程度把握することができました。
ポインタで気をつけること
存在しない変数のアドレス参照 |
消滅したり、最初から存在しない変数のアドレスを、渡したり参照するとエラーになる。関数内で作ったローカル変数のアドレスは関数終了時に消滅するので、これを外で使おうとするとエラーになる。 |
未初期化のポインタを参照 |
宣言したばかりのポインタにはゴミデータ(ランダム値)が入るので、ここに値を上書き参照すると、意図せずシステム上のメモリのどこかが書き変わり、最悪の場合システムが破壊されます。 |
定数のアドレスを、
ポインタで読み書き |
ポインタに、本来書き換えない「定数」のアドレスを渡すと、参照時に上書きすることもできます。これは未知のトラブルを引き起こす可能性があります。(const修飾子) |
はずれた場所まで読み書きする |
文字列定数の終端文字列を付けてないばかりに、プログラムが文字列の終わりを越えてどこまでもアドレスを読み続ける。他の領域まで間違って書き込んだりする。 |
【5.ポインタの演算--- 複数の変数を走査(多重間接参照 の前知識) 】
|
これらの使い方は、配列や文字列のように連続して並んだ変数を扱う場合に用いられます。
forやwhileループと合わせることで、参照する場所を少しづつずらしながら、たくさんの変数に順番に走査させることもできます。
ただ、ポインタの *(p+1) のような使い方を見ると、必ずしもポインタが指し示す位置から変数をとってくるとは限りません。
今までのケースはたまたま、ポインタの指す場所と、値を取ってくる(または上書きする)場所が同じケースでした。
ポインタ変数自体はアドレスに付けた「めじるし」に過ぎないなら、
間接演算子こそ、指すアドレスの変数の内容を取ってくる役割を担うことになります。
ポインタと配列の違い:
配列名は、配列の先頭アドレスを指し、ポインタのように見えます。しかし、配列は先頭アドレスが固定されているので、
ポインタのように自由に指すアドレスを変更することはできません。
ポインタとリファレンスの違い
他言語のリファレンスでは、ポインタ演算のように、メモリ上の「変数a」の次(+1)にあるアドレスの内容を参照するといったことはできません。
↓の記事を参考に
連続しない変数の場合
配列ではない変数を指す場合。
int a=5;
int*p ;
*p=&a;
このとき *(p+1)と記述すると、ポインタから、メモリ上の「変数a」の次(+1)にあるアドレスの内容を無条件に参照しようとことができてしまう。
そのアドレスにはどんな情報があるのか無いのか、予想がつかない。この自由さは、メモリ上に変数がどのように割り当てられるかを理解しないまま扱ってしまうと危険だということを意味します。
他には、指す対象の配列の最大添字をオーバーした領域を指そうとする参照も危険です。(対象となる配列は10個の要素なのに ポインタを15単位進めた場所の要素を参照しようとする場合)
|
間接演算子とポインタ表現型による分類
表現型 |
参照先 | ポインタptrの 指すアドレス |
概要 |
*ptr 型 |
移動しない |
移動しない |
一番シンプル |
*(ptr+n) 型 |
移動する |
移動しない |
ポインタの指すアドレスを基準にn単位移動させたアドレスを参照し、その変数の内容を取得。 |
ptr[n] 型 |
移動する |
移動しない |
*(ptr+n) の別の書き方。添字演算子[]で大量の変数を一括して扱うことを明示しますが、 配列とは別物。 |
*ptr++型 |
移動する |
移動する |
参照先もポインタ自身も1単位移動。
便利な反面、配列終端を越えたデータまでアクセスする恐れもある。
ポインタを1単位移動させてから (インクリメント)、参照代入される。 |
【6.配列、ポインタの配列(多重間接参照 の前知識)
●配列はどんな時に使う?
・大量の変数を一括して扱う場合に使う。
●配列の性質
・C言語での配列は、一旦作ると、どこかに移動させたり、長さを変更したりすることはできません。
・C言語には、多次元配列は存在しないそうです。2次元配列のように見えているものは、複数の1次元配列を束(たば)ねたものです。
例えば、枝や薪(たきぎ)を何本も、ひも等で束(たば)ねたものを想像して下さい。
または、列車が車両基地にたくさん、漢字の三の字のように並べられて、線路ごと車庫に格納されています。
それを3番車庫の列車の先頭から数えて5番目にある客車というふうに言っているのです。
・int a[3][5]、長さ[5]単位の無名配列を、[3]本、束(たば)ねた配列 a
・int a[][5] 、長さ[5]単位の無名配列を、任意[]本、束(たば)ねた配列 a
・int *a[3][5]、長さ[5]単位の無名ポインタ配列を、[3]本、束(たば)ねたポインタ配列 a
/* int型の 束配列 */
int a[3][3]={
{0,1,2},
{3,4,5},
{6,7,8}
};
/* ポインタの束配列に、アドレスの配列を入れたので、OK */
int *aa[3][3]={
{ &a[0][0], &a[0][1], &a[0][2] },
{ &a[1][0], &a[1][1], &a[1][2] },
{ &a[2][0], &a[2][1], &a[2][2] }
};
/* ×ポインタの配列に、アドレスではない配列をいれて宣言したので、 エラーが起きる */
int *tn[3][3]={
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
/* ×今度は、 配列で、配列を束ねて宣言しようとして、エラーが起きる */
int bb1[3]= {0,1,2};
int bb2[3]= {0,1,3};
int bb3[3]= {0,1,2};
int aa[3]={ /* ←aaには配列の先頭アドレスは入らないので、間違い */
bb1,
bb2,
bb3,
};
printf("値を表示 %d /n", aa[2][1] );/* 間違い */
/* 上のような関連づけをしたいなら、ポインタ配列*aa で 配列bb1〜bb4を束ねる */
int bb1[3]= {0,1,2};
int bb2[3]= {0,1,3};
int bb3[3]= {0,1,2};
int *aa[3]={
bb1,
bb2,
bb3,
};
printf("値を表示 %d /n", aa[2][1] ); /* aa[] は ポインタを束ねた配列 */
/* ポインタ配列*aa→ポインタ配列*bb1〜*bb4 */
int *bb1[3]={ &a[0][0], &a[0][1], &a[0][2] };
int *bb2[3]={ &a[0][0], &a[0][1], &a[0][2] };
int *bb3[3]={ &a[2][0], &a[2][1], &a[2][2] };
int **aa[3]={ bb1, bb2 ,bb3 };
printf("アドレスを表示 %p /n", aa[2][1] );
/* 配列宣言の間違いによりエラー。なぜなら宣言時の配列の添字は、入れ物の最大値を意味するため。 */
int bb[0]={ 2 ,4, 6 }; /* bbの要素の[0]番目を配列で初期化 ←間違い */
int bb[1]={ 8, 7, 4 };
int bb[2]={ 7, 8, 9 };
/* これならOK */
int bb[3]={ 2 ,4, 6 }; /* 大きさ[3]の配列bbを用意して初期化 */
bb[1]= 10; /* bbの要素の1番目に 10入れる */
bb[2]= 10;
※文字列は、束配列として扱うと都合が良い
char aa[][64]={
"aaa",
"bbbbbbbb",
"ccccc"
};
【7.多重間接参照(ダブルポインタ)】
ポインタと同じ性質、機能を持ちますが、大きく違う部分があります。
一般的にポインタを指すポインタと説明されることが多いですが。
ダブルポインタ1つから、
2つの情報が取り出せます。( *pp と **pp )
これを利用すると、
2次元配列を指しているポインタを使って、
その任意の要素へ、
狙ってアクセスすることができます。
下の図では、予め、ポインタ配列p に、 配列a に束ねられている 無名配列の各先頭アドレスを指すようにしました。
#include "stdio.h"
//関数のプロトタイプ宣言
void ptrs(int **pp, int x , int y, int d[3][3] );
//引数は、 int d[3][3] の代わりに、配列へのポインタ int (*d)[3]でもOK
int main( void ){
///配列
int a[3][3]={ {0,1,2},
{3,4,5},
{6,7,8}};
int *p[3];
//ポインタを配列の先頭要素を指すようにする。
int i ; //C言語のfor関数のため
for( i = 0; i < 3 ;i++){
p[i]=a[i];
}
printf ("配列の先頭アドレス : %p \n", a );
ptrs(p,1,2,a);//引数は、ポインタ, 行、列、配列の先頭アドレス
printf ("変更後 : %d \n",a[1][2]);
return 0;
}
// 配列を渡す。 ポインタ配列 または 配列の先頭アドレス の場合
void ptrs(int **pp, int x , int y ,int d[3][3] ){
printf ("ダブルポインタ a[%d][%d] = *(*(pp+%d)+%d))= %d \n",x,y,x,y, *(*(pp+x)+y));
printf (" 〃 の別表記 pp[%d][%d] = %d \n",x,y, pp[x][y]);
printf ("そのアドレス : %p \n", *(pp+x)+y);
printf ("配列にアドレスを渡した d[%d][%d] = %d \n",x,y, d[x][y]);
printf ("その先頭アドレス : %p \n", d );
*(*(pp+x)+y)=50; // ポインタの
//pp[x][y]=39; // ポインタの別表記通じて編集
//d[x][y]=19; // 配列の先頭アドレスを通じて編集
print ("----------------------\n");
}
【8.配列へのポインタ】
ポインタ配列とどこが違うのか
さて、以下の記述、一体どういう事を表しているのでしょう?
この2つの違いを区別しようとすると、配列やポインタの名前の付け方も悪いから頭が混乱しやすいです。
int *a[3]; /* 「int型の変数」 への ポインタ配列(入れ物の要素数は[3]) を 宣言 */
と
int (*a)[3];/* 「int型で要素数[3]を持つ配列」への ポインタ を 宣言。これが「配列へのポインタ」 */
int *a[3]は
3個集合したポインタで、配列の血が混じっていますが、ポインタです。
int (*a)[3]は逆に、ある特徴(3個の集合)を持った配列を指せるポインタが
1個あるという意味です。ポインタの血が濃いといえます。
*a[3] と (*a)[3]。後者は前者を指すことはできません。なぜなら後者は配列を指すことしかできないからです。
もし前者を指したければ、別個にダブルポインタを用意することになります。
【9.配列へのポインタと関数ポインタを一緒に考える】
ここでちょっと視点を変えて、配列の名前を、一般名詞「配列(dimension)」として捉えてみましょう。
そしておなじ仲間の、関数(function)ポインタと一緒に考えてみましょう。
//関数と配列の宣言。
int function(int a){ return 0;} /* イ 引数(int a )を持つ関数 */
int dimension[3]={ 1,2 ,3 }/* ロ 要素数[3] を持つ配列 */
//関数へのポインタ と 配列へのポインタ
int (*pointer_of_function)( int a); // 引数( int a )を 持つ 関数へのポインタ。/* ハ */
int (*pointer_of_dimension)[3];//要素数[ 3 ] を持つ 配列へのポインタ。/* ニ */
それぞれ↑の書き方、似ていませんか?
//先ほどの2つのポインタと同じ種類であれば、アドレスを入れられます。
pointer_of_function = function;// 関数へのポインタ「ハ」に、関数「イ」の先頭アドレスを入れる
pointer_of_dimension = dimension;// 配列へのポインタ「ニ」に、配列「ロ」の先頭アドレスを入れる
関数の仮引数という場合には、* を [] に置き換えて書く事ができるそうです。← 簡易記法とのこと
void function( int *a ); は
void function( int a[] ); と書けます
void function( int (*a)[3] ); は
void function( int a[][3] ); と書けます
このように、色々な似通った書き方で混乱しないためにも、
プログラム中の変数、配列、関数、ポインタ等の名前は
意味が通った命名にすると良いかもしれません。
【10.関数ポインタ】
関数を指すポインタ。関数ポインタ変数を複数束ねて配列にすると。動的に関数を切り替えることができます。
関数ポインタの作り方。
- 関数のプロトタイプを宣言
- 関数ポインタを宣言(↑関数のプロトタイプ同様、同じ引数型、同じ戻り値型でなければいけない)
- 関数名を関数ポインタに代入
- 関数ポインタの配列。 (*関数ポインタ名[数])(引数)={ 関数名の羅列 }
- 関数ポインタを使う。 (*関数ポインタ名)(引数)、 関数ポインタ名(引数)でも可能
関数ポインタで大変なのは、戻り値が関数のアドレスたっだりする場合で。
typedef で型を定義しておけば、戻り値の指定に頭を悩まされずに済む。
関数と関数へのポインタは、同じ引数の構造を持っていないと、機能しません。
参考:派生型として、関数ポインタを指すポインタや、関数ポインタの配列 も存在する。
func()
関数へのアドレスが戻り値の関数
戻り値が 1とか0とかの数値型、外部のポインタ変数1個ならば簡単にイメージがつきます。
戻り値が int なら 関数の宣言時も
int func(){〜}となる。
これが、特定の配列の先頭アドレスを返したり、
特定の関数のアドレスを return で返す場合 どう書くと良いでしょうか?
【 関数のアドレス型 】
下記の図は、int aaa(int b ,int c)のような構造を持つ関数を扱う場合
関数ポインタに渡す場合は、
関数a と戻値と引数の構造が 一致するように関数ポインタを宣言すると、入れられる。
構造が複雑な場合は、typedef を使って、関数aaaと同じ型(ここではret )を定義すること、わかりやすい構造にまとめています。
この図では、配列xを間に入れてますが、 アドレスを直接 func に入れたほうが簡単。
関数の戻り値に関数のアドレスを返すできませんか? - 教えて!gooを、
参考にして書いてみたもの。
#include "stdio.h"
//関数のプロトタイプ宣言
int other(int a);
int other2(int a);
//関数へのポインタの配列 宣言
int (*spoint[2])(int b)={ other, other2};
typedef int (*ret)( int b);
ret FUNC(int a);
//メイン
int main( void ){
FUNC(0)(110);// 参照先の引数も書いた
return 0;
}
//関数へのポインタの配列を返す 関数
ret FUNC(int b){
return (ret) spoint[b];
}
//普通の関数
int other( int b){
printf("other1参照されたよ: %d \n",b);
return b;
};
int other2( int b){
printf("other2参照されたよ: %d \n",b);
return b;
};
【11.ポインタを含むtypedef型定義】
次のようなプログラムの断片があるとして...複雑な戻り値の型、間接参照子の入れ子がわかりにくい
float fb=20.0f; // float型の変数
float* ggg(){ return &fb; };//float型変数のアドレスを返す 関数ggg
//■複雑。
float*(*hhh[10])()={ ggg }; // gggのような関数を入れられる、関数へのポインタの配列hhh
float*(*( *iii() )[10])(){ return &hhh;};// 関数へのポインタ配列hhh を返す、関数iii
float*(*(*(*jjj)())[10])()=iii; // 関数iii へのポインタ
これを、typedefを使って型を定義すると、もっとシンプルに書けます。
float fb=20.0f;
float* ggg(){ return &fb; };
//■typedef を使って、すっきりまとめる
typedef float *(a_t)();
typedef a_t *b_t[10];
typedef b_t *(*c_t)();
a_t *kkk[10]={ ggg };
b_t* lll(){ return &kkk;};
c_t mmm=lll; /* 関数 lll のアドレスを入れられる */
次のような複雑な typedef 型定義も作れますが、やはりわかりにくい
typedef float*(*(*(*complex_t)())[10])();
complex_t nnn=lll; /* こちらも関数 lll のアドレスを入れられる */
複雑な 型定義に出会ったら
このような直接的なtypedef型定義に遭遇してしまったときはどう読めば良いか?
typedef int ( * ( * ( *pointer ) (void ) ) [10] )(void );
という宣言がある場合、これは 多重間接参照の一種( ***pointer) だと思われます 。
括弧をヒントに、前後から攻める形で文章を分解し、出来た山の入れ子を、タテ割りに読んでみると、
今まで分りにくかった間接参照の構造が見えてきそう。
typedef int
(
* (
* (
*pointer
)(void)
)[10]
)(void);
この関数の型 pointer は、
int (x)(void); → *(x)[10]; → *(x)(void) → *pointer
int を返す関数 への 10個の配列ポインタ への 関数へのポインタ
を(戻り値)に持つ。
これをもとに、typedef で定義。
【12.構造体】
束配列(多次元配列もどき)は、同じ種類の配列しか束ねることができません。
しかし、構造体を使うと、配列や変数、関数(へのアドレス)等、複数のデータの種類をミックスさせたデータを作れます。
ポインタと組合せてることで動的なデータ群を作り出せます( 連結型リスト(連結したり切り離したり)、リング型リスト、自己参照型構造体)
その他
● あとで読みたい分野。
ローカル変数、グローバル変数 (スコープ)
変数型と消費メモリサイズ
記憶クラス 修飾子 変数型 変数名
|
- ポインタ---- 関数、変数、リテラル等のアドレスを指すことができる。
- 変数(識別子)--- 変更可能なデータ
- リテラル(定数) --- メモリのどこかに配置され、変更できないデータのこと
- 数値 32768
- 文字リテラル 'A' , 'B' '\0'
- 文字列リテラル(終端文字列付)"ABCDE"
- 組み込み型 int , long, double, float, char (C言語に標準で付いてくる型)
- 派生タイプ。ヘッダファイルをincludeすると使えるようになる
(型名の _t は 、typedef によって派生した型という意味らしい、組み込み型と区別するための接尾語)
- size_t ( 型や変数のメモリサイズを扱う型、sizeof 演算子の返値の型、 実は int型)
- time_t (時刻を扱う型)
- wchar_t( Unicodeのための ワイド文字列 )
- ライブラリによって定義された型。
- volatile修飾子。コンパイル時の最適化の禁止
- const 修飾子。 その変数や定数、ポインタのデータを変更不可(読取り専用)にする。
|
ポインタとconst修飾子
ポインタの場合、宣言時のconstの位置によって、ポインタが参照されるときの読み取り、上書きの挙動を細かく制限することができます。
式 | ポインタの性質 | 指し先の値の変更(*p=100) | 指す先の変更(p=&var) |
const int* p; | (*)参照先の値を変更できなくする | ×(読み取り専用) | ○ |
int* const p; | 指すアドレスを変更できなくする | ○ | ×(読み取り専用) |
const int* const p ; | 両方ともできなくする | ×(読み取り専用) | ×(読み取り専用) |
覚え方
const → int* : const効果は、参照先にかかる:よって、間接参照子(*)を使っても参照先を変更できない
const → p : const効果は、ポインタ自身にかかる:よって、 ポインタに他のアドレスを入れて変更できない
【重要】ポインタに付けられた const修飾子は、
関数の動作を把握する判断材料として、おおいに活用することができます。
関数とプロトタイプ宣言
定義する前に、関数を使うことは出来ません。これはコンパイラ(プログラムを解釈してアプリケーションを作る存在)が知らない関数に対し、どう解釈すればよいか分からないためエラーになります。
void main(){
int a,b;
a(a, b);/* 定義より先、 エラー */
}
/* 関数a の 定義 */
int a(int a,int b){ }
そこで関数のプロトタイプ(原型)宣言が役立ちます。この宣言は普通の関数の定義から、処理内容の{}を取り去ったものと同じで、 関数名と戻値の型、引数構造のみのシンプルな記述です。
プログラムの最初で、関数のプロトタイプを宣言し、
先に関数名と戻値、引数構造だけを知らせておくことで、プログラムを組み立てやすくします。
これは「関数へのポインタ」を作成する場合にも利用できます。
プロトタイプ宣言は、ソースコードの最初のほうに書いておくことができますが、
C言語のヘッダファイル .h のように、別のファイルに 関数のプロトタイプや構造体等の宣言をまとめ分けて書くこともできます。
これによって、別のソースコードの最初で #include すると、ライブラリのように、便利な関数を他のC言語のプログラムに組み込んでそこから使うことができます。
このとき、ヘッダファイルでは関数を二重に定義しないような仕組みを書く工夫が必要になります。
文字列と配列
文字列 = char型の配列 + 1(終端文字列が必須)
終端文字列が無い場合、文字列処理時に文字列を越えたメモリ領域まで走査してしまう(バグの原因となる)。
※ポインタとリテラルを関連づけた場合、間接参照によって文字列リテラルを上書きしようとすると、エラーになる。
※ポインタ配列で、複数の文字列リテラルの何文字目か調べる場合は、もう1個ポインタが必要になる→ ダブルポインタ
共用体
他言語では共用体のように、「多様」な変数を入れられるバリアント「variant」型 変数の概念があります。
メモリ操作関数
未初期化のポインタが指すアドレスに 値を書き込んだら、どうなったか。
#include "stdio.h"
int main (void)
{
int *p; // pはint型変数へのポインタを宣言
// 宣言しただけの 未初期化ポインタが指すアドレスに 値を書き込む。 ※危険
*p=5; // どこか知らないアドレスに 5という値を書き込む。
//pが指しているアドレスの変数は何ですか?
printf("「p=&a;」でpの値は%dになります\n", *p);
return 0;
}
↑のコードをコンパイルしたものを実行した。
アプリケーション "ポインタ.out" は予期しない理由で終了しました。
EXC_BAD_ACCESS(0x0001) KERN_PROTECTION_FAILURE(0x0002 at 0x********)
という エラーダイアログが出て、強制終了されました。
参考になった書籍やサイト
図解C言語 ポインタの極意 柴田 望洋 (著)
C言語ポインタ完全制覇 (標準プログラマーズライブラリ) 前橋 和弥 (著)
C言語のポインタがゼッタイにわかる本—最初からそう教えてくれればいいのに! 立山 秀利 (著)
C言語によるプログラムを手軽に作成、実行させるためのソフトTryC(Cで遊ぼう!)
二次元配列とダブルポインタの関係(Making The Road Blog)
関数の戻り値に関数のアドレスを返すできませんか? - 教えて!goo
配列とポインタの完全制覇
C言語での複雑な型表記を理解する方法 - ラシウラ
C: 関数へのポインタの typedef に関するヒント(e知識)
●あとでやりたい事
M+フォント ファミリーを使って、図を書き直す(進捗中)。
GIFアニメーション化
Last modified 9.03 、本文に関係ない部分を書き直す(前書き等)。
Last modified 2.07 、const に関する記述を追加。まとめる。
Last modified 2009.1.18 、ポインタ宣言・初期化の図表を更新
2008.11.08
Tsukubado