システム奮闘記:その36
C言語入門。ポインタと構造体
(2005年12月27日に掲載)
はじめに
オープンソースといえば、その言葉の通り、ソースコードが公開されている。
そのため、最大の利点と言えば、ソースを読んで勉強できたり、
上級者ともなれば、ソースコードの不具合を自分で修正できたりする。
さて、私のC言語のレベルといえば・・・
初歩レベルでーす (^^)
そうなのです。
長い間、私はポインタの所で足踏みをしていました。
ポインタと言えば、C言語の学習で、最初に越えなければならない壁だが、
私はその壁を乗り越える事ができず、足踏みをしていました。
しかし、ようやく、その壁を越える事ができたので、その壁を越えるまでの
話を書きたいと思います。
(免責事項)
この奮闘記で出てくるプログラムは、PlamoLinux4.0 の gcc-3.3.2で
動作確認をしていますが、他のOS等で動かない場合もあるかもしれません。
もし、他のOSで動かない場合があっても、プログラムを作成しているのは
プログラマーでなく、事務員なので、許してくださいね (^^;;
C言語のポインタとの出会い
私とC言語の出会いは、結構、古い。学生時代(1994年)まで遡る。
1994年の秋、大学2回生だった私は、C言語を勉強する必要が出てきた。
真面目に勉強なら良いが、実は、不純な動機からだった。
この時、Mosaic(IEやNetscapeの原型)にハマっていた私。
某N先端科学技術大学院大学のサイトから、スーパーモデルの水着画像を
どんどんダウンロードしたり、天体が好きな事から、NASAから天体画像を
大量にダウンロードしていた。
もちろん、芸術鑑賞で、おねーちゃん画像も、ダウンロードもしていた (^^;;
画像データをUNIX上で閲覧するにに、XVというソフトを使っていた。
大学にあったのがバージョンが2.1だった(と思う)
ある日、中学の時の友人Y君に「XVなら3.1があるよ」と教えてくれた。
早速、ダウンロードしてきたが
インストールの方法がわからない (TT)
私としては、XV3.1を入れてみたかった。
なぜなら、より高画質で、おねーちゃん画像が見れると思ったからだった (^^;;
当時、UNIXに詳しかったI君が「makeをして、コケなかったら、完成やで」と
教えてくれた。早速、makeをしたが、
コケてしもうた (TT)
I君は「じゃぁ、自力で頑張りな」と言った。
さて、困った。muleでメールを書いて、Mosaicでホームページの閲覧。
そして、XVでコレクションの画像を閲覧する事しか知らなかった私。
しかし、「何がなんでも高画質で、おねーちゃん画像を見てやるんだ!」と
燃えていた。エロパワーの発揮だった (^^;;
まずは、英語でインストールの方法が書いてあるファイルを読んだ。
普段なら諦める所なのだが、英文をエロパワーで解読した。
ccだとコンパイルできない。gccを入れる必要があると書いてあった。
単純な発想の私は、gccをインストールすれば良いと思った。
早速、gccをダウンロードした。そして、tar.gzから展開すると、50Mを越える。
当時は、ディスク容量も少なかったため、大学の情報教育センターの
ディスクが満杯になってしまった。
なのでセンターの職員に
お前、何、考えてんねん!!
と怒られた (--;;
gccの導入を諦めた。そこで、UNIXに詳しい先輩に聞くと
ccは、ANSI規格外のC言語で、gccはANSI規格のC言語と教えてもらった。
商用UNIXの場合、ccとgccとは別物 |
FreeBSD、Linuxだと、ccもgccも同じ(単にリンクしているだけ)なのだが、
商用UNIXだと、ccは独自の付属のコンパイラなので、gccとは別物になる。
大学2回生の時、情報教育センターには、日立のワークステーションがあった。
3回生になった時、SunのSolarisが入った。
私が3回生になり、配属された研究室には、SunOS4.3、HPUXがあった。
よく遊びに行っていた他の研究室には、IBMのAIX(RS6000)が入っていた。
商用UNIXで、gccはデフォルトで入っていない場合があったので、
ソフトのインストールする際は、ccでコンパイルを行う。
AIXが一番、makeが通りやすかったため、AIXが好きになった。
Solarisは、よくエラーが出たが、ソースを書き換える技術なんぞないので、
ソフトのインストールを断念した事が、しばしばだった記憶がある。
意外と商用UNIXを触る機会があったのだが、ちょっとでも難しい事になると、
チンプンカンプンで前に進まなかった (^^;;
|
gccでないとコンパイルができないと書いているのだが、単純な発想の私。
だったら、C言語を勉強して、ソースを書き換えれば良いと考えた。
そこで、C言語の本を買った。
「入門 C言語」、「実習 C言語」(三田 典玄:アスキー出版局)
エロパワーで、if文、for文などを覚えていったが、肝心の知りたいはずの
ccとgccとの違いには、遠く及ばない。
それ以前に、ポインタの所で力尽きてしまった。
あまり大した事のないエロパワーだった (^^;;
しばらくして、ふと、インストールの方法を書いたファイルを見直すと、
Makefileの一部に「#」をつけたら、ccでもmakeが通ると書いていた。
早速、該当の行に「#」をつけて、makeをしたら、コンパイルが通った。
思わず万歳!! (^^)V
さて、お楽しみの高画質と思ったら、高画質にならなかったので、がっかりした。
でも、この事をキッカケに、makeというコマンドを打てば、
UNIX上にソフトをインストールできる事を覚えた。
それ以来、情報処理センターのUNIXの自分のディレクトリーに、
emiclockやテトリスなどを入れて遊んでいた。
それからしばらく、C言語を触る事はなかった。
3回生の後期(1995年9月)に、研究室配属が決定した。
私が手先が不器用なので実験嫌いと誤解された上、パソコンで遊んでいたため
パソコン好きという事で、観測データの解析とシミュレーションをする事になった。
データ解析と、シミュレーションを行なうのに、C言語を使う事にした。
今までの先輩方の多くは、FORTRANでプログラムを作っていたが、
私の場合は、FORTRANは大嫌いだった。
他で知っていると言えば、少し触った程度の、C言語かBASICだったが、
BASICは速度が遅過ぎるため、何の迷いもなくC言語になった。
FORTRANが大嫌いな私 |
私が学生時代は、物理の計算といえばFORTRANだった。
数値計算に向いているという事で、計算物理などの本もFORTRANが主流だった。
数値計算を行うためのライブラリの蓄積という資産も大きい事もあるため、
現在でも簡単には他の言語には移行しないと思われる。
私が通った大学でも当時は、物理学科の2回生には、FORTRAN実習があった。
しかし、面白くない数値計算や、わけのわからないシステム用語で、嫌になった。
実習は1年間で、週3時間。しかし、2単位しかもらえないことから
「FORTRANは人間のやるもんやない」と勝手に宣言して、自主休講(サボり)をした。
それ以来、無条件でFORTRANに対して拒否反応を示すようになった。
さて、みんながFORTRANの実習で苦しんでいる間、自主休講の私は
UNIXのある部屋で、メールやネットサーフィンで遊んでいた。
当時、UNIXのある部屋には、海外から来た留学生が母国の友人との通信で、
電子メールを使っていた。そこで、留学生と仲良くなったりした。
一緒にボーリングへ行ったり、姫路城へ案内したりなど、楽しい思い出が
できたため、今、思えば、FORTRANに見切りをつけて正解だったと思う (^^)
|
まずは、シミュレーションプログラムを作成。
教授の指導の下、私は独自のプログラムを作成するため、
他人のソースを読む必要はなかった。
C言語で書くが、ポインタなんぞ理解していなくても、問題はなかった。
if文、for文、配列さえ使えば、シミュレーションプログラムを書けたので、
無理にポインタ、構造体を使う必要もなかった。
データを作成して、gnuplotでグラフ化するだけだった。
問題は解析プログラムだった。院生の人はFORTRANで作成していたのだが
私はFORTRANは全くダメ (--;;
お師匠様であるY教授に「君は、コンピューターが得意なはずではないかね。
FORTRANぐらい、すぐにできるやろ」と言ってきたので、私は「別物なので、
できません」と言った。3回生の時は、シミュレーションだけやった。
4回生になって、実験データの解析をやる事になった。
当時、博士課程にいたSさんがC言語で解析プログラムを作っていたので、
Sさんにソースを貰うことにしたが・・・
構造体が全くわからへん (TT)
だった。
構造体を理解するには、まず、ポインタを理解する必要がある。
そこで、ポインタを勉強する事になったのだが、とんでもない誤解を
していたのだった。
普通、変数を宣言した場合、次のような扱いとなる。
変数の宣言と使い方 |
int a ; (宣言)
a = 4 ; (変数「a」に数値「4」を格納)
&a ; (変数「a」の場所を表すアドレスの表示)
|
さて、ポインタ変数の場合は次のように行う。
ポインタ変数の宣言と使い方 |
int *a ; (宣言)
a = 23 ; (ポインタ変数「a」に、アドレス「23」の格納)
*a = 4 ; (格納された「23」の場所に、int型の「4」が格納される)
|
だが、当時、C言語の本を読んでも、アドレスとか、メモリなど、
全くわかっていなかった。
そのため、ポインタ変数の部分を読んでも、さっぱり理解ができない。
色々、考えた末、ポインタ変数を以下のように考えるようになった。
ポインタ変数をこんな風に間違えて覚えてしまった |
int *a ; (宣言)
*a = 4 ; (変数に「4」を代入)
a ; (変数「4」が格納されているアドレスを表示)
|
そして、この時、私は以下のような結論を出した。
ポインタ変数とは、単に変数に「*」をつけているだけで、変数と同じだ!
そして、私の頭の中では、わざわざ以下のような比較表まで作っていた。
私の頭の中にあった変数とポインタ変数との表現方法の比較表 |
比較項目 |
変数の場合 |
ポインタ変数の場合 |
宣言方法 |
int a ; |
int *a ; |
アドレスの表示 |
&a |
a |
そんなデタラメな理解だったので、ポインタの活用方法を読んでも
内容を理解できるわけがない上、そもそも活用なんぞできるわけがない。
しかし、当時は、わかったつもりだったので、エラソーに
初心者にとって、ポインタの壁は大きいのらー!!
と言っていたのであった (^^;;
結局、構造体どころか、ポインタすら理解できていない状態だったのだが、
ソースを丸写しをして、なんとかゴマ化す事ができた。
その上、よくあるパターンの誤解をしていた。
本来なら、ポインタと配列とは全くの別物なのだが・・・
ポインタと配列は同じと思っていた!!
のだった (^^;;
なぜ、そんな誤解をしていたのか覚えていないが、おそらく、本の記述か
何かで「配列とポインタは同じ」というものを見たためだと思う。
さすがに古い記憶を正確に呼び戻す事は困難だ (--;;
当時、研究室の中で、ポインタを使えば、計算速度があがると言われていたので、
私は、ポインタがダメでも、ポインタと同じ物だと思っていた配列を使えば、
プログラムの速度向上になると思ったため、無意味に配列を使っていた (^^;;
無意味な配列の利用について |
int型の変数aを宣言する時、C言語では次のように書く。
int a ;
当時、私は、配列を使えば、変数の出し入れが速くなると思い込み
普通にint型の変数を宣言する場合、わざわざ次のように配列の宣言をしていた。
int a[1] ;
今、思えば、無意味な利用法なのだが、天才的なぐらい速度の遅い
プログラムを書いていた私は、藁をも掴む思いで、配列を使っていた。
|
C言語とポインタから遠ざかる
大学時代、中学の時の友人Y君と話していて、プログラムの高速化が話題になった。
その時、Y君が「システム会社に入ったら、研修などで高速プログラムを作る
テクニックを教える」という話をしてくれた。
パソコン好きの私なので、就職先はSEを志望。なんとも単純な理由だった。
Y君の話を聞いていただけに、SEになれば、新人教育でポインタなどが
理解できるのではという淡い期待を持ったのだが、しかし・・・
どこのIT企業も雇ってくれへん(TT)
会社規模関係なく、30社くらいシステム会社、ソフト会社の就職試験に臨んだが、
ことごとく撃沈された・・・。
もちろん、理系なので技術職を目指して、メーカーなども攻めたが全て撃沈。
最後には、ヤケになり、証券会社や商社、怪しい会社も受けたが全滅・・・ (--;;
そして紆余曲折した後、最終的には、大学の事務職員の方が
「パソコンができる人を募集している会社があるよ」と言って、
今の勤務先を紹介してくれた。見事、内定がもらえた。
私は「SEになれる。システムをバリバリ触るぞ」と意気込んだのだが・・・
待っていたのは、事務の仕事だった (--;;
会社が求めていたのは、サーバー構築やネットワーク管理ができる
バリバリのSEではなく、エクセル、ワードができる、パソコンの事務処理に
強い人材だった。
そのため、C言語の習得から遠のく事になった (^^;;
私は物理学科出身なので、同じ物理学科の友人達の多くは技術系に就職。
だが、私は事務系。技術系の就職した友人達と話が噛み合わなくなる事がある。
プログラマーになったT君と会った時だった。
T君は「うちの会社のパソコン、全てリースやねん」と言ったので、
私が「そりゃ、資産管理や減価償却の手間を省いたり、リース期間は、
法定耐用年数より短くできるからやで」と言っても、話が通じない。
T君は「減価償却」や「法定耐用年数」が何なのかを知らないからだ。
逆に、T君が、VisualBasicの話をしても、私が理解できない (--;;
そのため、話題が噛み合わなくなる事もあった。
反対に、経済や経営を出ている友人とは、色々な話ができるようになった。
「やっちゃいけないワルな経理の話」をすると、相手は爆笑したりする。
労務や法務の知識はゼロで、本職の事務員としては全くのダメダメなのだが、
多少の経理ネタが話せるだけで、結構、話が盛り上がる。
事務員をやっていた得られた事と言える (^^)V
再度、ポインタに挑戦するが・・・
再び、C言語を勉強するようになったのは、2001年2月だった。
ネット販売システムの導入する時だった。最初は外注予定だったのだが
S社の強引なWindowsシステムの売り込みで、ブチ切れた私は、こんな所に
頼むぐらいなら、CGIを使って自社開発した方が良いと思った。
この時、Perlで組んだら、処理が重たくなると思った私は、
高速で動かすためにはCGIを、C言語で作らねばならないと思い
C言語の本を開いて勉強する事になった。
当時、東大の物理の博士課程だった渡辺さんのホームページを発見した。
C言語を使ったCGIのプログラムが書いてあった。
早速、渡辺さんの了承を得て、ソースコードを入手。しかし問題があった。
ポインタだらけで、わからへん(TT)
ポインタを使っているため、全くソースが読めなかった。
そこでC言語の本を取り出し勉強する事になった。
業者の態度にキレた私だったので、怒りのパワーで勉強した。
学生時代のエロパワーよりも馬力があったお陰で、CGIでシステムを完成させた。
この時、プログラムでポインタがあったが、なんとか活用できた。
そのため、ポインタを理解したと思い込んだ。
だが、この時も学生時代同様に、ポインタを覚えたつもりになっていた。
実際には、全く理解していなかった事が後になり発覚するのだった (^^;;
実はポインタを理解できていなかった・・・
さて、ポインタも構造体も理解できたと思い込んでいた私だったが、
2002年7月、その思い込みが簡単に崩れ去る時がやってきた。
前述でも登場した中学の友人Y君とのメールのやりとりだった。
Y君はソフトウェア工学が専門で、東大博士課程でメモリ管理の研究を行ない、
博士号を取得し、現在は、某社の研究所で勤務しているソフトウェアの研究者だ。
C言語が話題になった。
私が「構造体は知っている程度。もちろんポインタは苦手」と書いたので、
Y君は「ポインタを知らなくても構造体は分かるのか…」と返答してきた。
そこから始まったやりとりで、実は、私がポインタを理解していない事が発覚した。
Y君とのメールのやりとり |
Y君への返事 |
> ポインタを知らなくても構造体は分かるのか….
苦手と知らないとは別だよ。
ポインターは変数にメモリ指定をすることでしょ。
int *a ; として、変数は *a で表して、
そのデータの格納されている場所を a で表しているということでしょ。
a = アドレス で、アドレス指定もできるしね。
フツーに int a ; としたら、格納場所は &a で出るけど、
そのプログラムが終了するまで、同じ格納位置にあるとは限らないけど、
ポインター指定すれば、終了まで同じ場所に格納されている。
というよりも、その場所に、ずーと変数のデータが
確保されているということでしょ。
|
Y君からの返事 |
suga> ポインターは変数にメモリ指定をすることでしょ。
違います.
ポインタは,メモリアドレスを格納する変数です.
例えば,
int c, *d;
とすると,
c --> 整数を格納する変数.
d --> 整数が入っているメモリアドレスを格納する変数.
がシステム内に確保されます.例を挙げると,
0x7ffff938番地に整数をいれる変数 c を作る
0x7ffff93c番地にメモリアドレスをいれる変数 d を作る
んでもって
d = &c; <-- 変数c のアドレス 0x7ffff938 を
アドレス型変数 d の中に格納
c = 3; <-- 変数c に整数3を格納
ってやると終了時には
アドレス 中身
&c = 0x7ffff938, c = 3
&d = 0x7ffff93c, d = 0x7ffff938, *d = 3,
ってな感じになります.
変数 c の場所(0x7ffff938)には整数そのものの3.
変数 d の場所(0x7ffff93c)には整数 c のアドレス 0x7ffff938.
が入っているというワケですね.
suga> int *a ; として、変数は *a で表して、
int *a とした段階で,"a" という場所は,
「整数が入っているメモリアドレスを格納する場所」となります.
因みに int b とすると "b" という場所は,
「整数そのものを格納する場所」となります.
因みに,誤解してそうだからもう一つ言っておきますと,
int *a とすると,「整数メモリアドレス」型の変数領域が一つ
確保されるだけで,整数そのものが入る土地はどこにもありません.
んでもって,int b とすると,「整数」型の変数領域が一つ確保されます.
suga> フツーに int a ; としたら、格納場所は &a で出るけど、
suga> そのプログラムが終了するまで、同じ格納位置にあるとは限らないけど、
通常のC言語処理系であれば確実に同じ場所に配置されます.
(一部,GCをするC言語処理系であれば別.)
suga> ポインター指定すれば、終了まで同じ場所に格納されている。
suga> というよりも、その場所に、ずーと変数のデータが
suga> 確保されているということでしょ。
通常のC言語処理系では,システムが勝手に場所をうつすことは
出来ません,(因みに Java 処理系はシステムが勝手に場所を移します.)
|
Y君への返事 |
> 因みに,誤解してそうだからもう一つ言っておきますと,
> int *a とすると,「整数メモリアドレス」型の変数領域が一つ
> 確保されるだけで,整数そのものが入る土地はどこにもありません.
>
> んでもって,int b とすると,「整数」型の変数領域が一つ確保されます.
これは知らなかった。
int *a = 4 とすると、 *a に4が入るから、そう思っていた。
> suga> フツーに int a ; としたら、格納場所は &a で出るけど、
> suga> そのプログラムが終了するまで、同じ格納位置にあるとは限らないけど、
>
> 通常のC言語処理系であれば確実に同じ場所に配置されます.
> (一部,GCをするC言語処理系であれば別.)
ここも、誤解していた。
プログラムが終了して、2度目に動かした場合、
同じアドレスになるとは限らないが正しいよね。
> 正しく理解できていないに一票.
どちらにせよ難しい!!
だから、ポインターは嫌いなのらー!!
|
Y君からの返事 |
suga> > 因みに,誤解してそうだからもう一つ言っておきますと,
suga> > int *a とすると,「整数メモリアドレス」型の変数領域が一つ
suga> > 確保されるだけで,整数そのものが入る土地はどこにもありません.
suga> >
suga> > んでもって,int b とすると,「整数」型の変数領域が一つ確保されます.
suga>
suga> これは知らなかった。
suga> int *a = 4 とすると、 *a に4が入るから、そう思っていた。
入ることは入るのですが…….
実行する計算機側の立場になって考えてみると,それが
あまり良くないことが分かります.
例えば,
int a, *b;
b = &a;
…
ってやった場合,計算機は整数型1つ,整数アドレス型1つ,
計2つの変数をメモリ上に取ればよいと言うことになります.
一方,
int a, *b;
a = 4;
*b = 3;
ってやった場合,計算機は整数型2つ,整数アドレス型2つ,
計3つの変数をメモリ上に取らねばなりません.
この違い,1行目を見ただけでは分からないですよね?
常に下の場合を考慮して,int *b みたいな整数アドレス型を
用意する度に,必ず整数型変数も一つ用意していたら,かなり
無駄な感じしません?
普通のコンパイラは,1行だけではなく,関数の中をざっと
見渡してメモリの使い方を決定するので,上の2つを見分けることが
出来るのですが,これに頼るプログラムは良いモノではありません.
suga> どちらにせよ難しい!!
suga> だから、ポインターは嫌いなのらー!!
教え方もダメな気がするんですよね….
因みに先日,某学会誌に
Cや Fortran の様な高級言語から教え始めると
計算機の動く仕組みが理解されない.
アセンブラっぽいものから教えて行くべきだ.
っていう記事を投稿していた教授がおりました.
私も同意見.
|
私が全くポインタを理解していない事がバレバレだ (^^;;
そして、すごろくゲームで、振り出しへ戻されるような感じで、私のC言語の学習が
ポインタの手前の位置に戻された。
ここで、この時、私がしていた誤解をまとめてみます。
まずは、普通の変数とポインタ変数との違いを見てみる事にします。
普通の変数について |
|
例えば、int型で変数宣言を行う場合、
int a ;
となる。すると、int型の変数を格納する場所を
自動的に確保してくれる。
左図の場合だと、番地「2」が自動的に割り当てられた
状態になっている。
a = 4 ;
は変数に数値「4」を代入すると番地「2」に、
int型で表現された数値「4」が格納される。
さて、数値が格納される番地を知るには、
&a とすれば、番地がわかる。
|
さて、次にポインタ変数について見てみると
ポインタ変数について |
|
ポインタ変数の宣言を行う場合は
int *a ;
となる。ポインタ変数とは、アドレスを格納する変数で、
普通の変数同様、どこに格納されるのかは自動的に決まる。
上図の場合は、自動的に番地「3」に割り当てられた形になる。
アドレスが格納される場所(アドレス)を表示させるには
変数同様に「&a」とすればOK。
さて、ポインタ変数の場合、アドレスを格納させるのが目的なので
a = 25 ;
とすると、番地「25」という情報が、番地「3」に記録される。
この時、ポインタ変数の宣言で int *a ; になっているが
これはアドレス番地の表現がint型という意味でなく、格納されたアドレス
(この場合は番地「25」)にある物が、int型という意味になる。
|
だが、この時、ポインタ変数の事を以下のように思い込んでいた。
私が誤解していた内容 |
|
int *a ;
でポインタ変数を宣言するのだが、この後の理解がメチャクチャだった。
a = 25 ;
は、番地「25」をポインタ変数「a」に格納する意味なのに、
私は、int型の数値が入る場所に、番地「25」を指定したと思い込んでいた。
なんと番地を自由に指定できると思い込んでいたのだった。
*a = 4 ;
は、番地「25」の場所に、int型の「4」が格納されると思い込んでいた。
|
要するに、ポインタ変数の場合、変数が格納されるアドレスを自由に指定でき
そのアドレスの中に、目的の数値などを代入する物だと誤解していたのだった (^^;;
そのため、平気でint *a = 4 ;という発想が出てくるのであった。
だが、Y君に指摘された時点では、まだ、Y君が書いている内容の意味が
全く理解できていなかった。
唯一わかった事は・・・
私はポインタを理解できていない!
という事だった (^^;;
おまけに、Y君からのメールで、こんな事も書かれてしまった。
Y君とのメールのやりとり |
suga> ポインターを使っているソースは、見るのも嫌。
となると,Linux のカーネルとかは読めない?
そうなると,Linux を使う意味が半減.
|
まさに、痛恨の一撃だった。
「オープンソース(OSS)で中小企業のIT化」を掲げている私なのだが、
ソースコードが読めないので、オープンソースの恩恵を受けていない事が言える。
これでは、プログラムの中身がブラックボックスのWindowsと変わらない。
「無料だったらWindowsでも良いのか」と言われても仕方がない状態だ (--;;
3回目の挑戦を行うが・・・
2002年の終わり頃、次の本と出会った。
「C言語ポインタ完全制覇」(前橋 和弥:技術評論社)
この本はポインタがわかりやすく解説している本として知られている。
そこで、藁をもすがる気持ちで、この本に賭けてみる事にした。
この本でダメなら、ポインタの壁は、一生、越えられない壁だと思った。
しかし、「突破してやる!」という意気込みはなかった。なぜなら・・・
必要に迫られていないからだった(^^;;
必要に迫られないと、やる気が起こらない私。
本を買った事で満足してしまい、何もしなかった (^^;;
2003年の夏、本を買ったが「積ん読」だと勿体ないと思った私は、
暇を見ては、ポインタの勉強をする事にした。
この時、ポインタで、自分が誤解していた内容に気づいた。
ポインタ変数は、アドレスを格納する物だとわかった!
だが、そこから先を進もうとしたのだったが・・・
難しくて理解できへんかった (TT)
原因は、用語につまずいたのだった。
変数に、int型、doubles型があるように、ポインタにはポインタ型があると
書かれても
さっぱり、わからへん (TT)
だった。
アドレス演算子「&」、間接参照演算子「*」という感じで書かれても、
意味が理解できぬままだった。
わからない事がある時は、そこの部分を飛ばして読んでいくと、
意外な所で理解できる場合がある。
そこで、わからない所は飛ばして進む事にしたが、結局、ポインタが何なのか
そして、ポインタを使うメリットが見えて来なかった。
知識が全く整理されず、頭の中でグチャグチャのままだと、何も理解できない。
だが、一つ発見があった。malloc関数の意味だった。
メモリ領域の確保をするために使われる関数だと知る。
学生時代に、malloc関数を見て、意味不明だったのだが、
この時、メモリ確保のための関数だという事を知った。
一応、収穫はあった (^^;;
だが、結局、ポインタの学習で撃沈(というより自爆)してしまった。
それ以来、自分にはポインタの壁は一生乗り越える事ができないと確信し、
開き直ってしまった。そして「ポインタが理解できない私」と言っては
C言語ができない事を堂々と自慢(?)していた (^^)V
ポインタの理解を断念できない私
C言語の学習とは無縁の生活が続く。
プログラムが読めなくても、本やネットの情報で自分でソースを読まなくても
ある程度は、システムの中身を理解する事だってできると思っていた。
さて、2004年12月、神戸情報大学院大学の入学説明会へ行く。
(URL:http://www.kic.ac.jp/)
オープンソース専門の大学院で、講師陣には錚々たる顔ぶれだ。
赤松先生のセキュリティーのミニレクチャーを受ける。
ここで強く感じたのは、C言語を理解していると、ソースコードが読めるので
ソースを読んでプログラムの動きや、システムの動作原理を知ったり、
セキュリティーホールの問題点などを、ソースレベルで見れるようになる事だ。
赤松先生に「ポインタが、わからないのですが、大丈夫でしょうか」と尋ねると、
赤松先生は「キンコンカンで教えると、わかるようになります」とおっしゃった。
だが、キンコンカンは企業秘密(?)という事で、入学しない限り、
教えてはもらえない。
オープンソース専門の大学院が神戸にできる。神戸は私の地元なので、
夜間授業なら、通学してみようと思うが、授業が日中なので、
サラリーマンだと通学できない。
パラサイト・シングルなので、普段の生活費は、あまりかからないため、
会社を辞めて、バイトをしながら、大学院へ行くという手もあるのだが、
サラリーマンを辞めると・・・
システム奮闘記が書けなくなる (--;;
大学院でパワーアップをして、バリバリ活躍できる自分の姿を夢見ながら
簡単には入学できない現実が立ちはだかっていたのだった。
もちろん、入試に数学・英語があるため、入学試験に受かるのかと聞かれると
答えに詰まってしまうのだが・・・ (^^;;
ついに理解できたC言語のポインタの話
2005年1月。大プロジェクトだったインターネットVPNから解放される。
詳しくは「システム奮闘記:その35」(通信費用大幅削減。インターネットVPN導入)をご覧ください。
だが、次の課題が待っていた。
折しも、2005年4月から施行される個人情報保護法がある。
そのため、情報漏洩の防止のため、セキュリティーを強化する必要が出てくる。
セキュリティー対策を行うには、余程の実力が必要とされるため、
急に、セキュリティーを強化させる事などできるわけがない。
だが、何か事件が起こった場合、それまでの間、努力していた痕跡がないと
言い訳ができなくなる。
「急がば回れ」の言葉通り、地道に実力をつける以外、方法がない。
そこで、C言語を理解して、ある程度、ソースが読めるようになったり、
C言語をキッカケに、システムの動作原理を理解できるようになろうと考えた。
だが、ヌリ壁のように立ちふさがる「ポインタの壁」がある。
今回は、googleを使って、よりわかりやすいポインタの説明を書いているサイトを
探そうとした。すると、わかりやすいサイトを発見した。
小出俊夫さんの「KID's World」というサイトだ。
http://homepage1.nifty.com/toshio-k/
「C言語の要」という部分で、わかりやすく解説しているのを見た。
http://homepage1.nifty.com/toshio-k/prog/c/pointer.html
そして、自分の頭の中で整理して、ポインタがどういう物かを
思い描いてみる事にした。
ポインタを思い描いてみた |
|
ポインタ変数の宣言を行う場合は
int *a ;
となる。ポインタ変数とは、アドレスを格納する変数で、
普通の変数同様、どこに格納されるのかは自動的に決まる。
上図の場合、番地「3」が自動的に割り当てられた形になる。
アドレスが格納される番地を見るには「&a」とすれば良い。
さて、ポインタ変数の場合、アドレスを格納させるのが目的なので
a = 25 ;
とすると、番地「25」という情報が、番地「3」に記録される。
この時、ポインタ変数の宣言で int *a ; になっているが
これはアドレス番地の表現がint型という意味でなく、格納されたアドレス
(この場合は番地「25」)にある物が、int型という意味になる。
|
なるほど、ポインタって、こういう事だったのか!
C言語と出会ってから10年。
ようやく、ポインタが何かが頭の中で鮮明に描けた!!
当時、21の青年だった私は、今ではすっかり31のオッサンになっている。
ポインタを理解する遅さは、ギネスブックに載っても恥ずかしくない遅さだ!
ここで、次のような事を考えた。
int *a;で宣言した場合、ポインタ変数「a」に格納するアドレスを、
違う型式の変数「b」のアドレスにすれば、エラーが出るのはでと思った。
自分のポインタに関する理解が正しいかどうか確かめるため、
下のプログラムを書いて、コンパイルをかけてみる事にした。
プログラム (test.c) |
#include <stdio.h>
int main(void)
{
int *a ;
char b = 4 ; (int型のすべき所を、char型にしてみた)
a = &b ;
printf("%d \n",*a);
return(0);
}
|
suga@jitaku[~/c]% gcc test.c
test.c: 関数 `main' 内:
test.c:8: 警告: 互換性のないポインタ型からの代入です
|
すると、予想通り、警告(warning)が出た。
int *a ;で宣言したポインタ変数「a」に格納されるアドレスの場所には、
int型の値しか入らない事を証明(?)した。
ついでに、int *a ;で宣言したポインタ変数「a」に格納されるアドレスは、
int型でない事を確かめたくなった。
そこで、次のプログラムをコンパイルしてみた。
プログラム (test2.c) |
#include <stdio.h>
int main(void)
{
int *a ;
a = 4 ;
printf("a = %d \n",a);
return(0);
}
|
suga@jitaku[~/c]% gcc test2.c
test2.c: 関数 `main' 内:
test2.c:7: 警告: 代入により、キャストなしで整数からポインタを作りました
|
すると、予想通り、警告(warning)が出た。
強引に、ポインタ変数「a」のアドレスにint型の値を入れたので、
上のような結果になった。
2002年にY君から指摘されたint *a = 4 ;が、おかしい事も理解できた。
ポインタ変数を宣言しても、ポインタ変数「a」には格納されるはずの
アドレスが格納されていない。数字の「4」が、どこに格納されているのか、
不明なのに、無理矢理代入しようとするから、おかしくなるのだ。
わかりやすくするため、図に表してみる事にした。
int *a = 4 ;が、おかしい理由 |
|
ポインタ変数「a」を宣言した段階では、ポインタ変数の中に格納される
アドレスは入っていない。そのため、int *a = 4 ;としても、
一体、「4」を、どこに格納されば良いのか、わからなくなる。
|
頭でわかっても、実際に、自分の目で確かめないと気が済まない私。
そこで、次のプログラムを作って、コンパイルと実行を行った。
プログラム (test3.c) |
#include <stdio.h>
int main(void)
{
int *a = 4 ;
printf("%d \n",*a);
return(0);
}
|
suga@jitaku[~/pro]% gcc test3.c -o test3
test3.c: 関数 `main' 内:
test3.c:5: 警告: 初期化により、キャストなしで整数からポインタを作りました
suga@jitaku[~/pro]% ./test3
セグメントエラー
|
コンパイル時には警告(warning)は出るわ、実行するとエラーが出るわで
予想通りだった。
これは、倉庫にて、荷物の置場を決めていないのに、荷物が持ち込まれても
困るだけなのと同じで、値を格納する場所が決まっていないのに、
値だけを代入しようとしても、コンピューターは困るだけなのだ。
ポインタを活用する利点について
ポインタがどういう物か、わかったので、すっかりご機嫌になった私だが、
この時点では頭の中で思い描けても・・・
ポインタって、どんなご利益があるねん?
という感じだった。
ポインタが、どういう時に使われ、どんなご利益があるのか知らないと
本当に、わかった事にはならない。
そこで再び、次の本の登場になる。
「C言語ポインタ完全制覇」(前橋和弥:技術評論社)
2003年、この本を読んだ時は、難しい用語で頭が混乱し、挫折した本なのだが、
今回は、ポインタがどんな物かがハッキリ見えたので、一気に見通しが良くなる。
面白いほど、本の内容が理解できる!
ドミノ倒しのように、一度、ドミノが倒れると、その勢いは止まらなくなる!
まず、つまいづいていた用語が、どういう事を意味しているのかが理解できる。
「&」の事を、「アドレス演算子」と本に書いている。
変数の頭に「&」を付ければ、変数の居場所(アドレス)が表示される事から、
変数からアドレスを求める(演算する)物なので、「アドレス演算子」と言う。
「*」は、「間接参照演算子」と言われるが、ポインタ変数に格納されている
アドレスで、そのアドレスの場所には、何が格納されているのかを
間接的にポインタ変数を使って見るため、「間接参照演算子」と言う。
ポインタのイメージが頭に入っていると、用語の意味が理解できる。
変数は値を格納するが、ポインタ変数はアドレスを格納する。
値を格納する場合と、アドレスを格納する場合。
一体、アドレスを格納したから、どんな利点があるのか考えてみた。
すると、2つ以上の関数をまたいで値を操作したい場合に、
アドレスを格納させるポインタ変数の威力が発揮される。
そこで、まず、値だけを扱う変数について考えてみた。
値を他の関数へ渡した場合は |
|
変数の場合、変数の中に格納されている値を他の関数へ渡す事ができる。
だが、この場合、値そのものを渡すだけなので、main関数で代入した値を、
func関数で加工しても、main関数側では一切、関知しない。
|
変数の値を他の関数へ渡すだけだと、何ら変化はない。
ところが、アドレスを他の関数へ渡すとなれば、話が変わってくる。
アドレスを他の関数へ渡した場合 |
|
main関数で代入した値が入ったアドレスを、func関数へ渡すとする。
func関数はアドレスを格納するため、受け取ったアドレスを
宣言しているポインタ変数に代入する。
*bは、main関数から渡されたアドレスにある値を参照しているため、
*bを加工する事は、main関数から渡されたアドレスにある値を加工しているのと同じだ。
そのため、func関数が終了した時、main関数の変数「a」の値が加工された状態になる。
要するに、関数をまたいで値を加工したい場合に、値が保管されているアドレスを
他の関数に渡せば、アドレスを受け取った関数は、値が保管されている場所が
わかっているので、容易に値を加工する事ができる。
それを可能にしたのは、ポインタ変数というわけだ!
|
過去に、ポインタの勉強に挑戦した際、アドレスを他の関数に渡す話があったが
なぜ、アドレスを渡すのかが理解できなかった。
上の説明だと、アドレスを渡す事の利点を示すのには、ちょっと苦しい。
だが、配列や構造体の話になると、より具体的に利点が見えてくる。
配列、構造体については、後述しています。
ポインタと配列の違い
さて本を読み続ける。本には「配列とポインタは別物」と書いている。
まずは、配列とはどういう物か見てみる事にした。
配列とは何か? |
|
配列とは、同じ型の変数が並んだ物を言います。
上図のように a[5] と宣言しますと、5つ同じ型の変数が並びます。
もちろん、a[]の中に入る添字の順番通りの並びで番地に入ります。
配列を宣言した際に、配列の先頭(上図の場合 a[0])のアドレスは
5つ分の変数が並べる領域になるように、自動的に割り当てられる。
上図だと、先頭のアドレスが自動的に「21」番地が割り当てられている。
|
配列とは、横並びになっている変数であって、ポインタ変数とは
別物だという事がわかる。
次にポインタを見てみる事にする。
配列とポインタとの混同は、ポインタについている「添字」が問題になる。
そこで、ポインタについている「添字」が何なのかを見てみる事にした。
int *p ;とポインタを宣言した時に、*(p+i)という形で、
「i」という添字が出てくる事がある。
これが今まで何を意味しているのか謎だった上、ポインタ変数に添字をつけた
プログラムを書く人は「凄い人だ!」と思っていた。
だが、実は、私が思い込んでいたほど、難しい話ではなかったのだ。
ポインタの添字の意味 |
|
int *p ;
で、ポインタ変数を宣言します。
次に、ポインタ変数「p」に格納するアドレスを「21」とします。
p = 21 ;
次に添字が何を意味するかです。
*(p+1)の意味ですが、ポインタ変数「p」に格納されている
アドレス「21」の1つ隣の「22」に入っている物を見にいくという意味です。
*(p+2)は、2つ隣の「23」を、*(p+3)は、3つ隣の「24」を見にいきます。
つまり、*(p+i)の添字「i」の意味は、ポインタ変数に格納されている
アドレス(上図なら「21」)のi個隣りのアドレスに入っている物を
見にいくという意味だ。
|
ポインタの添字は、蓋を開けてみれば、意外と簡単な内容だった (^^;;
さて、配列が何なのか、ポインタの添字が何なのかが、わかった所で、
配列とポインタの妙な関連性がハッキリと見えてくる!
配列とポインタの関連性って
こういう事だった |
|
int *p;で、ポインタを宣言する。
そして、int a[5];で配列を宣言する。
もし、p = &a[0] ;(p = a ;でもOK)で、ポインタに格納するアドレスを
配列 a[] の先頭アドレスを格納する。
(配列のアドレスで、先頭アドレスの場合、&a[0]以外に、aで表せる)
ところで、ポインタの添字(例えば「i」)は、ポインタ変数に格納された
アドレスからi個隣りのアドレスに入っている物を見るのを意味している。
すると、上図のような構図が出来上がる!
*pは、アドレス「21」の中身を見ているので、a[0]を見ている事になる。
*(p+1)は、アドレス「22」の中身を見ているので、a[1]を見ている事になる。
*(p+i)は、アドレス「21」からi個隣りの中身を見ているので、
a[i]を見ている事になる。
本来、配列とポインタは別物なのだが、ポインタの添字を使う事で
配列とポインタが同じような振る舞いをしているように見える。
そのため、配列とポインタは同じ物だという誤解が生まれやすい!
|
全くの別物が、あたかも同じような物として振る舞う現象。
それをネタにした例題として、以下のようなプログラムを良く見かける。
配列とポインタの関連性を示したプログラム |
#include <stdio.h>
int main(void)
{
int a[] = { 4 ,5 , 3 , 5 ,3 } ;
int *p , i ;
p = a ;
for ( i = 0 ; i < 5 ; i++ )
{
printf("%d \n",*(p+i));
}
return(0);
}
|
ポインタを使って配列表示をさせるプログラム。
あたかも、*(p+i)とa[i]が同じように見える。
|
配列とポインタには、妙な関連性がある事を書いたのだが、
なぜ、そんな物が生まれた経緯は、わからないし、調べる気も起こらない。
物事を理解するには、歴史などを勉強する必要があると言われそうなので、
きちんと反論します。
事務員だから知らなくても良いのらー!!
いつもの如く「事務員だもーん」という常套手段を使って逃げる事にして、
この関連性の良さを書く事にします。
配列という物は、別の関数へ渡す事ができないという問題がある。
別の関数へ渡せるのができるのは、変数の値、アドレスです。
そこで、配列の先頭アドレスを他の関数へ渡し、渡した先の関数では
ポインタを使って操作を行うという技を使います。
以下のようなプログラムが活躍するというわけです。
配列とポインタの妙な関連性を活用したプログラム |
#include <stdio.h>
void func(int *p)
{
int i ;
for ( i = 0 ; i < 5 ; i++ )
{
printf("%d \n",*(p+i));
}
}
int main(void)
{
int a[] = { 4 ,5 , 3 , 5 ,3 } ;
func(a) ;
return(0);
}
|
main関数で配列a[]を宣言したのだが、この配列の中身を別の関数func()で
利用したい場合を考える。配列そのものは別の関数へ渡す事ができない。
そこで、配列の先頭アドレスを関数func()へ渡す事にする。
関数funcでは、ポインタ変数「p」に、受け取ったアドレスを格納する。
そして、*(p+i)を使って、あたかも配列と同じように振る舞う特徴を使えば
別の関数においても、main関数で宣言した配列の中身を使う事ができる。
|
ここまで来たのだが、配列とポインタが混乱していない。
「楽勝、楽勝」と思いつつも、なんで混乱するのだろうかと思いながら、
引続き「C言語ポインタ完全制覇」を読んでいく事にした。
すると、混乱する理由がわかった。
配列とポインタが混乱する理由 |
|
上図のような物を見ると、なんで配列とポインタが同等やねんと思う。
特に、配列とポインタは別物と書いている「C言語ポインタ完全制覇」に
上図の記述があるだけに、余計に混乱する。
|
「a[i]と*(a+i)が同等」という記述で、さすがに混乱してしまった。
なぜ、別物であるはずの配列とポインタが同等なのか。
そこで、本を読み進めると、意外な事が書いていた!
配列の宣言の int a[10]と、プログラム中で使われる変数 a[10]は別物 |
|
上図のような物を見ると、なんで同じ配列同士なのに別物やねんと思う。
こんな物を見ると、わけがわからなくなり、混乱に陥ってしまう (--;;
|
メガトン級の爆弾が投下された感じだ。
何せ、今まで「こうだ!」と思っていた事が、見事に破壊されたためだ。
しかも、驚いた事に、 a[10]はポインタだという。
配列だと思っていたのは、実はポインタだった |
|
上図を見ると、どう考えても、配列変数の10番目だと思う。
だが、配列の宣言 int a[10]; 以外で出てくる a[10]は
配列とは全く別物だという。しかもポインタだという。
配列だと思っていたのが、実は配列でなくポインタだというからビックリ!
|
なんで、ポインタなのか。本を見てみる事にした。
配列だと思っていたのは、実はポインタだった |
|
上図では、a[i]は、まさにポインタだという事を示している。
「C言語ポインタ完全制覇」で、a[i]は、*(a+i)の簡便法と書いている。
配列を宣言した後、配列は全て上図のように、ポインタに読み換えられる。
そのため、a[i]は、*(a+i)の簡便法という事が言える。
|
今まで配列だと思い込んでいた物がポインタだったという衝撃の事実を知る。
実は、ポインタを知らないと主張していた私だったが、知らず知らずの間に
ポインタを活用していたのだった。
衝撃の事実を知った後、配列とポインタの妙な関連性を示したプログラムで
ちょっとした書き換えができる事に気がつく。
配列とポインタの妙な関連性を活用したプログラム |
#include <stdio.h>
void func(int *p)
{
int i ;
for ( i = 0 ; i < 5 ; i++ )
{
printf("%d \n",p[i]); ← *(p+i)をp[i]に置き換えられる
}
}
int main(void)
{
int a[] = { 4 ,5 , 3 , 5 ,3 } ;
func(a) ;
return(0);
}
|
*(p+i)とp[i]が同じ事を意味しているので、上のように置き換えができる。
「C言語ポインタ完全制覇」では、ソースを読みやすくするために、
*(p+i)という表記はやめようと書いている。
最初、*(p+i)という表記はやめようという理由がわからなかったが、
ここにきて、ようやく理由がわかった。
|
配列とポインタの関連性。なかなか面白いと思った。
配列とポインタが同じ意味になる場合もある。
関数の仮引数で、仮引数が配列の場合、ポインタと同じ意味になるという。
仮引数って何?
仮引数が何か、わからないと、話が進まない (^^;;
そこで「仮引数」が何かを調べてみる事にした。
すると、引数、実引数という用語がセットになっている事を知った。
実引数って何? そもそも引数って何?
10年間、C言語に接していたが、ここまでC言語を知らない事を知り、
我ながら感心してしまう (^^)V
調べてみた結果、引数の意味は次の通りだった。
引数とは、関数の間で情報のやりとりをするための物だという。
引数の意味がわかった所で、仮引数、実引数は何なのか知りたくなる。
実引数とは、関数を呼び出す側が渡す物
仮引数とは、呼び出される側が受け取る物
言葉だけでは、わかりにくいので、具体例を挙げてみる事にした。
仮引数と実引数 |
#include <stdio.h>
void func(int a)
{
printf("a = %d \n",a);
}
int main(void)
{
int x = 4 ;
func(x);
return(0);
}
|
a は、呼び出される側が受け取る物なので、仮引数。
x は、関数を呼び出す側が渡す物なので、実引数。
|
これで、ようやく、仮引数と実引数という物がわかった。
さて、肝心の配列とポインタが同じ意味になる場合を見てみる。
仮引数の場合のみ、同じ意味になるという。
配列とポインタが同じ意味になる時 |
#include <stdio.h>
void func(int a[])
{
int i ;
for ( i = 0 ; i < 4 ; i++ )
{
printf("a[%d] = %d \n",i,a[i]);
}
}
int main(void)
{
int x[] = { 4 , 6, 3, 4 } ;
func(x);
return(0);
}
|
func関数の仮引数の宣言で、配列を宣言している。
だが、前述で触れたが、配列自体は、他の関数へ渡す事ができないため、
配列の先頭アドレスを渡している。
受け取る方も、先頭アドレスしか受け取っていない。
要素がいくつの配列という情報は、もらっていない。どんな配列か不明だ。
そのため、仮引数の場合、配列で宣言しても、自動的にポインタに
読みかえられるという。
|
この話を知って「ホンマ、奥が深いなぁ」と思った。
配列を他の関数へ渡す際の注意点として、他の関数へ渡されるのは
あくまでも配列の先頭アドレスだけであって、配列の要素数の情報は
全く渡されない。
だが、配列の要素数も算出して、相手の関数へ渡す事はできるので、
次のようなプログラムが書ける。
配列の要素数を他の関数へ渡すと便利(?) |
#include <stdio.h>
void func(int a[] , int n)
{
int i ;
for ( i = 0 ; i < n ; i++ )
{
printf("a[%d] = %d \n",i,a[i]);
}
}
int main(void)
{
int x[] = { 4 , 6, 3, 4 } ;
int y ;
y = sizeof(x) / sizeof(int) ;
func(x,y);
return(0);
}
|
ピンクの部分で、配列 x[] の要素数を求める。func関数へ要素数を渡す。
func関数では、配列 x[] の先頭アドレスを受け取るだけでなく、
受け取った要素数(n)があるので、先頭アドレスから何個まで
変数があるのかが、わかる。
|
ポインタを理解すれば、malloc関数も理解できる!
さて、本を読み進めると、malloc関数に出くわす。
malloc関数はメモリ確保だという事まではわかっていたが、
今までは、どういう物かを理解していなかった。
だが、今までの話を理解していれば、難しくはない。
malloc関数とは |
|
malloc関数は、メモリ確保に使われる関数だ。
ポインタ変数「*p」の宣言を行う場合は
int *p ;
さて、ここでmalloc関数を使う。
p = malloc(12);
この時、12バイトのメモリ領域が確保される。
確保された領域の先頭アドレスは自動的に割り当てられる。
アドレスとして使われるのは確保された領域の先頭のアドレスだ。
(上図だと、確保された領域の先頭のアドレスが「25」になっている)
その先頭のアドレスが、ポインタ変数に格納される。
上図のようにすれば、int型が3つ入れられるようになるため
p[0]、p[1]、p[2]は、自由に使う事ができる。
|
領域確保のためにmalloc関数を使う事がわかった。
では、領域を確保していない部分に値を入れると、どうなるのか。
解答は「メモリ領域違反のエラーが出る事がある」となる。
だが、答えだけ見ても性格柄、納得しないため、実際に、メモリ領域違反をする
プログラムを作成して、自分の目で確かめる事にした。
メモリ領域違反をするプログラム |
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *a , i ;
a = malloc(40);
for ( i = 0 ; i < 10000 ; i++ )
{
a[i] = i ;
printf("a[%d] = %d \n",i,a[i]);
}
return(0);
}
|
上のプログラムは、ポインタ変数「a」に、int型の値が10個格納できるように
a = malloc(40) ; で領域を確保した。
そして、a[i] = i ;で、確保した領域以外では、どこまで値を入れられるか
プログラムで試してみる事にした。
|
さて、私の自宅のLinuxでは、次の結果が出てきた。
実行結果 |
a[0] = 0
a[1] = 1
(途中省略)
a[1666] = 1666
a[1667] = 1667
a[1668] = 1668
a[1669] = 1669
セグメントエラー
|
無理矢理、確保していない領域に値を放り込んでいくと、
「セグメントエラー」が出てプログラムが止まった。
これは、無理矢理、値を入れようとしたメモリ領域は、他のアプリケーションが
使っているため、OSがプログラムに停止命令をかけたために出たエラーだ。
Windowsで「不正な処理が行われました」というエラーは、これと同じだ。
Windowsでお馴染みの「不正な処理」 |
|
Microsoft嫌いの私は「不正な処理」と見た瞬間、「おのれ、MSの不良品め!」と
叫ぶのだが、このエラーが出た時は、まだ、マシだと言える。
なぜなら、Windowsが「ここはダメ!」と言って、停止命令をかけるからだ。
最悪なのは、Windowsがストップをかけなかったため、Windowsが固まる事だ。
固まる原因は、Windowsが使っているメモリ領域まで、不正侵入してきたためだ。
これこそ、まさに、Windowsがメモリ管理が甘いと言われたりする所以だ。
詳しい事は「システム奮闘記:その46」をご覧下さい。
(OSが固まる原因。CPUの特権モードとバッファオーバーフロー)
そう考えると、UNIX系のソフトは、コアダンプを吐いて死ぬ事はあっても、
OSごと道連れになる事は滅多にない。(皆無に近いかもしれない)
ますます、反Microsoftに拍車がかかる私であった (^^)V
ところで、PHPや、BASICなどはメモリの事を考えずにプログラムを書けた。
だが、C言語は考慮しないと、大変な事になる場合がある。
C言語で、OSを書いたり、機械制御を行ったりできるプログラムなのだが
高度で複雑な事ができる分、それらを実現するための難しい事を
覚えないといけないのかなぁと思ったりする。
C言語の関数の略語の意味
さて、ポインタや配列の話が面白いように理解できて、ご機嫌そのものの私。
ふと、別のC言語の本を見てみる事にした。文字列の扱い方が書いていた。
以前から文字列のコピーや比較は、ややこしいと思っていた。
文字列のコピーにはstrcpy関数を使い、文字列の比較にはstrcmp関数を使う。
なぜ、ややこしいと思った理由なのだが、普通、変数の場合、
b = a ;のように、等号を使って、コピー(?)をする。
これだと非常にわかりやすいのだが、strcpy関数なんて、使われると、
意味のない呪文を覚える感じがして、ややこしい。
だが、この時、ふと気づいた。
strcpyって「string copyの略」ではないか!
日本語訳は「文字列の複製」だ。日本語訳なので「コピーでなく複製」 (^^)
strcmpって「string compareの略」ではないか!
日本語訳は「文字列の比較」
この瞬間、意味のないアルファベットの並びの関数だと思っていたのは、
実は、キチンと意味のある言葉の略だった事に気づいた。
これで、ややこしさが吹っ飛んだ (^^)
さて、話はstrcpy関数を使い方を見てみる。
strcpy関数を使う様子 |
#include <stdio.h>
#include <string.h>
int main(void)
{
char s1[10] ;
char s2[10] = "copy" ;
strcpy(s1,s2);
printf("%s \n",s1);
return(0);
}
|
char型の文字列「s2」を、同じ型の文字列「s1」へコピーするプログラム。
青い部分では、文字列「s1」と文字列「s2」が存在する先頭アドレスが
関数に送られる。
そして、「s2」から「s1」にコピーされた結果を出力する形になっている。
|
上の内容を図にしてみると
strcpy関数を使う様子 |
|
ちょっとしたメモリ領域違反の恐ろしさ
ところで、strcpy関数を扱う場合は、注意をする必要がある。
実は、strcpy関数は、メモリ領域違反を起こす可能性のある関数だからだ。
なぜメモリ領域違反を起こすのかだが、次のような事があるからだ。
strcpy関数でメモリ領域違反を起こす話 |
|
そこで、「百聞は一見にしかず」なので、プログラムを作成してみた。
メモリ領域違反をするプログラム |
#include <stdio.h>
#include <string.h>
int main(void)
{
char s1[3] ;
char s2[] = "I fall in love with Mayumi Ono. She is very pretty!!" ;
strcpy(s1,s2);
printf("%s \n",s1);
return(0);
}
|
suga@jitaku[~/pro]% gcc test5.c -o test5
suga@jitaku[~/pro]% ./test5
I fall in love with Mayumi Ono. She is very pretty!!
セグメントエラー
|
私の大好きなアコムのお姉さん(小野真弓)に、愛を捧げようと思ったら、
セグメントエラーが出てしまった (^^;;
でも、OSがメモリ領域違反に気がついて「おい待て、よそ様のメモリを使うな!」と
言ってくれるので、大きなトラブルになる事はない(と思う)
だが、本当に恐いのは「ちょっとしたメモリ領域違反」だ。
そこで、ちょっとしたメモリ領域違反のプログラムを書いてみた。
メモリ領域違反をするプログラム |
#include <stdio.h>
#include <string.h>
int main(void)
{
char s1[3] ;
char s2[] = "I am Japanese" ;
strcpy(s1,s2);
printf("%s \n",s1);
return(0);
}
|
コンパイルでは警告はなし。そして、実行をしてみた。
実行結果 |
suga@jitaku[~/pro]% gcc test6.c -o test6
suga@jitaku[~/pro]% ./test6
I am Japanese
|
エラーが出る事なく実行できた。
一見、エラーが出ていないので安心しそうなのだが、これが危険なのだ!
C言語ができた時、「プログラマーは全知全能」という前提が作られたため、
メモリ領域違反をコンパイル時に見つける術がないという。
だが、まだ、プログラムを走らせた時に、OS側で異変に気づき、
エラーが出れば、幸運だと言える。
ちょっとした領域違反の場合は、OS側でも異変に気づかないため、
エラーは出てこない。
もし、大事なプログラムを作成した場合、領域違反があるにも関わらず、
テスト時にはエラーが出なかったため、安心して本番で使って、エラーが出たら
間違いなくパニックに陥ると思う。
まして、プログラムが暴走したり、固まったりして、大事なデータを失うと
それこそ一大事なのだ!
そこで、メモリ領域違反における注意点を2つ挙げてみました。
メモリ領域違反における注意点 |
(1) |
コンパイル時には、警告が出ない! |
(2) |
ちょっとしたメモリ領域違反なら
実行してもエラーが出ない事がある! |
そのため「ちょっとしたメモリ領域違反」の恐さがある。
メモリ領域違反を防ぐには、プログラマーが注意を払う必要がある。
「ちょっとしたメモリ領域違反」の恐さ (2009/3/19) |
(1) |
バッファオーバーフロー攻撃の引き金になります。
詳しくは「システム奮闘記:その44」の
バッファオーバーフロー攻撃の手口をご覧ください。
|
(2) |
バッファオーバーフローの原理で、OSが固まったり
システムコールの脆弱性をついて管理者権限が奪われます。
詳しくは「システム奮闘記:その46」の
OSが固まる原因。CPUの特権モードの話をご覧ください。
|
そこで、メモリ領域違反を防ぐための話をします。
まずは、strncpy関数の有効活用があります。
strcpy関数だと、平気で領域違反をする事は説明しました。
strncpy関数を使うと、必要な文字数だけコピーをする事ができます。
strncpy関数 |
|
この場合、文字列「S2」がn文字よりも文字数が少ない場合は
残りのn文字目まで、ヌル文字で補われます。
|
そこで、strncpy関数を使ったプログラムを書いてみる事にした。
メモリ領域違反を防いだプログラム |
#include <stdio.h>
#include <string.h>
int main(void)
{
char s1[20] ;
char s2[] = "I fall in love with Mayumi Ono. She is very pretty!!" ;
strncpy(s1,s2,20);
s1[19] = '\0' ;
printf("%s \n",s1);
return(0);
}
|
s1[19] = '\0' ;をいれたのは、20文字目(最終部分)にヌル文字を入れないと、
表示の際、後ろに文字化けした尻尾がついてくるためです (^^;;
そのため、20文字目はヌル文字に置き換えました。
ヌル文字については、後述しています。
|
そして、実行してみる事にした。
実行結果 |
suga@jitaku[~/pro]% gcc test7.c -o test7
suga@jitaku[~/pro]% ./test7
I fall in love with
|
エラーが出る事なく、20文字のコピーが実行できた。
上手(?)にメモリ領域違反を防いだプログラムが完成した!!
だが、アコムのお姉さんへの愛の告白文は、途中でちぎれてしまった (TT)
メモリ領域違反を防ぐプログラム
さて、strncpy関数を使うと、メモリ領域違反が防げる事がわかった。
ところで、さっきのプログラムで、ヌル文字を入れた部分がありますが、
そのヌル文字について触れたいと思います。
最初、strncpy関数使ったプログラムとして、以下のソースを書いた。
strncpy関数を使ってメモリ領域違反を防いだプログラム |
#include <stdio.h>
#include <string.h>
int main(void)
{
char s1[20] ;
char s2[] = "I fall in love with Mayumi Ono. She is very pretty!!" ;
strncpy(s1,s2,20);
printf("%s \n",s1);
return(0);
}
|
だが、実行すると、
なんでやねん (TT)
という結果が出た。
実行結果 |
suga@jitaku[~/pro]% gcc test7.c -o test7
suga@jitaku[~/pro]% ./test7
I fall in love with H@懾 @心@
|
最初、文字化け「H@懾 @心@」が出てきた時、プログラムソースに
おかしい部分があるのではと思った。
しかし、どう考えても、おかしい部分がわからず、途方に暮れかけた。
|
なぜ、文字化けした物までが付いてくるのか、わからないため、
ここは、常套手段として「事務員なので、わかりませーん」で逃げようと考えた。
だが、本を見ていると、文字列の終わりを知らせるのに「ヌル文字」が必要
というのを記述を見て、文字列の最後の部分に、ヌル文字を入れたら、
文字化けが消えた。
C言語の本で「ヌル文字」を調べてみる事にした。
すると、次の事が書かれていた。
ヌル文字とは |
配列の最後のマス目に、自動的に付加される文字で
コンパイラが文字列の終わりとして認識するためのもの
|
配列の初期化などを行った際、自動的にヌル文字が付加されるという。
ヌル文字は自動的に付加される |
|
上のように配列の初期化をした場合、特に添字の数は決めていないので、
コンパイラ(※)は自動的に文字列の後ろにヌル文字を付けて、
配列が「文字数+ヌル文字」の6文字の並びとして認識される。
|
こんな事、今まで知らなかった!!
そうなのです。
C言語と出会って10年間。文字列の後ろにヌル文字が付いてくる話や
コンパイラがヌル文字を見て、文字列の終わりだと認識する話(※)は、
全く知らなかった。新しい発見に思わず喜んでしまう (^^)V
前述していますY君からの指摘 (2005/6/5 追加) |
まぢめに書くと長くなるんだけど…
『コンパイラは「Hello」の後ろを辿っていきヌル文字がある所まで、
一つの文字列と認識』
は,間違い.×コンパイラ,○ランタイム.
……多分,きちんと説明しようとすると,「コンパイラとは…」
「ランタイムとは…」で,散々長いことメールのやりとりをする羽目に
なるんでしょうねぇ(x_x)
|
Y君の指摘を見て「本め、うそ書きやがって!」と思った。
次に、以下のように、配列の初期化を行えば、どうなるのか考えてみた。
ヌル文字は付加されない例 |
|
この場合、配列は5つの文字列が並ぶだけとなっているために、
「Hello」の5文字が配列に入ると満杯で、ヌル文字が入れなくなる。
まさか、領域違反を承知で、6文字目にヌル文字を入れるとは考えにくい。
|
どうなるのか、プログラムを書いて実証してみる事にした。
配列の初期化で a[5]="Hello" とした場合 |
#include <stdio.h>
int main(void)
{
char a[5] = "Hello" ;
printf("%s \n",a);
return(0);
}
|
(実行結果) |
suga@jitaku[~/pro]% ./test8
Hello@懾
|
ピンクの部分が、「Hello」の後ろについてきた文字化けの部分
|
思った通り、後ろの文字化けの集団(?)が付いてきた。
ところで、何故、文字化けの集団が付いてくるのか、考えてみる事にした。
そして、一つの仮説を立てる事にした。
仮説 |
|
配列の宣言と初期化( char a[5]="Hello" )を行う。
だが、この場合、文字列「Hello」の後ろに、ヌル文字を付ける事ができない。
メモリ領域違反になるからだ。
そのため、ランタイムは「Hello」の後ろを辿っていき
ヌル文字がある所まで、一つの文字列と認識してしまう。
そのため、ヌル文字がある場所までの間に、何か文字が入っていると
それが「文字化け」のような文字として表示されてしまうのではと考えた。
|
さて、論より証拠。
そこで、実際に「Hello」の後ろを辿っていき、ヌル文字がある場所まで
一つの文字列と認識しているのか、次のプログラムで確かめる事にした。
配列の初期化で a[5]="Hello" とした場合
ヌル文字がある場所まで一つの文字列と認識するのかの実験プログラム |
#include <stdio.h>
int main(void)
{
char a[5] = "Hello" ;
int i ;
for ( i = 0 ; i < 100 ; i++)
{
if ( a[i] == '\0' ) break ;
}
printf("ヌル文字は %d 文字目にある。a = [%s] \n",i+1,a);
return(0);
}
|
(実行結果) |
suga@jitaku[~/pro]% ./test9
ヌル文字は 17 文字目にある。a = [Hello@懾]
|
ヌル文字は17文字目にあるという。
どうみても17文字目までの間「@懾」以外の文字があるとは思えない。
しかし、バイナリー文字の場合だと、人間の見える形で表現できずに、
文字化けした11文字分が「@懾」で表現されている可能性は考えられる。
|
さて、「@懾」と表示されているため、本当に、16文字の文字列として
ランタイムが認識してしまったのか不思議に思ってしまう。
本当に、16文字の文字列なのか確かめるため、strlen関数が活躍する。
strlen関数で文字列の長さをはかってみる |
#include <stdio.h>
int main(void)
{
char a[5] = "Hello" ;
int i , p ;
for ( i = 0 ; i < 100 ; i++)
{
if ( a[i] == '\0' ) break ;
}
p = strlen(a);
printf("ヌル文字は %d 文字目にある。a = [%s] \n",i+1,a);
printf("strlenで文字列の長さをはかると %d となる \n",p);
return(0);
}
|
(実行結果) |
suga@jitaku[~/pro]% ./test10
ヌル文字は 17 文字目にある。a = [Hello@懾]
strlenで文字列の長さをはかると 16 となる
|
これで、私の仮説が立証できたと思わず、喜んでしまう (^^)V
配列の初期化を行う際、配列の要素数に注意しないといけない事がわかった。
結論としては、次の事が言える。
配列の要素数 |
|
配列の初期化を行う際、配列の要素数(領域)まで決める場合は、
最低でも、配列に入る文字数に、ヌル文字(1文字分)を加えないと
ダメだという事が、今回の実験でわかった。
|
なかなか奥が深いと感心してしまう。
入門書に出てくるscanf関数の問題点
メモリ領域違反といえば、実は、scanf関数も、メモリ領域違反の原因になる場合がある。
数値の代入に使う場合は、問題ないと思われるが、文字列の代入の際に
問題を起こす。
scanf関数の問題点 |
|
文字列を変数に代入するのだが、文字列の文字数を制御する機能がない。
そのため、変数の領域が4文字分しかないのに、10文字代入すれば、
完全にメモリ領域違反になる。
|
そこで、また、メモリ領域違反のプログラムを作成した。
メモリ領域違反のプログラム |
#include <stdio.h>
int main(void)
{
char a[5] ;
scanf("%s",a);
printf("%s \n",a);
return(0);
}
|
(実行結果) |
suga@jitaku[~/pro]% ./test11
abcdefghijklmnopqrstuvwxyz1234567890
abcdefghijklmnopqrstuvwxyz1234567890
セグメントエラー
|
やはりエラーが出た。
C言語の入門書でお馴染みの関数が、実は、領域違反の危険のある関数なので
「取扱いには注意が必要」と明記しても良いのではと思ったりするが、
入門レベルでは領域の話は出てこない。
そのため入門の段階で「取扱いの注意」を説明するのは難しいと思う・・・。
領域違反という観点から、色々な事を知る事ができ、良かったと思う (^^)
(2005/11/6の追加)
実は、ちょっとしたメモリ領域違反はバッファオーバーフローという
重大なセキュリティーホールを生む原因になる事を知りました。
それにつきましては「システム奮闘記:その44」をご覧ください。
構造体入門
さて、話はポインタに戻します。いよいよ構造体に迫る。
構造体が何なのかを知るため、構造体の定義を見てみる。
構造体「TOCHI」の定義 |
struct TOCHI {
char chimei[60] ;
int banchi ;
};
|
構造体は、変数の集合体という感じだ。
構造体を絵で例えると、次のようになる。
構造体を絵で例えると |
|
変数の場合、1つの区画の中に、家があるという感じだ。
構造体は、1つの区画の中に、マンションや一戸建てがあったりする感じだ。
どんな建物の組み合わせにするかは、プログラマーが自由に決める事ができる。
どんな組み合わせにするのか決めた事を、プログラムに記述するのを
構造体の「定義」という。
|
さて、構造体を使う際は、構造体の変数を宣言する必要がある。
まずは宣言の方法を見てみる。
構造体「TOCHI」の変数「a」の宣言 |
struct TOCHI a ;
|
次に、構造体の宣言を図にしてみた。
構造体の変数の宣言 |
|
構造体の型を「定義」されても、変数を何にするかを決めないと
折角の構造体が使えない。そこで、変数の宣言が必要になる。
変数が宣言されると、変数の位置(アドレス)は自動的に割り当てられる。
上図だと「21」になる。
構造体の変数を何にしたかを、プログラムに記述する事を
構造体の「宣言」という。
|
さて、構造体の変数を宣言した際、自動的に変数が置かれる番地(アドレス)が
自動的に割り当てられる。
上図の場合だと「21」になっている。
ここで、構造体の変数の内容を、他の関数へ渡したい場合、
どうすれば良いかが問題になる。
配列と同様、アドレスを送りたい先の関数へ送れば良いのだ。
構造体のような、マンション、一戸建て住宅や学校、田畑などの街の建物を
全て明記して他の関数へ送る事は賢くない。
そこで、構造体の変数がある場所(アドレス)さえ送れば、
送った先の関数でも、簡単に見たい構造体の中身を見る事ができる。
さて、実際に、構造体を他の関数へ送ってみるプログラムを書いてみる事にした。
構造体を他の関数へ渡すプログラム |
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct TOCHI {
char chimei[60] ;
int banchi ;
};
void func(struct TOCHI *b)
{
printf("name = %s \n",b->chimei);
printf("number = %d \n",b->banchi);
}
int main(void)
{
struct TOCHI a ;
strcpy(a.chimei,"kobe");
a.banchi = 32 ;
func(&a);
return(0);
}
|
suga@jitaku[~/pro]% gcc struct.c -o struct
suga@jitaku[~/pro]% ./struct
name = kobe
number = 32
|
ところで構造体の説明の部分で、「宣言」と「定義」が出てきた。
「宣言」と「定義」の違い。よく混乱しやすい物だと言われる。
そこで、私なりに2つの違いを考えてみる事にした。
私の場合、C言語に詳しいわけでないため、C言語を勉強した経験からの
視点では、2つの言葉の違いを書く事ができない。
そこで、「宣言」と「定義」を言葉の意味から、2つの違いを考えてみた。
「宣言」と「定義」の違いについて |
C言語では、「宣言」と「定義」の使い分けが難しく混乱しやすいと言われる。
この2つの違いは何なのかを考える事にした。
国語辞典で「宣言」を調べると「個人や団体が、その意見や方針を
外部に対して広く表明すること。また、その言葉」という意味だ。
「定義」を調べると「ある概念の内容やある言葉の意味を他の概念や言葉と
区別できるように明確に限定すること。また、その限定」という意味だ。
ここで「なるほど」と思う。
構造体の型を決めた時は、「こうである!」と明確に限定しているため
「定義」という言葉が当てはまる。
そして、構造体の変数を決める時は、「変数はaだぞ」とアナウンスするため
「宣言」という言葉が当てはまる。
C言語は英語文化圏で生まれたので、念のため、英英辞典で2つの違いを
調べてみる事にした。英語では「declare」と「define」となる。
「declare」は「to announce something formally or officially 」や
「to make something known clearly」という意味で、
日本語の「宣言」と同じ意味になる。
「define」は「to state exactly the meaning of a word or phrase」や
「to state or to describe exactly the nature or extent of something」や
「to show a line , a shape , a feature」という意味がある。
これも日本語の「定義」の意味と同じになる。
英語に限らず、外国語と日本語の訳を照らし合わせた場合、1対1の訳にならず、
必ず、意味にズレが生じる。
「宣言」と「declare」、「定義」と「define」は、1対1の訳になるので
もしかしたら、元々、日本語には、「宣言」や「定義」を表す言葉がなく
明治以降に欧米の概念が輸入され、訳された言葉の可能性があるかもしれない。
(私は国語学者でも英文学者でないので、詳しい事はわかりませんが)
C言語のような英語文化圏で生まれた物の用語を考える際は、
日本語に訳された言葉の意味を考えても、訳にズレが生じている場合もあり
日本語で考えると、混乱する可能性があるので、英語の意味で
考えていく必要があったりもする。
|
そう考えると、int a ;が「宣言」なのも納得できる。
既に、「int型の箱」がどんな物か定義されているため、その箱の名称に「a」を
名付ける事をアナウンスするだけなので、「宣言」という言葉に当てはまる。
さて、構造体にも配列がある。「構造体の配列」と呼ばれる物だ。
構造体の配列を宣言する時は、以下のように行う。
構造体「TOCHI」の配列 a[3] の宣言 |
struct TOCHI a[3] ;
|
これを図に表すと以下の通りだ。
構造体の変数の宣言 |
|
考え方は、変数の配列と同じで、構造体の並びを意味する。
配列が宣言されると、変数の位置(アドレス)は自動的に割り当てられる。
上図だと配列の先頭は「21」になる。
|
構造体の配列は、いくつかの項目があるデータを扱うのに便利だ。
例えば、社員の住所録や、顧客情報、仕入先情報などが挙げられる。
一種のデータベースができ、それの取り扱いができるのだ!!
「構造体は難しい」と思って、構えていたのだが、意外と簡単に理解できたので
ちょっと拍子抜けした感じだったが、理解できたので良かった (^^)V
事務員でもわかるポインタ入門
最後に、ポインタを、誰でもわかるような説明がないかと考える事にした。
私と同じように、ドツボにハマり、10年も前に進めないとなれば、
最大100年しか生きれない短い人生にとって、大きな無駄になってしまうからだ。
そこで「事務員でもわかるポインタ入門」を考えてみた。
ふと思いついたのは、変数とポインタ変数を扱う上での考え方の違いに
注目してみれば、違いが見えてくると思った。
変数とポインタ変数の考え方の違い |
|
変数の場合、記録用紙(メモリ)に、置かれている物を記録している。
部屋の中に何が置かれているかは、自分(宣言した関数)しか、わからない。
だが、ポインタ変数の場合は、物が置いている場所を記録用紙(メモリ)に
記録している。そのため、自分(宣言した関数)以外の、他の人に
「冷蔵庫にある物を1個増やして」と指示を出すと、冷蔵庫の中身を確かめ
それが「りんご」である事がわかるので、りんごを1個追加する事ができる。
|
以上の事を踏まえて、関数Aと関数Bとのやりとりを見て行く事にします。
変数の考え方 |
|
関数Aが変数を宣言。
その中身を「りんご、みかん、醤油入れ、ゲーム機」とする。
上図のように、変数を関数Bへ渡す際、「りんご、みかんを使ってね」と言って
変数の中身(りんご、みかん)を伝える。
だが、関数Bは、りんご、みかんが置かれている場所を知らない。
となると、勝手に関数Aの部屋に入り込み、りんご、みかんを触ったら
怒られるだけなので、関数Bが「りんご、みかん」を自分で用意する。
|
|
そして、関数Bが用意した物で加工する。例えば、上図のように、
「みかんを食べた」とすれば、自分が用意した、みかんを食べただけなので
関数Aが持っているみかんの数量などには影響はない。
そして、関数Bが用意したりんごで、りんごジュースを作って、
関数Aに、りんごの加工の成果物(ジュース)を渡したい場合は、
returnコマンドを使って渡す必要がある。なぜなら、関数Aが持っている
りんごを加工したわけでないので、関数A自身、ジュースを持っていないからだ。
|
ポインタ変数の考え方 |
|
関数Aがポインタ変数を宣言。
その中身を「冷蔵庫、こたつ、食卓、リビング」とする。
もちろん、関数Aは、冷蔵庫の中には、りんご。こたつの上には、みかん。
食卓の上には醤油入れ。リビングにはゲーム機がある事も記録用紙に記入している。
関数Bに「冷蔵庫、こたつある物を触っても良いよ」と伝える。
関数Aから部屋の冷蔵庫とこたつにある物を触って良いという許可があるので
関数Aの部屋に入って、冷蔵庫とこたつを見て、りんご、みかんといのを知る。
|
|
関数Bは、関数Aの冷蔵庫から、りんごを取り出しジュースを作ったり、
こたつにある、みかんを取り、みかんを食べたりする。
関数Bが直接、関数Aの物を触っている形になる。
そして、関数Bが行動を終わる時(関数Bの終了時)、関数Aの部屋を
触っているため、関数Aが持っている記録用紙の中身が変わっている。
ポインタ変数には、相手に場所を伝えて、直接、物を触れるようにする
役目がある。そのため、わざわざ成果物を returnコマンドを使って
渡す必要がなくなる。
|
こんな感じで「事務員でもわかるポインタ入門」と書いてみましたが、
かえって、複雑になったかなぁと思ったりします (^^;;
まとめ
10年がかりでポインタを理解しました。
だが、C言語の最初の難関を突破できたにすぎず、C言語マスターを考えると、
まだまだ気が遠くなりそうです。
でも、ご覧になった読者の方が、私のつまずいた箇所や誤解していた箇所を見て
新人研修や後輩の指導などに、お役に立てれば幸いです (^^)
奮闘記の中でも紹介しました「C言語ポインタ完全制覇」ですが、
快刀乱麻を断つが如く、鮮やかにポインタの説明をしていて、
わかりやすい本なのですが、私が読んだ感想としては、ポインタを理解した人が
「なるほど、これは、こういう事だったのか!」と感激する本で、
私のように、ポインタの壁が越えられず、困り果てた人には難しい本だと思います。
ポインタの壁が越えられず、困り果てた方は、奮闘記でも紹介しました
小出俊夫さんの「KID's World」というサイトをご覧になられるのをお薦めします。
http://homepage1.nifty.com/toshio-k/
その上で、「C言語ポインタ完全制覇」を読まれると、
ポインタの学習には、非常に効果的だと思います。
次章:「ヤマハルーターRTX1000の設定入門」を読む
前章:「通信費用大幅削減。インターネットVPN導入」を読む
目次:Linux、オープンソースで「システム奮闘記」に戻る