1 Funktioner och procedurell abstraktion Det som gör programkonstruktion hanterlig och övergripbar och överhuvudtaget genomförbar är möjligheten att dela upp program i olika avsnitt, i underprogram. Vår förmåga att hantera ett stort komplext problem bygger till stor del på vår förmåga att dela upp problemet i mindre delar och därefter lösa var del för sig. Detta är kärnan i procedurell abstraktion. Procedurell abstraktion förenkling genom uppdelning Man kan enkelt ge exempel på procedurell abstraktion i vardagslivet Exempel, frukosttillagning medelst procedurell abstraktion 1 Koka kaffe 2 Stek två ägg 3 Gör en skinkmacka Detta är en anständig beskrivning av tillagningen av en frukost. Beskrivningen består av tre olika procedurer. Beskrivningen är abstrakt, det vill säga förenklad. Förenklingen/abstraktionen består i att man inte beskriver varje enskild detalj i exempelvis proceduren Koka kaffe. Denna förenkling kan ske på flera olika nivåer. Den enklaste kan vara att helt enkelt ange Laga frukost. En mindre abstrakt variant är de tre stegen som är beskrivna ovan. En ännu mindre abstrakt variant vore om man ytterligare beskrev de olika stegen, ex 1. Koka kaffe: Ta fram kaffepulver, häll 5 dl vatten i bryggaren, mät upp 7 mått kaffepulver i filtret, slå på bryggaren, vänta 2 minuter. Denna förfining kan dras in absurdum såsom exempelvis att ange exakt vila armrörelser och benrörelser man ska utföra föra att hälla 5 dl vatten i någon speciell kaffebryggare. Om man utformar dessa procedurer mer och mer exakt kommer samtidigt beskrivningen att bli mer och mer specialiserad. En angivelse av benrörelser fungerar troligtvis bara för ett speciellt kök, med speciellt placerade köksverktyg, medan den första beskrivningen fungerar i de flesta kök. Vid programmering kommer man aldrig att behöva förfina mer än att det direkt går att översätta till programsatser, eller till välkända lösningar på standardproblem. För att åstadkomma denna uppdelning i procedurer eller underprogram skriver man funktioner. 1.1 Funktioner Av de funktioner vi redan stött på kan vi utläsa på vilket sätt funktioner kommer att anropas. En funktion/ett funktionsanrop representerar ett återanvändbart programavsnitt som utförs då programmet stöter på ett anrop. Utdrag ur Exempel för användning av texthanteringsfunktioner // tilldelning namn1 > namn1och2 strcpy( namn1och2, namn1 ); // lägg till texten ", "
strcat(namn1och2, ", "); // lägg till namn2 strcat(namn1och2, namn2); strcpy( namn1och2, namn1 ); är ett funktionsanrop till en redan skriven funktion som utför det arbete som funktionen strcpy antyder, att genom en iteration kopiera över alla tecken i texten namn1 till textfältet namn1och2. Det är en liknande iteration som vi såg göras av en for sats vid tilldelningsoperationen för ett textfält på sidan Error: Reference source not found. 1.1.1 Funktionsdeklaration och funktionsdefintion En funktion består huvudsakligen av två delar 1. en deklaration som berättar hur funktionen ska användas i ett program 2. en definition som beskriver vilken programkod som ska utföras vid anropet. En deklaration har följande form returtyp funktionsnamn(argumenttypslista ); I vårt tidigare skrivsätt för operationer skulle detta se ut som funktionsnamn( argumentlista): returtyp En definition har följande form returtyp funktionsnamn(argumentdeklarationslista) sats/satsblock; Returtyperna och argumentlistorna måste stämma överens exakt med avseende på datatyperna. Den enklaste datatypen är void (ingenting) den använder man om man inte vill ge något värde till eller från funktionen. De funktioner som har returtyp void ger alltså inget returvärde. Dessa funktioner är av en speciell kategori funktion kallade procedurer. Författaren anser att distinktionen mellan sk. void funktioner(procedurer) och icke void funktioner (funktioner) är onödig, men eftersom många andra gör denna distinktion vill jag påpeka att den förekommer. Det finns däremot anledning att först i en text behandla void funktioner eftersom de är den syntaktiskt enklaste typen. Den enklast möjliga funktionsdeklarationen ser ut som // dra_linje ritar ett streck med tecken void dra_linje(); Kommentaren krävs förstås inte av kompilatorn men det är en god vana att kommentera varje funktionsdeklaration, så att man enkelt kan läsa ut syftet med funktionen.
En enkel definition för deklarationen kan se ut som void dra_linje() cout<<" "<<endl; #include<iostream> using namespace std; Kom ihåg att syftet med deklarationen är att berätta för både kompilator och användare hur funktionen ska användas. Definitionen utnyttjas sedan vid länkning för att koppla ihop anrop med programkod. När man skriver ett huvudprogram som använder funktioner bör man komma ihåg att syftet med funktionerna är procedurell abstraktion. Därför bör man ha deklarationerna väl synliga och lättillgängliga vid skrivandet av huvudprogrammet main(). Det är dessutom så att kompilatorn måste tolka deklarationer innan dess definitioner. I annat fall kommer definitionen att även tolkas som en deklaration (deklaration och definition på samma gång) och den efterföljande deklarationen kommer att betraktas som en dubbeldeklaration eller nydeklaration, vilket inte är tillåtet. I ett kort program kan vår funktionsdeklaration och funktionsdefinition fogas in som // Lätttillgängliga deklarationer // dra_linje ritar ett streck med tecken void dra_linje(); int main() dra_linje(); // anropa underprogram cout<<"hello World"<<endl; dra_linje(); return 0; void dra_linje() cout<<" "<<endl; Programmet anropar funktionen dra_linje två gånger. Vid varje anrop styrs programflödet över till satsblocket till funktionen, rad 17: cout<<" " kommer alltså att exekveras två gånger. När satsblocket till funktionen är slut styrs programflödet tillbaka till det anropande uttrycket. Då kompilatorn stöter på anropet till funktionen måste den ha sett en deklaration av funktionen som anropas. Resultatet blir:
Hello World Press any key to continue 1.1.2 Övning: int dra_linje() Prova att i din utvecklingsmiljö ändra definitionens första rad till int dra_linje(); Observera vilka fel som rapporteras vid kompilering / länkning Ändra tillbaka definitionen till det ursprungliga och ändra istället deklarationen till Observera vilka fel som rapporteras vid kompilering / länkning Funktionsnamn är om möjligt ännu viktigare än variabelnamn. Använd utförliga och tydliga funktionsnamn som beskriver dess syften väl. En funktion som aldrig anropas körs aldrig. Det kan mycket väl tänkas att du skrivit dina funktion korrekt i ditt program, men programmet gör ändå inte som du vill. Ibland beror det på att du glömt anropet av funktionen. Detta är ett fel som kompilatorn inte kan upptäcka. 1.2 Funktioner som behöver indata Vi har sett tidigare i samband med texthanteringen hur vi kunde berätta för funktionen att den skulle göra beräkningar på någon viss variabel, exempelvis den tidigare satsen int len = strlen( namn_1 ); utför en beräkning på variabeln namn_1 för att kontrollera hur många tecken som texten innehåller. Denna data meddelas genom argumenttyper och argumentnamn Den tidigare funktionen dra_linje() som drar ett streck av minustecken är inte vidare flexibel. Man kan till exempel inte ange hur långt strecket ska vara. Det kan man enkelt åstadkomma med funktionsargument. Detta skulle i huvudprogrammet kunna se ut som:
int main() dra_linje( 12 ); // skriv tolv minustecken cout<<"hello World"<<endl; dra_linje( 5 ); // skriv fem minustecken return 0; Siffrorna 12 och 5 kallas aktuella argument. Notera att antalet minustecken är ett heltal ( int ), en deklaration kan därför skrivas som // dra_linje(int ) ritar ett streck med ett visst antal tecken void dra_linje(int ); Detta är inte samma funktion som tidigare utan en ny funktion. På samma sätt som operatorer identifieras en funktion med hjälp av dess signatur, dvs. namn + argumenttyper. Våra funktioners signaturer är dra_linje() respektive dra_linje(int ). Notera att returtypen inte ingår i signaturen, det innebär att det kan inte finnas en funktioner döpt int dra_linje() samtidigt som en funktion void dra_linje(). I syntaxen för en funktionsdeklaration och definition anger man en argumenttypslista respektive argumentdeklarationslista Vi kan skriva en funktionsdefintion som void dra_linje(int antal_minus) for(int index=0; index< antal_minus; index++) cout<<" "; cout<<endl; Rad 1: Det görs nu en variabeldeklaration varje gång funktionen anropas. Variabeln inititeras med det värde som anges som aktuellt argument. Variabeln antal_minus kallas formellt argument. Det är det formella argumentet som används i funktionens programkod. Rad 3 6: Repetitionen skriver ut lika många minustecken som värdet i variabeln antal_minus.
#include<iostream> using namespace std; I ett komplett program kan de båda funktionerna skrivas och användas som // dra_linje ritar ett streck med tecken void dra_linje(); // dra_linje(int ) ritar ett streck med ett visst antal tecken void dra_linje(int ); int main() dra_linje(); // anropa funktion cout<<"hello World"<<endl; dra_linje(); dra_linje( 5 ); cout<<"hello"<<endl; dra_linje( 78 ); return 0; void dra_linje() cout<<" "<<endl; void dra_linje(int antal_minus) for(int index=0; index< antal_minus; index++) cout<<" "; cout<<endl; Programkörning går till som : Start vid main() Funktionsanropet dra_linje() identifieras som funktionen void dra_linje(), funktionen anropas, cout satsen utförs. Texten "Hello World" skrivs ut Funktionsanropet dra_linje() identifieras som samma funktion void dra_linje(), funktionen anropas, cout satsen utförs. Funktionsanropet dra_linje( 5 ) identifieras som void dra_linje( int ). Funktionen anropas med aktuellt argument 5, Funktionen anropas. I funktionen void dra_linje( int ): Argument deklarationen blir int antal_minus = 5. Repetitionssatsen deklarerar ytterligare ett heltal int index = 0. Iterationen skriver ut 5 minustecken.
När funktionens satsblock tar slut upphör variablerna antal_minus och index att vara meningsfulla, och de förstörs därför. Man säger att de har sin räckvidd (eng. scope) inom funktionen. Funktionen styr programflödet tillbaka till cout<<"hello"; Funktionsanropet dra_linje( 78 ) identifieras som void dra_linje( int ). Funktionen anropas med aktuellt argument 78. I funktionen void dra_linje( int ): Argument deklarationen blir int antal_minus = 78. Observera att detta är variabel nummer två med namnet antal_minus som deklareras. (den första har förstörts). Repetitionssatsen deklarerar variabeln index = 0 för andra gången. Iterationen skriver ut 78 minustecken. Funktionen styr programflödet tillbaka till programslut. Det centrala i detta program är återigen abstraktionen/förenklingen av att rita en linje. Då funktionerna dra_linje väl är skrivna är det inte längre intressant på vilket sätt de fungerar utan bara att de fungerar som de är deklarerade. Detta försöker man alltid sträva efter då man utformar sina program. Det är värt att notera att i funktionsdeklarationen har man tillåtelse att ge identifierare för det olika argumenten, för funktionen dra_linje(int ) skulle deklarationen kunna se ut som void dra_linje(int antal_tecken); Syftet med identifieraren är endast att upplysa den person som läser deklarationen om syftet med heltalet. Kompilatorn eller programmet tar ingen som helst hänsyn till den identifierare som skrivs i deklarationens argumentlista. Definitionen använder ju deklarationernas identifierare som variabler inuti funktionskroppen. 1.3 Funktioner som returnerar värden Ofta vill man att en funktion ska utföra en beräkning eller komma fram till något numeriskt resultat. Vi har tidigare sett en funktion som beräknar längden på en text int len = strlen( namn_1 ); Denna funktion ger tillbaka/utvärderas till ett heltalsvärde. Operationen kan beskrivas som strlen(char * ): int Heltalstypen anger som vanligt att uttrycket utvärderas till ett heltal. Vi kan konstruera egna funktioner med liknande utseende. För att beräkna arean av en rektangel tar man basen * höjden. Detta kan förstås skrivas som en funktion. I ett huvudprogram kan man tänka sig att funktionen anropas som
double bredd, hojd, yta; bredd= 5.0; hojd= 2.5; yta= area( bredd, hojd ); Funktionen utvärderas som ett flyttal och tar två flyttal som argument. Deklarationen blir då double area( double, double); Funktionsdefinitionen innehåller en enkel multiplikation. double area( double b, double h) double a = b * h; return a; Alternativt kan man skriva funktionen på ett enklare (men mindre tydligt) sätt som double area( double b, double h) return b*h; return satsen anger alltså vilket värde som funktionen ska utvärderas till. Datatypen för uttrycket i return satsen måsta vara av samma datatyp som, eller kunna omvandlas till, returtypen angiven i deklaration och definition. Satsblocket som hör till en funktion kallas ofta funktionens implementation. Då man talar om implementering eller implementation avser man programkoden eller skrivandet av programkoden. Funktioner ska skrivas med målet att de ska fungera i så många situationer som möjligt. Man talar om generella funktioner. Det leder oss direkt in på ett mycket vanligt nybörjarmisstag vid implementation av funktioner. En funktion ska aldrig innehålla in eller utmatningssatser om det inte uttryckligen är funktionens syfte. Area funktionen i tidigare exempel fungerar bra för att visa på nackdelarna med detta. Antag att man skrivit funktionen som
//Deklaration double area(); //Definition double area() double b,h; cout<<"ange rektangelns bredd:"; cin>>b; cout<<"ange rektangelns höjd:"; cin>>h; return b*h; Funktionen kan mycket väl vara lämpad för uppgiften i ett speciellt fall, men den är långt ifrån så generell som den tidigare area beräkningen. Antag att programmet redan kände till värden för bredd och höjd. Då skulle inmatningssatserna förmodligen göra programmet väldigt förvirrande för användaren, om inte rent obrukbart. Undantaget är alltså funktioner som uttryckligen är till för in och utmatning. Typexempel är funktioner typ dra_linje ovan. Funktioner för utmatning är väldigt ofta void funktioner (procedurer). 1.4 Speciella datatyper för funktionsargument 1.4.1 Fälttyper Man kan ge textfält som argument till funktioner på samma sätt som man ger vanliga variabler som argument. Det man måste hålla i minnet är att fältvariabeln inte representerar själva texten utan bara vart texten börjar. Detta får vissa konsekvenser för texter i funktioner. Betrakta programmet
#include<iostream> using namespace std; void skriv_10_ggr(char []); int main() char text1[]="ett"; char text2[]="två"; skriv_10_ggr( text1 ); skriv_10_ggr( text2 ); return 0; void skriv_10_ggr( char txt[]) for(int i = 0;i<10;i++) cout<< txt; Programmet ger följande utskrift EttEttEttEttEttEttEttEttEttEttTvåTvåTvåTvåTvåTvåTvåTvåTvåTvåPress any key to continue Det aktuella argumentet i första funktionsanropet är alltså adressen till den minnescell som innehåller tecknet 'E' i texten Ett. Argumentet är alltså ett slags heltal. Detta innebär att den information som behöver föras över till funktionen inte är hela texten, vilket skulle kunna bli väldigt resurskrävande om texten var lång, utan bara adressen till textens första tecken. Variabeln txt och variablerna text1 och text2 representerar alltså inte bara lika texter utan samma texter vid respektive funktionsanrop. Alltså txt==text1 sedan txt==text2, text1 och text2 är naturligtvis aldrig lika. Detta har ytterligare en effekt, förutom att vara resurseffektivt. Man kan nämligen manipulera innehållet i fältet inuti funktionen. Se exempelvis funktionen och anropet
void mata_in_textrad(char txt[]); void mata_in_tal( int ); int main() char inmatningsyta[200]; int talet; mata_in_textrad( inmatningsyta ); mata_in_tal( talet ); // går ej cout<<"du har matat in:"<< inmatningsyta<<endl; cout<<"och talet: "<<talet<<endl; return 0; void mata_in_textrad(char txt[]) cin.getline( txt, 80); void mata_in_tal( int tal) cin>>i; Det fält som skapas i och med deklarationen char inmatningsyta[200]; används direkt i funktionen eftersom variabeln inmatningsyta och txt (i funktionen mata_in_textrad) avser samma minnesyta med 200 tecken. Detta fungerar inte med vanliga datatyper som i funktionen mata_in_tal(int ) eftersom de deklareras som nya variabler oberoende av den anropande funktionens variabler. Dessa egenheter motiverar två speciella datatyper konstanter och referenser. 1.4.2 Konstanter Man kan definiera upp konstanter i c++ genom nyckelordet const. const double PI = 3.14159265358; const int NOLL = 0; const char ALFABET[] = "abcdefghijklmnopqrstuvwxyzåäö"; Konstanter har ofta identifierare som bara består av versaler. Den sista deklarationen skall läsas som ett fält bestående av const char. Tecknen i fältet får inte ändras. Att deklarera en variabel som konstant gör att man inte har tillåtselse att ändra variabelns värde. Samtliga följande satser är otillåtna PI = 3.14; NOLL += 1; ALFABET[3] = 'D'; // otillåtet för const double // otillåtet för const int // otillåtet för const char För funktioner innebär det att om den tidigare funktionen mata_in_textrad( char [] ) istället skulle skrivits som
void mata_in_textrad(const char txt[]); // och definition void mata_in_textrad(const char txt[]) cin.getline(txt, 80); är det en otillåten operation att ändra värdet på tecknen i fältet. cin.getline( txt, 80) är då en otillåten operation, kompilatorn ger felmeddelande, eftersom satsen ändrar innehållet i fältet. Jämför deklarationerna av texthanteringsfunktionerna i cstring. 1.4.3 Referenser int i = 3; int & ref = i; En annan speciell deklaration får man om man deklarerar en variabel som referens. Detta ser ut som Rad 2: Deklarerar identifieraren ref att vara en så kallad referens. En referensvariabel är en synonym till en redan existerande variabel. Referensen fungerar på exakt samma sätt som den ursprungliga variabeln. Genom referensen får man alltså här två variabelnamn/identifierare att referera till exakt samma data. Betrakta int i = 3; int j = 4; int & ref = i; ref = j; // obs tilldelning värdet i j kopieras till ref/i i++; cout<<"i:"<<i<<endl; cout<<"j:"<<j<<endl; cout<<"ref:"<<ref<<endl; ref++; cout<<"i:"<<i<<endl; cout<<"j:"<<j<<endl; cout<<"ref:"<<ref<<endl;
Programsnutten ger resultatet i:5 j:4 ref:5 i:6 j:4 ref:6 Detta kommer sig alltså av att ref och i representerar samma instans (samma faktiska heltal). Tilldelningssatsen på rad 4 ger alltså endast värdet av j till instansen i/ref. Med tanke på att referenser är synonymer till redan existerande instanser måste de initieras i samband med en deklaration, detta görs automatiskt om man gör dem till funktionsargument (de initieras då med de aktuella argumenten), men inte om man deklarerar dem som ovan. Följande sats är otillåten eftersom den inte refererar till något existerande heltal. int & j; int & k = 4; // otillåtet 4 är ingen heltalsvariabel I exempel som detta är det svårt att se nyttan med referenser. Nyttan blir tydligare om man visar hur det fungerar för ett funktionsargument. Betrakta funktionen och programmet void mata_in_tal( int&, int &); int main() int tal1, tal2; cout<<"mata in ett tal"<<endl; mata_in_2_tal( tal1, tal2 ); cout<<"du har matat in "<<tal; return 0; void mata_in_tal( int& i, int& j) cin>>i>>j; Denna programsnutt fungerar som tänkt eftersom de identifierare som deklarerats i funktionen, i och j, är synonymer till redan existerande variabler tal1 respektive tal2. De refererar till samma heltal. Se även funktionen swap( int&, int& ) i avsnittet standardalgoritmer.