はじめに
#include <string>
using namespace std;
int main()
{
string s = "abc";
}
C++には、言語の基本型に "string型" はありません。
上記のようなコードは、初期化式の左辺と右辺の関係を考えると不思議なところがあるのですが
JavaやC#、その他多数の言語の知識がある人が見ると
「どこが?」と思うのも無理はない気がします。
int i = 1;
これは、自然ですよね。int(整数)型のi
という変数に1
という整数を代入しています。
char c[] = { 'a', 'b', 'c', '\0' };
これも、自然です。char(文字)型の配列c
という変数に文字配列を代入しています。
const char* c = "abc";
これはある程度C/C++を理解している必要があります。
まず、文字列リテラルは文字型の配列になります。
そして、配列は先頭のアドレスのポインタで参照することになるので、char
型のポインタを使用します。
右辺は1つ前の例のような配列ではなく文字列リテラルになっており、中身を変更することができないものなので
ポインタもconst
修飾子が付きます。
実際には
char* c = (char*)"abc";
c[1] = 'B';
キャストを使って不変値ではないことにすることで、こういう事はできます。
(が、そもそも変更されないつもりで用意されたメモリに代入することになるので、やってはいけません。大変な事になります)
代入してみる
最初のコードは、標準C++ライブラリのヘッダ<string>
で定義されたstd::string
クラス(以降stringとします)が利用されています。
では、stringの代わりになるようなクラスを用意して、最初のコードと同じことをしてみましょう。
class MyString
{
};
int main()
{
MyString s = "abc"; // E0415
}
エラーになりました。何故でしょうか?
エラーコードはVisual C++のものです。
説明の都合上、次のように記述した場合を考えてみます。
class MyString
{
};
int main()
{
MyString s;
s = "abc"; // E0349
}
これが何故ダメなのかは、言うまでもなくMyString
型の変数にconst char*
型を代入しようとしているからです。
では何故stringを使うと上手くいくのでしょうか?
stringのソースコードは非常に複雑なので、C++の仕組みを使って解決手段を試みます。
演算子のオーバーロード
まず、左辺に対して異なる型の右辺から代入ができるということは、=
演算子がオーバーロードされているはずです。
演算子(オペレータ)のオーバーロードはC++だけでなくC#を始めとする様々な言語でもサポートされています。
Visual C++の説明が解りやすかったのでリンクを添付します。
class MyString
{
public:
// const char* 型の引数を代入する時
void operator=(const char* c)
{
const char* c_ = c;
}
};
int main()
{
MyString s;
s = "abc";
}
何の仕事もしないコードですが、これでコンパイルはできるようになりました。
では、最初にやろうとしたこちら
int main()
{
MyString s = "abc"; // エラー
s = "abc"; // OK
}
こちらは相変わらずコンパイルできません。何故でしょうか?
初期化はコンストラクタ
実は初期化時の代入はコンストラクタが呼び出されます。
即ち
int main()
{
MyString* s = new MyString("abc");
delete s;
}
ポインタ変数を使った場合にはこのような初期化式になるわけです。
そこで引数付きコンストラクタを実装します。
class MyString
{
public:
MyString(const char* c)
{
const char* c_ = c;
}
void operator=(const char* c)
{
const char* c_ = c;
}
};
int main()
{
MyString s = "abc";
s = "abc";
MyString* ps = new MyString("abc");
delete ps;
}
const char*
型の引数を受け取るコンストラクタを定義することで、全てのステートメントが解決しました。
(但し、このままでは何もしません)
折角なので
stringクラスと同じような機能ができるように実装してみます。
#include <cstddef>
#include <string.h>
using namespace std;
class MyString
{
private:
// 文字列記憶用配列(バッファ) ポインタ。
char* buffer = nullptr;
// バッファを初期化します。
void clearBuffer()
{
if (buffer) {
delete[] buffer;
buffer = nullptr;
}
}
// バッファを初期化し、内容を設定します。
void setBuffer(const char* text)
{
clearBuffer();
if (text) {
const size_t length = strlen(text);
buffer = new char[length + 1];
strcpy(buffer, text);
}
}
public:
// MyString オブジェクトを初期化します。
MyString()
{
}
// コピー コンストラクタ (引数に MyString が渡された場合の初期化)
MyString(const MyString& source) :
MyString(source.c_str())
{
}
// MyString オブジェクトを初期化します。
MyString(const char* text) :
MyString()
{
setBuffer(text);
}
// MyString オブジェクトを破棄します。
~MyString()
{
clearBuffer();
}
// const char* 型の代入
void operator=(const char* text)
{
setBuffer(text);
}
// const char* 型との比較
bool operator==(const char* text) const
{
return strcmp(buffer, text) == 0;
}
// インスタンス同士の比較
bool operator==(const MyString& text) const
{
return this->operator==(text.c_str());
}
// const char* 型との比較
bool operator!=(const char* text) const
{
return !this->operator==(text);
}
// インスタンス同士の比較
bool operator!=(const MyString& text) const
{
return this->operator!=(text.c_str());
}
// const char* 型との結合
MyString& operator+(const char* text)
{
char* newBuffer = new char[strlen(buffer) + strlen(text) + 1];
strcpy(newBuffer, buffer);
strcat(newBuffer, text);
clearBuffer();
buffer = newBuffer;
return *this;
}
// インスタンス同士の結合
MyString& operator+(const MyString& text)
{
this->operator+(text.c_str());
return *this;
}
// const char* 型との結合
void operator+=(const char* text)
{
this->operator+(text);
}
// インスタンス同士の結合
void operator+=(const MyString& text)
{
this->operator+(text.c_str());
}
// 現在のバッファ内容を文字配列のポインタとして返します。
const char* c_str() const
{
return buffer;
}
};
#include <iostream>
#include "MyString.hpp"
using namespace std;
int main()
{
MyString s = "Constructed.";
cout << s.c_str() << endl;
s = "Assigned.";
cout << s.c_str() << endl;
MyString t;
t = "Assigned.";
// オブジェクト同士の比較
if (t == s) {
cout << "Same." << endl;
}
// const char* との比較
if (s != "Constructed.") {
cout << "Different." << endl;
}
MyString u = "ABC";
// 結合
u = u + "DEF";
u += "GHI";
MyString v = "JKL";
u = u + v;
MyString w = "MNO";
u += w;
cout << u.c_str() << endl;
// ポインタ変数
MyString* ps = new MyString("Constructed with Pointer.");
cout << ps->c_str() << endl;
delete ps;
return 0;
}
str***のようなstring.hに定義された関数はバッファオーバーラン等の危険性があり
現在では推奨されていませんが、ここでは簡略化のため利用しています。
Visual C++でコンパイルする場合は、SDLチェックを無効にする必要があります。
実際にコーディングする場合は、strnlenやstrcpy_sのような、セキュリティ強化版の利用が推奨されています。
この実装は方法の1つとして紹介していますが、実際の標準C++ライブラリのstring クラスの実装とは異なります。
最後に
C++にstringという型はないという事が理解頂けたら幸いです。
ちなみにMFC(ATL)ではCString、C++BuilderではUnicodeString/AnsiString、QtではQStringという独自のクラスがあり
それぞれ使い勝手が異なります。