Java プログラマのための C++ メモ

言うほど大げさなものでは無いのですが、最近 C++ を触ってて思ったことをまとめていみました。数あるオブジェクト指向言語の中でもC++独特の仕様というのは結構あって、その中でも特に面倒だと思ったものを挙げます。単なる構文的な話題については触れません。

このエントリは、C言語(特にポインタ周り)とオブジェクト指向プログラミングの知識を仮定します。(というかそういう人にしか役に立たないと思う)

オブジェクトの生成 - 値とポインタ

C++ は newしなくてもオブジェクトが作れます。

string s;

等と宣言すると、その場でスタックに領域が確保され、コンストラクタが走ります。そしてスコープが切れると自動的に削除されます。
グローバルにオブジェクトを宣言したりすると、mainが走る前にコンストラクタが呼び出され、main終了後にデストラクタが呼ばれることになります。その中で例外を投げたりしても拾えません。
これがオブジェクトを「値として」宣言した場合の挙動です。

インスタンスの生成にはもうひとつ手段があって、動的に初期化し、スコープを越えて使いたい場合はポインタと new 演算子を使います。

string *s;
s = new string;
...
delete s;

こうすることでJavaの如く領域がヒープに確保されます。ただしGCが無いので、newされた領域はプログラマが責任を持って、プログラム内のどこかでdeleteする必要があります。

また、new 時にポインタに配列を割り付けることも可能です。その場合は delete ではなく delete[] を使います。

string *s;
s = new string[20];
for (int i=0; i<20; i++) {
  s[i] = ...;
}
...
delete[] s;

まぁ、大抵は配列使わずにvector使うのですが。

演算子オーバーロード

cin, cout のシフト演算子をどう思うかは人それぞれですが、最初に見た時は誰しも戸惑うのではないでしょうか。理解してしまえばなんということはないのですが、やはり本来の意味を変えて使うのはわかりにくいと思います。そういうことが「できてしまう」のがC++なので、プログラマの判断であまりにアレなものは自重していかないといけない気がします。

さて、C++ではクラスに対する演算子を関数として定義することができます。具体的にどう実装するかというと、「operator ○○」という関数を定義するのですが、演算子の性質によって宣言の仕方が変わってきます。

メンバ関数として実装
class Foo {
  int n;
public:
  virtual Foo &operator += (int m) {
    n += m;
  }
};

注意

  • キャストは返り値の型を宣言しない
    • 宣言の例:virtual operator int ();
  • 代入演算子はそのクラスのconstな実体を受け取り自分の実体を返す。
    • 宣言の例:Foo &operator= (const Foo &foo);
グローバルな関数として実装(+friendを指定)
class Foo {
  int n;
  friend Foo operator + (const Foo &a, const Foo &b) {
    Foo c;
    c.n = a.n + b.n;
    return c;
  }
};

friend

クラスを定義するとき、そのクラスの「友達」を宣言することができます。
友達として指定されるのは、他のクラスや関数です。
これは、「友達と認めた人たちには、自身のメンバ変数およびメンバ関数をprivateなものも含めて全て公開する」という大胆な宣言です。

なぜこういったものが必要なのかというと、クラス間の関連の強さには差があるからです。非常に密接な関係のある二つのクラスでは、相手クラスのコアなデータに頻繁にアクセスしたくなることがあります。かといってそのデータをpublicにしてしまうと、全てのクラスから触られてしまう危険性があります。そこで「特定のクラスに対してのみpublic」というアクセス制御が欲しくなるわけです。まぁ、それを提供する手段として「全てのメンバを公開」というのは大雑把すぎる気もしますが、C/C++って基本的にそういう「できることは増やすけど、それによって生じる問題の責任は言語ではなくプログラマが負う」みたいな性格をしてるのですよね。

批判されることが多い機能ですが、個人的にはそう悪いものでは無い気がします。もちろん濫用は厳禁ですし、こういった機能をつけるにしても他にやりようがあったんじゃないの?と思わないでもないですが、動機自体には賛成ですので。使い方を間違わなければ有用な機能の一つだと思います。

代入=オブジェクトのコピー

これが個人的に一番C++臭い点だと思います。というのも物理的なメモリがどうなっているかをイメージさせられるから。

オブジェクトを代入したり関数の引数に渡したりすると、他の言語では参照を渡すことが多いのですが、C++の場合は(デフォルトでは)メモリイメージのコピーになります。C言語において、構造体というのはスカラ以外で唯一まるごと代入できるものでした。それがオブジェクトにも拡張されているため、このような仕様になっています。

そしてそれゆえに起きる問題というのもあるわけで、それを以下に述べます。それが嫌な場合には、ポインタを使ったり引数に"&"をつけることで参照渡しとして関数を定義したりすることでこうした問題の多くが回避できます。効率の面からも安全の面からも、なるべくオブジェクトのコピーは避けるべきでしょう。

ポリモーフィズムが使えない?

クラスBarがクラスFooを継承しているとします。(Fooがスーパークラス・Barがサブクラス)
このとき、FooクラスのオブジェクトにBarクラスのオブジェクトを代入(コピー)すると、多態せず、BarとFooの差分が切り捨てられます(スライシング)。Fooへのキャストが起きている感じ。

Foo foo;
Bar bar;
...

foo = bar;   // 多態しない!!

というのも、ここでfooはすでに領域確保された実体です。そのメモリ領域にbarというさらに大きなバイト数を持つ実体をコピーしようとしたら当然無理が起こるわけです。

それではC++ではポリモーフィズムが使えないかというと勿論そんな筈はなくて、ポインタ(or参照)を使うことで実現されます。

Foo *foo;
Bar *bar;
bar = new Bar;
...
foo = bar;   // 多態する

このことからも、C++ではオブジェクトは基本的にポインタで扱ってnewとdeleteで管理する(orスマートポインタを使う)というのがオブジェクト指向的に真っ当なスタイルだと思います。

単純にコピーするとメモリ管理が大変になる

オブジェクトがコピーされると、メンバ変数に持っているポインタが指し示すアドレスもコピーされるわけです。つまり、「同じ領域を複数のポインタが指している」という状況ができます。これは中々面倒な状況です。というのも、自分が使い終わったと思って破棄した領域が、実はまだ他のインスタンスが使うものだったということになる可能性があるため、ポインタのdeleteをそのインスタンスの判断で行えないからです。

boost::shared_ptr なんかを使えばそのあたりの管理がだいぶ楽になるものの、循環参照してる時の問題なんかがあるため、単純にこれ使えば全部が解決するわけでもありません。

メモリ管理が面倒になるのが嫌なときは別の解決方法として、そもそも単純なメモリイメージのコピーというのをやめればいいという考え方があります。て、コピーが起きたときに、ポインタの指す場所をそのままコピーするのではなく、新たに領域を確保してそこにコピー元のポインタが指している内容をコピーするようにするわけです。

そのためには代入演算子やコピーコンストラクタを書き換えます。

class Foo {
  Bar *bar;
public:
  // 代入演算子
  Foo &operator=(const Foo &foo) {
    bar = new Bar;
    bar = *(foo.bar);
    return *this;
  }
  // コピーコンストラクタ
  Foo(const Foo &foo) {
    bar = new Bar;
    bar = *(foo.bar);
  }
  // デストラクタ
  virtual ~Foo() {
    delete bar;
  }
};

終わりに。

なんとなく書き散らしてみましたが・・・自分で見ても分かりづらいですね...。いつか書き直したい。

思うに、C++の理念である「Cとの互換性に拘ること」および「実行効率を重視すること」でオブジェクト指向言語としてのあるべき姿から歪められている部分が多いです。それがC++の悪いところであり、同時に他の言語と一線を画すC++らしいところでもあります。その辺りが分かってくると、うっかり愛着が湧いちゃったりなんかしたりして。

Web上にもこういった趣旨の文章が散らばっているので、いろいろ見てればC++というものがなんとなく分かってくるんじゃないでしょーか。