次のステップ

プログラミングと C++ の概要

このオンライン チュートリアルでは、より高度なコンセプトについて説明します。パート III をご覧ください。このモジュールでは、ポインタの使用とオブジェクトの使用の開始に焦点を当てます。

例 2 で学ぶ

このモジュールで重点を置いているのは、分解の実践演習、ポインタの理解、オブジェクトとクラスの使い方です。以下の例に沿って対応します。プログラムは指示に沿って自分で作成することも、実験することもできます。優れたプログラマーになるための秘訣は、練習、練習、そして練習を重ねることです。これはいくら強調しても足りないほどです。

例 1: その他の分解の練習

シンプルなゲームから、次の出力があるとします。

Welcome to Artillery.
You are in the middle of a war and being charged by thousands of enemies.
You have one cannon, which you can shoot at any angle.
You only have 10 cannonballs for this target..
Let's begin...

The enemy is 507 feet away!!!
What angle? 25<
You over shot by 445
What angle? 15
You over shot by 114
What angle? 10
You under shot by 82
What angle? 12
You under shot by 2
What angle? 12.01
You hit him!!!
It took you 4 shots.
You have killed 1 enemy.
I see another one, are you ready? (Y/N) n

You killed 1 of the enemy.

最初の説明は、プログラムの実行ごとに 1 回表示される導入テキストです。各ラウンドの敵の距離を定義する乱数ジェネレータが必要です。プレーヤーから角度の入力を取得するメカニズムが必要ですが、これは明らかにループ構造になっています。これは、敵にヒットするまで繰り返されるためです。また、距離と角度を計算する関数も必要です。最後に、敵にぶつかった回数と、プログラムの実行中に攻撃した敵の数を追跡する必要があります。以下は、メイン プログラムの概要です。

StartUp(); // This displays the introductory script.
killed = 0;
do {
  killed = Fire(); // Fire() contains the main loop of each round.
  cout << "I see another one, care to shoot again? (Y/N) " << endl;
  cin >> done;
} while (done != 'n');
cout << "You killed " << killed << " of the enemy." << endl;

Fire プロシージャは、ゲームのプレイを処理します。この関数で乱数ジェネレータを呼び出して敵の距離を取得し、プレーヤーの入力を取得して敵を攻撃したかどうかを計算するループを設定します。ループのガード条件は、敵にどれだけ近づいたかを示します。

In case you are a little rusty on physics, here are the calculations:

Velocity = 200.0; // initial velocity of 200 ft/sec Gravity = 32.2; // gravity for distance calculation // in_angle is the angle the player has entered, converted to radians. time_in_air = (2.0 * Velocity * sin(in_angle)) / Gravity; distance = round((Velocity * cos(in_angle)) * time_in_air);

cos() と sin() の呼び出しのため、math.h を含める必要があります。このプログラムを記述してみてください。問題分解の実践におすすめです。基本的な C++ の復習にぜひお役立てください。関数ごとにタスクは 1 つだけにしましょう。これは、これまでに作成したプログラムの中で最も洗練されたものであるため、少し時間がかかることがあります。こちらの解決方法をご覧ください。

例 2: ポインタを使って練習する

ポインタを操作するときは、次の 4 つの点に留意してください。
  1. ポインタは、メモリアドレスを保持する変数です。プログラムの実行中にすべての変数がメモリに格納され、各変数は固有のアドレスまたは位置に格納されます。ポインタは、データ値ではなくメモリアドレスを含む特殊なタイプの変数です。通常の変数が使用されたときにデータが変更されるのと同様に、ポインタに格納されたアドレスの値は、ポインタ変数が操作されると変更されます。次に例を示します。
    int *intptr; // Declare a pointer that holds the address
                 // of a memory location that can store an integer.
                 // Note the use of * to indicate this is a pointer variable.
    
    intptr = new int; // Allocate memory for the integer.
    *intptr = 5; // Store 5 in the memory address stored in intptr.
          
  2. 通常、ポインタは格納されている場所(「ポイント先」)を指します。したがって、上記の例では、intptr は指示先 5 を指しています。

    整数ポインタにメモリを割り当てる際に「new」演算子を使用していることに注目してください。これは、ポイント先にアクセスする前に行う必要があります。

    int *ptr; // Declare integer pointer.
    ptr = new int; // Allocate some memory for the integer.
    *ptr = 5; // Dereference to initialize the pointee.
    *ptr = *ptr + 1; // We are dereferencing ptr in order
                     // to add one to the value stored
                     // at the ptr address.
          

    C で参照解除するには * 演算子を使用します。C/C++ プログラマーがポインタを操作する際によく発生するエラーは、ポインタの初期化を忘れることです。これにより、メモリ内の不明なデータが含まれる場所にアクセスするため、ランタイム クラッシュが発生する可能性があります。このデータを変更しようとすると、微妙なメモリ破損を引き起こし、追跡が困難なバグになる可能性があります。

  3. 2 つのポインタ間のポインタ割り当てにより、両者が同じポインティをポイントするようになります。したがって、代入 y = x; は、y を x と同じ指示先を指します。ポインタの割り当てがポインタに触れません。あるポインタが、別のポインタと同じ位置を持つように変更されます。ポインタの割り当て後、2 つのポインタは指示先を「共有」します。 
  4. void main() {
     int* x; // Allocate the pointers x and y
     int* y; // (but not the pointees).
    
     x = new int; // Allocate an int pointee and set x to point to it.
    
     *x = 42; // Dereference x and store 42 in its pointee
    
     *y = 13; // CRASH -- y does not have a pointee yet
    
     y = x; // Pointer assignment sets y to point to x's pointee
    
     *y = 13; // Dereference y to store 13 in its (shared) pointee
    }
      

このコードのトレースは次のとおりです。

1. x と y の 2 つのポインタを割り当てます。ポインタを割り当てても、ポイント先は割り当てられません
2. ポインタを割り当て、x でポイントを指定します。
3. x を逆参照して、42 をポイント先に格納します。これは逆参照オペレーションの基本的な例です。x から開始し、矢印に沿ってその指示先にアクセスします。
4. y を逆参照して、13 をポイント先に格納します。y に指名先がないため、これはクラッシュします。指先が割り当てられたことがありません。
5. y が x の指示先を指すように y = x; を代入します。これで、x と y が同じ指し先を指すようになり、「共有」するようになりました。
6. y を逆参照して、13 をポイント先に格納します。今回は正常に機能します。前の割り当てで指名されたからです。

このように、画像はポインタの使用状況を理解するうえで非常に役立ちます。別の例を次に示します。

int my_int = 46; // Declare a normal integer variable.
                 // Set it to equal 46.

// Declare a pointer and make it point to the variable my_int
// by using the address-of operator.
int *my_pointer = &my_int;

cout << my_int << endl; // Displays 46.

*my_pointer = 107; // Derefence and modify the variable.

cout << my_int << endl; // Displays 107.
cout << *my_pointer << endl; // Also 107.

この例では、「new」演算子を使用してメモリを割り当てていません。通常の整数変数を宣言し、ポインタで操作しました。

この例では、ヒープメモリの割り当てを解除する delete 演算子と、より複雑な構造体への割り当て方法を示します。メモリの構成(ヒープとランタイム スタック)については、別のレッスンで説明します。ひとまずヒープとは、プログラムの実行に利用できる空きメモリと考えてください。

int *ptr1; // Declare a pointer to int.
ptr1 = new int; // Reserve storage and point to it.

float *ptr2 = new float; // Do it all in one statement.

delete ptr1; // Free the storage.
delete ptr2;

この最後の例では、ポインタを使用して関数を参照することで値を渡す方法を示します。これは、関数内の変数の値を変更する方法です。

// Passing parameters by reference.
#include <iostream>
using namespace std;

void Duplicate(int& a, int& b, int& c) {
  a *= 2;
  b *= 2;
  c *= 2;
}

int main() {
  int x = 1, y = 3, z = 7;
  Duplicate(x, y, z);
  // The following outputs: x=2, y=6, z=14.
  cout << "x="<< x << ", y="<< y << ", z="<< z;
  return 0;
}

Duplicate 関数定義の引数から & を外した場合は、変数を「値で」渡します。つまり、変数の値のコピーが作成されます。関数内の変数に変更を加えると、コピーも変更されます。元の変数は変更されません。

変数が参照によって渡された場合、値のコピーは渡されず、変数のアドレスが関数に渡されます。ローカル変数に変更を加えると、実際に渡された元の変数も変更されます。

C プログラマーにとって、これは新しい工夫です。C でも Duplicate()Duplicate(int *x) として宣言します。この場合、x は int へのポインタです。次に、引数 &x(x のアドレスの)を指定して Duplicate() を呼び出し、Duplicate()x の逆参照を使用します(下記参照)。一方、C++ では、参照によって関数に値を渡す簡単な方法がありますが、以前の「C」の方法は今も機能します。

void Duplicate(int *a, int *b, int *c) {
  *a *= 2;
  *b *= 2;
  *c *= 2;
}

int main() {
  int x = 1, y = 3, z = 7;
  Duplicate(&x, &y, &z);
  // The following outputs: x=2, y=6, z=14.
  cout << "x=" << x << ", y=" << y << ", z=" << z;
  return 0;
}

C++ 参照では、変数のアドレスを渡す必要はなく、呼び出された関数内で変数の逆参照も必要ありません。

次のプログラムの出力内容は次のうちどれですか。思い出の絵を描いてから考えましょう。

void DoIt(int &foo, int goo);

int main() {
  int *foo, *goo;
  foo = new int;
  *foo = 1;
  goo = new int;
  *goo = 3;
  *foo = *goo + 3;
  foo = goo;
  *goo = 5;
  *foo = *goo + *foo;
  DoIt(*foo, *goo);
  cout << (*foo) << endl;
}

void DoIt(int &foo, int goo) {
  foo = goo + 3;
  goo = foo + 4;
  foo = goo + 3;
  goo = foo;
} 

プログラムを実行して、正解を確認してください。

例 3: 参照で値を渡す

車両の速度と量を入力として受け取る accelerate() という関数を作成します。この関数は、車両を加速させる速度に量を加算します。speed パラメータは参照で、量は値で渡す必要があります。こちらで解決策をご覧ください。

例 4: クラスとオブジェクト

次のクラスについて考えてみましょう。

// time.cpp, Maggie Johnson
// Description: A simple time class.

#include <iostream>
using namespace std;

class Time {
 private:
  int hours_;
  int minutes_;
  int seconds_;
 public:
  void set(int h, int m, int s) {hours_ = h; minutes_ = m; seconds_ = s; return;}
  void increment();
  void display();
};

void Time::increment() {
  seconds_++;
  minutes_ += seconds_/60;
  hours_ += minutes_/60;
  seconds_ %= 60;
  minutes_ %= 60;
  hours_ %= 24;
  return;
}

void Time::display() {
  cout << (hours_ % 12 ? hours_ % 12:12) << ':'
       << (minutes_ < 10 ? "0" :"") << minutes_ << ':'
       << (seconds_ < 10 ? "0" :"") << seconds_
       << (hours_ < 12 ? " AM" : " PM") << endl;
}

int main() {
  Time timer;
  timer.set(23,59,58);
  for (int i = 0; i < 5; i++) {
    timer.increment();
    timer.display();
    cout << endl;
  }
}

クラスメンバー変数の末尾はアンダースコアです。これは、ローカル変数とクラス変数を区別するためです。

このクラスにデクリメント メソッドを追加します。こちらで解決策をご覧ください。

科学の素晴らしさ: コンピュータ サイエンス

練習

このコースの最初のモジュールにあるように、演習やプロジェクトに関する解答は提示しません。

良いプログラムであることを忘れないでください...

... は、1 つの関数が 1 つのタスクのみを行う関数に論理的に分解されます。

... プログラムが実行する内容の概要を示すメインプログラムがあります。

... わかりやすい関数、定数、変数名を持つ

... 定数を使用して、プログラムでの「マジック」な数字を回避しています。

... 使いやすいユーザー インターフェースを備えている。

ウォームアップエクササイズ

  • 演習 1

    整数 36 には完全平方で 1 ~ 8 の整数の和であるという特異な特性があります。次の数値は 1225、つまり 352 と、1 ~ 49 の整数の合計です。完全平方となる次の数と、1...n の級数の和を求める。この次の数値は 32767 より大きい可能性があります。ご存じのライブラリ関数や数式を使用すると、プログラムの実行を高速化できます。for ループを使用してこのプログラムを記述し、数値が完全平方であるか、級数の和であるかを判断することもできます。(注: マシンやプログラムによっては、この数値を見つけるまでにかなり時間がかかることがあります)。

  • 演習 2

    あなたが大学の書店で来年のビジネスを予測するために、あなたのサポートを必要としています。これまでの経験から、書籍がコースに必須か、単に任意かや、以前にクラスで使用したことがあるかどうかによって、売り上げが大きく左右されます。新しい必須教科書は入学見込みの 90% に販売されますが、以前にクラスで使用したことがある場合は 65% しか購入しません。同様に、入学見込みの 40% が新しいオプション教科書を購入しますが、クラスで実際に使用したのが購入するのは 20% のみです。(ここでの「中古品」とは、古書を意味するものではありません)。

  • 一連の本を入力として(ユーザーが標識を入力するまで)入力できるプログラムを作成します。書籍ごとに、書籍のコード、書籍の 1 部発行費用、現在手元に届く書籍の数、クラスへの登録予定数、書籍が必須 / 任意 / 新規 / 過去に使用されたかどうかを示すデータが必要です。出力として、すべての入力情報と注文する必要がある書籍の数(注文がある場合は、新しい書籍のみが注文される)、各注文の合計費用が表示されます。

    すべての入力が完了したら、すべての書籍の注文の合計費用と、ストアが正規価格の 80% を支払った場合の予想利益を表示します。プログラムに送信される大量のデータを処理する方法についてまだ説明していないため、一度に 1 冊ずつ処理を行い、その書籍の出力画面を表示するだけで済みます。ユーザーがすべてのデータの入力を完了すると、合計と利益の値が出力されます。

    コードの記述を始める前に、このプログラムの設計について少し時間をかけて検討してください。一連の関数に分解し、問題の解答の概要のように読み取る main() 関数を作成します。各関数が 1 つのタスクを実行するようにします。

    出力例を次に示します。

    Please enter the book code: 1221
     single copy price: 69.95
     number on hand: 30
     prospective enrollment: 150
     1 for reqd/0 for optional: 1
     1 for new/0 for used: 0
    ***************************************************
    Book: 1221
    Price: $69.95
    Inventory: 30
    Enrollment: 150
    
    This book is required and used.
    ***************************************************
    Need to order: 67
    Total Cost: $4686.65
    ***************************************************
    
    Enter 1 to do another book, 0 to stop. 0
    ***************************************************
    Total for all orders: $4686.65
    Profit: $937.33
    ***************************************************

データベース プロジェクト

このプロジェクトでは、シンプルなデータベース アプリケーションを実装する、完全に機能する C++ プログラムを作成します。

このプログラムを使用すると、作曲者のデータベースと作曲に関連する情報を管理できます。このプログラムには次のような機能があります。

  • 新しい作曲者を追加する機能
  • 作曲家をランク付けする機能(作曲者の音楽に対する評価を示します)
  • データベース内のすべての作曲者を表示する機能
  • すべての作曲者をランク別に表示する機能

「ソフトウェアの設計を構築するには 2 通りの方法があります。1 つ目は、明らかに欠陥がなくなるように非常にシンプルにする方法、もう 1 つは、明らかに欠陥がなくなるように非常に複雑にする方法です。1 つ目の方法ははるかに困難です。」- C.A.R. Hoare

私たちの多くは、「手続き型」のアプローチで設計とコーディングを学びました。まず「このプログラムで何をする必要があるか」という問いから始めましょう。問題の解をタスクに分解し、各タスクが問題の一部を解きます。これらのタスクは、main() または他の関数から順次呼び出される、プログラム内の関数にマッピングされます。段階的なアプローチは、解決が必要な問題に最適です。しかし多くの場合、Google のプログラムはタスクやイベントの線形シーケンスではありません。

オブジェクト指向(OO)のアプローチでは、「実世界のどのオブジェクトをモデリングしているか」という問いから始めます。前述のようにプログラムをタスクに分割するのではなく、物理オブジェクトのモデルに分割します。これらの物理オブジェクトは、一連の属性と、それらが実行できる一連の動作またはアクションによって定義された状態を持ちます。アクションは、オブジェクトの状態を変更する場合や、他のオブジェクトのアクションを呼び出す場合があります。基本的な前提は、オブジェクトはそれ自体が物事の進め方を「知っている」ということです。

OO 設計では、クラスとオブジェクト、属性と動作の観点から物理オブジェクトを定義します。通常、OO プログラムには多数のオブジェクトがあります。しかし、これらのオブジェクトの多くは本質的に同じです。次の点を考慮してください。

クラスとは、現実世界に物理的に存在する可能性のある、オブジェクトの一般的な属性と動作のセットです。上の図では、Apple クラスがあります。すべてのリンゴに、種類にかかわらず、色と味の属性があります。Apple がその属性を表示する動作も定義されています。

この図では、Apple クラスの 2 つのオブジェクトを定義しています。各オブジェクトの属性とアクションはクラスと同じですが、リンゴの種類別の属性がオブジェクトによって定義されます。さらに、Display アクションは特定のオブジェクトの属性を表示します。たとえば、「緑」と「サワー」。

OO 設計は、クラスのセット、各クラスに関連付けられたデータ、クラスが実行できるアクションのセットで構成されます。また、各クラスの相互作用を特定する必要もあります。このインタラクションは、あるクラスのオブジェクトによって行われ、他のクラスのオブジェクトのアクションを呼び出すことができます。たとえば、各 Apple オブジェクトの Display() メソッドを呼び出すことで、Apple オブジェクトの配列の色と味を出力する AppleOutputer クラスを作成できます。

OO 設計を行う手順は次のとおりです。

  1. クラスを識別します。また、各クラスのオブジェクトがデータとして保存する対象と、オブジェクトが実行できることの概要を定義します。
  2. 各クラスのデータ要素を定義する
  3. 各クラスのアクションと、あるクラスのアクションを他の関連クラスのアクションを使用して実装する方法を定義します。

大規模なシステムでは、これらのステップは異なる詳細レベルで繰り返し行われます。

Composer データベース システムには、個々の Composer に保存するすべてのデータをカプセル化する Composer クラスが必要です。このクラスのオブジェクトは、自身を昇格または降格(ランクを変更)し、属性を表示できます。

また、Composer オブジェクトのコレクションも必要です。そのために、個々のレコードを管理する Database クラスを定義します。このクラスのオブジェクトは、Composer オブジェクトの追加や取得を行うことができます。また、Composer オブジェクトの表示アクションを呼び出すことで、個々のオブジェクトを表示できます。

最後に、データベースに対してインタラクティブなオペレーションを提供するために、なんらかのユーザー インターフェースが必要です。これはプレースホルダ クラスです。つまり、ユーザー インターフェースがどのようなものになるかはまだわかりませんが、必要となることは確かです。グラフィカルなものや、テキストベースのものがあります。ここでは、後で入力できるプレースホルダを定義します。

Composer データベース アプリケーションのクラスを特定したら、次のステップではクラスの属性とアクションを定義します。より複雑なアプリでは、鉛筆と紙、UML カード、CRC カードOOD を使ってクラス階層とオブジェクトの相互作用をマッピングします。

この Composer データベースには、各 Composer に保存する関連データを含む Composer クラスを定義します。また、ランキングを操作し、データを表示するメソッドも含まれます。

Database クラスには、Composer オブジェクトを保持するためになんらかの構造が必要です。 新しい Composer オブジェクトをストラクチャに追加し、特定の Composer オブジェクトを取得できる必要があります。また、すべてのオブジェクトを、エントリ順またはランキング別に表示することもできます。

ユーザー インターフェース クラスは、Database クラスのアクションを呼び出すハンドラを使用して、メニュードリブンのインターフェースを実装します。

Composer アプリケーションのように、クラスが簡単に理解でき、その属性とアクションが明確であれば、クラスの設計は比較的簡単です。ただし、各クラスの相互関係や相互作用について不明な点がある場合は、コーディングを始める前に、まずそれを明確化し、詳細を考えることをおすすめします。

デザインの全体像を把握して評価したら(詳細は後述します)、各クラスのインターフェースを定義します。この時点では、実装の詳細を気にする必要はありません。属性とアクションは何か、クラスの状態とアクションのどの部分が他のクラスが利用できるかだけです。

C++ では通常、各クラスのヘッダー ファイルを定義します。Composer クラスには、Composer に保存するすべてのデータの限定公開データメンバーが含まれています。アクセサ(「get」メソッド)とミューテータ(「set」メソッド)、クラスの主なアクションが必要です。

// composer.h, Maggie Johnson
// Description: The class for a Composer record.
// The default ranking is 10 which is the lowest possible.
// Notice we use const in C++ instead of #define.
const int kDefaultRanking = 10;

class Composer {
 public:
  // Constructor
  Composer();
  // Here is the destructor which has the same name as the class
  // and is preceded by ~. It is called when an object is destroyed
  // either by deletion, or when the object is on the stack and
  // the method ends.
  ~Composer();

  // Accessors and Mutators
  void set_first_name(string in_first_name);
  string first_name();
  void set_last_name(string in_last_name);
  string last_name();
  void set_composer_yob(int in_composer_yob);
  int composer_yob();
  void set_composer_genre(string in_composer_genre);
  string composer_genre();
  void set_ranking(int in_ranking);
  int ranking();
  void set_fact(string in_fact);
  string fact();

  // Methods
  // This method increases a composer's rank by increment.
  void Promote(int increment);
  // This method decreases a composer's rank by decrement.
  void Demote(int decrement);
  // This method displays all the attributes of a composer.
  void Display();

 private:
  string first_name_;
  string last_name_;
  int composer_yob_; // year of birth
  string composer_genre_; // baroque, classical, romantic, etc.
  string fact_;
  int ranking_;
};

Database クラスも簡単です。

// database.h, Maggie Johnson
// Description: Class for a database of Composer records.
#include  <iostream>
#include "Composer.h"

// Our database holds 100 composers, and no more.
const int kMaxComposers = 100;

class Database {
 public:
  Database();
  ~Database();

  // Add a new composer using operations in the Composer class.
  // For convenience, we return a reference (pointer) to the new record.
  Composer& AddComposer(string in_first_name, string in_last_name,
                        string in_genre, int in_yob, string in_fact);
  // Search for a composer based on last name. Return a reference to the
  // found record.
  Composer& GetComposer(string in_last_name);
  // Display all composers in the database.
  void DisplayAll();
  // Sort database records by rank and then display all.
  void DisplayByRank();

 private:
  // Store the individual records in an array.
  Composer composers_[kMaxComposers];
  // Track the next slot in the array to place a new record.
  int next_slot_;
};

Composer 固有のデータを別のクラスに慎重にカプセル化しています。Composer レコードを表す構造体またはクラスを Database クラスに入れ、そこから直接アクセスすることもできます。ただし、これは「オブジェクト化が不十分」になります。つまり、可能な限りオブジェクトを使用してモデリングを行っていないということです。

Composer クラスと Database クラスの実装に取り掛かるにつれて、個別の Composer クラスを持つ方がはるかにクリーンなことがおわかりいただけるでしょう。特に、Composer オブジェクトに対して個別のアトミック操作を使用することで、Database クラスでの Display() メソッドの実装が大幅に簡素化されます。

もちろん、すべてをクラス化しようとする「過度なオブジェクト化」や、必要以上のクラスの数といった「過度なオブジェクト化」もあります。適切なバランスを見つけるには練習が必要ですが、プログラマーによって考え方は異なります。

多くの場合、クラスを慎重に図表化することで、物事を物体化しすぎているか、それとも過小化しているかを判断します。前述のように、コーディングを始める前にクラスの設計を設計することが重要です。これはアプローチの分析に役立ちます。この目的で使用される一般的な表記は UML(統合モデリング言語)です。Composer オブジェクトと Database オブジェクトに対してクラスを定義したところで、次に、ユーザーがデータベースを操作するためのインターフェースが必要になります。シンプルなメニューで役立ちます。

Composer Database
---------------------------------------------
1) Add a new composer
2) Retrieve a composer's data
3) Promote/demote a composer's rank
4) List all composers
5) List all composers by rank
0) Quit

ユーザー インターフェースは、クラスまたは手続き型プログラムとして実装できます。C++ プログラムのすべてをクラスでする必要はありません。実際、このメニュー プログラムのように、処理がシーケンシャルまたはタスク指向の場合は、プロシージャルに実装しても問題ありません。「プレースホルダ」として実装することが重要です。つまり、ある時点でグラフィカル ユーザー インターフェースを作成する場合は、ユーザー インターフェース以外は何も変更する必要がありません。

最後に、クラスをテストするプログラムを用意する必要があります。Composer クラスには、入力を受け取り、Composer オブジェクトに代入して表示し、クラスが正常に機能していることを確認する main() プログラムが必要です。 また、Composer クラスのすべてのメソッドを呼び出します。

// test_composer.cpp, Maggie Johnson
//
// This program tests the Composer class.

#include <iostream>
#include "Composer.h"
using namespace std;

int main()
{
  cout << endl << "Testing the Composer class." << endl << endl;

  Composer composer;

  composer.set_first_name("Ludwig van");
  composer.set_last_name("Beethoven");
  composer.set_composer_yob(1770);
  composer.set_composer_genre("Romantic");
  composer.set_fact("Beethoven was completely deaf during the latter part of "
    "his life - he never heard a performance of his 9th symphony.");
  composer.Promote(2);
  composer.Demote(1);
  composer.Display();
}

Database クラスにも同様のテスト プログラムが必要です。

// test_database.cpp, Maggie Johnson
//
// Description: Test driver for a database of Composer records.
#include <iostream>
#include "Database.h"
using namespace std;

int main() {
  Database myDB;

  // Remember that AddComposer returns a reference to the new record.
  Composer& comp1 = myDB.AddComposer("Ludwig van", "Beethoven", "Romantic", 1770,
    "Beethoven was completely deaf during the latter part of his life - he never "
    "heard a performance of his 9th symphony.");
  comp1.Promote(7);

  Composer& comp2 = myDB.AddComposer("Johann Sebastian", "Bach", "Baroque", 1685,
    "Bach had 20 children, several of whom became famous musicians as well.");
  comp2.Promote(5);

  Composer& comp3 = myDB.AddComposer("Wolfgang Amadeus", "Mozart", "Classical", 1756,
    "Mozart feared for his life during his last year - there is some evidence "
    "that he was poisoned.");
  comp3.Promote(2);

  cout << endl << "all Composers: " << endl << endl;
  myDB.DisplayAll();
}

最初のステップとしては、このようなシンプルなテスト プログラムが適していますが、出力を手動で検査してプログラムが正常に機能していることを確認する必要があります。システムが大規模になるほど、出力の手動検査はすぐに実用化できなくなります。次のレッスンでは、単体テストという形式でセルフチェック テスト プログラムを紹介します。

これで、アプリケーションの設計が完了します。次のステップでは、クラスとユーザー インターフェース用の .cpp ファイルを実装します。まず、上記の .h とテストドライバのコードをファイルにコピーして貼り付け、コンパイルします。テストドライバを使用してクラスをテストします。次に、次のインターフェースを実装します。

Composer Database
---------------------------------------------
1) Add a new composer
2) Retrieve a composer's data
3) Promote/demote a composer's rank
4) List all composers
5) List all composers by rank
0) Quit

Database クラスで定義したメソッドを使用して、ユーザー インターフェースを実装します。メソッドをエラーのないものにします。たとえば、ランキングは常に 1 ~ 10 の範囲になります。Database クラスのデータ構造を変更する予定がない限り、101 人の作曲者を追加することは許可しないでください。

注意: すべてのコードは、Google のコーディング規則に従う必要があります。ここでは、その規則を念頭に置いて説明します。

  • 私たちが記述するすべてのプログラムはヘッダー コメントから始まり、作成者の名前、連絡先情報、簡単な説明、用途(該当する場合)が記載されます。すべての関数やメソッドは、オペレーションと使用方法についてのコメントから始まります。
  • コード自体が文書化されていない場合(処理が難しい、自明でない、興味深い、重要な場合など)は、完全な文を使用した説明コメントが追加されます。
  • 常にわかりやすい名前を使用します。変数は、my_variable のように小文字の単語を「_」で区切ったものです。MyExcitingFunction() のように、関数名やメソッド名は大文字を使用して単語をマークします。定数は「k」で始まり、kDaysInWeek のように大文字を使用して単語をマークします。
  • インデントは 2 の倍数にします。最初のレベルは 2 つのスペースです。さらにインデントが必要な場合は、4 つのスペース、6 つのスペースなどを使用します。

現実の世界へようこそ!

このモジュールでは、ほとんどのソフトウェア エンジニアリング組織で使用されている 2 つの非常に重要なツールを紹介します。1 つ目はビルドツール、2 つ目は構成管理システムです。どちらのツールも、多くのエンジニアが 1 つの大規模なシステムで作業する産業ソフトウェア エンジニアリングには不可欠です。これらのツールは、コードベースに対する変更の調整と制御に役立ち、多数のプログラム ファイルとヘッダー ファイルからシステムをコンパイルおよびリンクする効率的な手段を提供します。

Makefile

プログラムのビルドプロセスは通常、ビルドツールで管理されます。ビルドツールは、必要なファイルを正しい順序でコンパイルしてリンクします。多くの場合、C++ ファイルには依存関係があります。たとえば、あるプログラムで呼び出された関数が、別のプログラムに常駐します。複数の異なる .cpp ファイルでヘッダー ファイルが必要になることもあります。ビルドツールは、これらの依存関係から正しいコンパイル順序を特定します。また、前回のビルド以降に変更されたファイルのみをコンパイルします。これにより、数百または数千のファイルで構成されるシステムで多くの時間を節約できます。

make と呼ばれるオープンソースのビルドツールが一般的に使用されています。詳しくは、こちらの記事をご覧ください。Composer データベース アプリケーションの依存関係グラフを作成してから、makefile に変換できるかどうかを確認します。(こちらをご覧ください。)

構成管理システム

産業ソフトウェア エンジニアリングで使用される 2 つ目のツールは、構成管理(CM)です。これは変更の管理に使用されます。たとえば、ボブとスーザンはどちらもテクノロジー ライターで、どちらも技術マニュアルの更新に取り組んでいるとします。会議では、マネージャーから同じドキュメントのセクションを 1 つずつ更新して更新します。

テクニカル マニュアルは、Bob と Susan の両方がアクセスできるパソコンに保存されています。 CM のツールやプロセスがなければ、さまざまな問題が発生する可能性があります。たとえば、ドキュメントが保存されているコンピュータで、Bob と Susan が同時にマニュアルで作業できないように設定されていることがあります。この場合、通信速度はかなり遅くなります。

ストレージ コンピュータで Bob と Susan の両方が同時にドキュメントを開くことができる場合は、より危険な状況が発生します。この場合、次のことが起こる可能性があります。

  1. ボブは自分のパソコンでドキュメントを開き、自分のセクションで作業しています。
  2. スーザンは自分のパソコンでドキュメントを開き、自分のセクションで作業しています。
  3. 変更を完了して、ストレージ コンピュータにドキュメントを保存します。
  4. Susan は変更内容を完了し、ストレージ コンピュータにドキュメントを保存します。

この図は、技術マニュアルの 1 部だけにコントロールがない場合に発生する可能性がある問題を示しています。Susan が変更を保存すると、Bob が行った変更は上書きされます。

このような状況を、キャンペーン マネージャー システムで制御することができます。CM システムでは、Bob と Susan が技術マニュアルを「チェックアウト」し、作業を進めています。Bob が自分の変更をチェックインすると、Susan が自分のコピーをチェックアウトしていることがシステムで認識されます。Susan が自分のコピーをチェックすると、Bob と Susan が行った変更をシステムが分析し、2 つの変更内容を統合した新しいバージョンを作成します。

CM システムには、前述の同時変更の管理以外にも数多くの機能があります。多くのシステムでは、ドキュメントが最初に作成された時点からそのすべてのバージョンのアーカイブが保存されます。技術マニュアルの場合、ユーザーが古いバージョンのマニュアルを持っていて、テクニカル ライターに質問しているときに非常に役立ちます。CM システムでは、テクニカル ライターは古いバージョンにアクセスして、ユーザーに表示されている内容を確認できます。

CM システムは、ソフトウェアに加えられた変更を制御するのに特に役立ちます。このようなシステムは、ソフトウェア構成管理(SCM)システムと呼ばれます。大規模なソフトウェア エンジニアリング組織における膨大な数の個々のソースコード ファイルと、それらに変更を加える必要がある膨大な数のエンジニアを考えると、SCM システムが重要であることは明らかです。

ソフトウェア構成管理

SCM システムは、ファイルの最終コピーが中央リポジトリに保持されるというシンプルな考え方に基づいています。リポジトリからファイルのコピーをチェックアウトして作業を行い、完了したら再度チェックインします。SCM システムは、1 つのマスターセットに対して複数のユーザーによるリビジョンの管理と追跡を行います。

すべての SCM システムには、次の基本機能が用意されています。

  • 同時実行管理
  • バージョニング
  • 同期

それぞれの機能を詳しく見てみましょう。

同時実行管理

同時実行とは、複数のユーザーが 1 つのファイルを同時に編集することを意味します。大規模なリポジトリでは、ユーザーがこれを実行できるようにすべきですが、問題が生じることがあります。

エンジニアリング分野の簡単な例で考えてみましょう。エンジニアがソースコードの中央リポジトリで同じファイルを同時に変更できるようにするとします。Client1 と Client2 の両方が同時にファイルを変更する必要があります。

  1. クライアント 1 が bar.cpp を開きます。
  2. クライアント 2 が bar.cpp を開きます。
  3. Client1 がファイルを変更して保存します。
  4. Client2 がファイルを変更し、Client1 の変更を上書きして保存します。

当然ながら、そのような事態は望ましくありません。2 人のエンジニアに(下の図のように)直接マスターセットではなく別々のコピーで作業させて状況をコントロールした場合でも、なんらかの形でコピーを調整する必要があります。ほとんどの SCM システムでは、複数のエンジニアがファイルをチェックアウトして(「同期」や「更新」など)、必要に応じて変更を加えることができるようにすることで、この問題に対処しています。次に、ファイルがリポジトリにチェックイン(「submit」または「commit」)されるときに、SCM システムが変更をマージするアルゴリズムを実行します。

これらのアルゴリズムは、シンプルなもの(競合する変更をエンジニアに解決するようエンジニアに依頼する)の場合もあれば、単純ではない(競合する変更をインテリジェントに統合する方法を決定し、システムが本当に停止した場合にのみエンジニアに確認する)の場合もあります。

バージョニング

バージョニングとは、ファイルのリビジョンを追跡することで、前のバージョンのファイルを再作成(またはロールバック)できるようにするものです。これを行うには、リポジトリにチェックインするときにすべてのファイルのアーカイブ コピーを作成するか、ファイルに加えられたすべての変更を保存します。アーカイブや変更情報はいつでも使用して以前のバージョンを作成できます。バージョニング システムでは、誰が変更にチェックインしたか、いつチェックインしたか、変更内容を示すログレポートも作成できます。

同期

一部の SCM システムでは、個々のファイルがリポジトリにチェックインおよびチェックアウトされます。より高性能なシステムでは、複数のファイルを一度にチェックアウトできます。エンジニアは、リポジトリ(またはその一部)の完全なコピーを必要に応じてチェックアウトして、ファイルで作業します。その後、定期的に変更をマスター リポジトリに commit し、他のユーザーが行った変更を最新の状態に保つために、自分の個人用コピーを更新します。このプロセスは、同期または更新と呼ばれます。

Subversion

Subversion(SVN)はオープンソースのバージョン管理システムです。上記の機能をすべて備えています。

SVN では、競合が発生した場合にシンプルな手法を採用している。競合とは、2 人以上のエンジニアがコードベースの同じ領域に異なる変更を加え、それぞれの変更を提出することです。SVN は競合があることをエンジニアにだけ警告します。競合を解決するかどうかはエンジニア次第です。

構成管理の理解を深めるために、このコース全体で SVN を使用します。このようなシステムは業界ではごく一般的なものです。

まず、システムに SVN をインストールします。手順については、こちらをクリックしてください。オペレーティング システムを見つけ、適切なバイナリをダウンロードします。

SVN 用語の一部

  • リビジョン: 1 つまたは複数のファイルセットにおける変更です。リビジョンとは、常に変化するプロジェクトの 1 つの「スナップショット」のことです。
  • リポジトリ: SVN がプロジェクトの完全な変更履歴を保存するマスターコピー。 各プロジェクトには 1 つのリポジトリがあります。
  • 作業コピー: エンジニアがプロジェクトに変更を加える際に使用するコピー。個々のエンジニアが、プロジェクトごとに作業コピーを多数所有している場合があります。
  • チェックアウト: リポジトリの作業コピーをリクエストします。作業コピーは、プロジェクトがチェックアウトされたときの状態と同じです。
  • Commit: 作業コピーから中央リポジトリに変更を送信します。 「チェックイン」または「送信」とも呼ばれます。
  • 更新: 他のユーザーの変更をリポジトリから作業中のコピーに取り込む、または作業コピーに commit されていない変更があることを示すため。これは、前述のとおり、同期と同じです。そのため、更新/同期により、作業コピーがリポジトリ コピーで最新の状態になります。
  • 競合: 2 人のエンジニアがファイルの同じ領域に対して変更を commit しようとする状況。SVN は競合を示しますが、エンジニアが競合を解決する必要があります。
  • ログメッセージ: commit 時にリビジョンに添付するコメント。変更の内容が記載されています。ログには、プロジェクトで行われていることの概要が示されます。

SVN をインストールしたので、基本的なコマンドを実行してみましょう。まず、指定したディレクトリにリポジトリを設定します。以下にコマンドを示します。

$ svnadmin create /usr/local/svn/newrepos
$ svn import mytree file:///usr/local/svn/newrepos/project -m "Initial import"
Adding         mytree/foo.c
Adding         mytree/bar.c
Adding         mytree/subdir
Adding         mytree/subdir/foobar.h

Committed revision 1.

import コマンドは、ディレクトリ mytree の内容をリポジトリ内のディレクトリ プロジェクトにコピーします。list コマンドを実行すると、リポジトリ内のディレクトリを確認できます。

$ svn list file:///usr/local/svn/newrepos/project
bar.c
foo.c
subdir/

インポートでは作業コピーは作成されません。そのためには svn checkout コマンドを使用する必要があります。これにより、ディレクトリ ツリーの作業コピーが作成されます。方法は次のとおりです。

$ svn checkout file:///usr/local/svn/newrepos/project
A    foo.c
A    bar.c
A    subdir
A    subdir/foobar.h
…
Checked out revision 215.

作業コピーが作成されたので、そのコピーでファイルとディレクトリを変更できます。作業中のコピーは他のファイルとディレクトリのコレクションと同様です。新しいファイルやディレクトリを追加したり、編集したり、移動したり、作業コピー全体を削除したりすることもできます。作業コピー内のファイルをコピーして移動する場合は、オペレーティング システム コマンドの代わりに svn copysvnmove を行うことが重要です。新しいファイルを追加するには svn add を使用し、ファイルを削除するには svn delete を使用します。編集のみを行いたい場合は、エディタでファイルを開いて編集できます。

Subversion でよく使用される標準のディレクトリ名がいくつかあります。「トランク」ディレクトリには、プロジェクトの開発のメインラインが格納されています。「branches」ディレクトリには、作業中のブランチ バージョンが格納されます。

$ svn list file:///usr/local/svn/repos
/trunk
/branches

たとえば、作業コピーに必要な変更をすべて行い、それをリポジトリと同期するとします。他の多くのエンジニアがリポジトリのこの領域で作業している場合は、作業用コピーを最新の状態に保つことが重要です。svn status コマンドを使用して、行った変更を表示できます。

A       subdir/new.h      # file is scheduled for addition
D       subdir/old.c        # file is scheduled for deletion
M       bar.c                  # the content in bar.c has local modifications

なお、ステータス コマンドには、この出力を制御するためのフラグが多数あります。 変更したファイルの特定の変更を表示するには、svn diff を使用します。

$ svn diff bar.c
Index: bar.c
===================================================================
--- bar.c	(revision 5)
+++ bar.c	(working copy)
## -1,18 +1,19 ##
+#include
+#include

 int main(void) {
-  int temp_var;
+ int new_var;
...

最後に、リポジトリの作業コピーを更新するために、svn update コマンドを使用します。

$ svn update
U  foo.c
U  bar.c
G  subdir/foobar.h
C  subdir/new.h
Updated to revision 2.

ここで 1 か所で競合が発生する可能性があります。上記の出力で、「U」はこれらのファイルのリポジトリ バージョンに変更がなく、更新が行われたことを示します。「G」は、マージが行われたことを示します。リポジトリのバージョンは変更されていますが、その変更は自分のバージョンと競合していません。「C」は競合であることを示します。これは、リポジトリからの変更と自分の変更が重複していたため、どちらかを選択する必要があることを意味します。

競合のあるファイルごとに、Subversion では次の 3 つのファイルを作業コピーに含めます。

  • file.mine: 作業コピーを更新する前に作業コピーに存在していたファイルです。
  • file.rOLDREV: 変更する前にリポジトリからチェックアウトしたファイルです。
  • file.rNEWREV: このファイルは、リポジトリの現在のバージョンです。

次の 3 つの方法のいずれかで競合を解決できます。

  • ファイルを確認し、手動で結合する。
  • SVN によって作成された一時ファイルの 1 つを、現在のコピー バージョンにコピーします。
  • 変更内容をすべて破棄するには、svnUninstall を実行します。

競合を解決したら、svnresolve を実行して SVN を通知します。これにより、3 つの一時ファイルが削除され、SVN が競合状態でそのファイルを表示しなくなります。

最後に、最終バージョンをリポジトリに commit します。これを行うには svn commit コマンドを使用します。変更を commit するときは、変更内容を説明するログメッセージを提供する必要があります。このログメッセージは、作成するリビジョンに添付されます。

svn commit -m "Update files to include new headers."  

SVN について、また SVN が大規模なソフトウェア エンジニアリング プロジェクトをサポートする方法について学ぶことは、まだまだ多くあります。ウェブ上で利用できる広範なリソースがあります。「Subversion」で Google 検索を行ってください。

演習として、Composer Database システムのリポジトリを作成し、すべてのファイルをインポートします。次に、作業コピーをチェックアウトし、上記のコマンドを実行します。

参照

オンライン Subversion ブック

SVN に関するウィキペディアの記事

Subversion ウェブサイト

アプリケーション: 解剖学の研究

テキサス大学オースティン校の eSkeletons を見る