プログラミング能力チェック問題のプログラミング・実行

目次

1  プログラミング能力チェック問題のプログラミング・実行・C++, Javaへの書き換え

1.1 Cプログラム

プログラミング能力チェック問題 のプログラムを実際にC言語によって記述し動作させる。

Linux環境の場合、コンパイル・実行の方法は「Linux等のコンソール上でのCプログラムのgccによるコンパイルと実行」を参考にすること。


課題2 プログラミングテストの解答プログラム

プログラミング能力チェック問題(1), (2)について、抜けているところを埋め完成させたプログラムを作成・実行する。プログラムリストとその出力をレポート (拡張子 txt のテキストファイル) としてまとめて提出すること。

(1)のプログラムを作成・実行しプリントと同じ出力 (出力(元)) となることを確認し、同様に(2)のプログラムを作成・実行し同じ出力 (出力(元)) となることを確認しなさい。その後、

プログラム (1), (2) について、点数データの末尾に 自分の学籍番号が qrCuvwx として、英語 qr, 国語 uv, 数学 wx (それぞれ左側が0のときは0を削除) のとき のデータを追加して実行したときの出力 (出力(新)) を求めなさい。

  • 自分の学籍番号と氏名
  • 課題番号 課題名
  • プログラムリスト(1)
  • プログラムリスト(2)
  • 出力(元) 問題と同一のもの
  • 出力(新) 点数を追加したもの

を続けたテキストファイルを保存し、Web課題提出ページで提出しなさい。


【補足1】Linux環境でのC/C++プログラムの gcc, g++ によるコンパイルと実行

Linux環境では、端末(コンソール)ウィンドウを開き、そこにコマンドを入れることによって、C/C++プログラムのコンパイルと実行を行う。

まずは、cd コマンドによってソースファイルのあるディレクトリをカレントディレクトリとする。

cd   ソースファイルのあるディレクトリ

「ソースファイルのあるディレクトリ」は自分で全部打ってもよいし、途中まで打って TAB キーで補完してもよい。
例えば、デスクトップにファイルを置いた場合のディレクトリは ~/Desktop または ~/デスクトップ のどちらかになり、ドキュメントにファイルを置いた場合のディレクトリは ~/Documents または ~/ドキュメント のどちらかになる。

次にコンパイルを行う。例えば、Cのソースファイルが「プログラム.c」という名前だとすると、

gcc  -O3  -Wall  -o プログラム   プログラム.c

というコマンドによって、ソースファイル「プログラム.c」がコンパイルされ、実行ファイル「プログラム」が生成される。
※ -O3 (大文字のオーに数字の3) オプションはレベル3の最適化をする指定で、実行速度を上げるために指定しておく。
※ -Wall オプションは警告(warning)をすべて表示させるもので、バグを見つけるのに有効な場合もあるので指定しておく。

C++ソースのコンパイルの場合、コマンドが g++ に変わる。ソースファイルが「プログラム.cpp」という名前だとすると、

g++  -O3  -Wall  -o プログラム   プログラム.cpp

となる。
C++11, C++14, C++17, C++20 等の新しい機能を使っているソースのコンパイルには

g++  -O3  -Wall -std=c++17  -o プログラム   プログラム.cpp
※ C++17 を指定する場合

のように、C++20用には -std=c++20 (または -std=c++2a)、C++17用には -std=c++17 (または -std=c++1z)、C++14用には -std=c++14 (または -std=c++1y)、C++11用には -std=c++11 等のオプションを指定しなければならない場合がある。指定すべきオプションは使用中の GCC によって異なる可能性がある。
オプション -std=c++11 などを指定しても nullptr がないエラーが出る場合は GCC のバージョンがかなり古いのでコンパイルは難しいと思われるが、念のため、さらに -Dnullptr=0 というオプション指定を追加してコンパイルを試みてみること。

生成された実行ファイル「プログラム」(カレントディレクトリにあるとする) を実行するには

./プログラム

のように、頭に ./ をつけてコマンド入力すればよい。

./プログラム   >   出力ファイル名

とすると、出力を指定したファイルにリダイレクトできる。


【補足2】Windowsで Visual Studio がインストールされている環境でのC/C++プログラムの cl コマンドによるコンパイルと実行

Windowsで Visual Studio がインストールされている環境では、スタートメニューから 「Visual Studio用のコマンドプロンプト」または「Visual Studio用のPowerShell」を起動し、そこにコマンドを入れることによって、C/C++プログラムのコンパイルと実行を行える。

まずは、/D オプションをつけた cd コマンドによってソースファイルのあるドライブおよびディレクトリをカレントドライブ・カレントディレクトリとする。

cd  /D   ソースファイルのあるディレクトリ      (コマンドプロンプトの場合)
cd     ソースファイルのあるディレクトリ      (PowerShellの場合)

「ソースファイルのあるディレクトリ」は自分で全部打ってもよいし、エクスプローラでソースのあるフォルダを開きアドレスをコピーし、コマンドプロンプト (PowerShell) に貼り付けてもよい。例えば、デスクトップにファイルを置いた場合のディレクトリは、一般的な環境では C:\Users\ユーザ名\Desktop となる。

次にコンパイルを行う。例えば、Cのソースファイルが「プログラム.c」という名前だとすると、

cl   /W3   プログラム.c
 
(最適化をする場合)
cl   /Ox  /W3   プログラム.c

というコマンドによって、ソースファイル「プログラム.c」がコンパイルされ、実行ファイル「プログラム.exe」が生成される。

※ /W3 オプションは警告(warning)を表示させるもので、必須ではないが、バグを見つけるのに有効な場合もあるので指定しておくとよい。
/Ox (大文字のオーに小文字のx) オプションは最大限の最適化を行なう指定で、一般には、高速な実行コードが生成されるので、実行速度を上げるために指定しておく。

C++の場合、ソースファイルが「プログラム.cpp」という名前だとすると、

cl   /Ox  /W3   プログラム.cpp
 
(/EHsc が必要と言われる場合)
cl   /Ox  /W3  /EHsc   プログラム.cpp

となる。

生成された実行ファイル「プログラム.exe」(カレントディレクトリにあるとする) を実行するには、コマンドプロンプトの場合、単に

プログラム

とコマンド入力すればよい。

PowerShellの場合、

.\プログラム

のように、先頭に .\ をつけて実行させる。

リダイレクトはLinuxの場合と同様に実行できる。

【補足終り】

1.2 C++プログラムへの書き換え


課題3 C++への書き換え

課題2 (1), (2) のプログラムそれぞれを C から C++ に書き換える。
C言語の元ファイルをコピーし、ファイル名を「〜.cpp」として、以下に示す最低限の変更点は必ず修正しなさい。その他の C++ 的な変更も自由にやってよい。

(2)のプログラムは、元のCのものを活かした書き換え方 (2a) と、C++ のライブラリ STL のリストを使った書き換え方 (2b)、さらに、C++11以降の機能・記述法を使った書き換え方 (2b2) をやりなさい。

まず、(1) と (2a) の書き換えについて説明する。
いずれも、ここに示した方法だけでなく、様々な書き換え方があるので、以下の説明通りに変換せず、自分の考えで書き換えてよい。

  1. C++ではC言語の標準関数も利用できるが、ヘッダを変更し、名前空間 std を指定する。

    ★ヘッダ

    #include <stdio.h>
    

    #include <cstdio>
    

    に変更。
    同様に、

    #include <stdlib.h>
    

    がある場合は

    #include <cstdlib>
    

    に変更。

    ★名前空間の指定

    Cの標準関数は、C++では std という名前空間に入るので、使用箇所にすべて std:: をつけて指定するか、using 宣言 で前方でまとめて指定する。 using ディレクティブという名前空間を一括で取り込む書き方もあるが、意図しない名前が取り込まれてしまう危険性があり非推奨。

    例えば、必要に応じ、ソース先頭近くに以下のような using 宣言を置けばよい。

    using std::printf;
    
    using std::malloc;
    

    など。

    以下では、もう一つの方法である、使用箇所にすべて std:: をつけて指定する書き方を示す。

    printf
    

    関数呼び出しを

    std::printf
    

    に変更。
    同様に、

    fputs および exit
    

    関数呼び出しがある場合は

    std::fputs および std::exit
    

    に変更。
    同様に、

    malloc
    

    関数呼び出しがある場合は

    std::malloc
    

    に変更。なお、malloc の呼び出し箇所

    std::malloc(sizeof(Bucket))
    

    において、C++では型が合わないエラーになるので、以下のように型キャストして void * から Bucket * に変換する。

    static_cast<Bucket *>(std::malloc(sizeof(Bucket)))
    
  2. 定数マクロをconstに

    C++では #define の使用は推奨されていない。

    #define  MAXT  100 /* 一科目の満点 */
    #define  KAMOKU  3 /* 科目数 (固定) */
    #define  MAXG  (MAXT*KAMOKU) /* 合計の満点 */
    

    const int MAXT = 100; // 一科目の満点
    const int KAMOKU = 3; // 科目数 (固定)
    const int MAXG = MAXT*KAMOKU; // 合計の満点
    

    に変更。

    また、N を #define している

    #define  N  (sizeof data / sizeof data[0])    /* 人数 */
    

    const int N = (sizeof data / sizeof data[0]);   // 人数
    

    に変更。

  3. NULL マクロを nullptr に

    C++では NULL マクロの使用は推奨されず、代わりに、無効なポインタであることを明示する nullptr を使うべきとされる。

    ソース中の NULL をすべて単純に nullptr に変更すればよい。

  4. 点数の構造体をクラスに変え、内部データを隠蔽しクラスの外部から変更されないようにする。生成時に合計点も計算し内部データとして持つ。
    各点数等は、いわゆるgetterメソッドを公開し、外部から利用できるようにする。(次項目参照)
    また、staticメンバ変数を使って全体の集計も行うように変更し、staticメソッドによって外部から参照できるようにする。

    struct Tensu { /* 各科目の点数を表す構造体 */
    	unsigned char eigo;
    	unsigned char kokugo;
    	unsigned char sugaku;
    } data[] = { /* 各人の得点データ */
    	{54, 85, 56}, {49, 92, 54}, {91, 85, 19}, {65, 47,100}, {72, 87, 53},
    	{64, 81, 51}, {60, 79, 57}, {45, 99, 51}, {76, 60, 70}, {58, 49, 65},
    	各自追加分
    };
    

    // 各科目の点数を表すクラス。staticメンバ変数によって全体の集計も行う
    class Tensu {
    private: // この部分はクラスの外部には見えない
    	const unsigned char eigo_;
    	const unsigned char kokugo_;
    	const unsigned char sugaku_;
    	const unsigned short gokei_; // 合計点
    
    	static int ninzuu_;
    	static int eigomin_;
    	static int eigomax_;
    	static int eigokei_;
    	static int kokugomin_;
    	static int kokugomax_;
    	static int kokugokei_;
    	static int sugakumin_;
    	static int sugakumax_;
    	static int sugakukei_;
    	static int gokeimin_;
    	static int gokeimax_;
    
    public: // この部分はクラスの外部から利用できる
    	// コンストラクタ
    	Tensu(int eigo, int kokugo, int sugaku) : eigo_(eigo),
    			kokugo_(kokugo), sugaku_(sugaku),
    			gokei_(eigo + kokugo + sugaku) {
    		++ninzuu_;
    		eigokei_ += eigo_;
    		if ( eigo_ > eigomax_ ) eigomax_ = eigo_;
    		if ( eigo_ < eigomin_ ) eigomin_ = eigo_;
    		kokugokei_ += kokugo_;
    		if ( kokugo_ > kokugomax_ ) kokugomax_ = kokugo_;
    		if ( kokugo_ < kokugomin_ ) kokugomin_ = kokugo_;
    		sugakukei_ += sugaku_;
    		if ( sugaku_ > sugakumax_ ) sugakumax_ = sugaku_;
    		if ( sugaku_ < sugakumin_ ) sugakumin_ = sugaku_;
    		if ( gokei_ > gokeimax_ ) gokeimax_ = gokei_;
    		if ( gokei_ < gokeimin_ ) gokeimin_ = gokei_;
    	}
    
    	int eigo() const { return eigo_; }
    	// TODO: ここに上と同様に国語と数学の点数を求めるメソッド kokugo() および sugaku() を記述
    
    	int gokei() const { 
    		// TODO: ここに合計点を求めるメソッド gokei() の中味を記述
    	}
    
    	// staticメソッド
    	static int ninzuu() { return ninzuu_; }
    	static int eigomin() { return eigomin_; }
    	static int eigomax() { return eigomax_; }
    	static int eigokei() { return eigokei_; }
    	static int kokugomin() { return kokugomin_; }
    	static int kokugomax() { return kokugomax_; }
    	static int kokugokei() { return kokugokei_; }
    	static int sugakumin() { return sugakumin_; }
    	static int sugakumax() { return sugakumax_; }
    	static int sugakukei() { return sugakukei_; }
    	static int gokeimin() { return gokeimin_; }
    	static int gokeimax() { return gokeimax_; }
    	static double eigoheikin() { return static_cast<double>(eigokei_) / ninzuu_; }
    	static double kokugoheikin() { return static_cast<double>(kokugokei_) / ninzuu_; }
    	static double sugakuheikin() { return static_cast<double>(sugakukei_) / ninzuu_; }
    	static double gokeiheikin() {
    		return static_cast<double>(eigokei_ + kokugokei_ + sugakukei_) / ninzuu_;
    	}
    };
    
    // staticメンバ変数の定義と初期値の設定
    /* static */ int Tensu::ninzuu_ = 0;
    /* static */ int Tensu::eigomin_ = MAXT;
    /* static */ int Tensu::eigomax_ = 0;
    /* static */ int Tensu::eigokei_ = 0;
    /* static */ int Tensu::kokugomin_ = MAXT;
    /* static */ int Tensu::kokugomax_ = 0;
    /* static */ int Tensu::kokugokei_ = 0;
    /* static */ int Tensu::sugakumin_ = MAXT;
    /* static */ int Tensu::sugakumax_ = 0;
    /* static */ int Tensu::sugakukei_ = 0;
    /* static */ int Tensu::gokeimin_ = MAXG;
    /* static */ int Tensu::gokeimax_ = 0;
    
    Tensu data[] = { // 各人の得点データ
    	Tensu(54, 85, 56), Tensu(49, 92, 54), Tensu(91, 85, 19), Tensu(65, 47,100), Tensu(72, 87, 53),
    	Tensu(64, 81, 51), Tensu(60, 79, 57), Tensu(45, 99, 51), Tensu(76, 60, 70), Tensu(58, 49, 65),
    	各自追加分
    };
    

    に変更。

    ※ ただし、最後の data 配列の中味は、「課題2の出力(新)」と同一内容のものとすること。

  5. 上記変更に伴い、関数 getGokei は削除。
    Tensuクラスに合計点を求めるメソッド gokei() を記述する。
    getGokei( &data[i] ) は data[i].gokei() に変更できる。
    あるいは、main関数の中の変数 gokei を参照している箇所をすべて data[i].gokei() に変更すれば、変数 gokei 自体が不要になる。

    Tensuクラスに英語の点数を求めるメソッド eigo() を記述。
    main関数の中での data[i].eigo は、メソッド利用 data[i].eigo() に変更。kokugo, sugaku も同様。

    main関数の中の 〜max, 〜min, 〜gokei および平均を集計している箇所を削除。
    main関数の先頭付近で定義されている変数

    	int gokei, gokeimax, gokeimin;
    	int eigokei, kokugokei, sugakukei;
    	int eigomax, kokugomax, sugakumax;
    	int eigomin, kokugomin, sugakumin;
    

    は不要なので、削除。
    ただし、変数 gokei は残す書き方もできる。上の説明を参考にすること。

    最後の表示部分では、
    英語ならば、Tensuクラスの eigomin(), eigomax(), eigokei(), eigoheikin() 各staticメソッドを使って最低点、最高点、合計点、平均点を求めるように変更。
    例えば、英語平均点は Tensu::eigoheikin() で求まる。doubleへの型キャストは不要である。
    国語、数学、合計点についても、英語と同様に変更。


※ 補足1 : コンストラクタ初期化子
C++のクラスのコンストラクタでは、メンバ変数に値を設定するためのコンストラクタ初期化子の利用が推奨されている。(上記サンプルコードでは下記の赤字部分)

	Tensu(int eigo, int kokugo, int sugaku) : eigo_(eigo), 
				kokugo_(kokugo), sugaku_(sugaku), 
				gokei_(eigo + kokugo + sugaku) {

動作としてはメンバ変数に値を設定するもので、上記では

	Tensu(int eigo, int kokugo, int sugaku) {
		eigo_ = eigo; 
		kokugo_ = kokugo; 
		sugaku_ = sugaku; 
		gokei_ = eigo + kokugo + sugaku;

と記述したのと同様の動作となる。ただし、constメンバ変数・参照メンバ変数などの値設定にはコンストラクタ初期化子を使わないといけないので、この書き方はエラーになる。

※ 補足1 終り


※ 補足2 : メソッドのconst指定
C++では、staticでないすべてのメンバ変数の内容を書き換えないメソッド(staticでないもの)を const 指定できる。
(上記サンプルコードでは、例えば下記の赤字部分)

	int eigo() const { return eigo_; }

const指定されたメソッドは、呼び出しによりインスタンスの状態に変化が無いことが保証される。

※ 補足2 終り



次に、(2b) のSTLを使う書き換えについて説明する。

STLには要素の集合を扱うコンテナが数種ある。

素朴でよく使われるコンテナとして、可変長の配列のように使える

std::vector

がある。以下では std::vector を単に vector と記す。vector は以下のヘッダを取り込んで利用する。

★ヘッダ

#include <vector>

まずは、int の 配列 jun を vector を使って書き換える。
vector を使うと、

	int jun[N];  /* 順位 */

の部分は、

	std::vector<int> jun(Tensu::ninzuu()); // 順位

のように書き換えられる。
STLでは テンプレート によって種々の型用のコンテナを

<型パラメータ>

で指定して利用する。<int> は、対象とするコンテナが int 用のコンテナであることを示す。
上に示した変数 jun の定義によって、Tensu::ninzuu() 個の int 要素を持った vector が生成される。

なお、上の変更に伴い、N の定義の行

const int N = (sizeof data / sizeof data[0]);   // 人数

を削除し、ソースの中で N を使用している部分をすべて Tensu::ninzuu() に置き換える。

変数 bucket についても vector を使うことも可能だが、各 bucket リストの要素数は大きく異なり得るし、最初、要素の追加が頻繁に起こるので、vector は効率的でない。
今回は、bucket については、本来は双方向リストだが、単純な単方向リストの機能も内包している

std::list

を使用する。以下では単に list と記す。list は以下のヘッダを取り込んで利用する。

★ヘッダ

#include <list>


今回は list を使用するので、(2a)で使っている Bucket は使用しない。定義部分・使用部分はすべて削除する。
まず、

/* 線形リストのための自己参照構造体型 */
typedef struct Bucket {
	struct Bucket *next;
	int bangou;
} Bucket;

の部分は削除する。

変数 bucket は list の配列 (大きさ MAXG + 1) として定義する。宣言をしている

	Bucket *bucket[MAXG + 1]; /* バケット */

の部分は、

	std::list<int> bucket[MAXG + 1]; // バケット

のように書き換える。
このlist配列変数の定義によって、個々の要素はデフォルトコンストラクタで初期化されるため空のリストとなる。従って、main関数の最初の方にある

	for ( int b = 0; b <= MAXG; ++b )
		bucket[b] = nullptr;

は不要になる。

その後の for 文の中のリスト処理をすべて削除し、list を使ったものに書き換える。
list への追加は push_front メソッド (リストの先頭への追加) または push_back メソッド (リストの末尾への追加) によって行う。
変数 g が int 型で合計点、変数 i が得点データ配列の int 型インデックスを表しているとすると、

		bucket[g].push_back(i);

で、リストの末尾に追加登録できる。

bucketデータを基にして、点数の高い方から低い方に順位をつけていく部分

	Bucket *p = bucket[b];
	int ii = 0;
	for ( ; p != nullptr; ++ii, p = p->next ) {
		jun[p->bangou] = j;
	}
	j += ii;

は、例えば以下のように書き換えられる。

	unsigned evenscores = bucket[b].size();
	if (evenscores > 0) {
		for (std::list<int>::const_iterator it = bucket[b].begin();
				it != bucket[b].end(); ++it) {
			jun[*it] = j;
		}
		j += evenscores;
	}

list の要素数は size メソッドで調べられる。
また、コンテナ内の要素に対する繰り返し処理のために、一般的には iterator を、コンテナ内の要素を書き換えないのであれば const_iterator を、それぞれ使用する。C++ の iterator (および const_iterator) は、間接参照やインクリメントがポインタと同様の記法で記述できる。

コンテナに対するiterator および const_iterator の型は

コンテナの型::iterator
コンテナの型::const_iterator

となる。

コンテナ.begin()

で先頭要素を指すiteratorが求まり、

コンテナ.end()

で最後の要素の一つ先を指すiteratorが求まる。iteratorのインクリメントは、特に後置形式にする必要性がない場合は、上のソース例 ++it に示したように前置形式の ++ を使用する。

C++11以降では、イテレータを使ってコンテナの要素内容を書き換えない場合は、

コンテナ.cbegin()

で先頭要素を指す const_iterator を求め、

コンテナ.cend()

で最後の要素の一つ先を指す const_iterator を求めて使用する。



C++11 以降のstd::forward_list、その他の新しい機能・記述法の利用

C++言語では、新しい規格として C++11 規格 および C++14 規格 が制定された。C++11 は 2011年8月に、C++14 は 2014年8月に制定されたが、2015年9月現在では、C++11/C++14規格に準拠していない処理系も少し残っているが、対応済みの処理系が多くなっている。

上のコードでは std::list を線形リストとして使用したが、C++11 以降では、単方向リストとして、機能を最低限に絞る代りにオーバーヘッドを極力なくした std::forward_list を使うことができる。以下では単に forward_list と記す。

上で作った C++ の (2b) のソースファイルをコピーし、別の名前とする。以下では、プログラム (2b2) で表す。
(2b2) に以下の変更を加えていく。
(2b) で使っていた std::list の代わりに、std::forward_list を使うため、

★ヘッダ

#include <forward_list>

で取り込む。#include <list> は不要となる。
また、変数 bucket の定義部分は、

	std::forward_list<int> bucket[MAXG + 1]; // バケット

とする。
forward_list には push_back メソッドはない。forward_list への追加は push_front メソッド (リストの先頭への追加) によって行う。push_backメソッドの呼び出し部分を push_front メソッドに置き換える。
変数 g が int 型で合計点、変数 i が得点データ配列の int 型インデックスを表しているとすると、

	bucket[g].push_front(i);

で、リストに登録できる。

bucketデータを基にして、点数の高い方から低い方に順位をつけていく部分は、

	unsigned evenscores = 0;
	for (std::forward_list<int>::const_iterator it = bucket[b].cbegin();
			it != bucket[b].cend(); ++it, ++evenscores) {
		jun[*it] = j;
	}
	j += evenscores;

のようにすればよい。forward_list には、要素数を調べる size メソッドがないので、繰り返しの中で個数をカウントしている。

また、C++11 の 型推論 と C++14 の std::cbegin 関数, std::cend 関数 を使えば、

	unsigned evenscores = 0;
	for (auto it = std::cbegin(bucket[b]);
			it != std::cend(bucket[b]); ++it, ++evenscores) {
		jun[*it] = j;
	}
	j += evenscores;

のように記述を短くできる。

さらに C++11 の range-based for 構文を使えば、上の for ループの部分を、iteratorを使わずに、

	for (int i : bucket[b]) {
		jun[i] = j;
		++evenscores;
	}

のように簡潔に記述できる。

C++11 の STL では、固定長配列の代替となるコンテナとして std::array を使うことができる。ここでは、backet配列を std::array を使って書き換える。

★ヘッダ

#include <array>

として利用する。

bucket の定義部分は、配列を std::array に置き換えると、

	std::array<std::forward_list<int>, MAXG + 1> bucket; // バケット

となる。使い方は配列のときと同じである。

また C++11 では、データ (配列、STLコンテナも含む) の初期化を 統一的な初期化構文 で指定できるようになった。
今回のソースでは、data の初期化部分を統一的な初期化構文を使うことによってより簡潔に記述すること。



(1)のプログラムを作成・実行しプリントと同じ出力となることを確認し、同様に (2a), (2b), (2b2) のプログラムを作成・実行し同じ出力となることを確認しなさい。その後、

  • 自分の学籍番号と氏名
  • 課題番号 課題名
  • プログラムリスト(1)
  • プログラムリスト(2a)
  • プログラムリスト(2b)
  • プログラムリスト(2b2)
  • 出力(新)

を続けたテキストファイルを保存しWebから提出すること。


1.3 Javaプログラムへの書き換え

Javaの配列については扱いがC言語と異なる点があるので、以下の説明を参考にする。

1.3.1 Javaにおける配列

Javaでは、配列は「配列オブジェクト」として扱われる。
使い方は C/C++ と似ている。インデックスは 0 から数える。

※ ただし、例えば a を一次元配列としたとき、C/C++ で成立する
a[3] ≡ *(a + 3) ≡ *(3 + a) ≡ 3[a]
のような関係は成り立たない。a[3] の形式のみ許される。

多次元配列は、配列の要素を配列とすることによって作る。

データ型[ ] 配列名;   または
データ型 配列名[ ];

データ型元となる要素の型
配列名その配列を表す変数名
 配列名[インデックス] の形で個々の要素を指し示す。

配列型の変数を作成する。変数自体の値は null (インスタンス変数またはクラス変数のとき) または不定 (ローカル変数のとき)。

※ null はその変数に有効なオブジェクトが結び付けられていないことを示す特殊な値。
  null は英語発音では nʌ́l (ナル) だが、日本では ヌル と言う人が多い。

データ型[ ] 配列名;  も   データ型 配列名[ ];  も同じ意味。以下でも同様。
Java の配列の意味をよく表しているのは前者なので、ここでは前者のみ用いる。


データ型[ ] 配列名 = new データ型[サイズ];

データ型元となる要素の型
配列名その配列を表す変数名
 配列名[インデックス] の形で個々の要素を指し示す。
サイズその配列の要素数

大きさ「サイズ」の配列を宣言する。配列名[0] から配列名[サイズ - 1] までのサイズ個の要素が確保される。
要素の初期値は 0 (整数型・浮動小数点型)、'\u0' (char型)、false (boolean型)、null (String型などのオブジェクト型)


データ型[ ] 配列名 = { 初期化データの並び };

データ型元となる要素の型
配列名その配列を表す変数名
 配列名[インデックス] の形で個々の要素を指し示す。
初期化データの並びその配列を初期化するデータをコンマで区切って並べる。

「初期化データの並び」で指定される配列を宣言する。配列の大きさは初期化データの大きさになる。


配列名.length

その配列の大きさを表す。


int[ ] a = new int[10];

int型の要素10個を持つ配列 a を作る。要素は a[0] から a[9] までで、すべて 0 になっている。
a.length は 10 になる。


double[ ] x = { 11.4, 83.5, 16.3, 20.7 };

double型の要素4個を持つ配列 x を作る。要素は x[0] (値 11.4) から x[3] (値 20.7) まで。
x.length は 4 になる。


String[ ] sel = new String[3];

String型の要素3個を持つ配列 sel を作る。
要素は sel[0], sel[1], sel[2] で、すべて null になる。空文字列 "" で初期化されるわけではないので注意。


String[ ] msg = { "No", "Yes" };

String型の要素2個を持つ配列 msg を作る。
要素は msg[0] が値 "No"、 msg[1] が値 "Yes" に初期化される。


double[ ][ ] mat = new double[5][3];

double型のサイズ5×3の2次元配列 mat を作る。
実際には、「サイズ3のdouble型配列」を5個持つ配列、となる。
要素はすべて 0 に初期化される。


課題4 Javaへの書き換え

課題3 (1) および (2b) のプログラムについて、書き換えたC++のコードを参考に、さらに Java に書き換える。
C++言語の元ファイル(1)をコピーし、ファイル名を「メインクラス名.java」とする。( メインクラス名 部分は実際には半角英数で適当なクラス名とすること)
ファイル「メインクラス名.java」を下記のように修正する。

  • クラス Tensu を C++ のコードを参考に記述する。
    上記のC++サンプルコードと同様、インスタンス変数、static変数はすべて private とすること。

    class Tensu {
    
    
    ‥‥‥‥‥
    
    
    }
    

    (1), (2b) のどちらのプログラムでも、ここに示した方法だけでなく、様々な書き換え方があるので、以下の説明通りに変換せず、自分の考えで書き換えてよい。

    Javaには unsigned char 型は無く、char型はあるが一般的な整数として使うものではない。そこで、unsigned char の代りに byte, short, int, long などの整数型を使うこと。
    一般的なC/C++では char は8ビットなので、unsigned char は 0 〜 255 の範囲となる。Javaの byte は符号付き8ビットで -128 〜 127 の範囲なので、unsigned char の完全な代替にはならない。

    C++ではstatic変数の定義のように、ソースコード上は class の外部に当該クラスのstatic変数、コンストラクタ、メソッドの実装を書く記法も使用されるが、Javaではコードをクラスの中に書かなければならない。

    例えば、Tensuクラスのstatic変数の初期値設定部分は、C++では下記コードでクラスの外部にある。

    // staticメンバ変数の定義と初期値の設定
    /* static */ int Tensu::ninzuu_ = 0;
    /* static */ int Tensu::eigomin_ = MAXT;
    /* static */ int Tensu::eigomax_ = 0;
    /* static */ int Tensu::eigokei_ = 0;
    /* static */ int Tensu::kokugomin_ = MAXT;
    /* static */ int Tensu::kokugomax_ = 0;
    /* static */ int Tensu::kokugokei_ = 0;
    /* static */ int Tensu::sugakumin_ = MAXT;
    /* static */ int Tensu::sugakumax_ = 0;
    /* static */ int Tensu::sugakukei_ = 0;
    /* static */ int Tensu::gokeimin_ = MAXG;
    /* static */ int Tensu::gokeimax_ = 0;
    

    Javaでは、クラス内のstatic変数定義部分 (下記) を編集し「 = 値」によってそれぞれの初期値を設定する。クラス外部のコードは削除する。

    	static int ninzuu_;
    	static int eigomin_;
    	static int eigomax_;
    	static int eigokei_;
    	static int kokugomin_;
    	static int kokugomax_;
    	static int kokugokei_;
    	static int sugakumin_;
    	static int sugakumax_;
    	static int sugakukei_;
    	static int gokeimin_;
    	static int gokeimax_;
    

    C++では static_cast などCには無かったキャスト指定があり使用が推奨されているが、Javaでは使えない。Javaでのキャストは、Cと同じ

    (    型    )
    

    によって行う。

    C++で使用していたコンストラクタ初期化子は、Javaでは使用できない。(上記C++サンプルコードでは下記の赤字部分)

    Tensu(int eigo, int kokugo, int sugaku) : eigo_(eigo), 
    			kokugo_(kokugo), sugaku_(sugaku), 
    			gokei_(eigo + kokugo + sugaku) {
    

    Javaでは、決まった値に設定するなら変数の定義部分に「= 値」を付けて指定する。インスタンスによって設定する値が変わる場合などは、コンストラクタの本体部分で値を代入する。

    また、C++で使用していたメソッドのconst指定は、Javaでは使用できないので削除する。
    一方、値を変更しない変数は const の代りに final を指定する。

    C++でconst変数として定義していた MAXT, KAMOKU, MAXG は、Tensu クラスの中で public static final として定数定義する。
    これらの定数はTensuクラスの外部からは、Tensu.MAXT のように「クラス名.」をつけて指定する。

  • データ配列 data (static とする) や main関数 (public static void main(String[] args) メソッドとする) を含むメインクラス

    public class メインクラス名 {
    
        // メインメソッドで使うstatic変数があれば、
        // この部分で定義する
        ‥‥‥‥‥
    
        // メインメソッド
        // void型なので、「return 返り値」はなし
        public static void main(String[] args) {
            
            
            ‥‥‥‥‥
            
            
        }
    }
    

    を作り、Tensuクラスの前に移動して、ファイル先頭にあるクラスとする。

    メインクラスのインスタンス変数、static変数はすべて private とすること。

    なお、data配列の大きさ (Cプログラムで N で表していたもの) は、Javaのdata配列では data.length で求まる。

    data配列の個々の要素はTensuクラスのオブジェクトでなければならないが、Javaでは C++ の書き方ではエラーになる。 各要素は、

    new Tensu(54, 85, 56)
    

    のように、new をつけて生成したオブジェクトを指定する必要がある。

    ちょっと考えてみれば分かるが、「staticメソッド内で扱えるのはstatic変数とローカル変数のみで、インスタンス変数は扱えない」ことに注意すること。

    C++での

    クラス名::staticメソッド名()
    

    は、Javaでは

    クラス名.staticメソッド名()
    

    と記述する。

以上の説明を参考に中身を変更後、

javac    メインクラス名.java

のようにコマンドを入れて javac でコンパイルする。

ソースファイルのエンコーディングとしては、Linux 環境では UTF-8 で、Windows 環境では シフトJIS で保存する。 ただし、2022年のJava 18以降をインストールして使っている場合は、Windows環境でもファイルを UTF-8 で保存する。

※ ソースファイルのエンコーディングが、システムの規定のものでない場合は、

javac    -encoding  エンコーディング指定   メインクラス名.java

とする必要がある。
エンコーディング指定には、UTF-8, Shift_JIS, windows-31j などがある。
例えば、Java 18 より前の Windows 環境で UTF-8 でソースファイルを保存した場合は、

javac    -encoding  UTF-8   メインクラス名.java

とする必要がある。
ただし、2022年のJava 18以降をインストールして使っている場合は、Windows環境でもシステム規定のエンコーディングが UTF-8 となっているので、ファイルを UTF-8 で保存し、

javac    メインクラス名.java

でコンパイルすることができる。

コンパイルでエラーがなければ、

java    メインクラス名

のようにコマンドを入れて、プログラムを実行させる。

実行させ、CやC++のプログラムと同じ出力となることを確認しなさい。

※※ Java 11 以降では、単一ソースファイルで、メインクラスがソースの先頭にあるファイルの場合、javac コマンドによるコンパイル過程をスキップして、単に

java    メインクラス名.java

だけで実行できる。(コンパイル処理は内部的になされる)
※※

その後、

  • 自分の学籍番号と氏名
  • 課題番号 課題名
  • プログラムリスト(1)   (C++の(1)を基にしたもの)
  • プログラムリスト(2b)   (C++の(2b)を基にしたもの)
  • 出力(新)

を続けたテキストファイルを保存しWebから提出すること。


(2b)では、出力が変わらない限り、Java に特化した変更も自由にやってよい。

※ Java (SE 5以降) では C の printf (C++ の std::printf) に相当するものが存在する。一般的には、String.format メソッドなどがあるが、簡単には

System.out.printf      (正確には java.lang.System.out.printf)

を使えば、Cのprintfと同様の出力ができる。ただし、Javaの場合は改行を %n で指定する。


C++ (2b) の書き換えでは、C++で STL コンテナを使っていた変数 bucket を、Javaのコレクション java.util.List インタフェース および実体クラス java.util.ArrayList あるいは java.util.LinkedList で置き換える。

なお、C++ の STL では、int配列 jun を std::vector<int> で置き換えたが、Java ではint配列のままにしておく。

例えば、LinkedList を使うとすると、ソースの先頭部分で

import java.util.List;
import java.util.LinkedList;

をしておき、C++での変数宣言

std::list<int> bucket[MAXG + 1]; // バケット

の部分 (他のコンテナを使っている場合もある) を、例えば

@SuppressWarnings("unchecked")
private static List<Integer>[] bucket = new List[Tensu.MAXG + 1]; // バケット

のように書き換える。
Javaでは Generics によって種々のオブジェクト用のコンテナを

<型パラメータ>

で指定して利用する。<Integer> は、対象とするコンテナが Integer クラスのオブジェクト用のコンテナであることを示す。

プリミティブ型 (int, long, double, 等) の値に対応するオブジェクトを表す不変クラスをラッパークラス (wrapper class)と言うが、Integer は int のラッパークラスである。

JavaのGenericsではプリミティブ型は指定できないのでラッパークラスを指定して利用する。
プリミティブ型の値とラッパークラスのオブジェクトとは auto boxing/unboxing で相互に自動変換してくれる。int/Integerの場合、int → Integer : boxing, Integer → int : unboxing という相互変換となる。

配列の個々の要素は初期化されないので、

	for ( int b = 0; b <= Tensu.MAXG; ++b )
		bucket[b] = new LinkedList<Integer>();

のように、明示的に空リストとして初期化しておく必要がある。


リストへの追加は add メソッドによって行う。パラメータとして要素一個のみを指定するとリストの末尾に要素が追加される。
変数 g が int 型で合計点、変数 i が得点データ配列の int 型インデックスを表しているとすると、

		bucket[g].add(i);

で、リストに登録できる。

リストの要素数は size メソッドによって調べることができる。
bucketデータを基にして、点数の高い方から低い方に順位をつけていく部分は

	int evenscores = bucket[b].size();
	if ( evenscores > 0 ) {
		for (int i : bucket[b]) {
			jun[i] = j;
		}
		j += evenscores;
	}

のように、拡張for文を使ってコンテナの中の要素を順に取り出して処理できる。

拡張forを使わずに、iteratorを使って繰り返す方法

	for (java.util.Iterator<Integer> it = bucket[b].iterator();
			it.hasNext(); ) {
		jun[it.next()] = j;
	}

もある。



さらなる書き換え:
上の書き方では、bucketはListの配列になっているが、

@SuppressWarnings("unchecked")
private static List<Integer>[] bucket = new List[Tensu.MAXG + 1];

のようにしか記述できず、

private static List<Integer>[] bucket = new List<Integer>[Tensu.MAXG + 1];

とするとエラーになる。
このように、配列を使うとGenericsの型情報が保持されないので、推奨できる記述法ではない。

そこで、bucket を List の List にする書き方が望ましい。例えば、ソースの先頭部分で

import java.util.List;
import java.util.ArrayList;
import java.util.LinkedList;

をしておき、bucket を List の ArrayList として以下のように宣言する。

private static ArrayList<List<Integer>> bucket
	= new ArrayList<List<Integer>>(Tensu.MAXG + 1); // バケット

bucketの外側のインデックスに対応する各要素 (LinkedListオブジェクトとする) は

	for ( int b = 0; b <= Tensu.MAXG; ++b )
		bucket.add(new LinkedList<Integer>());

のように、初期化しておく。bucketの外側のインデックスに対応する要素は、配列にしていたときは、

	bucket[ インデックス ]

だったが、この書き方では外側もリストにしているので、

	bucket.get( インデックス )

で求められる。bucket配列の添え字形式で指定している部分をすべて get メソッドに置き換えればよい。


1.4 C/C++/Javaコードレビュー


課題5 コードの説明 (C, C++, Java)

C, C++, Java で作ってきたソースプログラムを基に、以下の説明をする。

C
(1) sizeof演算子 (求まる値の意味・単位)、マクロ N について。構造体へのアクセス、構造体の配列、構造体へのポインタ、関数 getGokei について
(2) 変数 bucket の型、リスト処理、順位付け
(ここが一番難しい所。解説ページおよび下に示す参考解説ビデオを参照のこと)
C++
(1) クラスの構成 (変数、メソッド) (変数についてもメソッドについても、staticがないときと付いているときの意味・動作の違いを説明すること)、集計の仕組み (どのようにして集計が実現されているのか)
(2b) リスト処理、順位付け、Javaとの違い (下の Java と合せて比較説明してよい)
(2b2) C++11/14/17 の機能 auto, forward_list, array, range-based for, 初期化構文などの利用
Java
(2b) C++ (2b) との違い (上の C++ (2b) と合せて比較説明してよい)
Generics、auto boxing/unboxing が使われている部分 (autoで変換されないとしたときの代替コードを示し比較して説明)
下の参考解説ビデオを参照のこと。

説明は Microsoft Office の PowerPoint ファイル (.pptx) として作成し、後に Web 提出してもらう。

ファイル提出とともに、発表する週を設け、各人に説明してもらう。ソース内容についての質問もするので、いままでの課題のすべてのソースも見せられるよう準備しておくこと。

一つ前までの課題を終えた人は、作ってきた全ソースプログラムを細かく見て、分からないところは調べ、文法や動作を詳しく説明できるようにしておくこと。

※ 参考解説ビデオ (Teamsの「ファイル」に置く予定)


2  Pythonへの書き換え

最近技術系・理工学系の分野では、deep learning を中心に Python が多く使われている。Python は大きく分けてバージョン 2 系とバージョン 3 系があるが、今後は Python 3 が主として利用されていくと考えられている。

Pythonの基本については、Webに多くの解説があるので参照すること。ここでも、簡単な解説を用意してある。


課題6 Pythonによるコード書き換え

C++ の (2b) または Java の (2b) のプログラムをベースにして、同様の動作をするプログラムを Python 3 で記述する。

元となる C++またはJavaのプログラムと同様に、

を持つ Tensu クラスを定義し、メインから使用すること。
ソースの最初の方に Tensu クラスを記述し、その後メイン部分をクラスを使わずに記述すること。

なお、Python では C++ や Java 等にある private や public 等のアクセス制御を簡単に実現する言語サポートが十分でないので、簡単のため、 private や public 等を無視して、クラスの変数やメソッドをすべて public 相当で作って構わない。

CやJavaでの記述に慣れた人がPythonのプログラムを作ると、得てして range をいっぱい使い勝ちになる。今回のプログラムでは、下記に示す bucket の定義部分以外は range を使わずに記述できるので、できるだけ range を使わない形のプログラムを作成してほしい。

以下をまとめる。

を続けたテキストファイルを保存しWebから提出すること。


※ bucket の表現方法

Pythonでは配列は言語として用意されておらず、素のPythonでは配列の代わりにリスト (list)を使用する。
bucketは「リストのリスト」で表現できる。点数データを入れる前の初期値として、

「空リストを (Tensu.MAXG + 1) 個持つリスト」

に設定する必要がある。
(ここで MAXG は Tensu クラスの中で static 変数 (実際には定数扱い) として定義されているとする)
素朴に繰り返しを使って記述すると

    bucket = []
    for b in range(Tensu.MAXG + 1):
        bucket.append([])

となるが、少し冗長である。
Pythonでは同じ要素を持つリストを * を使って表せるので、

    bucket = [[]] * (Tensu.MAXG + 1)

でいいかと考えてしまうが、上記コードではbucketの外側のリストの要素はすべて同一の空リストを指す参照になるので、意図したようには動作しない。
そこでリスト内包表記を使って

    bucket = [[] for _ in range(Tensu.MAXG + 1)]

のように記述すれば、意図したように初期化ができる。

別の書き方もある。bucketの外側のリストは要素の追加・削除がなく各要素内容も変わらないので、タプル (tuple)とすることができる。
そこで、ジェネレータ式と tuple 関数を使って

    bucket = tuple([] for _ in range(Tensu.MAXG + 1))

のように記述することができる。この場合、bucket は「リストのタプル」になる。


※ 順位を持つリスト jun の領域確保と初期化

前述のように、Pythonでは同じ要素を持つリストを * を使って表せるので、Tensuクラスのstaticメソッド (またはクラスメソッド) に、データの人数を求める ninzuu() があるとして、

    jun = [0] * Tensu.ninzuu()

のように、人数分の領域を確保し、すべてを 0 で初期化することができる。



課題7 コードの説明 (Python)

Python で作ったソースプログラムの説明をする。 説明は Microsoft Office の PowerPoint ファイル (.pptx) として作成し、後に Web 提出してもらう。

作ったソースプログラムを細かく見て、分からないところは調べ、文法や動作を詳しく説明できるようにしておくこと。