Hatena::ブログ(Diary)

偏見プログラマの語り!

2012-08-23

unkode-mania でウンコ 300 個ゲットした

| 10:05

 食事はお済みでしょうか?ウンコの話を書きます。

 先日 GIGAZINE でも話題になった『ウンコード・マニア』というサイトがあります。このサイト、要はウンコみたいに酷いプログラムを投稿してシェアしようぜ、という旨のサイトです。投稿されたコードは誰でも見る事ができ、「これはウンコだなぁ」と思ったらウンコマークをつけられます。つまりウンコマークが多ければ多いほど臭いコードだというわけです。

 ウンコードといえば少し前に @lalha2 さんがこういうことを呟いていましたね。

この呟きではウンコードというネガティブな表現を「ひよコード」というプリティな表現に転化させていて素晴らしいと思いました。とはいえ実際は、ひよこはひよこ、ウンコはウンコだなぁというのが僕の開発経験による率直な所感です。

 さて、このサイトは結構面白い企画だなぁと思っていて、僕もちょぼちょぼと投稿していたんですが、先日めでたく獲得ウンコ数 300 を超えたので感じた事とかを書いておこうと思います。あ、ちなみに僕が投稿したのは以下 11 件。

タイトル言語ウンコ数コメント数
お前は何も分かっちゃいないC++496
名前が似てると気付かないよねRuby183
無限ループ楽しいC2011
n × 4 バイトC377
short から int への変換C3010
NULL と 8 の何故C5210
インド人を右にJava156
switch-caseは現場の臭いがするC4416
WinMainの1行目からウンコC53
ダイナミック型変換C++150
スベテカンスウJava4513

1. ウンコにも質がある

 現在サイトには多数のウンコードが投稿されていますが、その中にはウンコマークが山ほどついているものもあれば全然ついていないものもあります。良質のウンコードは誰が見ても明らかにダメダメなコードですが、そうでないものは何処がどの様にウンコなのかが分かりにくかったり、「それはウンコではないよ」と指摘が入っていたりと、色々理由が見つかります。一部では投稿者のレベルが高すぎて見ている人がついていけないケースも見受けられますが、おおむねウンコードの質というのはウンコがウンコたる様子をいかに伝えられているかという点に強く依存して決まるように感じます。

2. ウンコを見つめていると美しいコードを書く事の難しさが分かる

 ウンコードを眺めていると、「これがウンコである指摘は出来るけど、じゃあコレを完璧に正しく美しいコードに修正するとしたら、どう書くのが良いだろう?」と悩んだりします。まぁもちろん "それは状況によって変動するから完璧なんて無い!" 的な発想はあるとしても、ダメなコードを知る事は美しいコードを書くことの難しさを知る事に直結するように思います。

3. ウンコの排出されやすさが、プログラミング言語の質を物語っている

 @ichiro_satoh さんが↓こういうことを呟いていました。

僕はウンコードを見てバカにして笑うことと工夫を考えることは別の事だと思っているので、バカにして笑ったところで技術屋としての資質を疑ったりはしませんが、ウンコードにならないようにする取り組みが実際の開発現場において有益であることは間違いないでしょう。「完璧なコード」とまでは言わなくとも、「ウンコードではないことそれ自体」は現実的に獲得可能な価値であるということです。ウンコードの投稿数を言語別で見てみると、ウンコードを量産してしまいがちな言語とそうでない言語があるのが分かります。

言語投稿数
Java44
C38
JavaScript19
PHP18
C++17
C#12
VB.net10
Python3
Ruby1

 もちろんユーザーの多い言語ほどウンコードの投稿数が多くなる傾向が強いのですが、Pyhon と Ruby の投稿数の少なさには注目すべきだと思います。Ruby なんかは日本でユーザーも多いし、もっと投稿があっても良さそうな気がします。しかし僕も Ruby のウンコードを投稿しようとしてようやく気付いたのですが、Ruby って比較的ウンコードを作りにくい言語なんですね。Twitter勉強会でよく「Ruby は書くのが楽しい」とか言われてるのを目にしますが、それってつまり Rubyist が能天気なのではなく、Ruby がダメなコードになりにくい言語仕様に仕上がっているという事なんだろうと思います。

4. 「ウンコード・マニア」というサイト自体のウンコっぷり

 僕はこのサイトに対しておおむねポジティブに捉えているのですが、サイトの主たる機能がウンコを晒すことである以上このサイト自体のウンコっぷりを(要望受付を通さずに)晒すことはアリだと思っているので、いくつかウンコっぷりを書こうと思います。

i.コードの微修正でウンコマークの位置がずれる。

 このサイトでは投稿に対してウンコマークがついた後でも、コードを修正することができます。コレ自体は良いのですが、ウンコマークの位置を恐らく行数で管理しているのでしょう、ウンコマークの位置がずれます。コードが変わったのだから多少の食い違いはしょうがないのですが、例えば空行を削除しただけでウンコマークの位置がずれるのは嬉しく無いです。コードの変更を許可するなら、diff を使ってウンコマークの位置を補正すべきでしょう。プログラムソースを扱うサイトなんだし。

ii.コードの最大文字数が 1024。

 どう考えても短いです。ウンコードとは長くなりがちなものだというのに。

iii.ウンコマークが付けば付くほどソースが読みづらくなる。

 人気な投稿ほど可読性が下がるとかアリエナイ。投稿されたウンコードの質が下がります。恐らく、それを考慮して「ウンコマーク非表示ボタン」が用意されているのでしょうが、それだと企画の面白さそのものが損なわれます。例えば 10 個ウンコマークが溜まったら大きいウンコになる等の演出をするべきでしょう。

iiii.(8/23 11:05 追記)「まいた」

 f:id:kura-replace:20120823110350p:image


5. サイト利用者がウンコにならないために

 すごく大事で当たり前のことなのですが、実際に仕事で書かれたコードを投稿するのは避けましょう。業務で書いた/見つけたコードをこういう場所に晒すとか、コード以前に人格がウンコです。サイトの about ページにも創作に限定しろと注意書きがありますし、投稿時にも著作権を侵害しない旨のチェックボックスが置かれています。

2012-08-12

「こんなプログラマはアジャイル出来ますって言ったらアカンやろ」http://goo.gl/twvpi について。

| 20:31

アジャイル」「テスト駆動開発」「リファクタリング」をマシンガンのように賞賛していて、最近炎上している話題になっているこの記事ですが。

『こんなプログラマはアジャイル出来ますって言ったらアカンやろ』メソッド屋の日記

  • 「1.〜4.」が二つある。
  • これが大切、これが重要、という主張が山ほど書いてあって結局どれが大切で重要なのか分からない。
  • 想定読者が場所によって同業者、顧客、開発者の卵、とバシバシ切り替わっていてメッセージ性が薄れている。

【結論】記事自体をリファクタリングすべき。

2012-08-09

一流のプログラマは例外なく我慢強い

| 00:05

 プログラマはかくあるべき、という意見は様々ありますが、そのうちの多くは「プログラミングという作業をいかに効率的に遂行するか」という指標に沿っている気がします。例えば「知識の幅が広い」とか「設計を考えるのが速い」とか「自動化を徹底する意思が強い」とか「問題発見能力が長けている」とか「高いコミュニケーション能力でチームの意思決定を促進できる」とか、そういった類のものです。一般にプログラマの仕事はプログラムを作ることですので、それを速く安全に進めるプログラマほど優秀である、ということに異論は無いかと思います。世の中には Excel 漬けのプログラマだっているそうですが、基本的にプログラマと名がついている職はプログラムを作るという作業がそこにあるからこそ発生しているとみなすのが妥当でしょう。

 そうした "プログラマはかくあるべき" という外面的な観点から、"俺らプログラマはこういうことがしたい" という内面的な観点へ目を移してみると、ずいぶん重複した要求があることに気付きます。例えば「もっと色んなことを知りたい」とか「○○と××を組み合わせて設計すると面白そうだ」とか「△△は面倒だから自動化したい」とか「□□の辺りに問題がありそうだから議論したい」とか「もっとチームがうまく回ってゆくルールを提案したい」とか、そういう類のものです。こうした、プログラマの内なる欲求の本懐は、企業や顧客によらず万国共通であるように感じます。例えば "顧客の笑顔が見たい" というたった一つのピュアな親切心だけをモチベーションに日夜努力を続けるプログラマは恐らく少数派ですよね。

 まぁとにかく、プログラマと非プログラマがだいたい同じ方向を向いているからこそ、今こんなに多くのプログラマが存在しているんだろうと思います。

 さて、プログラマ達がだいたい同じ方向を向いているのであれば、その能力差は何らかのスカラ値で比較できそうな気がします。いまチームの目前に「○○な機能が欲しい」という欲求があったとします。広い知識を持ったプログラマ A は「それは××と△△を組み合わせればできる」と即座に提案できますが、そうでないプログラマ B は「ちょうどよいものがないか調べてみます」と言うことしかできません。仮にプログラマ B が 3 時間で調査を完了させて、プログラマ A とまったく同じ結論にたどりついたとしたなら、その能力差は少なくとも 3 時間であると言えます(実際はもっとだろうけど)。他にも、愚かな設計のせいで後々の拡張やメンテに手間がかかるとか、同じような手動オペレーションを何度もやるせいでいちいち遅いとか、連絡すらまともにできないせいでコミュニケーションコストが増大するとか... まぁとにかくイロイロな理由によってどんどん差が開いていきます。「プログラマの能力というのは(略)軽く5倍から10倍は違う」とか「コーディングの生産性で10倍、コードレビューの速度では6倍もの能力差がある」などと言われるように、確かにそこには明確な能力差があるのです。この差こそが、優秀なプログラマを優秀たらしめているのでしょう。

 こうやって blog の記事を読んでいるときぐらいは忘れていたいですが、開発現場はときに、重大な設計ミスやクリティカルバグに襲われます。これはスコールみたいなもので(前兆が有ったり無かったりの差はあれど)、大抵は事態の収束を目的としてリソースが投入されます。しかもこういうトラブルに限って、ウンザリするぐらい根が深かったりするんです。もちろん、事態の収束のために営業の剛腕の御力に頼ったり、涼しい顔でクレバーに仕様変更をキメる場合もあるかと思いますが、それらでは代替できない場面というのも確実にあるのです。対処にあたるプログラマは少なくとも優秀である必要があります。しかも、事態を収束させるためにはぐらつく積み木の城を崩さずに改築するかもしくは海底に潜ってそっと小石を置いてくるかそれとも車を運転しながら針の穴に糸を通すかという感じの非常にナーバスなアタックが必要とされます。しかしそれは、"俺らプログラマはこういうことがしたい" には含まれていないのです。つまり我慢強いプログラマでないと切り抜けられないピンチなのです。僕の少ない経験からいうと、こういうとき、一流のプログラマは本当に我慢強く対処にあたり、やがて事態を収束させます。一流のプログラマ、皆さんの身近にもたくさんいると思います。

 というかむしろ、そういう態度こそが「一流とみなされる理由」だし、「非プログラマによるプログラマ評価の核」でもあるように思います。なぜなら、目前のピンチは三流のプログラマでは手に負えないからです。ただ能力が有るか無いか。true or false の領域です。上で、プログラマの能力差は 3 時間だとか 10 倍だとか書きましたが、そういう"時間を買いさえすればカバーできちゃいそうな差"とは質が違うような気がするんですよね。本稿では、「一流の定義は...」とかそういう言葉遊びは面白くないのでやらないですし、別に「○○なプログラマこそが優秀である!」とか「××こそがプログラマのあるべき姿である!」とか声高に叫ぶつもりは無いです。ただ言いたかったのは「デスマでヒィヒィ言ってるプログラマの人たち、辛いだろうけどちゃんと信頼を勝ち取ってる(評価されてる)と思うよ!」ってこと。


 ... というわけで「偉そうなこと書いときながら、僕はぜんぜん優秀でも我慢強くも無いなー」とか反省してるうちに何だか満足したので、今日もご飯がウマいです!

2012-06-16

C 言語にポインタがある理由は省メモリ化・高速化・開発作業の省力化です

| 23:17

 前回の記事『プログラム初心者にC言語のポインタを不本意ながら教える羽目になったなら、こう教えると良いよ』ポインタの教え方を書きました。ソレに対して「そもそもどうしてポインタっていう仕組みがあるの?」という質問をもらったので、つらつらと書こうと思います。本稿は「ポインタがある理由の教え方」ではなく「ポインタがある理由」です。分かっている人には相当に退屈な文章ですのでそういう人は読まずにお帰りください。

 で、えーと、結論だけ先に言うと省メモリ化のため、次に速度アップのため、そして生産性アップのためです。


1. メモリは有限である。

 マシンに搭載されているメモリには限りがあります。メモリ空間は広大ですが、無限ではないのです。

f:id:kura-replace:20120616223014p:image

 好き放題にどんどんメモリを使ってデータを格納するわけにはいかないというわけです。しかしプログラムは計算のためにメモリ空間を占有します。仮に↓こんな感じに、わずかな有限メモリにどうしても計算に必要なデータ a が格納されている場合を考えます。

f:id:kura-replace:20120616223015p:image

 これを別の場所にコピーしたいと願っても、それは不可能です。なぜなら a 以外の場所に a と同じサイズの領域が余っていないからです。もっとメモリ空間が広ければコピー可能ですが、ジャブジャブ使うと、また上限に達してしまいます。これは避けようのない事実です。つまり、メモリを無駄遣いしたくないという要求があります。


2. 処理時間は有限である。

 一般に、プログラムの処理は遅いよりも速いほうが良いとされます。ゲームプログラムスピード感が損なわれたり、Web ブラウザで読み込みが遅いとイライラしますよね。プログラムは、計算ステップを踏めば踏むほど遅くなります。それは足し算であったり引き算であったりしますが、データのコピーであったりもするのです。大きなデータを何度も繰り返し、膨大な手間をかけてコピーすると、それだけでプログラムは遅くなります。これも避けようのない事実です。つまり、余計なコピーを避けて処理速度を上げたいという要求があります。


3. 開発工数は有限である。

 プログラマは無駄な作業を嫌います。例えば同じソース断片がプログラムのあちこちに散らばると、修正が必要になったときにその全てを修正しないといけないため、開発に手間がかかってしまいます。同じ処理が書かれている箇所が多ければ多いほど開発時間を奪われるのです。これも避けようのない事実です。つまり、似たようなソース断片はできるだけ一元管理して、無駄な記述を減らしたいという要求があります。


4. ポインタという仕組みは、以上 3 つの問題を解決するためにある

 これは C 言語のハナシではなく、ソフトウェア一般のハナシです。「データそのもの」だけでプログラムを作るのではなく、「データの場所を指すもの」を一緒に活用することでこうした問題にアプローチすることができます。C 言語では「データを指すもの」を表現するための仕組みをポインタと呼び、そのための文法が用意されている、というだけに過ぎません。

 例えばこんな場合を考えてみます。

    • 100 キロバイトのメモリを必要とする BigData 型の変数 a, b, c, d, e がある。
    • int の変数 user_input がある。
    • user_input が 0 だったら a の内容を画面に表示し、1 だったら b の内容を画面に表示し、… 4 だったら e の内容を画面に表示する。

 ソースにすると↓こんな感じ。

BigData a, b, c, d, e;
BigData that;
int user_input = (何らかの入力);
if( user_input == 0 ) {
  that = a;
} else if( user_input == 1 ) {
  that = b;
} else if( user_input == 2 ) {
  that = c;
} else if( user_input == 3 ) {
  that = d;
} else if( user_input == 4 ) {
  that = e;
}
display_one( that ); // BigData の内容を画面に表示する関数を呼ぶ

 ↑このプログラムを、ポインタを使ってより良いプログラムへと書き換えることを考えます。


5. ポインタを使って使用メモリを節約する

 まず、上のプログラムで使われている変数 that はメモリを無駄に消費しています。that は a や b と同じだけのデータが格納できるくらいに大きいからです。プログラムの構造を変えてやればメモリは節約できます。「if 分岐で a や b のコピーを得る」というプログラムを「if 分岐で a や b の位置を得る」ように変えるのです。ここで、ポインタ変数 p を使います↓。

BigData a, b, c, d, e;
BigData * p;
int user_input = (何らかの入力);
if( user_input == 0 ) {
  p = & a;
} else if( user_input == 1 ) {
  p = & b;
} else if( user_input == 2 ) {
  p = & c;
} else if( user_input == 3 ) {
  p = & d;
} else if( user_input == 4 ) {
  p = & e;
}
display_one( * p );

 that はメモリを 100 キロバイト消費していましたが、p は 4 バイト消費するだけで済みます。 p はメモリ上の位置を表現する整数値でしかないためです。


6. ポインタを使ってコピー回数を減らす

 5. で変数 that へのコピーを止めました。これでコピー回数も減ってますね。その代わり p へメモリ上の位置がコピーされるようになりましたが、p が小さいので that へコピーするより低コストで済みます。


7. ポインタを使って開発工数を減らす

 a, b, c, d, e のいずれかの値を表示するプログラムには柔軟性がありません。後から f, g, h,… が増えたときにそのぶんだけ else if を書き加えないといけないのは面倒ですよね。そこで、一つのアイデアを引っ張ってきます。「データを並べて、先頭にだけ名前をつける」というものです。前回の記事で使ったのと同じですね。

f:id:kura-replace:20120617174712p:image

BigData arr[5];
BigData * p;
int user_input = (何らかの入力);
p = & arr[user_input];
display_one( * p );

 ここまでの説明で、コピーが減ってソースコードの量も減りました*1。それを踏まえた上で、5. で書いたソースと比べてみてください。ポインタ変数 p は、a や b といった具体的な変数を指すこともできますし、arr[2] のように配列中の任意の要素を指すこともできることに気づくと思います。p が指す先に BigData がある、というただそれだけの仕組みなのでどちらの使い方もできるのです。

 さて、ここで注目すべきは「5 つの BigData がメモリ上に並んでいなくてはいけない」という制約が追加されてしまったことです。これはバカバカしい話です。たかがデータを表示するためのプログラムごときにデータの配置方法を決められてしまうなんて。それを改善するために、さらにまたポインタを使います。

 アイデアは、「5 つの BigData をメモリ上に並んでいなくてはいけない」を「5 つの BigData へのポインタがメモリ上に並んでいれば良い」に変えるというものです。BigData の配列 arr を使うのをやめて、BigData へのポインタ配列 parr を使うことにします。メモリ空間のイメージは↓こうです。

f:id:kura-replace:20120616223443p:image

↑こうすることで a や b が実際どこに配置されていても構わなくなります。これを C 言語のソースコードに書き下ろすと↓こんな感じになります。せっかくなので関数にしてしまいます。

void display( BigData * parr[] ) {
  BigData * p;
  int user_input = (何らかの入力);
  p = parr[user_input];
  display_one( * p );
}

 一番最初と比べると、いくらか洗練された事が分かると思います。


8. こんな話は、数あるポインタの使い方のほんの一部でしかない

 ポインタの使い方なんて無数にあります。教科書や本稿のような記事に書いてあるのはそのうちのほんの一部を具現化したサンプルでしかないのです。C 言語のプログラマ達は、ポインタを使ってメモリ使用量を抑えて実行速度を維持し、さらにデータ構造を抽象化して開発速度を上げる、ということを日常的にやっています。どういう使い方がどういう問題に対して有効であるか分からないうちは、他人のソースを読んで学ぶと良いでしょう。

 で、その前に本稿を読んで誤解してはいけないコトを書いておきますね。本稿ではコピー回数が少なければ少ないほど良い、的なニュアンスで説明をしました。しかしその考え方が万事通用するわけではありません。例えば、データのコピーをたくさんとっておいてそれらを戦略的に配置して圧倒的な速度アップを狙う、という場面はよくあります。また、ポインタを使って生産性を上げるという話を書きましたが、それも別の観点からみれば生産性を下げると言われていることにも留意すべきです。これは C 言語のポインタがあまりに強力すぎてバグ混入を容易にしてしまっているコトなどが主な理由です。


9. 「今どきポインタなんか使わざるを得ない C 言語を勉強するなんて終わってる」

 ... という指摘は、僕じゃなくて現場を仕切ってる人に言ってくださいね(何かそういう dis をいくつか受けたので一応)。

 あと、プログラミング初学者の人は「Javaポインタ無いじゃないか、Rubyポインタ無いじゃないか、なんで C 言語だけポインタあるんだよ!」っていう疑問を持っているようです。JavaRuby は C 言語のメモリ空間云々の話をそっくり覆い隠してくれているからポインタが無いのです。その代償として、C 言語ほど強力なチューニングはできません。しかしそれで良いのです。用途に応じて適切なプログラミング言語を使えば良いだけです。コンピュータを使う以上、ぎりぎりまでハードウェアの能力を引き出したいという要求があるので、その領域を C 言語が担っていて、だからこそポインタという仕組みが搭載されているのです。


10. 前回の記事でいただいたコメントへの返事

 前回の記事で "初心者" さんからこんなコメントをいただきました。

私はさぁこれからCを勉強しよう!としてすぐに挫折した屈強の初心者なので、こんなものを聞いてもさっぱり分かりませんでした。orz

もっと分かり易く説明お願いします。

どこが分からないかって?それが分からないくらい初心者です。。。

と言っても手の付けようがないでしょうから。

(1)ポインタがzの場所を教えるとありましたが、なぜ直前に出て来た変数aやbには必要ないのでしょうか?

(2)ポインタがメモリの有効利用が目的とありますが、ポインタを書かなければその分そちらの方がメモリが少なくて済むのではないでしょうか?

(3)ポインタを指定しない場合に起こる不都合というのを具体的に教えて頂けないでしょうか?(VBA程度は分かりますので、そこでの変数にはポインタがなくても不都合が生じません。なぜCには必要なのでしょうか?)

 回答、以下の通りです。

(1) もし "ポインタに数値を代入するとその場所に変数が生成される" と思っているのであればそれは間違いです。まずメモリが先にあって、それを指すデータをポインタと呼びます。質問された例でいうと、ポインタが「z の位置」を決定づけるのではありません。z があるから「z の位置」を得ることができるのです。z から「z の位置」を得る方法は "& z" です。もちろん z があるからといって「z の位置」を得ないといけないとか、z を指すポインタ変数を作らないといけないという決まりもありません。変数を指すポインタが無くても変数は存在できますし、ポインタが 300 個あっても構わないのです。直前に出てきた a や b は、「a の位置」や「b の位置」を使う必要が無かったので登場しませんでした。

(2) えぇと (1) の回答と本稿があれば、恐らく (2) の回答は必要ないですね…。ポインタ以外の方法を使ってプログラムを書くよりも省メモリで済ませられる場面が多いから、ポインタはメモリの有効利用に寄与するのです。

(3) これも (1) の回答があれば回答の必要は無さそうですね。ポインタという仕組みが無いと、省メモリ化、速度アップ、開発速度の向上ということが絶望的にやりにくくなります。だから必要です。初心者さんが VBAポインタを使わなくても事足りているのは、本稿の 9. で書いた JavaRuby を例に挙げたように、ポインタが不要だから覆い隠されているためです。

 以上。分かんなかったら Twitter で聞いてください。

*1:データが増えたときは arr[5] の 5 を 6 なり 7 なりに書き換えれば済むので else if を書き加えるより手間が減る

KeiKei 2012/06/17 14:29 昔だったら逆で、

そういう理由がある。どうしよう?

ポインタが使える!

という経験をしてきたんですが、特に今のPCのメモリの潤沢さを考えると、入門書での解説は首を捻るのもわかります。
組込ですら今はMB単位になってますからね。

あとは状態遷移を関数へのポインタを用いてすっきり書けるというところでしょうか。

kura-replacekura-replace 2012/06/17 14:54 ですね。僕は無限ループで malloc しまくるコードを書いてしまったときぐらいしか、メモリの上限意識することなんて無かったです。あと、関数ポインタを使ったプログラムの抽象化テクニックは、開発作業の省力化に含んで良いかなと思ってます。

tk_kappatk_kappa 2012/06/17 16:50 わたしは素人なので知らない事だらけです。勉強になります!

kura-replacekura-replace 2012/06/17 17:48 ありがとうございます!

yaya 2012/06/17 22:22 C言語の言語仕様にポインタが必須とされる理由は、アセンブラから置き換える言語という側面も大きいでしょう。
OSに守られたアプリケーションの開発に使われるだけなら必須ではありませんが、biosからドライバなど物理空間へのアクセスを必要とする場合にはポインタは必須になります。

otnotn 2012/06/17 22:25 それって「ポインタが使える場面」であって、存在理由じゃ無いよね。
存在理由は「あるのが自然だから」とか「CPUに機能としてあるから」か。

yaya 2012/06/17 22:34 「使える場面」としか見えないのは、C言語をそのような用途にしか使っていないからでしょうね。

kura-replacekura-replace 2012/06/17 23:40 コメントありがとうございます。

> ya さん
確かに、アセンブラの置き換え言語としての性質は強いですね。
bios の話もそうですが、例えば Win32API レベルの話で GetObject( HGDIOBJ, int, LPVOID ) みたいに頻繁に使われるものであってもポインタ無しには呼び出せないので、ポインタは C 言語に搭載されてしかるべき機能ですね。

> otn さん
その指摘は僕の書いた文章に対して言えることで、とどのつまり「あるのが自然だから」に収束するんだと思います。「CPU に機能としてあるから」という表現だと、C 言語が型の概念とポインタ構文を組み合わせた功績がやや軽視されるので少し踏み込み過ぎの感がありますけども。

yaya 2012/06/18 00:31 ポインタはcpuのアドレスレジスタを抽象化しただけに見えるので大好きなのですが、
やはり使わないで済むなら避けたいですね。
C++なら関数ポインタなども純粋仮想関数のインスタンスを使ってポインタを隠ぺいするなど置き換えるかなと思います。

kura-replacekura-replace 2012/06/18 01:02 C++ における関数ポインタの代替というハナシになると、クラスを使ったテクニックや関数オブジェクト、ラムダ式を出さない訳にはいかなくなるので一気に議論の幅が広がりますね。あと STL & Boost とかも。

初心者初心者 2012/06/18 17:56 kura-replaceさん、

おかげさまでポインタの存在意義がある程度わかりました!
もちろん本当はもっともっと奥深いものなのでしょうが、個人的理解としましては大きく前進することができたと思っています。本当にありがとうございました!!

私の業界ではC++が未だに主流なのですが、これは計算速度が非常に重要な業界なのでポインタの使えるからということが大きな理由の一つと理解することができました。(もちろん、他にも色々理由はあるでしょうが。)

これからしっかりプログラミングについて勉強していきたいと思いますので、
こちらのブログをお気に入りに入れ、これからちょくちょくお邪魔させて頂きますね!

kura-replacekura-replace 2012/06/18 18:16 理解の一助になれたのであれば幸いです。

foohogehogefoohogehoge 2012/06/18 19:19 Cを知らない同僚にも教えてあげたい。(めんどくさいからやらないけど)
.NETとかJAVAとかだってポインタを知ってると
だいぶ違うんだけどな〜

kura-replacekura-replace 2012/06/18 19:46 ありがとうございます。C 言語のポインタと同じではないにせよ、ポイントするという考え方そのものはたいていの言語で通用しますね。

テストテスト 2012/06/30 23:15 Javaの経験しかない私にとって、とてもわかりやすい記事でした。
ただ、疑問点が2つありますので、ここに質問をさせていただきます。

先日拝見した記事(※)の下記ソースと、上記7の例では配列の
扱いが違っているように見えます。
上記7の例では、配列ポインターの代入では、
p = & arr[user_input];を使用しています。
これはなぜでしょうか?

int * p;
arr[0] = 260;
arr[1] = 259;
arr[2] = 8;
p = arr;
printf( "%d%", p[1] ); // 259

※ http://d.hatena.ne.jp/kura-replace/20120611/1339376977


また、下記のようなソースを書いた場合、
p1とp2では&が有っても無くても出力結果が同じになります。
ポインターというのは結構曖昧なものなのでしょうか?

int arr[3];
int *p1;
int *p2;

arr[0] = 250;
arr[1] = 259;
arr[2] = 8;
// 下記二つがなぜ同じになるか?
p1 = arr;
p2 = &arr;

printf("%d\n", p1[1]);
printf("%d\n", p2[1]);

kura-replacekura-replace 2012/07/02 01:08 コメントありがとうございます。
・質問 1
 arr[0], arr[1] という文法は、それぞれ「arr という位置から数えて 0 個目、1 個目の int の値」という意味です。従って "[xx]" のつかない "arr" という表記は、メモリ上の位置を表しているのです。ポインタもメモリ上の位置を表しているので、p = arr という代入ができるようになっています。前回の記事では、arr と同じ場所を p にセットしていたわけです。
 次に p = & arr[user_input] という表記には & がついていることに注目してください。例えば p = arr[user_input] という表記があったとしたなら、p はメモリの位置、arr[user_input] は int のデータであり、互いに意味が違うので代入は不可能(コンパイルエラー)です。データが配置されている位置を取得するために & 演算子が用意されています。p = & arr[user_input] という表記は、『「配列 arr の user_input 番目」の場所』を p にセットしているわけです。
 つまり、前回の記事で使ったプログラムと、今回の記事で使ったプログラムでは p が指している場所が違うのです。

・質問2
 ポインタが曖昧なのではなく、これはローカル配列変数のちょっと特別な文法です。arr だけだとデータが始まる場所を示しますが、実は & arr も同じ意味になります。もちろんポインタ変数 p の場合は、p と & p で意味が異なります。

KeiKei 2012/07/02 01:16 テストさんへ。

筆者様のkura-replacesama様では無いですが、私の見解を述べさせてもらいます。

まず一つ目から。
Cの配列の原則として、
・式の中では、配列名は原則として配列の先頭要素へのポインタに変換される
というものがあります。
なので、
p = arr;

p = &arr[0];
と同じ意味です。

上記7の例では変数user_inputが実際に決まらないと判らない(名前からしてユーザの入力で変わる可能性がある)ということから、
p = &arr[user_input];
と明示的に添え字演算子を使い配列のどの要素のポインタが入るか判らない、という表現を表したかったのでは? と思います。
今回の記事は「C 言語にポインタがある理由」についてであり、やや実践的な使い方で表現した方が理に叶っていると思いますし。

そして最初に書いた通りのルールでいくと前記事「プログラム初心者にC言語のポインタを不本意ながら教える〜」での例は
int * p;
arr[0] = 260;
arr[1] = 259;
arr[2] = 8;
p = &arr[0]; /* 元は p = arr; */
printf( "%d%", p[1] ); // 259 p = &arr[0];
としても同じです。
恐らく、前記事では要素に代入する値をベタ書きしている為、こちらの記法の方が読みやすいのでは、と筆者様が考えた結果であると考えます。
前回の記事は「初心者にC言語のポインタを教える」というテーマであり、かつ過去の記事である為今回と表記が違っても仕方が無いと思います。

最初に原則とわざわざ書いたように例外があって、配列には、
1. sizeof演算子のオペランド(例: sizeof arr)
2. アドレス演算子(&)のオペランド(例: &arr;)
3. 配列変数初期化時の文字列リテラル(例: int a[] = { 'T', 'E', 'S', 'T', '\0' })
という原則であるはずの「配列名は配列の先頭要素へのポインタに読み換えられる」ことが「ない」(変換されない)という「例外のケース」が存在します。
この内「2.アドレス演算子を配列名に適用した場合」が2つ目の疑問に繋がってきます。

そして2つ目です。
// 下記二つがなぜ同じになるか?
p1 = arr;
p2 = &arr;

これは厳密には(代入演算子の右辺が)同じではありません。
&arrは「配列の先頭要素へのポインタ」ではなく「配列の大きさを持った配列そのものへのポインタ」を返します(表します)。
配列名から読み替えられたポインタは左辺値を持たないのでアドレス演算子のオペランドになれないはずなのですが、例外として配列全体のポインタを取得できるような仕様になっています(私には反論できません)。1次元配列では違いが解りにくいですが所謂2次元配列(本当は「配列の配列」)で「内側の配列の大きさでn個分ずらす」といったときに違いが現れます。

戻って、それならば何故
p2 = &arr;
として
printf("%d\n", p2[1]);
で意図したかのように動いているかというと、
p2 = (int*)&arr;
という暗黙の型変換が行われている為です。
別のルールとして
・代入時に左辺の型と右辺の型が異なっている場合で明示的なキャストが無い場合は左辺の型に暗黙的に変換される。
というものがあります。

あと蛇足かもしれませんが、「配列」というものは宣言の中でしか(先の例外を除き)直接扱えず、式の中で配列を操作しているようなところは(先の例外を除き)全てポインタで処理されるように処理系で変換されています。
arr[n];

*(arr + n);
のシンタックスシュガー(構文糖)といったように。
これは添字演算子[]を使った方が読みやすい(可読性を上げる)という理由で使われます(と私は思っています)。

Cは色々(原則と例外、不定と未定義など)こんがらがっているので、上に書いたものも正直私は自信がありません。筆者様の突込みを待ちつつ、Cを使うなら読んでおいた方が良いC FAQを紹介して締めにしたいと思います。
C FAQ 日本語訳 http://www.kouno.jp/home/c_faq/

長文失礼いたしました。

kura-replacekura-replace 2012/07/02 02:19 まさか僅か 8 分差 post になるとはw ありがとうございます。正しい内容だと思います。

テストテスト 2012/07/04 00:49 Keiさん

ご回答ありがとうございます!

C言語にも暗黙の型変換があるんですね。かなり意外です。
「配列の大きさを持った配列そのものへのポインタ」という箇所が、私にはイメージしづらかったので
実際に2次元配列を使ったコードをいくつか書いてみます。

また、配列のシンタックスシュガーについても、とても勉強になりました。
業務ではなかなかCに触れる機会がないので、本で読んだ知識では身に付かないことが
多く、オープンソースを読んでも全体が見えづらかったので、心が折れかけていました(笑)
Keiさんの解説やkura-replaceさんの記事のおかげで
Cの勉強を続けるモチベーションが上がりました。
ありがとうございます。

http://www.kouno.jp/home/c_faq/
こちらは時間がかかりそうなので、土日に挑戦してみます。

2012-06-11

プログラム初心者にC言語のポインタを不本意ながら教える羽目になったなら、こう教えると良いよ

| 10:09

 僕がプログラミングに触れた当時は、プログラミングといえば「まず C 言語」でした。それから 10 年以上が経ちました。学校の授業や企業の研修では未だに C 言語を教えているところがあるようです。関数型プログラミング言語という波が来ている 2012 年にもなって未だに C 言語をやっているというのはまるで進歩が無く残念な気もしますが、比較的多くのプログラマに浸透している共通言語を最初に教えるというのは、一方では喜ばしい事だと解釈する事もできるのかもしれません*1。まぁとにかく、本意にせよ不本意にせよ現場で プログラム初心者に C 言語を教える羽目になった 人がたくさんいて、プログラム初心者なのに C 言語を学ばざるを得なくなった 若者がたくさんいるということです。

 C 言語を教えるときに避けて通れないのがポインタで、プログラム初心者が C 言語を学ぶときにやたらとつまずく人が多いのがポインタです。ポインタの理解ごときに若者の貴重な時間を大量に浪費するのはアホらしいので、こういう教えかたすると早く教えられるよ、というハナシを書こうと思います。本稿は「ポインタの解説」ではなく「C 言語のポインタの教え方」です。

1. そもそも "変数は箱"、という例え話がポインタの説明をややこしくしている

 変数という概念を、全く別のメタファーである "箱" に喩えて説明するのはよく使われる手法ですが、その喩えはポインタの説明をするときに破綻します。「箱にラベルを貼るのがポインタです」という "喩え話続行戦略" にはそもそも無理があるのです。どうしても喩えを持ち出したいのであれば、その喩えを一旦信じてもらって理解をレベルアップさせた後、その喩えがなぜ妥当であったかを先生と生徒が共有できる程度まで噛み砕いた説明を付け足して、理解を定着させる必要があります。これはハイレベルな説明テクニックです。喩え話をした後の責任が取れないのであれば、その喩えはゴミ同然でしょう。

2. 「ポインタを教える」ために変数を教える

 ご存知の通り、C 言語のポインタ変数の説明抜きには説明できません。また、メモリの "効率的な利用" という基本的な存在意義があります。従ってメモリ空間をイメージさせることが、変数ポインタの両方を理解させるための近道です。もちろんメモリ空間のイメージを抜きにして文法だけで理解させることも不可能ではありませんが、その場合は C 言語のポインタ変数の宣言周りの文法で間違いなくハマります。表面の文法だけを舐めて理解させようとするのは、かなりハイレベルな説明スキルが必要だということです。相手はプログラミング初心者です。屈強な相手です。この戦略は避けるべきでしょう。

3. 「変数を教える」ためにメモリ空間をイメージさせる

 メモリ空間をイメージさせるために、もはや文法の説明は不要です。C 言語の説明を文法から始めるのは、きっぱり諦めましょう。C 言語のポインタの理解には概念の理解が絶対です。概念の説明をするために文法から説明するのは、少なくとも "C 言語のポインタを説明する" というミッションにおいて遠回りなのです。その理由は上で説明した通りです。メモリ空間をイメージさせるために大切なことは「文法の説明をしないこと」です。文法の説明無しに、方眼紙のようなメモリ空間をイメージさせるのはさほど難しくないはずです。しかし方眼紙状のイメージだけではイメージとして完結しないので、さらにもう一歩引き下がってステップを踏むのが効果的です。

4. 「メモリ空間をイメージさせる」ためにバイトという単位を教える

 バイト、バイト、バイト、バイト!!!ここから全ての説明を始めるのが良いです。「バイトと言っても、アルバイトとは違いますよ」とか小寒いおやじギャグを添えたりして、まずバイトを説明するのです!

 バイトとは、0 〜 255 のいずれか 1 つの整数値を表現できる単位です*2

5. バイトの解釈によって色んな表現が可能であることを説明する

 f:id:kura-replace:20120609233945p:image:right例えば、あるバイト x が 3 である場合、それをそのまま 3 と解釈することもできるし、「x は 100 からのオフセット値である」とするなら 103 と解釈することもできます。「x は 'A' から並ぶアルファベットの文字である」とするなら 'D' と解釈することもできます。

 「x は○○スーパーに並ぶ缶コーヒーの在庫数である」とするなら在庫数 3 と解釈することができます。しかし缶コーヒーの在庫数が 0 個であるか 42 個であるか 300 個であるかを表現するバイトは存在しません。なぜなら 0 〜 255 では表現しきれないからです。そこで、0 〜 300 個の缶コーヒーの在庫数を表現できるようにするために、1 つのアイデアを与えます。

f:id:kura-replace:20120609233944p:image:right

新しいバイト y を使って、x との組み合わせで数値を表現するというアイデアです。y が 0 のとき、x の値はそのまま缶コーヒーの在庫数と解釈することにします。y が 1 のとき、x の値は 256 ~ 511 個の缶コーヒーだと解釈することにします。バイトが 2 つあれば 65536 種類の値を表現することができます。バカみたいに簡単なハナシですが、バイト数が多ければそれだけ多彩な表現ができることを理解させましょう。

 あるバイト x をどう解釈するかは、プログラマが指定します。C でプログラムを書くということは、そのバイトが何を意味しているかをコンピュータに教える作業をするという事でもあるのです。

6. メモリ空間をイメージさせる

 コンピュータは計算を大量にしないといけません。従って、y が 1 のときは x の解釈は… という具合に 1 バイトずつ考えてゆくのは、とんでもなく大変です。そこで、もう一つアイデアが必要になります。それは例えば「x から始まる 2 バイトを数値として解釈する」という具合に、名前を消してしまうアイデアです。これを貫徹するためには、その 2 バイトが並んでいる必要があります。場合によっては 4 バイト、8 バイト... 何にせよ並ぶ必要があります。そこで、バイトをずらっと並べたものが用意されているわけです。これがメモリ空間です。*3

f:id:kura-replace:20120610230943p:image

7. 変数を教える

 メモリ空間にバイトが並んでいる理由が説明できたなら、変数をイメージさせるのは簡単でしょう。

f:id:kura-replace:20120611081901p:image

 箱のイメージではなく、数バイトのメモリを指す名前として変数をイメージさせることができました。次にこのイメージを使ってプログラムを作ることを説明すると良いと思います。ここで、少しだけソースコードを書きます。4 バイトで表現された数値を扱うために int という名前が用意されていることも含めて、教えることができるはずです。

int a, b;
a = 258;
b = 5;
printf( "%d, %d", a, b );  // 258,5
if( a < 200 ) {
  a = a + b;
} else {
  b = a - b;
}
printf( "%d, %d", a, b );  // 258, 253

 ここまでくれば C でプログラムを書くということがどういうことかを、何となくイメージさせることができます。

8. ポインタを教える

 僕らは普段の会話で「あれが欲しい」「それを見たい」という表現をします。マウスカーソルを動かしてアイコンをクリックします。具体的な対象の代わりに、指し示す表現を使っているわけです。「指す」ことを、プログラムの中でも実現するのがポインタです。

f:id:kura-replace:20120611090324p:image

「z の場所がどこであるか」は、「メモリ空間の何番目であるか」で表現できますよね。ポインタなんぞ、この程度に簡単なものでしかないです*4。次にポインタの文法を説明します。

9. ポインタの文法を教える

 ポインタの文法を説明するときに大切なことは、変数の宣言時初期化構文を見せないことです。

int a, b;
int * p;
a = 100;
b = 200;
p = & a;
printf( "%d", * p ); // 100
* p = b;
printf( "%d", * p ); // 200

 & 演算子が a の位置を表現する事、* 演算子で p が指すメモリを表現する事を説明します。

10. 配列ポインタと絡めて教える

 C 言語をいかにして教えるか、という議論において、配列ポインタを絡めるとややこしくなるからダメという意見が根強いですが、それは "変数は箱" という喩えを使っているからです。C 言語を扱うときに配列ポインタは切っても切れないのが事実なので、関係が強いことをアピールすべきでしょう。メモリ空間のイメージがあれば関係の強さもまた容易に理解させることができます。

f:id:kura-replace:20120611093423p:image

 文法の説明なぞ、二の次で良いのです。

int arr[3];
int * p;
arr[0] = 260;
arr[1] = 259;
arr[2] = 8;
p = arr;
printf( "%d%", p[1] ); // 259

11. NULL を教える

 ここまでの説明パスを経ることで、4 バイトのメモリを、メモリ上の位置を表現する数値だと解釈すること*5ポインタであることを理解させることができているはずです。次に、ポインタが「今はどこも指してないよ」を表現するために NULL という定数値があることを教えます。こういう説明順序を踏まえることで↓このコードが意味的におかしいことをすんなり理解させることができます。

int a;
a = NULL;

12. その先

 以上が、プログラム初心者に C 言語を教えざるを得ないときにポインタを教えるための説明パスです。この先は省略しますけども、メモリ空間のイメージを用いて説明しているので、以下のつまずき箇所を説明するときのハードルもぐっと下がります。

・2 重ポインタ、3 重ポインタ

・矩形配列とジャグ配列

関数引数に n 次元配列を渡すときのコンパイルエラー

・int * から double * への型キャストが不適切な理由

文字列配列で表現されること

関数ポインタ

そんなわけで、ポインタの説明(というか間違った理解を補正するための補足説明の嵐)に時間と手間をかけるのはもう辞めようぜ!というお話でした。

*1:それを意図しているかは別として。

*2:7bit や 9bit の処理系もあるとか、そういう厳格性を問う議論はここでは不要でしょう

*3:バイトが並ぶっていう日本語は妙ですが、まぁそこは汲み取ってください

*4ポインタポインタ変数は違う、という指摘が入りそうですがそれはごもっとも

*5:4 バイトとは限らないとかそういう話は今は置いておきましょう

ko1kunko1kun 2012/06/11 10:20 素晴らしいです。わかりやすいです。新人研修の参考にさせていただきます。
私自身は、最初にC言語を学んだときはポインタがよく理解できなかったのですが、その後でアセンブリ言語(8086や68000など)を学んだら、すごくスッキリわかりました。変数とメモリの関連が把握できると簡単なんですよね。

kura-replacekura-replace 2012/06/11 10:56 ありがとうございます。メモリのハナシ抜きにポインタ構文の説明して理解されない場面を何度も見てきたので、文章にしてみました。研修の一助になれれば嬉しいです。

h_easth_east 2012/06/11 11:24 10のコード部分のtypoです。
> p = a;
> printf( "%d%", (*p)[1] ); // 259
p = arr;
printf( "%d\n", p[1] ); // 259

説明わかり易いです。

kura-replacekura-replace 2012/06/11 11:36 うぉ、凡ミスですね...。ありがとうございます!

KeiKei 2012/06/11 14:11 Cでのポインタを使うケースが一部の組み込み以外では
・配列の走査
・関数の引数で参照的に使いたい(swapや複数の返り値が欲しい等)
・構造体を扱うときのコストを下げたい
・そもそも関数の仮引数で配列をそのまま渡せない(構造体でラップするとかは無し)
ぐらいなのに、入門書ではint型の一つの変数を色々しているのは今でも不思議です。
もし私がCをまったく知らなかったら、あのコードを見せられても「はい。で?」になると思います。

kura-replacekura-replace 2012/06/11 14:39 おぅふ... すみません。

KeiKei 2012/06/11 15:34 あうあう。もしかして私が書いた「あのコード」がkura-replaceさんが書いた「9. ポインタの文法を教える」のコードと誤解なさっておられますでしょうか?
だとしたら全くもって違います。私の言いたかった「あのコード」とは私自身が書いた前の行の「(私が思う)入門書」のコードのことです。誤解を招く文章で申し訳ありません・・・。
文法を教える例で「のみ」、プリミティブな型で色々するのは、寧ろ私が挙げた4つの例等で実際使うときに厳密になる(ウォーニング、バグを出さずにすむ等)という点で必要だと思っています。
私が肝心なことを書き忘れてしまったのですが、書きたかったことは入門書のポインタの部分は「あのコード」を書いて、文法以外のことも教えたつもりになっているような気がしてならないということです。
その点、この記事でまとまっている様に書けば、全体が俯瞰できて良いと思います。

上記の私のコメントを訂正すると、
・あのコードを -> ポインタの章であのコード「だけ」を
となります。

長文ごめんなさい。この記事はわかりやすくて、もしそんな状況があればこの記事を参考にさせていただきたいと思います。ありがとうございます。

kura-replacekura-replace 2012/06/11 15:42 おっと、誤読失礼しました。文法だけの説明では理解させるのは難しいっていうの、同感です。ありがとうございます。

初心者初心者 2012/06/12 17:45 私はさぁこれからCを勉強しよう!としてすぐに挫折した屈強の初心者なので、こんなものを聞いてもさっぱり分かりませんでした。orz
もっと分かり易く説明お願いします。
どこが分からないかって?それが分からないくらい初心者です。。。

と言っても手の付けようがないでしょうから。
・ポインタがzの場所を教えるとありましたが、なぜ直前に出て来た変数aやbには必要ないのでしょうか?
・ポインタがメモリの有効利用が目的とありますが、ポインタを書かなければその分そちらの方がメモリが少なくて済むのではないでしょうか?
・ポインタを指定しない場合に起こる不都合というのを具体的に教えて頂けないでしょうか?(VBA程度は分かりますので、そこでの変数にはポインタがなくても不都合が生じません。なぜCには必要なのでしょうか?)

kura-replacekura-replace 2012/06/13 10:54 > 初心者さん
コメントありがとうございます。ちょっと回答待ってください。

kura-replacekura-replace 2012/06/16 23:18 > 初心者さん
回答書きました。http://d.hatena.ne.jp/kura-replace/20120616/1339856279

narucynarucy 2013/03/08 21:01 まったく素晴らしい。しょうもない喩えなどいらない。ありのままを伝えればいいってことです。

プリミティブ型のポインタなんて、まったく使わない、使う意味がないんだから、そこをすっとばして、配列のポインタから教えてもいいと思ってます

tk2tk2 2013/03/14 19:10 長いことプログラムをしているので自分が初心者のころなど
忘れてしまっているのですが、ひょんなことでC言語を初心者に
教えることになってしまいました。
教科書に指定されたいわゆる入門書というのをみたのですが、
いろいろな部分でこれで分かるのか?という疑問が浮かびました。
このページ見てうんうんこれなら分かるはずと納得しました。
手順もしっかり踏まえているしすばらしいです。
参考にさせて頂きます。

kura-replacekura-replace 2013/03/15 10:11 コメントありがとうございます。

>narucy さん
OOP で Dog クラスを使って鳴かせるとかいうのも喩えに失敗してる例ですよね :P

>tk2 さん
ありがとうございます。一助になれれば幸いです。