C++ まとめ -言語編-

ページ内目録

オブジェクト指向プログラミング

オブジェクト指向プログラミング

ユーザ定義型

型とは,コンセプトを具体的に表現したものである.例えば算術演算型とそれが持つ+や-,*などの演算は,数学の実数概念の概要を具体化している.プログラマは「組み込みデータ型に直接の対応物を持たないコンセプト」を定義するために新しい型を定義できる.それらの型はユーザ定義型と呼ばれる.ユーザ定義型は次の4種類ある.

  • クラス
  • 構造体
  • 共用体
  • 列挙体
ここでは,クラスや構造体,共用体について説明し列挙体については,「オブジェクト」を参照されたし.

クラス

クラスとはユーザ定義型の1つである.C++のクラスの概念は,プログラマに組み込みデータ型と同じように便利に使える新しい型を作成するツールを提供することを目的としている.

クラスは,関数や変数,定数などの要素(メンバ)を一緒に保持する型である.あるクラスは別のクラスを作るための基にすることができる(継承).この時,基になったクラスを基底クラス,基にしたクラスを継承クラスと呼ぶ.プログラマは,クラスのメンバに対する継承クラスからのアクセスや生成されたオブジェクトからのアクセスを制限すること(アクセス制御)ができる.全てのクラスは,生成される際にコンストラクタと呼ばれる初期化関数のようなものによって,自動的に初期化される.そして,一部のクラスはオブジェクトが解体される際にデストラクタと呼ばれる関数のようなものによって,資源の後始末などを行う.

class-head 
{
  member-specificationopt
}
class-head
class-key
  • class
  • struct
  • union
補足
  • identifier名前(識別子)
  • member-specificationoptメンバに関する記述で,各メンバの定義やそのアクセス制御.
  • base-clauseopt継承をする際に使用.
  • nested-name-specifier完全修飾名
    • class-or-namespace-name :: nested-name-specifieropt
    • class-or-namespace-name ::template nested-name-specifier
  • class-or-namespace-nameは名前空間名かその別名,クラス名,template-idのいずれか.
  • template-idの詳細は「テンプレート」で説明する.
新しくクラス型(または構造体)の型を生成することは,クラス定義(class definition)と呼ばれる.また,歴史的経緯からクラス定義はクラス宣言(class declaration)と呼ばれることも多い.クラス定義は宣言と定義で示すODR(the one-definition rule)に従う(未完成-_-.).

また,Cとの互換性を保つために,クラスと同じ名前の非クラスは,同じスコープで宣言できる.

メンバ

クラス定義では,各メンバの定義やそのアクセス制御を行う.アクセス制御については後で説明する.文法については以下の通りである.

member-specification
  • member-declaration member-specificationopt
  • access-specifier : member-specificationopt

各メンバの定義のための文法は次のようになる.

member-declaration
  • decl-specifier-listopt member-declarator-listopt ;
  • function-definitionopt;
  • ::opt nested-name-specifier templateopt unqualified-id ;
  • using-declaration ;
  • template-declaration ;
member-declarator-list
  • member-declarator
  • member-declarator-list member-declarator
member-declarator
  • declarator pure-specifier
  • declarator constant-intializeropt
  • identifieropt : constant-expression
補足

C++のクラスの特性を理解するために,これから日付の概念を構造体といくつかの関数によって実装する例を考える.


struct Date { 
  int d, m, y; // dは
};
void init_date(Date& d, int dd, int mm, int yy);	// dを第2-4引数を使って初期化
void add_year(Date& d, int n);			// d.yにn年を加算
void add_math(Date& d, int n);			// d.mにn年を加算
void add_day(Date& d, int n);			// d.dにn年を加算
この例では,型(構造体のメンバ)といくつかの関数には,明示的な繋がりがない.このような結合関係は,関数をメンバとして宣言することによって確立される.

struct Date { 
  int d, m, y;		// dは日,mは月,yは年の情報を保持

  void init(int dd, int mm, int yy);	 // dとm,yを第1-3引数を使って初期化
  void add_year(int n);			 // yにn年を加算
  void add_math(int n);			 // mにn年を加算
  void add_day(int n);			 // dにn年を加算
};
クラス定義の中で宣言された関数はメンバ関数と呼ばれる.あるクラスから生成されたオブジェクトのメンバ関数は,構造体メンバアクセスの演算子(ドット演算子やアロー演算子)を使って適切な型の特定の変数だけを対象として呼び出すことができる.メンバの名前は,クラススコープに属する. そのためメンバ関数をクラスのブロックの外で定義する際に,スコープ演算子を使用した完全修飾名を使用する必要がある.例えば,前述のinit関数を定義するためのは次のように書く.

void Date::init(int dd, int mm, int yy)
{
  d = dd;
  m = mm;
  y = yy;
  return;
}
クラスの中やメンバ関数の中では,各メンバはスコープ演算子なしで名前を指定できる.

演算子の多重定義

C++のプログラマは,後述する演算子トークンの演算子(すなわち+や&らは単項演算子と二項演算子の両方)を多重定義できる.この時,基の演算子と新たに定義する演算子の項の数は等しい(異なる場合はエラー).

operator
  • +
  • -
  • *
  • /
  • %
  • ^
  • &
  • |
  • ~
  • !
  • =
  • <
  • >
  • +=
  • -=
  • *=
  • /=
  • %=
  • ^=
  • &=
  • |=
  • <<
  • >>
  • >>=
  • <<=
  • ==
  • !=
  • <=
  • >=
  • &&
  • ||
  • ++
  • --
  • ->*
  • ,
  • ->
  • []
  • ()
  • new
  • new[]
  • delete
  • delete[]
演算子の定義は関数定義と同じように行われ,その関数は演算子関数と呼ぶ. 演算子関数は,メンバ関数であるか少なくとも1つのユーザ定義型引数を取らなければならない(newとdeleteを除く).
function-definition
  • decl-specifier-listopt declarator ctor-initializeropt function-body
  • decl-specifier-listopt declarator try ctor-initializeropt function-try-body handler-list
補足
  • declarator の詳細は後述するが,演算子の多重定義にはoperator-function-id を指定.
  • ctor-initializerはメンバ初期化リストを使用する際に記述.
  • function-bodyは複合文.

declarator
  • direct-declarator
  • ptr-operator declarator
direct-declarator
  • ::opt operator-function-id
  • direct-declarator ( parameter-declaration-clause ) cv-qualifier-listopt exception-specificationopt
  • direct-declarator [ constant-expressionopt ]
  • (declarator )
operator-function-id
  • operator operator
補足
演算子の多重定義のために,関数を定義する.この時,その関数のことを演算子関数と呼ぶ.演算子関数は直接呼び出さすことは可能だが,普通は演算子として機能を果たすために実行される.これは,+の単行演算子の多重定義の例である.

struct T { int x; };
void operator+(T v){ std::cout << "hoge\n"; return;}
int main()
{
  T t;
  operator+(t); // ok
  +t;		// ok
  return 0;
}
T型に対する+の単項演算子は返り値はなしで,副作用で"hoge\n"を出力する.

演算子関数の定義は,演算子に応じて決まった方法がある.静的でないメンバ関数としての定義の場合,演算子関数の引数の数は項の数-1,非メンバ関数の場合,演算子関数の引数の数は項の数に等しい.以下にいくつかの定義の例を示す(Xはクラス,@は演算子トークンを表す).

前置の単項演算子

引数を取らない静的でないメンバ関数,あるいは1つの引数を取る非メンバ関数として宣言できる.

静的でないメンバ

class T
{
  T operator@(){}
};
非メンバ関数

T operator@(T t){}


二項演算子

1つの引数を取る静的でないメンバ関数,または2つの引数を取る非メンバ関数のどちらとして宣言しても良い.

静的でないメンバ

class T
{
  T operator@(T t2){}
};
非メンバ関数

T operator@(T t1, T t2){}

代入演算子と複合代入演算

静的でないメンバ関数でなければならない.そしてそれは継承されず,プログラマがoperator=を定義しないならば,標準ではそのクラスのメンバのメンバ毎の代入として定義される.

関数呼び出し

静的でないメンバ関数でなければならない.()演算子は1つ目の項に一次式,2つ目の項に0以上の引数列を持つ二項演算子とみなす.そのメンバ関数の引数は,引数列の引数の数と同じ.


struct T
{
  void operator()(int arg1, int arg2){ std::cout << arg1 << ", " << arg2 << std::endl; return;}
};
int main()
{
  T t;
  t(1, 1000);
  return 0;
}

添字付け

静的でないメンバ関数でなければならない.[]演算子は1つ目の項が,2つ目の項が添字の二項演算子として扱われる.

クラスメンバ・アクセス

単行演算子として考えられる.operator->はは静的でないメンバでなければならない.

インクリメントとデクリメント

1つの引数を取るoperator++やoperator--は前置のインクリメントやデクリメント,2つの引数を取るoperator++やoperator--は後置のインクリメントやデクリメントに対応する.後置の場合,第二引数は必ずint型でなければならず,演算子として呼び出された時第二引数には0が与えられる.後置の時は,1つの引数を取る静的でないメンバ関数,または2つの引数を取る非メンバ関数のどちらとして宣言しても良い.前置の時は,引数を取らない静的でないメンバ関数,または1つのint型の引数を取る非メンバ関数のどちらとして宣言しても良い.

C++では,新しい演算子トークンは定義できない.しかし処理内容自体は,関数呼び出し記法で代用できる.

コンストラクタ(構築子)

あるオブジェクトの初期化のために関数を用意して使用することは,初期設定を忘れたり2回行ったりというミスを行う要因を含む.そこでC++では,コンストラクタ(構築子)と呼ばれるオブジェクトが生成される際に自動的に呼び出される関数もどきが用意されている.

例えばHogeという構造体のコンストラクタは次のように定義できる.

struct Hoge{
  int x;
  Hoge();
};
Hoge::Hoge(){
  x = 10;
}
コンストラクタは関数と同様のルールに従って多重定義できる.

コンストラクタの本体にあるreturn文は,戻り値を指定できない. コンストラクタのアドレスは取得できない.

明示的なコンストラクタ

コンストラクタによるユーザ定義変換は,標準では暗黙的に呼び出される.しかし,explicitで修飾することで,それを回避できる.このようなコンストラクタをexplicitコンストラクタと呼ぶ.


class Hoge
{
public:
  int x;
  explicit Hoge(int i){ x = i;}
}
Hoge h = 0; // エラー
Hoge g = Hoge(0); // ok
コンストラクタによる型変換を明示的な呼び出しのみに限定することで,ミスによる代入を防ぐことができる.explicitは,コンストラクタのみを修飾できる.

デフォルトコンストラクタ

デフォルトコンストラクタとは,引数を指定せずに呼び出せるコンストラクタのことである.いくつかの組み込みデータ型もデフォルトコンストラクタを持つ.引数があるかどうかに関係なく,引数を省略可能なもの(デフォルト引数を持つ)を指す.


int x = int();

組み込みデータ型のデフォルトコンストラクタは,その型に変換した0(論理型のfalseや,文字型のNULL文字など)を返す.

プログラマがデフォルトコンストラクタを宣言している場合には,それが使用される.ただしデフォルトコンストラクタも他のコンストラクタも宣言しておらず,かつデフォルトコンストラクタが必要な場合は,コンパイラがデフォルトコンストラクタを生成しようとする.生成されたデフォルトコンストラクタは,基底クラス群のデフォルトコンストラクタを暗黙的に呼び出す.基底クラスとは,そのクラスを基にしたクラスらのことである.継承についての詳細は後述する.また,何らかの型のポインタのコンストラクタは構文エラーとなる.

初期化

基本的にクラスのメンバは定義順に初期設定される(定義でない宣言の順ではない).つまりソースコードの上に書いたもの下に書いたものよりも先に初期化される.同様に左に書いたものは右に書いたものより早い.

クラス型(および構造体)のオブジェクトは,コンストラクタによって初期化される.この時,明示的に指定しない限りデフォルトコンストラクタが呼び出される.また,組み込みデータ型のオブジェクトは,明示的に初期設定しない限り初期設定されない.この仕様はCに対する互換性を確保するためである.また,記号定数(const)と参照型は初期設定が必要なので,プログラマがそれらに対して明示的に初期設定をしない限り,それらの型のメンバを持つクラスは,デフォルトコンストラクタを生成できない.以下に例を示す.


struct X {
  const int a;
  const int& r;
};
X x; // エラー
デフォルトコンストラクタを持たない静的なオブジェクトは,初期設定をしない限りその型の0で初期化される.

基底クラスがコンストラクタを持つ時,派生クラスのコンストラクタの呼び出し前に呼び出される.

クラスが基底クラスを多重継承している場合,基底クラスのコンストラクタは宣言順に,最後に自身のコンストラクタが呼び出される.基底クラスの宣言順序は左から右の深さ優先探索が行われる.例えば次のようなコードがあったとする.


struct A { A(){std::cout << "A\n";} };
struct B : A { B(){std::cout << "B\n";} };
struct C : A { C(){std::cout << "C\n";} };
struct D { D(){std::cout << "D\n";} };
struct E : B, C, D { E(){std::cout << "E\n";} };
E e;
この時,コンストラクタは次のように呼び出される.
A → B → A → C → D → E

仮想基底クラスの場合,コンストラクタは1度だけしか呼び出されない.例えば,前述の例の構造体Aの継承を仮想基底クラスに変更したとする.


struct A { A(){std::cout << "A\n"; return;} };
struct B : A { B(){std::cout << "B\n"; return;} };
struct C : A { C(){std::cout << "C\n"; return;} };
struct D { D(){std::cout << "D\n"; return;} };
struct E : B, C, D { E(){std::cout << "E\n"; return;} };
E e;
この時,コンストラクタは次の順で呼び出される.
A → B → C → D → E

ちなみに破壊(デストラクタ)はこの逆順に行われる.

配列のコンストラクタの呼び出しは,下位アドレスから上位アドレスに向かう順序で行われる.

メンバ初期化リスト

メンバの初期化方法にメンバ初期化リストメンバイニシャライザと呼ばれるものを用いる方法がある. 次に示すオブジェクトメンバは,メンバ初期化リストでのみ初期化できる.

  • 静的でないデフォルトコンストラクタを持たない型のオブジェクトメンバ
  • 静的でない定数オブジェクトメンバ
  • 静的でないリファレンスオブジェクトメンバ
メンバ初期化リストは,明示的にメンバを初期化するためのコンストラクタを指定する.メンバ初期化リストで指定されたコンストラクタは,自身のコンストラクタの本文が実行されるよりも先に実行される.そして,その順番はメンバの宣言順によって行われる(つまり,メンバ初期化リストでの順番に左右されない).デフォルトコンストラクタを持つ型のオブジェクトメンバをデフォルトコンストラクタで初期化する場合,リストに書かなければ自動的にそれが呼び出される.これらのメンバのデストラクタの呼び出し順序は,初期化と逆順である.メンバ初期化リストには,コンストラクタが受け取った変数を使用できる.メンバ初期化リストの文法は次の通りである.
function-definition
  • decl-specifier-listopt declarator ctor-initializeropt function-body
  • decl-specifier-listopt declarator try ctor-initializeropt function-try-body handler-list
ctor-initializer
  • : mem-initializer-list
mem-initializer-list
  • mem-initializer
  • mem-initializer , mem-initializer-list
mem-initializer
  • mem-initializer-id (expression-listopt )
mem-initializer-id
  • ::opt nested-name-specifieropt class-name
  • identifier
補足
  • declarator 内のid-expressionにはクラス名が入る(コンストラクタだから).
以下に例を示す.

class Hoge
{
public:
  const int a = 10; // エラー  
  int y;
  const int x;
  Hoge(int n) : x(n), y(100) { std::cout << x << std::endl; }
  ~Hoge(){}
};
int main()
{
  Hoge h(1024);
  return 0;
}

デストラクタ

公開されていなければならない. デストラクタは,関数が通常通りに終了したか,例外が投げられたので終了したかに関わらず必ず呼び出される.

フレンド

クラスの非公開部は,通常外部からアクセスできない.しかしfriend宣言を用いることで,特定の関数やクラスからのアクセスが可能になる.このような関数をフレンド関数,クラスをフレンドクラスと呼ぶ.フレンドクラスやフレンド関数を持つクラスを基底とする継承クラスでは,それらの効果は反映されない.friendは,密接に結びついた概念を表現する場合にのみに使用する.

クラス内で宣言されたメンバ関数は,修飾子や宣言によっていくつかの性質を変更できる.

修飾子なしstaticfriend
非公開メンバへのアクセスを制限する.
クラススコープに属する.
オブジェクトを通してのみ通常のメンバ関数が実行できる.
staticはアクセスされる各メンバ関数に対して付けるが,friend宣言はその関数やメンバがクラス内のメンバに対してアクセスを許可するかを示すものである.
フレンド関数
フレンド関数はメンバ関数ではない.それゆえ,フレンド関数の定義は,グローバルスコープで行う.

class Hoge
{
private:
  int x;
 friend void foo();
public:
  Hoge(){ x = 100; }
  ~Hoge(){}
};

void foo()
{
  Hoge h;
  std::cout << h.x << std::endl;
  return;
}

int main()
{
  Hoge h;
  h.foo(); 	// エラー
  foo();	// ok
  return 0;
}
フレンドクラス

class Hoge
{
  friend class Fuga;
private:
  int x;
public:
  Hoge(){ x = 100; }
  ~Hoge(){}
};

class Fuga
{
public:
  Fuga(){
    Hoge h;
    std::cout << h.x << std::endl;
  }
  ~Fuga(){}
};

int main()
{
  Fuga f;
  std::cout << f.x << std::endl; // エラー
  return 0;
}
フレンドクラス(非公開派生の場合)

class Hoge
{
friend class Fuga;
void func(){std::cout << "hoge\n";}
};


class Fuga : private Hoge
{
public:
Fuga(){ func(); }
};

カプセル化

クラスの各メンバは,生成されたオブジェクトや継承されたクラスからアクセス可能かどうかなどを指定できる.オブジェクト内部のデータや振る舞い,実際の型を隠蔽する行為はカプセル化と呼ばれ,オブジェクト指向の設計方針における重要な概念の1つである.

指定にはpublicやprivate,protectedキーワードを使用する.メンバはアクセス制限によって次のように呼ばれる.

  • 公開(public)メンバ
  • 非公開(private)メンバ
  • 限定公開(protected)メンバ

オブジェクトの各メンバへのアクセス

クラスのオブジェクトの各メンバへのアクセスは,次の構文によって制御される.

access-specifier : member-specificationopt
access-specifier
  • private
  • protected
  • public
クラスの先頭か前述の3つのいずれかのキーワードをラベルとした行から次の同様のラベルまでのことは,それぞれprivateキーワードを使ったラベルから始まる文は,privateインタフェースと呼ばれる.他の2つについても同様の命名規則で呼ばれる(未完成-_-.).先頭から最初のアクセス制御のラベルまでのインタフェースは,classの場合にpublicインタフェース,structの場合にprivateインタフェースと定義されている.

実際の例を以下に示す.


class Hoge {
  int x;	// private
public:
  int y;	// public
  Hoge(){}	// public
  ~Hoge(){}	// public
};
メンバのアクセスを制限することは次のようなメリットがある.
  • 基底クラスのprivateメンバの名前の隠蔽ができる.
  • オブジェクトに無効な(意図しない)値の代入を防ぐことができる(例↓).
    
    class Year {
    private: // 省略可能
      unsigned int y;
    public:
      void change(int yy){ 	// changeを呼ぶことで値を変更できる
        if (1 <= yy && yy <= 12)
          y = yy;
        return;
      }
      int val(){		// valを呼ぶことで値を取得できる
        return y;
      }
      Date(int yy = 1){ change( yy ); } // コンストラクタ:引数がない時yearは1になる
    };
    
前述のアクセス制御によって次のものから各メンバに対するアクセスが可能に,または不可能になる.
非公開メンバ限定公開メンバ公開メンバ
他のメンバ関数
フレンド
派生クラスのメンバ関数×
クラスのオブジェクト××

コンストラクやデストラクタは,必ず公開されていなければならない.

また,C++が保護してくれるのは過失による不正アクセスのみであり,アドレス操作や型変換を用いた意図的な不正アクセスを検知できない.汎用言語の悪用からシステムを守れるのは,ハードウェアだけであり,現実のシステムではそれさえ厳しい.

基底クラスのアクセス制御

基底クラスのアクセス制御
  • access-specifier virtualopt ::opt nested-name-specifieropt class-name
  • access-specifieropt virtual ::opt nested-name-specifieropt class-name
補足
  • nested-name-specifierは完全修飾名.
  • class-nameは名前(識別子)かtemplate-id
  • virtualキーワードについての詳細は「仮想基底クラス」で.
派生の種類によって,基底クラスの公開メンバと限定公開メンバへのアクセスと,派生クラス型から基底クラス型へのポインタや参照型の変換を制御が行われる.基底クラスの非公開メンバについては派生の種類に関係なくフレンドと自身のクラスのメンバ関数でしかアクセスできない.また,ある派生クラスから非公開派生された基底クラスへの型変換はできない.
非公開派生限定公開派生公開派生
限定公開メンバ公開メンバ限定公開メンバ公開メンバ限定公開メンバ公開メンバ
派生クラスのメンバ関数
フレンド
派生クラスを基底とする派生クラスのメンバ関数××
任意の関数×××××
継承の際にアクセス指定子を省略した場合,派生クラスがstructの時に公開派生,派生クラスがclassの時に非公開派生になる.また,「派生クラスを基底とする派生クラスのメンバ」とは,途中にいくつの継承を挟んでもこのようになる.

多重継承(複数の基底クラスからの継承)である時,あるメンバに対するアクセス許可が1つでもあれば,そのメンバにアクセスできる.

また,アクセス指定子はいずれの場合も省略可能であるが,明示的に記述した方がソースコードは見やすくなる.

静的メンバ

static修飾子のつけて宣言されたメンバを静的メンバと呼ぶ.静的オブジェクトメンバの値は,クラスやそのオブジェクトで共有される(メモリ上の実体は常に1つだけしかない).静的メンバは,オブジェクトを生成しなくてもアクセス可能である(むろん公開部のみ).constでない静的メンバオブジェクトを宣言した場合,その定義はクラス定義の外側に記述しなければならない.constな静的メンバオブジェクトの場合,定数式の初期設定子によって初期設定できる.


class Hoge
{
public:
  static int x;
  const static int y = 10;
  Hoge(){}
  static void func(){ std::cout << "hoge\n"; }
};
int Hoge::x = 10;

int main()
{
  Hoge::func();
  return 0;
}

静的メンバは大域変数や名前空間内の変数と同様にmain関数の呼び出し前に生成と初期化が行われ,main関数が制御を返した後にデストラクタが呼び出される.ただし,ダイナミックリンクされるライブラリで定義された場合,生成や初期設定はリンクされた時である(関数内の静的変数は制御の流れが最初に通った時に初期化される).静的メンバや,グローバルスコープや名前空間スコープに属するオブジェクトは,ソースコード内の上の方に定義したものから順に初期化され,デストラクタはこの逆順に呼び出される.しかし複数のソースファイルを扱う場合,どのファイルを先に初期化するか,またはデストラクタを呼び出すかは言語処理系に依存する.

初期設定の文もデフォルトコンストラクタもない静的オブジェクトメンバは,その型の0(bool型ならfalse,char型ならnull文字といったぐらい)で初期化される.

定数メンバ

constで修飾されたメンバを定数メンバと呼ぶ.定数オブジェクトメンバは,複合代入を含む値の代入ができない(初期化は代入ではないことに注意).静的な定数オブジェクトメンバは右辺が定数式の初期設定子によって,静的でない定数オブジェクトメンバはメンバ初期化リストによって初期化できる.静的な定数オブジェクトメンバの初期設定を明示的に行わない場合は,値はその型の0になる.定数オブジェクトメンバは定数であるため,初期値が必須である.


class Hoge
{
public:
  const int a;		  // ok
  const int b = 2;	  // エラー
  static int c;		  // ok 初期値:0
  static const int d = 3; // ok
  Hoge() : a(1) {}
};

定数(const)メンバ関数

メンバ関数は,メンバの値を変更しないこと明示的にを表せる.

<未完成-_-.>
このような宣言を行って,メンバの値を変更するような記述を行った場合,コンパイラはエラーを返す.非定数メンバ関数は,定数メンバ関数と非定数メンバ関数からの呼び出しが可能であるが,定数メンバ関数は,定数メンバ関数からしか呼び出すことができない.以下にサンプルコードを示す.

class Hoge
{
public:
  int x;
  void change();
  {
    x = 100;
    return;
  }
  void func() const;
}

void Hoge::func() const
  {
    x = 10; // エラー
    change(); // エラー

    return;
  }

クラスでは,カプセル化によって見かけ上が不変であっても,実体が不変でない場合がある.例えば,あるメンバ関数が状態を記録する非公開オブジェクトを保持している場合である.このような場合,プログラマはその関数を論理的にメンバの値を書き換えないものとして扱うことができる.これを実装する例を2つ示す.

定数メンバ関数は,キャストによりconstを除去することで,実際にはメンバの値の書き換えができてしまうことがある.


class Hoge
{
private:
  int x;
public:
  void func() const
  {
    Hoge *p;    
    p = const_cast<Hoge*>(this);
    p->x 10;
    return;
  }
}
ただしこのような記述が可能かどうかは,コンパイラに依存する.

mutable

記憶クラス指定子mutableを付けて宣言されたメンバは,定数メンバ関数であっても書き換え可能である.


class Hoge
{
private:
  mutable int x;
public:
  void func() const
  {    
    x = 10;
    return;
  }
}

継承

クラス宣言の際,別のクラスの情報を引き継いで定義すること(継承)ができる.まず継承の文法を以下に示す.

base-clause
  • : base-specifier-list
base-specifier-list
  • base-specifier
  • base-specifier-list , base-specifier
base-specifier
  • ::opt nested-name-specifieropt class-name
  • access-specifier virtualopt ::opt nested-name-specifieropt class-name
  • access-specifieropt virtual ::opt nested-name-specifieropt class-name
補足
  • nested-name-specifierは完全修飾名.
  • class-nameは名前(識別子)かtemplate-id
    • template-idの詳細はテンプレートのところで.
  • access-specifierはアクセス制御.
それぞれのアクセス指定子に対応する派生の種類については,「カプセル化」
を参照されたし.非公開派生と限定公開派生は,一般的に実装の詳細を表現するために使用され,一般的には公開派生が用いられる.また,access-specifierを省略した時,派生クラスがstructであればpublicな継承,classであればprivateな継承が行われる.アクセス制御については,明示的に記述した方がソースコードが見やすくなる.

class Human
{
    
};
class John : Human
{

};
なお,派生クラスのオブジェクトのメモリの割り当て順序(派生クラスと基底クラスの順)は,言語処理系に依存する.

基底クラスのコンストラクタは明示的に呼び出せる. 派生クラスのコンストラクタが明示的にコンストラクタを呼び出さない場合, コンストラクタは,基底クラスから順番に呼び出される.

デストラクタは,派生クラスから順番に呼び出される. 派生クラスのデストラクタが明示的にデストラクタを呼び出させない.

宣言の一致 エラー: 名前の隠蔽によるエラー


#include 

class Hoge
{
public:
  int foo(char c){return 1;}
  Hoge(){}
  ~Hoge(){}
};

class Fuga : public Hoge
{
public:
  int foo(char *c){return 0;}
  Fuga(){}
  ~Fuga(){}
};


int main()
{
  Fuga* p = new Fuga();
  p->foo(1);
  return 0;
}

多様性(ポリモフィズム)

複数のクラスを定義する時,共通の目的で同じ動作を行う関数を基底クラスで定義し,異なる目的で異なる動作を行う関数を派生クラスで定義すれば,それらの関係を簡潔に表現できる.しかし特定の状況では,共通の目的で異なる動作を行う例がある.C++では,このような場合に関数を基底クラスで定義し派生クラスで上書き(オーバーライド)することで,表すことができる.これにより,同じインタフェースでオブジェクトに応じて異なる処理を行える.このような性質を多様性(ポリモフィズム)または多態性などと呼ぶ.

例えば「剣」,「槍」というクラスを定義しようとする.これらの共通の性質を「武器」というクラスにまとめ,独立する性質をそれぞれのクラスで表現することで,簡潔な構造となる.「攻撃」という動作は,全ての武器で共通する性質だが,その振る舞いはそれぞれの武器で異なる. このような場合,次のように表現することができる.


class Weapon
{
public:
  Weapon(){}
  ~Weapon(){}
  virtual void attack(){return;};
};

class Sord : public Weapon
{
public:
  Sord(){}
  ~Sord(){}
  void attack(){ std::cout << "剣の攻撃\n"; return;}
};

class Lance : public Weapon
{
public:
  Lance(){}
  ~Lance(){}
  void attack(){ std::cout << "槍の攻撃\n"; return;}
};

int main()
{
  Weapon *w = new Sord;
  w->attack(); // Sord::attack()が呼び出される.
  delete w;

  w = new Lance;
  w->attack(); // Lance::attack()が呼び出される.
  delete w;

  return 0;
}
コード内のWeapon*型のwは,オブジェクトの実体毎に異なる振る舞いをする. この時,プログラマは目的とそれに対応するインタフェースの使い方さえ知っていれば,異なるオブジェクトであっても同じように使用することができる.

多重定義

多重継承

C++では,クラスは複数の基底クラスを持つことができる.このような継承を多重継承という. また,

多重継承による曖昧さによるエラーの解決

名前の隠蔽
基底クラスが重複した場合(菱形継承)の例を示す.

class Hoge
{
public:
  int x;
  Hoge(){}
  ~Hoge(){}
};

class Fuga : public Hoge
{
public:
  Fuga(){x=10;}
  ~Fuga(){}
};


class Piyo : public Hoge
{
public:
  Piyo(){x=100;}
  ~Piyo(){}
};

class Foo : public Fuga, public Piyo
{
public:
  Foo(){x=1000;} // エラー:曖昧であるため
  ~Foo(){}
};
このような場合,派生クラスで同じ名前のメンバを宣言することで基底クラスの名前を隠蔽し,エラーを解決できる.

class Hoge
{
public:
  int x;
  Hoge(){}
  ~Hoge(){}
};

class Fuga : public Hoge
{
public:
  Fuga(){x=10;}
  ~Fuga(){}
};


class Piyo : public Hoge
{
public:
  Piyo(){x=100;}
  ~Piyo(){}
};

class Foo : public Fuga, public Piyo<
{
public:
  int x;
  Foo(){x=1000;}
  ~Foo(){}
};
上書き(オーバーライド)
複数の基底クラスのメンバ名の重複する例を示す.

class Hoge
{
public:
  Hoge(){}
  ~Hoge(){}
  virtual void func(){std::cout << "hoge\n";return;}
};

class Fuga
{
public:
  Fuga(){}
  ~Fuga(){}
  virtual void func(){std::cout << "fuga\n";return;}
};

class Piyo : public Hoge, public Fuga
{
public:
  Piyo(){}
  ~Piyo(){}
};

int main(){
  Piyo p;
  p.func(); // エラー:曖昧であるため
  return 0;
}
複数の基底クラスの仮想関数が重複する場合,単一の関数によって上書きできる.

class Hoge
{
public:
  Hoge(){}
  ~Hoge(){}
  virtual void func(){std::cout << "hoge\n";return;}
};

class Fuga
{
public:
  Fuga(){}
  ~Fuga(){}
  virtual void func(){std::cout << "fuga\n";return;}
};

class Piyo : public Hoge, public Fuga
{
public:
  Piyo(){}
  ~Piyo(){}
  void func(){std::cout << "piyo\n";return;}
};

int main(){
  Piyo p;
  p.func();
  return 0;
}
using宣言
複数の基底クラスのメンバ名の重複する例をもう1つ示す.

class Hoge
{
public:
  Hoge(){} 
  ~Hoge(){} 
  int foo(int i){ return i;}
};

class Fuga
{
public:
  Fuga(){}
  ~Fuga(){}
  float foo(float f){ return f;}
};

class Piyo : public Hoge, public Fuga
{
public
  Piyo(){}
  ~Piyo(){}
};

int main()
{
  Piyo p;  
  std::cout << p.foo(0.5) << std::endl; // エラー:曖昧であるため
  return 0;
}
このような場合は,名前の隠蔽やオーバーライドをしなくても解決可能である. 基底クラスの曖昧な関数は,派生クラスでusing宣言を行うことで曖昧性を解消できる.例えば,呼び出される関数をHoge::fooにしたいならばPiyoでusing宣言を行えば良い.

class Hoge
{
public:
  Hoge(){} 
  ~Hoge(){} 
  int foo(int i){ return i;}
};

class Fuga
{
public:
  Fuga(){}
  ~Fuga(){}
  float foo(float f){ return f;}
};

class Piyo : public Hoge, public Fuga
{
public
  Piyo(){}
  ~Piyo(){}
  using Hoge::foo;
};

int main()
{
  Piyo p;  
  std::cout << p.foo(0.5) << std::endl;
  return 0;
}
仮想基底クラス
菱形継承のエラーの例のように,基底クラスが重複する場合にデータを共有することでエラーを解決できる.詳細は「仮想基底クラス」で説明する.このデータの共有を指定する仕組みを仮想基底クラスという.

実際の実行結果だと 基底クラスのコンストラクタの順番 (宣言順 左から右)

デストラクタ (宣言順 左から右)の逆

仮想基底クラス

継承で基底クラスを指定する際に,virtualキーワードを指定できる.

base-specifier
  • access-specifier virtualopt ::opt nested-name-specifieropt class-name
  • access-specifieropt virtual ::opt nested-name-specifieropt class-name
この場合,基底クラスを仮想基底クラスという.仮想基底クラスは,共有を指定するための仕組みである. 菱形継承のエラー(基底クラスが重複)の場合,重複した名前のメンバを共有することで,エラーを解決できる.

class Hoge
{
public:
  int x;
  Hoge(){}
  ~Hoge(){}
};

class Fuga : virtual public Hoge
{
public:
  Fuga(){x=10;}
  ~Fuga(){}
};


class Piyo : virtual public Hoge
{
public:
  Piyo(){x=100;}
  ~Piyo(){}
};

class Foo : public Fuga, public Piyo
{
public:
  Foo(){x=1000;}
  ~Foo(){}
};

その他

thisポインタ

thisキーワードは,非静的メンバ関数の中で,メンバ関数の呼び出し対象のオブジェクトを指すポインタ.thisは変数ではない.それゆえ,thisのアドレスを変更することはできない.使用例を以下に示す.


class Hoge
{
public:
  int i;
  void func()
  {
    this->i;
    return;
  }
};

目録

キーワード

-緑の文字をクリック-