Område 3 · lektion 12
Villkor i C++
Sanningstabellerna från förra passet ska nu skrivas som kod. Det är fyra tecken att lära sig, och ett av dem har en nästan identisk tvilling som gör något helt annat utan att kompilatorn säger ifrån. Den tvillingen är den här guidens huvudperson.
Mål
- Du kan använda
bool, och vet varför den tar en hel byte på ett Uno-kort trots att den bara rymmer två värden. - Du kan skriva AND, OR, NOT och XOR som villkor i C++ och kontrollera koden mot sanningstabellen rad för rad.
- Du kan förklara skillnaden mellan
&&och&, och känna igen symtomet när fel variant råkat komma med. - Du kan bygga ett villkor av två insignaler som styr en utgång, och sätta parenteser så att det betyder det du menar.
Genomgång
bool: en typ med två värden och åtta bitar
En bool rymmer true eller false,
ingenting annat. Den skrivs som vilken variabel som helst:
bool larmet_pa = false;
bool luckan_oppen = true;
Här kommer den detalj som förvånar: en bool tar
en byte, alltså åtta bitar, inte en bit. Skälet är att
chippets minne är adresserat i byte – den minsta bit av minnet som har en
egen adress är åtta bitar stor. En variabel som bara har två giltiga
värden kan alltså inte packas mindre än så utan extra arbete, och sju
åttondelar av utrymmet står tomt.
8 bitar − 1 använd bit = 7 outnyttjade bitar per bool
Det är inget att bekymra sig över i den här kursen. Unons ATmega328P har 2 048 byte arbetsminne, så tusen booler får plats med marginal. Men det förklarar varför du i datablad ser bitar packade i register i stället för i variabler: när utrymmet verkligen tar slut är åtta booler i en byte skillnaden.
Jämförelser är också bool
Varje jämförelse i C++ lämnar ett sanningsvärde. Du kan därför lägga undan resultatet i en variabel i stället för att upprepa jämförelsen, och koden blir läsbar på köpet:
bool a_nere = (digitalRead(KNAPP_A_PIN) == LOW); // LOW betyder nedtryckt
bool b_nere = (digitalRead(KNAPP_B_PIN) == LOW);
if (a_nere && b_nere) {
luckservo.write(VINKEL_OPPEN);
}
Den här formen är värd att ta för vana. Inversionen – att nedtryckt är
LOW – hanteras då på ett ställe högst upp, och
resten av koden handlar om knappar som är nere eller uppe. Blandas
inversionen in i varje villkor blir det förr eller senare fel i ett av
dem, och det villkoret är det du inte tittar på.
De tre operatorerna, mot tabellen
| Grind | C++ | Sant när | Exempel |
|---|---|---|---|
| AND | && | båda leden är sanna | a_nere && b_nere |
| OR | || | minst ett led är sant | a_nere || b_nere |
| NOT | ! | ledet är falskt | !a_nere |
| XOR | != | leden är olika | a_nere != b_nere |
Kontrollera koden mot tabellen genom att gå igenom raderna, inte genom att läsa villkoret och tycka att det ser rimligt ut. Fyra rader tar en halv minut:
a_nere | b_nere | a && b | a || b | a != b | !a && !b |
|---|---|---|---|---|---|
| false | false | false | false | false | true |
| false | true | false | true | true | false |
| true | false | false | true | true | false |
| true | true | true | true | false | false |
Sista kolumnen är NOR – "ingen av dem" – och den är sann i exakt en rad, precis som AND är, fast i den motsatta.
Ett et-tecken är inte två
Det här är områdets dyraste fel, och det är dyrt just för att ingenting
klagar. && och & är två olika
operatorer. Båda kompilerar, båda ger ett tal, och den ena gör inte det du
tror.
&&är logiskt och: det tittar på om leden är sanna eller falska, och lämnar sant eller falskt.&är bitvis och: det tar talen bit för bit, gör AND på varje bitpar för sig, och lämnar ett tal.
Med två knappvärden som ändå bara är 0 eller 1 ger de samma svar, och därför kan felet ligga kvar i månader. Så fort något av leden är ett annat tal än 0 eller 1 skiljer de sig. Ta 4 och 2:
| Bitposition | 8 | 4 | 2 | 1 |
|---|---|---|---|---|
| Talet 4 | 0 | 1 | 0 | 0 |
| Talet 2 | 0 | 0 | 1 | 0 |
4 & 2, bit för bit | 0 | 0 | 0 | 0 |
4 & 2 = 0 → falskt
4 && 2 = true → sant, eftersom båda talen är skilda från noll
Två tal som båda är sanna ger alltså falskt när fel operator används. Det finns ingen varning, ingen röd rad och ingen ledtråd i seriemonitorn – bara ett villkor som aldrig blir sant.
Samma sak gäller || och |, men där är
konsekvensen lömskare:
4 | 2 = 6 → sant, eftersom 6 är skilt från noll
4 || 2 = true → sant
Här ger den felaktiga varianten rätt svar, den här gången. Det är sämre än att den ger fel svar, eftersom felet då stannar kvar i koden tills dagen då det inte längre stämmer.
Kortslutningen är den andra skillnaden
&& slutar räkna så fort svaret är avgjort. Är
vänsterledet falskt läses högerledet aldrig. & räknar
alltid ut båda leden. Så länge leden bara är variabler märks det inte, men
så fort ett led är ett funktionsanrop – digitalRead(), en
mätning, något som tar tid – blir skillnaden verklig. Regeln är enkel:
i ett villkor skriver du alltid det dubbla tecknet. Det
enkla hör hemma i bitoperationer på register, och dit kommer du i
område 5.
XOR i C++ finns, men inte där du letar
Det finns && och ||, men det finns
inget ^^. C++ har ingen logisk
XOR-operator alls. Tecknet ^ finns, men det är
bitvis XOR, samma familj som & och
|.
För två bool-värden fungerar ^ ändå, eftersom
de bara kan vara 0 eller 1. Men samma fälla som ovan gäller: så fort du
råkar skicka in ett heltal som inte är 0 eller 1 blir svaret bitvis och
inte logiskt. Använd därför !=:
bool exakt_en_nere = (a_nere != b_nere); // logiskt XOR
Det läses "a är skilt från b", vilket är precis vad XOR-tabellen säger,
och det fungerar för alla värden en bool kan ha.
Två insignaler som styr en utgång
Nu sitter delarna ihop. Regeln nedan är den enklaste versionen av bygge 2:s logik: luckan öppnas när båda knapparna hålls nere, och stängs annars.
#include <Servo.h>
const int KNAPP_A_PIN = 2;
const int KNAPP_B_PIN = 3;
const int SERVO_PIN = 9;
const int VINKEL_STANGD = 15;
const int VINKEL_OPPEN = 100;
Servo luckservo;
void setup() {
pinMode(KNAPP_A_PIN, INPUT_PULLUP);
pinMode(KNAPP_B_PIN, INPUT_PULLUP);
luckservo.attach(SERVO_PIN);
luckservo.write(VINKEL_STANGD);
Serial.begin(9600); // behövs så fort något ska skrivas ut
}
void loop() {
bool a_nere = (digitalRead(KNAPP_A_PIN) == LOW); // LOW betyder nedtryckt
bool b_nere = (digitalRead(KNAPP_B_PIN) == LOW);
if (a_nere && b_nere) {
luckservo.write(VINKEL_OPPEN);
} else {
luckservo.write(VINKEL_STANGD);
}
}
Den här versionen duger som ett första steg och räcker inte som bygge 2. Två saker fattas, och båda kommer i de två följande guiderna: luckan bryr sig inte om när knapparna trycktes, och den minns ingenting mellan varven. Släpper du en knapp stänger den omedelbart, och tejpar du fast den ena knappen fungerar bygget med en hand. Precis den bristen är skälet till att en industripress har fler krav än "båda knapparna nere".
Parenteser, och vad som binder hårdast
Blandar du && och || i samma villkor
gäller en prioritetsordning, precis som att multiplikation går före
addition. Den är:
! binder hårdast, sedan &&, sedan ||
Det betyder att a || b && c läses som
a || (b && c). Det kan vara vad du menade, och det
kan vara raka motsatsen. Sätt parenteser även när de är överflödiga –
koden blir längre och tvetydigheten försvinner:
if ((vaxel_i_p || vaxel_i_n) && bromsen_nere) {
startmotorn_far_ga = true;
}
Utan parenteserna hade villkoret betytt "växeln står i P, eller också står den i N och bromsen är nere" – vilket hade låtit bilen startas i P utan fot på bromsen.
I industrin och i vardagen
I industrin: en PLC – den styrenhet som sitter i nästan varje maskin – programmeras oftast i ladderdiagram, ett språk som ser ut som ett elschema och inte som text. Två lodräta linjer är matningen, och varje vågrät rad kallas en slinga. Kontakter i serie är AND. Kontakter parallellt är OR. Spolen längst till höger är utgången.
- Den övre slingan är AND. Strömmen når spolen K1 bara om både A och B är slutna, eftersom kontakterna sitter efter varandra.
- Den nedre slingan är OR. Strömmen når K2 om A eller B är sluten, eftersom det finns två vägar fram.
- Kontaktsymbolen är två korta lodräta streck med ett glapp emellan – samma symbol vare sig det är en fysisk knapp eller ett internt minne i styrenheten.
- Spolen ritas som två bågar. I ett gammalt reläskåp var det en verklig spole. I en PLC är det en variabel som skrivs.
Poängen för dig: samma logiska funktion som du skriver
a && b ritas i industrin som två kontakter efter
varandra. En underhållselektriker läser din regel snabbare som en ritning
än som en textrad, och det är därför språket ser ut som det gör. Det är
också därför sanningstabellen är det som gäller när ni jämför: den ser
likadan ut oavsett vilket av de två språken funktionen är skriven i.
I vardagen: en automatväxlad bil startar inte hur som helst. Villkoret är att växelväljaren står i P eller N och att bromspedalen är nedtryckt. Skrivet som kod är det exakt raden med parenteser ovan. Räknar man de tre signalerna som oberoende blir det 2³ = 8 rader i tabellen, men två av dem – de där växeln påstås stå i både P och N samtidigt – kan aldrig inträffa. Kvar blir sex möjliga lägen, och i två av dem får startmotorn gå:
| P | N | Broms | Startar |
|---|---|---|---|
| 0 | 0 | 0 | nej |
| 0 | 0 | 1 | nej, växeln står i ett körläge |
| 0 | 1 | 0 | nej, foten saknas |
| 0 | 1 | 1 | ja |
| 1 | 0 | 0 | nej, foten saknas |
| 1 | 0 | 1 | ja |
Det är två genuint olika sorters krav i samma villkor: växelläget är en mekanisk förutsättning för att bilen inte ska rulla, och bromspedalen är ett krav på att en människa är på plats. Den som bara läser koden ser tre likadana signaler. Den som läser bilen ser skillnaden, och det är den skillnaden du ska kunna beskriva i dokumentationen till bygge 2.
Prova själv
- Ladda upp exemplet med de två knapparna och servot ovan. Kontrollera att luckan bara öppnas när båda knapparna hålls nere.
- Byt
&&mot||och prova igen. Skriv ner i en mening hur beteendet ändrades, innan du byter tillbaka. - Byt till
!=så att luckan öppnas när precis en knapp är nere. Kontrollera alla fyra kombinationerna mot XOR-raderna i tabellen. - Skriv ut ett tal i stället: låt
Serial.println(4 & 2);ochSerial.println(4 && 2);stå efter varandra isetup(). Du ska få 0 och 1. Det är hela felet, synligt på två rader. - Bygg villkoret
(a_nere || b_nere) && !larmmedlarmsom en tredje knapp på D4. Rita sanningstabellens åtta rader först, på papper, och bocka av dem mot bygget efteråt. - Ta bort parenteserna i föregående steg och pröva igen. Hitta alla rader i tabellen där beteendet skiljer sig, och skriv ner vilka. Det är fler än en.
Kontrollfrågor
Vad blir 4 & 2, och vad blir
4 && 2?
4 & 2 = 0. Talen är 0100 och 0010 binärt; ingen
bitposition har etta i båda, så resultatet blir 0000. 4
&& 2 är true, eftersom båda talen är
skilda från noll och logiskt och därför är sant. Samma två tal, motsatt
svar.
Varför är det farligare när | råkar användas i
stället för || än när & råkar användas i
stället för &&?
Därför att | ofta ger rätt svar av misstag. 4
| 2 = 6, som är skilt från noll och alltså sant – samma
svar som || hade gett. Ett fel som ger rätt svar upptäcks
inte, och det ligger kvar tills ett talpar dyker upp där svaren
skiljer sig. Ett fel som ger fel svar direkt är billigare.
Hur skriver du "precis en av knapparna är nedtryckt" i C++?
a_nere != b_nere. Det är logiskt XOR. Skriv inte
^^ – den operatorn finns inte i C++ och ger ett
kompileringsfel. ^ finns men är bitvis, och bör sparas till
register.
Hur stor är en bool på ditt kort, och hur många
bitar av den används?
En byte, alltså 8 bitar, varav 1 används och 7 står tomma. Skälet är att minnet är adresserat i byte: en enskild bit har ingen egen adress. Med 2 048 byte arbetsminne i ATmega328P är det inget problem i den här kursen.
Vad betyder a || b && c utan
parenteser?
a || (b && c), eftersom &&
binder hårdare än ||. Menade du
(a || b) && c är det ett annat villkor med annat
beteende. Sätt parenteserna själv, så slipper du komma ihåg
ordningen.
Ditt villkor if (a_nere & b_nere) fungerar
perfekt i bygget. Är det ändå fel?
Ja. Det fungerar bara därför att en bool aldrig är något
annat än 0 eller 1, så bitvis och logiskt och råkar sammanfalla. I
samma sekund som något av leden byts mot ett heltal – ett mätvärde, en
räknare, ett läge – slutar det fungera, och felet ser då ut att sitta i
det nya och inte i den gamla raden. Rätta det medan du minns varför.
Vanliga fel
- Ett et-tecken i stället för två. Villkoret blir
aldrig sant, eller blir sant vid fel tillfälle, och kompilatorn säger
ingenting. Sök igenom din kod efter ensamma
&och|innan du felsöker något annat. ^^skrivs i tron att XOR finns. Det är ett kompileringsfel, vilket är tur. Använd!=.- Inversionen upprepas fel.
digitalRead(A) == LOWpå ett ställe ochdigitalRead(B) == HIGHpå ett annat, i tron att båda betyder nedtryckt. Läs in båda tillbool-variabler överst iloop()och slipp problemet. - Ett likhetstecken i villkoret.
if (a_nere = true)tilldelar i stället för att jämföra och är alltid sant. Skriv hellreif (a_nere)– då kan felet inte uppstå. - Parenteser saknas när
&&och||blandas. Koden gör något rimligt, bara inte det du menade. Skillnaden syns ofta i bara ett par av tabellens rader – i bilexemplet ovan i en enda, den där växeln står i P utan fot på bromsen – och just därför märks den inte förrän den raden inträffar. - Villkoret testas i huvudet i stället för mot tabellen. Fyra rader tar en halv minut att gå igenom. Ett villkor som "ser rätt ut" har fällt fler byggen än något annat i det här området.
- Servot skrivs varje varv. Exemplet ovan anropar
write()i varje loopvarv, vilket fungerar men gör servot surrigt och strömtörstigt. Från och med nästa guide har du verktygen för att bara skriva när något faktiskt ändrats.
Vidare
Kapitlet Villkor i
språkreferensen är den här guidens uppslagsdel, och
Typer och variabler innehåller
bool och de övriga storlekarna.
Nästa guide är Tid utan delay. Där löses den brist som exemplet ovan har: koden vet ingenting om när något hände, och kan därför inte kräva att två knappar trycks nära varandra i tiden.