← Kursens material C++ efter Python · område 2 Område 2

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

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:

TypStorlek på UnoRymmerAnvänds till
bool1 bytetrue eller falseav eller på, tryckt eller inte
byte1 byte0 … 255små positiva tal, till exempel ett pinnummer
int2 byte−32768 … 32767vanliga heltal: vinklar, räknare, mätvärden
long4 byte−2 147 483 648 … 2 147 483 647stora tal, och tid i millisekunder
float4 byteungefär 6–7 siffrors noggrannhetspä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:

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.

PythonC++
if x > 5:if (x > 5) {
indrag} avslutar
and · or · not&& · || · !
True · Falsetrue · 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);
}

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

  1. Skriv en sketch som i setup() deklarerar int sekunder = 60; och skriver ut både sekunder * 1000 och (long)sekunder * 1000 till seriemonitorn. Titta på de två talen. Kunde du förutsäga dem?
  2. 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?
  3. Ändra Serial.begin(9600) till Serial.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.
  4. 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

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.