Föreläsning 3: Vector och struct Nu är det dags att börja prata lite datastrukturer i c++. Vi börjar med fält. Vill man ha ett klassiskt fält så kan man i sitt c++-program deklarera t.e.x int tio_heltal[] = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10; eller int fem_heltal[5]; for (int i = 0; i < 5; ++i) fem_heltal[i] = i Dessa fält är i grund och botten bara pekare, så det finns t.ex. inga kontroller (så som i Ada) på om man går utanför. Det skulle t.ex. vara helt ok att göra, så länge det minnesutrymme som man råka peka ut inte ligger utanför det minne som tillhör programmet: cout << fem_heltal[10]; En char* (c-sträng) är ett typexempel på ett sådant fält. Jag går nog inte in mer på sådana fält nu eftersom vi i c++ har en riktigt juste implementation på ett dynamiskt fält, nämligen vector. En vector är ett fält som kan växa och krympa, vilket är praktiskt. Det liknar ganska mycket den enkellänkade listan som ni redan har gjort. Vi tar lite exempel på vad man kan göra: #include <iostream> #include <vector> vector<int> mitt_falt; mitt_falt.push_back(5); mitt_falt.push_back(10); mitt_falt.push_back(15); Jag måste ta med biblioteket för vector. Jag deklarerar att mitt_falt är en vektor som består av heltal. <- hakarna finns där för att tala om att vector är generisk, man kan lagra vilken datatyp man vill i en vector. Men precis som med vanliga fält måste det vara samma datatyp på varje plats. Jag stoppar sen in en femma, en tia och femton med funktionen push_back, som lägger på på slutet av vektorn. mitt_falt: 5 10 15 0 1 2 Precis som ett vanligt fät har vectorn index på varje plats. Indexeringen börjar från 0 i c++. Om jag nu t.ex. vill skriva ut fältet så kan jag göra följande: cout << mitt_falt.at(0) << mitt_falt.at(1) << mitt_falt.at(2) << endl;
Då får vi ut: 51015 Vi gör nu detta lite generellt och snyggt med ett underprogram: vector<int> mitt_falt; for (int i = 5; i < 20; i+=5) mitt_falt.push_back(i); print(mitt_falt); void print(const vector<int>& falt) for (int i = 0; i < falt.size(); ++i) cout << falt.at(i) << endl; Vi kan alltså komma åt elementen med funktionen at! Om vi vill kan vi använda []-parenteserna här också, men at rekommenderas eftersom den kontrollerar att elementet faktiskt finns. Försöker man gå utanför kommer man få undantaget out_of_range. Ett annat sätt att fylla en vektor med värden är att använda initiering: vector<int> annat_falt1, 2, 3, 4, 5; Ett annat sätt att loopa över talen i vectorn är att använda den intervallbaserade for-loopen: for (int tal : annat_falt) cout << tal << endl; Här kommer tal anta varje värde från annat_falt tills alla talen har gåtts igenom. Om man vill ändra på talen medan mar går igenom så kan man använda sig av referenser här: for (int& tal : annat_falt) ++tal; print(annat_falt); // vi får ut 2 3 4 5 Vi kan jämföra två vector med operatorn "<", då blir det precis som i Ada att man jämför element för element från vänster (tänk bokstavsordning). Detta fungerar så länge typen som ligger i fältet går att jämföra med "<". Det finns många bra inbyggda funktioner som jobbar med vektorer! T.ex: annat_falt.clear(); //rensar hela fältet.
Poster Nu går vi över till poster. I c++ (och många andra språk) heter post struct. Om vi t.ex. vill representera en bok kan vi skapa datatypen Book: struct Book string title; string author; int pages; ; Här får man inte glömma det där semi-kolonet efter struct-deklarationen! Nu kan vi skapa hur många Bookvariabler vi vill! Book b; b.title = "Ada 95 för nybörjare och erfarna"; b.author = "Torbjörn Jonsson"; b.pages = "331"; return 0; Vi kan även här använda initiering med om vi vill: Book primer "C++ primer", "Lippman", 885; Då fyller vi på delarna av posten i ordningen som de deklarerades. Om vi inte fyller i hela listan med värden så får de delar som inte fick något värde bara default värden (tom sträng, 0 o.s.v.): Book b2 "Professional C++"; Vi kan självklart göra tilldelning av hela structar: b2 = b; Om vi skall skicka structar som parameter så bör vi använda const-referens, annars blir det mycket onödig kopiering. Att komma åt delar av posten ser exakt lika dant ut som i Ada.: i main: my_print(b); void my_print(const Book& book) cout << "Title: " << book.title << endl << "Author: " << book.author << endl << book.pages << " pages" << endl; Vi kan ju passa på och göra en inläsningsrutin samtidigt:
void my_input(book& book) cout << "Input book: "; cin >> book.title >> book.author >> book.pages; Självklart kan man kombinera vector och struct på vilka sätt man känner för! Just detta skall ni göra på laborationen! Vi kan också passa på att skapa oss en operator som jämför två böcker. Man kanske vill ha en hel bokhylla, då vill man självklart jämföra först på författare och sedan på titel. bool operator<(const Book& left, const Book& right) if (left.author == right.author) return left.title < right.author; return left.author < right.author; Egna Headerfiler Nu har vi gjort många bra funktioner för vår datatyp Book. Detta kan säkert komma till nytta i fler program, så vi skulle gärna vilja lägga detta på en egen fil så att vi kan inkludera det på andra ställen senare. Vi gör då en egen headerfil som vi lägger vår kod i. Som ett första steg skulle vi kunna flytta ut det som har med Book att göra till en.h-fil, t.ex. book_handling.h. Vi måste då komma ihåg att lägga till de bibliotek som behövs där: #include <iostream> struct Book string title; string author; int pages; ; void my_print(const Book& book) cout << "Title: " << book.title << endl << "Author: " << book.author << endl << book.pages << " pages" << endl; void my_input(book& book) cout << "Input book: "; cin >> book.title >> book.author >> book.pages;
bool operator<(const Book& left, const Book& right) if (left.author == right.author) return left.title < right.author; return left.author < right.author; Huvudprogrammet blir då ganska litet: #include <iostream> #include "book_handling.h" Book b; cout << "Mata in en bok:"; my_input(b); my_print(b); Observera att vi använder "-tecken för att inkludera headerfiler som vi gjort själva. Gör vi på detta sätt får vi dock ingen uppdelning mellan specifikation och implementation (som i Ada). Man skall också komma ihåg att om man gör "" så gäller ju det tillsvidare eftersom koden från book_handling.h i princip "klipps in" ovanför main. Detta kanske inte gör så mycket, om man inte vill jobba med fler namnrymder smatidigt. Om vi nu skulle dela upp detta ytterligare så skulle vi kunna flytta själva implementationerna till en implementationsfil, t.ex. book_handling.cpp. Då blir book_handling.h bara detta: #include <string> struct Book std::string title; std::string author; int pages; ; void my_print(const Book& b); bool operator<(const Book& left, const Book& right); void my_input(book& b); Och book_handling.cpp:
#include <iostream> #include "book_handling.h" void my_print(const Book& b) cout << "Titel: " << b.title << endl << "Författare: " << b.author << endl << "Sidor: " << b.pages << endl; bool operator<(const Book& left, const Book& right) if (lhs.author == rhs.author) return lhs.title < rhs.title; return lhs.author < rhs.author; void my_input(book& b) getline(cin, b.title); getline(cin, b.author); cin >> b.pages; cin.ignore(1000, '\n'); Observera att vi i implmenentationsfilen måste inkludera specifikationsfilen! Att vi väljer att kalla dessa.h och.cpp för samma sak nu är bara en behändighetsgrej, kompilatorn kommer inte göra någon koppling mellan filer bara för att de heter lika (som gnatmake gör). Nej, för att få detta ihoplänkat rätt så måste vi vara mycket specifika. När vi nu kompilerar skriver vi då: g++ std=c++11 main.cpp book_handling.cpp Eftersom vi inte använde iostream i h-filen längre så tog jag bort den. Det enda som egentligen behövdes var string, så den tar vi med. Istället för att köra using kan vi då helt enkelt skriva strings fulla namn, d.v.s. std::string. I implementationsfilen är det behändigt att använda using eftersom vi där använder mycket cout, cin och endl. I vissa lägen kan man råka ut för tråkigheter när man gör #include. Det kan hända att saker inkluderas flera gånger (om man t.ex. har tre filer A, B, C där A inkluderar B och C, och B också inkluderar C). Då kommer man att få "multipla deklarationer". Det man kan göra då är att lägga till ett preprocessordirektiv som ser till att koden bara inkluderas en gång. Längst upp i vår h-fil lägger vi då (i vårt fall): #ifndef BOOK_HANDLING_H #define BOOK_HANDLING_H och längst ner i den filen: #endif Detta kallas för en "inkluderingsgard" och betyder helt enkelt, "om följande symbol inte redan finns: definiera den och lägg till allt det här"