Område 2 · lektion 7
C++ efter Python
Du kan redan programmera. Den här guiden handlar inte om att lära sig det igen, utan om de fyra ställen där C++ inte gör som Python – och där du annars sitter och stirrar på ett felmeddelande som inte liknar något du sett förut.
Mål
- Du kan läsa ett kompileringsfel och hitta raden det handlar om.
- Du kan deklarera en variabel med rätt typ, och säga varför typen behövs.
- Du kan säga vad en
intrymmer på det här kortet, och känna igen ett värde som spillt över. - Du kan skriva ut ett värde till seriemonitorn och läsa det utan att det blir teckensallad.
Genomgång
Felet kommer före körningen
I Python körs raderna en och en tills något går sönder. Ett stavfel längst ner i filen märks först när programmet kommit dit. C++ fungerar tvärtom: hela sketchen läses igenom och översätts till maskinkod innan något körs, och hittas ett enda fel översätts ingenting.
Det låter som en nackdel och är motsatsen. På ett kort utan skärm är det betydligt bättre att felet dyker upp i IDE:n än att en spak rör sig åt fel håll när ingen tittar.
Ett typiskt felmeddelande ser ut ungefär så här:
sketch.ino: In function 'void loop()':
sketch.ino:9:23: error: expected ';' before '}' token
Läs det bakifrån: vad som saknas (;),
var (rad 9, tecken 23), och i vilken funktion
(loop()). Ett saknat semikolon anmärks ofta på raden
efter den där det saknas, eftersom kompilatorn läser vidare tills
den möter något som inte går ihop. Står felet på en rad som ser klanderfri
ut – titta på raden ovanför.
Det första felet är det enda som räknas Ett enda misstag kan ge tjugo rader felmeddelanden, eftersom kompilatorn tappar tråden och sedan klagar på allt som följer. Rätta det översta felet, kompilera om, och se hur många som blev kvar. Att läsa hela listan uppifrån och ner är bortkastad tid.
Varje variabel har en typ, och typen står utskriven
I Python skriver du antal = 5 och tolken listar ut resten. I
C++ måste du säga vilken sorts låda värdet ska ligga i, och lådan byter
aldrig sort efteråt.
int antal = 5;
float spanning = 4.92;
bool tand = false;
char bokstav = 'A';
Fem typer räcker långt i den här kursen:
| Typ | Storlek på Uno | Rymmer | Används till |
|---|---|---|---|
bool | 1 byte | true eller false | av eller på, tryckt eller inte |
byte | 1 byte | 0 … 255 | små positiva tal, till exempel ett pinnummer |
int | 2 byte | −32768 … 32767 | vanliga heltal: vinklar, räknare, mätvärden |
long | 4 byte | −2 147 483 648 … 2 147 483 647 | stora tal, och tid i millisekunder |
float | 4 byte | ungefär 6–7 siffrors noggrannhet | spänningar och andra decimaltal |
Den enda överraskningen som verkligen biter: int är 16 bitar
På den här kontrollern är en int 2 byte,
alltså 16 bitar. Läser du en C++-bok skriven för en vanlig dator står det
att en int är 32 bitar, och kommer du från Python är du van
vid heltal utan övre gräns alls. Ingetdera gäller här.
Sexton bitar ger 216 = 65 536 olika värden. Hälften används till negativa tal, så området blir
−2¹⁵ … 2¹⁵ − 1 = −32768 … 32767
Vad händer när talet går över taket? Det viker runt till andra änden. 32767 + 1 blir −32768. Ett exempel som faktiskt drabbar folk:
int sekunder = 60;
int millisekunder = sekunder * 1000; // 60000 ryms inte i en int
Serial.println(millisekunder); // skriver -5536, inte 60000
Räkna efter varifrån −5536 kommer:
60 000 − 65 536 = −5 536
Talet 60 000 ligger över taket, och när det viker runt hamnar det 65 536 steg längre ner. Lösningen är att välja en större låda:
long millisekunder = (long)sekunder * 1000; // 60000, som det ska
(long) framför säger åt kompilatorn att räkna i den större
typen redan från början. Skriver du bara
long millisekunder = sekunder * 1000; hinner
multiplikationen spilla över innan resultatet läggs i den stora
lådan, och du får −5536 ändå. Det är ett fel som ser omöjligt ut när man
inte vet varför.
Därför är tid alltid long
Kortets egen millisekundräknare, som du möter i område 3, passerar 32767
efter 32,767 sekunder – knappt en halv minut. Alla tider mäts därför i
unsigned long, som rymmer 0 … 4 294 967 295 millisekunder.
Det räcker i 4 294 967 295 / 1000 / 60 / 60 / 24 ≈ 49,7 dygn innan även
den viker runt.
Konstanter: värden som inte ska ändra sig
Pinnummer, vinklar och gränsvärden hör hemma överst i sketchen, med namn
och med const framför.
const int KNAPP_PIN = 2;
const int SERVO_PIN = 9;
const int VILOVINKEL = 20;
Tre skäl, i stigande ordning:
- Namnet säger vad talet är.
digitalRead(2)kräver att du minns vad som satt på tvåan.digitalRead(KNAPP_PIN)gör det inte. - Ändringen sker på ett ställe. Flyttar du knappen till pinne 4 ändrar du en rad, inte sju.
consthindrar dig från att skriva över värdet av misstag. Försöker du det blir det ett kompileringsfel, alltså ett fel du ser innan bygget rör sig.
Versaler i konstantnamn är ingen regel i språket utan en vana – samma vana som i Python. Den gör att du ser på en rad kod om ett namn är något som ändrar sig eller inte.
Måsvingar i stället för indrag
Python använder indraget för att avgöra vad som hör ihop. C++ använder
måsvingar { } och struntar fullständigt i indraget. Att koden
ändå ska vara indragen är för din skull, inte kompilatorns – och just
därför blir felen elakare: koden kan se rätt ut och betyda något annat.
| Python | C++ |
|---|---|
if x > 5: | if (x > 5) { |
| indrag | } avslutar |
and · or · not | && · || · ! |
True · False | true · false |
# kommentar | // kommentar |
| radslut avslutar satsen | ; avslutar satsen |
Måsvingar direkt efter if är valfria när det bara är en rad
som ska styras, men skriv dem alltid. Utan dem hör bara den första raden
till villkoret, hur du än drar in de följande – och det är ett fel som
överlever många genomläsningar.
Serial är en sladd, inte stdout
Det finns ingen konsol. Serial.println() skickar tecken ut
genom USB-sladden, och seriemonitorn i IDE:n är programmet som tar emot dem
i andra änden. Två saker följer av det.
Först: hastigheten måste stämma i båda ändar.
Serial.begin(9600) säger att kortet skickar 9600 bitar per
sekund. Står seriemonitorns rullgardin på något annat tal kommer tecknen
fram som obegriplig teckensallad – inte som ingenting, utan som fel
tecken. Det är den vanligaste orsaken till en monitor full av frågetecken
och rutor.
Sedan: det tar tid. Varje tecken skickas som tio bitar (en startbit, åtta databitar, en stoppbit), så
9600 bitar/s / 10 bitar per tecken = 960 tecken per sekund
1 / 960 s ≈ 0,001 s = 1 ms per tecken
Tjugo tecken tar alltså ungefär 20 ms att få ut genom sladden. Själva
anropet återvänder snabbare än så – tecknen läggs i en buffert och skickas
i bakgrunden – men taket på 960 tecken i sekunden gäller ändå. Skriver du
ut tjugo tecken i varje varv av loop() kan varven i längden
inte komma tätare än 20 ms isär, hur snabbt kortet än är i övrigt: det är
genomströmningen som sätter gränsen, inte väntan vid varje anrop.
Utskrifter är ett felsökningsverktyg, inte något som ska ligga kvar när
bygget är klart.
void setup() {
pinMode(2, INPUT_PULLUP);
Serial.begin(9600);
Serial.println("Start");
}
void loop() {
int lage = digitalRead(2);
Serial.print("Knapp: ");
Serial.println(lage);
delay(500);
}
Serial.begin(9600)hör hemma isetup(), en gång.Serial.print()skriver utan radbrytning,Serial.println()med.- Öppna seriemonitorn med förstoringsglaset uppe till höger i IDE:n, och kontrollera att den står på 9600.
- Att öppna seriemonitorn startar om kortet. Programmet börjar alltså om
från
setup()när du tittar – det är inte ett fel. - Håll dig till a–z i utskrifter. Å, ä och ö går ofta fram, men inte alltid, och en trasig bokstav mitt i felsökningen hjälper ingen.
I industrin och i vardagen
I industrin: en påsfyllningsmaskin väger upp varje sats
på en vågcell och räknar i gram. Läggs vikten i en int går det
utmärkt ända upp till 32 767 g, alltså 32,767 kg. Sedan tar det stopp: en
sats på 40 000 g blir 40 000 − 65 536 = −25 536 g, ett negativt värde som
styrsystemet tolkar som att skålen är tom. Maskinen fortsätter fylla. Det
är därför riktiga styrsystem har typvalet som en dokumenterad
konstruktionsfråga och inte som något programmeraren avgör i förbifarten –
i säkerhetskritiska system granskas det utifrån, precis som en ritning.
I vardagen: när musikvideon till Gangnam Style passerade
2 147 483 647 visningar slog YouTubes räknare i taket. Det talet är
231 − 1, alltså exakt vad en 32-bitars long
rymmer – samma sorts tak som vår int, fast sexton bitar
bredare. YouTube fick byta till 64 bitar, som rymmer ungefär 9,2
triljoner.
Skillnaden mot exemplet ovan är inte tekniken utan konsekvensen: där blev
det fel visningssiffra, här blir det en påse som fylls tills den rinner
över.
Prova själv
- Skriv en sketch som i
setup()deklarerarint sekunder = 60;och skriver ut bådesekunder * 1000och(long)sekunder * 1000till seriemonitorn. Titta på de två talen. Kunde du förutsäga dem? - Ta bort ett semikolon någonstans mitt i sketchen med flit och tryck Verifiera. Läs felmeddelandet och kontrollera: pekar det på raden där semikolonet saknades, eller på nästa?
- Ändra
Serial.begin(9600)tillSerial.begin(4800)men låt monitorn stå kvar på 9600. Ladda upp och titta. Så ser fel hastighet ut – lär dig känna igen bilden. - Skriv om blinkexemplet från förra guiden så att pinnummer och
blinktid ligger i
const int-konstanter överst i filen, med namn du själv valt.
Kontrollfrågor
Vad blir 32767 + 1 i en int på det här
kortet?
−32768. Talet viker runt till andra änden av området, eftersom 32767 är det största värde de sexton bitarna kan hålla. Det ger inget felmeddelande, varken vid kompilering eller vid körning – bara ett oväntat tal.
Du behöver hålla ett antal millisekunder som kan bli 100 000. Vilken typ väljer du?
long eller unsigned long. En int
tar slut vid 32767, alltså långt under 100 000. Kan värdet aldrig bli
negativt är unsigned long det naturliga valet, och det är
också den typ kortets egen tidräknare använder.
Varför står det void framför setup()?
Det är funktionens returtyp, och void betyder att den inte
lämnar tillbaka något värde. C++ kräver att varje funktion talar om vad
den ger tillbaka, till skillnad från Python där
def setup(): räcker. setup() gör saker, den
räknar inte ut något åt någon annan.
Seriemonitorn visar rader som ÿ?ÿ?? i stället för
dina siffror. Vad är fel?
Hastigheten stämmer inte mellan koden och monitorn. Kontrollera att
talet i Serial.begin() är samma som det som står i
monitorns rullgardin. Att det kommer något är själva ledtråden:
sladden fungerar, det är tolkningen av bitarna som är fel.
Vad är skillnaden mellan int vinkel = 20; och
const int VILOVINKEL = 20;?
Den första kan skrivas om medan programmet kör, den andra kan inte.
Försöker du ändra en const stoppar kompilatorn dig med ett
felmeddelande. Använd const för allt som är en inställning –
pinnummer, gränsvärden, fasta vinklar – och vanliga variabler för det
som faktiskt förändras medan systemet arbetar.
Hur lång tid tar det ungefär att skriva ut raden
Knapp: 1 vid 9600 baud?
Raden är nio tecken plus radbrytningen, alltså omkring tio tecken. Vid 960 tecken per sekund blir det 10 / 960 ≈ 0,010 s, alltså cirka 10 ms. I ett program som ska reagera på en knapp är det en märkbar tid.
Vanliga fel
- Semikolon glömt. Felet anmärks nästan alltid på raden efter den där det saknas. Titta uppåt, inte på raden meddelandet pekar ut.
- Variabeln deklarerad inuti
loop()men förväntas minnas. En variabel som skapas iloop()börjar om från sitt startvärde varje varv. Ska den minnas mellan varven måste den stå utanför funktionerna, överst i filen. - Typen glömd vid deklarationen.
antal = 5;utanintframför är ett kompileringsfel i C++, inte en ny variabel som i Python. =där det skulle stå==.if (lage = LOW)tilldelar i stället för att jämföra, och villkoret blir något helt annat än du tänkt. Kompilatorn varnar ibland men inte alltid.- Talet spiller över tyst. Inget felmeddelande, bara ett
värde som plötsligt är negativt eller orimligt litet. Ser du ett
oförklarligt negativt tal: kontrollera om något multiplicerats i en
int. - Fel baudhastighet i monitorn. Teckensallad, inte
tomhet. Tomhet betyder i stället att
Serial.begin()saknas eller att fel port är vald. - Utskrifter kvar i färdig kod. Varje tecken tar cirka
en millisekund. Ett bygge som reagerar trögt på en knapp har ofta ett
Serial.println()i varje varv som ingen tagit bort. - Måsvingar som inte går ihop. Ett saknat
}ger ett felmeddelande långt ner i filen, ofta på sista raden. Räkna vingarna, eller låt IDE:n formatera om koden och se var indraget hoppar fel.
Vidare
Kapitlen Typer och variabler och Från Python till C++ i språkreferensen hör till den här guiden. Slå upp dem mitt i ett bygge i stället för att bläddra tillbaka hit.
Nästa guide är digital ingång, där kortet för första gången får veta något om världen utanför.