← Kursens material Villkor i C++ · område 3 Område 3

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

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

GrindC++Sant närExempel
AND&&båda leden är sannaa_nere && b_nere
OR||minst ett led är santa_nere || b_nere
NOT!ledet är falskt!a_nere
XOR!=leden är olikaa_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_nereb_nerea && ba || ba != b!a && !b
falsefalsefalsefalsefalsetrue
falsetruefalsetruetruefalse
truefalsefalsetruetruefalse
truetruetruetruefalsefalse

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.

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:

Bitposition8421
Talet 40100
Talet 20010
4 & 2, bit för bit0000

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.

Två slingor i ett ladderdiagram mellan två lodräta matningslinjer. Den övre slingan har två kontakter A och B efter varandra i serie och därefter en spole K1. Den nedre slingan har kontakterna A och B parallellt över varandra, med en gemensam väg fram till spolen K2. A B K1 A B K2

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å:

PNBromsStartar
000nej
001nej, växeln står i ett körläge
010nej, foten saknas
011ja
100nej, foten saknas
101ja

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

  1. Ladda upp exemplet med de två knapparna och servot ovan. Kontrollera att luckan bara öppnas när båda knapparna hålls nere.
  2. Byt && mot || och prova igen. Skriv ner i en mening hur beteendet ändrades, innan du byter tillbaka.
  3. Byt till != så att luckan öppnas när precis en knapp är nere. Kontrollera alla fyra kombinationerna mot XOR-raderna i tabellen.
  4. Skriv ut ett tal i stället: låt Serial.println(4 & 2); och Serial.println(4 && 2); stå efter varandra i setup(). Du ska få 0 och 1. Det är hela felet, synligt på två rader.
  5. Bygg villkoret (a_nere || b_nere) && !larm med larm som en tredje knapp på D4. Rita sanningstabellens åtta rader först, på papper, och bocka av dem mot bygget efteråt.
  6. 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

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.