Feilmeldinger

Innhold

Introduksjon

Når man skriver kode klarer man ikke alltid å unngå at det av og til enten ikke fungerer som man ønsker, eller at man ikke får kjørt programmet i det hele tatt. Derfor er det lurt å ha en ide om hva man kan gjøre hvis dette skjer. I dette dokumentet vil vi forklare mange av feilene som kan oppstå når man programmerer C++, og hvordan man kan fikse disse.

Når man skriver kode, er det lurt å teste ofte. Gjerne etter hver funksjon man lager. Hvis man gjør dette slipper man å rette masse feil når man tror programmet er ferdig. Det er enklere å finne feil i en liten del av programmet enn å måtte lete i hele programmet.

Vær oppmerksom på at vi ikke forklarer eller kommenterer alle detaljer. Dette dokumentet er ment som en guide til hvordan du skal rette feil i koden din på egenhånd, og det som blir beskrevet her er i de aller fleste tilfeller nok til å oppnå dette. Vi forklarer det du skal se etter, og det som ikke blir kommentert er ikke relevant eller en del av emnet.

Ord og uttrykk

Vi begynner med å forklare en del ord og uttrykk som brukes mye i dette faget. Det er viktig å merke seg forskjellen på disse spesielt når man skal løse oppgaver.

Bygging av et program

Å bygge et program består av tre deler; pre-prosessering, kompilering og linking. Pre-prosessering går ut på at alle linjer med # foran blir byttet ut med det den linjen representerer. F. eks. hvis det står #include "std_lib_facilities.h", blir denne linjen byttet ut med innholdet i std_lib_facilities.h. Kompileringsdelen består av å oversette hver fil til lavnivå assembly kode. Assembly er ikke relevant for faget, men kompilatoren er avhengig av at all kode i C++ er skrevet «etter reglene» (blant annet semikolon på slutten av linjer og balanserte parenteser) for at den skal få til å oversette koden til assembly. Til slutt linkes filene, som betyr at alle filene blir satt sammen til en kjørbar fil.

Et prosjekt

Nå som vi har oversikt over hvordan et program bygges, og hva noen ord og uttrykk betyr kan vi se på hvordan et prosjekt kan se ut. Vi deler ofte opp programmene våre i flere filer. Dette gir oss bedre oversikt over koden. I C++ har vi to forskjellige filtyper; headerfiler (.h/.hpp) og implementasjonsfiler (.cpp). I headerfiler plasserer vi deklarasjoner og andre ting vi vil at andre filer skal ha tilgang på, f.eks. funksjonsdeklarasjoner enum-klasser, klasse-deklarasjoner og konstante map. I implementasjonsfiler implementerer/definerer vi funksjoner.

Når vi skriver kode er det anbefalt at hver implementasjonsfil har en tilhørende headerfil. Dette gjør det lettere å holde oversikt over koden du har skrevet. F.eks. har du en headerfil som heter myfunctions.h der funksjonsdeklarasjonene står, og en tilhørende implementasjonsfil, myfunctions.cpp der funksjonene blir implementert. Hovedfilen(main.cpp) er en spesiell implementasjonsfil som inneholder mainfunksjonen, og ofte kun main-funksjonen. Dette er den eneste implementasjonsfilen som ikke har en tilhørende headerfil. Når du kjører et program er det main() som blir kjørt.

Hvorfor må vi egentlig ha begge deler? Hvorfor kan vi ikke bare ha implementasjonsfiler og så inkludere disse? Grunnen til at vi ikke kan gjøre dette er fordi da ville de fleste funksjoner fått mer enn en definisjon. Vi viser dette med et eksempel.

		

// Implementasjonsfil (example.cpp)

int add(int a, int b){
	return a + b;
}				

	
		

// Hovedfil (main.cpp)

#include "example.cpp"
int main() {
	int c = add(5, 3);
}				

	

Når programmet bygges vil først linjen #include "example.cpp" i hovedfilen byttes ut med innholdet i example.cpp. Hovedfilen vil da bli seendes slik ut:

		

// Hovedfil (main.cpp)

int add(int a, int b){
	return a + b;
}
int main(){
	int c = add(5, 3);
}		

	

Dette ser egentlig ikke så ille ut, men husk på at vi fortsatt har example.cpp. Så når filene skal linkes (settes sammen til en kjørbar fil) så ser linkeren at det finnes to definisjoner av add(), en i hovedfilen og en i implementasjonsfilen. Dermed feiler byggingen av programmet.

En positiv ting med å ha både implementasjonsfiler og headerfiler er at det er enkelt å se hvilke funksjoner som finnes uten å måtte lese all koden. For å se hvilke funksjoner som eksisterer kan du se på funksjonsdeklarasjonene i headerfilene. Du trenger heller ikke vite hvordan en funksjon er implementert for å kunne bruke den. Du trenger kun å vite funksjonsprototypen.

Når vi oppretter et prosjekt i Visual Studio Code (VScode) er det viktig at vi gjør det som beskrevet i øving 0. Hvis vi ikke gjør det, vil ingenting fungere.

Feil

Vi kan dele feil fra C++ inn i fire kategorier:

De to første feilene fører til at programmet ikke vil bygges (ofte bruker vi ordet kompilere her, selvom det av og til er feil å si). Du vil få en feilmelding i terminalen som sier hvor mange feil (eng: error) du har, og hvis du leter litt i terminalen vil du finne hva slags type feil de er. Disse feilmeldingene kan virke overveldende og rotete, men vi skal forklare hva du skal se etter når du får en feilmelding. Kjøretidsfeil er feil som fører til at programmet krasjer under kjøring. Og logiske feil fører til at programmet ikke har ønsket oppførsel.

Eksempel

Først må man vite hvor man finner feilmeldinger. Dette gjør man i den interne terminalen i VScode. Vi skal begynne med å se på et enkelt eksempel. Koden under vil ikke bygges fordi det mangler et semikolon på linje 4.

		

#include "std_lib_facilities.h"
int main()
{
	int a
}

	

Windows

Could not load image

Vi skal først gå gjennom eksempelet på Windows. Deretter går vi gjennom hvordan det ser ut på Mac. Figuren over viser terminal utskriften man får når byggingen feiler i Windows. Hvis man skroller lenger opp i terminalvinduet kan man se hvilke spesifikke feilmeldinger man har.

Could not load image

Som du ser, er error markert med rødt. Du ser kanskje også at feilmeldingen sier at vi mangler et semikolon. Linjen C:\Users\eiler\Desktop\project\main.cpp(4,7) forteller oss i hvilken fil, på hvilken kodelinje og hvor på linjen feilen er. Her er feilen i filen main.cpp, på linje 4 og som tegn nummer 7. Hvis man trykker på C:\Users\eiler\Desktop\project\main.cpp(4,7) samtidig som man holder inne Ctrl vil man bli ført til kodelinjen med feilen.

Når vi har enkle syntaksfeil, som her får vi en rød strek under feilen. I tillegg vil det dukke opp en feilmelding i PROBLEMS-fanen. Merk at de røde strekene og feilmeldingene i PROBLEMS ofte oppdaterer seg litt tregt, så ikke få panikk hvis de ikke forsvinner med en gang du retter feil.

Could not load image

Figuren over viser PROBLEMS-fanen med samme feil som i sted. Legg merke til at den forventer et semikolon som første tegn på linjen under der semikolonet mangler.

Mac

Nå skal vi se på hvordan en feilmelding ser ut på Mac.

Could not load image

Figuren over viser hvordan feilmeldingen for koden vil se ut på Mac. Hvis du ser på den øverste linjen så står det: main.cpp:4:7:. Dette forteller i hvilken fil, på hvilken kodelinje og hvor på denne linjen feilen er. Feilen er i main.cpp, på linje 4, som tegn nummer 7. Feilmeldingen sier at det mangler et semikolon som siste tegn på linje nummer 4.

Could not load image

Når man har enkle syntaksfeil som i eksemplet vil det dukke opp en rød strek under koden som er feil, og det vil dukke opp feilmeldinger i PROBLEMS-fanen i VScode. Figuren over viser hvordan PROBLEMS-fanen ser ut på Mac. Legg merke til at den sier at det er to feil, selvom det bare er en feil. Det mangler kun et semikolon. Feilmeldingene i PROBLEMS-fanen oppdatere seg ofte litt tregt så ikke få panikk hvis feilmeldingen ikke forsvinner med en gang du retter feilen.

Videre i dokumentet vil vi for det meste vise feilmeldinger i Windows. Dette er fordi det er enklere å lese feilmeldinger på Mac. Hvis du klarer å lese feilmeldingen man får i Windows vil du også klare å lese feilmeldingene du får på Mac.

Komplileringsfeil

Kompileringsfeil er feil som blir oppdaget under kompileringssteget av byggingen. Disse feilene er ofte syntaksfeil og typefeil. Syntaksfeil er ganske selvforklarende, det er feil som oppstår pga. gal syntaks. F.eks. at det mangler et semikolon, eller at det står Double istedenfor double. Typefeil er feil som oppstår når man prøver å gjøre ting som ikke er definert for en type. F.eks. å skrive en vector til skjerm direkte.

		

vector<int> vec = {2, 4, 3, 0, 4, -4};
cout << vec; // ikke definert og vil gi feilmelding

	

Noen eksempler

Eksempel: syntaksfeil

		

include "std_lib_facilities.h"
int main()
{
	vector<int> a;
	for(int i = 0; i < a.size; ++i){
		a.push_back(i);
	}
}			

	

Hvis man bygger koden over vil man få følgende feilmelding:

Could not load image

Som du ser, er denne feilmeldingen litt vanskelig å tyde. Hva betyr egentlig «reference to non-static member function must be called» ? Det betyr at size ikke er en funksjon, ikke en non-static member function som det blir kalt her. Man kan også se av feilmeldingen at den gir et forslag på hva som mangler, parenteser. Dette viser at det ikke er nødvendig å forstå alt som står i feilmeldingen for å kunne rette feilen.

Eksempel: feil bruk av operator

		

#include "std_lib_facilities.h"
const map<string, int> stringToInt = {{"one", 1}, {"two", 2}};
int main()
{
	cout << stringToInt["one"];
}

	

Koden over gir følgende feilmelding:

Could not load image

Som du ser, er linje nummer to i feilmeldingen ganske vanskelig å lese. basic_string<char, char_traits<char> , allocator<char> er bare et annet navn for string, og vi kan overse det når vi leser feilmeldingen. Denne feilmelding sier at det ikke er lov å bruke []-operatoren på et konstant map («no viable overloaded operator[] for type ’const map<std::string, int>»). Når vi bruker []-operatoren kan vi endre på innholdet i map-et og dette er ikke lov siden det er konstant. For å rette opp i denne feilmeldingen må vi bruke .at() istedenfor []-operatoren, som ikke kan endre på innholdet i map-et, når det er konstant.

Eksempel: glemt å inkludere header

		

int main()
{
	cout << "Hello World!\n";
}

	
Could not load image Could not load image

Vi ser her at feilmeldingene sier at cout er udefinert.

Merk: Når vi kommer litt ut i semesteret skal vi slutte å bruke std_lib_facilities. Da må du passe på å inkludere de bibliotekene du trenger.

Linkerfeil

Linkerfeil er feil som oppstår i det siste steget av byggingen, linkersteget. Når alle filene blir satt sammen til en kjørbar fil. Noen av feilene som kan dukke opp her er redefinisjon av funksjoner, skrivefeil, definering av funksjoner i headerfiler og sirkulærinkludering. Det er her man ofte får de vanskeligste feilmeldingene.

Noen enkle eksempler

Vi skal begynne med å se på noen enkle eksempler på linkerfeil for å få en ide om hva vi skal se etter. Senere skal vi se på noen litt vanskeligere eksempler.

Eksempel: skrivefeil

		

// Headerfil (example.h)

#pragma once
int add(int a, int b);

	
		

// Hovedfil (main.cpp)

#include "example.h"
int main(){
	int c = add(5, 3);
}

	
		

// Implementasjonsfil (example.cpp)

#include "example.h"
int ad(int a, int b){
	return a + b;
}

	

I eksempelet over er funksjonen add() deklarert i headerfilen, men det er ad(), som er definert i implementasjonsfilen. Når vi kaller add() i main-funksjonen vil vi få en linkerfeil. Merk at vi her ikke får opp noe i PROBLEMS-fanen. Dette er fordi hovedfilen har inkludert example.h der add() er deklarert. Dermed tror hovedfilen at funksjonen er definert

Could not load image

Vi får feilmeldingen over hvis vi prøver å bygge programmet. Legg merke til at etter error i rødt står det at linkingen feilet, men ikke hvor eller hva som har feilet. Hvis vi ser litt lenger opp kan vi se at feilen oppstår i main.obj, objekt-filen til main, som er filen man får etter kompileringssteget av byggingen er ferdig. Feilmeldingen sier at hovedfilen ikke har tilgang på definisjonen av add() ved å si «unresolved external symbol "int _cdecl add(int,int)"».

I dette eksemplet dukker feilen opp i hovedfilen, siden det er der vi kaller funksjonen. Men den egentlige feilen er at det mangler en d i add i implementasjonsfilen. Så vær oppmerksom på at linkerfeilmeldingene du får for det meste sier fra om at det ikke fungerer og hvorfor, men ikke hvordan du skal rette feilen.

Hvis man får en slik feilmelding, der det står: «unresolved external symbol», er som oftest problemet at man har skrevet funksjonsprototypen (i dette tilfellet int add(int a, int b)) feil i implementasjonsfilen. For å unngå at dette skjer med deg er et lite triks å kun skrive prototypen i headerfilen og så kopiere den til implementasjonsfilen. Du kan også få denne feilmeldingen hvis du har glemt å definere funksjonen.

Eksempel: redefinisjonsfeil (f.eks. dobbel main-funksjon)

		

// Headerfil (example.h)

#pragma once
int add(int a, int b);

	
		

// Hovedfil (main.cpp)

#include "example.h"
int main(){
	int c = add(5, 3);
}

int add(int a, int b){
	return a + b;
}	

	
		

// Implementasjonsfil (example.cpp)

#include "example.h"
int add(int a, int b){
	return a + b;
}

	

I koden over er funksjonen add() definert i både main.cpp og example.cpp. Definisjonene er like, men det vet ikke linkeren. Man får følgende feilmelding hvis man prøver å bygge programmet.

Could not load image

Siden det er en linkerfeil må man se på linjene over den røde error-meldingen. Det er litt ekstra informasjon der som vi ikke er interessert i. I figuren under er det viktigste markert med røde firkanter.

Could not load image

Da er det litt lettere å se at problemet ligger i hovedfilen (main), og at add() allerede er definert i example.cpp. Når du ser teksten «one or more multiply defined symbols found» så vet du at du enten har definert en funksjon flere ganger, definert en funksjon i en headerfil, eller definert en ikke-konstant variabel i en headerfil.

På Mac står det istedenfor «duplicate symbol» etterfulgt av funksjonsnavnet/variabelnavnet i feilmeldingen. Se neste eksempel for mer informasjon

Eksempel: definering av funksjon i headerfil

		

// Headerfil (example.h)

#include "std_lib_facilities.h"
int add(int a, int b){
	return a+b;
}

	
		

// Hovedfil (main.cpp)

#include "std_lib_facilities.h"
#include "example.h"
int main(){
	int n = add(1,2);
}

	
		

// Implementasjonsfil (example.cpp)

#include "example.h"

	

I programmet over har vi definert funksjonen add i headerfilen example.h. Dette fører til følgende feilmelding. Merk at feilmeldingen vises på Mac.

Could not load image

Under vises samme feilmelding, men her er de delene du skal bry deg om markert med røde firkanter

Could not load image

Det første vi ser (i boksen bak error) er at det er linkingen som har feilet. Deretter står det i den øverste boksen at funksjonen add er definert flere ganger (duplicate symbol). Og i den siste boksen står det hvilke filer den er definert i. Når vi inkluderer headerfilen i example.cpp og i main vil definisjonen til add bli limt inn og dermed blir den definert flere ganger, som vi har sett tidligere kan man ikke redefinere funksjoner. For å unngå dette må vi definere funksjonene våre i implementasjonsfilene, og ikke i headerfilene.

Merk: Hvis du leser dette når du har kommet litt lenger i faget stusser du kanskje på at funksjoner ikke kan defineres i headerfiler, vi gjør jo av og til det med enkle medlemsfunksjoner, dette har å gjøre med noe som heter inline og er ikke pensum i dette faget.

Noen litt vanskeligere eksempler

Til nå har vi for det meste sett på eksempler med datatypen int. Disse feilmeldingene er nå ganske rett frem å lese når man vet hva man skal se etter. Dette er fordi int er en innebygd datatype, du kan se hvilke typer som er innebygde i §A.8 i læreboken. Andre typer som string, vector og map har ofte andre navn i feilmeldingene, f.eks. string er std::basic_string<char>. Dette er pga. måten språket er bygd opp på. Merk at dette ikke er en del av pensum. Dette blir nevnt for at det skal være enklere å lese feilmeldinger. Vi skal nå se på noen eksempler med andre typer.

Eksempel: feil parameter

		

// Headerfil(example.h)

#pragma once
#include "std_lib_facilities.h"
void printVector(const vector<string>& vec);			

	
		

// Implementasjonsfil(example.cpp)

#include "example.h"
void printVector(const vector<string> vec){
	for(const auto& v: vec){
		cout<< v <<'n';
	}
}			

	
		

// Hovedfil(main.cpp)

#include "example.h"
int main()
{
	vector<string> v =
	{"one", "two", "three"};
	printVector(v);
}

	

I koden over er funksjonsprototypene til printVector forskjellige i deklarasjonen og definisjonen. Deklarasjonen tar inn en konstant referanse til en vector<string>, og definisjonen tar inn en konstant vector<string>. Koden gir oss denne feilmeldingen:

Could not load image

Her er det litt verre å finne ut hva som er galt. Men hvis man ser på første linje står det «unresolved external symbol» så da vet vi at det enten er snakk om at prototypene til deklarasjonen og implementasjonen er forskjellige, eller at funksjonen ikke er definert. Og hvis vi ser litt lenger bort på den samme linjen står det «printVector». Så det er denne funksjonen det er noe galt med. Resten av feilmeldingen kan sees bort fra. Vi fokuserer kun på det som er markert med rødt i figuren under.

Could not load image

Eksempel: sirkulærinkludering

		

// Hovedfil(main.cpp)

#include "std_lib_facilities.h"
#include "Person.h"
#include "Family.h"
int main(){
	Person per{"Per", 21};
	vector<Person> p{per};
	Family madsen{"Madsen", p};
}

	
		

// Headerfil(Family.h)

#include "std_lib_facilities.h"
#include "Person.h"
struct Family{
	string surName;
	vector<Person> members;
};		

	
		

// Headerfil(Person.h)

#include "std_lib_facilities.h"
#include "Family.h"
struct Person{
	string name;
	int age;
};

	

Over er det vist tre filer main.cpp, Person.h og Family.h, Person og Family har tilhørende .cpp filer som ikke er vist. For å gi filene tilgang til hverandre bruker vi #include i toppen av filene. Her er det inkludert slik at alle filene skal ha tilgang til hverandre, men når programmet bygges skjer dette

Could not load image

Det kommer ekstremt mange feilmeldinger, dersom vi ser nøyere på terminalen ser vi at det er mye repetisjon. Det står «Family.h included multiple times» og dersom vi blar lengre opp ser vi også «Person.h included multiple times». Vi har laget sirkulærinkludering av filene våre ved at Family og Person inkluderer hverandre. I dette programmet trenger ikke Person å inkludere Family fordi Family ikke brukes her, derfor bør #include "Family.h" fjernes fra Person. For å unngå sirkulærinkludering er det vanlig å starte alle .h-filer med #pragma once, det forteller kompilatoren at .h-filen bare skal inkluderes én gang i .cpp-filen. I denne situasjonen vil det ikke løse problemet vårt, vi får nå opp denne feilmeldingen som forteller oss at Family.h ikke finner Person.

Could not load image

Grunnen til dette er at Person er inkludert før Family i main, så når Family prøver å inkludere Person vil #pragma once stoppe det fordi den allerede er inkludert en gang. Vi trenger ikke å inkludere Person i main, når den er inkludert i Family vil main ha tilgang til Person når Family inkluderes. Dersom vi fjerner #include "Person.h" i main kjører programmet som forventet. En god huskeregel når det gjelder «includes» er å bare inkludere det man trenger.

Eksempel: ikke konstant i headerfil

		

// Headerfil(example.h)
#pragma once
#include "std_lib_facilities.h"
enum class Animal{Dog, Cow, Sheep};
map<Animal, string> animalToString
{{Animal::Dog, "Dog"},{Animal::Cow, "Cow"},
{Animal::Sheep, "Sheep"}};

void printAnimal(Animal a);

	
		

// Implementasjonsfil(example.cpp)
#include "example.h"
void printAnimal(Animal a){
	cout << animalToString.at(a) << '\n';
}

	
		

// Hovedfil(main.cpp)

#include "example.h"
int main()
{
	Animal a = Animal::Sheep;
	printAnimal(a);
}

	

I koden over har vi definert et map i headerfilen. Når headerfilen (example.h) blir limt inn i både hovedfilen og implementasjonsfilen blir animalToString definert flere ganger. Og vi får følgende feilmelding:

Could not load image

Figuren under viser den samme feilmelding med det du skal se etter markert med røde firkanter.

Could not load image

Som du ser sier feilmeldingen at animalToString som er definert i hovedfilen allerede er definert i example.cpp. Hvis vi markerer map-et som konstant, ved å skrive const map<Animal, string> istedenfor map<Animal, string>, vil linkeren forstå at definisjonen av animalToString kun skal tas med en gang i linkingen. Akkurat hvordan dette forgår er ikke så viktig. Det viktige her er at du kun kan ha konstante definisjoner av variabler i headerfiler. Siden map-et nå er konstant kan vi ikke endre på det. Grunnen til at vi har map og andre variabler i headerfiler er at vi har lyst til å ha tilgang på de i flere filer.

Kjøretidsfeil

En kjøretidsfeil er en feil som oppstår når programmet kjører. Disse feilene fører til at programmet terminerer/krasjer under kjøringen. Det som gjør kjøretidsfeil vanskelig å håndtere er at kompilatoren ikke gir oss noe tilbakemelding på hvor feilen er, vi vil kun få en feilmelding som kan si noe om typen feil som har oppstått. De vanligste typene kjøretidsfeil er divisjon med 0, feil i indeksering (eng: range error) eller uventet brukerinput. Siden disse feilene kan være vanskelig å finne anbefales det å bruke debugger i slike situasjoner, hvordan du bruker debuggeren er beskrevet lenger nede i dokumentet, der er det også et eksempel på å løse en «range error» vha. debugger.

Logisk feil

Logiske feil er feil som fører til at programmet ditt ikke fungerer som du ønsker. Disse vil ikke gjøre at programmet krasjer og de vil heller ikke produsere en feilmelding, vi får dermed ingen indikasjon på hverken hva feilen er eller hvor den befinner seg. Igjen er den letteste måten å finne slike feil ved hjelp av debuggeren, bruk av debugger er beskrevet under. Ved logiske feil er det spesielt nyttig å følge med på variablene, løkker og steder det blir gjort valg, som if-setninger og switches.

Advarsler

Når du bygger et program, kan du både få feilmeldinger og advarsler (eng: warnings). Hvis man får feilmeldinger vil ikke programmet bygges, men hvis man får advarsler bygges programmet. Vi har lyst til å skrive kode uten advarsler. Advarsler er en måte for kompilatoren å si fra at om her kan det gå galt når du kjører koden, selvom det er lovlig C++ kode. En advarsel kan f.eks. være at du prøver å sammenligne en int med en unsigned int i en for-løkke:

		

vector<int> vec = {1,2,3,4};
for(int i = 0; i < vec.size(); ++i){
	vec[i]++;
}				

	

Her er i en int og vec.size() en unsigned int. Du vet jo at dette fungerer som du vil, men det vet ikke kompilatoren.

Andre ganger er advarslene veldig nyttige, og avdekker feil i programmet ditt. F.eks. se på koden under

		

#include "std_lib_facilities.h"

int main(){
	int i{0};
	if (i = 1){cout << "i is 1";}
	else {cout << "i is not 1";}
}

	

Når koden bygges vil VScode gi en veldig viktig advarsel, under vises advarselen slik den er i PROBLEMS- fanen. Den sier akkurat hvor feilen er, linje 5, tegn 8.

Could not load image

Advarselen forteller at det er brukt en tilordning (eng: assignment) som betingelse (eng: condition). Vi har glemt == inne i if-betingelsen og endt opp med en tilordning istedenfor en sammenligning. Uten denne rettelsen ville programmet ikke fungert som forventet og det viser hvor viktig det kan være å ta hensyn til advarsler.

Debugger

Å debugge et program betyr å fjerne feilene fra programmet, VScode har en innebygd debugger som hjelper oss å gjøre dette. Debuggeren i VScode gjør at vi kan stoppe underveis i koden og få innsyn i tilstanden til programmet, dermed får vi bedre forståelse for hvordan programmet fungerer, eller eventuelt ikke fungerer.

Når man ønsker å kjøre et program i VScode vha. debugger trykker man på F5, eller så kan man velge «Run» øverst og så «Start Debugging». Hvis dette gjøres i et program uten videre vil det bare kjøre helt vanlig, for at vi skal få nytte av debuggeren må vi sette breakpoints i programmet.

Breakpoints er kanskje det mest essensielle når det kommer til debugging, breakpoints er punktene/linjene man ønsker å pause programmet på. Et breakpoint kan opprettes på to måter. Enten ved å trykke til venstre for tallet til den kodelinjen man ønsker å pause på, eller trykke på F9 for å få et breakpoint i den kodelinjen man befinner seg på. Da vil det komme en rød prikk i margen som symboliserer et breakpoint, bildet under viser et program med et breakpoint på linje 4. Breakpoint fjernes vet at du trykker på dem igjen.

Could not load image

Når man så kjører programmet med debuggeren vil kjøringen midlertidig stoppe når den kommer til et breakpoint, dette ser man enkelt ved at det er en gul pil rundt den røde prikken og linjen er farget gul. Merk: linjen breakpointet har stoppet på er ikke utført enda.

Could not load image

På dette tidspunktet har vi en god mulighet til å få innsyn i tilstanden til programmet. Det første du må legge merke til er kolonnen til venstre. Her ser du blant annet variablene i programmet og call stacken. I første omgang skal vi se på variablene under «Variables», de som dukker opp her er de som er i skopet til breakpointet. For enkle variabler vises verdien deres. Dersom vi har en beholder, eks. vector eller set, kan man blant annet se størrelsen og innholdet deres, men man må trykke på dem for å få det frem, dette gjelder også for brukerdefinerte typer/klasser. Hvis man ønsker å holde et ekstra øye med en eller flere av disse variablene kan man høyreklikke på dem og velge «Add to Watch», da dukker de opp i «Watch», slik som a på bildet og vi kan enkelt se hvordan den endres i løpet av programmet.

I tillegg til at man kan få oversikt over variablene kan vi nå bestemme hva programmet skal gjøre videre, øverst i VScode-vinduet har det kommet en meny, som vist på bildet, med et par alternativer som blir beskrevet under.

Noen eksempler

Eksempel: debugging

Dette eksempelet viser debugging i et enkelt program, det anbefales at du skriver koden som er vist på bildet under selv og gjør stegene underveis.

Could not load image

Det første vi gjør er å sette breakpoint på linje 9 og 11 og så trykker F5 for å starte debuggeren. Bildet over viser programmet når det har stoppet ved første breakpoint. Først i main opprettes et par variabler, vi ser at når vi kjører debuggeren dukker disse opp under «Variables» med en gang, men ingen av dem er initialisert. Vi har også lagt til variabelen a i «Watch». Dersom vi velger «Continue» vil programmet kjøre til neste breakpoint, som er to linjer under, da ser vi på bildet under at a og vec er blitt initialisert.

Could not load image

Nå kan vi velge «Step Over», da blir myMessage initialisert og vi stopper automatisk på neste linje, selvom det ikke er et breakpoint her. Her kalles det på en funksjon, dersom vi nå velger «Step Into» vil debuggeren ta oss til definisjonen, det ser vi ved at den gule linjen blir flyttet dit (av og til kan definisjonen ligge i en helt annen fil, da blir vi flyttet dit). Nå har vi bare en variabel i «Variables», message, vi ser at den ble midlertidig satt til «Hello World» da vi kalte på funksjonen, dette kan vi og se ved å ta musen over variabelen inne i funksjonsdefinisjonen.

Could not load image

Nå kan vi «Step Over» hvert steg i funksjonen, men funksjonen er enkel og vi skjønner hva den gjør, så da kan vi velge «Step Out», det vil for det første ta oss tilbake til kallet på funksjonen i main, men det er viktig å huske at det også utfører funksjonen. Trykker vi «Continue» igjen vil resten av programmet kjøres og terminere som normalt, da er det ikke lenger mulig å se de ulike variablene.

Den andre veldig nyttige delen av debuggeren er at vi kan se call stacken, dette er en slags liste over hvilke funksjoner som blir kalt, hver gang en funksjon kalles legges den til øverst i call stacken.

Eksempel: call stack

I koden under har vi to funksjoner, func1 og func2, der func2 kaller func1, vi setter et breakpoint på linje 10 og kjører programmet med debuggeren.

		

#include "std_lib_facilities.h"
void func1(){}

void func2(){
	func1();
}

int main()
{
	func2();
}

	

Da ser vi fra call stacken under at main har kalt func2 som har kalt func1, helt til høyre ser vi også hvilken fil funksjonene ble kalt fra og på hvilken linje.

Could not load image

Eksempel: out of range

Vi har funksjonen changeVector som er definert under. Vi kaller på denne funksjonen i main med vektoren vår myVec som argument.

		

#include "std_lib_facilities.h"

void changeVector(vector<int> vec, int newVal){
	for (int i = 0; i <= vec.size(); i++){
		vec.at(i) = newVal;
	}
}

int main()
{
	vector<int> myVec(10);
	changeVector(myVec, 1);
	return 0;
}

	

Koden kompilerer, men når den kjøres får vi følgende feilmelding. Merk: Denne dukker opp i terminalen, IKKE i VScode.

Could not load image

Feilmeldingen sier at programmet terminerte fordi det ble kastet et unntak av typen «out of range», men den sier ingenting om hvor det skjedde. For å finne feilen ønsker vi å bruke debugger. Det første vi gjør er å sette et breakpoint ved initialiseringen av vektoren vår myVec så vi er sikker på at den initialiseres korrekt. Etter at den har stoppet på breakpointet velger vi «Step Over» da ser vi i «Variables» at vektoren er korrekt initialisert.

Could not load image

Hvis vi nå velger «Continue» for å kjøre resten av programmet vil vi få opp en side fylt med nokså uforståelige tall (dette er fordi det ble kastet et unntak som ikke ble fanget). Dersom vi tar en titt på call stacken ser vi at det er kalt en rekke funksjoner vi ikke kjenner til, men den øverste av de vi kjenner til er changeVector, dette indikerer at det er her feilen ligger. Hvis vi trykker på changeVector i call stacken vil VSCode vise oss tilstanden til programmet som gjorde at den første ukjente funksjonen ble kalt.

Could not load image

Her ser vi blant annet fra den gule pilen at det var linje 5 som ble forsøkt utført, og i «Variables» kan vi se at variabelen i har verdien 10. Vi skjønner hvor feilen ligger, men ikke nødvendigvis hva som er galt, så da kan vi kjøre debuggeren på nytt.

Denne gangen velger vi "Step into" når vi kommer til kallet på changeVector. Dette tar oss først til en ukjent funksjon som spesifiserer vektoren vår, denne kalles implisitt når vi sender en vektor som argument (den er ikke relevant) så vi kan «Step Out» og «Step in» igjen, da kommer vi til definisjonen av changeVector. Her ønsker vi å gå nøye gjennom steg for steg og legger til både vec og i i «Watch». Vi velger «Step over» gjennom funksjonen og ser elementene i vektoren endres.

Could not load image

Etter litt kommer vi til situasjonen på bildet over der i = 10 og hvis vi ser på vec er alle elementene byttet allerede. Dessuten ser vi at vec har høyeste indeks 9 og her vil funksjonen prøve å aksessere indeks 10, altså kan vi konkludere at løkken går et steg for langt. Da kan vi enkelt endre den øvre grensen i løkken, fjerne =. Hvis vi kjører koden på nytt ser vi at den nå kjører som forventet.

I dette eksempelet virker det kanskje unødvendig å bruke debuggeren, men se for deg samme feilmelding i et mye større program, med mange vektorer og flere kompliserte funksjoner som opererer på dem. Da er det ikke like lett å se hvor problemet er og debuggeren vil da være til stor hjelp.

Debuggeren gir god oversikt over flyten i et program, men man kan for eksempel ikke bruke debuggeren om programmet ikke kompilerer. Den kan derfor bare hjelpe oss å finne kjøretidsfeil og logiske feil. Da har vi ofte to situasjoner, enten et uventet resultat eller at programmet krasjer, i begge disse tilfellene bør du bruke debuggeren. I programmer med løkker kan man lett miste oversikt, her kan det lønne seg å sette et breakpoint på toppen av løkken og gå et par runder gjennom løkken for å sjekke at den oppfører seg som forventet. I tillegg hvis du bare mister kontroll over programmet ditt eller skal sette deg inn i et nytt program kan du gå sakte og rolig gjennom programmet ditt med debuggeren for å forstå flyten i programmet.