Containers, templates og standard algoritmer i C++

Tags:    c++
<< < 123 > >>
Skrevet af Bruger #5688 @ 10.01.2005

Flere Containere



Indtil videre har jeg kun kort beskrevet vector, men STL har et stort antal containers, der hver især er velegnede til visse former for opgaver. Som jeg tidligere sagde, er vector velegnet til indsættelse, og fjernelse, af elementer i slutningen af containeren, samtidigt med at der leveres random access iterators. vector kan dog ikke så meget mere, og ydelsen er utroligt ringe hvis man vil indsætte elementer i midten af den. Der er for mange containers i C++ til at jeg kan beskrive dem alle, men jeg vil forsøge at dække et nyttigt subset.

Der er en række der er understøttet af (næsten) alle containere. Disse, og deres virkning for containeren c, er:

c.begin() -- returnerer en iterator til det første element i c.
c.end() -- returnerer en iterator til det sidste element i c.
c.size() -- returnerer antallet af elementer i c.
c.empty() -- returnerer true hvis c ikke har nogen elementer, ellers false.
c.clear() -- sletter alle elementer i c. Returnerer void.

list

list-typen er STL's pendant til traditionelle linkede lister. Sædvanligvis er list også selv implementeret via en linked list. En list's helt store styrke er dens evne til at indsætte elementer på et vilkårligt sted i containeren, en handling der kan implementeres som en O(1)-algoritme. list har kun bidirectional operators, så subscripting-operatoren ([]) virker ikke -- den kunne nok godt implementeres for en list, men algoritmen ville være O(n) eller værre. Dette betyder også at en list ikke kan sorteres via den sort()-funktion der er defineret i <algorithm>, derimod har en list sin egen sort()-metode:

Fold kodeboks ind/udKode 


list::sort() kan naturligvis også tage et prædikat som parameter, ganske som den almindelige sort(). list har også en medlemsfunktion ved navn splice. Denne kan bruges til at splejse lister sammen, og er, takket være list's typiske implementation som en linked list, ret hurtig. list::splice() kan bruges på tre forskellige måder. Alle funktionerne returnerer void, og gør alle iteratorer til den indsplejsede liste ugyldige:

Fold kodeboks ind/udKode 


Denne brugsmetode indsætter alle elementer fra listen l2 ind i listen l, før det punkt iteratoren it peger på. Det siger sig selv at it skal være en gyldig iterator til l. Du kan bruge l.end() som it hvis du bare vil tilføje elementerne fra l2 i slutningen af l. Bemærk at alle elementerne i l2 bliver flyttet over i l, og altså fjernet fra l2.

Fold kodeboks ind/udKode 


Dette fungerer som i Fig. 5.1, med den undtagelse at det eneste element der flyttes fra l2 til l, er det it2 refererer til. it2 skal være en gyldig iterator for l2.

Fold kodeboks ind/udKode 


Dette fungerer som i Fig. 5.3, med den undtagelse at alle elementer i intervallet [b;e] flyttes til l. Disse skal naturligvis være gyldige iteratorer til elementer i l2.

pair

pair er en simpel container der er at finde i <utility>. Den tillader en at gruppere to objekter sammen i ét objekt. Simpelt, men nyttigt. pair holder to objekter, der ikke nødvendigvis er af samme type, og disse kan tilgås via pairs first og second members. Der findes også en make_pair() funktion der let kan generere pair-objekter. Her er et fyldestgørende eksempel på hvordan pair kan bruges, og hvilke fordele klassens brug har:

Fold kodeboks ind/udKode 


Dette program tager en række parametre, mindst 2 og et lige antal, indeler disse parametre i "navn-værdi"-par, indsætter det i en vector (bemærk at der er mellemrum imellem vinkelparanteserne -- dette er nødvendigt, for at compileren ikke fortolker det som <<-operatoren), sorterer denne vector, og udskriver indholdet. Programmet bruger make_pair() til at oprette pair-objekterne, en funktion der tager to parametre, og returnerer et pair objekt indeholdende disse parametre. Jeg bruger atoi() for at konvertere et tal i en char*-string til et heltal, idet vores vector indeholder pair-objekter af typen pair<string, int>. C++'s standard library rummer ikke en standardfunktion der kan sammenligne to pair<string, int>-objekter, så jeg definerer min egen sort_elem()-funktion, der så benyttes som et funktionspointerparameter til sort(). Dette bevirker at elem_vec bliver sorteret efter faldende værdi for de enkelte elementer. Til sidst udskrives indholdet af elem_vec endnu engang. En typisk session med programmet kunne se således ud:

Fold kodeboks ind/udKode 


Programmet er ikke helt robust nok til andet end tutorial-brug, idet der ikke er fejlrapportering hvis et givent parameter ikke kan konverteres til et tal (i så fald returnerer atoi 0).

map

map er en meget nyttig container, der er at finde i <map>. Grundlæggende er det en associativ container, hvor man kan associere et objekt af en given type (key-typen), men et andet objekt af en evt. anden type (value-typen). I Fig. 5.4 kunne man have brugt en map<string, int>, frem for en vector<pair<string, int>. map sorterer automatisk elementerne (ved at sammenligne deres key-værdier), så derfor skal en sammenligningsfunktion være tilgængelig for den type der benyttes som key-type. Denne er naturligvis automatisk defineret for alle indbyggede typer, men gør det nødvendigt at specificere et prædikat der kan foretage sammenligningen hvis der benyttes brugerdefinerede typer, for hvilke sammenligningsoperatorerne ikke er definerede.

Fold kodeboks ind/udKode 


Denne linje definerer m som et objekt af typen map, med K som key-type, V som value-type, og et prædikat, cmp, af typen P. Hvis sammenligningsoperatorerne er definerede for typen K kan de prædikatrelaterede parametre udelades. Elementer indsættes i et map via funktionen map::insert(), der tager et parameter af typen pair<K, V>, hvor K og V er af samme type som de relevante key- og value-værdier i det map der indsættes i. make_pair kan passende bruges til at generere disse pair-objekter:

Fold kodeboks ind/udKode 


Her er keyobj og valobj to objekter af typerne hhv. K og V for map-objektet m. Bemærk at hvis m allerede indeholder et element hvis key-værdi er den samme som keyobj vil et nyt element ikke blive indsat. Der kan altså kun være én instans af en given key-værdi i et map. Dette er meget vigtigt at huske på. map::insert()'s returtype er pair<map<K, V>::iterator, bool>, hvor iteratoren peger på elementet, i det map-objekt der er blevet indsat et element, der har en key-værdi der er lig med keyobj, og bool-objektet er true hvis der blev indsat et nyt element (altså, hvis key-værdien ikke fandtes i map-objektet allerede), og false hvis der ikke blev indsat et nyt element.

map har også en find() funktion. map::find() tager et parameter, af samme type som key-værdien for containeren, og returnerer en iterator der refererer til elementet med denne key. Hvis et sådan element ikke findes, returneres map::end().

Bemærk, at når en map-iterator derefereres, fås et pair-objekt, hvor man så kan tilgå first og second.

map tillader brug af subscripting-operatoren ([]), hvor parametren er af samme type som det relevante maps key-værdi. Hvis et element med den givne key-værdi findes, returneres en reference til dette element (ganske som med arrays, eller en vector), hvis ikke, oprettes et nyt element, med den givne key-værdi, og en reference til dette nye element returneres. Eftersom subscripting-operatoren således kan ændre objektet, må det map den benyttes med, ikke være const. Den reference der fås ved brug af subscripting-operatoren, kan tildeles en værdi af samme type som map-objektets value-type.

Her er en omskrevet udgave af Fig. 5.4, der nu bruger map, og altid har værdier for "Athas" og "Bjarne."

Fold kodeboks ind/udKode 


Bemærk at outputtet er sorteret efter navn, og ikke tal, i modsætning til det oprindelige program. map sorterer efter sin key.

vector

Denne container er generelt den man skal bruge, med mindre man har eksplicitte krav den ikke kan leve op til (f.eks. indsættelse af mange elementer i midten af containeren). En vector er meget let at have med at gøre. Den har random access iterators, den er lineær og den har et meget lille overhead i forhold til et typisk array. Forskellen i run-time hastighed er så lille at den knapt kan måles, og en vector bruger kun nogle få bytes til at gemme information om sin egen længde. En vector styrer selv sit hukommelsesforbrug, og kan dynamisk ændre sin størrelse for at akkomodere for et større antal elementer. Dette betyder at en push_back pludselig kan tage længere tid end sædvanligt, mens vectoren allokerer hukommelsen og flytter rundt på data, og dette er ikke altid praktisk. vector har derfor to funktioner der kan kontrollere hukommelsesbrug, så man kan placere allokering på strategisk rigtige tidspunkter i programmet. Hvis v er en vector:

v.reserve(n) -- reallokerer v så den kan indeholde mindst (muligvis flere) n elementer. Hvis n er mindre end v.size() vil overskydende elementer ikke blive slettet, og v vil ikke antage en mindre størrelse. Det eneste denne funktion garanterer, er at v kan indeholde n elementer uden yderligere reallokering.
v.resize(n) -- Reallokerer v til at kunne indeholde n elementer. Bevarer de første n elementer i v, men sletter overskydende elementer hvis n er mindre end v.size(). Denne funktion gør alle iteratorer til c ugyldige. At bruge en ugyldig iterator er en meget alvorlig fejl.

Typiske Container-Funktioner



De sekventielle containere (list, vector og string) har en række funktioner der gør dem lettere at arbejde med. Disse, hvis c er en sekventiel container, er beskrevet er:

c.insert(it, t) -- denne funktion indsætter en kopi af objektet t, i containeren c, lige før placeringen refereret til af iteratoren it. Denne iterator skal naturligvis være gyldig for c. Denne funktion returnerer en iterator til det netop indsatte objekt.
c.insert(t, b, e) -- denne funktion indsætter sekvensen [b;e[ i containeren c, lige før den placering der refereres til af it. Denne funktion returnerer void.
c.erase(it) -- denne funktion sletter det element it refererer til. Denne funktion returnerer en iterator der refererer til elementet lige efter det der blev slettet.
c.erase(b, e) -- denne funktion sletter alle elementer i intervallet [b;e[, og returnerer en iterator der refererer til elementet lige efter sekvensen der blev slettet.
c.push_back(t) -- indsætter en kopi af objektet t i slutningen af containeren c. Returnerer void.
c.pop_back() -- sletter cs sidste element og returnerer void. Må ikke benyttes hvis c ikke indeholder elementer.
c.push_front(t) -- indsætter en kopi af objektet t i starten af containeren c. Returnerer void. Er ikke gyldig for string og vector.
c.pop_front() -- sletter cs første element og returnerer void. Må ikke benyttes hvis c ikke indeholder elementer. Er kun gyldig for list.

Konklusion



Det var det. Som jeg skrev i indledningen er dette på ingen måde en komplet guide. Faktisk mener jeg selv at den er meget ufærdig, men den burde indeholde den information der skal til for at man kan bruge containere i sine C++-programmer. Der er mange ting jeg ikke fik dækket, bl.a. flere detaljer om iteratorer, algoritmer, flere containerfunktioner, hvordan containere implementeres, flere forskellige containere, og mere. Dette vil muligvis blive dækket i en senere artikel. Få jer en bog som Accelerated C++ (http://www.acceleratedcpp.com/ -- min ene store inspiration til denne artikel) eller The C++ Programming Language (http://www.research.att.com/~bs/C++.html -- min anden store inspiration til denne artikel).

Templates, containers og generiske algoritmer styrer. Brug dem.



<< < 123 > >>

Hvad synes du om denne artikel? Giv din mening til kende ved at stemme via pilene til venstre og/eller lægge en kommentar herunder.

Del også gerne artiklen med dine Facebook venner:  

Kommentarer (10)

User
Bruger #4803 @ 10.01.05 22:48
meget lang.. eventuelt split den op i 2? 3? :S
User
Bruger #5688 @ 10.01.05 22:54
Den kan ikke splittes op uden at forstyrre sammenhængen. Det er et meget komplekst emne.
User
Bruger #6649 @ 10.01.05 22:57
En rigtig god artikel... den har fortjent en femmer
User
Bruger #479 @ 11.01.05 16:32
Flot artikel... du får en femmer herfra.

Jeg håber du senere får tid til at skrive lidt mere om templates.
User
Bruger #5779 @ 12.01.05 14:45
god artikel
User
Bruger #5688 @ 16.01.05 21:30
Efterfølgeren bliver ikke nær så stor, men den skal nok komme engang.
User
Bruger #3470 @ 23.01.05 15:00
God artikel, lærte godt nok ikke noget nyt, men håber at andre gjorde ;-)
Selvom jeg ikke lærte noget, så du får skam stadig 5 :-P

Jeg vil opfordre alle andre til at læse meget mere om dette emne, for der er rent faktisk stor ydelsesmæssig forskel på de forskellige containers. Det blev der ikke nævnt særligt meget om i denne artikel, så jeg synes at jeg burde fremhæve det her.
User
Bruger #601 @ 19.03.05 08:48

Dejligt med en artikel som bevæger sig ud over det rene begynder niveau. Eneste minus - som forfatteren ikke kan gøre noget ved - er at man ikke kan hente artiklen i fx. PDF format ;-).

Forsæt endelig det gode arbejde.
User
Bruger #13085 @ 09.01.08 08:40
Jeg er ny bruger

Udemærket artikel.

Jeg har kun skimmet den, da jeg har nogle ret velskrevne, uddybende refs til STL/templates på engelsk. Jeg vil give artiklen 4. Fordi: Indholdet er solidt, men strukturen er lidt forvirrende, og den er måske mere forvirrende for folk, der intet kender til emnet.

Jeg vil som nogle andre skrev ovenfor også klart mene, at det ville være fedt, hvis forfatteren skrev videre på artiklen. MEN Det BEHØVER vel ikke egentlig ikke gøres af den samme person? Måske kunne NOGEN føre artiklen videre, hvis forfatteren ikke har tid. Rigtig "open source"-stil!

Jeg har surfet lidt rundt her på siden og synes, at der mangler en rigtig "børnehave"-C++ tutorial.

Noget kunne jo "for skæg" sende et "manuskript" ind til censur og se, hvad "redaktøren" siger?

Personligt ville jeg sagtens kunne skrive en "lille" tekst, som indtroducerer sproget C/C++ på en MEGET, MEGET pædagogisk måde, så de sidste 10%, der er bange for programmering kan komme med.

Idé?

User
Bruger #8985 @ 20.04.09 23:59
Nicolai Lystner Fersner: Hvad er forskellen på ydelse mellem de forskellige containere? Hvilke containere snakker vi om? God kommentar, lærte godt nok ikke noget nyt, og tvivler andre gjorde.

Stefan E. Nielsen: Du kan jo læse lidt af artiklen, holde pause, arbejde med det, du har lært, og så læse videre. På den måde vil artiklen ikke virke så lang.

Christian Dyrnesli: Hvad holder dig tilbage? Det virker som en god idé, synes jeg.

Troels Henriksen: God artikel! Jeg har arbejdet med templates før, men du har vist mig nogle metoder at anvende dem på, jeg end ikke havde overvejet.
Du skal være logget ind for at skrive en kommentar.
t