*:85/ID200V C++ HT07 Föreläsning 4 Tillägg till C-syntaxen Några småsaker Resten-av-raden -kommentarer // (som i Java) Datatypen bool med värdena false och true, implicit kompatibel med int (d.v.s. int-värden kan fortfarande användas som boolska värden men det är bekvämare och tydligare med bool) Deklarationssatser, d.v.s. deklarationer tillåtna bland satser (som i Java) Deklaration av styrvariabel i for-satsens initieringsuttryck (som i Java) Deklaration av styrvariabel (?) i ett if- eller while-villkor: if (char *ptr = strchr(line, \n )) *ptr = \0 ; else while (getchar()!= \n ) ; Deklarationsvidden för en sådan variabel är hela if-satsen inkl. else-grenen. Tyvärr får en sådan deklaration inte ingå i ett mer komplext uttryck Bild 47 1
Referenser En referens i C++ är ett nytt namn på ett utrymme. Först syntax, sedan användnig Referens deklareras med modifieraren & efter typen: int i = 135; int& iref = i; iref är nu ett annat namn på samma utrymme som variabeln i har. Referensnamnet kan användas på precis samma sätt som det ursprungliga namnet: iref = 177; samma effekt som i = 177; iref++; samma effekt som i++; int *ipek = &iref; samma effekt som int *ipek = &i; Det finns inget sätt att ändra en referens att referera till annat utrymme, en referens är knutet till samma utrymme under hela sin livstid (precis som den ursprungliga variabeln). En referens måste alltså knutas till utrymme genom initiering vid sin definition (eller snarare vid sin initiering - om referensen definieras som medlem i en klass eller struct kan den inte initieras vid definitionen utan i konstruktorns initieringslista) Bild 48 Obs skillnaden mot adresstagningsoperatorn! Obs att denna användning av &-tecknet är helt ny och har ingenting med adresstagningsoperatorn & att göra! Adresstagningsoperatorn är en operator, den skrivs framför en variabel (e.d.) i ett uttryck för att få fram adressen till variabeln: int i = 57; int* ipek; ipek = &i; addresstagning i ett uttryck Typmodifieraren & skrivs efter typnamnet i en deklaration och innebär att man deklarerar ett nytt namn för ett existerande utrymme: int& iref = i; typmodifierare i en deklaration Bild 49 2
const-referenser En vanlig referens måste knutas till ett redan namngivet utrymme, högerledet i initieringen måste alltså vara ett lvalue (representera ett utrymme): int i = 150; char *pek = NULL; int arr[4]; int& refi = i; ok char *& pekref = pek; ok int& ar3ref = arr[3]; ok int& felref = 13; fel!!! Eftersom referenser används väldigt mycket för argumentöverföring till funktioner ansågs denna begränsning för restriktiv. Man har därför infört att const-referenser (som alltså refererar till utrymmen vilkas innehåll inte kan förändras) får initieras med värden: const int& ref = 13; ok Vid en sådan initiering skapas ett anonymt utrymme där värdet lagras och referensen blir namnet på detta utrymme. Bild 50 Referenser, huvudsaklig användning Formella argument till funktioner som skall kunna ändra sina aktuella parametrar void swap(int& x, int& y){ int z = x; x=y; y=z; Precis som Pascals VAR-parametrar void funk(){ int a = 5, b = 7; swap(a, b); Formella argument till funktioner, där argumenten är större än en int eller en pekare (för att undvika onödig kopiering av objekten, om argumenten inte skall förändras deklareras de const). Det är alltså det rekommenderade sättet att skicka argument i C++! bool check_name(const Person& p, const string& name){ return p.get_name() == name; Bild 51 3
Referenser, huvudsaklig användning, forts. Returtypen för funktioner (eller operatorer) som skall returnera ett lvalue : class Text{ int siz; char *cptr; char& operator[](int index){ return cptr[index]; ; lvalue - en tilldelningsbar storhet void funk(){ Text namn( Jozef ); namn[2] = s ; Bild 52 Dynamisk minnesallokering För dynamisk minnesallokering resp. avallokering används operatorerna new och delete, båda i två varianter - för enskilda objekt eller arrayer: Person *perspek=new Person( Jozef, 13); Person *folk=new Person[5]; (ovan anropas default-kontruktorn för vart och ett av de 5 objekten) char *str=new char[1024]; Motsvarande avallokeringar: delete perspek; delete [] folk; delete [] str; Vid new allokeras utrymme och konstruktorn anropas. Vid delete anropas destruktorn och utrymmet avallokeras. Det finns ingen motsvarighet till realloc (men bibliotekets vektor<> ger ju en dynamisk arrayliknande samling och bibliotekets string är dynamisk, så det finns egentligen inget behov). Bild 53 4
Dynamisk minnesallokering, forts. Om det saknas utrymme på heapen vid new generas undantaget bad_alloc som kan fångas upp : try{ char *pek = new char[1024]; catch(std::bad_alloc&){ std::cerr << Out of memory, canceled!\n ; Man kan även installera en egen funktion som då anropas istället: void out_of_memory(){ std::cerr << Out of memory, program exits!\n ; std::exit(13); int main(){ std::set_new_handler(out_of_memory); char *pek = new char[1024]; Den installerade funktionen anropas i en loop tills allokeringen lyckats. Funktion borde alltså antingen friställa utrymme så att allokeringen lyckas eller terminera programmet. Bild 54 Dynamisk minnesallokering, forts. malloc, calloc, realloc och free kan användas men man måste vara konsekvent : allokerat med malloc, calloc eller realloc - avallokera med free, allokerat med new -avallokeramed delete Obs att X-alloc-funktionerna inte anropar konstruktorer och att free inte anropar destruktorer! new och delete kan definieras om (överlagras), t.ex. för att allokera resp. återlämna utrymme någon annanstans än på heapen (exempelvis i en på förhand allokerad pool av utrymmen). Detta gör att man t.ex. kan skapa en automatisk garbage collector för sina program. Överlagringen av new och delete kan göras globalt eller för enskilda klasser. Det finns även varianter av new där man kan begära t.ex. placering av objekten på angivna adresser i minnet o.s.v. Intressant som allt detta är faller det utanför ramen för denna kurs. Bild 55 5
Funktionsöverlagring Man kan ju ha flera funktioner med samma namn men olika argumentlistor. Samtidigt behöver inte aktuella argumentvärden vid ett funktionsanrop vara av exakt samma typ som formella argument, bara det går att omvandla med implicit konvertering, t.ex. void funk(double d); void gunk(){ int i=13; funk(i); // Fungerar som funk((double)i); Om det nu hade funnits flera funktioner funk() med möjlighet till implicit konvertering, t.ex. void funk(char c); void funk(long l); osv så måste det finnas regler för vilken funktion som väljs ut för anrop. Dessa regler följer på nästa bild. De kompletteras dessutom med följande: returtypen beaktas inte vid överlagring funktioner från olika deklarationsområden (scope) utom namnrymder överlagrar inte varandra, men kan hämtas in genom using-deklarationer för medlemsfunktioner: const-medlem kan överlagra icke-const-medlem Bild 56 Function overloading resolution Identifiering av funktioner och funktionsmallar som kan komma ifråga. Av funktionsmallar väljs den mest specialiserade. Rangordning av kandidater: Exakt överensstämmelse eller triviala konverteringar (array till pekare, funktion till pekare, icke-const till const ) Befordringskonverteringar (bool till int, char till int, float till double osv.) Standardkonverteringar (int till double, double till int, Subklass* till Basklass*, TYP* till void* osv.) Användardefinierade konverteringar (genom konstruktorer och konverteringsfunktioner) Variabla argumentlistor, dvs. sist i argumentlistan Bäst-passande funktion: bäst för minst ett argument, ej sämre än någon annan för övriga. Om flera funktioner passar lika bra är det fel (tvetydighet, ambiguity), måste lösas genom explicit konvertering av argumenten vid anrop - d.v.s. av tillämpningsprogrammeraren ( entydiggörande, disambiguering?). Om ingen funktion passar är det givetvis också fel. Kontroll om anrop av den valda funktionen är korrekt (synlighet, returtyp) Bild 57 6
Defaultargument Om en funktion (eller medlemsfunktion, eller konstruktor) har argument för vilka det finns ett meningsfullt default-värde kan detta värde anges vid deklarationen av funktionen och behöver då inte anges vid anrop: void skriv_tecken(int antal, char tecken= * ){ for(int i=0; i<antal; i++) cout << tecken; cout << endl; int main(){ skriv_tecken(10,? ); // Skriver tio? skriv_tecken(10); // Skriver tio * Argumenten med defaultvärden måste stå sist i argumentlistan. Defaultvärdet får bara anges en gång, om en funktion deklareras på ett ställe och definieras på ett annat så anges det antingen i deklarationen eller i definitionen. Obs att det inte får finnas en överlagrad funktion som gör att anropet blir tvetydigt, om det t.ex. finns void skriv_tecken(int antal) {.. så vet inte kompilatorn vilken av funktionerna som anropas vid anropet skriv_tecken(30); En konstruktor med endast argument med defaultvärden blir defaultkonstruktorn. Bild 58 Infogade (inline) funktioner Med nyckelordet inline framför en funktionsdefinition eller deklaration anger man för kompilatorn att man vill att maskininstruktionerna för funktionen skall kopieras in i objektkoden på varje ställe där anrop sker, istället för att lagras på en plats och skötas med subrutinanrop. Detta görs för mycket små funktioner, oftast funktioner som bara anropar en annan funktion, t.ex. Klocka operator+(const Klocka& k, int min){ Klocka(k.get_tot_min() + min); inline Klocka operator+(int min, const Klocka& k){ return k + min; // Anrop av ovanstående operator+, // vänder bara på argumenten inline-funktioner utgör bl. a. ett alternativ till preprocessormacros, som inte bör användas inline är bara ett önskemål, kompilatorn avgör om funktionen går att översätta på detta sätt Medlemsfunktion som definieras inne i en klassdefinition är implicit deklarerade inline (men man kan inline-deklarera en medlemsfunktion definierad utanför klassen också). Bild 59 7
Infogade funktioner och one-definition rule För att kompilatorn skall kunna lägga in koden för en inline-funktion istället för anrop måste den ha tillgång till hela funktionsdefinitionen vid kompilering av de moduler som anropar funktionen. Om funktionen hör till en annan modul måste alltså definitionen finnas i headerfilen! I C skulle detta skulle leda till problem om två olika moduler som inkluderade samma headerfil med en funktionsdefinition kompilerades var för sig och länkades ihop - i C89 är det inte tillåtet. I C++ gäller följande (något förenklat): inline-funktionsdefinitioner, klassdefinitioner och mallar (och endast dessa) får inkluderas i källkoder för olika moduler som sedan länkas ihop om de är textuellt exakt lika och har samma betydelse. I praktiken innebär det helt enkelt att man kan placera dessa deklarationer och definitioner i headerfiler. Fortfarande måste man gardera sig mot upprepad inkludering av samma headerfil med #ifndef-direktiv Fortfarande gäller också att definitioner av vanliga funktioner måste göras i källkodsfiler som kompileras separat. Bild 60 C:s typecast double d = 3.14; int i = (int) d; Bör inte användas (?) Explicit typkonvertering C++ gammaldagskonverteringar - funktionsnotation: int i = int(d); Kräver ett typnamn: void func(void *vptr){ typedef char* Charpointer; char *cptr = Charpointer(vptr); Standard C++-konverteringar ( nya ): static_cast<måltyp>(uttryck) const_cast<måltyp>(uttryck) dynamic_cast<måltyp>(uttryck) reinterpreted_cast<måltyp>(uttryck) int i = static_cast<int>(d); Bild 61 8
Explicit typkonvertering - konstruktorer Antag följande: class Text{ int siz; char *cptr; Text(char *str){ ; void funk(const Text& t){ Säg att vi vill anropa funk() med strängen Jozef. Text:s konstruktor kan användas för att konstruera ett temporärt Text-objekt ur en char *: funk(text( Jozef )); Detta är ju helt i enlighet med funktionsnotation för typkonverteringar. Detta kallas för användardefinierad konvertering. Det finns även en annan variant av användardefinierad konvertering, tas upp senare. Bild 62 Implicit typkonvertering Den explicita konverteringen av char * till Text på föregående bild är inte nödvändig, det hade fungerat lika bra att anropa funk så här: funk( Jozef ); Detta beror på att kompilatorn, när den träffar på ett värde av en annan typ än den förväntade typen, försöker hitta ett sätt att konvertera det påträffade värdet till ett värde av den förväntade typen. Det är inget konstigt med det, vi är vana vid att int-värden automatiskt konverteras till double-värden när de blandas med double-värden i uttryck och många fler sådana situationer. Så kompilatorn ser Text-konstruktorn som ett sätt att från det påträffade char *-värdet konstruera ett värde av den förväntade Text-typen och kommer att anropa Text-konstruktorn själv. Om man vill förhindra sådan implicit konvertering så kan man deklarera konstruktorn med nyckelordet explicit: class Text{ int siz; char *cptr; explicit Text(char *str){ Bild 63 ; 9
Temporära objekt Uttrycket Text( Jozef ) i funk(text( Jozef )); är ett explicit konstruktoranrop. Det resulterar i att ett temporärt objekt skapas, används och städas bort. Precis samma sak händer vid implicit konvertering. Temporära värden är vi vana vid från t.ex. aritmetiska uttryck, vid beräkningen int sek_per_dygn = 24 * 60 * 60; beräknas först 24 * 60, detta resultat måste lagras någonstans för vidare beräkning. Temporära objekt betraktas på samma sätt: de lever en kort stund och städas bort automatiskt. Om det finns en const-referens som är knutet till objektet så lever objektet så länge referensen finns, annars försvinner det när det uttryck däri det skapats är utvärderat (en referens som inte är const kan inte knytas till ett temporärt objekt). Obs dock att konstruktorn körs som vanligt vid skapandet och destruktorn vid bortstädandet. Bild 64 Temporära objekt, forts. Skapande av temporärt objekt genom explicit konstruktoranrop är ett bekvämt sätt att konstruera ett objekt som behövs bara för att omdelbart kopieras någonstans eller knutas till en const-referens-argument för att existera bara under ett funktionsanrop. class Klocka{ int totmin; Klocka(int min_efter_midnatt){ totmin = min_efter_midnatt; int get_tot_min() { return totmin; ; Klocka operator+(const Klocka& k, int min){ return Klocka(k.get_tot_min() + min); Bild 65 10
Objekttyper per livslängd Statiska objekt Definierade utanför funktioner eller som lokala statiska objekt Ligger i den statiska arean Initieras vid programstarten, finns under hela programexekveringen, destrueras automatiskt när exekveringen terminerar Lokala automatiska objekt Definierade lokalt i funktioner (ev. i inre block). Läggs på stacken. Initieras under anropet av funktionen när exekveringen nått definitionen, finns under anropet (ev. exekveringen av inre blocket), destrueras automatiskt när anropet terminerar (ev. när exekveringen lämnar det inre blocket) Dynamiskt skapade objekt Skapas med new, t.ex Text *tpek=new Text( Dynamo ); Läggs på heapen Finns tills de städats bort med delete, t.ex. delete tpek; Temporära objekt Skapas genom explicit anrop av konstruktorn, t.ex. Text( Tempo ) eller då kompilatorn tycker sig behöva ett sådant objekt Destrueras automatiskt så fort uttrycket det ingick i är klart eller då referensen som knutits till det försvinner Bild 66 Statiska klassmedlemmar Liksom i Java kan man deklarera statiska medlemmar som då blir globalt existerande enheter i en enda uppsättning, tillgängliga för alla objekt av klassen men även då det inte finns något objekt. Statiska medlemmar kan endast deklareras i klassen, de måste definieras utanför (ett undantag utgörs av definitioner av heltalskonstanter). Åtkomst görs genom räckviddsoperatorn :: Det klassiska exemplet - objekträknande klass: class Counted{ static int count; // Endast deklaration string data; // Instansvariabel Counted(string s):data(s){ count++; Counted(const Counted& other):data(other.data){ count++; ~Counted(){ count--; string get_data() const { return data; static int get_count(); // Endast deklaration ; Bild 67 11
Statiska medlemmar, forts. Definition av statiska medlemmar måste göras utanför klassen (i.cpp-filen): int Counted::count=0; // Definition med initiering int Counted::get_count(){ // Definition av medlemsfunktionen, return count; // ordet static upprepas inte Anrop av get_count() görs enligt följande: void funk(){ Counted c1( Jozef ), c2( Stefan ); cout << Counted::get_count() << endl; Statiska medlemmar är inte lika vanligt förekommande som i Java eftersom det i C++ ju finns fristående funktioner - i Java måste alla funktioner tillhöra någon klass vilket leder till missbruk av statiska medlemsfunktioner. I C++ grupperar man klasser och tillhörande hjälpfunktioner i namnrymder. Bild 68 friend-deklarationer Genom att i en klass deklarera en funktion som friend ger man funktionen tillgång till klassens privata medlemmar: class Klocka { int min; friend Klocka operator+(int, const Klocka&); ; const Klocka operator+(int m, const Klocka& k){ return Klocka(m + k.min); Obs! att friend-deklarationen inte deklarerar funktionen, den säger bara att om det finns en sådan funktion och om den försöker göra åtkomst till privata data i denna klass så är det ok. En friend-deklaration introducerar ju inte en medlem, så det spelar ingen roll om den skrivs i en public-, private- eller protected-sektion. Bild 69 12
friend-deklarationer, forts. Om friend-funktionen är en medlemsfunktion i en annan klass anges dess namn som vanligt med klassens namn och deklarationsviddsoperatorn, definitionen av denna klass måste i så fall stå före: class Alfa{ void funk(beta& b) { b.x++; ; class Beta{ int x; friend void Alfa::funk(const Beta&); ; Om man vill ge alla medlemsfunktioner i en annan klass sådan åtkomst deklarerar man klassen som friend, t.ex. friend class Alfa; Om två klasser skall ge varandra ömsesidig tillgång till sina privata delar kan kravet på att definitionen skall stå innan inte uppfyllas, då kan en av klasserna deklareras först: class Alfa; class Beta{ friend class Alfa;. ; class Alfa{ friend class Beta; ; Bild 70 const-ighet :) Att vissa objekt eller värden inte får modifieras (alls eller av ett visst programavsnitt) är en viktig aspekt av programdesign. I C++ finns utbyggt språkligt stöd för detta med kompilatorkontroll. Givet en typ, t.ex. int eller Person, så betraktas typen const int eller const Person som en annan typ, besläktad med den ursprungliga typen men utan modifieringsoperationer. Det är tillåtet med tilldelning från icke-const till const men inte tvärtom. Bild 71 13
const-ighet, forts. Antag class Coord{ int x, y; Coord(int xx, int yy):x(xx), y(yy){ int getx() { return x; int gety() { return y; void move(int dx, int dy) { x+=dx; y+=dy; ; Kompilatorn kontrollerar att man inte har möjlighet att (av misstag) modifiera ett const-objekt: void gunk(coord& a){ // funk med icke-const argument, kan ändra a void funk(const Coord& x){ Coord *pek1 = &x; // kompileringsfel! const Coord *pek2 = &x; // ok gunk(x); // kompileringsfel! gunk(const_cast<coord&>(x)); // ok int y = x.getx(); // kompileringsfel! Bild 72 const-ighet, forts. Det sista felet på föregående bild int y = x.getx(); // kompileringsfel! beror på att medlemsfunktionen getx inte deklarerar att den inte förändrar sitt objekt, därför får man inte anropa den hos ett const-objekt. Att en medlemsfunktion inte förändrar sitt objekt deklareras med ordet const i funktionshuvudet efter argumentlistan: class Coord{ int x, y; Coord(int xx, int yy):x(xx), y(yy){ int getx() const { return x; int gety() const { return y; void move(int dx, int dy) { x+=dx; y+=dy; ; Detta gäller endast icke-statiska medlemsfunktioner. Ordet const här är en del av signaturen, ska alltså stå både i deklarationer och i definitionen. Bild 73 14
const-ighet, forts. Ibland uppstår behovet att ha en viss operation i en klass implementerad på två olika sätt så att den kan utföras på ett sätt i ett const-objekt och på ett annat sätt i ett icke-const-objekt. Man kan då ha två medlemsfunktioner med samma namn och argument men den ena deklarerad som const och den andra inte. Det vanligaste exemplet är indexeringsoperatorn: class Text{ int siz; char *cptr; char& operator[](int index){ return cptr[index]; char operator[] (int index) const { return cptr[index]; ; Hos ett const-objekt kommer const-funktionen att anropas, hos ett icke-const-objekt anropas icke-const-funktionen. Bild 74 const-ighet, forts. Objekt av klasser med värdesemantik ska ibland bete sig som modifierbara objekt, men ibland som icke-modifierbara värden. T.ex. ska ju resultatet av en överlagrad operator+ vara ett värde, inte ett lvalue, givet Klocka k1(13,0), k2(23,0); ska man ju inte kunna göra: k1 + 3*45 = k2; // Borde vara fel! För att ange att ett objekt ska betraktas som värde deklarerar man dess typ som const, t.ex. const Klockslag. T.ex. borde operator+ för Klockslag vara: const Klocka operator+(const Klocka& k, int min){ return Klocka(k.get_tot_min() + min); varvid modifieringar av resultatet förbjuds. Bild 75 15
const-ighet, forts. Vid avancerad utformning av biblioteksklasser kan det hända i mycket speciella situationer att någon medlemmar som inte är synlig för tillämpningsprogrammeraren ska kunna modifieras även i const-objekt. I så fall kan medlemmen deklareras med nyckelordet mutable och kan då modifieras även om dess objekt är const. Långsökt exempel: Text-klass där man samlar indexeringsstatistik: class Text{ int size; char *cptr; mutable int times_indexed; Text():size(0), cptr(0), times_indexed(0){ Text(const char *str); char& operator[] (int index){ times_indexed++; return data[index]; char operator[] (int index) const { times_indexed++; return data[index]; int get_times_indexed() const { return times_indexed; ; Bild 76 16