A C függvények nagyban hasonlítanak a C# metódusaira, de van különbség. Ha definiálunk egy paraméter nélküli FunctionTest() függvényt, akkor azt meg tudjuk hívni paraméterekkel is, annak ellenére, hogy nem definiáltunk bemeneti paramétereket. Ezért ha tényleg azt szeretnénk, hogy egy függvény ne fogadjon paramétereket, akkor a void kulcsszót ki kell tennünk a függvény paraméterlistájába:
/*Ez a függvény már nem fogad argumentumot*/
void FunctionTest2(void)
{
printf("Ez egy fuggveny");
}
Az értékek átadása a függvények esetén alapértelmezetten érték szerint történik, vagyis a függvény nem a változót kapja meg, hanem annak az értékét. Ezért ha a függvény módosítja a kapott paramétert, akkor az nem lesz hatással az eredeti változóra. De történhet referencia szerinti átadás is. Ez pointerek segítségével van megoldva. A függvény a paramétert pointerként definiálja, a hívó pedig az & operátorral a változó memóriacímét adja át a függvénynek. Erre a viselkedésre láthattunk már példát a programjainkban a scanf különböző variánsainak használatakor.
A tömbök és a struktúrák átadhatóak referenciaként és értékként is. Viszont arra ügyelni kell, hogy a tömbök esetén csak az első elemre mutató pointer értéke fog lemásolódni, nem a teljes memóriaterület. A struktúráknál a teljes memóriatartalom másolódni fog, ha érték szerinti átadás történik. Az alábbi program az érték és referencia szerinti átadást mutatja be:
#include<stdio.h>
void increment(int x) {
x++;
}
void incrementByReference(int *x) {
(*x)++;
}
int main()
{
int num = 5;
printf("eredeti: %d\r\n", num);
increment(num);
printf("increment() hivas utan: %d\r\n", num);
incrementByReference(&num);
printf("incrementByReference() hivas utan: %d\r\n", num);
return 0;
}
A program kimenete:
eredeti: 5
increment() hivas utan: 5
incrementByReference() hivas utan: 6
A tömbök mutatóként kerülnek átadásra. A függvény a tömb első elemét kapja meg mutatóként, a méretéről azonban nem kap információt. Ennek az átadásáról a programozónak kell gondoskodnia:
#include <stdio.h>
/*A tömb pointer! Méretet is kell vele adni*/
void modifyArray(int arr[], size_t size) {
/*mivel pointer, módosítható is az érték*/
for (size_t i = 0; i < size; i++) {
arr[i] *= 2;
}
}
int main()
{
int numbers[] = {1, 2, 3, 4, 5};
size_t size = sizeof(numbers) / sizeof(int);
modifyArray(numbers, size);
for (size_t i=0; i<size; i++)
{
printf("%d ", numbers[i]);
}
return 0;
}
A program kimenete:
2 4 6 8 10
A referenciaátadás esetén problémás lehet, ha csak úgy passzolgatjuk a referenciáinkat, mert nem tudhatjuk, hogy mi módosítja azt és mi nem. Éppen ezért, ha referenciát vár a függvényünk és jelezni szeretnénk azt, hogy nem fogja módosítani a referenciát, akkor tegyük ki a const kulcsszót a paraméter elé. Ha az előző programban módosítjuk a modifyArray függvényt a const kulcsszóval, akkor fordítási hibát fogunk kapni a *= 2 kifejezésnél.
void modifyArray(const int arr[], size_t size) {
for (size_t i = 0; i < size; i++) {
arr[i] *= 2;
}
}
A const kulcsszó konstansok definiálására szolgál, azonban nagyon nem mindegy, hogy hol helyezkedik el a kulcsszó mutatók esetén, ha paraméterekről van szó:
const int *x;
int const *x;
A fenti két deklaráció ugyanazt jelenti, mégpedig azt, hogy az x által mutatott memória konstans és nem módosítható. Azonban, ha a const a * után szerepel, akkor teljesen más jelentéssel bír:
int *const x;
Ez azt jelenti, hogy az x mutató a konstans és nem az általa mutatott memória. Ez gyakorlatban azt jelenti, hogy x értéke nem módosítható az értékadó (=) operátorral, de az x által mutatott memóriaterület igen.
A const kettőssége akár kombinálható is:
int const * const x;
A fenti esetben x által mutatott memória és a pointer is konstans lesz. Gyakorlati haszna ugyan nincs, de a const kulcsszó viselkedéséből adódik a lehetősége.
C függvények esetén még egy fontos téma a függvényhívás módszere. Ebből három létezik és bináris kompatibilitás miatt fontos tudni, hogy ha egy DLL fájlba belehívunk, akkor a paramétereknek hogyan kell majd a memóriában elhelyezkedniük, illetve kinek a felelőssége a stack eltakarítása a hívás után. Ha nem jelezzük egy metódus esetén a hívási módszert, akkor a __cdecl hívási módszer kerül alkalmazásra. Ez a C alapértelmezett viselkedése. DLL fájlokba hívás és Windows API esetén az __stdcall hívási módszer preferált.
Ezen felül létezik még a __fastcall hívási módszer, ami a 64 bites x64 processzorok esetén elveszítette a relevanciáját. Régen, x86-ra fordított kódok esetén olyan függvényeknél alkalmazták őket, amik kevés paraméterrel dolgoztak és kritikus volt, hogy a hívás ideje ne tartson sokáig. Ennek ellenére X64-re fordított kód esetén is működik, de leginkább csak kompatibilitási okokból tartották meg, tényleges előnnyel modern hardveren nem jár a használata.
| Hívási mód | Stack takarítás | Paraméterek |
|---|---|---|
__cdecl |
Hívó | Paraméterek a stack-en, fordított sorrendben (jobbról balra) |
__stdcall |
Hívott | Paraméterek a stack-en, fordított sorrendben (jobbról balra) |
__fastcall |
Hívott | Paraméterek regiszterben, majd a stack-en |
Változó argumentumszámú függvények
Hasonlóan a C#-hoz a C is lehetőséget ad változó argumentumszámú függvények definiálására. Ehhez az stdarg.h headerre lesz szükségünk. Nézzünk egy példát:
#include <stdio.h>
#include <stdarg.h>
double average(int arg_count, ...)
{
va_list valist;
double sum = 0.0;
/* változó argumentumszám feldolgozásának kezdete */
va_start(valist, arg_count);
/* argumentumok kiolvasása */
for (int i = 0; i < arg_count; i++)
{
sum += va_arg(valist, int);
}
/* argumentum lista felszabadítása*/
va_end(valist);
return sum / arg_count;
}
int main()
{
printf("average(1, 2, 3, 4, 5) = %f\r\n", average(5, 1, 2, 3, 4, 5));
return 0;
}
A változó argumentumszámú függvények esetén egy argumentumban definiálnunk és átadnunk kell a paraméterek számát. Ezt követően a ... karakterekkel jelezhetjük, hogy a függvény korlátlan számú paramétert fogad. A va_start függvényhívással olvashatjuk ki ezeket az argumentumokat. Ez egy va_list típusban tárolja majd ezeket, aminek az első paraméternek kell lennie. A második paraméter a definiált paraméterek száma.
Ezt követően a va_arg n alkalommal való meghívásával megszerezhetjük az n darab argumentumot. A va_end hívással pedig felszabadíthatjuk a függvényargumentumokat. A hívásnál látható, hogy 6 számot adunk át. Ebből az első a további paraméterek száma.
A program kimenete:
average(1, 2, 3, 4, 5) = 3.000000
Inline
Függvények esetén lehetőségünk van az inline módosító használatára. Ez a kulcsaszó egyfajta ajánlásként működik a fordító számára, vagyis a fordító dönthet úgy, hogy figyelmen kívül hagyja.
Az inline módosítóval azt ajánljuk a fordítónak, hogy ha lehetősége van rá, akkor a függvényt ne függvényhívással hívja meg, hanem a generált kódot a hívás helyére másolja be.
Ez bizonyos platformok esetén hatalmas előnnyel járhat, mert a függvényhívásnak van egy költsége és ha egy függvényt sokszor hívunk, akkor a hívás költsége jóval nagyobb lehet, mint a tényleges végrehajtási ideje.
Azonban a függvény kódjának beágyazása negatív hatású is lehet, mivel függvényhívás helyett minden hívási helyre bemásolódik a kód, ami végső soron a program méretét növeli. Ez beágyazott rendszerek esetén okozhat problémát, ahol a memóriát még mindig KiB vagy esetlegesen pár MiB méretben mérik.
Mivel a mai asztali számítógépek esetén a függvényhívás költsége elhanyagolható, az inline kulcsszónak nagyon sok hatása nincs, sőt a modern fordítók egy az egyben ignorálják és a választott optimalizációs szinttől függően dönthetnek úgy, hogy az inline kulcsszóval megjelölt függvényt nem ágyazzák be, de egy másik nem jelöltet pedig igen.
/*ajánlás a fordítónak*/
inline int squareGcc(int num) {
return num*num;
}
Előfordulhat, hogy optimalizációs céllal mégis ki szeretnénk kényszeríteni egy függvény beágyazását. Ha erre lenne szükségünk, akkor Visual Studio alatt a __forceinline használható, míg GCC esetén a __attribute__((always_inline)) módosítóval kell ellátnunk a függvényünket.
Megjegyzés: Ezen megoldások használata nem ajánlott, csak akkor, ha mindenképpen szükség van rá.
/*beágyazás kikényszerítése GCC és Clang esetén*/
inline __attribute__((always_inline)) int squareGcc(int num) {
return num*num;
}
/*beágyazás kikényszerítése Visual Studio esetén*/
inline __forceinline int square(int num) {
return num*num;
}