今回は、JavaやC#でいうStringBuffer, StringBuilderにあたるモジュールを作成します。これはプロンプトから入力された文字を受け取るのに必要となります。buffer.h
, buffer.c
を見ていきます。
ソースコードはこちら
Cのファイルスコープ
皆さんはCのファイルをどのように分割していますか?Cのファイルスコープに着目して、1つのファイルを1つのモジュールないしクラスとみなしてみると可読性が向上するのではないでしょうか?
今回からの3回はモジュールを作ることに注力します。文字列バッファ、コンスバッファ、環境の3つはmy-lispで生み出したモジュールであり、それぞれのオブジェクトを生成・操作するものであります。従って、これらのファイルサイズは小さいですが意図的に分割されているものです。
buffer.h
/*
* buffer.h
*/
#ifndef BUFFER_H_
#define BUFFER_H_
typedef void * BUFFER;
BUFFER buffer_allocate(void);
void buffer_free(BUFFER buf);
void buffer_write_char(BUFFER buf, char c);
size_t buffer_get_size(BUFFER buf);
void buffer_copy(BUFFER buf, char *dst);
void buffer_clear(BUFFER buf);
#endif
このファイルではBUFFER
型が定義されています。しかしそれが何であるのかについては明かされていません。その実体はヘッダーではなくソースファイルに書いてあります。
buffer.c
/*
* buffer.c
*/
#include <stdlib.h>
#include <string.h>
#include "buffer.h"
#define BUFFER_SIZE 256
typedef struct tag_NODE {
struct tag_NODE *prev;
struct tag_NODE *next;
char body[BUFFER_SIZE];
} NODE;
typedef struct tag_HEADER {
NODE *start;
NODE *cur;
int index;
} HEADER;
その実体は256バイトの配列を持つNODE
型と、NODE
型にアクセスするためのHEADER
型からなります。NODE
型は双方向リストを構成するようになっています。実は現在公開されている機能だけだと単方向リストで問題ないことが分かるかと思います(あらかじめアナウンスしておきます)。
BUFFER buffer_allocate(void) {
HEADER *h = (HEADER *)malloc(sizeof(HEADER));
NODE *n = (NODE *)malloc(sizeof(NODE));
h->start = n;
h->cur = n;
h->index = 0;
n->prev = 0;
n->next = 0;
memset(n->body, 0, BUFFER_SIZE);
return (void *)h;
}
buffer_allocate
関数はHEADER
とNODE
を生成し、HEADER
を戻り値としてユーザーに渡しています。
void buffer_free(BUFFER buf) {
HEADER *h = (HEADER *)buf;
NODE *n = h->start;
while (n->next) {
n = n->next;
free(n->prev);
}
free(n);
free(h);
}
buffer_free
関数は生成したNODE
を全て解放し、HEADER
も解放します。ユーザーはbuffer_allocate
でBUFFER
を生成したら必ずbuffer_free
で解放する必要があります。my-lispでは必ずしもこれが守れていたわけではないのですが...
malloc
/free
の話が出てきたのでここで話しておきます。my-lisp2ではGC(ガーベジコレクタ)を実装しません。これは具体的にはmalloc
したメモリをfree
しないということを意味します。この実装が許されるかどうかは、実はOSに依存していて、デスクトップOSならば問題がないようです(あくまで経験則です)。組み込み系の場合はmalloc
したメモリをfree
しないと不具合を生じるようで、その対策は万全に行なっているようです。
とはいえmy-lisp2はmy-lispよりもエラー処理、終了処理を強化して、文字列バッファ、コンスバッファの解放はしっかり行う心積りです。
void buffer_write_char(BUFFER buf, char c) {
HEADER *h = (HEADER *)buf;
h->cur->body[h->index] = c;
h->index++;
if (h->index == BUFFER_SIZE) {
NODE *n = (NODE *)malloc(sizeof(NODE));
h->cur->next = n;
h->index = 0;
n->prev = h->cur;
n->next = 0;
h->cur = n;
memset(n->body, 0, BUFFER_SIZE);
}
}
buffer_write_char
関数はBUFFER
に1文字書き込みます。NODE
はそのサイズが256バイトと決められているので、必要に応じてNODE
を追加で生成します。この関数がそもそも文字の書き込みであって文字列の書き込みでない理由は、my-lisp2が最初にリーダーでストリームから1文字ずつ読み込むことに依ります。
size_t buffer_get_size(BUFFER buf) {
HEADER *h = (HEADER *)buf;
NODE *n = h->start;
size_t res = 0;
while (n != h->cur) {
res += BUFFER_SIZE;
n = n->next;
}
res += h->index;
return res;
}
buffer_get_size
関数は、BUFFER
に書き込まれた文字数を返します。
void buffer_copy(BUFFER buf, char *dst) {
HEADER *h = (HEADER *)buf;
NODE *n = h->start;
while (n != h->cur) {
memcpy(dst, n->body, BUFFER_SIZE);
dst += BUFFER_SIZE;
n = n->next;
}
memcpy(dst, n->body, h->index);
}
buffer_copy
関数は、BUFFER
から連続したメモリに文字列を書き出します。ただしこの関数がヌル終端処理を行なっていないことに気をつけてください。buffer_get_size
関数と組み合わせて使うことで安全にデータのコピーを行うことができます。
void buffer_clear(BUFFER buf) {
HEADER *h = (HEADER *)buf;
NODE *n = h->start;
while (n) {
memset(n->body, 0, BUFFER_SIZE);
n = n->next;
}
h->cur = h->start;
h->index = 0;
}
buffer_clear
関数はメモリの解放を行わずにBUFFER
を初期化します。BUFFER
は使い回すことが想定されています。