ポインタ不要論 - やまざき@BinaryTechnology
「デスマーチと戦う武蔵流プログラマ やまざき のページ」

TopPage
(サイトマップ)


Book
(書籍)


「火事場プロジェクトの法則」
サポートページ


「LHAとZIP」
サポートページ



Document
(文章)

デスマーチの記録に見る
運命の分かれ道
NEW!

武蔵流プログラマからの提言

武蔵流プログラマが斬る Eclipse

コードデザイン最前線
1
2 3 4 5 6 7 8 9
10 11 12 13 ML

C++で読む
デザインパターン


ポインタ不要論

データ圧縮の基礎

プログラマへの
アドバイス


データ指向の話1 2

インターフェースの話


Diary & Books
(日記と本屋)

やまざきの
はてなダイアリ
(日記)
[] [PC] [資産運用]
[デスマ] [映画] [2足ロボ]

やまざきの本屋


SoftWorks
(ソフトウェア)


(1) DeepFreezer
(ディープ・フリーザー)
高速アーカイバ

(English Page)

(2) Closedown-Planet
(クローズダウン・プラネット)
アクションパズルゲーム


(3) PieceMaker
(ピース・メーカー)
ファイル分割/結合


(4) WakuPita
(枠ピタ)
ウィンドウ移動便利ツール

(English Page)

(5) ググ郎
(Bookmarklet)
選択文字列をGoogleで検索

NEW!


Developing
(開発中)


(1) DeepFreezer2
yz2dlg.dll alpha6


C Magazine特集yz2


Hobby & Favorite
(道楽/お気に入り)


2LegRobo
MindStorms



p.s.
(雑談)


Profile

i_want^^;


やまざきが書いた本


[システム開発]
火事場プロジェクトの法則
どうすればデスマーチをなくせるか?
2006/09/13 発売


LHAとZIP
圧縮アルゴリズム×プログラミング入門

奥村さんと共著です。
2003/12/01 発売


やまざきが寄稿した本


SEの読書術
「本質を読む」力を磨く10の哲学 2006/02発売。



開発の現場 Vol.002
「反デスマーチ大研究」という記事。2005/09/13発売。



Software People Vol.3
「武蔵流プログラマからの提言」という記事。2003/10/31発売。



Eclipse パーフェクトマニュアル vol.1
「武蔵流プログラマが斬る Eclipse」という記事。2003/06/05発売。




ポインタ不要論

●はじめに

 この文章は、"NEW PROGRAMMING MAGAZINE 2001/Spring"に掲載された文章を元に HTML 化しています。

●NEW PROGRAMMING MAGAZINE Spring2001

<克服企画>C/C++プログラミング道場

 一読して理解しにくいポインタ。バグを作りこんでしまう場合もあります。それでは,ポインタを使わずにプログラミングする方法はないのでしょうか。最近注目を浴びているSTL(StandardTemplateLibrary)というのものがあります。本稿では,STLですっきりとしたプログラミングをすることを提案します。

やまざき@BinaryTechnology
http://www.01-tec.com/

●バグとの戦い

 プログラムのバグは,全プログラマの共通の悩みであると思います。また,バグを撲滅することがプログラマの共通の目標であるとも思います。特に長い間たくさんのコードを書いてきた熟練プログラマはバグとの戦いに疲れきってしまい,引退を考えてしまうほど辛い経験だったかもしれません。年齢の若いプログラマは,気力も体力もあり,多少のバグではこたえないかもしれません。しかし,この戦いを永遠につづけたくなはいでしょう。C言語の熟練プログラマであれば,ほとんどの人があるとき悟るはずです。

「ポインタはバグの巣窟である」と。

 よく「C言語ではポインタを制覇すれば勝ったのも同然」といわれます。そうなのです。C言語はほかの言語と違って,ポインタの扱いが特に難しいのです。
 プログラミングの世界では「バグ」を作りこむことを悪いことであるとして,嫌う傾向にあります。これはこの世界の構造上しかたのないことと思います。しかし,だからといって何もしないのでは前に進むことはできないでしょう。また,誰がいったのか知りませんが,

「失敗することは罪ではない。
失敗を恐れて何もしないことが罪である」と。

 これは,よい言葉だと思います。そうです。最初は誰でも失敗するのです。しかし,だからといって二度も三度も同じ失敗をしては意味がないのです。それではせっかく「学習」できるこの体をもって生まれた意味がありません。もったいないと思いませんか。では,どうするのか,バグはなぜ出たのか,回避する方法はないのか,みなさんも一緒に考えてみてください。
 本稿では,私の感じたことを,ポインタ関連のバグを減らすための1つの方法として,ポインタの扱い方に対する提案をしたいと思います。

●ポインタは危険?!

 ポインタは,BASIC言語などでよく使われるgoto文と同じように,便利であると同時に危険です。goto文は適切に使えば特に問題になりませんが,やたら使うとプログラムがからみ合い読みにくくなります。読みにくくなるとバグを作りこむことになり,またこうして作りこんでしまったバグは発見しにくくなるという悪循環を繰り返します。同じように,ポインタも適切に使えば問題はありませんし,処理の高速化や概念の実装に役立つでしょう。だからと言って、ポインタに頼りすぎるとgoto文と同じように読みにくくなりバグを作りこむことになります。
 ポインタに関連したバグは根が深く発見しにくい厄介なモノが多いのです。以下に,ポインタ関係でよく見かける主なバグをいくつかあげてみます。

ポインタに関する主なバグ
バグその1 「ポインタの構文や概念は・理解しにくい」
バグその2 「領域破壊」
バグその3 「'\0'終端」
バグその4 「ランダム爆撃」
バグその5 「メモリリーク」
バグその6 「配列は引数にのらない」
バグその7 「スタック参照/スタック破壊」

 本稿ではポインタに関する主なバグを7例ほど紹介します。詳細なコードについては,それぞれのリストをご覧ください。いずれも,コンパイルは正常終了(もしくは警告:Warning)で通り,実行時にバグを出してしまうケースです。よくあるケースを簡単にあげてみたつもりですが,ほかにも根の深いポインタ関係のバグは多いと思います。

 最近のC/C++コンパイラは最適化の性能も良く,ポインタを使わなくてもかなり高速なマシンコードを作ることができます。

「ポインタを使うと効率のよい高速なマシンコードができる」

という言い伝えはすでに過去の迷信となりつつあります。また,最近ではバグを減らすためには,

「わかりやすい(可読性のよい)コードを書くべきである」

 といわれるようになり,この「言い伝え?」には私も異論はありません。最近では,ポインタの存在意義が薄くなっているのも事実であるとも思います。
 では,その危険で古い概念ともいわれるポインタを使わない,もしくは隠蔽することはできるのでしょうか?そのことに焦点をあてて述べてみたいと思います。

●ポインタを使わない?!

 C言語でポインタを使わないことはほぼ無理です。なぜなら,多くの標準ライブラリがポインタを使うことを前提に作られているため,この多くの標準ライブラリを使わずにプログラムを組むことは不可能といえるからです。
 しかし,C++であればこの状況はかなり改善されます。以下に述べる項目を実行すれば,あらかたのポインタは隠蔽され消えていきます。C++はC言語のほぼ上位互換の機能を持っています。C++のすべての機能を知らなくても(C++の機能/能力をフルに使いきらなくても)ちょっぴり高機能なC言語としてC++を使うこともできるのです。ですから,ポインタの使用を減らす(やめる)目的で,開発環境をC言語からC++に移ることはよいことだと思います。
 では,ポインタの使用を減らす(やめる)ための3つのポイントを紹介しましょう。

▼(1)文字列は char* ではなく STL の std::string を使う

 C言語では文字列を char 型の配列としてあつかいます。しかも,文字列の終端が'\0' であることを前提としています。このことがC言語での文字列の扱いを極端に難しくしています。一方,C++には STL という標準テンプレートライブラリ(StandardTemplateLibrary)があります。その中に std::string というクラスがあり,文字列の概念を実装しています。簡単にいえば,文字列の操作がBASIC言語のように楽ができるのです。C++では文字列をあつかうために,char型の配列やポインタを駆使する必要はないのです。

▼(2)動的配列では malloc()/free() ではなく STL の std::vector<> を使う

 C言語には動的な配列という概念がありません。配列の大きさをプログラム実行時に決めたり,実行中に大きく拡張したりするのは大変な作業になります。主に malloc()/free() 関数を使って実装するのですが,それなりにデメリットがあります。一方,C++には STL の中に std::vector<> というコンテナクラスがあります。この std::vector<> を使うことで,動的な配列を簡単に実装できます。そのために用意されたコンテナクラスなのですから,危険な malloc() やポインタなど使わずに楽をしましょう。

▼(3)関数の引数では,参照(リファレンス)を使う

 C言語には参照(リファレンス)という概念がありません。関数の引数では値渡し(コピー渡し)が主な渡し方ですが,呼び出し元に複数のデータを返す場合には,値渡しではうまくいきません。そこでポインタを渡してデータを返してもらうのがあたりまえの習慣になっています。C++では,参照(リファレンス)という概念が追加されました。これを使うことで,データを返してもらうためだけの理由で危険なポインタを使う必要はなくなります。データを返してもらいたいのであれば参照(リファレンス)渡しのほうが危険も少なく効率的といえます。

 ポインタは危険であることはこの後のバグの例にて解説するとして,まず,ポインタは必要以上に強力な道具であることを理解してください。「ダイナマイト」とか,「諸刃の刃(もろはのけん)」などのイメージがあてはまるでしょう。適切に使えばそれなりの効果を発揮しますが,誤った使い方をするとかなり痛い目をみます。強力ではあるがそれなりに危険も含んでいるということなのです。こういった危険な道具は「ここぞ」という時以外は隠蔽すべきであると思います。隠蔽できるのであればそうして使い,安全なプログラミングを心がけてバグを減らすというスタンスです。では,これらの提案が実際のポインタに関連するバグにどのような影響をあたえるのか具体的に見てみることにしましょう。

●バグその1「ポインタの構文や概念は理解しにくい」

 まず,ポインタという概念を理解するには少し時間がかかります。これは誰でもが経験することであると思います。ポインタを頭の中でこんな([●]→[__])記号(図形)に変換できれば最初のハードルはクリアです。しかし,その簡単な変換も少し複雑な宣言や記述になってくると急に難しくなってきます。これは経験が解決するともいえることですが,できることならもっとわかりやすい概念を使いたいものです。以下の例はその理解しにくい概念に惑わされて文字列の比較に失敗してしまったものです。

リスト1
バグその1「ポインタの構文や概念は理解しにくい」
リスト2
バグその1改良版(STL)
#include <stdio.h> /* puts() */

int main()
{
    
char * abc_str = "aBC" ;

    abc_str[0] = 'A' ;
/* 'a'->'A'に上書き */

    
if ( abc_str == "ABC" )
    {
        
/* 同じにならないのはなぜ?*/
        puts( "同じ" ) ;
    }
    
else
    {
        puts( "違う" ) ;
    }

    
return 0 ;
}
#include <stdio.h> // puts()
#include <string> // STL std::string

int main()
{
    std::string abc_str = "aBC" ;

    abc_str[0] = 'A' ;
// 'a'->'A'に上書き

    
if ( abc_str == "ABC" )
    {
        puts( "同じ" ) ;
    }
    
else
    {
        puts( "違う" ) ;
    }

    
return 0 ;
}
コンパイル結果 -エラー0,警告0 コンパイル結果 -エラー0,警告0
実行結果 違う 実行結果 同じ

 リスト1について解説します。C言語ではポインタを直接比較してもアドレスを比較しているだけなので文字列を比較することにはならないのです。C言語の文法では文字列の比較ではなくポインタ内のアドレスと文字列のアドレスを比較すると解釈されてしまいます。ポインタと文字列の概念の違い(混同)から発生するバグであると思います。
 文字列をポインタ char* ではなく std::string クラスを使って記述したものが,リスト2になります。std::stringクラスを使うことで'=='演算子で文字列どうしの比較がでるようになります。文字列の概念としてもわかりやすく,バグの発生をおさえる効果があります。

●バグその2「領域破壊」

 ポインタとはある領域を示すモノです。そのある領域の広さは固定でありプログラマの思い通りに自動的に増えたり減ったりするものではないのです。以下の例は,その領域を越えて書き込んでしまい失敗した例です。

リスト3
バグその2「領域破壊」
リスト4
バグその2改良版(STL)
#include <stdio.h> /* puts() */
#include <string.h> /* strcat() */

int main()
{
    
char * abc_str = "ABC" ;
    
char * xyz_str = "XYZ" ;

    strcat( abc_str, xyz_str ) ;

    puts( abc_str ) ;
/* "ABCXYZ"と表示したい */

    
return 0 ;
}
#include <stdio.h> // puts()
#include <string> // STL std::string

int main()
{
    std::string abc_str = "ABC" ;
    std::string xyz_str = "XYZ" ;

    abc_str += xyz_str ;

    puts( abc_str.c_str() ) ;
// ABCXYZと表示したい

    
return 0 ;
}
コンパイル結果 -エラー0,警告0 コンパイル結果 -エラー0,警告0
実行結果 「アプリケーションエラー」 実行結果 ABCXYZ

 リスト3の例は,ポインタは領域を示すモノであって,領域を確保しているわけではないことを理解していないとこのようなバグを作ってしまうことになります。
 そこで,文字列をポインタ char* ではなく,STLの std::string クラスを使ったのがリスト4になります。STLの std::string クラスは文字列の操作においても領域確保などの面倒な作業はクラスの中で自動的に行われます。プログラマは領域確保などの心配(雑務)から解放されるわけです。

●バグその3「'\0'終端」

 C言語の文字列では,終端記号があることを前提にしています。しかし,charの配列を確保しただけでは文字列として初期化されているわけではないのです。以下の例は,終端記号の存在(文字列としての初期化)を忘れたために失敗してしまった例です。

リスト5
バグその3「'\0'終端」
リスト6
バグその3改良版(STL)
#include <stdio.h> /* puts() */
#include <string.h> /* strcat() */

int main()
{
    
char buff[128] ;

    strcat( buff, "ABC" ) ;

    puts( buff ) ;
/* ABCと表示したい */

    
return 0 ;
}
#include <stdio.h> // puts()
#include <string> // STL std::string

int main()
{
    std::string buff ;

    buff = "ABC" ;

    puts( buff.c_str() ) ;
/*ABCと表示したい*/

    
return 0 ;
}
コンパイル結果 -エラー0,警告0 コンパイル結果 -エラー0,警告0
実行結果 フフフフフフフフフフフフフフフフフフフフフ
「アプリケーションエラー」
実行結果 ABC

 リスト5のバグの原因は,C言語であつかう文字列には終端記号 '\0' が必要なことです。このバグの場合 char 型の配列内(buff)には意味のない値(ゴミ)が入っています。プログラマの都合よく '\0' で初期化されているわけではないのです。そして '\0' がない文字列では strcat() など文字列操作関数は正しく動作しません。
 これに対してリスト6のように,STLの std::string buff はちゃんと空の文字列として初期化されます。文字列領域は自動確保されますし領域確保を心配する必要はありません。

●バグその4「ランダム爆撃」

 リスト7については,初期化されていないポインタには意味のない値(ゴミ)のアドレスが入っています。その意味のない値(ゴミ)の指すアドレスに値を代入することは,プログラムやOSなどのどこかわからない領域に値を書き込む(私はこのことを「爆撃する」と呼んでいます)ことになります。この書き込み(爆撃)がたまたま無害な領域である場合もあります。その場合はすぐにバグの症状があらわれないため発見しにくいバグになってしまいます。

リスト7
バグその4「ランダム爆撃」
リスト8
バグその4改良版(STL)
#include <stdio.h> /* puts() */
#include <string.h> /* strcat() */

int main()
{
    
char * str ; /* 初期値なし */

    *str = 'A' ;
/* ランダム爆撃 warningC4700: */

    puts( str ) ;
/* A と表示したい */

    
return 0 ;
}
#include <stdio.h> // puts()
#include <string> // STL std::string

int main()
{
    std::string str ;
// 初期値なし

    str = 'A' ;

    puts( str.c_str() ) ;
// A と表示したい

    
return 0 ;
}
コンパイル結果 「warningC4700:値が割り当てられていないローカルな変数'str'に対して
参照が行われました」

-エラー0,警告1
※VC++6.0では,親切にwarningを出してくれる。
この警告(Warning)に気がつかずに実行してしまうと,
アプリケーションエラーとなります。
コンパイル結果 -エラー0,警告0
実行結果 「アプリケーションエラー」 実行結果 A

 このリスト7をSTLを使うように改良します。STLの std::string を使うことで str = 'A' ; の意味もプログラマの意図したとおりに(あまりこのような記述をする人はいないと思いますが)解釈されます。また,正しく初期化されていますし爆撃する心配もありません。
●バグその5「メモリリーク」

 ポインタを使っていて知らない間に作りこんでしまうバグの1つにメモリリークがあります。メモリリークとはmalloc()やnewなどヒープ領域から確保した領域を解放し忘れることを言います。メモリリークをしてもプログラム終了時にOSが解放してくれるので,大きな事故になりにくいのです。しかし,すぐに終了しないプログラムや常駐し続けるプログラムでは,メモリリソースを喰いつくしほかのプログラムやシステム全体に影響を及ぼす危険があります。よく「OSが解放してくれるのだからわざわざ解放しなくてもよい」という意見を耳にします。この意見もある意味では正しいと思います。しかし,解放しない癖をつけてしまうと常駐するタイプのプログラムを作ったときに大きな事故につながる可能性があります。こういう危険なクセはつけないようにしましょう。精神論かもしれませんが「借りたものは返す」これは常識です。

リスト9
バグその5「メモリリーク」
リスト10
バグその5改良版(STL)
#include <stdlib.h> /* malloc() */
#include <crtdbg.h> /* CrtDumpMemoryLeaks() */

int main()
{
    
int i = 0 ;

    
for ( ; i<3 ; ++i )
    {
        
/* 4Kbyte確保 */
        
int * tbl = (int*)malloc( sizeof(int) * 1024 ) ;
            ;
        
/* free(tbl);がない。*/
    }

    
/*メモリリークが検出されると,デバッグヒープ上の全メモリ
    
*ブロックをダンプします(デバッグ版のみ)。
    
*/
    _CrtDumpMemoryLeaks() ;

    
return 0 ;
}
#include <crtdbg.h> // CrtDumpMemoryLeaks()
#include <vector> // STL std::vector<>

int main()
{
    
int i = 0 ;

    
for ( ; i<3 ; ++i )
    {
        
//4Kbyte確保
        std::vector<
int > tbl( 1024 ) ;
            ;
        
// deleteやfree()する必要はない。
    }

    
/*メモリリークが検出されると,デバッグヒープ上の全メモリ
    
*ブロックをダンプします(デバッグ版のみ)。
    
*/
    _CrtDumpMemoryLeaks() ;

    
return 0 ;
}
コンパイル結果 -エラー0,警告0 コンパイル結果 -エラー0,警告0
実行結果 (VC++のデバッグ用出力に以下のメッセージが出力される)
Detectedmemoryleaks!
Dumpingobjects->
{41}normalblockat0x00344638,4096byteslong.
Data:<>CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD
{40}normalblockat0x003435F0,4096byteslong.
Data:<>CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD
{39}normalblockat0x003425A8,4096byteslong.
Data:<>CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD
Objectdumpcomplete.
実行結果 (なにも表示されず正常に終了する)

 リスト9は,メモリ領域の確保に malloc() を使用して,そのまま解放(free)せずに終了しています。プログラムの実行結果を見ても正常に動いているようにみえます。しかし,実際にはメモリリークしています。VC++6.0には _CrtDumpMemoryLeaks() ; というデバッグ用の関数が用意されています。これを呼ぶとメモリリークの状況がチェックされます。このバグの例ではVC++上にメモリがリークしたことを示すメッセージが表示されます。
 リスト10については,メモリ領域の確保にSTLの std::vector<> を使っています。std::vector<> を使うことで,malloc() や new と同様に動的なメモリ領域の確保ができます。また,std::vector<> のメモリ領域の解放はスコープ(有効範囲)が切れるところ(リスト10の14行目)でクラス内のデストラクタが実行され自動的に解放されます。このため free() や delete をする必要はありません。よって,メモリリークの心配もありません。

●バグその6「配列は引数にのらない」

 意外に知られていない事実なのですが,C言語では配列を関数の引数に渡すことはできません。関数の引数に配列を渡しているような記述はできますが実際にはポインタが渡ります。Cコンパイラではそのように解釈されるのです。以下の例は関数の引数に配列が渡るという勘違いから失敗してしまった例です。C言語では関数の引数に配列を記述しても,リスト11の場合,配列の記述はポインタとして解釈されます。これはC言語の変な言語仕様の1つであるといえます。例ではこのことにより引数にて渡された配列のサイズを表示しようとして失敗しています。引数in_tblが配列であれば

sizeof( in_tbl ) は 3

になるはずです。また,配列であればこの構文

in_tbl = "a" ; はエラー

になるはずです。さらに,VC++6.0のコンパイル結果ではたまたま動作していますが,実際には

printf( "'%c'\n", in_tbl[2] ) ;

では参照できないところを参照してしまっています。

リスト11
バグその6「配列は引数にのらない」
リスト12
バグその6改良版(STL)
#include <stdio.h> /* printf() */

void foo( char in_tbl[3] )
{
    
/* 配列のサイズを表示したい */
    printf( "tblsize:%d\n", sizeof(in_tbl) ) ;

    in_tbl = "a" ;
/* in_tbl[0] = 'a' の誤り*/

    printf( "'%c'", in_tbl[0] ) ;
/*'a'と表示したい*/
    printf( "'%c'", in_tbl[1] ) ;
/*'B'と表示したい*/
    printf( "'%c'\n", in_tbl[2] ) ;
/*'C'と表示したい*/
}

int main()
{
    
char abc_tbl[3] ;

    abc_tbl[0] = 'A' ;
    abc_tbl[1] = 'B' ;
    abc_tbl[2] = 'C' ;

    foo( abc_tbl ) ;

    
return 0 ;
}
#include <stdio.h> // printf()
#include <vector> // STL std::vector<>

void foo( std::vector< char > in_tbl )
{
    
//配列のサイズを表示
    printf( "tblsize:%d\n", in_tbl.size() ) ;

    
//in_tbl="a"; // in_tbl[0] = 'a' の誤り errorC2679
    in_tbl[0] = 'a' ;

    printf( "'%c'", in_tbl[0] ) ;
// 'a' と表示したい
    printf( "'%c'", in_tbl[1] ) ;
// 'B' と表示したい
    printf( "'%c'\n", in_tbl[2] ) ;
// 'C' と表示したい
}

int main()
{
    std::vector<
char > abc_tbl ;

    abc_tbl.push_back( 'A' ) ;
    abc_tbl.push_back( 'B' ) ;
    abc_tbl.push_back( 'C' ) ;

    foo( abc_tbl ) ;

    
return 0 ;
}
コンパイル結果 -エラー0,警告0 コンパイル結果 in_tbl="a";はちゃんとエラーになります。
「errorC2679:二項演算子'=':型'char[2]'の右オペランドを扱う演算子は定義されていません」
in_tbl[0] = 'a' ; であれば問題なくコンパイルできます。

-エラー0,警告0
実行結果 tblsize:4
'a'''''
実行結果 tblsize:3
'a''B''C'

 リスト11をSTLで改良したリスト12については,配列にSTLの std::vector<> を使い関数 foo() の引数に渡しています。in_tbl はポインタではなく std::vector<> 型(クラス)として認識されています。これにより配列のサイズ情報も同時に正しく渡っています。なお,このケースはコピー渡しですが参照(リファレンス)で渡したほうが効率のよい場合もあります。これは配列を関数 foo() の中で更新して返すのか,それとも更新はしないで foo() の中だけでの一時的な利用なのか,場合によって使い分けるとよいと思います。

●バグその7「スタック参照」

 最後に,厄介なバグといわれるスタック参照のバグです。VC++など警告してくれるコンパイラも増えてきたのでバグを作りこんでしまうケースも少なくなっているとは思います。しかし,このバグにハマってしまう人も少なくありません。なぜうまく動かないのかちゃんと理解してバグを撲滅しましょう。

リスト13
バグその7 「スタック参照」
リスト14
バグその7 改良版(STL)
#include <stdio.h> /* puts() */

char * foo()
{
    
char buff[] = "OK" ; /* スタック領域 */

    
return buff ; /* "OK" を返すwarning C4172 */
}

int main()
{
    
char * buff = foo() ;

    puts( buff ) ;
/* OK と表示したい*/

    
return 0 ;
}
#include <stdio.h> // puts()
#include <string> // STL std::string

std::string foo()
{
    std::string buff = "OK" ;
// スタック領域

    
return buff ; // "OK" を返す
}

int main()
{
    std::string buff = foo() ;

    puts( buff.c_str() ) ;
// OK と表示したい

    
return 0 ;
}
コンパイル結果 「warning C4172:ローカル変数またはテンポラリのアドレスを返します」
- エラー0 ,警告1
VC++6.0 では,親切にwarning を出してくれます。
この警告(warning)に気が付かずに実行しても正しく動作しません。
コンパイル結果 -エラー0,警告0
実行結果 (正しく表示されない) 実行結果 OK

 リスト13については,関数 foo() で返している値 "OK" はスタック領域(関数foo()内でのみ有効な一時的な領域)であるため,

puts( buff ) ;

の時点ではすでにその領域は破棄されて不定になっています。このコードの厄介なところはこのスタック領域はすぐに破棄されるわけではなくたいていの場合,値"OK"がそのまま残っているのです。そのため多くのケースで正常に動作していて,ほかの場所でスタック領域が使われた後にバグが出るという発見の難しいバグになりやすいのです。
 リスト13 を STL によって改良したリスト14 は,関数foo()の戻り値として std::string を返しています。関数foo()で返している値"OK"はスタック領域の std::string ですが戻り値として消える前に

std::string buff = foo() ;

に正しくコピーされます。
 また,このケースでは引数に参照(リファレンス)を使うことでポインタを使わずに値を返すことが可能になります。ポインタとの大きな違いは,参照では参照先が保証されているということです。たとえば

int * ptr ; // OK

初期値なしのポインタはエラーになりませんが

int & ref ; // ERROR

初期値なしの参照はエラーになります。参照型の参照先は必須条件なのです。ポインタではポインタの先にちゃんとデータが入っていることを期待してコードを書いているともいえるので安全性の面から見ても参照(リファレンス)の方が安全といえます。

リスト15
バグその7 参照で書き直した例
#include <stdio.h> // puts()
#include <string> // STL std::string

void foo( std::string &out_str ) // 参照で受けとる
{
    out_str = "OK" ;
// 参照先を更新
}

int main()
{
    std::string buff ;

    foo( buff ) ;

    puts( buff.c_str() ) ;
// OK と表示したい

    
return 0 ;
}
コンパイル結果 - エラー0 ,警告0
実行結果 OK

 リスト15 で,参照で書いた例を示します。これは,関数の引数を,参照(リファレンス)渡しにしているため関数への受け渡し時にコピーは作成されません。特に,大きなクラスを引数に乗せる場合は参照が有効といえます。また,関数内で参照先を上書きされたくない場合は,const (上書き禁止)をつけて,

void foo( const std::string & in_str )

とするとよいでしょう。const を書けば必ず上書きされない/値が保証されるというわけではなく,強引に型変換(キャスト)されれば上書きするコードは書けてしまいます。const は中身を更新する意思がないことを示しているレベルであると認識しましょう。そう書いてあるのだからそれを信じましょうということです。

void foo( const int i ) ;

と書いてあったら,

「i は変わらないよ。信じて」

と読みとればよいのです。本当にi が変わらないかどうかは foo() の中を見てみない限りわからないということです。

●これからの新しい世界

 このように,std::string, std::vector<>, 参照(リファレンス)を使うことで,ポインタを使わないコードが書けるようになります。実際には,ポインタは std::string や std::vector<> などのクラスの中に隠蔽されているだけで,ポインタそのものが消えたというわけではないのですが。また,ポインタを使わないことは,同時にポインタに関するバグから解放されることにもつながるともいえます。

 私はよく日本語の表現力というか言葉の持つ力はすごいと思うことがあります。今回,強くそう思った言葉は,

「適材適所」

です。この言葉の本来の意味は

「人には適切な仕事がある」

という意味ですが、私はこの言葉を

「モノを作るときにはそのモノにあった材料を選ぶべき」

 という意味であると解釈しています。たとえば,犬小屋を作るときに材料は何にするか,鉄,石,レンガ,プラスチック,紙,木,そのほかたくさんの材料の中から一番適したものを選択することでしょう。プログラムを作るときもプログラミング言語や環境を選ぶのはもちろんですが,もっと細かいレベルでも,どのアルゴリズムを使うかとか,どういう概念を適用するか(構造化設計で行くのか?, オブジェクト指向設計で行くのか?, ウォーターフォールモデルで開発するのか?, スパイラルモデルで開発するのか?, XPモデルで開発するのか?)など選択の幅はたくさんあると思います。今回はその材料として,C 言語ではポインタしか選択できなかったケースにおいても,C ++では,std::string,std::vrctor<>, 参照(リファレンス), ポインタと C言語より多くの選択肢の中から適切な材料を選択できるようになっています。今回は特にポインタについて「こう使ってはどうか?」という提案をしました。みなさんも,この機会にポインタの使い方について考えてみてはいかがでしょう。また,是非一度C ++を使って「ポインタなしのコード」に挑戦してみてください。きっと新しい世界が見えてくることと思います。

● COLUMN 「VC++6.0 の簡単な使い方」

 このV C ++6 .0 はかなり多機能です。起動するとわかると思いますが,ボタンの数やメニューの数は相当な数があり,初心者でなくても混乱し、使いこなすのは大変です。ここでは,そんなV C ++6 .0 を使って簡単にコンパイルする方法を説明します。

@ 作業場所の準備
 まず,フォルダを 1つ作ります。どんな名前でも結構です。わかりやすい名前で作成してください。

A 拡張子の変更
 そのフォルダの中にファイルを 1つ作ります。テキストファイルでよいのですが,その拡張子(テキストファイルでは.txt )を.cpp か.cxx に変更します。変更するには前もって「ツール」メニューから「フォルダオプション」ダイアログを開き「表示」タグ内の「登録されているファイルの拡張子は表示しない」のチェックをはずしておく必要があります。拡張子を変更すると確認のメッセージがでますが気にせず「O K 」を押します。

B Visaul C++の起動
 ここまでできたら,あとはこの .cpp ファイルをダブルクリックしてVC++6.0 を起動します。すると,白い画面が表示されますので,プログラムを入力します。

C コンパイル
 プログラムが入力し終わったら,コンパイルです。[F7]キーを押します。いろいろメッセージが出ますがO K です。正しくコンパイルできると「エラー0 警告0 」になります。

D ファイルの実行
 コンパイルができたら実行です。最初は実行 [Ctl]+[F5] がよいでしょう。[Ctl]+[F5] と [F5]の違いは [Ctl]+[F5] はブレイクポイントで停止しないで実行し,[F5] はブレイクポイントで停止する実行の違いがあります。ここまででコンパイルそして実行までできました。

E 次回作業は.dsw ファイルから
 最後に,VC++6.0 を終了すると,いろいろなファイルが作られています。いろいろな情報を覚えていますので,次回からは.dsw ファイルから起動しましょう。これで終わりです。たったこれだけで C++ のコンパイルができます。ぜひ一度お試しください。

● COLUMN 「std::stringとCStringの違いについて」

 文字列を実装するクラスとして std::string ではなくて CString を使っている人も多いと思います。CString は MFC(MicrosoftFoundationClass) で用意されたクラスです。std::string は STL で用意されたテンプレートクラスです。共に文字列を実装するためのクラスであり,使い勝手に関しても大きな違いがあるわけではありません。ただ1つ大きな違いがあります。それは,MFC はマイクロソフト社が開発したクラスライブラリであるのに対して STL は ANSI C++ にて標準化された「標準C++ライブラリ」であることです。これがどういうことなのかというと,CString はマイクロソフト社の提供する C++コンパイラ(たとえばVC++)ではないと使えないのに対して,std::string はANSIC++に準拠する C++ コンパイラであればどのコンパイラでも使える(もちろんVC++でも使える)ということなのです。開発環境や実行環境が開発途中で変わることはあまりありませんが,一度書いたソースコードを長い間使おうと考えたり,ほかの環境への移植を考えた場合,CString を使うより std::string を使ったほうがなにかとお得というわけです。このような理由から私は CString より std::string を使うことをお薦めします。

● COLUMN 「STLについて」

 STL はテンプレートライブラリです。テンプレートは C++ での比較的新しい概念ですが なかなか便利な機能です。しかし,構文はややこしいので,ソースコードが若干読みにくくなる傾向があります(個人的にはC++の構文はかなり危機的な状況にあると思います。複雑すぎて生産性を落としているともいえます)。「テンプレートを理解していないと STL は使えないのですか?」と問われる方もいますが,「そんなことはありません」。ただ,テンプレートを理解していればそれに越したことはないと思います。たとえば std::string や std::vector<> についてはテンプレートについて詳細まで熟知していなくても気軽に使うことができます。また,std::string や std::vector<> だけでも十分に使う価値があります。さらに,オブジェクト指向についても完全に理解していないとSTLは使えないというわけではありません。たしかにクラスライブラリの一種なのでオブジェクト指向の知識があればベストですが(継承とか多態性などの概念は出てきませんし)なくてもなんとかなるレベルだと思います。まずは堅苦しく考えずに気軽な気持ちで使ってみるとよいでしょう。
 STLの主な機能は,コンテナクラスとアルゴリズムの大きく2種類の機能を提供しているといえます。ほかにもイテレータや関数オブジェクトなどの少し難しい機能もありますが,これらはアルゴリズムを実装するための部品だと私は思います。アルゴリズムを使用しない限り特に必須というわけではないと思います。コンテナクラスとは std::vector<> などと同じ種類のクラスで std::set<>, std::map<>, std::list<> などのほかのクラス(型)をコンテナのようにまとめる機能を提供します。これらのコンテナクラスの概念はとても便利で使いやすくプログラミングの効率やソースコードの可読性を向上することができます(読みくだすことが楽だということ)。さらに私はバグを減らすことにも有効であると思っています。とにかくコンテナクラスはお薦めできる機能ですので最初はだまされたつもりで使ってみるとよいと思います。もう一方のアルゴリズムの機能ですが,概念としては理解しているつもりなのですが,いまひとつ私にはメリットが直感できないでいます。アルゴリズム機能のメリットについてはこれからゆっくり時間をかけて勉強していきたいと思います。そのようなわけで,みなさんはまず コンテナクラスからご利用ください。


last update 2003/02/04
since 2001/07/24



やまざきのおすすめエレクトロニクス


やまざきのおすすめ本

やまざきのおすすめDVD

やまざきのおすすめCD



Copyright(c) 1998-2006.
YAMAZAKI Satoshi.
All rights reserved.

since 1997/12/15


このページのURLをメールで送る(友人・知人に教えてあげる)
このページを「お気に入り」に追加する(忘れないように…)
● お手紙はこちら↓。仕事の話は大歓迎です。(忙しくて返信できなかったらごめんなさい。)