見出し画像

c++の勉強メモ|c++応用編

参照渡し

c言語ではできなくてc++でできることのひとつが参照渡し。ポインタ渡しの他にも参照渡しがある。

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 void ref(int&);
 6 void print(int);
 7 
 8 int main() {
 9    // 整数の値を代入
10    int n = 5;
11    print(n);
12    // 参照渡し
13    ref(n);
14    print(n);
15    return 0;
16  }
17  
18  void ref(int& n) {
19    n = 1;
20  }
21  void print(int n) {
22    cout << "n=" << n << endl;
23  }

処理の流れ

13行目のref(n)の時点ではn = 5

ステップインで18行目のvoid ref(int& n)関数に入る。

入った時点のnの値とアドレス
n = 5 
&n= 0x0027fc01{5}

19行目のn = 1を実行したnの値とアドレス
n= 1
&n = 0x0027fc01{1}

ref関数に渡すint& nによって、nのアドレスが参照される。
n = 5のアドレスが参照されるので、アドレスは同じで、値だけ1に書き換わる。


ローカル変数

c++では好きなところで変数の宣言ができる。構造体やクラスの中にいっぱい変数を宣言するとメモリを消費するので、途中で宣言できると便利。


クラス間の相互参照

クラスAとクラスBが互いに参照しあっている場合

Aのヘッダーファイルにclass B と宣言を書き、
Bのヘッダーファイルにclass A と宣言を書くことで参照できる。

A、Bのヘッダーファイルでお互いにincludeでクラスA、クラスBを読み込んでしまうと、コンパイラが循環して終わらないことになり、エラーが発生する。

→A、Bの.cppファイルで必要なクラスをそれぞれincludeする。なので、A.cpp、B.cppにそれぞれ下記を書く。
#include "A.h"
#include "B.h"


main.cpp

#include <iostream>
#include "/home/amaenbo/c++/header/A.h"
#include "/home/amaenbo/c++/header/B.h"
using namespace std;
int main() {
	A a;
	a.foo();
	a.bar();
	return 0;
}

A.h

#ifndef _A_H_
#define _A_H_
class B;	// クラスBへの参照
class A {
	private:
		B * m_pB;
	public:
		A();	// コンストラクタ
		virtual ~A();	// デストラクタ
		void foo(); 
		void bar(); 
};
#endif // _A_H_

B.h

#ifndef _B_H_
#define _B_H_
class A;	// クラスAへの参照        
class B {
	private:
		A * m_pA;
	public:
		B(A* pA);	// コンストラクタ
		virtual ~B();	// デストラクタ
		void hoge(); 
};
#endif // _B_H_

A.cpp

#include "/home/amaenbo/c++/header/A.h"
#include "/home/amaenbo/c++/header/B.h"
#include <iostream>
using namespace std;
A::A() {
	m_pB = new B(this);
}
A::~A() {
	delete m_pB;
}
void A::foo() {
	cout << "foo" << endl;
}
void A::bar() {
 m_pB->hoge();
}

B.cpp

#include "/home/amaenbo/c++/header/A.h"
#include "/home/amaenbo/c++/header/B.h"
#include <iostream>
using namespace std;
B::B(A* pA) {
	m_pA = pA;
}
B::~B() {
}
void B::hoge() {
	cout << "bar" << endl;
	m_pA->foo();
}


相互参照の処理の流れ

main.cppのA a;が実行され、メイン関数の中にAのインスタンスaが生成される。

概要

→A.cppのA::A()コンストラクタが実行され、コンストラクタ内に書いている処理によってm_pBを使ってm_pB->hoge()のようにしてクラスBのメンバ関数がつかえるようになる。


動作の流れ

コンストラクタAの働きによって、クラスAのインスタンスが生成される。インスタンスAのアドレスは0x003afaa0。

Aコンストラクタの中では、new BとすることでクラスBのコンストラクタを実行し、クラスBのインスタンスが生成される。インスタンスBが入っているアドレスは0x0071d7a0 。インスタンスBのアドレスをm_pBに代入することで、クラスAの中でクラスBのメンバ関数をm_pB->hoge()のようにして使うことができるようになる。

コンストラクタBでは、インスタンスAのアドレス0x003afaa0がthisを通じてpAで参照され、m_pAに代入されることで、クラスBの中でインスタンスAをm_pA->foo()のように使えるようになる。

以上から、クラスAからクラスBを使え、クラスBからクラスAが使える状態となる。


修飾子const

パラメーターの値が変わらないことを保証したいときにconst修飾子をつける。インスタンスを引数に渡した場合、渡したインスタンスのメンバ変数などの値に変化がないかといった問題がある。その際の引数の状態が変わらないことを保証するのがconst.

constをメンバ変数、メンバ関数、メンバ関数の引数につけることができる。

メンバ変数、メンバ関数の引数には前にconstを付ける。

メンバ関数には後ろにconstを付けるというルールがある。

メンバ関数につけた場合、メンバ関数のインスタンスのメンバ変数は変化しないことを保証できる。

const修飾子の効果
constをつけるとコンパイラが最適化をしやすくなって処理速度が向上したり、メモリの使用が効率的になったりする効果がある。

また、constをつけると変数の使用方法に制限がつくので、プログラミングの間違いを防止できる効果もある。

main.cpp

#include <iostream>
#include <string>
#include "/home/amaenbo/c++/header/sample.h"
using namespace std;
int main() {
  CSample s;
  cout << "定数" << s.m_cst << endl;
  s.setStr("ABC");
  cout << s.getStr() << endl;
  return 0;
}


sample.h

#ifndef _SAMPLE_H_
#define _SAMPLE_H_
#include <iostream>
#include <string>
using namespace std;
class CSsample {
 private:
   string m_str;
 public:
   CSample();
   void setStr(const string str);
   string getStr() const;
 public:
   static const int m_cst = 100;
};
#endif //_SAMPLE_H_

sample.cpp

#include "/home/amaenbo/c++/header/sample.h"
CSample::CSample() {
 m_str = "";
}
void CSample::setStr(const string str) {
 m_str = str;
 //str = ""; ←constがついているメンバ変数を変えようとするとエラーになる
}
string CSample::getStr() const {
 // m_str = ""; ←constがついているメソッドのメンバを変えようとするとエラーになる
 return m_str;
}

const をつけたメンバ変数のm_cstには100をセットしてある状態。

この変数をmain.cppの中でm_cst = 200;のようにして書き換えようとすると、constつけてあるので、書き換えることができず、エラーとなる。

このようにして、constをつけることで書き換えできないように定数にすることができる。


template関数

ポリモーフィズムのオーバーロードのように関数を都度定義してもいいが、テンプレート関数を使うと関数の定義自体は1個にできる。オーバーロードの関数をまとめることができる。

#include <iostream>
#include <string>

using namespace std;

// テンプレート関数の定義
template <typename T>
T add(T x, T y) {
 return x + y;
}

int main() {
 cout << add<int>(4, 3) << endl;
 cout << add<string>("ABC", "DEF") << endl;
 cout << add(1, 2) << endl;
 return 0;
}

型は一致させないと、エラーとなる。

例)一方はint型の1を渡してもう一方はdouble型の2.5を渡しているのでエラーとなる。

cout << add(1, 2.5) << endl;

string型も型指定が必要。string型なのかchar型なのか判断できず、下記はエラーとなる。
cout << add("abc", "def") << endl;


templateをクラスの中で使う

calc.h

テンプレートを使うときは、ヘッダーファイルに収まるようにするのが一般的なので、calc.cppのファイルは作らず、関数の実装もヘッダーファイルの中に書く。

 1 #ifndef._CALC_H_
 2 #define._CALC_H_
 3 
 4 template<typename.T>.class CCalc.{
 5 ..private:
 6 ....T.m_n1;
 7 ....T.m_n2;
 8 ..public:
 9 ....inline.void.set(const.T.n1,.const.T.n2).{
10 ......m_n1 =.n1;.m_n2 =.n2;
11 ....};.//.引数のセット
12 ....inline T.add().const.{
13 ......return.m_n1 +.m_n2;
14 ....}.//.計算結果
15 };

T m_n1;のTはintとすればintになるしdoubleにしたらdoubleになる。


inlineとは

inline修飾子はちょっと古めのc++によく出てくるもので、inlineをつけたブロックをマシン語のコードにする。

inlineは、先頭につけるとその関数はコンパイル時にインライン展開されるという宣言。

普通の関数はコンパイルされたアセンブラの中でプログラムの流れとは別の部分に書かれていて必要なときに呼び出されるけど、inlineをつけるとこの部分が処理の部分に直接埋め込まれるので、処理の呼び出しなどのオーバーヘッドがなくなり、処理速度がアップする。


inlineブロックに書いている中身、この例ではm_n1 = n1; m_n2 = n2は他のところからデータを持ってこないことになる。

他のところから関数などを呼び出したりはしないでコンパイルされる。

なので処理速度が速くなる。でもマシン語のコードが大きくなってしまう。

逆に普通の関数はプログラムは小さいけど関数呼び出しの分岐がオーバーヘッドになる。

コンパイラに依存する部分が大きいので、必ずinlineにすると効果がでるわけではない。

inlineはセッターとゲッターでよく使われている。セッターとゲッターは頻繁に使われるしオーバーヘッドが少ないので、constと一緒に使うとパフォーマンスが向上するといわれている。


main.cpp

 1 #include.<iostream>
 2 #include.<string>
 3 #include."/home/amaenbo/c++/header/calc.h"
 4 
 5 using.namespace std;
 6 
 7 int main().{
 8 ..CCalc<int>.i1;
 9 ..CCalc<string>.i2;
10 ..i1.set(1..2);
11 ..12.set("ABC",."DEF");
12 ..cout.<<.i1.add().<<.endl.<<.i2.add().<<.endl;
13 ..return.0;
14 }


templateは実際はあまり使わないほうがいい。トラブルの元になりかねないので。stlで使う。

演算子がおなじであれば、型が何であっても使えるようにすることをジェネリック・プログラミングという。

c++でジェネリックプログラミングを実現するための方法がtemplate.

templateには大きく2つあって、template関数とtemplateクラス。


複数のテンプレートの宣言方法

template<typename T, typename S>

上記のように宣言し、テンプレートの使用は下記のようにする。

void add<int, double>(1, 2.3)


テンプレートを使うことのデメリット

実行するまで不明な事項が多い。プログラムを動かすまで動作などわからないので不安材料となる。

生成されるコードが大きくなる。

使用する場合、c言語のマクロのように最低限にしたほうがよく、テンプレートを使うならSTLを使う。


STLとは

Standard Template Libraryの略。誰でも利用できる標準的なc++のテンプレートのらいぶらり。

複雑な記述をせずに様々なデータ形式を簡単に利用できる。

テンプレートは一種のデータ構造で、めっちゃいっぱいライブラリがあるので、代表的なものだけでも把握しておく。

vector、list、mapは配列の概念を拡張したもので、サイズを意識せずに使える配列。

通常の配列だと、予め配列のサイズを確保しておかないといけないので、ファイルの読み込みなど予めどのくらいの配列のサイズを確保すればいいのかわからないケースには不向き。

予め配列のサイズを確保しないといけない、大きさが固定的な通常の配列を静的配列といい、vectorのように動的にサイズが確保できる配列を動的配列という。

静的配列は予め確保するメモリの容量がはっきりしているというメリットがあるけど、どのくらいメモリを確保しないといけないのか不明確なときには不向き。そういうときに便利なのが動的配列となる。


STL | 配列のサイズを動的に変えることができるvector

vectorは動的配列と言われるもので、サイズを動的に変えられる。

vectorは#include <vector>でインクルードすることで使える。

main.cpp


 1 #include.<iostream>
 2 #include.<string>
 3 #include.<vector>
 4 
 5 using.namespace.std;
 6 
 7 int.main().{
 8 ..vector<int>.v1;
 9 ..vector<string>.v2;
10 ..v1.push_back(1);
11 ..v1.push_back(2);
12 ..v1.push_back(3);
13 ..v2.push_back("ABC");
14 ..v2.push_back("DEF");
15 ..unsigned.int.i;
16 ..for.(i.=.0;.i.<.v1.size();.i++).{
17 ....cout.<<."v1[".<<.i.<<."]=".<<.v1[i].<<.endl;
18 ..}
19 ..for.(i.=.0;.i.<.v2.size();.i++).{
20 ....cout.<<."v2[".<<.i.<<."]=".<<.v2[i].<<.endl;
21 ..}
22 }

普通はint a[10]; のようにサイズを定義する必要があるけど、vectorではvector<int> v1;のようにして、サイズを定義しなくても、配列に要素を入れることに応じて動的にサイズが確保される。

vector<int> v1;と宣言したときのサイズは、{ size = 0 }の状態。

vectorのメンバ関数のpush_back() を使い、

v1.push_back(1) ;を実行すると、v1に1を入れる際にサイズが確保され、

{ size = 1 }となる。 (配列の0番目に1が入る)。


vectorの成分へのアクセス

配列と同じように扱える。

vector<int> v;
v[0]=1; // 値の代入
cout << v[0] << endl; // 値の取得


vectorの主なメンバ関数

push_back()    // 要素の追加

clear()     // 要素のクリア

size()     // 配列の大きさを得る関数

capacity()     // 動的配列に追加できる要素の許容量

empty()    // 要素が空かどうかを調べる


STL | listクラス

vectorと似ているけどlistと根本的に違う。

#include <list>とすることで使える。

vectorは動的配列なので配列のインデックスが変わってしまうような、前へのデータ挿入はできない仕様になっている。

listは任意の位置に自由にデータを挿入できる。

listには、通常の配列やvectorのようなインデックスを使うことができない。その代わりにイテレータを使い配列の要素にアクセスする。

main.cpp

 1 #include.<iostream>
 2 #include.<list>
 3 
 4 using.namespace.std;
 5 
 6 int.main().{
 7 ..list<int>.li;
 8 ..li.push_back(1);...//.後ろにデータを挿入
 9 ..li.push_back(2);...//.後ろにデータを挿入
10 ..li.push_front(3);..//.前にデータを挿入
11 ..list<int>::iterator.itr;
12 ..//.データの挿入
13 ..itr.=.li.begin();...//.イテレータを先頭に設定
14 ..itr++;..............//.一つ移動
15 ..li.insert(itr,.4);..//.値の挿入
16 ......................//.データの表示
17 ..for.(itr.=.li.begin();.itr.!=.li.end();.itr++).{
18 ....cout.<<.*itr.<<.".";
19 ..}
20 ..cout.<<.endl;
21 ..return.0;
22 }

vectorクラスと違って、listクラスではpush-front()で前にデータを挿入できる。そうすると、サンプルコードで最初に入れていた2つのデータの配列の番号が最初の値から変化(ずれる)するので、配列の0番目、1番目...といった番号が意味を持たなくなる。

そこで登場するのがイテレーター。

イテレーターはSTLにおけるポインタのようなもの

イテレータの作り方

list<int>::iterator itr; 

→itrはlist型のintに対するポインタとなり、。itrポインタによってデータの位置を指差すことができるようになる。

リストの先頭にイテレータを設定

itr = li.begin();

→リストの前頭にイテレータが設定される。

→itrはリストの先頭の値の3を指している。

イテレータを一つ移動させる

itr++;

→イテレータを一つ移動。

→itrはリストの2番めの値の1を指している。

この状態でデータを挿入すると

li.insert(itr,.4);

→itrの指しているリストの2番めに4が挿入される。もともとの値1は1つ後ろにずれる。


イテレータによるlistの全要素へのアクセス

listクラスをつかうと配列の番号が意味をもたないので、配列に何個のデータがあるか、何番目にどのデータがあるかをfor文でみていく。

listの先頭をitr = li.begin();で指定し、リストの終わりをitr != li.end();で指定する。

イテレータの値を先頭から最後までインクリメントを繰り返すことで、すべての要素にアクセスできる。

listの値はイテレータitrを*itrにてポインタのように使うことで取得できる。


listクラスの主なメンバ関数

push_front()    // 先頭に要素を追加する

push_back()    // 末尾に要素を追加する

pop_front()    // 先頭の要素を削除する。

pop_back()    // 末尾の要素を削除する。

insert()    // 要素を挿入する

erase()    // 要素を削除する

clear()    // 全要素を削除する


STL | イテレータ

イテレータはlistクラスだけでなく、大量のデータを扱う構造に使えるので、イテレータは他のSTLのクラスでも普通は用意されている。

main.cpp

 1 #include.<iostream>
 2 #include.<string>
 3 #include.<vector>
 4 #include.<list>
 5 
 6 using.namespace.std;
 7 
 8 int.main().{
 9 ..vector<string>.v;
10 ..list<string>.l;
11 ..v.push_back("HELLO");
12 ..v.push_back("WORLD");
13 ..l.push_back("hello");
14 ..l.push_back("world");
15 ..l.push_back("!");
16 ..//.vectorでのイテレータ
17 ..vector<string>::iterator.i1;
18 ..list<string>::iterator.i2;
19 ..for.(i1.=.v.begin();.i1.!=.v.end();.i1++).{
20 ....cout.<<.*i1.<<.endl;
21 ..}
22 ..//.listの要素の削除
23 ..i2.=.l.begin();
24 ..l.remove(*i2);..//.要素の削除(listにしかできない)
25 ..for.(i2.=.l.begin();.i2.!=.l.end();.i2++).{
26 ....cout.<<.*i2.<<.endl;
27 ..}
28 }

出力結果:HELLO WORLD world !と表示される。

vectorとlistの違い:listクラスには要素を削除するメソッドが用意されている。それがremoveメソッド。イテレータで指定した要素を削除することができる。

lには最初、配列の0番目にhello、1番目にworld、2番めに!が入っている。

l[0] = "hello"
l[1] = "world"
l[2] = "!" 

i2 = l.begin();でイテレータを配列の0番目に設定
→l.remove(*i2);でイテレータが指している0番目の要素を削除
l[0] = "world"
l[1] = "!"


vectorとlistの特徴

vector

vectorはあくまでも配列の延長の概念で、サイズを最初に指定しなくても使えることを主眼にしている。

任意の場所に要素を追加したり削除することが目的ではないのがvector.


list

listは双方向連結リストといい、任意の場所の要素が削除されたり挿入されるといった使用方法が想定されている。

要素の位置や順番が変化することから、配列と違ってインデックスで管理することができない。


STL | mapクラス

vectorと同様、mapクラスは配列の一種。

vectorは要素へのアクセスを0,1,2,など添字の数字のインデックスで行うが、mapは数値でのアクセスをしない。

mapクラスは配列の添字を文字列にできるクラス。mapクラスは配列の添字の代わりに好きな型を使える。

mapは辞書のイメージで、単語を指定したら、その意味が書かれているようなイメージ。

上記で単語に当たる部分をキーと呼び、単語の意味に相当するものを要素と呼ぶ。

このキーと要素の組み合わせによる記憶方法を連想記憶という。

mapは連想配列とも呼ばれている。


#include <map>とすることでmapクラスが使える。

mapは、

map <キーの型, 値> map配列の名称; と宣言する。


mapの主なメンバ関数
clear() すべての要素をクリアする。
empty() マップが空のときにtrueを返し、空でないときにfalseを返す。
erase() 指定した要素をクリアする。
size() マップの中の要素数を返す。
find() マップの中から引数で指定したキーに一致する要素を探し、イテレータを返す。

map.cpp

 1 #include.<iostream>
 2 #include.<string>
 3 #include.<map>
 4 
 5 using.namespace.std;
 6 
 7 int.main().{
 8 ..map.<string,.int>.score;..//.mapのデータ構造を用意する
 9 ..string.names[].=.{."Tom","Bob","Mike".};
10 ..score[names[0]].=.100;..//.キーと値の関連付け1.Tom.:.100
11 ..score[names[1]].=.80;...//.キーと値の関連付け2.Bob.:.80
12 ..score[names[2]].=.120;..//.キーと値の関連付け3.Mike.:.120
13 ..int.i;
14 ..for.(i.=.0;.i.<.3;.i++).{
15 ....cout.<<.names[i].<<.":".<<.score[names[i]].<<.endl;
16 ..}
17 ..return.0;
18 .}

score[names[0]] = 100; とすることで、[names[0]]の部分が置き換えられて["Tom"] = 100という配列が作られる。

score[names[1]] =  80; -> ["Bob"] = 80

score[names[2]] = 120 -> ["Mike"] = 120


STL | setクラス

set = 集合という意味

指定した型の構造を作ってそこにどんどんデータを入れていくだけのクラス。

set<string> names;
names.insert("Tom");
names.insert("Mike");
...

setの特徴が、データのダブリを許さないところ。

もともとあるデータとあとで追加するデータが重複したら、もともとあるデータとして1個として扱われる。具体的には、あとで追加する処理(names.insert())がfalseとなり処理(names.insert()が)実行されない。

setの主なメンバ関数
clear() 全ての要素をクリアする。
empty() 集合が空であるときにtrueを返し、空でないときにfalseを返す。
erase() 指定した要素をクリアする。
size() 集合の中の要素数を返す。

main.cpp

#include <iostream>
#include <string>
#include <set>
using namespace std;
int main() {
 set<string> names;  // setのデータ構造を用意する
 // 値を代入
 names.insert("Tom");
 names.insert("Mike");
 names.insert("Mike"); // 同じ名前をダブって代入させる
 names.insert("Bob");
 // 登録されている全データを表示
 set<string>::iterator it; // イテレータを用意
 for (it = names.begin(); it != names.end(); it++) {
   cout << *it << endl;
 }
 // Bob,Steveがデータ内に存在するか調べる
 string n[] = { "Bob", "Steve" };
 int i;
 for (i = 0; i < 2; i++) {
   it = names.find(n[i]);
   if (it == names.end()) {
     // データがset内に存在しない
     cout << n[i] << " is not in a set. " << endl;
   }
   else {
     // データがset内に存在する
     cout << n[i] << " is in a set. " << endl;
   }
 }
 return 0;
}

it = names.find(n[i]);  データの有無を調べる。戻り値はイテレータ。
→namesのデータからn[i]のデータを見つける。

戻り値はnames.end()でなかったら、データが存在することになり、そのデータに該当するイテレータが返ってくる。

namesのデータにn[i]に該当するデータがなかったら、names.find()はnamesの配列の端までいき、戻り値はイテレータがnames.end()だったらデータが存在しないということ。

端って配列の最後なので\nのことかも。。


STL | stackクラスとqueueクラス

stackは積み重ねるという意味で、最後に入力したデータが最初に出力されるデータ構造。

あとから積み重ねたものから先に探すようなデータの探索方法をLIFO(Last In First Out)という。

queueはstackと逆の概念で、最初に登録したデータから順に検索していくデータ形式で、FIFO(First In First Out)という。


stackもqueueもループで要素を取り出したらpop()でその要素を取り除く(削除)する操作が必要。

stackもqueueもデータを配列に入れたときの見た目は同じ。

[0] = 1
[1] = 2
[2] = 3
のようにデータが入る。

アウトプットのときにstackとqueueは違ってくる。


stackの取り出され方
[2] = 3, [1] = 2, [0] = 1の順に取り出される。最後に入れたものから取り出される。


queue
[0] = 1, [1] = 2, [2] = 3の順に取り出される。先に入れたものから取り出される。


main.cpp

#include <iostream>
#include <stack>
#include <queue>
using namespace std;
int main() {
 stack<int> stk; // スタックのデータを宣言
 queue<int> que; // キューのクラス宣言
 int data[] = { 1, 2, 3 }; // 登録するデータ
 int i;
 // データの登録
 for (i = 0; i < 3; i++) {
   stk.push(data[i]);
   que.push(data[i]);
 }
 // データの出力(stack)
 cout << "stack : ";
 while (!stk.empty()) {
   // topで要素を取得しpopでその要素をstkから取り除く(2段階の作業が必要)
   cout << stk.top() << " ";
   stk.pop();
 }
 cout << endl;
 // データの出力(queue)
 cout << "queue : ";
 while (!que.empty()) {
   // frontで要素を取得しpopでその要素をqueから取り除く(2段階の作業が必要)
   cout << que.front() << " ";
   que.pop();
 }
 cout << endl;
 return 0;
}



仮想関数

メンバ関数の頭にvirtualを付けたものを仮想関数という。

仮想関数を使うと、スーパークラスの処理をサブクラスに委ねることができる。

スーパークラスのメンバ関数にvirtualを付ける→スーパークラスを継承したサブクラスで同じ名前の関数を使ってサブクラス独自の実装をした場合、virtualがついたスーパークラスのメンバ関数の実装は実行されず、サブクラスで実装した関数が実行される。


※ファイルが増えてわかりにくくなるので、今回はヘッダファイルに関数の定義も書いている。

bird.h

#ifndef _BIRD_H_
#define _BIRD_H_
#include <iostream>
#include <string>
using namespace std;
class CBird {
public:
 // 鳴く関数(仮想関数)
 virtual void sing() { cout << "鳥が泣きます" << endl; }
 // 飛ぶ関数
 void fly() { cout << "鳥が飛びます" << endl; }
};
#endif // _BIRD_H_


chicken.h

#ifndef _CHICKEN_H_
#define _CHICKEN_H_
#include "/home/amaenbo/c++/header/bird.h"
// ニワトリクラス
class CChicken : public CBird {
public:
 // 鳴く関数(仮想関数)
 void sing() { cout << "コケコッコー" << endl; }
 // 飛ぶ関数
 void fly() { cout << "ニワトリは飛べません" << endl; }
};
#endif // _CHICKEN_H_


crow.h

#ifndef _CROW_H_
#define _CROW_H_
#include "/home/amaenbo/c++/header/bird.h"
// カラスクラス
class CCrow : public CBird {
public:
 // 鳴く関数(仮想関数)
 void sing() { cout << "カーカー" << endl; }
 // 飛ぶ関数
 void fly() { cout << "カラスが飛びます" << endl; }
};
#endif // _CROW_H


main.cpp

#include <string>
#include "/home/amaenbo/c++/header/bird.h"
#include "/home/amaenbo/c++/header/chicken.h"
#include "/home/amaenbo/c++/header/crow.h"
using namespace std;
int main() {
 CBird* b1, *b2;
 b1 = new CCrow();
 b2 = new CChicken();
 b1->sing();
 b1->fly();
 b2->sing();
 b2->fly();
 delete b1, b2;
 return 0;
}


c実行結果

カーカー
鳥が飛びます
コケコッコー
鳥が飛びます

スーパークラスであるCBirdクラスでsing()メソッドについてはvirtualを付けたので、

CBirdクラスのポインタ型を通じてnewしたchickenクラスとcrowクラスのsing()メソッドは、それぞれのクラスのsing()メソッドが実行される。

スーパークラスであるCBirdクラスのvirtualをつけていないfly()メソッドの方は、chickenクラスとcrowクラスのfly()メソッドは実行されず、スーパークラスであるCBirdクラスのfly()メソッドが実行される。


仮想関数の便利なところは、一つのメソッド名でそれぞれのクラスに応じた結果をちゃんと出力してくれるところ。

例)カーカー、コケコッコーなど、鳴くメソッドsing()はクラスによって出力が違うけど、sing()メソッド一つでちゃんとクラスに応じた出力を出してくれる。

main.cppのところで

CBird* b1, *b2;
b1 = new CCrow();
b2 = new CChicken();

のようにしていることがポイント。


完全仮想関数

仮想関数のサンプルではCBirdクラスの仮想関数は
virtual void sing() {...}のように実装部分があったけど、純粋仮想関数は実装がなく、サブクラスで実装を書く感じ。

サンプルではCBirdクラスのsing()メソッドをスーパークラスのメソッドにすることとし、

virtual void sing() = 0;

のように初期化しておき、

sing()の実装はサブクラスで書く。これを完全仮想関数という。

完全仮想関数は、完全仮想関数をメンバ関数に持つスーパークラスを継承したサブクラスで実装をすることを前提としている。 

bird.h

#ifndef _BIRD_H_
#define _BIRD_H_
#include <iostream>
#include <string>
using namespace std;
class CBird {
public:
 // 鳴く関数(仮想関数)
 virtual void sing() = 0;
 // 飛ぶ関数
 void fly() { cout << "鳥が飛びます" << endl; }
};
#endif // _BIRD_H_

完全仮想関数の注意点

完全仮想関数を取り入れたスーパークラスは前述のように実装を持たないので、インスタンスを生成できない。

このサンプルでは、CBirdクラスのインスタンスをnewで生成しようとするとエラーが起こる。

こんな感じで実装を持たないクラスを抽象クラスという。

完全仮想関数を一つでももったクラスは抽象クラスとなり、抽象クラス自体のインスタンスは生成できない。

※抽象クラスを継承したクラスのインスタンスは生成できる。

→抽象クラスがあるということは、具体的な実装を持つサブクラスを作ることが求められているということ。

抽象クラスをつくって共通する部分を持たしておくことで、あとは種類に応じた実装をするだけとなりラクになる。

抽象クラスとして鳥クラスをもっておき→ニワトリクラス、スズメクラスなどの種類に応じた具象クラスを作成する感じ。



仮想デストラクタ

スーパークラスのデストラクタにvirtualを付けないと、継承されたサブクラスのインスタンス破棄時に、スーパークラスのデストラクタは実行されるけどサブクラスのデストラクタが実行されないという不具合が起こる。

ちゃんとインスタンスのデストラクタも呼ばれるようにするために、スーパークラスのデストラクタにvirtualを付ける。


Interface

c++にはじめからインターフェースの昨日が備わっているわけではないけど、仮想関数と純粋仮想関数を使うことで細かい単位で動作の切り分けができインターフェースとしての機能を実現できる。

インターフェースにすることで、ひとつのクラスをいろんな側面で切り取れるイメージ。

完全仮想関数だけで構成されたクラスをインターフェースという。


main.cpp

#include <iostream>
#include "/home/amaenbo/c++/header/iinf1.h"
#include "/home/amaenbo/c++/header/iinf2.h"
#include "/home/amaenbo/c++/header/sample.h"
// IInf1のみが使える関数
void foo(IInf1*); 
// IInf2のみが使える関数
void bar(IInf2*); 
int main() {
 CSample * s = new CSample();
 foo((IInf1*)s);
 bar((IInf2*)s);
 return 0;
}
// IInf1のみが使える関数
void foo(IInf1* p) {
 p->func1();
 p->func2();
}
// IInf2のみが使える関数
void bar(IInf2* p) {
 p->func3();
 p->func4();
}

CSampleのインスタンスsを生成しfoo()関数はIInf1クラスでキャスト、bar()関数はIInf2クラスでキャストしている。

これによってfoo()関数ではfunc3()とfunc4()が使えない。bar()関数ではfunc1()とfunc2()が使えない。

以上のようにIInf1クラスとIInf2クラスのメンバ関数を純粋仮想関数にすることで、切り分けができる。これがインターフェース。

sample.h

#ifndef _SAMPLE_H_
#define _SAMPLE_H_
#include <iostream>
#include "/home/amaenbo/c++/header/iinf1.h"
#include "/home/amaenbo/c++/header/iinf2.h"
using namespace std;
class CSample : public IInf1, public IInf2 {
public:
 void func1() { cout << "func1" << endl; }
 void func2() { cout << "func2" << endl; }
 void func3() { cout << "func3" << endl; }
 void func4() { cout << "func4" << endl; }
};
#endif // _SAMPLE_H_

クラスCSampleのメンバ関数はfunc1()〜func4()まであり、

func1()とfunc2()はAさんだけ使えるメンバ関数にしたい。
func13()とfunc4()はBさんだけ使えるメンバ関数にしたい。

という感じに、ひとつのクラス内のメンバ関数を切り分けたい。

ここで、CSampleクラスはIInf1、IInf2を継承したクラスとなっているのがポイント。


iinf1.h

#ifndef _IINF1_H_
#define _IINF1_H_
// インターフェースクラス1
class IInf1 {
public:
 virtual void func1() = 0;
 virtual void func2() = 0;
};
#endif // _IINF1_H_

IInf1クラスは純粋仮想関数func1()とfunc2()をメンバ関数に持つ。

純粋仮想関数なのでインスタンスは生成できない。

iinf2.h

#ifndef _IINF2_H_
#define _IINF2_H_
// インターフェースクラス2
class IInf2 {
public:
 virtual void func3() = 0;
 virtual void func4() = 0;
};
#endif // _IINF2_H_

IInf2クラスもIInf1クラスと同様。純粋仮想関数func3()とfunc4()を持つ。


演算子のオーバーロード


vector2.h

#ifndef _VECTOR2_H_
#define _VECTOR2_H_
#include <iostream>
#include <string>
using namespace std;
class Vector2 {
public:
	double x;
	double y;
public:
	//  =‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
	Vector2 & operator=(const Vector2& v);
	//  +=‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
	Vector2& operator+=(const Vector2& v);
	//  -=‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
	Vector2& operator-=(const Vector2& v);
};
//  +‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
Vector2 operator+(const Vector2&, const Vector2&);
//  -‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
Vector2 operator-(const Vector2&, const Vector2&);
//  *‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
Vector2 operator*(const double, const Vector2& v);
#endif // _VECTOR2_H_

仮引数が2つある演算子の定義をVector2クラスの外にしているのは、戻り値として得られるものがまったく新しいインスタンスになってしまうので、特定のインスタンスを表すクラスのメソッドして使えない。

インスタンスa + bの結果は、全く新しいインスタンスになり、

インスタンスを生成して、そこから代入したりするのと性質が異なる。

そのため、クラスのメンバ関数にはぜずに、クラスの外で普通に関数として定義している。


vector2.cpp

#include "vector2.h"
//  +‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
Vector2 operator+(const Vector2& v1, const Vector2& v2)
{
	Vector2 v;
	v.x = v1.x + v2.x;
	v.y = v1.y + v2.y;
	return v;
}
//  -‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
Vector2 operator-(const Vector2& v1, const Vector2& v2)
{
	Vector2 v;
	v.x = v1.x - v2.x;
	v.y = v1.y - v2.y;
	return v;
}
//  ƒXƒJƒ‰[”{
Vector2 operator*(const double d, const Vector2& v)
{
	Vector2 r;
	r.x = d * v.x;
	r.y = d * v.y;
	return r;
}
//  =‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
Vector2& Vector2::operator=(const Vector2& v)
{
	x = v.x;
	y = v.y;
	return *this;
}
//  +=‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
Vector2& Vector2::operator+=(const Vector2& v)
{
	x += v.x;
	y += v.y;
	return *this;
}
//  -=‰‰ŽZŽq‚̃I[ƒo[ƒ[ƒh
Vector2& Vector2::operator-=(const Vector2& v)
{
	x -= v.x;
	y -= v.y;
	return *this;
}


main.cpp

#include <iostream>
#include "vector2.h"
using namespace std;
void vec(string, Vector2&);
int main() {
	Vector2 v1, v2, v3, v4;
	//  ƒxƒNƒgƒ‹‚É’l‚ð‘ã“ü
	v1.x = 1.0;
	v1.y = 2.0;
	v2 = v1;            //  ’l‚ð‘ã“ü
	v3 = 4.0 * v1;      //  ƒxƒNƒgƒ‹‚̃XƒJƒ‰[”{
	v4 = v1 + v2;
	vec("v1=", v1);
	vec("v2=", v2);
	vec("v1 + v2=", v4);
	vec("v3=", v3);
	v3 += v1;           //  ‘ã“ü‰‰ŽZŽqi+=j
	vec("v3=", v3);
	v1 -= v2;           //  ‘ã“ü‰‰ŽZŽqi-=j
	vec("v1=", v1);
	return 0;
}
void vec(string vecname, Vector2& v)
{
	cout << vecname << "(" << v.x << "," << v.y << ")" << endl;
}








いkoop

この記事が気に入ったらサポートをしてみませんか?