はじめに
C言語の文字操作のめんどくささを回避すべく、Stringクラスもどきを作ってみました。
Cでのクラス作成はオブジェクト指向言語と異なり、データの入れ物と、それを扱うライブラリを提供するという形になります。
以下、さっそくソースコードを見てみましょう。
ソースコード
#include <stdio.h> #include <string.h> /*************************************** * Stringクラス作成 ***************************************/ //Stringクラスに格納できる文字数 #define STRING_DATASIZE 15 //メンバ定義 typedef struct { int length; //文字数 int capacity; //格納できる文字数 char data[STRING_DATASIZE+1];//文字配列 } String; //コンストラクタ // メンバの初期化を行う String* Construct_String(String* this, char pChar[], int length){ memset(this, 0x00, sizeof(String)); if(length > STRING_DATASIZE) { printf("\nbuffer over error\n"); return NULL; } this->length = length; this->capacity = STRING_DATASIZE; memcpy(this->data, pChar, length); return this; } //メソッド1 append // 文字を後ろにつなげる String* String_append(String* this, String* pStr){ if((this->length + pStr->length) > this->capacity){ printf("\nbuffer over error\n"); return NULL; } memcpy((this->data + this->length), pStr->data, pStr->length); this->length += pStr->length; return this; } /** * コンストラクタマクロ * 文字配列の長さを取得し、Stringのコンストラクタを呼び出す * @string String * @arrChar char[] */ #define CNSTRUCT_STRING(string, arrChar)\ Construct_String(&string, arrChar, sizeof(arrChar)-1) /****************************************/ //使用例 void main(){ String str1; CNSTRUCT_STRING(str1, "hello"); printf("\n%d", str1.length); //5 printf("\n%s", str1.data); //hello String str2; CNSTRUCT_STRING(str2, " world!!"); String_append(&str1, &str2); printf("\n%d", str1.length); //13 printf("\n%s", str1.data); //hello world!! String str3; CNSTRUCT_STRING(str3, "hey"); String_append(&str1, &str3); //buffer over error printf(str1.data); //hello world!! }
以上です。
Stringというより、bufferという感じですね。
以下、順を追って見ていきたいと思います。
C言語の文字配列のめんどくささ
C言語で文字を扱おうとするといろいろとめんどくさいです。
- バッファオーバーフローしないか
- 終端文字の領域を確保しているか
- 終端文字が保証されているか
など、細かいところで注意しなければいけないことが多く、つい、
やっちまったぜ、、、
なんてことが非常に起きやすいです。
というわけでそんな細かいこと気にしなくていいStringクラスを作ってみたいと思いました。
目標は、以下の三点です。
- 文字配列のサイズを気にせず使える
(バッファオーバーフロー時は、実行時にエラーで教えてくれる) - 終端文字の領域を気にしないで使える
- 終端文字を保証する
これで簡単な単語連結程度を目指しました。
以下、少し詳しく見ていきます
C言語でクラスを作成する
やることは以下の三点です
- メンバ定義
- コンストラクタの作成
- メソッドの作成
メンバ定義
メンバを定義します。
細かい説明は、以下のソースコードを見てください。
//Stringクラスに格納できる文字数 #define STRING_DATASIZE 15 //メンバ定義 typedef struct { int length; //文字数 int capacity; //格納できる文字数 char data[STRING_DATASIZE+1];//文字配列 //終端文字領域を保証する } String;
ちなみに、C言語ではインスタンスメソッドを定義できません。
理由について詳しくは後述しますが、自分自身を操作するような動的なメソッドをうまく作れませんでした。
ちなみに、静的なメソッドの登録ならもちろんできます。が、まあメリットがrenameできるくらいしかないですね。
コンストラクタの作成
コンストラクタを作成します。
//コンストラクタ // メンバの初期化を行う String* Construct_String(String* this, char pChar[], int length){ memset(this, 0x00, sizeof(String)); //データ領域を0初期化する if(length > STRING_DATASIZE) { //エラー処理 printf("\nbuffer over error\n"); return NULL; } //メンバに初期値を設定する this->length = length; this->capacity = STRING_DATASIZE; memcpy(this->data, pChar, length); return this; } /** * コンストラクタマクロ * 文字配列の長さを取得し、Stringのコンストラクタを呼び出す * マクロにより文字配列サイズを意識せずにStringを使用できる * @string String * @arrChar char[] */ #define CNSTRUCT_STRING(string, arrChar)\ Construct_String(&string, arrChar, sizeof(arrChar)-1)
コンストラクタマクロの利点
C言語では、関数の引数に配列は渡せません。
強制的にポインタにキャストされてしまいます。
そのため、文字配列だけをコンストラクタ関数に渡すと、配列の長さが分からなくなってしまいます。
だからコンストラクタ引数に文字の長さがあるわけですが、、、はっきり言って入れるのめんどくさいですよね。
マクロを使うと、配列を扱うときのこういっためんどくささを回避できます。
関数呼び出しにならず、そのままコード展開されるため、sizeof()で配列のサイズが分かるんですね。
このコンストラクタマクロの使用により、文字配列のサイズを意識せずにStringコンストラクタが呼び出せるようになりました。
メソッドの作成
メソッドを作成します。
メソッドの第一引数には、対象のインスタンスを必ず渡します。
//メソッド1 append // 文字を後ろにつなげる String* String_append(String* this, String* pStr){ if((this->length + pStr->length) > this->capacity){ printf("\nbuffer over error\n"); return NULL; } memcpy((this->data + this->length), pStr->data, pStr->length); this->length += pStr->length; return this; }
インスタンスのメソッドを作りたい
本当は str1.append(str2); みたいに書きたかったんですが、無理でした。
書いていて気付いたんですが、C言語だと、thisが参照できないんですね。
関数コールをした時点で、どこからその関数が呼ばれたか(なんのインスタンスから呼ばれたか)なんていう情報は参照できなくなってしまいます(スタックを覗けばできるか??、、、だけどやだ)
なので、C言語でのメソッド定義は、ライブラリの提供という形になります。
そして、操作対象のインスタンスを、呼び出し時に注入するという使い方になります。
実際に使ってみる
void main(){ String str1; //スタックにデータ領域を確保する CNSTRUCT_STRING(str1, "hello"); //"hello"で初期化 printf("\n%d", str1.length); //5 printf("\n%s", str1.data); //hello String str2; CNSTRUCT_STRING(str2, " world!!"); String_append(&str1, &str2); //str2を末尾に追加 printf("\n%d", str1.length); //13 printf("\n%s", str1.data); //hello world!! String str3; CNSTRUCT_STRING(str3, "hey"); String_append(&str1, &str3); //buffer over error printf(str1.data); //hello world!! }
以上です
文字列というより、バッファという感じが強いですね。
ですが、最初の方で述べた以下の三点は気にせずに、気軽に文字を扱えるようになったのではないでしょうか。
- サイズを気にせず使える
(オーバーフロー時は、実行時にエラーで教えてくれる) - 終端文字の領域を気にしないで使える
- 終端文字を保証する
所感
今まで、プログラミング言語なんて、どれもそんなに変わんないじゃん、と思ってたんですが、Cでクラス作るの思ったより大変でした。
なんていうか、C++とかはコンパイラが多機能なんだなあ
コメント