Program innehåller ofta fel. För att hitta och avlägsna programfel behöver man testa och felsöka programmen. Testning och felsökning ("debuggning") är en hel vetenskap och vi ska inte dyka ner djupare i problematiken. Men några synpunkter, lärorika exempel och tips skadar nog inte.
Många fel kan undvikas redan från början genom användning av en god programmeringsmetod. Ett bra sätt att minimera antalet "buggar" och övriga fel och misstag är att testa programmet under det att det skrivs. Detta, s k inkrementell testning, är möjligt att utföra om programmet utvecklas genom stegvis förfining, på så sätt att man alltid har "fungerande kod": Efter varje nyskrivet avsnitt kompileras och testkörs programmet.
Fördelarna med inkrementell testning är flera:
Gången vid skrivning av program enligt denna modell blir i stora drag följande:
.... osv tills dess programmet är klart.
Vid konstruktionen av stora program kan det vara lämpligt att dela upp problemet ytterligare, dvs framta programdelar separat enligt samma modell. För att testköra dessa kan då speciella "testprogram" behövas. Specialskrivna testprogram är givetvis också på sin plats vid utarbetandet av biblioteksrutiner (inkluderingsfiler och enheter), liksom när speciellt komplicerade eller kritiska underprogram ska konstrueras och uttestas.
Programfel kan indelas i tre grupper: kompileringsfel, exekveringsfel och logiska fel.
Fel som uppträder vid kompileringen. Vem har inte råkat ut för sådana och retat sig på att kompilatorn bråkat över avsaknaden av ett semikolon eller någon annan "futtig detalj"? Men om vi ser nyktert på saken är kompilatorns stränghet en välsignelse, ty de fel som kompilatorn upptäcker är de som är lättast att lokalisera och rätta till. Därför bör man under programutvecklingsfasen välja kompileringsdirektiv så att kompilatorn ges möjlighet att fånga upp så många felaktigheter som möjligt (dessa direktiv kan avlägsnas när programmet är klart). Egendefinierade datatyper, i stället för standardtyper (Integer, Char, osv), kan också vara till hjälp, ty därmed erhålls skärpt typkontroll, vilket bidrar till att fel upptäcks på ett tidigt stadium.
Fel som leder till programkrasch under körning. Denna feltyp är värre än kompileringsfel, ty det kan vara svårt att lista ut vad som gått snett, även om man för det mesta får ledning av de felmeddelanden som ges vid sammanbrottet. Men i vilket fall som helst är det givetvis bättre att exekveringsfel uppträder under programutvecklingsfasen än att de "buggar" som orsakar felen slinker igenom till det färdiga programmet. Felaktigheter som upptäcks kan ju rättas till. Detta är en viktig anledning till att kompileringsdirektivet R+ bör utnyttjas under programutvecklingen.
Fel i algoritmen eller kodningen av densamma. Sådana fel yttrar sig i att programmet ger felaktiga resultat, dvs utför något annat än det avsedda. Detta är den värsta feltypen och kan ge fullständigt katastrofala resultat. Det enda botemedlet är egentligen en noggrann problemanalys, men det är inte tillräckligt, ty trots allt kan man ha förbisett någon väsentlig detalj. För att försäkra sig om att programmet fungerar som det ska måste det noga testas med väl valda testdata. Körning av programmet med debugger kan här vara till ovärderlig hjälp.
Här följer en rapsodi på mer eller mindre vanliga logiska fel. Vissa av exemplen kan förefalla "konstruerade", men vi försäkrar att de feltyper som illustreras har förekommit i praktiken (med erfarna programmerare inblandade) och i vissa fall gett upphov till många timmars huvudbry. Här har naturligtvis felen "renodlats" för att de ska bli uppenbara, men i mer komplicerade sammanhang kan det vara mycket svårt att hitta dem. Genom att vara uppmärksam på farorna kan mycket tidsödande felsökningsslit undvikas.
För det mesta spelar ett extra semikolon ingen roll, men i 2 fall kan de orsaka svårfunna fel: i if-then-else-konstruktioner och efter do.
Exempel:
sum:= 0;
while sum <= 5 do;
sum:= sum + 1;
Resultatet blir här en oändlig loop.
Vilken utskrift ger programmet nedan?
var
a: array [1..4] of Integer;
i: Integer;
begin
i:= 5;
a[i]:= 6;
WriteLn(a[i], i:10);
end.
Index 5 ligger utanför arrayens indexgränser, men kompilatorn protesterar inte. Vid exekveringen erhåller variabeln i värdet 6 !!! (detta beror på att den är deklarerad direkt efter arrayen). Utskriftssatsen skriver därför ut värdet av det icke-existerande arrayelementet a[6], dvs ett slumpvärde, samt talet 6.
Sådana fel kan alltså ge mycket underliga resultat. För att fånga upp denna feltyp, utnyttja R+-direktivet, vilket här ger exekveringsfel, och använd gärna också egendefinierade delintervall för index.
Exempel 1 (linjärsökning):
i:= 1;
(* Arrayen a har indexgränserna 1 och max *)
while (i<=max) and (a[i] <> sokt) do
i:= i+1;
if i <= max then
WriteLn(sokt, ' finns på plats ',i);
Här går allt fint om sokt finns i arrayen. Om inte så är fallet, då erhåller räknaren i värdet max+1 och test kommer att göras på (101<=max) and a[101] <> sokt, dvs vi hamnar utanför arrayen. I t ex C/C++ skulle testen avbrytas redan vid (101<=max), dvs det andra villkoret evalueras ej, men i Pascal är standardbeteendet att samtliga tester utförs i ett villkorsuttryck. I vissa sammanhang spelar detta ingen roll (i ovanstående exempel skulle sökningsrutinen fungera ändå, om bara max < maxInt), men i andra situationer kan detta leda till svårartade fel. Man bör därför vara medveten om problemet. Detta är f ö återigen ett fall där kompileringsdirektivet R+ skulle "säga ifrån".
Låt oss försöka eliminera problemet:
i:= 0;
while (i<100) and (a[i] <> sokt) do
i:= i + 1;
if i <= max ...
Tyvärr, denna lösning är sämre än den förra: Nu sker direkt test på i=0, som ligger utanför index-gränserna. Och i detta fall skulle vi - om olyckan vore framme - kunna få svaret att det sökta elementet finns i position 0!!! Detta skulle f ö alltid inträffa om sokt är deklarerad omedelbart före arrayen.
Det finns 2 sätt att klara ut det hela: med en vaktpost eller med en boolsk variabel (se avsnittet om sökning i detta kapitel).
Exempel 2 (binärsökning):
Testa följande variant av binärsökning:
const
max = 2; (* För test *)
type
ElementType = Integer;
ElementArray = array [1..max] of ElementType;
var
y: Integer;
a: ElementArray;
x, ix: Integer;
found: Boolean;
procedure BinSearch(s: ElementType;
n: Integer;
var ix: Integer;
var found: Boolean);
var
first, last, middle: Integer;
begin
first:= 0; last:= n + 1;
found:= false;
repeat
middle:= (first + last) div 2;
if (s > a[middle]) then
first:= middle + 1
else if (s < a[middle]) then
last:= middle - 1
else
found:= true;
until found or (first > last);
ix:= middle;
end; (* BinSearch *)
begin (* Testprogram *)
x:= 5;
y:= -5;
a[1]:= 1;
a[2]:= 2;
BinSearch(x, 2, ix, found);
if found then
WriteLn(x,' finns och har index', ix);
BinSearch(y, 2, ix, found);
if found then
WriteLn(y,' finns och har index: ',ix);
end.
Vilket resultat erhålls tror du - och varför? Ledning: Se kommentarerna till linjärsökningen ovan.
Loopar, i synnerhet nästlade sådana, tillhör de konstruktioner som bör ägnas extra eftertanke. Särskilt viktigt är att noga analysera extremfallen, dvs att se till att begynnelse- och termineringsvillkoren är korrekta.
Ett trivialt exempel:
var
i, j: Integer;
begin
j:= i+10;
WriteLn(i, j:10);
end.
Resultatet är oförutsägbart, ty variabeln i har ej tilldelats något värde. I sådana här fall ger tyvärr inte kompilatorn någon hjälp. Däremot finns specialprogram som fångar upp feltypen - se nedan.
Ett lurigare exempel:
var
i, j: Integer;
begin
Write('Skriv ett heltal: ');
ReadLn(i);
if i < 0 then j:= 5;
WriteLn(i, j:10);
end.
Om i>=0 erhåller inte variabeln j något värde. Kompilatorn upptäcker inte felet, inte heller finns (vad jag vet) något hjälpprogram som klarar av biffen. I villkorssatser (if-then-else, och case) faller det alltså helt på programmerarens lott att se till att inga variabler blir utan värden.
Fel av typen "oinitierade variabler" kan vara särskilt svårlokaliserade om de förekommer i underprogram (procedurer och funktioner) som man tror är ordentligt uttestade. När det gäller utparametrar och funktionsresultat bör man därför vara särskilt uppmärksam på att alla möjliga fall verkligen täcks:
function Sign(x: Real): Integer;
begin
if x > 0 then
Sign:= 1
else if x < 0 then
Sign:= -1
end; (* Sign *)
Om x har värdet 0 kommer funktionen Sign att returnera ett slumpvärde.
Exempel:
var
i: Integer;
a: array [1..3] of Integer;
n: Integer;
procedure Mystery;
begin
i:= 4;
end; (* Mystery *)
begin
n:= 100;
WriteLn('n=', n);
for i:= 1 to 3 do begin
if i = 3 then Mystery;
WriteLn('Index=', i);
a[i]:= i;
end; (* for *)
WriteLn('n=', n);
for i:= 1 to 3 do
Write('a[', i, ']=', a[i], ' ');
end.
Körning av programmet gav utskriften:
n=100 Index=1 Index=2 Index=4 n=4 a[1]=1 a[2]=2 a[3]=-15732
Här har variabeln n fått värdet 4, trots att den "borde" ha bibehållit värdet 100. Dessutom har a[3] fått ett "slumpvärde". Roten till det onda är anropet till proceduren Mystery. Där ges den globala variabeln i, som också används som styrvariabel i for-loopen, värdet 4, vilket ger alla följdfel. Med kompileringsdirektivet R+ skulle felet ha fångats upp (ger exekveringsfel). Om däremot i tilldelas ett värde som håller sig innanför arrayens index-gränser skulle inte ens R+ vara till någon hjälp. Då kan andra fel uppträda (beroende på vad som utförs i loopen). Den allra viktigaste lärdomen av detta exempel är därför att globala variabler bör undvikas: Använd lokala variabler när så är möjligt. Därmed kan oönskade sidoeffekter undvikas. Tänk särskilt på detta i samband med loopar, där det kan vara frestande att tillgripa globala variabler ("det är så jobbigt med en massa överflödiga deklarationer").
Exempel:
procedure SwapItems(item1, item2: DataType);
var temp: DataType;
begin
temp:= item1;
tem1:= item2;
item2:= temp;
end; (* SwapItems *)
SwapItem utför inte sin uppgift. Varför? Jo, parametrarna item1 och item2 har ej var-deklarerats. I ovanstående exempel upptäcker nog de flesta snabbt felet, men i större program kan det vara besvärligt, särskilt om parameterlistorna är långa.
Även motsatsen, dvs var-deklaration av parametrar som borde vara värdeparametrar, kan leda till svårupptäckta fel:
var
a: Real;
procedure WriteReal(var b: Real);
begin
a:= a + 1;
WriteLn(b:5:2);
end; (* WriteReal *)
begin
a:= Pi;
WriteReal(a);
end.
I huvudprogrammet vill man skriva ut a-värdet (här = Pi) med proceduren WriteReal. Av "effektivitetsskäl" var-deklareras parametern b. Inne i proceduren tilldelas variabeln a ett nytt värde och värdet av b skrivs ut, dvs 3.14 eller hur? Nej, här skrivs 4.14 ut: Eftersom b är var-deklarerad är a och b två olika namn för samma variabel. Variabeln b borde alltså inte ha var-deklarerats.
De flesta flyttal representeras inte exakt i datorn (gäller t ex decimalbråken 0.1, 0.2, 0.3 0.4, 0.6 etc), vilket kan leda till en hel del problem som man bör vara uppmärksam på.
Extra stor försiktighet bör iakttas i loopar där flyttal utnyttjas i villkorsuttryck. Speciellt bör relationsoperatorerna = och <> undvikas, eftersom risken är stor att slutvillkoret aldrig inträffar.
Följande program leder exempelvis till en oändlig loop:
program Realtest1;
var
number: Real;
begin
number:= 0.0;
repeat
WriteLn(number:0:10);
number:= number + 0.1;
until number = 1.0;
end.
Detsamma gäller programmet
program Realtest2;
var
number: Real;
begin
number:= 0.0;
while number <> 1.0 do begin
WriteLn(number:0:10);
number:= number + 0.1;
end; (* while *)
end.
Studera gärna värdet på variabeln number genom att köra programmen med den inbyggda debuggern.
För att försäkra sig om att loopar terminerar i sådana fall kan slutvillkoren i stället formuleras med relationsoperatorerna <, >, >= eller <=. Exempelvis kan villkoret i första programexemplet skrivas
until number >= 1.0;
och i det andra exemplet
while number <= 1.0 do begin
Men även i sådana sammanhang bör man vara på sin vakt, eftersom antalet gånger som en viss loop genomlöps kan variera. Den sistnämnda loopen kommer exempelvis att genomlöpas 11 gånger (slutvärdet på number blir större än 1), medan en liknande loop i ett annat sammanhang bara genomlöps 10 gånger. Av denna anledning är det bäst att när så är möjligt utnyttja heltalsvariabler i loop-tester.
Andra problem som kan uppträda vid flyttalsberäkningar är kancellation och utskiftning, vilka båda kan ge katastrofala resultat för noggrannheten av en numerisk beräkning.
Kancellation inträffar när 2 ungefär lika stora tal subtraheras från varandra:
1.234567881 - 1.234567882 = -0.000000001
De ursprungliga talen har båda 11 värdesiffror, medan differensen har bara 1 (en) värdesiffra! Inte nog med det. Vid en test där vi lät datorn utföra subtraktionen erhölls resultatet -9.98625E-10, vilket beror på att ursprungstalen inte representeras exakt.
Noggrannhetsförlusterna kan bli mycket stora vid sådana subtraktioner. För att reducera förlusten av signifikanta siffror beroende på kancellation är det ibland nödvändigt att omformulera beräkningsuttrycken. För en mer allsidig belysning och analys av problemet och hur det kan bemästras bör någon lärobok i numeriska metoder tillfrågas.
Utskiftning uppträder när tal av olika storleksordning adderas eller subtraheras. Programmet nedan illustrerar problemet:
var
dx: Real;
begin
dx:= 1.0;
while 1.0 <> 1.0+dx do begin
dx:=dx*0.5;
Write(dx);
end; (* while *)
end.
Med Borland Pascal 7.0 (och utan "Numeric Processing" aktiverat) avbryts programkörningen då dx fått värdet 4.5474735089E-13, vilket således inte ger något bidrag alls till 1.0. Vi har därmed erhållit total utskiftning och detta trots att vi valt dx så att det hela tiden representeras exakt i datorn. Partiell utskiftning, då bara en del av ett tal utskiftas inträffar givetvis tidigare. Orsaken till fenomenet är att precisionen för datatypen Real är sådan att den (högst) kan lagra 12 decimala siffror.
Om man inte tänker sig för kan stora beräkningsfel uppkomma av detta skäl, särskilt om ett stort antal små tal läggs till ett stort tal. Med ett dx-värde som är mindre än det ovan skulle summan förbli 1.0 hur många gånger vi än adderar dx till 1.0.
Ett sätt att reducera utskiftning är att först (addera/subtrahera) de små termerna.
För att beräkna summan av den s k harmoniska serien (1 + 1/2 + 1/3 + + 1/n) är det t ex bättre att addera "bakifrån":
harmSum:= 0.0;
for i:= n downto 1 do
harmSum:= harmSum + 1/i;
än att gå den "vanliga vägen":
harmSum:= 0.0;
for i:= 1 to n do
harmSum:= harmSum + 1/i;
Copyright © 2000 Martin Fahlgren
martin@1-1-7-46a.ghn.gbg.bostream.se[Till början av dokumentet] [Till artikelförteckningen]