数年前、私がオブジェクト指向ってものがあって、どうやらこれから先はオブジェクト指向言語が流行るらしい、、、ということに気がついた頃。オブジェクト指向言語・・・ここでは 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 かどうかのチェックをしなければなりません。
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 は、先に値が格納できるだけの領域を確保してから、その確保した領域へコピーします。
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;
}
}
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++ で書くと...なんか忘れてる。怪しいけど、勘弁。
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