プリント基板設計・シミュレーション

TOP > アポロレポート > コラム > 第44回 プログラミングについて『C++のクラスとオブジェクトの事始め』
コラム
2023/09/29

第44回 プログラミングについて『C++のクラスとオブジェクトの事始め』

アポロレポート

 今回は前回に続いてC++についてお話をしましょう。前回はC++の変なところをみて頂きましたが、今回はもう少し突っ込んで考えてみましょう。

 C++はオブジェクト指向言語です。これまでにC言語でオブジェクトを作成する方法を説明したことがありました。C言語ではデータと手続きを1つのソースファイルにして分割コンパイルを行なうことでそれを実現するものでしたが、勿論C++でもこの手法でオブジェクトを作ることはできますが、言語の機能上でさらに高度なオブジェクトを作ることができます。

 C++ではデータと手続きを1つにした型を作成することができます。この型のことをクラスと言い、C言語の構造体を拡張したようなものだと思って差し支えないと思います。構造体が構造体の型から実態を生成するように、オブジェクトもオブジェクトの型から実態を生成します。

 簡単な例をまず見てみましょう。下の例はオブジェクトの型の宣言(クラスの宣言)です。

 class test_class{
  private:
   int i;

  public:
   void put(int iv);
   int get();
 };

 構造体の宣言に非常に似ていますが、クラスの宣言の中には大きく分けてデータの宣言と手続きの宣言の2つがあります。まずここが構造体とは違うところです。勿論クラス内に構造体の宣言があっても構いません。上の例では、 int i; がデータの宣言です。また、void put(int iv); と int get(); が手続きの宣言です。ここで見慣れないものがあることに気付かれるとおもいます。private と public です。

 オブジェクト指向言語ではオブジェクトの内容、つまりデータと手続きをアクセスできる人を自分で決めることができることも重要な要素になっています。例えば、自分だけの秘密、友達との間での秘密、秘密でもなんでもないことと同じで、データや手続きについてもクラスでは規定することができるのです。自分だけの秘密を private、友達との間の秘密を friend、 秘密でもなんでもないものを public というキーワードになっています。この他に protrcted というものもありますがここでは話が面倒になりますので触れないことにします。それを踏まえて上の例を見てみると、データ i は自分だけの秘密で、関数 put と get は秘密でもなんでもないことになります。

 それではこのクラス宣言を使って実際にオブジェクトを作ってみましょう。

 #include <iostream.h>

 class test_class{
  private:
   int i;

  public:
   void put(int iv);
   int get();
 };

 void main()
 {
  test_class test;         -(1)

  test.put(123);          -(2)

  cout << test.get() << endl;   -(3)
 }

 void test_class::put(int iv)    -(4)
 {
  i=iv;
 }

 int test_class::get()
 {
  return i;
 }

 簡単な例ですが、始めてC++のソースコードを見た方にはかなり奇異に感じられるかもしれません。

(1) が test_class 型のオブジェクト test を作成しています。この記述方法は通常の変数や構造体の定義と同じです。(2) は test_class のパブリックのメンバー関数 putを使って test に 123 を代入しています。 (3) は同じく test_class のパブリックメンバー関数 get から得られた値を表示しています。オブジェクトのメンバー関数をアクセスするには構造体のメンバーのアクセスと同じで変数名にドットを付けてその関数名を指定します。関数であるので test.get() などのようにカッコは必要です。(4)が test_class のメンバー関数の定義です。クラス名の後に :: を付けてメンバー関数の名称を記述します。それ以外は通常の関数の定義と同じです。

 このプログラムを実行すると、

 123

と表示されます。特に変ったことは何も起きません。そこで、行(2)を次のように削除して実行してみましょう。

 void main()
 {
  test_class test;

  cout << test.get();
 }

すると、

 -2124489332

と訳の分からないものが表示されました。原因は推測がつくと思います。test は自動変数ですのでデータ i は初期化されていないからです。これも通常の自動変数と同じです。がしかし、オブジェクトには通常の変数にはない機能があります。それを次に説明します。

 C言語では変数を定義したときには明示的に初期化しない限り、それが静的な変数のときには0に、自動変数のときには値は不定となります。そのため変数は必ずどこかで初期化しなければなりません。変数の初期化を忘れてしまったためにバグが発生した経験は誰にでもあるはずです。オブジェクトにはそれが生成されたときと消滅するときに特別な関数が自動的に(これも定義しなければなりませんが)呼び出されるようにコンパイラによってコードが生成されるのです。この生成時に呼び出される関数をコンストラクタ、消滅時によびだされる関数をデストラクタと呼びます。コンストラクタは静的なオブジェクトのときにはプログラムの起動時に、動的な変数のときにはその変数が生成されるときに、デストラクタは静的なオブジェクトのときにはプログラムの終了時に、動的なときにはブロックの終了時に呼び出されます。
 このような特別な関数がオブジェクトの生成時と消滅時に自動的に呼び出されることで、オブジェクトの初期値を確実に設定し、終了時には不要になったメモリの開放などを確実に行なえるようになるのです。ただしその内容は責任をもって記述しなければなりませんので注意して下さい。
 それでは上の例にコンストラクタを組み込んでみましょう。クラスの宣言を次のようにします。

 class test_class{
  private:
   int i;

  public:
   test_class();
   void put(int iv);
   int get();
 };

 ただ単にパブリックなメンバーに test_class() を追加しただけなのですが、クラス名と同じパブリックなメンバー関数をコンパイラはコンストラクタとみなします。この場合のコンストラクタを、

 test_class::test_class()
 {
  i=0;
 }

としましょう。コンストラクタには関数の型はありませんので型は指定しません。改めてプログラム全体を書くと、

 #include <iostream.h>

 class test_class{
  private:
   int i;

  public:
   test_class();
   void put(int iv);
   int get();
 };

 void main()
 {
  test_class test;

  cout << test.get() << endl;
 }

 test_class::test_class()
 {
  i=0;
 }

 void test_class::put(int iv)
 {
  i=iv;
 }

 int test_class::get()
 {
  return i;
 }

となり、実行すると、

 0

が表示されますので正しく動作しています。コンストラクタは今紹介したもの一種類だけではなく、任意に色々と作ることができます。上の例ではオブジェクトが生成されたときには 0 をその初期値としましたが、いつも 0 が初期値であって欲しいとは限りませんので、生成と同時に初期値を指定できるようにしたいときがあります。そこで、クラスの宣言にもう1つコンストラクタを宣言しましょう。

 class test_class{
  private:
   int i;

  public:
   test_class();
   test_class(int iv);
   void put(int iv);
   int get();
 };

この test_class(int iv) がそれです。これにともなって次のようにプログラムを改修します。

 #include <iostream.h>

 class test_class{
  private:
   int i;

  public:
   test_class();
   test_class(int iv);
   void put(int iv);
   int get();
 };

 void main()
 {
  test_class test1;
  test_class test2(123);

  cout << test1.get() << endl;
  cout << test2.get() << endl;
 }

 test_class::test_class()
 {
  i=0;
 }

 test_class::test_class(int iv)
 {
  i=iv;
 }

 void test_class::put(int iv)
 {
  i=iv;
 }

 int test_class::get()
 {
  return i;
 }

 これを実行すると、

 0
 123

となります。この例で分かるように、コンストラクタは生成時の指定の方法によって適切なものが使用されるのです。これは以前お話した関数のオーバーロードが行われているのです。

 それではデストラクタはどのように記述すればいいのでしょうか。コンストラクタはオブジェクトの生成方法(引数の指定方法)によって複数個のものを用意しることができましたが、デストラクタは1つのみです。次のようにクラスの宣言に記述します。

 class test_class{
  private:
   int i;

  public:
   test_class();
   test_class(int iv);
   ~text_class();    <--- デストラクタ。
   void put(int iv);
   int get();
 };

 デストラクタはクラス名の先頭にチルドを付けた関数名で指定します。デストラクタの本体は、

 test_class::~test_class()
 {
 }

 の形式でこの中にオブジェクトの消滅時に行ないたいことを記述します。上の例では消滅時に特に行なうことがありませんので、なくても構いません。

 言い忘れましたが、クラスから生成されたオブジェクトをインスタンスと呼びます。クラスが印鑑とすればインスタンスは印影に相当します。そうなるとクラスが唇だったらワイシャツに付いたキスマークがインスタンスということか...

 今回はC++のオブジェクト指向の最も基本になる重要な部分の話をしました。多分C++の本を買ってきて勉強を始めたときには、新しい言葉の洪水で内容を理解する前に言葉を理解することの方が苦労するかも知れません。私も最初の頃は苦労しましたがこれも慣れてしまうまでの苦しみですのでくじけないで下さい。

 それではまた次回。


そのお悩み、
アポロ技研に話してみませんか?

アポロ技研でワンランク上の物創りへ!
そのお悩み、アポロ技研に話してみませんか?