Nächste Schritte

Einführung in Programmierung und C++

In dieser Onlineanleitung werden fortgeschrittene Konzepte erläutert. Bitte lesen Sie Teil III. In diesem Modul konzentrieren wir uns auf die Verwendung von Zeigern und die ersten Schritte mit Objekten.

Lernen anhand von Beispiel 2

In diesem Modul konzentrieren wir uns darauf, mehr Übung im Umgang mit Zerlegung, dem Verständnis von Zeigern und den ersten Schritten mit Objekten und Klassen zu sammeln. Arbeiten Sie die folgenden Beispiele durch. Schreiben Sie die Programme selbst, wenn Sie dazu aufgefordert werden, oder führen Sie die Experimente durch. Wir können gar nicht genug betonen, dass der Schlüssel zu einem guten Programmierer in Übung, Übung und Übung liegt!

Beispiel 1: Stärkere Zersetzungspraxis

Betrachten Sie die folgende Ausgabe eines einfachen Spiels:

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.

Die erste Beobachtung ist der Einführungstext, der einmal pro Programmausführung angezeigt wird. Wir benötigen einen Zufallszahlengenerator, um die feindliche Distanz für jede Runde zu definieren. Wir brauchen einen Mechanismus, um die Winkeleingabe vom Player zu erhalten, und dies ist offensichtlich eine Schleifenstruktur, da sie so lange wiederholt wird, bis wir den Feind treffen. Außerdem benötigen wir eine Funktion zum Berechnen von Entfernung und Winkel. Schließlich müssen wir im Auge behalten, wie viele Schüsse erforderlich waren, um den Feind zu treffen, und wie viele Feinde wir während des Programms getroffen haben. Hier ist ein möglicher Überblick über das Hauptprogramm.

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;

Das Fire-Verfahren steuert das Spiel. In dieser Funktion rufen wir einen Zufallszahlengenerator auf, um die feindliche Entfernung zu ermitteln, und richten dann eine Schleife ein, um die Eingabe des Spielers zu erhalten und zu berechnen, ob er den Feind getroffen hat oder nicht. Die Guard-Bedingung in der Schleife gibt an, wie nah wir dem Feind bereits gekommen sind.

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);

Aufgrund der Aufrufe von cos() und sin() müssen Sie Math.h einbeziehen. Versuchen Sie, dieses Programm zu schreiben – es ist eine großartige Übung bei der Problemzerlegung und eine gute Übersicht über grundlegendes C++. Denken Sie daran, nur eine Aufgabe pro Funktion auszuführen. Dies ist das ausgefeilteste Programm, das wir bisher geschrieben haben. Die Umsetzung kann also etwas dauern.Hier finden Sie unsere Lösung.

Beispiel 2: Mit Mauszeigern üben

Bei der Arbeit mit Zeigern sind vier Dinge zu beachten:
  1. Zeiger sind Variablen, die Speicheradressen enthalten. Bei der Ausführung eines Programms werden alle Variablen an einer eigenen Adresse oder an einem eigenen Speicherort im Arbeitsspeicher gespeichert. Ein Zeiger ist ein spezieller Variablentyp, der eine Speicheradresse anstelle eines Datenwerts enthält. So wie Daten bei Verwendung einer normalen Variablen geändert werden, wird der Wert der in einem Zeiger gespeicherten Adresse als Zeigervariable geändert. Hier ein Beispiel:
    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. In der Regel sagen wir, dass ein Zeiger auf den Speicherort „verweist“ (der „Pointee“). Im Beispiel oben verweist also „intptr“ auf den Spitzenreiter 5.

    Beachten Sie die Verwendung des Operators „new“, um Arbeitsspeicher für unseren ganzzahligen Pointee zuzuweisen. Das müssen wir tun, bevor wir versuchen, auf den Pointee zuzugreifen.

    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.
          

    Der *-Operator wird zur Dereferenzierung in C verwendet. Einer der häufigsten Fehler von C/C++-Programmierern bei der Arbeit mit Zeigern besteht darin, dass sie vergessen, den Pointee zu initialisieren. Dies kann manchmal zu einem Laufzeitabsturz führen, da wir auf einen Speicherort im Arbeitsspeicher zugreifen, der unbekannte Daten enthält. Wenn wir versuchen, diese Daten zu ändern, können wir eine kleine Speicherbeschädigung verursachen, wodurch die Ermittlung des Fehlers schwierig wird. 

  3. Bei der Zeigerzuweisung zwischen zwei Zeigern zeigen sie auf denselben Pointee. Die Zuweisung y = x; führt also dazu, dass y auf denselben Pointee wie x verweist. Die Zeigerzuweisung berührt den Pointee nicht. Es ändert lediglich einen Zeiger so, dass er dieselbe Position wie ein anderer Zeiger hat. Nach der Pointer-Zuweisung "teilen" die beiden Mauszeiger den Pointee. 
  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
    }
      

Hier ist ein Trace dieses Codes:

1. Weisen Sie die beiden Zeiger x und y zu. Durch das Zuweisen der Mauszeiger werden keine Pointees zugewiesen.
2. Ordnen Sie einen Pointee zu und stellen Sie das x so ein, dass es darauf zeigt.
3. Dereferenzieren x, um 42 in seinem Pointee zu speichern. Dies ist ein einfaches Beispiel für den Dereferenzvorgang. Beginnen Sie bei x und folgen Sie dem Pfeil darüber, um auf das Objekt zuzugreifen.
4. Versuche, 13 in der Spitze zu dereferenzieren. Dieser stürzt ab, weil y keinen Pointee hat – ihm wurde keiner zugewiesen.
5. Weisen Sie y = x; zu, sodass y auf den Punkt von x zeigt. Jetzt zeigen x und y auf denselben Pointee – sie "teilen" sich.
6. Versuche, 13 in der Spitze zu dereferenzieren. Diesmal funktioniert es, weil du bei der vorherigen Aufgabe einen Punkt erhalten hast.

Wie Sie sehen, sind Bilder sehr hilfreich, um die Verwendung von Zeigern zu verstehen. Hier ist ein weiteres Beispiel.

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.

Beachten Sie, dass in diesem Beispiel nie Speicher mit dem Operator "new" zugewiesen wurde. Wir haben eine normale Ganzzahlvariable deklariert und mithilfe von Zeigern bearbeitet.

In diesem Beispiel veranschaulichen wir die Verwendung des Operators „Delete“, mit dem die Zuweisung von Heap-Arbeitsspeichern aufgehoben wird. Außerdem wird erläutert, wie die Zuweisung für komplexere Strukturen erfolgen kann. Weitere Informationen zur Speicherorganisation (Heap- und Laufzeitstack) erhalten Sie in einer anderen Lektion. Stellen Sie sich den Heap vorerst als kostenlosen Speicher für laufende Programme vor.

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;

In diesem letzten Beispiel zeigen wir, wie Zeiger verwendet werden, um Werte durch Verweis auf eine Funktion zu übergeben. Auf diese Weise ändern wir die Werte von Variablen innerhalb einer Funktion.

// 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;
}

Wenn wir das „&“ von den Argumenten in der Funktionsdefinition duplizieren, übergeben wir die Variablen „nach Wert“, d.h., der Wert der Variablen wird kopiert. Alle Änderungen, die in der Funktion an der Variablen vorgenommen werden, wirken sich auf die Kopie aus. Sie ändern die ursprüngliche Variable nicht.

Wenn eine Variable über einen Verweis übergeben wird, übergeben wir keine Kopie ihres Werts, sondern die Adresse der Variablen an die Funktion. Jede Änderung, die wir an der lokalen Variablen vornehmen, ändert tatsächlich die ursprüngliche Variable, die übergeben wurde. 

Für C-Programmierer ist das eine ganz neue Wendung. Dasselbe können wir in C tun, indem wir Duplicate() als Duplicate(int *x) deklarieren, wobei x ein Zeiger auf eine Ganzzahl ist. Dann rufen Sie Duplicate() mit dem Argument &x (address-of x) auf und verwenden die Dereferenzierung von x in Duplicate() (siehe unten). C++ bietet jedoch eine einfachere Möglichkeit, Werte durch Verweis an Funktionen zu übergeben, auch wenn die alte "C"-Methode noch funktioniert.

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;
}

Bei C++-Verweisen müssen wir weder die Adresse einer Variablen übergeben noch müssen wir die Variable innerhalb der aufgerufenen Funktion dereferenzieren.

Was gibt das folgende Programm aus? Zeichne ein Bild deines Gedächtnisses, um es herauszufinden.

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;
} 

Führen Sie das Programm aus, um zu sehen, ob Sie die richtige Antwort erhalten haben.

Beispiel 3: Werte durch einen Verweis übergeben

Schreiben Sie eine Funktion namens „Accelerate()“, die als Eingabe die Geschwindigkeit eines Fahrzeugs und einen Betrag annimmt. Die Funktion addiert den Wert zur Geschwindigkeit zum Beschleunigen des Fahrzeugs. Der Geschwindigkeitsparameter muss als Referenz und Menge pro Wert übergeben werden. Hier finden Sie unsere Lösung.

Beispiel 4: Klassen und Objekte

Betrachten Sie die folgende Klasse:

// 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;
  }
}

Beachten Sie, dass Klassenmitgliedsvariablen einen Unterstrich haben. Dies geschieht, um zwischen lokalen und Klassenvariablen zu unterscheiden.

Fügt dieser Klasse eine Dekrementmethode hinzu. Hier finden Sie unsere Lösung.

Die Wunder der Wissenschaft: Informatik

Übungen

Wie im ersten Modul dieses Kurses bieten wir keine Lösungen für Übungen und Projekte an.

Ein gutes Programm...

... wird logisch in Funktionen aufgeteilt, wobei jede einzelne Funktion nur eine einzige Aufgabe ausführt.

... hat ein Hauptprogramm, das sich wie eine Übersicht über die Funktionen des Programms liest.

... hat beschreibende Funktions-, Konstanten- und Variablennamen.

... verwendet Konstanten, um "magische" Zahlen im Programm zu vermeiden.

... hat eine benutzerfreundliche Oberfläche.

Aufwärmübungen

  • Übung 1

    Die Ganzzahl 36 hat eine besondere Eigenschaft: Sie ist ein perfektes Quadrat und ist auch die Summe der Ganzzahlen von 1 bis 8. Die nächste Zahl ist 1.225, also 352, und die Summe der Ganzzahlen von 1 bis 49. Bestimme die nächste Zahl, die ein perfektes Quadrat ist, und auch die Summe einer Reihe 1...n. Die nächste Zahl kann größer als 32.767 sein. Sie können Bibliotheksfunktionen, die Sie kennen, (oder mathematische Formeln) verwenden, um Ihr Programm schneller auszuführen. Dieses Programm kann auch mit For-Schleifen geschrieben werden, um festzustellen, ob eine Zahl ein perfektes Quadrat oder die Summe einer Reihe ist. Hinweis: Je nach Computer und Programm kann es eine Weile dauern, diese Nummer zu finden.

  • Übung 2

    Ihre Universitätsbuchhandlung benötigt Ihre Hilfe, um den Geschäftserfolg für das nächste Jahr zu kalkulieren. Erfahrungsgemäß hängt der Verkauf stark davon ab, ob ein Buch für einen Kurs erforderlich oder nur optional ist und ob es bereits in diesem Kurs verwendet wurde. Ein neues, obligatorisches Lehrbuch wird zu 90% der potenziellen Immatrikulationen verkauft, aber wenn es bereits im Kurs verwendet wurde, kaufen nur 65 %. Ebenso kaufen 40% der potenziellen Einschreibungen ein neues, optionales Lehrbuch. Wenn es im Kurs bereits verwendet wurde, kaufen es aber nur 20 %. Hinweis: „Gebraucht“ bedeutet hier nicht gebrauchte Bücher.

  • Schreiben Sie ein Programm, das eine Reihe von Büchern als Eingabe akzeptiert, bis der Nutzer einen Sentinel eingibt. Fragen Sie für jedes Buch nach: einem Code für das Buch, den Kosten für das einzelne Exemplar, der aktuellen Anzahl der verfügbaren Bücher, der voraussichtlichen Einschreibung für den Kurs und Daten, aus denen hervorgeht, ob das Buch erforderlich/optional, neu/gebraucht ist. Zeige als Ausgabe alle Eingabeinformationen in einem schön formatierten Bildschirm sowie die Gesamtkosten jeder Bestellung an. Außerdem wird angezeigt, wie viele Bücher ggf. bestellt werden müssen (falls vorhanden).

    Wenn alle Eingaben abgeschlossen sind, werden die Gesamtkosten aller Buchbestellungen und der erwartete Gewinn angezeigt, wenn das Geschäft 80% des Listenpreises bezahlt. Da wir noch nicht darüber gesprochen haben, wie Sie mit einer großen Menge von Daten, die in ein Programm einfließen, umgehen können (bleiben Sie dran!), verarbeiten Sie einfach ein Buch nach dem anderen und zeigen Sie den Ausgabebildschirm für dieses Buch an. Wenn der Nutzer alle Daten eingegeben hat, sollte das Programm die Gesamt- und Gewinnwerte ausgeben.

    Bevor Sie mit dem Schreiben von Code beginnen, nehmen Sie sich etwas Zeit, um über das Design dieses Programms nachzudenken. Teilen Sie sich in einen Satz von Funktionen auf und erstellen Sie eine Funktion main(), die wie eine Gliederung für Ihre Lösung des Problems liest. Achten Sie darauf, dass jede Funktion eine Aufgabe erledigt.

    Hier ist eine Beispielausgabe:

    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
    ***************************************************

Datenbankprojekt

In diesem Projekt erstellen wir ein voll funktionsfähiges C++-Programm, das eine einfache Datenbankanwendung implementiert.

Mit unserem Programm können wir eine Datenbank mit Komponisten und relevanten Informationen über sie verwalten. Das Programm bietet unter anderem folgende Funktionen:

  • Die Möglichkeit, einen neuen Komponisten hinzuzufügen
  • Die Fähigkeit, einen Komponisten zu bewerten (d.h. angeben, wie sehr uns die Musik des Komponisten gefällt oder nicht gefällt)
  • Möglichkeit, alle Komponisten in der Datenbank anzusehen
  • Anzeige aller Komponisten nach Rang

„Es gibt zwei Möglichkeiten, ein Softwaredesign zu erstellen: Eine Möglichkeit besteht darin, es so einfach zu gestalten, dass es offensichtlich keine Mängel gibt, und zum anderen, es so kompliziert zu machen, dass es keine offensichtlichen Mängel gibt. Die erste Methode ist viel schwieriger.“ – C.A.R. Hoare

Viele von uns haben das Entwerfen und Programmieren mithilfe eines „prozeduralen“ Ansatzes gelernt. Die zentrale Frage, mit der wir beginnen, lautet: „Was muss das Programm leisten?“ Wir zerlegen die Lösung eines Problems in Aufgaben, die jeweils einen Teil des Problems lösen. Diese Aufgaben entsprechen Funktionen in unserem Programm, die nacheinander aus „main()“ oder aus anderen Funktionen aufgerufen werden. Dieser Schritt-für-Schritt-Ansatz ist ideal für einige Probleme, die wir lösen müssen. Meistens bestehen unsere Programme jedoch nicht nur aus linearen Abfolgen von Aufgaben oder Ereignissen.

Bei einem objektorientierten Ansatz beginnen wir mit der Frage „Welche realen Objekte modelliere ich?“. Anstatt ein Programm wie oben beschrieben in Aufgaben zu unterteilen, unterteilen wir es in Modelle von physischen Objekten. Diese physischen Objekte haben einen Status, der durch eine Reihe von Attributen definiert wird, sowie eine Reihe von Verhaltensweisen oder Aktionen, die sie ausführen können. Die Aktionen können den Status des Objekts ändern oder Aktionen anderer Objekte aufrufen. Grundvoraussetzung ist, dass ein Objekt „wissen“, wie es Dinge eigenständig tun kann. 

Beim OO-Design definieren wir physische Objekte in Form von Klassen und Objekten, Attributen und Verhaltensweisen. In einem OO-Programm gibt es im Allgemeinen eine große Anzahl von Objekten. Viele dieser Objekte sind jedoch im Wesentlichen identisch. Hier einige Tipps:

Eine Klasse ist eine Reihe allgemeiner Attribute und Verhaltensweisen für ein Objekt, die in der realen Welt existieren können. In der Abbildung oben haben wir eine Apple-Klasse. Alle Äpfel haben unabhängig von ihrer Art Farb- und Geschmacksattribute. Außerdem haben wir ein Verhalten definiert, bei dem Apple seine Attribute anzeigt.

In diesem Diagramm haben wir zwei Objekte definiert, die zur Apple-Klasse gehören. Jedes Objekt hat dieselben Attribute und Aktionen wie die Klasse, aber das Objekt definiert die Attribute für eine bestimmte Apfelart. Außerdem zeigt die Anzeigeaktion die Attribute für das jeweilige Objekt an, z.B. „Grün“ und „Sauer“.

Ein OO-Design besteht aus einer Reihe von Klassen, den mit diesen Klassen verknüpften Daten und den Aktionen, die die Klassen ausführen können. Außerdem müssen wir herausfinden, wie verschiedene Klassen interagieren. Diese Interaktion kann von Objekten einer Klasse ausgeführt werden, die Aktionen von Objekten anderer Klassen aufrufen. Wir könnten zum Beispiel eine AppleOutputer-Klasse haben, die die Farbe und den Geschmack eines Arrays von Apple-Objekten ausgibt, indem sie die Display()-Methode jedes Apple-Objekts aufruft.

Im Rahmen des Designs für OO führen wir folgende Schritte aus:

  1. Identifizieren Sie die Klassen und definieren Sie im Allgemeinen, was ein Objekt jeder Klasse als Daten speichert und was ein Objekt tun kann.
  2. Die Datenelemente jeder Klasse definieren
  3. Definieren Sie die Aktionen jeder Klasse und wie einige Aktionen einer Klasse mithilfe von Aktionen anderer verwandter Klassen implementiert werden können.

In einem großen System erfolgen diese Schritte iterativ auf verschiedenen Detailebenen.

Für das Composer-Datenbanksystem benötigen wir eine Composer-Klasse, die alle Daten enthält, die in einem einzelnen Composer gespeichert werden sollen. Ein Objekt dieser Klasse kann sich selbst herauf- oder abwerten (d. h. seinen Rang ändern) und seine Attribute anzeigen.

Wir benötigen auch eine Sammlung von Composer-Objekten. Dazu definieren wir eine Datenbankklasse, die die einzelnen Datensätze verwaltet. Ein Objekt dieser Klasse kann Composer-Objekte hinzufügen oder abrufen und einzelne Objekte anzeigen, indem die Anzeigeaktion eines Composer-Objekts aufgerufen wird.

Schließlich benötigen wir eine Benutzeroberfläche, um interaktive Vorgänge in der Datenbank bereitzustellen. Dies ist eine Platzhalterklasse, d.h., wir wissen noch nicht, wie die Benutzeroberfläche aussehen wird, aber wir wissen, dass wir eine benötigen. Vielleicht wird es grafisch oder vielleicht textbasiert sein. Für den Moment definieren wir einen Platzhalter, den wir später ausfüllen können.

Nachdem Sie nun die Klassen für die Composer-Datenbankanwendung identifiziert haben, definieren Sie im nächsten Schritt die Attribute und Aktionen für die Klassen. In einer komplexeren Anwendung würden wir mit Stift und Papier, UML oder CRC-Karten oder OOD die Klassenhierarchie und die Interaktion der Objekte skizzieren.

Für unsere Composer-Datenbank definieren wir eine Composer-Klasse, die die relevanten Daten enthält, die in jedem Composer gespeichert werden sollen. Sie enthält auch Methoden zur Manipulation von Rankings und zur Anzeige der Daten.

Die Datenbankklasse benötigt eine Struktur zum Speichern von Composer-Objekten. Wir müssen in der Lage sein, der Struktur ein neues Composer-Objekt hinzuzufügen und ein bestimmtes Composer-Objekt abzurufen. Außerdem möchten wir alle Objekte entweder in der Reihenfolge des Eintrags oder nach Rangfolge anzeigen.

Die Klasse "Benutzeroberfläche" implementiert eine menübasierte Oberfläche mit Handlern, die Aktionen in der Datenbankklasse aufrufen. 

Wenn die Klassen leicht verständlich sind und ihre Attribute und Aktionen wie in der Composer-Anwendung klar erkennbar sind, lassen sich die Klassen relativ einfach entwerfen. Wenn Sie jedoch Fragen dazu haben, wie die Kurse miteinander in Beziehung stehen und interagieren, sollten Sie sie am besten als Erstes skizzieren und die Details durcharbeiten, bevor Sie mit dem Programmieren beginnen.

Sobald wir ein klares Bild vom Design haben und es bewertet haben (mehr dazu in Kürze), definieren wir die Schnittstelle für jede Klasse. Die Implementierungsdetails spielen im Moment keine Rolle. Es geht lediglich darum, welche Attribute und Aktionen verwendet werden und welche Teile des Zustands und der Aktionen einer Klasse für andere Klassen verfügbar sind.

In C++ definieren wir dies normalerweise, indem wir für jede Klasse eine Header-Datei definieren. Die Composer-Klasse hat private Datenmitglieder für alle Daten, die auf einem Composer gespeichert werden sollen. Wir benötigen Zugriffsmethoden ("get"-Methoden) und Mutatoren ("set"-Methoden) sowie die primären Aktionen für die Klasse.

// 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_;
};

Die Datenbankklasse ist ebenfalls einfach.

// 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_;
};

Wie Sie sehen, haben wir die Composer-spezifischen Daten sorgfältig in einer separaten Klasse gekapselt. Wir hätten eine Struktur oder Klasse in die Datenbankklasse aufnehmen können, um den Composer-Eintrag darzustellen, und dort direkt darauf zugreifen. Das wäre jedoch eine „Unterobjektifizierung“, d.h. wir modellieren nicht so viel wie möglich mit Objekten. 

Zu Beginn der Implementierung der Klassen Composer und Database werden Sie feststellen, dass eine separate Composer-Klasse viel übersichtlicher ist. Insbesondere werden durch separate atomare Vorgänge für ein Composer-Objekt die Implementierung der Display()-Methoden in der Datenbankklasse erheblich vereinfacht.

Natürlich gibt es auch eine Art von Überobjektifizierung, bei der wir versuchen, alles zu einer Klasse zu machen, oder mehr Klassen haben, als wir benötigen. Es braucht Übung, um die richtige Balance zu finden. Sie werden feststellen, dass die einzelnen Programmierer unterschiedliche Meinungen haben.

Um zu bestimmen, ob Sie zu viel oder zu wenig objektiv betrachten, können Sie dies häufig durch eine sorgfältige Diagrammerstellung Ihrer Klassen klären. Wie bereits erwähnt, ist es wichtig, ein Klassendesign zu entwickeln, bevor Sie mit dem Programmieren beginnen. Dies kann Ihnen bei der Analyse Ihrer Herangehensweise helfen. Eine gängige Notation für diesen Zweck ist UML (Unified Modeling Language). Nachdem wir nun die Klassen für die Composer- und Database-Objekte definiert haben, benötigen wir eine Schnittstelle, über die der Nutzer mit der Datenbank interagieren kann. Dafür gibt es ein einfaches Menü:

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

Wir könnten die Benutzeroberfläche als Klasse oder als Verfahrensprogramm implementieren. Nicht alles in einem C++-Programm muss eine Klasse sein. Wenn die Verarbeitung sequenziell oder aufgabenorientiert erfolgt, wie in diesem Menüprogramm, können Sie sie sogar verfahrenstechnisch implementieren. Es ist wichtig, es so zu implementieren, dass es ein „Platzhalter“ bleibt. Das heißt, wenn wir irgendwann eine grafische Benutzeroberfläche erstellen möchten, sollten wir nichts im System ändern müssen, sondern nur an der Benutzeroberfläche.

Zum Abschluss der Anwendung benötigen wir noch ein Programm zum Testen der Klassen. Für die Composer-Klasse benötigen wir ein main()-Programm, das Eingaben annimmt, ein Composer-Objekt füllt und es dann anzeigt, um sicherzustellen, dass die Klasse ordnungsgemäß funktioniert. Außerdem möchten wir alle Methoden der Composer-Klasse aufrufen.

// 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();
}

Wir benötigen ein ähnliches Testprogramm für die Datenbankklasse.

// 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();
}

Diese einfachen Testprogramme sind ein guter erster Schritt. Wir müssen die Ausgabe jedoch manuell prüfen, um sicherzugehen, dass das Programm ordnungsgemäß funktioniert. Wenn ein System größer wird, wird eine manuelle Überprüfung der Ausgabe schnell unpraktisch. In einer weiteren Lektion stellen wir selbstprüfende Testprogramme in Form von Einheitentests vor.

Das Design für unsere Anwendung ist jetzt fertig. Im nächsten Schritt implementieren Sie die CPP-Dateien für die Klassen und die Benutzeroberfläche.Kopieren Sie als Erstes den obigen .h- und Testtreibercode in Dateien und kompilieren Sie sie.Testen Sie Ihre Klassen mithilfe der Testtreiber. Implementieren Sie anschließend die folgende Schnittstelle:

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

Verwenden Sie die Methoden, die Sie in der Datenbankklasse definiert haben, um die Benutzeroberfläche zu implementieren. Sorgen Sie dafür, dass Ihre Methoden fehlerfrei sind. Ein Ranking sollte beispielsweise immer im Bereich von 1 bis 10 liegen. Fügen Sie auch keine 101-Komponisten hinzu, es sei denn, Sie möchten die Datenstruktur in der Datenbankklasse ändern.

Der gesamte Code muss unseren Codierungskonventionen entsprechen, die hier noch einmal aufgeführt sind:

  • Jedes Programm, das wir schreiben, beginnt mit einem Kopfzeilenkommentar, in dem der Name des Autors, dessen Kontaktdaten, eine kurze Beschreibung und gegebenenfalls die Nutzung angegeben wird. Jede Funktion/Methode beginnt mit einem Kommentar zu Vorgang und Nutzung.
  • Wir fügen erklärende Kommentare in ganzen Sätzen hinzu, wenn der Code sich nicht selbst dokumentiert, z. B. wenn die Verarbeitung schwierig, nicht offensichtlich, interessant oder wichtig ist.
  • Verwenden Sie immer aussagekräftige Namen: Variablen sind Wörter in Kleinbuchstaben, die durch _ getrennt sind, wie in „meine_variable“. Funktions-/Methodennamen verwenden Großbuchstaben, um Wörter zu markieren, wie in MyExcitingFunction(). Konstanten beginnen mit einem "k" und verwenden Großbuchstaben, um Wörter zu markieren, wie in "kDaysInWeek".
  • Der Einzug erfolgt als Vielfaches von zwei. Die erste Ebene besteht aus zwei Leerzeichen. Wenn eine weitere Einrückung erforderlich ist, verwenden wir vier, sechs Leerzeichen usw.

Willkommen in der realen Welt!

In diesem Modul stellen wir zwei sehr wichtige Tools vor, die in den meisten Softwareentwicklungsunternehmen verwendet werden. Die erste ist ein Build-Tool und das zweite ein Konfigurationsverwaltungssystem. Beide Tools sind im Bereich der industriellen Softwareentwicklung unverzichtbar, da viele Ingenieure oft an einem großen System arbeiten. Diese Tools helfen Ihnen, Änderungen an der Codebasis zu koordinieren und zu steuern, und bieten ein effizientes Mittel zum Kompilieren und Verknüpfen eines Systems aus vielen Programm- und Headerdateien.

Makefiles

Die Erstellung eines Programms wird normalerweise mit einem Build-Tool verwaltet, das die erforderlichen Dateien in der richtigen Reihenfolge kompiliert und verknüpft. C++-Dateien weisen häufig Abhängigkeiten auf. So befindet sich eine Funktion, die in einem Programm aufgerufen wird, in einem anderen Programm. Vielleicht wird aber auch eine Header-Datei für verschiedene CPP-Dateien benötigt. Ein Build-Tool ermittelt aus diesen Abhängigkeiten die richtige Kompilierungsreihenfolge. Außerdem werden nur Dateien kompiliert, die sich seit dem letzten Build geändert haben. Dies kann in Systemen, die aus mehreren Hundert oder Tausenden von Dateien bestehen, viel Zeit sparen.

Häufig wird ein Open-Source-Build-Tool namens Make verwendet. Weitere Informationen finden Sie in diesem Artikel. Prüfen Sie, ob Sie eine Abhängigkeitsgrafik für die Composer-Datenbankanwendung erstellen und diese dann in ein Makefile umwandeln können.Hier finden Sie unsere Lösung.

Konfigurationsverwaltungssysteme

Das zweite Tool im industriellen Software Engineering ist die Konfigurationsverwaltung (CM). Dies wird zum Verwalten von Änderungen verwendet. Angenommen, Bob und Susan sind technische Writer und arbeiten beide an Updates eines technischen Handbuchs. Während einer Besprechung weist die zuständige Führungskraft jeder Person einen Abschnitt desselben Dokuments zu, der aktualisiert werden soll.

Das technische Handbuch ist auf einem Computer gespeichert, auf den Bob und Susanne zugreifen können. Ohne CM-Tools oder -Prozesse könnten eine Reihe von Problemen auftreten. Ein möglicher Szenario ist, dass der Computer, auf dem das Dokument gespeichert wird, so eingerichtet ist, dass Bob und Susan nicht gleichzeitig am Handbuch arbeiten können. Dies würde sie erheblich verlangsamen.

Eine gefährliche Situation tritt auf, wenn der Speichercomputer zulässt, dass das Dokument gleichzeitig von Bob und Susan geöffnet wird. Folgendes kann passieren:

  1. Max öffnet das Dokument auf seinem Computer und arbeitet an seinem Bereich.
  2. Susan öffnet das Dokument auf ihrem Computer und arbeitet an ihrem Bereich.
  3. Max nimmt die Änderungen vor und speichert das Dokument auf dem Computer.
  4. Susanne nimmt die Änderungen vor und speichert das Dokument auf dem Computer.

Diese Abbildung zeigt das Problem, das auftreten kann, wenn das technische Handbuch keine Steuerelemente enthält. Wenn Susan ihre Änderungen speichert, überschreibt sie die von Bob gemachten Änderungen.

Genau diese Situation kann ein CM-System in den Griff bekommen. Mit einem CM-System „checken“ Bob und Susan ihre eigene Kopie des technischen Handbuchs aus und arbeiten daran. Als Bob seine Änderungen noch einmal überprüft, weiß das System, dass Susan ihre eigene Kopie ausgecheckt hat. Wenn Susan die Kopie überprüft, analysiert das System die von Ben und Susan vorgenommenen Änderungen und erstellt eine neue Version, in der die beiden Änderungen zusammengeführt werden.

CM-Systeme bieten neben der oben beschriebenen Verwaltung gleichzeitiger Änderungen eine Reihe von Funktionen. Viele Systeme speichern Archive aller Versionen eines Dokuments ab dem Zeitpunkt seiner Erstellung. Bei einem technischen Handbuch kann dies sehr hilfreich sein, wenn ein Nutzer eine alte Version des Handbuchs hat und einem technischen Redakteur Fragen stellt. Über ein CM-System kann der technische Redakteur auf die alte Version zugreifen und sehen, was der Nutzer sieht.

CM-Systeme sind besonders nützlich, um Änderungen an Software zu steuern. Diese Systeme werden Software Configuration Management (SCM) genannt. Wenn Sie die große Anzahl einzelner Quellcodedateien in einem großen Softwareentwicklungsunternehmen und die große Anzahl der Entwickler betrachten, die Änderungen daran vornehmen müssen, ist ein SCM-System von entscheidender Bedeutung.

Verwaltung der Softwarekonfiguration

SCM-Systeme basieren auf einer einfachen Idee: Die endgültigen Kopien Ihrer Dateien werden in einem zentralen Repository gespeichert. Die Nutzer können Kopien von Dateien aus dem Repository abrufen, an diesen Kopien arbeiten und diese dann wieder einchecken, wenn sie fertig sind. SCM-Systeme verwalten und verfolgen Überarbeitungen von mehreren Personen mit einem einzigen Master-Satz. 

Alle SCM-Systeme bieten die folgenden grundlegenden Funktionen:

  • Gleichzeitigkeitsmanagement
  • Versionsverwaltung
  • Synchronisierungsrechte

Sehen wir uns diese Funktionen einmal genauer an.

Gleichzeitigkeitsmanagement

Gleichzeitigkeit bezieht sich auf die gleichzeitige Bearbeitung einer Datei durch mehrere Personen. Bei einem großen Repository möchten wir, dass dies möglich ist, aber das kann zu Problemen führen.

Ein einfaches Beispiel im Engineering-Bereich: Angenommen, wir ermöglichen es Entwicklern, dieselbe Datei gleichzeitig in einem zentralen Repository mit Quellcode zu ändern. Client1 und Client2 müssen gleichzeitig Änderungen an einer Datei vornehmen:

  1. Client1 öffnet bar.cpp.
  2. Client2 öffnet bar.cpp.
  3. Client1 ändert die Datei und speichert sie.
  4. Client2 ändert die Datei und speichert sie, wobei die Änderungen von Client1 überschrieben werden.

Das möchten wir natürlich nicht. Auch wenn wir die Situation dadurch gesteuert haben, dass die beiden Entwickler an separaten Kopien statt direkt an einem Mastersatz arbeiten lassen (wie in der Abbildung unten), müssen die Kopien auf irgendeine Weise abgeglichen werden. Die meisten SCM-Systeme lösen dieses Problem dadurch an, dass mehrere Entwickler eine Datei prüfen („Synchronisieren“ oder „Aktualisieren“) und nach Bedarf Änderungen vornehmen können. Das SCM-System führt dann Algorithmen aus, um die Änderungen zusammenzuführen, sobald die Dateien wieder in das Repository eingecheckt werden („Submit“ oder „Commit“).

Diese Algorithmen können einfach (die Entwickler bitten, widersprüchliche Änderungen zu beheben) oder nicht so einfach sein (bestimmen, wie die in Konflikt stehenden Änderungen intelligent zusammengeführt werden können, und nur einen Entwickler zu fragen, wenn das System wirklich hängen bleibt). 

Versionsverwaltung

Die Versionsverwaltung bezieht sich auf das Nachverfolgen von Dateiüberarbeitungen, sodass eine vorherige Version der Datei neu erstellt oder ein Rollback durchgeführt werden kann. Dazu wird entweder eine Archivkopie jeder Datei beim Einchecken in das Repository erstellt oder jede an einer Datei vorgenommene Änderung gespeichert. Wir können die Archive jederzeit verwenden oder Informationen ändern, um eine frühere Version zu erstellen. Versionsverwaltungssysteme können auch Logberichte darüber erstellen, wer Änderungen eingecheckt hat, wann sie eingecheckt wurden und was die Änderungen waren.

Synchronisierungsrechte

Bei einigen SCM-Systemen werden einzelne Dateien in das Repository ein- und aus diesem ein- und daraus entfernt. Mit leistungsstärkeren Systemen können Sie mehrere Dateien gleichzeitig überprüfen. Entwickler prüfen ihre eigene, vollständige Kopie des Repositorys (oder von Teilen davon) und arbeiten nach Bedarf an den Dateien. Sie übertragen ihre Änderungen dann regelmäßig an das Master-Repository und aktualisieren ihre eigenen persönlichen Kopien, um über die Änderungen anderer Personen auf dem Laufenden zu bleiben. Dieser Vorgang wird als Synchronisierung oder Aktualisierung bezeichnet.

Marinieren

Subversion (SVN) ist ein Open-Source-System zur Versionsverwaltung. Sie bietet alle oben beschriebenen Funktionen.

SVN verfolgt beim Auftreten von Konflikten eine einfache Methode. Ein Konflikt tritt auf, wenn zwei oder mehr Entwickler unterschiedliche Änderungen am selben Bereich der Codebasis vornehmen und dann beide ihre Änderungen senden. SVN benachrichtigt nur die Entwickler, wenn ein Konflikt vorliegt – es liegt an den Entwicklern, diesen zu beheben.

In diesem Kurs verwenden wir SVN, um Sie mit der Konfigurationsverwaltung vertraut zu machen. Solche Systeme sind in der Branche weit verbreitet.

Der erste Schritt besteht darin, SVN auf Ihrem System zu installieren. Eine Anleitung dazu finden Sie hier. Suchen Sie Ihr Betriebssystem und laden Sie das entsprechende Binärprogramm herunter.

SVN-Terminologie

  • Überarbeitung: Eine Änderung in einer oder mehreren Dateien. Eine Überarbeitung ist ein „Snapshot“ in einem sich ständig ändernden Projekt.
  • Repository: Die Masterkopie, in der SVN den vollständigen Überarbeitungsverlauf eines Projekts speichert. Jedes Projekt hat ein Repository.
  • Arbeitstext: Die Kopie, in der ein Entwickler Änderungen an einem Projekt vornimmt. Es kann viele Arbeitskopien eines bestimmten Projekts geben, die jeweils einem einzelnen Entwickler gehören.
  • Auschecken: Um eine funktionierende Kopie aus dem Repository anzufordern. Eine Arbeitskopie entspricht dem Status des Projekts zum Zeitpunkt des Auscheckens.
  • Commit: Änderungen von Ihrer Arbeitskopie werden an das zentrale Repository gesendet. Wird auch als Check-in oder Einreichen bezeichnet.
  • Aktualisierung: Um die Änderungen anderer Nutzer aus dem Repository in Ihre Arbeitskopie zu übernehmen oder um anzuzeigen, ob Ihre Arbeitskopie Änderungen ohne Commit enthält. Dies entspricht einer Synchronisierung, wie oben beschrieben. Durch „Aktualisieren/Synchronisieren“ wird Ihre Arbeitskopie also mit der Repository-Kopie auf dem neuesten Stand.
  • Konflikt: Die Situation, in der zwei Entwickler versuchen, Änderungen am selben Bereich einer Datei zu übernehmen. SVN weist auf Konflikte hin, die jedoch von den Entwicklern behoben werden müssen.
  • Lognachricht: Ein Kommentar, den Sie einer Überarbeitung nach einem Commit anhängen und die Änderungen beschreiben. Das Log bietet eine Zusammenfassung der Abläufe in einem Projekt.

Nachdem Sie SVN installiert haben, führen wir nun einige grundlegende Befehle aus. Als Erstes richten Sie ein Repository in einem bestimmten Verzeichnis ein. Dies sind die Befehle:

$ 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.

Mit dem Befehl import wird der Inhalt des Verzeichnisses „mytree“ in das Verzeichnisprojekt im Repository kopiert. Mit dem Befehl list können wir uns das Verzeichnis im Repository ansehen.

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

Beim Import wird keine funktionierende Kopie erstellt. Dazu müssen Sie den Befehl svn checkout verwenden. Dadurch wird eine Arbeitskopie der Verzeichnisstruktur erstellt. Lassen Sie uns das jetzt tun:

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

Nachdem Sie nun eine Arbeitskopie haben, können Sie dort Änderungen an den Dateien und Verzeichnissen vornehmen. Ihre Arbeitskopie ist wie jede andere Sammlung von Dateien und Verzeichnissen: Sie können neue Dateien hinzufügen oder bearbeiten, verschieben und sogar die gesamte Arbeitskopie löschen. Wenn Sie Dateien in Ihrer Arbeitskopie kopieren und verschieben, müssen Sie svn copy und svnmove anstelle der Betriebssystembefehle verwenden. Wenn Sie eine neue Datei hinzufügen möchten, verwenden Sie svn add, zum Löschen einer Datei svn delete. Wenn Sie sie nur bearbeiten möchten, öffnen Sie die Datei einfach mit Ihrem Editor und bearbeiten Sie sie.

Es gibt einige Standardverzeichnisnamen, die häufig mit Subversion verwendet werden. Das Verzeichnis „Trunk“ enthält die Hauptentwicklungslinie für Ihr Projekt. Das Verzeichnis „Zweige“ enthält jede Zweig-Version, an der Sie möglicherweise arbeiten.

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

Angenommen, Sie haben alle erforderlichen Änderungen an Ihrer Arbeitskopie vorgenommen und möchten sie mit dem Repository synchronisieren. Wenn viele andere Entwickler in diesem Bereich des Repositorys arbeiten, ist es wichtig, Ihre Arbeitskopie auf dem neuesten Stand zu halten. Mit dem Befehl svn status können Sie die von Ihnen vorgenommenen Änderungen aufrufen.

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

Beachten Sie, dass für den Befehl status viele Flags vorhanden sind, um diese Ausgabe zu steuern. Wenn Sie die spezifischen Änderungen in einer geänderten Datei sehen möchten, verwenden Sie 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;
...

Schließlich aktualisieren Sie die Arbeitskopie aus dem Repository mit dem Befehl svn update.

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

Dies ist ein Ort, an dem ein Konflikt auftreten kann. In der obigen Ausgabe gibt das "U" an, dass an den Repository-Versionen dieser Dateien keine Änderungen vorgenommen wurden und eine Aktualisierung durchgeführt wurde. Das „G“ bedeutet, dass eine Zusammenführung durchgeführt wurde. Die Repository-Version wurde geändert, aber die Änderungen standen nicht mit Ihrer Version in Konflikt. Das „C“ steht für einen Konflikt. Das bedeutet, dass sich die Änderungen aus dem Repository mit Ihren Änderungen überschnitten haben und jetzt Sie zwischen ihnen wählen müssen.

Für jede Datei, bei der ein Konflikt vorliegt, fügt Subversion drei Dateien in Ihre Arbeitskopie ein:

  • file.mine: Dies ist die Datei, wie sie in Ihrer Arbeitskopie vorhanden war, bevor Sie die Arbeitskopie aktualisiert haben.
  • file.rOLDREV: Dies ist die Datei, die Sie aus dem Repository ausgecheckt haben, bevor Sie Ihre Änderungen vorgenommen haben.
  • file.rNEWREV: Diese Datei ist die aktuelle Version im Repository.

Sie haben drei Möglichkeiten, den Konflikt zu lösen:

  • Gehen Sie die Dateien durch und führen Sie die Zusammenführung manuell durch.
  • Kopieren Sie eine der von SVN erstellten temporären Dateien in Ihre Arbeitskopie.
  • Führen Sie den Befehl svnreset aus, um alle Änderungen zu verwerfen.

Wenn Sie den Konflikt gelöst haben, informieren Sie SVN darüber, indem Sie svn fixed ausführen. Dadurch werden die drei temporären Dateien entfernt und SVN sieht die Datei nicht mehr in einem Konfliktstatus.

Als Letztes müssen Sie Ihre endgültige Version per Commit im Repository speichern. Dazu führen Sie den Befehl svn commit aus. Wenn Sie eine Änderung mit einem Commit speichern, müssen Sie eine Lognachricht angeben, die Ihre Änderungen beschreibt. Diese Lognachricht wird an die von Ihnen erstellte Überarbeitung angehängt.

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

Es gibt noch so viel mehr über SVN und die Möglichkeiten zur Unterstützung großer Softwareentwicklungsprojekte zu erfahren. Im Web sind umfangreiche Ressourcen verfügbar. Führen Sie einfach eine Google-Suche nach "Subversion" durch.

Erstellen Sie zu Übungszwecken ein Repository für das Composer-Datenbanksystem und importieren Sie alle Ihre Dateien. Prüfen Sie dann eine Arbeitskopie und führen Sie die oben beschriebenen Befehle aus.

Verweise

Online-Subversionsbuch

Wikipedia-Artikel zu SVN

Subversionswebsite

Anwendung: Eine Studie in der Anatomie

Sehen Sie sich eSkeletons der University of Texas in Austin an.