Prosedyrer Hensikten med en prosedyre Hensikten med en prosedyre er, logisk sett, å representere en jobb eller en funksjonalitet i et eller flere programmer. Bruk av entall er viktig: vi har generelt en prosedyre for en funksjonalitet (ikke en prosedyre for flere funksjonaliteter). For eksempel, alle funksjonaliteter i programmer som Microsoft Word (funksjonalitet relatert til knapper, input fra bruker, tegning av grafikk etc.) har minst en prosedyre relatert til seg. Prosedyre-basert programmering er en løsning for å håndtere kompleksitet i programutvikling. Komplekse eller store programmer er utfordrende å utvikle og vedlikeholde fordi man må gjøre endringer i store deler av koden hele tiden. Det er snakk om endringer som forbedringer, endring i kodestruktur, og korrigering av feil. Løsningen for å håndtere slik kompleksitet er alltid å dele programmet opp i mindre biter. I prosedyre-basert programmering, er disse bitene representert som prosedyrer. Prosedyrer i Java: syntaks Prosedyrer opprettes i Java ved følgende syntaks: public static <returtype> navn_til_prosedyre(<parameter liste>) { // implementasjon av prosedyren return <variabel eller verdi>; // retur-verdien til prosedyren Eksempler: // I Java, må alle prosedyrer bli deklarert inne i en klasse. // Klasser er relatert til objekt-orientert programmering, noe // som blir introdusert senere i faget public class Eksempel { // Prosedyre som returnerer en int verdi public static int a() { return 5; // Prosedyre som inverterer en input boolsk verdi public static boolean b(boolean input) { return!input; // Parameterlisten angis med komma mellom hver variabel public static int sum(int a, int b) { return a + b;
// Void-prosedyrer returnerer ingen verdier public static void skriv_melding() { System.out.println("Hello Procedure World!"); // Man trenger ikke return instruksjonen for void // return er et ubetinget hopp: den hopper ut av prosedyren public static void ret_demo(boolean verdi) { if(verdi) { System.out.println("Verdien er true!"); return; // hopp ut av prosedyren else { System.out.println("Verdien er false!"); return; // trenger ikke verdi fordi typen er void // Pga return-hopp, så er det umulig å nå instruksjonene // etter denne if-else konstruksjonen. // Main-prosedyren er en 'spesiell' prosedyre som angir // starten av programmet public static void main(string[] args) { int i = a(); boolean bool = b(true); int i2 = sum(i,5); skriv_melding(); ret_demo(bool); // slutt på klassen Håndteringen av prosedyrer i datamaskinen Datamaskinen støtter ikke prosedyrer direkte. Prosedyrer oppnås hovedsakelig av kompilatoren som konverterer prosedyrer til maskinkode. Kompilatoren utfører mesteparten av minnehåndteringen som er beskrevet under. Vi har så langt i faget sett på to minnesegmenter til et program: instruksjonssegmentet og datasegmentet. Instruksjonssegmentet inneholder alle instruksjonene til programmet, mens datasegmentet inneholder verdien til alle variablene til programmet. Prosedyrer og data assosiert med prosedyrer er relatert til datasegmentet. For denne diskusjonen er det hensiktsmessig å dele opp datasegmentet i to deler: global og stabel (eng: stack). Stabelsegmentet inneholder verdier til alle variabler som har blitt deklarert i prosedyrer. Det globale segmentet inneholder verdier til alle variablene som har blitt deklarert utenfor prosedyrene til programmet.
public class Eksempel { // Dette er en variabel som er deklarert utenfor prosedyrene // til programmet. Verdien (5) til variabelen global_variabel // ligger nå i det globale datasegmentet. public static int global_variabel = 5; // Main er en prosedyre. Alle variablene som blir deklarert // inne i en slik prosedyre blir plassert på stabelen. public static void main(string[] args) { int stabel_variabel = 5; Variabler plassert i det globale datasegmentet er tilgjengelige i alle prosedyrer, dermed navnet 'global'. Disse variablene kalles ofte globale variabler. Slike globale variabler er alltid tilgjengelige og blir opprettet når programmet starter opp. Det vil si at antall variabler som er plassert i det globale datasegmentet er konstant, noe som fører til at størrelsen til dette datasegmentet, i antall bytes, også er konstant. Siden størrelsen til segmentet er konstant og verken kan utvides eller forminskes, refereres et slik segment til som et statisk segment (må ikke forveksles med nøkkelordet static i Java). En variabel plassert på stabelen er utelukkende tilgjengelig i prosedyren den er deklarert i. En slik variabel eksisterer også utelukkende på minne hvis prosedyren 'kjører'. Hvis prosedyren ikke kjører, så finnes ikke variabelen på minne. Dette fører til at stabelsegmentet kontinuerlig endres i størrelse: når en prosedyre 'starter', vil alle variablene relatert til prosedyren legges til stabelsegmentet. Når prosedyren er ferdig å kjøre, blir variablene slettet fra stabelsegmentet. Det vil si, desto flere prosedyrer som er aktive i programmet, desto mer minne bruker programmet. Et slikt datasegment som kontinuerlig endres i størrelse, refereres til som et dynamisk segment. Dataene relatert til en prosedyre kalles et 'stack frame' (jeg bruker det engelske uttrykket for nøyaktighetens skyld). Et stack frame inneholder følgende data: Parameterlisten Lokale variabler Returadresse Eksempel på neste side.
// Data på stack frame til p: // - verdiene til a, b, c (parametere) // - verdiene til d og e (lokale variabler) public static void p(int a, short b, byte c) { char d = 'd'; int e = a + b + c + d; public static void main(string[] args) { p(5,6,7); // Faktiske verdier til stack framen til p: // - 5, 6, 7 (parametere) // - 'd', a+b+c+d (lokale variabler) // - adressen til neste instruksjon (instruksjonen under) System.out.println("I'm back"); // instruksjonen over skal utføres etter at p er ferdig utført. Enver prosedyre trenger å vite returadressen for at programmet kan manipulere programtelleren slik at man hopper tilbake til den kallende prosedyren. I dette eksempelet, må prosedyren p vite at den kom fra main-prosedyren (det er i main p(5,6,7) kalles), slik at neste instruksjon i main kan utføres. Når datamaskinen utfører kjøringen til et program, utfører den alltid en prosedyre, der vi starter med main-prosedyren. Hvis main kaller på en prosedyre, som p i eksempelet, blir stack framen til p lagt til stabelsegmentet. Etter at p er ferdig utført, blir stack framen slettet fra stabelsegmentet. Det eneste som overlever en prosedyre er returverdien til prosedyren. Håndteringen av stabelsegmentet gjøres via datastrukturen stabel (eng: stack). Dette er en enkel form for datastruktur som kan sammenlignes med en stabel av tallerkener (f.eks. i kantinen): du legger til og fjerner tallerkener fra stabelen utelukkende fra toppen (antatt at du ikke løfter opp tallerkenene for deretter å plassere nye tallerkener på bunnen). I denne analogien, er et stack frame en tallerken og stabelen av tallerkener er de aktive prosedyrene til programmet. Merk at main-prosedyren vil alltid ligge på bunnen av stabelen. Eksempel: 1: public static void a(int b, int c) { 2: int d = b + c; 3: System.out.println(d); 4: 5: public static void main(string[] args) { 6: int b = 5; 7: int c = 6; 8: a(b,c); 9: System.out.println("Done!"); 10: I dette eksempelet, vil stabelen se slik ut ved linje 7 (ved å bruke fargekodene relatert til hver prosedyre):
Verdien til args "Done!" (dette er også en variabel) Returadresse (ikke viktig for main) Ved linje 7, er det dermed en aktiv prosedyre i programmet: main. Det vil si at stabelen inneholder et stack frame stack framen til main. Ved linje 8 kaller vi på prosedyren a. Da blir stabelen utvidet slik: 5+6 (variabel d) 9 (returadresse) Verdien til args "Done!" (dette er også en variabel) Returadresse (ikke viktig for main) Merknader: Verdier blir kopiert fra en prosedyre til en annen. Vi ser at stabelen inneholder to verdier av 5 og 6. Noen språk, som C, tilbyr alternative kopieringsmodus, som kopiering via peker. Dette konseptet introduseres i neste forelesning. En prosedyre kan bare aksessere data på stack framen som ligger øverst på stabelen. Det vil si at prosedyren a ikke har tilgang til verdiene i de røde cellene i tabellen. Når en prosedyre starter, vil den bare angi verdier til variabler som den vet verdien til. Verdien til variabelen d i prosedyren a er dermed udefinert før den utfører instruksjon 2. Det vil si at instruksjonene 6 og 7 faktisk ikke utføres fordi verdiene til b (5) og c (6) blir angitt direkte av programmerer og kan bli tildelt før main prosedyren starter sine instruksjoner. Ulike praktiske konsekvenser av oppførselen til stabelen introduseres i oppgavesett 4. Prosedyren a kan også kalle på andre prosedyrer, noe som vil videre utvide stabelen. Eksempel på neste side. Eksempelet viser at stabelen utvides når nye prosedyrer kalles på. Hvert program har et begrenset området på minne allokert til seg for stabelen. Hvis stabelen går utover denne begrensningen, vil operativsystemet avslutte programmet (programmet vil krasje hvis man ikke eksplisitt håndterer dette). Dette kalles stabeloverflyt (eng: stack overflow).
1: public static void a(int b, int c) { 2: int d = sum(b,c); 3: System.out.println(d); 4: 5: public static int sum(int a, int b) { 6: return a + b; 8: 9: public static void main(string[] args) { 10: int b = 5; 11: int c = 6; 12: a(b,c); 13: System.out.println("Done!"); 14: Stabelen ved instruksjon 2, dvs. etter at prosedyren sum blir opprettet på stabelen: 5 (variabel a) 6 (variabel b) 5+6 (returverdien) 3 (returadresse) sum(a,b) (variabel d) 13 (returadresse) Verdien til args "Done!" (dette er også en variabel) Returadresse (ikke viktig for main) Scopes Over, så vi at en prosedyre bare kan aksessere variabler som ligger i den øverste stack framen og globale variabler (som alle prosedyrer kan aksessere). Høy-nivå programmeringsspråk, som Java, tilbyr en videre begrensing for aksessering: scopes. I Java, angis et scope med krøllparentesene {. Regelen er slik at variabler som deklareres i et scope, ikke er tilgjengelig utenfor scopet. Eksempel: public static void p() { // scope 1 int a = 4; // tilgjengelig i scope 1, 2, og 3 { // scope 2 int b = 4; // tilgjengelig i scope 2 og 3 while(b > 0) { // scope 3 int c = 1; // tilgjengelig i scope 3 b -= c; // slutt på scope 3 // slutt på scope 2 // slutt på scope 1
Merk at denne begrensningen, og slike 'scopes' generelt, håndteres av kompilatoren til språket og ikke av datamaskinen. Det er dermed kompilatoren sin jobb å oppdage om programmereren har brutt regelen relatert til scope, som å bruke en variabel som ikke er en del av det nåværende scopet. Dette betyr at håndteringen av stabelen er den samme selv om en prosedyre bruker flere scopes. Eksempel: public static void p1() { int a = 0; int b = 2; for (int i = 0; i < 1000; i++) { a += b; public static void p2() { int a = 0; for (int i = 0; i < 1000; i++) { int b = 2; a += b; Ut ifra koden over, kan man kanskje tro at variabelen b blir opprettet 1000 ganger i prosedyren p2. Dette er ikke tilfellet. Alle lokale variabler, selv om de blir deklarert i en løkke, blir opprettet til stack framen til prosedyren på samme måte, dvs. før prosedyren starter å utføre sine instruksjoner. Stack framene til p1 og p2 er dermed like store, i antall bytes, fordi de har like mange variabler relatert til seg (og variablene er av samme type).