5. Texter

 

    1. Teckensträngar.
    2. Kommentarer.
    3. Skriva ut texter.
    4. Skriva in texter.
    5. cin.getline().
    6. cout.width().

 

 

1. Teckensträngar.

Som du kanske lade märke till fanns det ingen datatyp för text, bara en för enstaka tecken (char). Hur lagrar man då en text? Svaret är enkelt: bokstav för bokstav. För att sedan kunna komma åt texten på ett enkelt sätt bör man ha ett enda namn på alla de char-variabler som texten består av. Detta åstadkommer vi genom att lagra tecknen i en "lista". Vi ska läsa mer om listor senare. För stunden räcker det att veta hur vi deklarerar en lista av typen char:

char variablenamn[max antal tecken + 1];

T.ex:

char cMinText[22];

Observera att det fortfarande inte är en variabel, utan en lista på variabler av typen char. Man kan inte tilldela mer än en variabel ett värde i en tilldelningssats. Det är därför inte möjligt att tilldela en ‘textvariabel’ ett värde på följande sätt:

char cNamn[25];

cNamn = "Pelle Fnutt"; // error C2446: ´=´ no conversion...

Naturligtvis kan man tilldela ett tecken i taget. Vi ska egentligen vänta med det tills vi läser mer om listor, men här är ett exempel:

char cNamn[25];

cNamn[0] = ´P´;

cNamn[1] = ´e´;

cNamn[2] = ´l´;

cNamn[3] = ´l´;

cNamn[4] = ´l´;

cNamn[5] = ´ ´;

cNamn[6] = ´F´;

cNamn[7] = ´n´;

cNamn[8] = ´u´;

cNamn[9] = ´t´;

cNamn[10] = ´t´;

cNamn[11] = ´\0´;

Det finns även ett annat sätt. Kompilatorn kan läsa text och initiera hela listor åt oss. Det är dock inte en tilldelning i egentlig mening. Det är en initiering i samband med deklaration. Så här kan man t.ex. deklarera en variabel ´MittNamn´ och samtidigt initiera den med mitt namn:

char cMittNamn[11] = "Håkan Berg";

Hur kunde jag nu få mitt namn till att bli 11 tecken? Till att börja med måste vi räkna med mellanslaget, men då blir det ändå bara 10 tecken. Sedan var det ju så att C++ inte hade någon datatyp för textsträngar, utan varje funktion som använder en sådan arbetar mot en lista med ett visst antal element, ett för varje tecken. Hur ska då den funktionen veta när texten är slut? Den kan ju egentligen bara fortsätta att läsa i minnet och tolka all data som ASCII-text, vare sig det är text, numeriska värden eller program. Lösningen är att reservera ett speciellt tecken som textslut, och man har valt värdet noll (inte siffran ‘0’, utan det numeriska värdet noll). Detta lägger man till i slutet av texten (det elfte tecknet i exemplet ovan), varvid cout vet att sluta i tid.

Minnet i datorn kommer på den plats som reserverats under det logiska namnet "cMittNamn" och framåt att innehålla:

48 7D 6B 61 6E 20 42 65 72 67 00

 

Övningsuppgift 5.1.1:

Överkurs: I exemplet ovan ingår koder med bokstäver. Anledningen är att vi är vana vid att räkna på 10 fingrar, vi använder en talbas 10. Denna bas kallas decimal av decima som betyder tio. Till dessa har vi tio tecken, 0 - 9. Datorn passar bättre med en talbas 2, 4, 8, 16 eller 32 etc. Man har valt att använda den hexadecimala talbasen, vilket blir 16. Hexa betyder 6, 6 + 10 = 16, därav hexadecima. Man kan se det som om datorn har 16 fingrar.

Det finns inga tecken för talen 10 - 15, så då har man använt bokstäver i stället:

1 2 3 4 5 6 7 8 9 A B C D E F 10 11 12 ... 19 1A 1B ... 1F 20.

Alltså gäller t.ex. följande:     dec    hex    dec    hex    dec    hex

    0    0    11    B    20    14

    1    1    15    F    32    20

    9    9    16    10    65    41

    10    A    17    11    255    FF

Överkursuppgift 5.1.2:

    • Vad har blanktecken för decimal kod?        
    • Vad har ‘A’ för decimal kod?        
    • Vad motsvarar 80 för decimalt tal?        

2. Kommentarer.

Vi har visserligen inte skrivit några komplicerade program än, men det blir snabbt svårt att läsa en källkod och försöka förstå hur programmet fungerar. För att råda bot på detta har man infört möjligheten att kommentera programkoden. Det innebär att man kan skriva in text som förtydligar programkoden, men som inte påverkar den. Tekniken är den att man markerar kommentarerna som text som ska läsas endast av människor, inte av kompilatorn, "for your eyes only".

Det finns två sätt att markera text som kommentar:

En radkommentar börjar med två snedstreck ´//´ och slutar där raden slutar.

Ett kommentaravsnitt börjar med ett snedstreck och en asterisk ´/*´ samt avslutas med en asterisk och ett snedstreck ´*/´. Ett kommentaravsnitt kan alltså sträcka sig över flera rader.

Det hör till god programmeringssed att skriva kortfattade men förtydligande kommentarer i källkoden.

I ett tidigare exempel använde jag en radkommentar, där jag ville visa att en viss felkod skulle uppstå:

cNamn = "Pelle Fnutt"; // error C2446: ´=´ no conversion...

Naturligtvis har man normalt inte behov av sådana kommentarer i ett program, men här i kurshäftet passar det utmärkt. Vi kommer att se andra, och bättre, exempel i nästa avsnitt.

Alla övningsuppgifter du löser på denna kurs ska vara kommenterade så att man tydligt kan se hur programmet fungerar.

3. Skriva ut texter.

Nu har vi sett hur man kan göra en ‘variabel’ för text. Vi kommer senare att titta på andra möjligheter att ändra värdet i en ‘textvariabel’, men nu ska vi titta på hur man kan skriva ut texter. Vi har redan i förra kapitlet sett att man kan skriva ut en text med hjälp av cout. Det går naturligtvis även att skriva ut en 'textvariabel':

char cNamn[] = "Pelle Fnutt";

cout << cNamn;

Här döljer sig en liten hemlighet. Egentligen har vi inte deklarerat någon variabel som heter cNamn. Vi har deklarerat en lista, och då måste man tala om vilket element i listan man vill använda (lugn, vi ska titta mer på listor senare):

cout << cNamn[0];

Detta resulterar dock i att endast första tecknet skrivs ut. Egentligen vill vi skriva ut alla element i listan, men vi vill undvika att skriva så här:

cout << cNamn[0] << cNamn[1] << etc.

Detta kräver dessutom att vi vet hur många tecken som ska skrivas ut. Tittar vi på det exempel där jag initierade varje tecken för sig, kan vi se att jag lade till tecknet ´\0´ efter namnet. Detta för att markera var texten slutar. Det är ju inte säkert att alla element i listan används. Nu är det så att cout kan känna igen detta tecken (vilket blir värdet noll), så vi skulle kunna skriva ut texten genom att lämna över adressen till första tecknet i stället. Det finns en operator ´&´ som ger adressen till en variabel:

cout << &cNamn[0];

Detta, fungerar. Det finns dessutom en förkortning för &cNamn[0], och den är helt enkelt cNamn. Däför kan vi skriva:

cout << cNamn;

Definition: Skriver man ett variabelnamn avser man det data som finns på den minnesplats som associerats med variabelnamnet. Vill man veta var i minnet detta data står kan man använda den så kallade adressoperatorn. Detta gör man genom att skriva ett &-tecken före variabelnamnet.

&-tecknet kallas adressoperatorn.

 

Vi kommer att använda adressoperatorn mer senare, bland annat när vi diskuterar pekare.

En annan sak som var ny i deklarationssatsen ovan var att hakparantesen inte var ifylld:

cNamn[] "Pelle Fnutt";

Vi kommer att nämna detta i avsnittet om listor, men just nu räcker det att vi vet att kompilatorn själv kan räkna hur många bokstäver texten innehåller, och reservera minnesutrymme för dessa plus ett sluttecken, en ´slutnolla´.

Då kan vi slutligen konstatera att vi har två olika sätt att skriva ut texter, dels från en ´textvariabel´, dels genom att skriva texten direkt i en cout-sats:

#include <iostream.h>

void main()

{

// Deklarera och initiera en ´textvariabel´:

char cText[] = "Denna text står i en textvariabel.";

// Skriv ut en textkonstant:

cout << "Nu tänker jag skriva ut variabeln sText:\n";

// Skriv ut ´textvariabeln´:

cout << cText;

// Avsluta med två radslutstecken:

cout << "\n\n";

}

Övningsuppgift 5.3.1:

I ovanstående exempel är texten i variablen cText respektive cNamn initierade i programkoden. Detta kallas ibland för ´hårdkodade´ texter. Det korrekta namnet är textkonstanter. På samma sätt har vi i föregående kaptel använt numeriska konstanter.

 

4. Skriva in texter.

Precis som cout kan hantera texter kan även cin göra det. Principen är densamma. Naturligtvis behöver man inte initiera texten, så det räcker med att deklarera en char-lista. Därefter kan man skriva in något från tangentbordet genom att använda cin:

#include <iostream.h>

void main()

{

char cText[81];

// Skriva in text:

cout << "Skriv nu något intelligent på en rad.\n";

cin >> cText;

// Skriva ut text:

cout << "Detta var vad du skrev:\n";

cout << cText;

cout << "\n\n";

}

 

Övningsuppgift 5.4.1:

 

Övningsuppgift 5.4.2:

 

5. cin.getline().

Som vi redan sett innehåller cin en liten finess. Vi kan mata in flera saker i rad, antingen genom att ange flera strömningsoperatorer och flera variabler i en cin-sats, eller att skriva flera cin-satser i rad. Finessen är den att användaren kan skilja de olika inmatningarna på mer än ett sätt. Det går att använda 'Enter' mellan de olika värdena, vilket väl är det vanligaste i DOS, men det går även att skilja dem åt med tabulatortangenten, vilket är den tangent man använder i Windows, eller med mellanslag. Använder användaren mellanslag kan det till exempel se ut så här:

    

Observera att två tal skrivits åtskilda av mellanslag. Därefter har användaren avslutat inmatningen med hjälp av 'Enter'.

Denna finess skapar i själva verket ett litet problem åt oss. Hur blir det om man matar in en text, och vill att texten ska innehålla blanksteg. Det kan till exempel vara ett postnummer. Skulle vi försöka med att skriva '214 63', så tolkar cin detta som två inmatningar. Vår charlista för postnumret innehåller endast '214':

#include <iostream.h>

void main()

{

    char cPostNummer[7];

    cout << "Var god ange postnummer: ";

    cin >> cPostNummer;

    cout << "\nDu skrev: " << cPostNummer;

    cout << "\n\n";

}

Utskriften visar att variabeln cPostNummer inte får rätt innehåll:

    

Okej, men vart tog '63' vägen då? Jo, den strängen tog vi aldrig hand om , så den låg kvar i den så kallade tangenbordsbuffern, en mellanlagring i RAM, som operativsystemet har hand om. När vårt program tog slut kastade helt enkelt operativsystemet bort texten '63' för oss.

Skulle vi nu fortsätta inmatningen med att mata in ort efter postnumret, så kommer '63' att ligga klar när vi kommer till inmatning av ort, och vi får aldrig en chans att skriva något. Vi kan bygga på exemplet:

#include <iostream.h>

void main()

{

    char cPostNummer[7];

char cOrt[21];

cout << "Var god ange postnummer: ";

    cin >> cPostNummer;

cout << "Var god ange ort.......: ";

    cin >> cOrt;

    cout << "\n\nDu skrev:\n\n";

    cout << "Postnummer: " << cPostNummer << '\n';

    cout << "Ort.......: " << cOrt;

    cout << "\n\n";

}

Här ser vi att hamnade i cOrt, vilken vi sedan aldrig han ange:

    

Den lösning de kunskaper hittills fått räcker bara till en klumpig lösning, nämligen att lagra postnumret i två strängar för postnummer. Det skulle vara enligt samma modell som exemplet ovan, där vi matar in två tal. Det vore dock en ganska klumpig lösning.

De vise män som försett oss med objektet cin har dock redan brytt sina grå celler med problemet. Vi kan nu inte här gå närmre in på teknikaliteter, eftersom vi inte har grundkunskaper för att förstå oss på objekt, men vi kan använda ett recept, och de vise har försett oss med en metod i objektet cin, vilken heter getline(). Så här använder man den för att lösa vårt problem:

cin.getline(cPostNummer,7);

Om man ersätter den gamla inmatningsraden för postnummer med ovanstående så fungerar det:

    

Syntaxen för cin.getline() anger att man ska ange ett maxantal tecken för inmatning. Detta skyddar vårt program från att cin.getline() skriver utanför vår deklarerade charlista, ett skydd som cin inte innehåller.

Tyvärr har vi därigenom fått ännu ett nytt problem på halsen. Skulle användaren mata för många tecken i postnumret blir det tecken över i inmatningsbuffern. Detta resulterar i precis samma fenomen som vi hade tidigare, det ligger kvar till nästa inmatning. Så här kan det bli om användaren råkar slå dubbla treor i postnumret:

    

Som synes är objektet cin inte helt perfekt, och vi kommer inte längre här. Användaren får mata in rätt uppgifter, och vi får skriva progam som ger användaren möjlighet att rätta felaktigt data i efterhand.

Om dock en cin.getline() följs av en annan cin.getline() och den förra får en inmatning som precis passar i charlistan, vilket ju blir fallet med postnumret, så kommer tecknet från tryckningen på Enter-tangenten att ligga kvar i bufferten, och vi får återigen inte tillfälle att fylla i ort. Detta löser man bäst genom att göra charlistan något för lång, och så även längdangivelsen i cin.getline(). Anledningen att ovanstående exempel fungerar är bara den att vi fyller i ort med en cin i stället för en cin.getline().

Problemet blir detsamma när man just matat in en siffra. Objektet cin är nämligen inte heller snäll om man blandar olika sorters data vid olika inmatningar. När man matat in ett numeriskt värde hämtar cin nämligen endast siffrorna. Ja, tänker man kanske då, det var ju siffrorna jag ville ha. Men vad har man egentligen tryckt på för tangenter, jo först siffrorna och sedan Enter! Den tangenttryckningen ligger sedan kvar och 'skräpar' i buffern. Nästa gång man vill göra en inmatning finns det redan en Enter, vilken gör att nästa inmatning läser den. Om den inmatningen avser text, får man direkt en tom sträng, innan användaren hinner skriva något. Vi kan testa detta:

#include <iostream.h>

void main()

{

    char namn[31];

    int alder;

cout << "Var god ange ålder: ";

    cin >> alder;

    cout << "Var god ange namn: ";

    cin.getline(namn,31);

    cout << "\nDu skrev:\n\n";

cout << "Ålder: ";

    cout << alder << '\n';

cout << " Namn: ";

    cout << namn << '\n';

    cout << "\n\n";

}

Detta resulterar i nedanstående utseende. Lägg märke till att vi aldrig fick en chans att fylla i namnet:

    

Det finns ett sätt att ta bort Enter ur buffern innan man försöker mata in namnet. Utan att gå in på tekniska detaljer accepterar vi bara att nedanstående rad löser problemet, om man skriver den omedelbart före inmatningssatsen för namnet:

cin.ipfx(0);

Nu ska det fungera:

    

Övningsuppgift 5.5.1:

    

6. cout.width().

Ibland kan man ha ett behov av att skriva ut uppgifter i jämna kolumner. Objektet cout skriver ut saker och ting med den bredd de har, och det låter oss inte skriva i jämna kolumner. Skulle vi till exempel göra en tabell med namn och telefonnummer, skulle telefonnumren inte komma ovanför varandra. Skulle man sedan sätta in en rubrikrad ser det extra fult ut:

 

 

m man anger bredden på den vänstra kolumnen, den för namn, kommer samtidigt den högra kolumnen att bli rak. Detta kan man göra genom att skriva programsatsen:

cout.width(23);

...omedelbart före utskriftssatsen för namnet. Detta bara effekt under nästföljande utskrift, så vi måste skriva in det tre gånger, en för varje utmatning av namn. Nu blir det litet snyggare:

Som synes blir namnet högerjusterat. Detta är alltså egentligen anpassat för heltal, och vi kommer senare i kursen att behöva just detta.

Övningsuppgift 5.6.1: