13.
Mer om funktioner
1. Prototyper.
2. Makron.
Innan man anropar en funktion måste kompilatorn känna till den. Om kompilatorn inte känner till funktionens gränssnitt, det vill säga dess typ och dess argumentlista, kan den inte kontrollera att vi har skrivit rätt när vi anropar funktionen. Beroende av omständigheter som vi inte går in på här kan kompilatorn göra en av två saker. Den kan göra ett antagande som baseras på hur vi anropat funktionen, varvid eventuella felaktigheter upptäcks av länkaren. Det vanligaste är dock att den klagar på att funktionsnamnet inte är deklarerat.
För att undvika detta deklarerar man vanligen funktionerna i sådan ordning att de deklareras före de funktioner som anropar dem:
#include
<iostream.h>
int Kvadrat(int a)
{
return a * a;
}
void main()
{
int iTal, iSvar;
cout << "Var god ange ett tal: ";
cin >> iTal;
iSvar = Kvadrat(iTal);
cout << "Kvadraten på " << iTal
<< " är " << iSvar << "\n\n";
}
Det är dock inte alltid möjligt att göra så här. När man använder funktioner som redan är kompilerade är det inte säkert att man vill eller kan kompilerar dem en extra gång bara för att kompilatorn ska känna till dem. Så är fallet med alla de extra funktioner man använder från iostream.h, stdio.h string.h etcetera. Dessa funktioner är redan kompilerade, och finns i så kallade objektsbibliotek som länkaren genomsöker när kompilatorn är klar med det man själv skrivit.
Detta dilemma löses med hjälp av så kallade prototyper. Man skriver helt enkelt ett funktionshuvud utan tillhörande programkod. Efter argumentlistan sätter man bara ett avslutande semikolon:
int Kvadrat(int a);
Efter att kompilatorn läst ovanstående sats har den tillräckligt med information för att kunna utföra de tester som behövs när man skrivit ett anrop till funktionen 'Kvadrat()'.
Övningsuppgift 13.1.1:
· Skapa ett projekt Proto.
· Skriv in ovanstående program, men se till att deklarera funktionen 'Kvadrat()' efter funktionen main(). (Placera den efter slutklammern i main(), inte i main()!)
· Kompilera programmet. Det ska bli tre felmeddelande. De två sista felen
är följdfel. Skriv här vilka felmeddelanden du fick, och förklara varför:
Fel nummer 1:_________________________________________________
____________________________________________________________
____________________________________________________________
____________________________________________________________
____________________________________________________________
____________________________________________________________
Fel nummer 2:_________________________________________________
____________________________________________________________
____________________________________________________________
____________________________________________________________
____________________________________________________________
____________________________________________________________
Fel nummer 3:_________________________________________________
____________________________________________________________
____________________________________________________________
____________________________________________________________
____________________________________________________________
____________________________________________________________
Övningsuppgift 13.1.1:
· Öppna projektet Proto om det inte redan är öppet.
· Skriv en funktionsprototyp till 'Kvadrat' före main().
· Kompilera, länka och testa. Så här kan det se ut när man kör programmet:
En prototyp är alltså bara en funktionsheader utan tillhörande funktion. Därav namnet headerfiler och filnamnstillägget '.h'. I dessa filer finns prototyper som beskriver egenskaper hos de färdigkompilerade funktioner som följer med C++.
Övningsuppgift 13.1.2:
· Skapa en ny textfil utan att spara den.
· Skriv <string.h>.
· Detta kan du göra i vilken fil som helst, även en existerande källkodsfil i vilket projekt som helst. Det enda vi behöver är ovanstående text i Developer Studios redigeringsfönster.
· Klicka med höger mustangent på ordet '<string.h>'. En snabbmeny ska nu visas.
· Välj 'Open string.h'.
· Nu öppnas headerfilen string.h. Du får inte göra några ändrinar i den!
· Om du vet var string.h finns kan du öppna den på sedvanligt sätt, men ovanstående sätt är bekvämt att kunna.
· Leta efter funktionsprototyperna för funktionerna 'strcpy()', 'strcmp()' respektive 'strcat()'.
· Tips: sök först efter kommentaren 'Function prototypes' med hjälp av redigerarens sökfunktioner i 'edit-menyn'.
· Förutom det vi känner igen från vad vi redan lärt finns det en extra del i dessa deklarationer som talar om tekniska detaljer om enligt vilken standard funktionen ska anropas: _CRTIMP och __cdecl. Dessa diskuterar vi inte här.
· Skriv här nedan funktionsprototyperna för ovan nämnda funktioner och
förklara vad de anger (förutom _CRTIMP och __cdecl):
strcpy():___________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
strcmp():__________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
strcat():___________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
Övningsuppgift 13.1.2:
· Öppna stdio.h.
· Skriv här nedan funktionsprototyperna för fopen(), fclose(), fgetc()
samt fputc() och förklara vad dessa prototyper anger:
fopen():___________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
fclose():___________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
fgetc():____________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
fputc():____________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
_________________________________________________________
Kompileringsdirektivet #define kan användas för att ersätta text i programmet. Man kan till exempel slippa att skriva 3.14159265359F på ett flertal ställen i sitt program, om man nu använder pi flitigt. Man kan skriva:
#define PI 3.14159265359F
...så kan vi på alla de ställen där vi behöver pi i vårt program bara skriva 'PI' i stället för hela talet:
fOmkrets = PI * fDiameter;
Observera att #define är ett kompileringsdirektiv, och inte en programsats. Därför ska raden inte avslutas med semikolon. Så här ser syntaxen ut:
#define TEXTKONSTANT text som ska kompileras
Det första ordet är #define. Tecknet '#' anger att det är ett kompileringsdirektiv och 'define' anger att vi vill definiera en text som ska bytas ut.
Första ord som följer '#define', skilt av ett eller flera mellanslag och/eller tabulatorer blir det ord som vi kan använda i programmet. Det bör skrivas med endast stora bokstäver, vilket är vedertagen standard. Ordet kallas textkonstant.
All efterföljande text är det som kompilatorn läser på de ställen där vi använt textkonstanten i vårt program. Den texten får innehålla mellanslag och tabulatorer.
Alla kompileringsdirektiv, inte bara '#define', ska stå på en rad. Det finns dock ett spacialtecken som gör att flera rader uppfattas av kompilatorn som en och samma rad, radfortsättningstecknet, '\' (engelska line continuation character).
Övningsuppgift 13.2.1:
· Skapa ett projekt Omkrets.
· Deklarera textkonstanten PI enligt ovanstående.
· Programmet ska innehålla en do-slinga som avbryts om en variabel fDiameter innehåller värdet 0.0F.
· I slingan ska användaren uppmanas att ange en diameter. Därefter ska omkretsen beräknas med hjälp av PI, och presenteras på skärmen.
· Så här kan det se ut:
Kompileringsdirektivet #define kan även användas för att skapa enkla makron. Det går alltså även att använda anropsvariabler, d.v.s. en motsvarighet till funktionernas argument. Detta är egentligen ingen äkta användning av argument utan bara textsubstitution, men kan vara väl så användbart. Textkonstanten kallas i så fall ett makronamn.
Tillägget blir det att man skriver till en 'argumentlista' direkt efter makronamnet. Observera att det inte får förekomma några blanktecken mellan makronamnet och argumentlistan! Argumenten i argumentlistan används sedan i den text som kompilatorna ska läsa. Det är viktigt att man förstår att argumenten i argumentlistan inte är deklarationer i vanlig mening, utan bara ett i makrot internt namn för textsubstitution. Syntaxen kan utökas till följande:
#define MAKRONAMN(ARGUMENTLISTA) text som ska kompileras
I texten som ska kompileras ska alltså argumenten användas. I nedanstående exempel finns ett 'argument': 'X', vilket används i texten som ska kompileras:
Man kan till exempel deklarera ett makro KVADRAT på följande sätt:
#define KVADRAT(X) X * X
Detta skulle kunna användas till exempel så här:
int a;
int b = 5;
a = KVADRAT(b);
...vilket kompilatorn tolkar som:
a = b * b;
Det rör sig om rent textutbyte, så a och b kunde även varit av annan datatyp för vilken operationen '*' är meningsfull, till exempel 'float'. De måste dock i detta exempel vara av kompatibla typer, det vill säga datatyper som passar ihop.
Man skulle även kunna ange konstanter, eller till och med beräkningar, men här får vi se upp! Om vi använder makrot så här:
a = KVADRAT(b + 5);
...så blir inte resultatet det vi förväntade oss (100, eller hur?). Kompilatorn tolkar det ju som:
a = b + 5 * b + 5;
...och här träder operatorernas prioritet in i bilden. Multiplikation räknas före addition, så resultatet blir 35 i stället. Detta kan man naturligtvis förebygga genom att använda parenteser, men för att slippa använda parenteser när man använder makrot bör man i stället bygga in parenteserna i det:
#define KVADRAT(X) (X) * (X)
Nu tolkas vårt uttryck riktigt:
a = (b + 5) * (b + 5);
...vilket blir 100!
Skulle man använda KVADRAT tillsammans med någon operator som har högre prioritet än gånger, till exempel typomvandlingsoperatorn, kanske man skriver något i stil med detta:
float a =
10.0F;
int b;
b = (int)KVADRAT(a);
Här skulle man kunna tro att resultatet av kvadreringen, vilket är något av typen 'float', skulle typomvandlas till 'int' före tilldelning till b. Tyvärr uppfattar kompilatorn uttrycket så här i stället:
b = (int)a * a;
...vilket uppfattas som att den första förekomsten av variabeln a ska typomvandlas till heltal, inte resultatet av beräkningen.
För att slippa att tänka på vilka problem som kan uppstå är det bäst att 'klämma till' med parenteser överallt där det är möjligt. Så här blir den slutgiltiga versionen av vårt makro:
#define KVADRAT(X) ((X) * (X))
Här följer ett exempel att studera. Makrot beräknar kuben av ett tal:
#include
<iostream.h>
#define
KUB(X) ((X)*(X)*(X))
void
main(void)
{
float fSida = 1.0F, fVolym;
cout << "Jag beräknar kuber åt dig.\n\n";
while(fSida > 0)
{
cout << "Ange kubens sida: ";
cin >> fSida;
if(fSida > 0)
{
fVolym = KUB(fSida);
cout << "Kubens volym = " <<
fVolym << ".\n\n";
}
}
cout << "\n\n";
}
Så här kan ett körexempel se ut:
Övningsuppgift 13.2.2:
· Skapa ett nytt projekt Moms.
· Skapa ett makro som heter VARAVMOMS, vilket beräknar hur mycket moms som finns inräknat i ett pris.
· Använd momssats 25%, men tänk på att ett pris på vilket man lagt på 25% moms inte består av 25% moms plus nettopris.
· Gör en programslinga som frågar efter priset inklusive moms, och som använder makrot till att beräkna priset exklusive moms, för att sedan redovisa detta. Programslingan ska kunna avbrytas genom att användaren anger 0 som pris.
· Så här ska det kunna se ut:
Det går att ange fler argument. Vi ska dock inte gå närmre in på dessa här, men eftersom det förekommer flitigt i standardpaket ska vi se på ett makro som Microsoft levererat med MFC:
#define
_IMPLEMENT_RUNTIMECLASS(class_name, \
base_class_name, wSchema, pfnNew) \
CRuntimeClass* PASCAL
class_name::_GetBaseClass() \
{ return RUNTIME_CLASS(base_class_name);
} \
AFX_DATADEF CRuntimeClass
class_name::class##class_name \
={#class_name, sizeof(class class_name),
wSchema, pfnNew, \
&class_name::_GetBaseClass, NULL };
\
static const AFX_CLASSINIT \ _init_##class_name(&class_name::class##class_name);
\
CRuntimeClass* class_name::GetRuntimeClass()
const \
{ return
&class_name::class##class_name; } \
Observera användningen av radfortsättningstecknet. Utan att göra anspråk på att kunna förstå hur detta makro fungerar, kan man se hur de argument som deklarerats i argumentlistan, class_name, base_class_name, wSchema samt pfnNew, förekommer i den text som ska läsas av kompilatorn. Ett annat, kanske inte fullt så hiskligt exempel är detta, vilket bara tar ett argument.:
#define
AFX_ZERO_INIT_OBJECT(base_class) \
memset(((base_class*)this)+1, 0,
sizeof(*this) \
- sizeof(class base_class));
Polymorfism är utrikiska och betyder 'flerformig'. När det gäller C++ programmering talar man också om 'överlagrade funktioner' (overloaded functions).
Vad menar man då med 'flerformig' respektive
'överlagrad'? Som vanligt gäller det att skapa nya termer, vilka på effektivast
möjliga sätt håller novisen på mattan. (It's done only to confuse the Russians.)
Det hela handlar egentligen bara om att ge en funktion flera olika former. En funktion behöver inte längre hålla sig till en specifik argumentlista, utan kan ha flera olika argumentlistor. I själva verket har man flera olika funktioner med samma namn. Kompilatorn kan skilja dem åt genom att analysera argumentlistan. En funktion 'Byt()' vilken tar två heltal som argument är inte densamma som en funktion 'Byt()' vilken tar två flyttal som argument. Om vi har dessa två funktioner tillgängliga, och anropar Byt med två heltal som argument, kommer kompilatorn automatiskt att välja rätt funktion. Man kan se det så att själva datatyperna i argumentlistan på sätt och vis ingår i funktionens namn.
Kommer ni ihåg övningsexemplet 8.6.2, från kapitel 8, den som byter värden mellan två variabler av samma typ. Vi var tvungna att skriva en ibyt(), en cbyt(), och en fbyt. Vad vi nu kan göra är att ge dem alla samma namn. Kompilatorn skiljer dem åt genom att kontrollera argumentens typ.
Övningsuppgift 13.3.1:
· Öppna projektet MyHead.
· Ändra funktionerna så att alla heter byt().
· Gör samma ändringar vid anropen.
· Kompilera, länka och testa. Programmet ska fungera som tidigare.
· Observera att funktionerna ucase() och lcase() inte har med av denna ändring. att göra.
Överlagrade funktioner används mycket i samband med klasser och objekt, vilket vi som sagt var ska titta på senare, vilket just nu betyder i nästa kapitel.