はじめに
本記事は、C言語を勉強してさあ次はC++をやるぞ、と意気込んだもののクラスの有難みがいまいち分からない、カプセル化を意識するのも面倒臭い、なんでメンバ関数経由でメンバ変数にアクセスしなければならないのか、というような方へ向けて書かれたものです。黒魔術師のみなさんにはあまり面白い内容ではないことを先にお断りさせていただきます。
クラスの前に、C言語でstring構造体をつくってみる
C言語では(C++でもですが)、文字列はポインタで示されるバイト列です。扱いが難しく、よく使う割に扱いづらくて致命的なミスも生みやすいという最悪のものですが、これを構造体とそれを操作するための関数群によって扱いやすくしましょう。
まずは基本となる構造体を定義します。文字列へのポインタと文字列の長さ、バッファの容量を保持するようにしました。
typedef struct tag_string {
char* c_str;
size_t length;
size_t capacity; /* NULL文字分は含まない */
} string;
次に、このstringを生成するための関数と、適切に破壊するための関数を作ります。
stringを使うときは、必ずこの関数を使って生成し、破壊してもらうようにします。
string create_string(const char* str, size_t capacity) {
string ret_val = {0};
ret_val.length = strlen(str);
ret_val.capacity = capacity;
if (ret_val.length > ret_val.capacity) { /* 指定された大きさのバッファでは足りない */
ret_val.capacity = ret_val.length;
}
ret_val.c_str = (char*) malloc(sizeof(char) * ret_val.capacity + sizeof(char)); /* 終端文字を考慮 */
memset(ret_val.c_str, 0, sizeof(char) * ret_val.capacity + sizeof(char));
strncpy(ret_val.c_str, str, ret_val.length);
return ret_val;
}
void destroy_string(string* p) {
if (p) {
free(p->c_str);
p->c_str = NULL;
p->length = p->capacity = 0;
}
}
エラーチェックは端折ってあります。create_stringにヌルポインタを渡した場合とmallocで領域を確保できなかった場合に未定義動作を引き起こしますのでこのままでは実用はしないでください。
mallocなどを使って文字列を生成するときは常に危険が付きまとっていますから、この関数は細心の注意を払って作る必要がありますね。(私もそのようにしたつもりですが、もしいけないところを発見されたらご指摘ください……)
せっかくですから使ってみましょう。必ずこのように生成と破壊をします。
int main(){
string str1 = create_string("abcdef", 6);
printf(str1.c_str);
destroy_string(&str1);
return 0;
}
少なくともこれだけでも価値あるものになったと思います。それでは、もっと便利に色々機能を追加してみましょう。
begin_string, end_string関数は文字列の最初と文字列の最後を返してくれる関数です。
面倒くさいコピーも文字列の比較も関数に押し込めてしまいます。
/* begin, end */
char* begin_string(string* str) {
return str->c_str;
}
char* end_string(string* str) {
return str->c_str + str->length; /* 終端文字を返す */
}
const char* cbegin_string(const string* str) {
return str->c_str;
}
const char* cend_string(const string* str) {
return str->c_str + str->length; /* 終端文字を返す */
}
/* copy */
void copy_string(string* s1, const string* s2) {
if (s1->capacity >= s2->length) {
strncpy(s1->c_str, s2->c_str, s2->length);
s1->length = s2->length;
memset(end_string(s1), 0, (s1->capacity - s1->length + 1) * sizeof(char));
return;
}
/* s1のバッファには入りきらない */
string ret_val = create_string(s2->c_str, s2->length);
destroy_string(s1);
*s1 = ret_val;
}
/* compare */
_Bool equal_string(const string* s1, const string* s2) {
return !strcmp(s1->c_str, s2->c_str);
}
つかってみる。
int main(void){
string str1 = create_string("abcdef", 6);
string str2 = create_string("abcdefg", 0);
string str3 = create_string("", 10);
copy_string(&str2, &str1);
copy_string(&str3, &str1);
printf("string: %s\nlength: %d\ncapacity:%d\n",
str2.c_str, str2.length, str2.capacity);
printf("string: %s\nlength: %d\ncapacity:%d\n",
str3.c_str, str3.length, str3.capacity);
if (equal_string(&str1, &str2)) {
printf("str1 == str2\n");
}
destroy_string(&str1);
destroy_string(&str2);
destroy_string(&str3);
return 0;
}
利用するときは、構造体のメンバには直接触らずに、必ずこれらの関数群を利用することが大切です。なぜなら、これら関数群はstring構造体がこれら関数群を使って生成されていることを前提として引数を扱っているからです。
例えば、lengthに適当な値を入れられたらこのstring構造体と関数群は途端にダメになってしまいます。ですが、それを防ぐ手立てはありません。使い手のモラルとマナーにすべては託されています。
そこでカプセル化という概念が出てくるのです。カプセル化は、string構造体に対してこれらの関数群を使うことを強制します。直接c_strやlength, capacityを弄ったりすることを禁止し、どんな使い方をされてもデータの整合性を保つようにすることを可能にするのです。
コードの全体を載せておきます。 http://ideone.com/0ChvuH
c++でstring構造体をstringクラスにかきなおしてみる
実際には、C言語で書く場合とほとんど変わりません。
まずは、基本となるクラスを定義します。中身はC言語のものと変わりません。
class string {
char* _c_str;
std::size_t _length;
std::size_t _capacity;
};
全てのメンバがプライベートなのでこのままでは使いようがありません。C言語では次に生成用と破壊用の関数を用意しましから、C++でも同様にしたいと思います。
ただし、生成用の関数はコンストラクタ、破壊用の関数はデストラクタとして書きます。C++では生成時にはコンストラクタが、オブジェクトの寿命に従ってデストラクタが自動で呼び出されるので、確実に生成用の関数と破壊用の関数を使わせることができます。
class string {
char* _c_str;
std::size_t _length;
std::size_t _capacity;
public:
string(const char* str, std::size_t capacity)
: _c_str(nullptr), _length(0), _capacity(0)
{
_length = strlen(str);
_capacity = capacity;
if (_length > _capacity) { // 指定された大きさのバッファでは足りない
_capacity = _length;
}
_c_str = reinterpret_cast< char* >(malloc(sizeof(char) * _capacity + sizeof(char))); // 終端文字を考慮
memset(_c_str, 0, sizeof(char) * _capacity + sizeof(char));
strncpy(_c_str, str, _length);
}
~string() {
free(_c_str);
_c_str = nullptr;
_length = _capacity = 0;
}
const char* c_str() const noexcept { return _c_str; }
std::size_t length() const noexcept { return _length; }
std::size_t capacity() const noexcept { return _capacity;}
};
C++のありがたいカプセル化の機能によりメンバ変数に外からはアクセスできないので、専用のメンバ関数を用意しました。
このメンバ関数は、外から値を得ることはできても、外から内部の値を変更することはできません。
ちなみに、()の後のconstは、呼び出してもオブジェクトの変更がないことを約束し、noexceptは例外を送出しないことを約束します。
早速使ってみましょう。コンストラクタには初期値となる文字列と、予め確保する容量capacityを指定していますが、C言語ではcapacityを外から自由に操作して内部状態を滅茶苦茶にすることができたのに対し、このクラスではそれは不可能になっています。
int main() {
string str("abcdef", 6);
printf("string:%s\nlength:%d\ncapacity:%d\n",
str.c_str(), str.length(), str.capacity());
}
先ほどと同じように、C言語で作った関数群をC++のクラスにも足してみましょう。
class string {
char* _c_str;
std::size_t _length;
std::size_t _capacity;
public:
string(const char* str, std::size_t capacity)
: _c_str(nullptr), _length(0), _capacity(0)
{
_length = strlen(str);
_capacity = capacity;
if (_length > _capacity) { // 指定された大きさのバッファでは足りない
_capacity = _length;
}
_c_str = reinterpret_cast< char* >(malloc(sizeof(char) * _capacity + sizeof(char))); // 終端文字を考慮
memset(_c_str, 0, sizeof(char) * _capacity + sizeof(char));
strncpy(_c_str, str, _length);
}
// copy
string(const string& rhs)
: string(rhs.c_str(), rhs.length()) {
}
string& operator= (const string& rhs) {
if (this->capacity() >= rhs.length()) {
strncpy(this->_c_str, rhs.c_str(), rhs.length());
this->_length = rhs.length();
memset(this->end(), 0, (this->capacity() - this->length() + 1) * sizeof(char));
return (*this);
}
// バッファには入りきらない場合
string new_val(rhs);
std::swap(this->_c_str, new_val._c_str);
std::swap(this->_length, new_val._length);
std::swap(this->_capacity, new_val._capacity);
return (*this);
}
~string() {
free(_c_str);
_c_str = nullptr;
_length = _capacity = 0;
}
const char* c_str() const noexcept { return _c_str; }
std::size_t length() const noexcept { return _length; }
std::size_t capacity() const noexcept { return _capacity;}
// begin, end
char* begin() { return _c_str; }
char* end() { return begin() + length(); } // 終端文字を返す
const char* begin() const { return _c_str; }
const char* end() const { return begin() + length(); } // 終端文字を返す
const char* cbegin() const { return begin(); }
const char* cend() const { return end(); } // 終端文字を返す
};
// compare
bool operator== (const string& s1, const string& s2) {
return !strcmp(s1.c_str(), s2.c_str());
}
bool operator!= (const string& s1, const string& s2) {
return !(s1 == s2);
}
copy_string関数は代入演算子になりました。これを使うと、より直観的にコピーを行うことができます。ただ、コピーコンストラクタ作ったおかげで宣言の順番が前後しています。
begin, end関数はconst修飾版とオーバーロードすることで数が一対増えています。
比較関数は、等価比較演算子になりました。これもより直観的な比較を行うことができるようにするためのものです。
使ってみましょう。
int main() {
string str1("abcdef", 6);
string str2("abcdefg", 0);
string str3("", 10);
str2 = str1; // 代入演算子
str3 = str1;
printf("string: %s\nlength: %d\ncapacity:%d\n",
str2.c_str(), str2.length(), str2.capacity());
printf("string: %s\nlength: %d\ncapacity:%d\n",
str3.c_str(), str3.length(), str3.capacity());
if (str1 == str2) { // 等価比較演算子
printf("str1 == str2\n");
}
}
コード全体はこちらです。 http://ideone.com/uKTJp9
このstringクラスを非常に高機能にしたものが、<string>ヘッダに入っているstd::basic_stringクラスです。本来なら自前で書かずにこのクラスを利用すべきところではありますが、今回は簡単なstringクラスを書くことで少しでもオブジェクト指向に納得がいったと思っていただけたらうれしいです。
C++のクラスは基本的には以上のような思想に基づいて設計されています。
クラスという概念ができてからは派生的な役割が増え、上の思想によらないクラスも多く存在します。しかしやれvirtualだ派生だなどという前に、上のような最も基本的な発想や概念、思想を知ることが大事だと考えこの記事を書くに至りました。ご意見などございましたらコメントをお寄せくださると幸いです。