Następne kroki

Wprowadzenie do programowania i C++

Ten samouczek online zawiera bardziej zaawansowane zagadnienia. Przeczytaj część III. W tym module skupimy się na używaniu wskaźników i rozpoczynaniu pracy z obiektmi.

Ucz się na przykładzie nr 2

W tym module skupimy się na przyswajaniu wiedzy w zakresie rozkładania elementów, zrozumieniu wskaźników oraz rozpoczynaniu pracy z obiektami i klasami. Przeanalizuj podane niżej przykłady. Pisać programy samodzielnie, gdy zostaniesz o to poproszony, lub przeprowadzić eksperymenty. Nie możemy podkreślić wystarczająco, że kluczem do zostania dobrym programistą jest ćwiczenie, ćwiczenie i jeszcze raz ćwiczenie.

Przykład 1. Więcej ćwiczeń rozkładu

Oto dane wyjściowe z prostej gry:

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.

Pierwsza obserwacja to tekst wprowadzający wyświetlany raz na program. Potrzebujemy generatora liczb losowych, aby określić odległość wroga dla każdego runda. Potrzebujemy mechanizmu do pobierania danych kątowych z odtwarzacza, przypomina pętlę, która powtarza się, dopóki nie trafimy na wroga. Dodatkowo potrzebujesz funkcji do obliczania odległości i kąta. Musimy też śledzić, ile strzałów potrzebowaliśmy, aby trafić wroga, a także ile wrogów udało nam się trafić podczas wykonywania programu. Oto możliwy zarys głównego programu.

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;

Procedura „Ognia” służy do przebiegu rozgrywki. W tej funkcji wywołujemy funkcję z generatora liczb losowych, aby zmierzyć odległość wroga, a następnie skonfigurować pętlę, uzyskać dane wejściowe gracza i obliczyć, czy uderzył wróg, czy nie. Warunek warowny oznacza, jak blisko zbliżyliśmy się do wroga.

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

Ze względu na wywołania funkcji cos() i sin() musisz dodać funkcję math.h. Wypróbuj na pisaniu tego programu. To wspaniała praktyka w rozkładaniu problemów. poczytaj o podstawowej wersji C++. Pamiętaj, aby dla każdej funkcji wykonać tylko jedno zadanie. To najbardziej zaawansowany program, jaki do tej pory napisaliśmy, więc może Ci zająć trochę czasu. Oto nasze rozwiązanie.

Przykład 2. Ćwiczenie za pomocą wskaźników

Podczas pracy ze wskaźnikami należy pamiętać o 4 rzeczach:
  1. Wskaźniki to zmienne, które przechowują adresy pamięci. Podczas realizacji programu Wszystkie zmienne są przechowywane w pamięci, każda z osobnym, unikalnym adresem lub lokalizacją. Wskaźnik to specjalny typ zmiennej, który zawiera adres pamięci zamiast wartości danych. Tak jak dane są modyfikowane po użyciu zwykłej zmiennej, wartość adresu zapisanego we wskaźniku jest modyfikowana jako zmienna wskaźnika jest manipulacją. Oto przykład:
    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. Zwykle określamy, że wskaźnik „wskazuje” do lokalizacji, w której jest przechowywana. („pointee”). W powyższym przykładzie intptr wskazuje na punkt 5.

    Zwróć uwagę na użycie atrybutu „nowy” operator przydzielający pamięć dla liczby całkowitej Pointee. To musisz zrobić, zanim spróbujesz uzyskać dostęp do punktu.

    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.
          

    Operator * służy do usuwania odniesienia w C. Jednym z najczęstszych błędów popełnianych przez programistów C/C++ podczas pracy z wskaźnikami jest zapominanie o inicjalizacji wskazywanego obiektu. Czasami może to spowodować awarię w czasie wykonywania, ponieważ uzyskujemy dostęp do lokalizacji w pamięci, która zawiera nieznane dane. Jeśli spróbujemy to zmienić może powodować niewielkie uszkodzenie pamięci, co utrudnia wykrycie błędów.

  3. Przypisanie wskaźnika między 2 wskaźnikami sprawia, że wskazują one ten sam punkt. Przypisanie y = x; powoduje, że zmienna y wskazuje na ten sam obiekt co zmienna x. Przypisanie wskaźnika nie dotyka punktu. Zmienia się tylko 1 wskaźnik na tę samą lokalizację jako kolejny wskazówkę. Po przypisaniu wskaźnika dwa wskaźniki „udostępnij” 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
    }
      

Oto ślad tego kodu:

1. Przydziel 2 wskaźniki x i y. Przydzielenie wskaźników nie przydzielać żadnych punktów.
2. Przydziel punkt i ustaw x, aby wskazać ten punkt.
3. Odwołaj się do wartości x, aby zapisać w punkcie 42 wartość 42. To jest podstawowy przykład. podczas operacji usuwania danych. Zacznij od x. Aby przejść, podążaj za strzałką. swojego punktu widzenia.
4. Spróbuj przywrócić parametr 13 do zapisu w jego punkcie. Awaria, ponieważ nie ma wskaźnika – nigdy nie został mu przypisany.
5. Przypisz y = x; tak aby y wskazywał punkt x. Teraz x i y wskazują ten sam element docelowy – „dzielą” się.
6. Spróbuj przywrócić parametr 13 do zapisu w jego punkcie. Tym razem działa, ponieważ poprzednie przypisanie nadało zmiennej y wartość wskazywaną.

Jak widać, obrazy bardzo pomagają w zrozumieniu użycia wskaźnika. Oto inny przykład.

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.

Zwróć uwagę, że w tym przykładzie nigdy nie przydzielaliśmy pamięci z parametrem „new” . Zadeklarowaliśmy normalną zmienną całkowitą i zmieniliśmy ją za pomocą wskaźników.

W tym przykładzie pokazujemy użycie operatora usuwania, który zwalnia przydział. pamięci sterty i jak można je przydzielić do bardziej złożonych struktur. Organizację pamięci (pniazd i stosu w czasie wykonywania) omówimy w innej lekcji. Na razie pomyśl o stosie jako wolnym obszarze pamięci dostępnym dla uruchomionych programów.

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;

W ostatnim przykładzie pokazujemy, jak wskaźniki służą do przekazywania wartości przez odwołanie. do funkcji. W ten sposób zmieniamy wartości zmiennych w funkcji.

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

Gdybyśmy zostawili znak & w argumentach w definicji funkcji zduplikowanej, przekazujemy zmienne „według wartości”, tj. kopia zawiera wartość tę zmienną. Wszelkie zmiany wprowadzone w zmiennej w funkcji powodują modyfikację kopii. Nie modyfikują oryginalnej zmiennej.

Jeśli zmienna jest przekazywana przez odwołanie, nie przekazujemy kopii jej wartości, przekazujemy do funkcji adres zmiennej. Wszelkie modyfikacje zmieniamy też zmienną lokalną, która powoduje modyfikację oryginalnej przekazywanej przez nią zmiennej.

Jeśli jesteś programistą C, to zupełnie nowa odsłona. To samo możemy zrobić w języku C, deklarując funkcję Duplicate() jako Duplicate(int *x), gdzie x jest wskaźnikiem do zmiennej typu int, a potem wywołując funkcję Duplicate() z argumentem &x (adres x) i wykorzystując odwoływanie xDuplicate() (patrz poniżej). C++ zapewnia jednak prostszy sposób przekazywania wartości do funkcji przez mimo że poprzednie „C” nadal działa.

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

Zwróć uwagę na odwołania do C++: nie musimy przekazywać adresu zmiennej ani należy usunąć odwołanie do zmiennej wewnątrz wywołanej funkcji.

Co generuje następujący program? Narysuj obraz pamięci, aby go zrozumieć.

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

Uruchom program, aby sprawdzić, czy odpowiedź jest prawidłowa.

Przykład 3: Przekazywanie wartości za pomocą odwołania

Napisz funkcję o nazwie przyspieszyć(), która pobiera jako dane wejściowe prędkość pojazdu oraz ilość. Funkcja dodaje wartość do prędkości pojazdu. Parametr speed powinien być przekazywany przez odniesienie, a parametr amount – przez wartość. Oto nasze rozwiązanie.

Przykład 4. Klasy i obiekty

Weźmy pod uwagę te zajęcia:

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

Zwróć uwagę, że zmienne członkowskie klasy mają podkreślnik na końcu. W ten sposób można odróżnić zmienne lokalne i zmienne klasy.

Dodaj do tej klasy metodę zmniejszania. Oto nasze rozwiązanie.

Cuda nauki: informatyka

Ćwiczenia

Tak jak w pierwszym module tego szkolenia, nie udostępniamy rozwiązań dotyczących ćwiczeń i projektów.

Pamiętaj, że dobry program:

... jest logicznie podzielony na funkcje, w których dowolna wykonuje tylko jedno zadanie.

... ma główny program, który stanowi zarys tego, co program będzie robić.

... ma opisowe nazwy funkcji, stałych i zmiennych.

... używa stałych, by uniknąć wszelkiej „magii” w programie.

... ma przyjazny interfejs.

Ćwiczenia rozgrzewające

  • Ćwiczenie 1

    Liczba całkowita 36 ma dziwną właściwość: jest to kwadrat idealny, a także sumę liczb całkowitych z zakresu od 1 do 8. Następna taka liczba to 1225, która to 352 i sumę liczb całkowitych z zakresu od 1 do 49. Znajdź następny numer który stanowi idealny kwadrat, a także sumę szeregu 1...n. Ten następny numer może być większy niż 32 767. Możesz korzystać z funkcji biblioteki, o których znasz, (lub wzorów matematycznych), aby przyspieszyć działanie programu. Program można też napisać za pomocą pętli for, aby określić, czy liczba jest kwadratem doskonałym czy sumą ciągu. (Uwaga: w zależności od komputera i programu znalezienie tego numeru może trochę potrwać).

  • Ćwiczenie 2

    Twoja księgarnia uniwersytecka potrzebuje Twojej pomocy w oszacowaniu możliwości roku. Doświadczenie pokazuje, że sprzedaż zależy w dużym stopniu od tego, czy potrzebna jest książka dla danego kursu lub tylko opcjonalnego, i określ, czy został on użyty podczas zajęć wcześniej. Nowy, wymagany podręcznik zostanie sprzedany 90% studentów, ale jeśli był on wcześniej używany podczas zajęć, nabywa go tylko 65% użytkowników. Podobnie 40% studentów kupi nowy, opcjonalny podręcznik, jeśli zostało użyte podczas zajęć, zanim tylko 20% z nich dokona zakupu. (zwróć uwagę na słowo „używane” nie oznacza książek z drugiej ręki).

  • Napisz program, który akceptuje jako dane wejściowe serię książek (dopóki użytkownik nie wpisze hasła) strażnik). W przypadku każdej książki podaj: kod książki, koszt pojedynczego egzemplarza książkę, aktualną liczbę książek pod ręką, prawdopodobną liczbę uczniów, i dane wskazujące, czy książka jest wymagana/opcjonalna czy nowa/używana w przeszłości. Jako wszystkie dane wejściowe na odpowiednio sformatowanym ekranie liczbę książek, które należy zamówić (jeśli występują, pamiętaj, że zamawiane są tylko nowe książki), łączny koszt każdego zamówienia.

    Następnie, po wprowadzeniu wszystkich danych, pokaż łączny koszt wszystkich zamówień książek oraz spodziewany zysk, jeśli sklep zapłaci 80% rynkowej ceny detalicznej. Ponieważ nie omawialiśmy jeszcze sposobów obsługi dużych zbiorów danych przesyłanych do programu (obserwuj nas!), przetwarzaj po jednej książce naraz i wyświetlaj ekran wyjściowy dla danej książki. Następnie, gdy użytkownik zakończy wpisywanie wszystkich danych, program powinien wyświetlić sumy i zysku.

    Zanim zaczniesz pisać kod, zastanów się nad stworzeniem tego programu. Rozłóż problem na zestaw funkcji i utwórz funkcję main(), która wygląda jak zarys rozwiązania. Upewnij się, że każda funkcja wykonuje tylko jedno zadanie.

    Oto przykładowe dane wyjściowe:

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

Projekt bazy danych

W ramach tego projektu utworzymy w pełni funkcjonalny program w C++, który wdraża prostą aplikację bazy danych.

Nasz program pozwoli nam zarządzać bazą danych kompozytorów i odpowiednich informacji o nich. Funkcje programu:

  • możliwość dodania nowego kompozytora.
  • Możliwość określenia pozycji kompozytora w rankingu (tzn. wskazanie, co nam się podoba, a co nie) muzyki kompozytora)
  • Możliwość wyświetlania wszystkich kompozytorów w bazie danych
  • możliwość wyświetlania wszystkich kompozytorów według rangi;

„Można zaprojektować oprogramowanie na 2 sposoby: można je stworzyć w taki sposób, aby było tak proste, że nie było w nim żadnych oczywistych braków, albo tak skomplikowane, aby nie było w nim żadnych oczywistych braków. Pierwsza metoda jest o wiele trudniejsza”. – C.A.R. Hoare

Wielu z nas nauczyło się projektowania i programowania w sposób „proceduralny”. Najważniejsze pytanie, od którego zaczynamy, brzmi: „Co ma robić program?”. Rozkładamy rozwiązanie problemu na zadania, z których każde rozwiązuje pewną część problemu. Te zadania są mapowane na funkcje w naszym programie, które są wywoływane kolejno z main() lub z innych funkcji. Ta szczegółowa procedura jest idealna dla niektórych, które musimy rozwiązać. Większość naszych programów nie jest jednak zwykłą sekwencją zadań lub zdarzeń.

Przy podejściu zorientowanym na obiekt zaczynamy od pytania „Jaki jest modelowanych obiektów?” Zamiast dzielić program na zadania, jak opisano powyżej, dzielimy go na modele obiektów fizycznych. Te fizyczne obiekty stan zdefiniowany przez zestaw atrybutów oraz zestaw zachowań lub działań, które mogą osiągnąć. Te działania mogą zmienić stan obiektu lub mogą wywoływania działań innych obiektów. Podstawowe założenie jest takie, że obiekt „wie” jak do samodzielnego działania.

W projekcie ogólnym definiujemy obiekty fizyczne za pomocą klas i obiektów. atrybuty i zachowaniach. W programie operacyjnym jest zwykle bardzo dużo obiektów. Wiele z tych obiektów jest jednak zasadniczo takich samych. Weź pod uwagę poniższe kwestie.

Klasa to zbiór ogólnych atrybutów i zachowań danego obiektu, który może istnieć fizycznie w świecie rzeczywistym. Na ilustracji powyżej mamy zajęcia Apple. Wszystkie jabłka, niezależnie od rodzaju, mają atrybuty koloru i smaku. Mamy zdefiniowano także zachowanie, w ramach którego Apple wyświetla swoje atrybuty.

Na tym diagramie zdefiniowaliśmy 2 obiekty klasy Apple. Każdy obiekt ma te same atrybuty i działania co klasa, ale definiuje atrybuty dla konkretnego typu jabłka. Dodatkowo, akcja wyświetla atrybuty danego obiektu, np. „Zielony” i „kwaśne”.

Projekt OO składa się z zestawu klas, danych powiązanych z nimi oraz zestaw działań, które można wykonać na zajęciach. Musimy też określić na sposób interakcji między poszczególnymi klasami. Obiekty mogą wykonywać tę interakcję klasy wywołującej działania obiektów innych klas. Na przykład możemy może mieć klasę AppleOutputer, która zwraca kolor i smak tablicy przez wywołanie metody Display() każdego obiektu Apple.

Oto czynności, które wykonujemy podczas projektowania OO:

  1. Zidentyfikuj klasy i zdefiniuj ogólnie obiekt każdej z nich i funkcji obiektu.
  2. Zdefiniuj elementy danych każdej klasy
  3. Zdefiniuj działania każdej z zajęć i sposoby realizacji niektórych z nich zaimplementowane za pomocą działań innych powiązanych klas.

W dużym systemie te kroki następują iteracyjnie na różnych poziomach szczegółowości.

W systemie bazy danych kompozytora potrzebna jest klasa Composer, która zawiera wszystkie dane, które chcemy przechowywać w odniesieniu do konkretnego kompozytora. Obiekt tej klasy może promować lub degradować swoją pozycję (zmienić pozycję w rankingu) i wyświetlać jego atrybuty.

Potrzebny jest też zbiór obiektów Composer. Definiujemy to na potrzeby klasy bazy danych. który zarządza poszczególnymi rekordami. Obiekt tej klasy może dodawać i pobierać Composer, a potem wyświetlać poszczególne, wywołując działanie wyświetlania obiektem Composer.

Potrzebujemy też jakiegoś interfejsu, aby zapewnić interaktywne operacje w bazie danych. To jest klasa zastępcza, czyli nie wiemy jeszcze, jak będzie wyglądać interfejs użytkownika, ale wiemy, że będzie nam on potrzebny. Może to być grafika, a może tekst. Na razie definiujemy obiekt zastępczy, które możemy podać później.

Teraz, gdy znamy już klasy aplikacji bazy danych kompozytora, Następnym krokiem jest zdefiniowanie atrybutów i działań związanych z klasami. W ze złożonymi procesami, wymagalibyśmy od Ciebie pracę z ołówkiem i papierem, UML lub karty CRC lub OOD aby ułożyć hierarchię klas i sposób interakcji obiektów.

W bazie danych kompozytora definiujemy klasę Composer zawierającą które mają być przechowywane na poszczególnych kompozytorach. Zawiera też metody manipulowania rankingi i wyświetlanie danych.

Klasa bazy danych musi mieć jakąś strukturę do przechowywania obiektów Composer. Musimy mieć możliwość dodawania do struktury nowego obiektu Composer oraz pobierania konkretnego obiektu Composer. Chcielibyśmy też wyświetlać wszystkie obiekty w kolejności wpisania lub według rankingu.

Klasa interfejsu użytkownika implementuje interfejs oparty na menu, który zawiera moduły obsługi, dla działań wywołania w klasie Database.

Jeśli klasy są łatwe do zrozumienia, a ich atrybuty i działania są jasne, tak jak w aplikacji kompozytora, opracowanie klas jest stosunkowo łatwe. Jeśli jednak masz jakieś pytania dotyczące wzajemnych relacji i interakcji klas, najlepiej najpierw narysuj diagram, a potem dopiero zacznij kodować.

Gdy mamy już jasny obraz projektu i go ocenimy (więcej na ten temat wkrótce), definiujemy interfejs dla każdej klasy. Nie przejmujemy się implementacją czyli jakie są atrybuty, działania i elementy, klasy” stan i działania są dostępne dla innych klas.

W C++ zwykle robimy to, definiując plik nagłówkowy dla każdej klasy. Kompozytor klasa zawiera prywatne dane użytkowników obejmujące wszystkie dane, które chcemy przechowywać w narzędziu kompozycyjnym. Potrzebujemy akcesorów („get”) i mutatorów („set”) oraz metod głównych działań dla danej klasy.

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

Klasa Database jest też prosta.

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

Zwróć uwagę, że dane dotyczące kompozytora ujęliśmy w oddzielnym miejscu zajęcia. W klasie Database można umieścić strukturę lub klasę reprezentującą rekord Composer i dostęp do niej uzyskać bezpośrednio. Byłoby to jednak „under-objectification” (do obiektywności), czyli nie modelujemy z obiektami tak często, jak to tylko możliwe.

Za chwilę zaczniesz pracę nad implementacją narzędzi Composer i Database. , że znacznie wygodniejsze jest posiadanie osobnej klasy Composer. W szczególności oddzielne operacje niepodzielne na obiekcie Composer znacznie upraszczają implementację metod Display() w klasie Database.

Oczywiście istnieje też coś takiego jak „nadmierne obiektywizacja”, gdy próbujemy sklasyfikować wszystko lub mamy więcej klas niż potrzebujemy. Potrzeba aby znaleźć odpowiednią równowagę, mamy różne opinie.

Określenie, czy użytkownik nie zgadza się z nimi, czy nie jest zbyt obszerne, można często tworzenie diagramów w klasach. Jak już wspominaliśmy, ważne jest opracowanie zajęć. projektu, przed rozpoczęciem programowania. Pomoże to w analizie podejścia. Częstym w tym celu UML (Unified Modeling Language) Mamy już zdefiniowane klasy dla obiektów Composer i Database, interfejsu, który pozwala użytkownikowi na interakcję z bazą danych. Wystarczy proste menu:

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

Interfejs użytkownika można zaimplementować jako klasę lub program proceduralny. Nie wszystko w programie C++ musi być klasą. Jeśli przetwarzanie odbywa się sekwencyjnie, lub zadaniowa, tak jak w tym programie menu, można go stosować proceduralnie. Ważne jest, aby wdrożyć ją w taki sposób, by zawsze była obiektem zastępczym, Na przykład, jeśli chcemy w przyszłości utworzyć graficzny interfejs użytkownika, nie muszą wprowadzać żadnych zmian w systemie poza interfejsem użytkownika.

Ostatnim elementem, który musimy wypełnić, jest program do przetestowania zajęć. W klasie Composer potrzebny jest program main(), który pobiera dane wejściowe, wypełnia composer, a potem wyświetla go, by upewnić się, że klasa działa prawidłowo. Chcemy też wywoływać wszystkie metody klasy 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();
}

Potrzebujemy podobnego programu testowego dla klasy bazy danych.

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

Te proste programy testowe to dobry pierwszy krok, ale wymagają one aby ręcznie sprawdzić wynik, by upewnić się, że program działa prawidłowo. Jako gdy system staje się większy, ręczna kontrola danych wyjściowych gwałtownie staje się niepraktyczna. W następnej lekcji przedstawimy programy testów samokontroli w postaci testów jednostkowych.

Projekt naszego zgłoszenia jest już gotowy. Następnym krokiem jest wdrożenie pliki .cpp zajęć i interfejs.Na początek: Skopiuj, wklej w plikach plik .h i testuj kod sterownika, a następnie je skompiluj.Używaj jak sprawdzić swoje zajęcia. Następnie zaimplementuj ten interfejs:

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

Aby zaimplementować interfejs użytkownika, użyj metod zdefiniowanych w klasie Database. Dopilnuj, aby Twoje metody były odporne na błędy. Na przykład ranking powinien zawsze mieścić się w zakresie 1–10. Nie zezwalaj też nikomu na dodawanie 101 kompozytorów, chyba że planujesz zmienić strukturę danych w klasie Database.

Pamiętaj, że cały kod musi być zgodny z naszymi konwencjami kodowania, które zamieszczamy tutaj dla Twojej wygody:

  • Każdy program, który piszemy, zaczyna się komentarzem nagłówka zawierającym imię i nazwisko autora, jego dane kontaktowe, krótki opis oraz informacje o użyciu (jeśli to konieczne). Każda funkcja lub metoda zaczyna się od komentarza na temat operacji i użycia.
  • Komentarze objaśniające, używając pełnych zdań, za każdym razem, gdy kod nie dokumentować ani udokumentować, np. jeśli przetwarzanie jest trudne, nieoczywiste, interesujące lub ważne.
  • Zawsze używaj nazw opisowych: zmienne zawierają małe litery rozdzielone znakiem _ jak w zmiennej moja_zmienna. W nazwach funkcji i metod należy oznaczać wielkie litery na przykład w funkcji MyExcitingFunction(). Stałe zaczynają się od „k” oraz należy używać wielkich liter do oznaczania słów, na przykład kDaysInWeek.
  • Wcięcie składa się z wielokrotności dwóch wartości. Pierwszy poziom to 2 spacje. jeśli dalej wymagane jest wcięcie. Stosujemy cztery spacje, sześć spacji itp.

Witaj w prawdziwym świecie!

W tym module przedstawiamy 2 bardzo ważne narzędzia wykorzystywane w większości inżynierii oprogramowania organizacji non-profit. Pierwszy to narzędzie do tworzenia, a drugi to zarządzanie konfiguracją. systemu. Oba te narzędzia są niezbędne w przemysłowym inżynierii oprogramowania, gdzie wielu inżynierów często pracuje nad jednym dużym systemem. Te narzędzia pomagają koordynować i kontrolować zmiany w bazie kodu oraz zapewniają skuteczne kompilowanie i linkowanie systemu z wielu plików programowych i z plików nagłówkowych.

Makefilee

Proces budowania programu odbywa się zwykle za pomocą narzędzia do kompilacji, które kompiluje i linki do wymaganych plików we właściwej kolejności. Pliki w języku C++ często zawierają zależności, np. funkcja wywołana w jednym programie znajduje się w innym programu. Być może plik nagłówka jest potrzebny kilku różnym plikom cpp. O narzędzie do tworzenia kompilacji określa właściwą kolejność kompilacji na podstawie tych zależności. Będzie kompilować tylko te pliki, które zmieniły się od ostatniej kompilacji. Może to zaoszczędzić dużo czasu w systemach z setkami lub tysiącami plików.

Często używane jest narzędzie do kompilacji typu open source o nazwie make. Więcej informacji znajdziesz na stronie przez to, artykuł. Zobacz, czy możesz utworzyć graf zależności dla aplikacji Composer Database, a potem przełożyć ten plik na plik Makefile. Oto nasze rozwiązanie.

Systemy zarządzania konfiguracją

Drugim narzędziem stosowanym w inżynierii oprogramowania przemysłowego jest zarządzanie konfiguracją. (Menedżer Społeczności). Służy do zarządzania zmianą. Załóżmy, że Bob i Zuzanna są pisarzami na temat technologii Obaj pracują nad aktualizacją podręcznika technicznego. Podczas spotkania przypisuje każdej z nich sekcję tego samego dokumentu do aktualizacji.

Instrukcja obsługi jest przechowywana na komputerze, do którego mają dostęp zarówno Robert, jak i Zuzanna. Bez tego narzędzia i procesu mogłyby się pojawić różne problemy. Jeden Możliwe, że komputer, na którym jest przechowywany dokument, może zostać tak skonfigurowany, Robert i Zuzanna nie mogą jednocześnie pracować nad tym podręcznikiem. To spowolniłoby działanie znacznie je zmniejszyć.

Bardziej niebezpieczna sytuacja ma miejsce, gdy komputer z danymi zezwala na otwarcie dokumentu przez Jakuba i Suzanna w tym samym czasie. Oto, co może się wydarzyć:

  1. Robert otwiera dokument na komputerze i pracuje nad swoją sekcją.
  2. Zuzanna otwiera dokument na komputerze i pracuje nad swoją sekcją.
  3. Bob wprowadza zmiany i zapisuje dokument na komputerze z pamięcią masowej.
  4. Zuzanna kończy wprowadzanie zmian i zapisuje dokument na komputerze pamięci masowej.

Ta ilustracja pokazuje problem, który może wystąpić, jeśli nie ma żadnych elementów sterujących na pojedynczej kopii podręcznika technicznego. Gdy Susan zapisuje swoje zmiany, zastępuje te wprowadzone przez Boba.

Właśnie takie sytuacje są sterowane przez system Menedżera Społeczności. W systemie CM zarówno Bob, jak i Susan „wypożyczają” swoją kopię instrukcji technicznej i pracują nad nią. Kiedy Robert sprawdza zmiany, system wie, że Susan ma własną kopię, Kiedy Susan sprawdza w swojej kopii, system analizuje zmiany wprowadzone przez Boba i Zuzannę, po czym tworzy nową wersję, łączy oba zestawy zmian.

Systemy CM mają wiele funkcji poza zarządzaniem równoczesnymi zmianami, co opisano powyżej. Wiele systemów przechowuje archiwa wszystkich wersji dokumentu, od pierwszej od chwili jego utworzenia. W przypadku instrukcji technicznych może być ona bardzo przydatna, gdy użytkownik ma starszą wersję podręcznika i zadaje pytania autorowi związanemu z technologią. System CM pozwoliłby autorowi technicznemu uzyskać dostęp do starej wersji aby sprawdzić, co widzi użytkownik.

Systemy CM są szczególnie przydatne do kontrolowania zmian wprowadzanych w oprogramowaniu. Taka są nazywane systemami zarządzania konfiguracją oprogramowania (SCM). Jeśli rozważasz ogromnej liczby plików z kodem źródłowym w dużej inżynierii oprogramowania organizacji i ogromnej liczby inżynierów, którzy muszą wprowadzić w nich zmiany, system SCM ma kluczowe znaczenie.

Zarządzanie konfiguracją oprogramowania

Systemy SCM działają w oparciu o prostą koncepcję – ostateczne kopie plików są przechowywane w centralnym repozytorium. Ludzie przeglądają kopie plików z repozytorium, na tych kopiach, a kiedy będą gotowe. SCM systemy zarządzają wersjiami poszczególnych użytkowników i śledzą je w odniesieniu do jednego mastera ustawiony.

Wszystkie systemy SCM oferują te podstawowe funkcje:

  • Zarządzanie równoczesnością
  • Obsługa wersji
  • Synchronizacja

Przyjrzyjmy się dokładniej każdej z tych funkcji.

Zarządzanie równoczesnością

Równoczesność oznacza edycję pliku przez więcej niż 1 osobę. Przy dużym repozytorium chcemy, aby użytkownicy mogli to robić. do pewnych problemów.

Weź pod uwagę prosty przykład z zakresu inżynierii: załóżmy, że zezwalamy inżynierom na jednoczesną modyfikację tego samego pliku w centralnym repozytorium kodu źródłowego. Klienci1 i 2 muszą wprowadzić zmiany w pliku w tym samym czasie:

  1. Klient1 otwiera plik bar.cpp.
  2. Klient2 otwiera plik bar.cpp.
  3. Klient1 zmienia i zapisuje plik.
  4. Klient2 zmienia plik i zapisuje go, zastępując zmiany wprowadzone przez Klienta1.

Oczywiście tego nie chcemy. Nawet gdybyśmy kontrolowali sytuację zatrudnienie dwóch inżynierów do pracy na osobnych kopiach zamiast bezpośrednio nad masterem. (jak widać na ilustracji poniżej), kopie trzeba w jakiś sposób uzgodnić. Większość systemów SCM rozwiązuje ten problem, zezwalając wielu inżynierom na sprawdzanie pliku („synchronizowanie” lub „aktualizowanie”) i wprowadzanie zmian w razie potrzeby. Menedżer ds. Menedżera treści system uruchamia algorytmy, które scalają zmiany w miarę sprawdzania plików. („submit” lub „commit”) do repozytorium.

Te algorytmy mogą być proste (poproś programistów o rozwiązanie sprzecznych zmian) albo niezbyt proste (ustal, jak w inteligentny sposób połączyć sprzeczne zmiany i zapytaj inżyniera tylko wtedy, gdy system naprawdę się zawiesił.

Obsługa wersji

Obsługa wersji oznacza śledzenie wersji pliku, co umożliwia odtworzyć (lub przywrócić) poprzednią wersję pliku. Odbywa się to albo Tworząc kopię każdego pliku podczas sprawdzania w repozytorium, lub zapisując każdą zmianę wprowadzoną w pliku. W każdej chwili możemy użyć archiwum lub zmień informacje, by utworzyć poprzednią wersję. Systemy obsługi wersji mogą też tworzyć raporty w dziennikach z informacjami o tym, kto i kiedy sprawdza zmiany oraz co ze zmianami.

Synchronizacja

W niektórych systemach SCM poszczególne pliki są sprawdzane i wychodzące z repozytorium. Bardziej zaawansowane systemy umożliwiają wypożyczenie więcej niż jednego pliku naraz. Inżynierowie sprawdzają własną pełną kopię repozytorium (lub jego części) i pracują nad plikami w razie potrzeby. Następnie mogą zatwierdzić zmiany z powrotem w repozytorium głównym. okresowe, a także aktualizowanie własnych kopii, aby być na bieżąco ze zmianami. jakie zrobiły inne osoby. Ten proces nazywamy synchronizacją lub aktualizacją.

Subversion

Subversion (SVN) to system kontroli wersji typu open source. Ma wszystkie funkcje funkcji opisanych powyżej.

W przypadku wystąpienia konfliktów SVN stosuje prostą metodologię. Konflikt występuje, gdy co najmniej 2 inżynierowie wprowadzą różne zmiany w tym samym obszarze kodu źródłowego, a następnie obaj prześlą swoje zmiany. SVN informuje tylko inżynierów o tym, że , ale rozwiązanie go należy do inżynierów.

W trakcie tego kursu będziemy używać SVN, aby pomóc Ci zapoznać się do zarządzania konfiguracją. Takie systemy są bardzo popularne w branży.

Najpierw zainstaluj w systemie SVN. Aby uzyskać instrukcje, kliknij tutaj. Znajdź swój system operacyjny i pobierz odpowiedni plik binarny.

Określona terminologia SVN

  • Wersja: zmiana w pliku lub zestawie plików. Wersja to jedna „zrzut” w stale zmieniającym się projekcie.
  • Repozytorium: kopia główna, w której SVN przechowuje pełną historię wersji projektu. Każdy projekt ma 1 repozytorium.
  • Kopia robocza: kopia, w której inżynier wprowadza zmiany w projekcie. Może istnieć wiele kopii roboczych danego projektu, z których każda należy do innego inżyniera.
  • Wymeldowanie: aby poprosić o roboczą kopię z repozytorium. Robocza kopia oznacza stan projektu w momencie płatności.
  • Zatwierdź: służy do wysyłania zmian z kopii roboczej do centralnego repozytorium. Inna nazwa to meldowanie się lub przesyłanie.
  • Aktualizacja: Aby udostępnić adresy URL innych osób zmiany z repozytorium na kopię roboczą lub aby wskazać, czy w kopii roboczej są jakieś niezatwierdzone zmiany. To jest tak samo jak w przypadku synchronizacji, co opisaliśmy powyżej. Aktualizacja/synchronizacja zapewnia kopię roboczą zawierać aktualną kopię repozytorium.
  • Konflikt: sytuacja, w której 2 inżynierów próbuje wprowadzić zmiany w tym samym w obszarze pliku. SVN wskazuje konflikty, ale inżynierowie muszą je rozwiązać.
  • Komunikat logu: komentarz dołączany do wersji przy jej zatwierdzeniu. opisuje wprowadzone przez Ciebie zmiany. Log zawiera podsumowanie tego, co działo się w projekcie.

Teraz, gdy masz zainstalowany SVN, omówimy kilka podstawowych poleceń. pierwszą rzeczą, jaką musisz zrobić, jest skonfigurowanie repozytorium w określonym katalogu. Oto polecenia:

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

Polecenie import kopiuje zawartość katalogu mytree do z katalogu w repozytorium. Katalogowi można przyjrzeć się z poleceniem list.

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

Import nie spowoduje utworzenia kopii roboczej. Aby to zrobić, musisz skorzystać z pliku svn Checkout. Spowoduje to utworzenie roboczej kopii drzewa katalogów. Zacznijmy zrób to teraz:

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

Po utworzeniu kopii roboczej możesz wprowadzić zmiany w plikach i katalogach tam. Twoja kopia robocza nie różni się niczym od innych zbiorów plików i katalogów. – dodawać nowe, edytować, przenosić, a nawet usuwać całą roboczą kopię roboczą. Pamiętaj, że jeśli skopiujesz i przeniesiesz pliki w kopii roboczej, Ważne jest, by użyć kopii svn i przenoszenia svn zamiast poleceń systemu operacyjnego. Aby dodać nowy plik, użyj polecenia svn add, a potem usuń. i użyć polecenia svn delete. Jeśli chcesz edytować za pomocą edytora i zacznij edytować.

Istnieje kilka standardowych nazw katalogów często używanych z funkcją Subversion. „Pień” katalog to główna linia rozwoju Twojego projektu. „gałęzie”, katalog to dowolna wersja gałęzi, nad którą pracujesz.

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

Załóżmy, że wszystkie wymagane zmiany zostały wprowadzone w kopii roboczej które chcesz zsynchronizować z repozytorium. Jeśli pracuje wielu inżynierów, w tym obszarze repozytorium, pamiętaj o aktualizowaniu kopii roboczej. Możesz użyć polecenia svn status, aby wyświetlić wprowadzone zmiany. podjętych działań.

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

Pamiętaj, że w poleceniu stanu jest wiele flag do sterowania tymi danymi wyjściowymi. Jeśli chcesz wyświetlić konkretne zmiany w zmodyfikowanym pliku, użyj 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;
...

Aby zaktualizować kopię roboczą z repozytorium, użyj polecenia svn update.

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

tylko w jednym miejscu, w którym może wystąpić konflikt. W powyższych danych wyjściowych „U” wskazuje nie wprowadzono żadnych zmian w wersjach repozytorium tych plików i została zaktualizowana nie mieliśmy do czynienia. „G” oznacza, że nastąpiło scalenie. Wersja repozytorium miała została zmieniona, ale zmiany nie były sprzeczne z Twoimi. Litera „C” oznacza konflikt. Oznacza to, że zmiany z repozytorium nakładały się na Twoje, i musisz wybrać jeden z nich.

Dla każdego pliku, który jest w konflikcie, Subversion umieszcza 3 pliki w Twojej pracy kopiuj:

  • file.mine: to jest plik w postaci, w jakiej istniał w kopii roboczej przed zaktualizował(a) kopię roboczą.
  • file.rOLDREV: To jest plik pobrany z repozytorium przed wprowadzenie zmian.
  • file.rNEWREV: to bieżąca wersja w repozytorium.

Aby rozwiązać konflikt, możesz wykonać jedną z 3 czynności:

  • Przejrzyj pliki i scal je ręcznie.
  • Skopiuj jeden z plików tymczasowych utworzonych przez SVN, aby zastąpić wersję roboczą.
  • Uruchom polecenie svn restore, aby odrzucić wszystkie zmiany.

O rozwiązaniu konfliktu powiadamiasz o tym SVN, uruchamiając polecenie svn rozwiązaniem. Spowoduje to usunięcie 3 plików tymczasowych, a SVN przestanie wyświetlać plik w .

Ostatnią rzeczą, jaką musisz zrobić, jest zatwierdzenie ostatecznej wersji w repozytorium. Można to zrobić za pomocą polecenia svn commit. Po wprowadzeniu zmiany muszą być aby dostarczyć komunikat logu z opisem zmian. Ten komunikat logu został załączony do utworzonej wersji.

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

Znajdziesz tam o wiele więcej informacji o SVN i o tym, jak obsługuje on duże oprogramowanie w projektach technicznych. W internecie dostępne są obszerne materiały. Wystarczy wyszukać w Google hasło „Subversion”.

Aby przećwiczyć, utwórz repozytorium dla systemu bazy danych Composer i zaimportuj wszystkie pliki. Następnie wyświetl działającą kopię i wykonaj opisane polecenia. powyżej.

Pliki referencyjne

Poradnik online

Artykuł w Wikipedii na temat SVN

Subversion

Zastosowanie: badanie dotyczące anatomii

Zobacz eSkeletons na uniwersytecie Teksasu w Austin