何となく Blog by Jitta
Microsoft .NET 考

目次

Blog 利用状況
  • 投稿数 - 761
  • 記事 - 18
  • コメント - 36162
  • トラックバック - 222
ニュース
  • IE7以前では、表示がおかしい。div の解釈に問題があるようだ。
    IE8の場合は、「互換」表示を OFF にしてください。
  • 検索エンジンで来られた方へ:
    お望みの情報は見つかりましたか? よろしければ、コメント欄にどのような情報を探していたのか、ご記入ください。
It's ME!
  • はなおか じった
  • 世界遺産の近くに住んでます。
  • Microsoft MVP for Visual Developer ASP/ASP.NET 10, 2004 - 9, 2011
広告

記事カテゴリ

書庫

日記カテゴリ

ギャラリ

その他

わんくま同盟

同郷

 

数年前、私がオブジェクト指向ってものがあって、どうやらこれから先はオブジェクト指向言語が流行るらしい、、、ということに気がついた頃。オブジェクト指向言語・・・ここでは C++ に限定して、C++ では、C に比べてオブジェクトを作る分実行速度が遅い。だから C でやる。。。という話が当たり前にありました。私の周りでは、ね。

ふむ。。。じゃぁ、その、「オブジェクトを作る」ってのは、どういうことなんだろう?

おそらく。そういっている人のほとんどは聞きかじりで、どういう意味なのか知らなかったんじゃないだろうか。


というわけで、ここから先は「今は C 言語を触っている。C++ に移行したいけど、なにがおいしいの?」って人向け。。。を目指す(-_-;

というか。知ってもらうのにどう説明すればいいか、そのたたき台です。実際には、絵を描いたりするんだろうな、と。


C でも C++ でもどちらでも、プログラムをする上でまず知らなければならないのは、「変数に値を格納する領域を確保する」ということ。これがわかってないと、SEGV 出しまくりになります(←この辺に、UNIXer の名残)。

変数の宣言の方法には2種類あります。必要な値を格納するのに十分な領域を確保して貰う方法と、領域は自分で確保するけど使うってことだけ宣言しておく方法です。この二つを単純にコードで表すと、次のようになります。

char a; // 必要な領域を確保
char* b; // 宣言だけする

この2つで、何が違うのでしょうか。変数 a は、char 型の値を格納することができます。変数 b は、char 型の値をしまう場所を格納することができます。ここで、「値」なのか「しまう場所」なのかの違いがあります。

「しまう場所」の場合、「それじゃ、どこにしまうの?」という疑問が生じます。しまう場所を指し示すことはできますが、しまう場所はまだ確保されていないのです。

しまう場所を確保するために、malloc 関数を使います。malloc 関数は、メモリ ヒープに指定された空き容量があるか調べ、ある場合は指定され量だけ使いますよとマークして、その場所を返します。また、十分な領域がない場合は、NULL を返します。このとき、Windows XP 以前では、以前に使用されていたままの値が入っています。Windows Vista では、malloc か、free のタイミングで、何らかの値で埋められるようです。今メンテしているアプリをデバッガでウォッチしていると、そのように振る舞っています。

ここでは、単純な基本型を扱いましたが、普通使用されるのは構造体でしょう。では、構造体を定義します。

とりあえずの構造体

typedef struct _chainList { struct _chainList* next; char* name; char* city; // 一応、市だけ入れるつもり char phone[14]; // xx-xxxx-xxxx, 0x0-xxxx-xxxx unsigned int age; } ChainList, *PChainList; ChainList list1; PChainList list2;

さて、このように宣言したとします。このとき、アドレス型と int 型は32ビット、4バイトとすると、4 + 4 + 4 + 14 + 4 = 30 バイトが、ChainList 構造体のサイズになります。

ブッブー!

一部の環境ではワード単位に境界をそろえます。そのような環境では 4 + 4 + 4 + 14 + 2 + 4 = 32 バイトとなります。2は境界をそろえるために追加された、使用されない(できないではない)領域です。

では、list2 に確保される領域はどれくらいでしょうか。たった4バイトです。なぜなら、list2 はしまう場所を示すように宣言されているからです。

次、使うとき。まずは、C をよく知らない人のコードから見てみましょう。

よく知らない人のコード例

for (int i = 0; i < count; i++) { PChainList next; next->name = テキトーなデータ[i].name; next->city = テキトーなデータ[i].city; strcpy(next->phone, テキトーなデータ[i].phone); next->age = テキトーなデータ[i].age; list2->next = next; }

C とかいいながら、一部 C++ な部分はスルーしていただくとして。「ありえねぇ~!」と思えた人。幸せですね。うらやましいです。こういう人に教えなければいけないから、このエントリを作っているのです。。。

添削するのもしんどいですが、まず、list2 および next。これらは「しまう場所を指定することを宣言しただけ」です。よって、しまう場所はまだできていません。よって、しまう場所を宣言します。

しまう場所を宣言

list2 = (PChainList) malloc(sizeof(ChainList)); for (int i = 0; i < count; i++) { PChainList next; next = (PChainList) malloc(sizeof(ChainList)); next->name = テキトーなデータ[i].name; next->city = テキトーなデータ[i].city; strcpy(next->phone, テキトーなデータ[i].phone); next->age = テキトーなデータ[i].age; list2->next = next; }

ところが、先に書いたように、十分な領域がない場合は、NULL が返ってきます。そのため、まずあり得ないことですが、万が一のために NULL かどうかのチェックをしなければなりません。

NULL チェックを追加

list2 = (PChainList) malloc(sizeof(ChainList)); if (list2 == NULL) { exit(-1); } for (int i = 0; i < count; i++) { PChainList next; next = (PChainList) malloc(sizeof(ChainList)); if (next == NULL) { exit(-1); } next->name = テキトーなデータ[i].name; next->city = テキトーなデータ[i].city; strcpy(next->phone, テキトーなデータ[i].phone); next->age = テキトーなデータ[i].age; list2->next = next; }

さて、malloc したら free しなければなりません。そうしないと、いつまでも領域を確保し続け、やがてメモリが足らなくなるからです。このように、確保した領域を適切に開放しないこと、解放する手段を用意していないために開放されない領域ができることを、メモリ リークといいます。

まぁ、このコードは大きなコードの中のほんの一部ですから、他のところで解放していることでしょう。

え?!だったら大変じゃない??

「テキトーなデータ」に注目です。このデータの、name や city は、どのように確保されたものなのでしょうか。もちろん、このコードからはそれが見えてきません。もしこのデータが、list2 の有効期間にわたってずっと有効なのであれば、このコードで問題はありません。しかし、list2 の生存中に解放されてしまったら?ChainList::name は、場所を指すように宣言されています。その場所が無くなってしまったら、どうなるでしょう?アプリケーションが異常終了するかどうかは環境に依存しますが、少なくとも使おうとしたときに変な値が入っていることがあり得ることは、確かです。

というわけで、name や city は、先に値が格納できるだけの領域を確保してから、その確保した領域へコピーします。

name と city を値コピーするように修正

list2 = (PChainList) malloc(sizeof(ChainList)); if (list2 == NULL) { exit(-1); } for (int i = 0; i < count; i++) { PChainList next; next = (PChainList) malloc(sizeof(ChainList)); if (next == NULL) { exit(-1); } next->name = (char*) malloc(strlen(テキトーなデータ[i].name) + 1); if (next->name == NULL) { exit(-1); } strcat(next->name, テキトーなデータ[i].name); next->city = (char*) malloc(strlen(テキトーなデータ[i].city) + 1); if (next->city == NULL) { exit(-1); } strcat(next->city, テキトーなデータ[i].city); strcpy(next->phone, テキトーなデータ[i].phone); next->age = テキトーなデータ[i].age; list2->next = next; }

なんか、わざとらしいですが、そういう突っ込みは無しでお願い。だって、知ってもらわないといけないことなんだから。

先ほど、malloc で確保した領域は、以前に使ったものがそのまま残っていると書きました。ところで、strcat は、第一引数で指定した文字列の後ろに、第二引数で指定する文字列を追加するものです。ということはですよ。malloc したままの name や city には、他の“文字”が残っている可能性があるんですよ!!と、いうことはですよ。malloc したままのアドレスに strcat すると、ちょうどの領域を用意したつもりで、はみ出してしまう可能性がある、ということです。

はい、とっとと修正しましょう。ついでに、主題と関係ないところも、こそっと修正します。

確保したら初期化する

list2 = (PChainList) malloc(sizeof(ChainList)); if (list2 == NULL) { exit(-1); } memset(list2, NULL, sizeof(ChainList)); for (int i = 0; i < count; i++) { PChainList next; next = (PChainList) malloc(sizeof(ChainList)); if (next == NULL) { exit(-1); } memset(next, NULL, sizeof(ChainList)); next->name = (char*) malloc(strlen(テキトーなデータ[i].name) + 1); if (next->name == NULL) { exit(-1); } memset(next->name, NULL, strlen(テキトーなデータ[i].name) + 1); strcat(next->name, テキトーなデータ[i].name); next->city = (char*) malloc(strlen(テキトーなデータ[i].city) + 1); if (next->city == NULL) { exit(-1); } memset(next->city, NULL, strlen(テキトーなデータ[i].city) + 1); strcat(next->city, テキトーなデータ[i].city); strcpy(next->phone, テキトーなデータ[i].phone); next->age = テキトーなデータ[i].age; next->next = list2; list2 = next; }

さて、これで一応動くでしょう。でも、なんか、冗長に感じないですか?メモリ確保して、NULL チェックして、初期化して。この3つ、必ずセットなのに、セットだから、必ず書かれます。

あ、ごめん。「一応動く」けど、解放処理を忘れてた。name や city は、この中で確保しています。また、next が指すアドレスも、解放するか、より上位につなげ直す必要があります。で、それって、毎回書くの?

もちろん、一つの方法は、関数化してしまうことです。こんな感じ。

解放処理

void FreeChainList(PChainList list) { while (list != NULL) { if (list->name != NULL) { free(list->name); } if (list->city != NULL) { free(list->city); } PChainList next = list->next; free(list); list = next; } }

ついでに allocate も

void* myalloc(unsigned int sz) { void* ret = malloc(sz); if (ret == NULL) { exit(-1); } memset(ret, NULL, sz); return ret; }

あれれ?myalloc、つまり、このために作った malloc です。こんなことくらい、ライブラリ化されていないの?

はい、されていません。初期化するのが、必ず NULL でしょうか?必ず exit していいのでしょうか?もちろん、NULL を返してもいいですが、その場合、NULL チェックが必ず2回されることになります。それって、冗長ですよね?

というわけで。ここで、やっと、オブジェクトが出てきます。長かった。

これを、C++ で書くと...なんか忘れてる。怪しいけど、勘弁。

C++ バージョン

public class ChainList { private string name; private string city; private string phone; private uint age; private ChainList* next; //アクセッサは省略 public ChainList(string n, string c, string p, int a, ChainList* x) { name = new string(n); city = new string(c); phone = new string(p); age = a; next = x; } public ~ChainList() { delete name; delete city; delete phone; delete next; } }

リストを作るところ

ChainList* list = NULL; for (int i = 0; i < count; i++) { ChainList* next = new ChainList( テキトーなデータ[i].name , テキトーなデータ[i].city , テキトーなデータ[i].phone , テキトーなデータ[i].age , list); list = next; }

なんてシンプル!!(アクセッサ省略したからな)あ、string は、STL だったかもしれない。

それで、最初の「C++ では、C に比べてオブジェクトを作る分実行速度が遅い」なのですが。今頃出てくるか!!はい、今頃です。予定外に時間をとりました。

よく考えてみてください。「オブジェクトを作る」というのは、malloc としたいことは同じです。ということは、「メモリを確保する」ということについては、実行速度はそう変わらないはずです。では、何が遅いのでしょうか。初期化処理です。コンストラクタが呼ばれる分、遅くなります。場合によっては memset で NULL クリアしていたものと同じような処理もされるので、その分遅くなります。でも、ちょっと待ってください。その分は、自分で書いているのではないでしょうか。

つまり、こんな風に言い換えることができないでしょうか。

C では、1から5までしかしない。6から10は自分で書く必要がある。C++ では、1から10までしてくれる。でも、自分で書かない6から10の分、遅く見える

「C++ では、C に比べてオブジェクトを作る分実行速度が遅い」というのは、実はこういうことではないでしょうか。



ということを、若葉君の C プログラム コードを添削しながら思ったのでした。

投稿日時 : 2007年7月26日 22:20
コメント
  • # re: オブジェクトって、おいしい?
    中博俊
    Posted @ 2007/07/27 9:19
    ぶっぶー
    8+8+14+2+8=40バイトになりまーす
    64bit環境だと
  • # re: オブジェクトって、おいしい?
    とっちゃん
    Posted @ 2007/07/27 11:09
    string は、文字列クラスなので、new は不要です。
    この場合は、C と同様、char* とするのがいいです。<説明上。
    ま、余計な部分を排除するという意味でですがw

    >C++ では、C に比べてオブジェクトを作る分実行速度が遅い

    えーっと、遅いというのは本気で迷信です。
    同じコードなら速度は変わりません。

    で、その昔C++が嫌われていたのは、同じプログラムがCより
    も大きくなる(ことが多い)から。
    これに例外の歴史的経緯(詳細はエピさんの昔話参照w)とかが
    からんで、結果遅いという迷信になります。

    いや、実際うちも15年近く前はそういうことがあったんで...w

    #仕方ないので、実証しましたよw
    #もちろん、当時のC-Maga(などの雑誌)片手にですがw
  • # re: オブジェクトって、おいしい?
    Jitta
    Posted @ 2007/07/27 12:20
    中さん
    だから32ビットと(^_^;)


    とっちゃんさん
    あ...便利なクラスが定義されてるょ、ってことで、string*でおながい
  • # re: オブジェクトって、おいしい?
    とっちゃん
    Posted @ 2007/07/27 14:14
    >便利なクラスが定義されてるょ
    であれば、string にして、
    new/delete はいらないですよ。
    string(STL のそれや、MFC/ATL のCString)は、C#のそれの基本となっている、文字列を1つのオブジェクトとして形成させるための手段です。

    なので、C++ でクラスを作るってときには昔からよく作成対象としてもあげられていたのですが...って論点ずれてるよw

    C++ きらーい!な人に論じていくのであれば
    もう一段階用意して(最後の最後でコンストラクタ内だけw)

    char* -> string という段取りを入れるのがおいら的にはいいのでは?と思いますよ。
  • # 答えは教えない。理解して欲しい。
    何となく Blog by Jitta
    Posted @ 2007/07/28 21:36
    答えは教えない。理解して欲しい。
  • # re: 答えは教えない。理解して欲しい。
    何となく Blog by Jitta
    Posted @ 2007/07/31 22:00
    re: 答えは教えない。理解して欲しい。
タイトル
名前
Url
コメント