Writing in C or C++ is like running a chain saw with all the safety guards removed
Do ukończenia tego laboratorium wymagana jest znajomość wskaźników i referencji z C. Jeżeli ktoś nie czuje się w tym temacie na siłach, w laboratorium Wskaźniki i referencje przygotowane zostało krótkie przypomnienie najważniejszych informacji.
Refaktoryzacja polega na ulepszeniu kodu bez zmiany jego zachowania. Np. zmiany nazwy funkcji w celu wyjaśnienia jej działania (zamist bool pal(std::string s) lepszą nazwą będzie bool IsPalindrome(std::string single_word_phrase)) Refaktoryzacje mogą też przyspieszyć pisanie kodu, początkowo kod można napisać w postaci jednej dłużej metody a następnie powykrawać z niego podprocedury w celu poprawy jego czytelności i modułowości
void ScheduleAppointment(const std::string &customer, const std::string &room, const std::string &when) { std::regex valid_customer {R"(\w+ \w+)"}; if ( !std::regex_match(customer,valid_customer) ) { return; } std::regex valid_room {R"([A-Z]-\d{3})"}; if ( !std::regex_match(room,valid_room)) { return; } std::regex valid_when {R"(\d{2}:\d{2})"}; if ( !std::regex_match(when,valid_when)) { return; } Room *found_room = nullptr; for (auto &r : rooms) { if (r.Name() == room) { found_room = &r; } } ... }
po refaktoryzacji kod mógłby wyglądać następująco:
bool IsValid(const std::string &str, const std::regex &validator) { return std::regex_match(str, validator); } ... bool IsValidCustomer(const std::string &customer) { std::regex valid_customer {R"(\w+ \w+)"}; return IsValid(customer,valid_customer); } Room FindRoom(const std::string &room) { for (auto &r : rooms) { if (r.Name() == room) { return r; } } return {}; } void ScheduleAppointment(const std::string &customer, const std::string &room, const std::string &when) { if ( !IsValidCustomer(customer) ) { return; } if ( !IsValidRoom(room)) { return; } if ( !IsValidWhen(when)) { return; } Room found_room = FindRoom(room); ... }
Zapoznać się z możliwościami automatycznej refaktoryzacji pod linkiem do 8 min 30 s.
Często zdarza się, że nie jesteśmy w stanie z góry przewidzieć ile pamięci będzie potrzebował nasz program do poprawnego działania. Na przykład w prostym kalkulatorze macierzowym nie można z góry ustalić wymiarów tablicy przechowującej macierz ani z góry ustalić ile takich tablic-macierzy będzie występować w równaniu!
Jedynym rozwiązaniem jest tworzenie nowych macierzy o ustalonych wymiarach podczas działania programu.
Wraz z dynamiczną alokacją pamięci idą w parze wskaźniki. Pamięć zawsze alokowana jest pod jakimś adresem. A zmienne przechowujące adres i pozwalające się dostać do danych w niej przechowywanych, to właśnie wskaźniki.
Operator new jest ekwiwalentem znanej z C funkcji malloc ale dużo potężniejszym. Wynikiem zwracanym przez operator new jest wskaźnik do zaalokowanej pamięci
Przykład dynamicznego tworzenia zmiennych:
#include <iostream> using namespace std; int main(void){ char* ptr = new char; //Stworzy zmienną typu char int* tab = new int[10]; //Stworzy tablicę typu int o rozmiarze 10 // NIEPOPRAWNIE!!! int** tab2d = new int[10][10]; }
Operator delete działa podobnie jak funkcja free znana z C. Pamięć w momencie kiedy nie jest już wykorzystywana przez program powinna być zwolniona. Przykład dynamicznego tworzenia i usuwania zmiennych:
#include <iostream> using namespace std; int main(void){ char* ptr = new char; //Stworzy zmienną typu char int* tab = new int[10]; //Stworzy tablicę typu int o rozmiarze 10 // Tutaj znajdują się operacje na zmiennych delete ptr; delete [] tab; // NIEPOPRAWNIE!! // Jeśli tab2d jest poprawnie zaalokowaną tablica dwuwymiarową // nie można zwolnić pamięci w następujący sposób delete [][] tab2d; }
Niestety należy pamiętać o tym by zwolnić pamięć przydzieloną inaczej uzyskujemy wyciek pamięci i program w trakcie działania zaczyna zużywać coraz więcej pamięci operacyjnej mimo, że już jej i tak nigdy nie użyje! Sprawa też nie jest taka prosta, bo nie zawsze wiadomo, czy pewne elementy nie są już na pewno przez nikogo używane.
W C++11 nie używamy już makra NULL zostało dodane specjalne słowo kluczowe reprezentujące pusty wskaźnik nullptr
int *v = nullptr; delete v;
Jedną z możliwości automatycznego zarządzania pamięcią jest oddelegowanie tego zadania do kontenerów zdefiniowanych w bibliotece standardowej, które automatycznie zwolnią pamięć jeśli przestanie ona być używana.
Jednym z takich kontenerów jest tablica rozszerzalna vector, zapoznać się z dokumentacją uruchomić przykłady podane w dokumentacji przez run code i przyglądnąć się dokumentacji funkcji:
Przenalizować poniższy kod:
#include <iostream> #include <vector> using std::cout; using std::endl; using std::vector; void PrintVector(const vector<int> &v) { bool first = true; for(auto n : v) { if (!first) { cout<< ", "; } else { first = false; } cout << n; } cout<<endl; } int main() { // Create a vector containing integers vector<int> v = {7, 5, 16, 8}; cout << "initilized with 4 numbers"<<endl; PrintVector(v); // Add two more integers to vector v.emplace_back(25); v.emplace_back(13); cout << "after 2 more inserted"<<endl; PrintVector(v); //copy of the value inside vector for (auto n : v) { ++n; } cout << "BUT: after incrementation"<<endl; PrintVector(v); //reference to the value inside vector, therefore one can change the value for (auto &n : v) { ++n; } cout << "and again... can you spot the difference?"<<endl; PrintVector(v); }
Tablica asocjacyjna, która pozwala zaadresować dowolny element innym typem, niekoniecznie tylko liczbą naturalną.
#include <iostream> #include <map> using std::cout; using std::endl; using std::map; void PrintMap(const map<char,int> &v) { bool first = true; for(const auto &n : v) { if (!first) { cout<< ", "; } else { first = false; } cout << n.first << " -> " << n.second; } cout<<endl; } int main() { // Create a vector containing integers map<char, int> v = {{'a',17}, {'c',-2}, {'g',67}, {'z',13}}; cout << "initilized with 4 numbers"<<endl; PrintMap(v); // Add two more integers to vector v.emplace('h',25); v.emplace('$',13); cout << "after 2 more inserted"<<endl; PrintMap(v); //copy of the value inside vector for (const auto &n : v) { //Compilation error //++n.second; } cout << "BUT: after incrementation"<<endl; PrintMap(v); //reference to the value inside vector, therefore one can change the value for (auto &n : v) { ++n.second; } cout << "and again... can you spot the difference?"<<endl; PrintMap(v); }
Operacje na plikach wykonywane są w C++ przy użyciu strumieni - analogicznie jak operacja czytania z klawiatury i wypisywania na ekran.
Należy pamiętać o tym, że każdy plik, który został otwarty, powinien zostać również zamknięty, kiedy korzystanie z niego nie jest już potrzebne.
Obiekty strumieni służące do odczytu i zapisu do pliku znajdują się w pliku nagłówkowym <fstream>. Biblioteka ta zawiera dwa obiekty strumieni analogiczne do obiektów cin i cout:
Przykład wczytywania pliku wyraz po wyrazie:
#include <iostream> #include <fstream> using namespace std; int main(void){ ifstream myfile("file.txt"); char word[64]; if(!myfile) cout << "Nie można otworzyć pliku!" << endl; while(myfile >> word) cout << word; myfile.close(); return 0; }
Przykład wczytywania pliku linia po linii:
#include <iostream> #include <fstream> using namespace std; int main(void){ ifstream myfile("file.txt"); char line[256]; if(!myfile) cout << "Nie można otworzyć pliku!" << endl; while(!myfile.eof()){ myfile.getline(line,256); cout << line; } myfile.close(); return 0; }
Przykład dopisywania do pliku:
#include <iostream> #include <fstream> using namespace std; int main () { // Aby zrozumieć zapis: ios_base::in | ios_base::app // Zobacz kolejny podrozdział ofstream myfile ("file.txt", ios_base::in | ios_base::app); if(!myfile) cout << "Nie można otworzyć pliku!" << endl; myfile << "Tekst zapisany w pliku" << endl; myfile.close(); return 0; }
Plik można otworzyć w 6 różnych trybach; kombinacje trybów też są możliwe.
Wartość flagi | Znaczenie |
---|---|
ios_base::app | (append) Ustawia wskaźnik pozycji strumienia na samym końcu strumienia. Służy do dopisywania do pliku. |
ios_base::ate | (at end) Ustawia wskaźnik pozycji strumienia na samym końcu pliku. |
ios_base::binary | (binary) Traktuje strumień jako strumień binarny a nie tekstowy |
ios_base::in | (input) Umożliwia zapis do pliku |
ios_base::out | (output) Umożliwia odczyt z liku |
ios_base::trunc | (truncate) Jeśli otwierany pli posiadał jakąś zawartość, jest ona pomijana i wskaźnik pozycji w strumieniu jest ustawiany na początku pliku. |
Otwieranie strumienia ifstream z opcją ios_base::out mija się z celem. Do strumieni, które mają służyć zarówno zapisowi jak i odczytowi, stosuje się ogólną postać strumienia o nazwie fstream.
fstream file("file.txt", ios_base::in | ios_base::out | ios_base:app);
int **Array2D(int n_rows, int n_columns); void DeleteArray2D(int **array, int n_rows, int n_columns);
./program plik-tekst.txt plik-szyfr.txt 1
std::string PolybiusCrypt(std::string message); std::string PolybiusDecrypt(std::string crypted);
#include <string>
ForwardList *CreateNode(int value); void DestroyList(ForwardList *list);
ForwardList *PushFront(ForwardList *list, int value); void Append(ForwardList *list, ForwardList *tail);
int GreatestProduct(const std::vector<int> &numbers, int k);
#include <vector>
//po obiekcie vector można iterować w pętli for for (int v : numbers) { //zmienna v przyjuje kolejne wartości elementów zawartych w tablicy } std::vector<int> another_vector; //utworzenie nowej tablicy rozszerzalnej another_vector.push_back(17); //wstawienie na sam koniec nowego elementu (pamięć jest przydzielana automatycznie!) if (anotehr_vector.size() > 0) //vector można odpytać o wielkość
std::string XorCypherBreaker(const std::vector<char> &cryptogram, int key_length, const std::vector<string> &dictionary);
#include <string> #include <vector>
#include <algorithm> using std::find; using std::vector; using std::string; vector<string> dictionary {"the","of"}; //THERE ARE BETTER WAYS TO LOOK FOR A WORD BUT IT WORKS if (find(dictionary.begin(),dictionary.end(),"of") != dictionary.end()) { //FOUND! }