1 c++によるオブジェクト指向言語入門tokai/tokai/doc2007/soft/...1...
TRANSCRIPT
1 C++によるオブジェクト指向言語入門
1.1 Cとの構文の違い
オブジェクト指向を学ぶ前に C++の構文について学ぶ。C++は Cを拡張したものであるため、Cと同じ構文を使用することも出来るが C++独特の構文も存在する。以下の節ではそれらについて説明する。
1.1.1 標準入出力ストリーム
Cでは画面への出力は printf()、キー入力は scanf()などでおこなったが、C++ では「標準入出力ストリーム」を使用する。例えば Cにおける hello worldは printf()を用いて
ソース 1-1
#include <stdio.h>
int main()
{
printf( "hello world\n" );
return 0;
}
であったが、C++では次の様になる。
ソース 1-2
#include <iostream>
int main()
{
std::cout << "hello world" << std::endl;
return 0;
}
同様に、キー入力について Cでは scanf()を用いて
ソース 1-3
#include <stdio.h>
int main()
{
int a;
scanf( "%d", &a );
printf( "a = %d\n", a );
return 0;
}
としたが、C++では
1
ソース 1-4
#include <iostream>
int main()
{
int a;
std::cin >> a;
std::cout << "a = " << a << std::endl;
return 0;
}
となる。
ここで ソース 1-3と ソース 1-4を比較して説明する。まず始めに C++(ソース 1-4 )では stdio.hの代わりに iostreamをインクルードする。この時 iostream.hとしていないことに着目すること。C++では includeするライブラリのヘッダに.hを付けないことになっている。次に ソース 1-4では scanf()の代わりに「std::cin」と「>> 演算子」を使っていることに着目する。この std::cinの
ことを「標準入力ストリーム (stream)」と呼び、 >> 演算子と組み合わせて標準入力 ( std::cin )から int型変数 a の中にデータを流れ込ませるということを意味している。
また ソース 1-4 では printf()の代わりに「std::cout」と「<< 演算子」を使っている。これは標準出力ストリーム (std::cout ) の中に変数 aの内容を流れ込ませることを意味している。また std::endlは改行 ( end of line )の意味である。この様に C++では標準入出力ストリーム (std::cin, std::cout)とストリーム演算子 ( << 又は >> )を組み合わせて画面表示とキー入力を実現する。なお printf()や scanf()とは異なり型の認識は自動でおこなわれる。
問題 1-1 標準入出力ストリームを用いて任意の文字列を読み込み表示せよ (10点)
問題 1-2 標準入出力ストリームを用いて数字 (double型)を 2つ読み込み、足し算、引き算、掛け算、割り算をおこなえ。ただし std::cin, std::cout は各一度しか使用しないこと。(10点)
1.1.2 ファイル入出力ストリーム
ファイルからのデータの入出力は Cでは fopen()、fclose()などを用いたが、C++では std::fstreamストリームを用いる。例えばファイル出力とファイル入力はそれぞれ
ソース 1-5
#include <fstream>
int main()
{
std::fstream fout;
int a = 123;
fout.open( "data.txt", std::ios::out );
fout << a << std::endl;
fout.close();
return 0;
}
2
ソース 1-6
#include <iostream>
#include <fstream>
int main()
{
std::fstream fout;
int a;
fout.open( "data.txt", std::ios::in );
fout >> a ;
fout.close();
std::cout << a << std::endl;
return 0;
}
の様におこなう。 ソース 1-5 、 ソース 1-6 において着目する部分は「fstream」ヘッダをインクルードしていること、open()の際の引数、及びストリーム演算子の方向である。ファイルにデータを出力する場合は open()の引数にファイル名と「std::ios::out」を指定し、「<< 演算子」を用いる。一方、ファイルからデータを入力する場合は open()の引数にファイル名と「std::ios::in」を指定し、「>> 演算子」を用いる。なお、標準入出力ストリームと同様に変数の型認識は
自動でおこなわれる。
問題 1-3 キーボードから入力した任意の文字列をファイル入出力ストリームを用いてファイルに出力せよ (10点)
問題 1-4 ファイル入出力ストリームを用いてファイルから文字列を読み込み画面に表示せよ (10点)
1.1.3 変数スコープ
厳密な Cでは変数を定義できる場所は関数の先頭部分のみと決められている。一方、C++では自由な場所で変数を定義することが出来るが、定義した変数には有効な範囲 (スコープ)があることに気を付けること。具体的には中カッコ内で定義された変数はその中カッコ外で使用することは出来ない。例えば
ソース 1-7
#include <iostream>
int main()
{
int a;
{
std::cin >> a;
}
std::cout << a << std::endl;
return 0;
3
}
はコンパイルエラーにならないが、次のソースではコンパイルエラーとなる。
ソース 1-8
#include <iostream>
int main()
{
{
int a;
std::cin >> a;
}
std::cout << a << std::endl;
return 0;
}
(補足) C++で変数 (および関数)を定義する際に、「名前空間」という近代的なプログラミング言語において必須の概念を用いることもあるが、今回の演習の範囲を越えるので割愛する。各自自習しておくこと。
問題 1-5 次のコードを実行させたときに起こる現象を調べ、なぜそうなったか考察せよ。 (10点)
ソース 1-9
#include <iostream>
int main()
{
int a = 3;
{
int a;
std::cin >> a;
std::cout << a << std::endl;
}
std::cout << a << std::endl;
return 0;
}
4
1.1.4 newと delete
Cでは動的にメモリを確保するために malloc()と free()を使用したが、C++では「new、delete演算子」を使用する。例えば Cで int型のメモリを n個確保するには
ソース 1-10
#include <stdio.h>
#include <string.h>
int main()
{
int n, i;
int *a;
n = 4;
a = ( int* ) malloc( sizeof( int ) * n );
for( i = 0; i < n ; ++i ) scanf( "%d", a+i );
for( i = 0; i < n ; ++i ) printf( "[%d]=%d\n", i, a[i] );
free( a );
return 0;
}
としたが、C++では new, delete 演算子を用いて
ソース 1-11
#include <iostream>
int main()
{
int n = 4;
int* a = new int[n];
for( int i = 0; i < n; ++i ) std::cin >> a[i];
for( int i = 0; i < n; ++i ) std::cout << "[" << i << "]=" << a[i] << std::endl;
delete a;
return 0;
}
とする。ここで a = new int[n] の部分で int型のメモリを n個確保して delete aでメモリを開放している。ソース 1-11から分かるように、new 演算子は malloc()関数と異なり自動で確保するバイト数の計算をおこなうため、わざわざバイト数 (sizeof(int)*n バイト)をプログラマ側が指定する必要は無い。
(補足) C++では変数の型やバイト数などコンピューダの内部実装に関わる部分は極力排除するように出来ているが、JavaScriptや Rubyなどのオブジェクト指向言語と比べると完全には排除しきれていない。
問題 1-6 int型変数 nに標準入力ストリームを用いて数字を入力し、長さ nの double型配列を newを用いて作り、その配列に標準入力ストリームを用いて数字を代入し、その和を表示するプログラムを作成せよ。deleteも忘れずに呼ぶこと (10点)
5
1.1.5 std::vector
前節で説明した newを用いて配列を作成すると、delete忘れによるメモリリークが生じる場合があるため、C++ではベクトル型クラスの「std::vector」クラスを配列の代わりに用いることが多い。例えば ソース 1-11 を std::vectorクラスを用いて書き直すと次の様になる。
ソース 1-12
#include <iostream>
#include <vector>
int main()
{
int n = 4;
std::vector< int > a;
a.resize( n );
for( int i = 0; i < n; ++i ) std::cin >> a[i];
for( int i = 0; i < n; ++i ) std::cout << "[" << i << "]=" << a[i] << std::endl;
return 0;
}
ここで「vector」ヘッダをインクルードし、「std::vector< int > 」と int 型を std::vectorの引数として指定した後でa.resize( n )とベクトルサイズを変更していることに着目せよ。このように std::vectorクラスを用いて配列を作成すると、プログラマが意図的に deleteを呼んでメモリを開放しなくても配列の使用が終わる (変数スコープから外れる)と自動的に確保したメモリが開放されるためメモリリークを防ぐことが出来る。
(補足) std::vectorクラスには「イテレータ (iterator)」というオプジェクト指向言語では極めて重要な概念も関わってくるが、今回の演習の範囲を越えるので割愛する。各自自習しておくこと。
問題 1-7 前節で作成したプログラムを std::vectorクラスを用いて書き直せ ( 10点 )
1.1.6 std::stringによる文字列操作
Cでは文字列を char型の配列で表現していたが、バッファオーバフロー (BOF)によるセキュリティの問題が生じるため C++では文字列クラスの「std::string」を用いて文字列を表すことが多い。例えば Cで文字列を二つ読み込んでそれを合成するには sprintf()を用いて
ソース 1-13
#include <stdio.h>
#include <string.h>
int main()
{
char a[256];
char b[256];
char c[256];
scanf( "%s", a );
6
scanf( "%s", b );
sprintf( c, "%s%s", a, b );
printf( "%s\n", c );
return 0;
}
としていたが、std::stringクラスを用いると単に + 演算子を用いて
ソース 1-14
#include <iostream>
#include <string>
int main()
{
std::string a;
std::string b;
std::string c;
std::cin >> a;
std::cin >> b;
c = a + b;
std::cout << c << std::endl;
return 0;
}
とする。ここで「string」ヘッダをインクルードしていることに注意すること。また 2つの文字列が等しいか判定するには Cでは strcmp()を用いて
ソース 1-15
#include <stdio.h>
#include <string.h>
int main()
{
char a[256];
char b[256];
scanf( "%s", a );
strcpy( b, "テスト" );
if( strcmp( a, b ) == 0 ) printf( "等しい\n" );
else printf( "等しくない\n" );
return 0;
}
7
としたが、std::stringクラスを用いると単に「 == 演算子」を用いて
ソース 1-16
#include <iostream>
#include <string>
int main()
{
std::string a;
std::string b;
std::cin >> a;
b = "テスト";
if( a == b ) std::cout << "等しい\n";
else std::cout << "等しくない\n";
return 0;
}
だけで良い。
問題 1-8 int型変数 nに標準入力ストリームを用いて数字を入力し、長さ nの std::stringクラス配列を std::vectorを用いて作り、その配列に標準入力ストリームを用いて文字列を代入し、それらの文字列を連結して表示するプログラム
を作成せよ (10点)
1.1.7 関数のオーバロード (多重定義)
C言語では同じ名前の関数はひとつしか定義できなかったが、C++では「引数の型が異なれば」いくつでも同じ名前の関数を定義できる。これを「関数のオーバロード (多重定義)」と言い、引数の型の違いによってコンパイラが自動的に呼び出す関数を決定する。
問題 1-9 次のコードを実行させたときに起こる現象を調べ、なぜそうなったか考察せよ。 (10点)
ソース 1-17
#include <iostream>
double test( int a ){ return a + 1; }
double test( double a ){ return a - 1; }
int main()
{
int a = 1;
double b = 1;
std::cout << test( a ) << std::endl;
std::cout << test( b ) << std::endl;
8
return 0;
}
1.1.8 関数への変数の参照渡し
Cでは関数への変数の渡し方として値渡しとポインタ渡ししかなかったが、C++ではさらに参照渡しという方法が加わる。参照渡しとは関数の引数において型の後に&を付けることによって、関数内での変数の変更を呼出元の変数に反
映させる方法である。例えば
ソース 1-18
#include <iostream>
void test( int& z ){ z += 1; }
int main()
{
int a = 1;
test( a );
std::cout << a << std::endl;
return 0;
}
を実行すると変数 aが関数 test()内で更新されて 2が表示される。参照渡しを用いるとポインタ渡しと異なって変数の前に*を付ける必要がないため分かりやすいコードになる。
(補足) 参照渡しの本当のメリットは関数呼び出し時のオーバヘッドの軽減と、コンパイル時に実体 (メモリ)を確保していないオブジェクトを関数に渡そうとするとコンパイル時にエラーが出るため、セグメンテーションフォルトを未然
に防ぐことができることである。なお C++では原則としてポインタ渡しは使用しない。
問題 1-10 次のコードの test1()、test2()、test3()の引数の渡し方をそれぞれ何と呼ぶか。また以下のコードを実行させたときに起こる現象を調べ、なぜそうなったか考察せよ。 (10点)
ソース 1-19
#include <iostream>
void test1( int a ){ a += 1; }
void test2( int* a ){ *a += 1; }
void test3( int& a ){ a += 1; }
int main()
{
int a1 = 1, a2 = 1, a3 = 1;
test1( a1 );
test2( &a2 );
9
1.2 クラスとオブジェクト (インスタンス)
この章では C++のクラス作成を通じてオブジェクト指向プログラミングについて学ぶ。オブジェクト指向プログラミングでは変数の型のことをクラス、実体化したクラスをオブジェクトまたはインスタンスと呼ぶ。例えば int a,bを考えると、intが (整数型)クラス、aと bがオブジェクト (インスタンス)である。
1.2.1 クラスの定義方法
C++のクラスは Cの構造体を拡張してメンバ関数を付け加えた形となっているので、構造体の定義の方法から順を追って説明していく。始めに Cでテストの成績 (SCORE)という構造体を定義しよう。
ソース 1-20
#include <stdio.h>
struct SCORE
{
int math;
int english;
};
int main()
{
struct SCORE sc;
sc.math = 80;
sc.english = 70;
printf( "math = %d english = %d\n", sc.math, sc.english );
return 0;
}
これをクラス化して C++で書き直すと次の様になる。
ソース 1-21
#include <iostream>
class SCORE
{
public:
int math;
int english;
};
int main()
{
SCORE sc;
11
sc.math = 80;
sc.english = 70;
std::cout << "math = " << sc.math << " english = " << sc.english << std::endl;
return 0;
}
ソース 1-20と ソース 1-21の違いは「struct」が「class」に変わったことと、「 int math;」の前に「public:」というキーワードが入ったことだけである。「public:」に関してはアクセス属性の節で説明するが、SCOREクラス内で定義された変数 math, english のことを「SCOREクラスの math,englishメンバ変数」と呼ぶ。さて Cで mathと englishの平均値を計算するには関数mean()を次のように定義して
ソース 1-22
#include <stdio.h>
struct SCORE
{
int math;
int english;
};
double mean( struct SCORE sc )
{
return ( sc.math + sc.english )/2.0;
}
int main()
{
struct SCORE sc;
sc.math = 80;
sc.english = 70;
printf( "math = %d english = %d\n", sc.math, sc.english );
printf( "mean = %lf\n", mean( sc ) );
return 0;
}
としていたが、C++では関数mean()をクラスの内部で定義して
ソース 1-23
#include <iostream>
class SCORE
{
12
public:
int math;
int english;
double mean(){ return ( math + english )/2.0; }
};
int main()
{
SCORE sc;
sc.math = 80;
sc.english = 70;
std::cout << "math = " << sc.math << " english = " << sc.english << std::endl
<< "mean = " << sc.mean() << std::endl;
return 0;
}
とする。ソース 1-22と ソース 1-23の違いはmean()の引数が無くなったことと、mathや englishの前の sc.が無くなってメンバ変数に直接アクセスしていることである。ソース 1-23 のクラス定義内で定義した mean()のことを「SCOREクラスの maen()メンバ関数」と呼ぶ。また「sc.maen()」がメンバ関数を呼び出しているところである。このように、、あるオブジェクトのメンバ関数にアクセスするには「オブジェクト名.メンバ関数名 ()」とする。以上で示したように、クラスはメンバ変数とメンバ関数と呼ばれるクラス内部で定義された変数と関数から出来てい
て、オブジェクト指向プログラミングでは複数のオブジェクトが互いにメンバ変数やメンバ関数にアクセスすることに
よつて処理を進めていく。
問題 1-11 ソース 1-23 に mathと englishのうち大きい数字の方を返す int max()メンバ関数を追加せよ。
1.2.2 複数のオブジェクト (インスタンス)の生成
クラスを実体化したものがオブジェクト (インスタンス)であったので、当然一つのクラスから複数のオブジェクトを生成する事が出来る.例えば SCOREクラスからオブジェクト tokai,oyama を作るには次のようにする。
ソース 1-24
#include <iostream>
class SCORE
{
public:
int math;
int english;
double mean(){ return ( math + english )/2.0; }
};
int main()
{
13
SCORE tokai,oyama;
tokai.math = 10;
tokai.english = 20;
oyama.math = 90;
oyama.english = 80;
std::cout << "mean of tokai = " << tokai.mean() << std::endl
<< "mean of oyama = " << oyama.mean() << std::endl;
return 0;
}
ここで tokai.mean()と oyama.mean()の戻り値が異なることに注目すること。tokai.mean()は関数内部で tokaiオブジェクトの math,englishオブジェクトの平均を計算しているのに対して、oyama.mean()は関数内部で oyamaオブジェクトの math,englishオブジェクトの平均を計算している。この様に、オブジェクト指向プログラミングではクラス定義は共通でもメンバ変数の値が異なるとオブジェクトの振
舞いが変化することに気を付けること。
問題 1-12 前節の問題で作成した max()メンバ関数が定義された SCOREクラスから tokai,oyama,自分、の 3つのオブジェクトを生成してそれぞれのオブジェクトの最大値を出力せよ。
1.2.3 コンストラクタとデストラクタ
オブジェクトを生成する際に、メンバ変数の値をいちいち定義するのは面倒であるので何らかの関数に引数を渡して
一気に値を設定したい。この時に使用する特殊な関数が「コンストラクタ (construncor)」である。コンストラクタはオブジェクトが生成される時に一度だけ呼ばれる特殊な関数であってオブジェクトの初期化のため
にに使用される。例えば前節の SCOREクラスをコンストラクタを用いて書き直すと
ソース 1-25
#include <iostream>
class SCORE
{
public:
int math;
int english;
SCORE( int m, int e ){
math = m;
english = e;
}
double mean(){ return ( math + english )/2.0; }
};
int main()
{
14
SCORE tokai( 10, 20 ), oyama( 90, 80 );
std::cout << "mean of tokai = " << tokai.mean() << std::endl
<< "mean of oyama = " << oyama.mean() << std::endl;
return 0;
}
となり、main()関数が非常にすっきりすることが分かる。ここで「SCORE( int m, int e )」がコンストラクタであり、「クラス名と同じ関数名」「戻り値が無い」という特徴がある。
一方、オブジェクトが消滅するときに最後に呼び出される特殊関数を「デストラクタ (destruntor)」と呼び、オブジェクトを使い終わった後の後始末のために使用される。例えば
ソース 1-26
#include <iostream>
class SCORE
{
public:
int math;
int english;
SCORE( int m, int e ){
math = m;
english = e;
}
~SCORE()
{
std::cout << "消滅\n";
}
double mean(){ return ( math + english )/2.0; }
};
int main()
{
SCORE tokai( 10, 20 ), oyama( 90, 80 );
std::cout << "mean of tokai = " << tokai.mean() << std::endl
<< "mean of oyama = " << oyama.mean() << std::endl;
return 0;
}
を実行すると main()関数が終わってオブジェクト tokai,oyamaが消滅する際に「消滅」という文字を表示する。ここで「~SCORE()」がデストラクタであり、「クラス名と同じ関数名の前にチルダを付ける」、「戻り値が無い」、「引数が無い」という特徴がある。
問題 1-13 次のコードを実行して動作を観察し、なぜそのような挙動になるか考察せよ ( 10 点 )
15
ソース 1-27
#include <iostream>
#include <string>
class FRUIT
{
public:
std::string name;
FRUIT( std::string n )
{
name = n;
std::cout << name << "生成\n";
}
~FRUIT(){ std::cout << name << "消滅\n"; }
};
int main()
{
FRUIT *fr = new FRUIT( "apple" );
delete fr;
fr = new FRUIT( "orange" );
delete fr;
return 0;
}
1.2.4 継承 (インヘリタンス)
ある定義済みのクラスを基に、新しいメンバ変数やメンバ関数を追加・拡張して別のクラスをつくり出すことを「継
承 (インヘリタンス:inheritance)」と言う。基となったクラスの事を「基底クラス」「スーパークラス」などと呼び、継承したクラスの事を「派生クラス」「サブクラス」などと呼ぶ。例えば果物 (FRUIT)クラス
ソース 1-28
#include <iostream>
#include <string>
class FRUIT
{
public:
std::string name;
FRUIT( std::string n )
{
name = n;
16
}
void show_name()
{
std::cout << name << std::endl;
}
};
を基にしてりんご (APPLE)クラスとみかん (ORANGE)サブクラスを作るには次のようにする。
ソース 1-29
class APPLE : public FRUIT
{
public:
APPLE() : FRUIT( "りんご" ){}
};
class ORANGE : public FRUIT
{
public:
ORANGE() : FRUIT( "みかん" ){}
};
int main()
{
APPLE apple;
ORANGE orange;
apple.show_name();
orange.show_name();
return 0;
}
ソース 1-29においてはじめに注目するのは、APPLEや ORANGクラスの定義の後についている「 : public FRUIT 」である。これはは「このクラスは FRUIT クラスを継承している」ということを意味している。つぎに注目するのはコンストラクタのあとについている「 : FRUIT( ”名前” ) 」である。これは「継承したクラスのコンストラクタを呼び出す前にスーパークラスのコンストラクタを呼び出す」ということを意味している。最後に注目するのは apple.show name()と orange.show name()である。このようにサブクラスはスーパークラスのメンバ変数やメンバ関数を文字どおり「継承」して用いることが出来る。
もし派生クラスに新しいメンバ変数やメンバ関数を付け加えたい場合は次のように普通のクラスを作成するのと同様
に関数を定義すれば良い。
ソース 1-30
class APPLE : public FRUIT
17
{
public:
int size;
APPLE() : FRUIT( "りんご" ){
size = 10;
}
void show_size()
{
std::cout << size << " cm" << std::endl;
}
};
int main()
{
APPLE apple;
apple.show_name();
apple.show_size();
return 0;
}
さらに派生クラスでは、つぎのようにスーパークラスのメンバ関数をオーバライドすることもできる。
ソース 1-31
class APPLE : public FRUIT
{
public:
int size;
APPLE() : FRUIT( "りんご" ){
size = 10;
}
void show_name()
{
std::cout << "名前は" << name << "です" << std::endl;
}
};
int main()
{
APPLE apple;
18
apple.show_name();
return 0;
}
この例では、main()で apple.show name()を呼び出すと、スーパークラス (FRUIT)の show name()ではなくて、サブクラス (APPLE)で定義された show name()が呼び出される。
問題 1-14 飲物 (DRINK) クラスは名前 (name) と値段 (yen) というメンバ変数とそれを表示するメンバ関数(show name(),show yen())を持ち、名前と値段はコンストラクタでセットされる。飲物クラスを拡張して任意の飲物のサブクラスを 2つ作成してそれらの名前と値段を表示せよ (10点)
1.2.5 アクセス属性とカプセル化
前節の ソース 1-29 で定義した APPLEクラスには、クラスの外から名前を勝手に書き変えることができるという問題がある。例えば ソース 1-29 の main()関数を
ソース 1-32
int main()
{
APPLE apple;
apple.name = "ぶどう";
apple.show_name();
return 0;
}
の様に書き換えられると「りんご」が「ぶどう」に変わってしまう。
これまで Cだけを習ってきた者にとってはこの現象は特に問題ないと思うかもしれないが、大規模なソフトウェア開発をおこなう際に、他のクラスや関数などからクラスの内部状態 (メンバ変数)を勝手に書き換えられてしまうことは誤動作の原因となるため好ましくないとされる。
そこでオブジェクト指向プログラミングの世界では、他のクラスからクラスの内部状態を勝手に書き換えられないよ
うにするため「カプセル化」と呼ばれる手法を用いることが多い。カプセル化をおこなうために、メンバ変数やメンバ関
数に対して「アクセス属性」を定義してどこまで他のクラスからクラス内部にアクセスすることが出来るかを設定する。
C++のアクセス属性には「public」「private」「protected」の 3つがある。「public」は「他のクラスから自由にアクセスしても良い」という意味であり、これまで例として挙げたクラスのメンバ変数は全て「public」指定をしていたため、クラス外部から自由にメンバ変数を書き換えることが出来た。
一方「private」は「そのクラスの内部からのみアクセス可能」ということを意味している。例えば
ソース 1-33
#include <iostream>
#include <string>
class FRUIT
{
private:
19
std::string name;
public:
FRUIT( std::string n )
{
name = n;
}
};
int main()
{
FRUIT fruit( "ぶどう" );
fruit.name = "腐ったぶどう";
return 0;
}
をコンパイルしようとしても、private指定されている name メンバ変数を main() 関数から書き換えようとしているためコンパイルエラーが出て書き換えることは出来ない。
最後の「protected」は「自分の内部と、自分を継承した派生クラスからのみアクセス可能」を意味している。例えば
ソース 1-34
#include <iostream>
#include <string>
class FRUIT
{
protected:
std::string name;
public:
FRUIT( std::string n )
{
name = n;
}
};
class APPLE : public FRUIT
{
public:
APPLE() : FRUIT( "りんご" ){}
};
int main()
{
APPLE apple;
20
apple.name = "腐ったりんご";
return 0;
}
は main()関数から protected指定されている FRUITクラスの name メンバ変数を書き換えようとしたためコンパイルエラーが出るが、
ソース 1-35
#include <iostream>
#include <string>
class FRUIT
{
protected:
std::string name;
public:
FRUIT( std::string n )
{
name = n;
}
};
class APPLE : public FRUIT
{
public:
APPLE() : FRUIT( "りんご" ){}
void change_name(){ name = "腐ったりんご"; }
};
int main()
{
APPLE apple;
apple.change_name();
return 0;
}
とした場合は、FRUITの派生クラスの APPLE内で nameメンバ変数を書き換えるためコンパイルエラーは生じない。以上のようにしてオブジェクト指向プログラミングではメンバー変数やメンバー関数に適切なアクセス属性を与えて
カプセル化を実現する。なお「メンバ変数とメンバ関数は原則として private属性にすること」。ちなみに何も指定しない場合は自動的に private属性になる。例えば
ソース 1-36
21
class FRUIT
{
std::string name;
public:
FRUIT( std::string n )
{
name = n;
}
};
としたとき、nameは private属性になる。
問題 1-15 前節の問題で作成した DRINKクラスのメンバ変数を privateにしたとき、main()関数から名前や値段を変えようとするとコンパイルエラーが起きることを確認せよ。また、メンバ変数の private指定は外さずに、main()関数から自由に名前や値段を変えたい場合はどうすれば良いか考察して実装せよ。
1.2.6 その他
今回は範囲を越えるので取り扱わないが、オブジェクト指向には「仮想 (ヴァーチャル)関数とポリモーフィズム」という極めて大事な概念がある。これについては各自で調べておくこと。
22
1.3 クラス図を用いた設計
1.3.1 has a 関係と is a 関係と関連
クラス間の関係は「has a(包含)」関係と「 is a(継承)」関係と「関連」で表すことが出来る。「has a」関係はクラスAがクラスBをメンバ変数として持っている (包含している)ことを意味している。つまり「 A
has a B 」ということである。例として次のようなソースコードを考えよう。
ソース 1-37
class TIRE
{
public:
TIRE(){}
};
class CAR
{
TIRE front_right, front_left, rear_right, rear_left;
public:
CAR(){}
};
ソース 1-37 は車 (CAR)クラスが 4つのタイヤ (TIRE)クラスをメンバ変数として持っている。「 is a」関係はクラス Aがクラス Bから継承されて出来ていることを意味している。つまり「A ia a B」ということである。例として次のようなソースコードを考えよう。
ソース 1-38
class CAR
{
public:
CAR(){}
};
class TRUCK : public CAR
{
public:
TRUCK(){}
};
ソース 1-38 はトラック (TRUCK)クラスが車 (CAR)クラスを継承して出来ている。「関連」は「has a」でも「 is a」でもないクラス間の関係を表す。具体的にはクラス Aのメンバ関数の引数としてクラス Bを使用したり、クラスAの中の一時的なローカル変数としてクラス Bを使用している場合などである。例として次のようなソースコードを考えよう。
ソース 1-39
#include <iostream>
23
class CAR
{
public:
CAR(){ std::cout << "車を借りた\n"; }
~CAR(){ std::cout << "車を返した\n"; }
void drive(){ std::cout << "車に乗った\n"; }
};
class HUMAN
{
public:
HUMAN(){}
void drive_car()
{
CAR car;
car.drive();
}
};
int main()
{
HUMAN human;
human.drive_car();
return 0;
}
ソース 1-39 は人 (HUMAN)が車 (CAR)に乗る (void drive car())時に一時的に車を借りてきて乗っていることを意味している。
問題 1-16 ソース 1-39を「has a」関係にせよ。すなわち人 (HUMAN)が自分の車 (CAR)を所有し、void drive car()において自分の車に乗るように変更せよ。
24
1.3.2 クラス図の基本
オブジェクト指向プログラミングでは PAD図やフローチャートではなく「クラス図」によって設計をおこなう。「hasa」や「 is a」などクラス間の関係はこのクラス図によってあらわされるが、ひとつのクラスだけをクラス図で表したものが次の図である。
図 1: クラス図の基本
図 1において、「クラス名」にはそのクラスの名前、「属性」にはメンバ変数名、「操作」にはメンバ関数名を記入する。またメンバ変数やメンバ関数のアクセス属性をそれらの名前の前に記号で記す。「+」が public、「-」が private、「#」がprotectedである。なお、メンバ変数のクラス名、メンバ関数の戻り値や引数、コンストラクタ、デストラクタなどは省略される。例えば次の人 (HUMAN)クラス
ソース 1-40
class HUMAN
{
std::string name;
protected:
void change_name( std::string new_name ){ name = new_name; }
public:
HUMAN(){}
void show_name(){ std::cout << name << std::endl; }
};
をクラス図で表すと次の様になる。
図 2: 人 (HUMAN)クラスのクラス図
25
1.3.3 has a(包含)のクラス図
「has a」関係はひしがた付きの直線でクラスをつないで表される。ひしがたは他のクラスを所有している方のクラス側に付ける。例えば
ソース 1-41
class PENCIL
{
public:
PENCIL(){}
};
class HUMAN
{
PENCIL pencil;
public:
HUMAN(){}
};
は次のように表される。
図 3: has a 関係
1.3.4 is a(継承)のクラス図
「 is a」関係は中抜き三角の矢印でクラスをつないで表される。矢印はスーパークラスの方に付ける。例えば
ソース 1-42
class CAR
{
public:
CAR(){}
};
class TRUCK : public CAR
{
public:
26
TRUCK(){}
};
は次のように表される。
図 4: is a 関係
1.3.5 関連のクラス図
関連は単純に矢印でクラスをつないで表される。矢印は使用するクラス側から使用されるクラス側の方向に付ける。例
えば
ソース 1-43
class PENCIL
{
public:
PENCIL(){}
};
class HUMAN
{
public:
HUMAN(){}
void use_pencil()
{
PENCIL pencil;
}
};
は次のように表される。
27
図 5: 関連
1.4 設計の実際例
この章では、実際にどのようにしてオブジェクト指向プログラミングにおいて設計を行うかを簡単な例を用いて示す。
(1) 仕様作成「テレビ」を作る。チャンネルを切り替えるとテレビの画面にテレビ局の名前が表示される。テレビ局は「ABC局」
「DEF局」の 2局である。実際に「テレビ」のスイッチを入れる (プログラムを実行する)と表示するチャンネルを尋ねてきて、チャンネル番号
を入力すると画面にテレビ局の名前が表示されてまたチャンネルが尋ねられる。「off」と入力すると電源が切れる (プログラムが終了する)。
(2) 機能分割始めに「テレビ」を機能 (役割)別に分解する。
• 必要な機能 (役割)は「TV本体」「画面」「チャンネル」の 3つである。
• 「TV本体」はチャンネルを変更する役割を持つ
• 「画面」は表示の役割を持つ
• 「チャンネル」は対応する TV局の名前を知らせる役割を持つ。
• 「チャンネル」は「ABC局のチャンネル」、「DEF局のチャンネル」の 2種類に分かれる (is a)。
• 「TV本体」は「画面」、「ABC局のチャンネル」及び「DEF局のチャンネル」から出来ている (has a)。
(3) クラス設計機能分割の結果から次の事が言える。
• 必要なクラスは「TV」(TV本体)「SCREEN」(画面)「CHANNEL」(チャンネル)の 3つとなることが分かる。さらに「CHANNEL」は「ABC」(ABC局)、「DEF」(DEF局)の 2クラスに派生する。
• 「TV」のメンバ変数は「SCREEN screen」「ABC abc」「DEF def」の 3つであり全て privateとする。メンバ関数は「void change channel()」(チャンネルを変更する)のひとつであり publicとする。チャンネル変更では offが入力されるまでチャンネル入力と局名表示を繰り返す。
• 「SCREEN」のメンバ変数は無し、メンバ関数は「void show text( std::string text )」(画面に文字列を表示) のひとつであり publicとする。
•「CHANNEL」のメンバ変数は「std::string name」(名前)でありprivateである。メンバ関数は「std::string get name()」(名前取得)であり publicである。
• 「ABC」「DEF」はメンバ変数、メンバ関数無しで、コンストラクタで名前を設定する。
28
以上を表にまとめると次の様になる。
クラス名 基底クラス メンバ変数 メンバ関数
TV -screen +change channel()-abc-def
SCREEN +show text()
CHANNEL -name +get name()
ABC CHANNEL
DEF CHANNEL
(4) クラス図以上の結果をクラス図で表すと次の様になる
図 6: TV クラス図
(5) コーディング以上を C++でコーディングしたものは次のようになる。
ソース 1-44
#include <iostream>
#include <string>
class SCREEN
{
public:
SCREEN(){}
void show_text( std::string text )
{
std::cout << text << std::endl;
}
};
29
//----------------------------------------
class CHANNEL
{
std::string name;
public:
CHANNEL( std::string n ){ name = n; }
std::string get_name(){ return name; }
};
//----------------------------------------
class ABC : public CHANNEL
{
public:
ABC() : CHANNEL( "ABC 局" ){}
};
//----------------------------------------
class DEF : public CHANNEL
{
public:
DEF() : CHANNEL( "DEF 局" ){}
};
//----------------------------------------
class TV
{
SCREEN screen;
ABC abc;
DEF def;
public:
TV(){ std::cout << "スイッチ on\n"; }
~TV(){ std::cout << "スイッチ off\n"; }
void change_channel()
{
for(;;){
std::cout << "チャンネル入力 [ 0 or 1 or off ] : ";
std::string input;
std::cin >> input;
if( input == "0" ) screen.show_text( abc.get_name() );
if( input == "1" ) screen.show_text( def.get_name() );
if( input == "off" ) break;
}
}
};
30
//----------------------------------------
int main()
{
TV tv;
tv.change_channel();
return 0;
}
(6) 実行実際に実行すると次のようになる
スイッチ on
チャンネル入力 [ 0 or 1 or off ] : 0
ABC 局
チャンネル入力 [ 0 or 1 or off ] : 1
DEF 局
チャンネル入力 [ 0 or 1 or off ] : off
スイッチ off
31
1.5 オブジェクト指向プログラミング・レポート課題 計 100点
始めに手書きで良いので (1)~(4)が出来たら持ってくること。OKをもらったら C++でコーディングして (1)~(4)をPCで清書してから提出すること。
(1) 好きなプログラムを書いてよい。作成するプログラムの仕様を書け。(10点)
(2) (1)の仕様から、どのように機能分割するか考えよ。ただし is a関係と has a関係の両方を必ず一回は使用すること。(20点)
(3) (2)の機能分割から、クラス名、基底クラス、メンバ変数とメンバ関数の表を作成せよ。(20点)
(4) (3)の表からクラス図を書け。(20点)
(5) (4)のクラス図を C++のコードに変換せよ。(20点)
(6) 実行した様子を示せ。 (10点)
32