FO_1_Intro_Momstabel..

Föreläsning 1: Momstabellen i C++
Nu sätter vi igång med C++!
På den här föreläsningen skall jag ta ett stort exempel och ett par små och med dessa hoppas jag att
täcka in mycket av det som är grundläggande. Idag är tanken att vi skall hinna med hur man skriver
ett litet c++-program, hur satser (tilldelning, if-satser, loopar, m.m.) ser ut i c++ och en del annat. I
det första exemplet skall vi ta ett gammalt välkänt problem som vi redan har löst och titta på
övergången mellan Ada och c++. Här är problemet:
Skriv ett program som låter användaren mata in två priser och ett steg (realla tal) och som sedan
skriver ut en momstabell. Momssatsen är alltid 25%. Programmet skall göra rimlighetskontroller på
indatat.
Körexempel:
Mata in första pris (mellan 0 och 100): 10 Mata in sista pris (minst första pris): 12 Mata in steglängd: 0.3 MOMSTABELL (mini) 10.00 2.50 12.50 10.30 2.58 12.88 10.60 2.65 13.25 10.90 2.73 13.62 11.20 2.80 14.00 11.50 2.88 14.38 11.80 2.95 14.75 Detta känner vi igen, det är ju vår gamla lab 1 (i princip). Vi rotar fram koden och modifierar lite, då
får vi (lämplig att dela ut, eller köra på OH) mom.adb:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
with Ada.Text_IO;
with Ada.Float_Text_IO;
with Ada.Integer_Text_IO;
use Ada.Text_IO;
use Ada.Float_Text_IO;
use Ada.Integer_Text_IO;
procedure Mom is
Fp : Float;
Lp : Float;
S : Float;
M : Float;
N : Integer;
begin
Put("Mata in första pris (mellan 0 och 100): ");
Get(Fp); while Fp < 0.0 or Fp > 100.0 loop
Put("ERROR: Mata in första pris igen: ");
Get(Fp);
end loop;
loop
Put("Mata in sista pris (minst första pris): ");
Get(Lp);
if Lp >= Fp then
exit;
end if; end loop;
Put("Mata in steglängd: ");
loop
Get(S);
exit when S > 0.0;
Put("ERROR: Mata in steglängd igen: ");
end loop;
M := 25.0; ­­ Svensk normalmoms (för kläder)
N := Integer(Float'Floor((Lp ­ fp) / S + 1.0));
Put_Line("MOMSTABELL (mini)");
for I in 0 .. N ­ 1 loop
Put(Fp + Float(I) * S,
Fore => 5, Aft => 2, Exp => 0);
Put((Fp + Float(I) * S) * M / 100.0,
Fore => 7, Aft => 2, Exp => 0);
Put((Fp + Float(I) * S) * (1.0 + M / 100.0),
Fore => 7, Aft => 2, Exp => 0);
New_Line;
end loop;
end Mom;
Ett par korta kommentarer om detta program:
Vi har ett par olika varianter på rimlighetskontrollerna. Detta för att vi skall få se lite olika skrivsätt när vi går
över till c++. Många brukar lösa den här laborationsuppgiften med en while för själva tabellutskriften. I detta
exempel har jag valt att använda en for (egentligen är detta bättre) och helt enkelt räkna ut hur många varv
som loopen skall gå.
Vi sätter igång på direkten. I den här kursen kommer ni att fortsätta att programmera i emacs (eller vad ni nu
har använt tidigare) så det är precis samma som när vi skulle skapa ett ada-program. Vi skapar en ny fil som
vi kallar för mom.cpp.
Vad skriver vi nu i denna? Jo vi börjar, precis som i ada, med själva programskellettet, vilket är ganska
simpelt:
1
2
3
4
5
int main()
{
return 0;
}
Vad innebär nu detta? Från Ada är vi vana vid att kunna döpa vårt huvudprogram till vad vi vill, eller snarare
att huvudprogrammet heter ju som filen. I C++ heter huvudprogrammet alltid main. Parenteserna efter main
är en parameterlista. Man kan alltså lägga till parametrar till huvudprogrammet. Vi kommer senare att se att
det är här man kan lägga till saker om man vill ha kommandoradsargument till sitt program, vi hoppar över
det så länge.
Klamrarna (eller måsvingarna, eller vad man nu vill kalla dem) markerar att här börjar ett block, vi kan tänka
på det som "begin" och "end" från ada. Alla konstruktioner som spänner över flera satser kommer behöva
sådana block (t.ex. if-satser och loopar). Dessa talar alltså om vilka satser som hör till huvudprogrammet.
Ordet "int" precis innan main beskriver returtypen för funktionen. Funktionen? Jajemän i c++ är även ditt
huvudprogram en funktion precis som alla andra underprogram (det finns inga procedurer). Detta hänger
alltså ihop med att vi returnerar ett tal längst ner i programmet. Varför skall man returnera ett heltal då? Jo,
egentligen är det så att program bör returnera ett tal som talar om hur de avslutades. Att returnera 0 betyder
att programmet avslutades normalt, allting annat kommer betyda att något gick fel under körning. Ett
program kan på ett sådant sätt returnera "felkoder". Vi kommer inte jobba jättemycket med det. Skulle man
glömma att göra "return 0;" i sitt program så kommer faktiskt kompilatorn lägga till det åt dig, men man
skall ha "int" som returntyp för sitt huvudprogram i c++.
Innan vi går vidare så kan jag bara säga något om hur vi kompilerar. Vi kommer att använda kompilatorn
g++ (en del av gcc, precis som gnatmake). För att kompilera detta skulle vi alltså skriva:
g++ mom.cpp
Om det blir kompileringsfel eller varningar så kommer g++ att beskriva dessa. Nu vill jag varna er lite, g++
är inte alls lika snäll som gnatmake. Ta det därför lugnt i början och kompilera ofta så att ni slipper många fel
samtidigt. Det är också så att g++ inte varnar på lika mycket som t.ex. gnatmake gjorde, för att få det
beteendet måste man själv lägga till varningsflaggor t.ex. -Wall och -Wextra. Så här:
g++ ­Wall ­Wextra mom.cpp
Om ni kompileringen gick bra så kommer man få den körbara filen a.out (den heter alltid så). Vill man att
den körbara filen skall heta något annat så kan man lägga till flaggan -o och sedan namnet:
g++ mom.cpp ­o mitt_program
Nu skulle alltså den körbara filen heta mitt_program. På vår hemsida kommer vi ha en liten "kom igång"sida länkad vid laborationerna om vilka flaggor som kan vara bra att använda, och lite andra justa tips.
Nu tittar vi tillbaka i mom.adb. Vad gjorde vi där? Jo först så deklarerade vi lite variabler. Ett c++-program är
inte uppdelat i en deklarationsdel och en satsdel. Man får alltså deklarera variabler vart man vill i sin kod, vi
måste inte deklarera dem i början. Vi kan ju börja med ett par stycken, så här deklarerar jag variabler:
1
2
3
4
5
6
7
int main()
{
double fp;
double lp, s;
return 0;
}
Man skriver alltså datatypen först och sedan namnet på sin variabel. Här är det "double" som är datatypen. I
c++ finns float också, men vill man representera realla tal så används vanligtvis double eftersom den har mer
precision. Precis som i Ada kan jag deklarera två variabler samtidigt (rad 4). Värt att nämna också är att c++
skiljer på stora och små bokstäver, så kompilatorn kommer inte att förstå om vi senare t.ex. skriver "FP".
Här är lite andra datatyper i c++ (och motsvarigheten i Ada):
Ada
C++
Float
float (double används oftast)
Integer
int
Character
char
Boolean
bool
String
string
Vad är nästa sak som skall ske? Jo vi skall skriva ut lite text. I C++ använder vi cout till detta. cout är en
utmatningsström som skriver ut till standard out d.v.s. till terminalen. Precis som i ada finns den inte i
språkets "kärna" utan vi måste lägga till något, biblioteket iostream, detta gör vi med preprocessorkommandot #include. Detta talar om för preprocessorn (något som körs innan kompilatorn) att den även skall
ta med rutinerna i iostream innan vi kompilerar. Man kan tänka på detta som "with" i Ada, fast i c++ är det
mer att vi "klipper och klistrar" med koden. Precis som i Ada så kan man nu göra något ytterligare för att vi
sedan bara ska behöva skriva cout. Ni kom väl ihåg när vi började och var tvugna att skriva Ada.Text_IO.Put
hela tiden? Det är samma sak nu. Gör vi nu inget mer så måste vi skriva std::cout varje gång vi vill använda
cout. Gör vi inte det så kommer kompilatorn bråka om att cout inte är deklarerad. "std" är nämligen den
namnrymd som cout tillhör. Om vi då skriver till using namespace std; är det sedan fritt fram att bara säga
cout. Namnrymder kan (till skillnad från att göra "use" på ett paket i Ada) spänna över flera bibliotek. Allt
som allt får vi:
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
using namespace std;
int main()
{
double fp;
double lp, s;
cout << "Mata in första pris (mellan 0 och 100): ";
Just "<<" är en operator för "formaterad utmatning", mer om det senare. För tillfället kan vi se detta som att
vi skickar strängen till cout, detta motsvarar alltså Put i Ada.
Nu vill vi låta användaren mata in ett första pris. Inmatning gör man med couts motsvarighet, cin, som läser
från standard in d.v.s. tangentbordet. Hur ser detta ut, jo så här:
12 cin >> fp;
Detta motsvarar alltså Get (för flyttal) i Ada. ">>" är operatorn för "formaterad inmatning" vad detta innebär
vet ni egentligen redan från hur Get beter sig i Ada. D.v.s. läs bort inledande vita tecken, läs sedan in något
som kan tolkas som ett flyttal, t.ex. 10.0.
Om vi nu tittar i Ada-koden så ser vi att vi vill ha en while-loop. Att göra detta i c++ blir mycket likt:
13
14
15
16
17
18
while (fp < 0.0 || fp > 100.0)
{
cout << "ERROR: Mata in första pris igen: ";
cin >> fp;
}
Skillnaderna är som sakt mycket små. Det är den logiska operatorn "or" som har bytts mot "||". Vi har inget
ord "loop" eller "end loop", vi använder klamrarna istället. Det går att skippa klamrarna om man bara har en
sats i loopen (samma med if-satser) men gör inte det, ha alltid med dem. Det blir så lätt fel om man senare
går tillbaka och lägger till en till sats. Jag vill passa på att säga att vita tecken inte har någon betydelse, precis
som i Ada (om man inte råkar slå ihop två ord eller bryta isär t.ex. while till whi le). Exakt vart man sätter
sina radbrytningar och hur många mellanslag man vill ha bestämmer man alltså lite själv. Givetvis är det bra
att följa någon sorts kodstil. Vi på kursen (och i följande kurser) kommer t.ex. alltid sätta klamrarna på egen
rad, i indenteringsnivå med den sats de tillhör.
Nu tittar vi på nästa loop (d.v.s den som börjar på rad 22 i Ada-koden). Detta är ju någon form av loop med
ett villkor, precis som while, fast kontrollen av villkoret är i slutet. I Ada finns det ingen sådan inbygd loop
så man har här använt den vanliga "loop" och satt en if-sats med en "exit" sist. I c++ och c-relaterade språk
finns det dock just en sådan loop som vi vill ha, den heter do-while:
19
20
21
22
23
24
do
{
cout << "Mata in sista pris (minst första pris): ";
cin >> lp;
}
while (lp < fp);
Eftersom villkoret nu talar om när loopen skall fortsätta, snarare än när loopen skall avbrytas måste vi nu
vända på villkoret. Det fanns alltså en sats i c++ som inte fanns i Ada! Språk är olika helt enkelt. Det är inte
så att vissa språk är "bättre" än andra, utan de är "bättre" i vissa givna situationer. I vårt fall såg vi att det inte
spelade någon roll att do-while inte fanns i Ada, vi kunde lösa det ändå. Nu kommer vi till något som finns i
Ada men inte i c++, nämligen nästa loop (rad 31). Den här loopen avbryts mitt i. C++-motsvarigheten till
"exit" är break, så det kan vi fixa, men hur får vi en loop som motsvarar Adas "loop". Här kan man göra på
lite olika sätt, men det vanligaste är att man använder en while med ett villkor som alltid är sant, t.ex. true:
25
26
27
28
29
30
31
32
33
34
cout << "Mata in steglängden: ";
while ( true )
{
cin >> s;
if ( s > 0.0)
{
break;
}
cout << "ERROR: Mata in steglängden igen: ";
}
Vi blir tvugna att ta till en if-sats för vår break. Just "exit when" är en ada-specialitet. If-satsen i c++ är
mycket lik den i Ada. Vi har inget "then" eller "end if", vi använder klamrar istället. Det finns självklart else,
och också else if.
Nu kommer vi fram till den punkt då vi vill sätta att momsen skall vara 25% (rad 37). I vårt c++-program har
vi ju inte ens deklarerat m än. Det kan vi göra nu. Vi kan samtidigt passa på att säga att m skall vara en
konstant, då måste vi sätta ms värde direkt:
35 const double m = 25.0; "const" marekare alltså att m är ett data som inte får ändras senare i vårt program. Tilldelning gör vi med "=",
alltså inte ":=" som i Ada. Man kan då fundera på hur man jämför om två saker är lika i C++, jo med "==",
mer om det senare. Just att tilldela en variabel då den deklareras kallas för initiering. Det finns en nyare, mer
modern syntax för detta som använder klamrar, jag kommer tillbaka till den strax.
Nu vill vi beräkna hur många varv som den kommande for-satsen skall gå. Vi deklarerar därför variabeln n
och tilldelar den resultatet från ett uttryck:
36 int n;
37 n = floor((lp ­ fp) / s) + 1.0);
Saknas det inte något? Tja, det ser lite märkligt ut eftersom högerledet här verkar ju bli ett flyttal (double)
medan till till vänster är heltal. Här sker en automatisk omvandling från double till int. C++ är ett typat språk,
men det kan ske automatiska omvandlingar, så det gäller att se upp lite. funktionen "floor" avrundar nedåt
och finns inuti ett annat bibliotek som heter cmath, så det får vi lägga till längst upp. I detta fall skulle man
faktiskt inte behöva göra "floor" eftersom den automatiska omvandlingen trunkerar (hugger av decimalerna).
Nu skall vi bara skriva ut en rad till (rubriken på tabellen), och dessutom göra ett new_line:
38 cout << "MOMSTABELL (mini)" << endl;
Man kan göra radbrytningar med specialtecknet '\n'. En "endl" gör precis detta och "flushar" dessutom
utmatningsbufferten så att man vet att texten verkligen har skrivits ut innan man går vidare till andra satser.
Nu till själva tabellen. Hur ser en for-loop ut i c++? Tja egentligen är det bara en glorifierad while, en
while++. Generellt så skriver man den på detta sätt:
Villkor för att
fortsätta upprepa.
for ( ; ; )
Satser som görs en gång
innan första varvet
Satser som görs
i slutet av varje varv.
Vi kan alltså formulera vår for-loop på detta sätt:
39 for ( int i = 0 ; i < n ; ++i )
40 {
Innan första varvet vill vi ju ha en ny variabel (precis som vi får från Adas for-loop). I C++ deklarerar vi den
i for-satsens första del. Den variabeln kommer bara att existera i for-satsens block. Det är det vi är vana vid
från Ada. Villkoret för att fortsätta är att i håller sig mindre än n, d.v.s det sista värde som i får anta är n - 1. I
slutet av varje varv vill vi räkna upp i med 1. Vi hade lika gärna kunnat skriva i = i + 1 här, men vi kan passa
på att introducera preinkrementsoperatorn ++ som gör precis samma sak. Här är lite andra varianter:
i++ i += 1 postinkrement, räknar upp i men returnerar det gamla värdet.
exakt samma som att skriva i = i + 1.
Tycker man att dessa verkar krångliga så är det helt ok att skriva i = i + 1.
Nu är vi snart klara. Vi skall bara skriva ut raderna i vår tabell. Vi kan använda cout för att skriva ut flyttal
också, men om vi inte säger något extra blir det inte snyggt (vi får inte ut några decimaler!). För att säga hur
många decimaler man vill skriva ut med så kan man använda utskriftsmanipulatorn setprecision, och fixed.
När man använder dessa så kommer de att gälla för alla utskrifter som görs till cout framöver.
cout << setprecision(2) << fixed << fp; //skriver ut fp med två decimaler
För övrigt är "//" kommentarsymbolen i c++. Man kan även göra kommentarer som spänner över flera rader
med "/*" och "*/", men gör inte det, det brukar sällan bli bra.
Vi vill också säga att vi vill att talet skall skrivas ut med en viss bredd, det kan vi göra med manipulatorn
setw. Detta kommer att ange bredden för hela talet, vi räknar lite och kommer fram till att det borde bli 8. (I
Ada-koden var ju Fore 5. Med två decimaler och en plats för decimalpunkt blir detta 8). Setw (till skillnad
från setprecision) gäller bara för just nästa utskrift dock:
41 cout << setprecision(2) << fixed
42 << setw(8) << fp + i * s;
Obs: åter igen sker en automatisk typomvandling på rad 42 (i är ju int). Vi ser nu att setprecision och fixed
alltså borde kunna ligga utanför loopen, men men.
Utskriftsmanipulatorerna ligger i biblioteket iomanip, så det får vi också lägga till längst upp.
Nu behöver vi bara skriva ut resten. Man kan skicka många saker till cout när man ändå håller på:
43
44
45
46
47
48
49
cout << setw(10)
<< (fp + (i * s)) * m / 100.0
<< setw(10)
<< (fp + i * s) * (1.0 + m / 100.0);
<< endl;
} // slut på for­loopen
Så! Det var hela programmet.
Nu tänkte jag gå över och titta på ett annat exempel som illustrerar lite mer I/O. Säg att vi vill ha ett program
som låter användaren mata in sitt för- och efternamn (två ord) och sin hobby (en hel rad text). Programmet
skall sedan upprepa tills man väljer att avsluta.
Körexempel:
Mata in ditt för och efternamn: Eva Andersson
Mata in din hobby: Åka snowboard och skidor
Vill du avsluta (j/n): n
(programmet upprepar)
I c++ löser vi lätt de två första problemen med datatypen string. En string i c++ kan innehålla godtyckligt
många tecken - hurra! Det är alltså inte riktigt så enkelt som i Ada där strängar helt enkelt var fält av tecken.
Exakt hur string är implementerad kan vi titta på någon annan gång, just nu är vi intresserade av hur vi
använder den:
1 #include <iostream>
2
3 using namespace std;
4
5 int main()
6 {
7 string first_name, last_name;
8
9
10
11 cout << "Mata in ditt för och efternamn: ";
12 cin >> first_name >> last_name;
13
14 return 0;
15 }
Med formaterad inmatning läser cin alltså tecken till strängen tills den stöter på ett vitt tecken. Detta fungerar
bra för inläsningen av våra namn. Hur gör vi med hobbyn, det skulle ju vara en hel rad. Lyckligtvis finns det
en bra rutin för detta, nämligen get_line, som läser in ända till radslut:
14 15 cout << "Mata in din Hobby: ";
16 getline(cin, hobby);
Vi får lägga till hobby bland våra strängvariabler. Nu måste vi dock tänka till lite om tangentbordsbufferten
igen. Efter första inmatningen kommer det ju att ligga ett enter-tecken kvar i bufferten. Det innebär ju att
getline direkt kommer att ta detta som att användaren skrev in 0 tecken på hobbyn. Mellan inmatningarna
måste vi alltså rensa bufferten, en Skip_Line från Ada hade varit trevligt. I C++ finns en liknande rutin som
heter ignore som anropas direkt på cin. Till den skickar man hur många tecken som skall ignoreras och vilket
tecken som ignoreringen skall sluta vid. Vi lägger alltså till efter den första inmatningen:
13 cin.ignore(1000, '\n');
Detta betyder alltså, börja kasta bort tecken tills antingen du har kastat bort ett entertecken eller tills du har
kastat bort 1000 stycken. Varför valde jag just 1000? Tja, om det skulle komma mer skräp innan
entertecknet. Nu fungerar ju inte detta om det råkar vara mer än 1000 skräptecken, så egentligen skulle jag
vilja göra detta på något mer generellt sätt, men det löser vi inte nu.
Nu skall dessa upprepas, så vi lägger på en do-while. Men vad skall vi ha för krav på avslutandet av loopen?
Jo, vi låter användaren mata in ett tecken. Vi deklarerar variabeln c (med datatypen char) och läser in endast
ett tecken. Det kan vi göra med rutinen get. Nu ser programmet ut så här:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;
int main()
{
string first_name, last_name;
string hobby;
char c;
do {
cout << "Mata in ditt för och efternamn: ";
cin >> first_name >> last_name;
cin.ignore(1000, '\n');
cout << "Mata in din hobby: ";
getline(cin, hobby);
cout << "Vill du avsluta (j/n): ";
cin.get(c);
} while (c != 'j');
return 0;
}
Här är det viktigt att man faktiskt skriver "!=", det är operatorn "skiljt ifrån". Okej vi hade ju lika gärna
kunnat skriva "c == n", men om vi skulle använda Adas operatorer "/=" eller "=" så skulle detta tolkas som
något helt annat. "=" är ju faktiskt tilldelning, och tilldelning returnerar ett värde i C++. Alltså skulle det
fortfarande gå att kompilera men programmet skulle bete sig mycket underligt. Jag varnar alltså därför åter
en gång, tänk till när ni skriver "=" i c++, är det tilldelning eller jämförelse ni menare. Detta är en superlätt
grej att göra fel på när man är ny.
Innan vi avslutar så skall jag ta ett litet exempel till. Man kan faktiskt läsa tecken från tangentbordet tills det
"tar slut". Ett sådant slut brukar kallas för filslut och markeras med ctrl-d. Nu skall vi göra ett program som
läser tecken till filslut och summerar hur många vita tecken som matades in.
När man använder cin för att läsa in så returneras det alltid sant eller falskt. Så länge det gick bra att läsa så
returneras sant, om det inte går bra (t.ex. för att indatat har "tagit slut") så kommer falskt att returneras. Det
betyder att man kan sätta själva inläsningen i en while-loops villkor:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <cctype>
using namespace std;
int main()
{
char c;
int num_spaces{0};
while ( cin.get(c) )
{
if (isspace(c)) {
num_spaces += 1;
}
}
cout << "Du skrev in " << num_spaces
<< " vita tecken." << endl;
return 0; }
På rad 9 vill vi initiera variabeln num_spaces till 0. Vi skulle kunna göra detta med tilldelningsoperatorn
("=") men jag visar här ett annat sätt att initiera variabler på.
Funktionen isspace finns i biblioteket cctype. Där finns det en hel del bra rutiner för att hantera tecken, t.ex.
rutiner för att kolla om något är en stor bokstav, om ett tecken är ett numeriskt tecken o.s.v.