KURZUS: Programozás alapjai

MODUL: III. modul

8. lecke: Objektumok és függvények

Ebben a fejezetben pontosítjuk az objektumok különféle attribútumaival (pl. típus, tárolási osztály, hatókör) kapcsolatos ismereteinket. Ezen kívül megismerkedünk a register tárolási osztállyal, mely bizonyos körülmények között segít gyorsítani programjainkat. A static kulcsszóval függvényeink "emlékezhetnek" korábbi hívások során felvett állapotukra, illetve bizonyos azonosítók hatáskörét a forrásfájlra korlátozhatjuk. Megnézzük, milyen előnyöket rejt a függvény prototípusok használata, mikor nem egyezik az azonosítók hatóköre és láthatósága, illetve milyen kapcsolódással rendelkeznek objektumaink és függvényeink. Foglalkozunk rekurzív függvények írásával is.

A lecke végére a hallgatónak tisztában kell lennie

  • Az objektumok és függvények összes attribútumával, az ezek által felvehető értékekkel, az attribútumok értékének szabályozását elvégző szabályokkal és kulcsszavakkal, valamint az attribútum értékek hatásával.
  • A hallgatónak értenie kell a deklaráció, definíció, és függvények esetén a prototípus fogalma közti különbséget, értenie kell, milyen ellenőrzéseket képes végrehajtani a fordító a prototípus birtokában, és miért kell a függvények használata előtt gondoskodni deklarációjukról/prototípusukról, vagy könyvtári függvények esetén a megfelelő fejfájl beszerkesztéséről.
  • Képesnek kell lennie létrehozni egyszerűbb önrekurzív függvényeket.

A lecke kulcsfogalmai: objektum, attribútum, típus, tárolási osztály, hatáskör, láthatóság, élettartam, kapcsolódás, névterület, deklaráció, definíció, prototípus, heap.

Az objektumok formális szintaktikai szabályai

Az azonosítók "értelmét" a deklarációk rögzítik. Tudjuk, hogy a deklaráció nem jelent szükségképpen memóriafoglalást. Csak a definíciós deklaráció ilyen.

deklaráció:
  deklaráció-specifikátorok<init-deklarátorlista>

init-deklarátorlista:
  init-deklarátor
  init-deklarátorlista, init-deklarátor

init-deklarátor:
  deklarátor
  deklarátor=inicializátor

Az inicializátorokkal és inicializátorlistákkal az Alapismeretek 2/2 lecke Inicializálás fejezetében foglalkoztunk. A deklarátorok a deklarálandó neveket tartalmazzák. A deklaráció-specifikátorok típus és tárolási osztály specifikátorokból állnak:

deklaráció-specifikátorok:
  tárolási-osztály-specifikátor<deklaráció-specifikátorok>
  típusspecifikátor<deklaráció-specifikátorok>
  típusmódosító<deklaráció-specifikátorok>

A típusspecifikátorokat a Típusok és konstansok leckében tárgyaltuk, s ezek közül kivettük a const és a volatile típusmódosítókat.

Objektumok attribútumai

Az objektum egy azonosítható memória területet, mely konstans vagy változó érték(ek)et tartalmaz. Az objektum egyik attribútuma (tulajdonsága) az adattípusa. Tudjuk, hogy van ezen kívül azonosítója (neve) is. Az objektum adattípus attribútuma rögzíti az objektumnak

  • allokálandó (lefoglalandó) memória mennyiségét és
  • a benne tárolt információ belsőábrázolási formáját.

Az objektum neve nem attribútum, hisz különféle hatáskörben több különböző objektumnak is lehet ugyanaz az azonosítója. Az objektum további attribútumait (tárolási osztály, hatáskör, láthatóság, élettartam stb.) a deklarációja és annak a forráskódban elfoglalt helye határozza meg.

Tárolási osztályok

Az objektumokhoz rendelt azonosítóknak van legalább

  • tárolási osztály és
  • adattípus

attribútuma. Szokás e kettőt együtt is adattípusnak nevezni. A tárolási osztály specifikátor definíciója:

tárolási-osztály-specifikátor:
  auto
  register
  extern
  static
  typedef

A tárolási osztály meghatározza az objektum élettartamát, hatáskörét és kapcsolódását. (A kapcsolódásra még ebben a leckében visszatérünk.) Egy adott objektumnak csak egy tárolási osztály specifikátora lehet egy deklarációban. A tárolási osztályt a deklaráció forráskódbeli elhelyezése implicit módon rögzíti, de a megfelelő tárolási osztály kulcsszó expliciten is beleírható a deklarációba.

Tárolási osztály kulcsszó nélküli deklarációk esetében a blokkon belül deklarált

  • objektum mindig auto definíció, és
  • a függvény pedig extern deklaráció.

A függvénydefiníciók és az ezeken kívüli objektum és függvénydeklarációk mind extern, statikus tárolási osztályúak.

Kétféle tárolási osztály van.

Automatikus (auto, register) tárolási osztály

Az ilyen objektumok lokális élettartamúak, és lokálisak a blokk egy adott példányára. Az ilyen deklarációk definíciók is egyben, azaz megtörténik a memóriafoglalás is. Ismeretes, hogy a függvényparaméterek is automatikus tárolási osztályúaknak minősülnek. Rekurzív (önmagukat közvetlenül vagy közvetve hívó függvények) kód esetén az automatikus objektumok garantáltan különböző memória területen helyezkednek el mindenegyes blokkpéldányra.

A C az automatikus objektumokat a program vermében tárolja, s így alapértelmezett kezdőértékük "szemét". Expliciten inicializált, lokális automatikus objektum esetében a kezdőérték adás viszont mindannyiszor megtörténik, valahányszor bekerül a vezérlés a blokkba. A blokkon belül definiált objektumok auto tárolási osztályúak, hacsak ki nem írták expliciten az extern vagy a static kulcsszót a deklarációjukban. Csak lokális hatáskörű objektumok deklarációjában használható az auto tárolási osztály specifikátor (tehát tilos külső deklarációban vagy definícióban alkalmazni).

Az automatikus tárolási osztályú objektumok lokális élettartamúak és nincs kapcsolódásuk. Miután ez az alapértelmezés az összes lokális hatáskörű objektum deklarációjára, nem szokás és szükségtelen expliciten kiírni.

Az auto tárolási osztály specifikátor függvényre nem alkalmazható!

Az automatikus tárolási osztály speciális válfaja a regiszteres. A register kulcsszó a deklarációban azt jelzi a fordítónak, hogy

  • a változót nagyon gyakran fogjuk használni, és
  • kérjük, hogy az illető objektumot regiszterben helyezze el, ha lehetséges.

A regiszteres tárolás rövidebb gépi kódú programot eredményez, hisz elmarad a memóriából regiszterbe (és vissza) töltögetés. Emiatt, és mert a regiszter a memóriánál jóval kisebb elérési idejű, a szoftver futása is gyorsul.

A hardvertől függ ugyan, de valójában csak kevés objektum helyezkedhet el regiszterben, és csak meghatározott típusú változók kerülhetnek oda. A fordító elhagyja a register kulcsszót a felesleges és a nem megfelelő típusú deklarációkból, azaz az ilyen változók csak normál, automatikus tárolási osztályúak lesznek.

A regiszteres objektumok lokális élettartamúak, és ekvivalensek az automatikus változókkal. Csak lokális változók és függvényparaméterek deklarációjában alkalmazható a register kulcsszó.

Külső deklarációban vagy definícióban a register kulcsszót tilos alkalmazni! Függetlenül attól, hogy a register változó valóban regiszterben helyezkedik el, vagy sem, tilos a címére hivatkozni!

Írjunk int prime(int x) függvény, mely eldönti pozitív egész paraméteréről, hogy prímszám-e!

A prímszám csak 1-gyel és önmagával osztható maradék nélkül. Próbáljuk meg tehát egész számok szorzataként előállítani. Ha sikerül, akkor nem törzsszámról van szó. A szám prím viszont, ha ez nem megy. Indítunk tehát 2-ről mindig növelgetve egy osz változót, és megpróbáljuk, hogy osztható-e vele maradék nélkül az x.

Meddig növekedhet az osz? x négyzetgyökéig, mert a két szorzótényezőre bontásnál fordított arányosság van a két tényező között.

int prime(register x){
  register osz = 2;
  if(x < 4) return 1;
  while(osz*osz <= x){
    if(!(x%osz)) return 0;
    ++osz;
    if(!(osz&1)) ++osz; }
  return 1; }

A prime paramétere és az osz lokális változó register int típusú programgyorsítási céllal. A függvény utolsó előtti sorából látszik, hogy legalább a páros számokat nem próbáljuk ki osztóként, miután 2-vel nem volt maradék nélkül osztható az x.

Az olvasóra bízzuk, hogy kísérje meg még gyorsítani az algoritmust! (Megjegyezzük, hogy a modern PC-k nagyon gyorsan végeznek az osztásokkal, de egyszerűbb architektúrákon ez általában lassú művelet. A prímszámok keresésének gyorsabb, osztásokat nélkülöző, de memóriaigényes módja az Eratoszthenész szitája néven ismert algoritmus.)

Készítsünk programot, mely megállapítja egy valós számsorozat átlagát és azt, hogy hány átlagnál kisebb és nagyobb eleme van a sorozatnak! A valós számokat a szabvány bemenetről kell beolvasni! Egy sorban egyet! A sorozat megadásának végét jelentse üres sor érkezése a bemenetről!

Kezdjük a kódolást az int lebege(char s[]) függvénnyel, mely megállapítja karakterlánc paraméteréről, hogy formálisan helyes lebegőpontos szám-e! A karaktertömb elején levő fehér karaktereket át kell lépni, és a numerikus rész ugyancsak fehér, vagy lánczáró zérus karakterrel zárul. A lebegőpontos számnak ki kell egyébként elégítenie a lebegőpontos konstans írásszabályát!

#include <ctype.h>
int lebege(char s[]){
  int i=0, kezd;
  /* Fehér karakterek átlépése a lánc elején: */
  while(isspace(s[i])) ++i;
  /* A mantissza előjele: */
  if(s[i]=='+'||s[i]=='-') ++i;
  kezd=i; /* A számjegyek itt kezdődnek. */
  /* A mantissza egész része: */
  while(isdigit(s[i])) ++i;
  /* A mantissza tört része: */
  if(s[i]=='.') ++i;
  while(isdigit(s[i])) ++i;
  /* Nincs számjegy, vagy csak egy . van: */
  if(i==kezd||kezd+1==i&&s[kezd]=='.') return 0;
  /* Kitevő rész: */
  if(toupper(s[i])=='E'){
    ++i;
    if(s[i]=='+'||s[i]=='-')++i;
    /* Egy számjegynek lennie kell a kitevőben! */
    if(!isdigit(s[i])) return 0;
    while(isdigit(s[i])) ++i;}
  /* Vége: */
  if(isspace(s[i])||!s[i]) return 1;
  else return 0; }
Statikus (static, extern) tárolási osztály

A statikus tárolási osztályú objektumok kétfélék:

  • blokkra lokálisak, vagy
  • blokkokon át külsők.

Az ilyen tárolási osztályú objektumok statikus élettartamúak. Bárhogyan is: megőrzik értéküket az egész program végrehajtása során, akárhányszor is hagyja el a vezérlés az őket tartalmazó blokkot, és tér oda vissza. Függvényen, blokkon belül úgy definiálható statikus tárolási osztályú változó, hogy a deklarációjába ki kell expliciten írni a static kulcsszót. A static kulcsszavas deklaráció definíció.

Készítsünk double gyujto(double a) függvényt, mely gyűjti aktuális paraméterei értékét! Mindig az eddig megállapított összeget adja vissza. Ne legyen "bamba", azaz ne lehessen vele "áttörni" az ábrázolási határokat! (A lebegőpontos túl-, vagy alulcsordulás futásidejű hiba!)

#include <float.h>
double gyujto(double a){
  static double ossz=0.0;
  if(a<0.0&&-(DBL_MAX+a)<ossz || DBL_MAX-a>ossz)
    ossz+=a;
  return ossz; }

Látszik, hogy nincs összegzés, ha az ossz -DBL_MAX-hoz, vagy +DBL_MAX-hoz a távolságon belülre kerül.

Rekurzív kódban a statikus objektum állapota garantáltan ugyanaz minden függvénypéldányra.

Explicit inicializátorok nélkül a statikus változók minden bitje zérus kezdőértéket kap. Az implicit és az explicit inicializálás még lokális statikus objektum esetén is csak egyszer történik meg, a program indulásakor.

Éppen emiatt a gyujto-ben teljesen felesleges zérussal inicializálni a statikus ossz változót, hisz implicit módon is ez lenne a kezdőértéke.

A függvénydefiníciókon kívül elhelyezett, tárolási osztály kulcsszó nélküli deklarációk külső, statikus tárolási osztályú objektumokat definiálnak, melyek globálisak az egész programra nézve.

A külső objektumok statikus élettartamúak. Az explicit módon extern tárolási osztályúnak deklaráltak olyan objektumokat deklarálnak, melyek definíciója nem ebben a fordítási egységben van, vagy befoglaló hatáskörben található.

extern int MasholDefinialt; /* Más ford. egységben */
void main(){
  int IttDefiniált;
  {
    extern int IttDefiniált;
    /* A befoglaló hatáskörbeli IttDefiniált-ra való
      hivatkozás. */ } }

A külső kapcsolódást jelölendő az extern függvény és objektum fájl és lokális hatáskörű deklarációiban használható. Fájl hatáskörű változók és függvények esetében ez az alapértelmezés, tehát expliciten nem szokás kiírni. Az extern kulcsszó explicit kiírása tilos a változó definiáló deklarációjában!

A külső objektumok és a függvények is deklarálhatók static-nek, amikor is lokálissá válnak az őket tartalmazó fordítási egységre, és minden ilyen deklaráció definíció is egyben.

Folytatva a példánkat: a szabvány bemenetről érkező, valós számokat valahol tárolni kéne, mert az átlag csak az összes elem beolvasása után állapítható meg. Ez után újra végig kell járni a számokat, hogy kideríthessük, hány átlag alatti és feletti van köztük.

A változatosság kedvéért, és mert a célnak tökéletesen megfelel, használjunk vermet a letároláshoz, melyet és kezelő függvényeinek definícióit helyezzük el a dverem.c forrásfájlban, és a más fordítási egységből is hívható függvények prototípusait tegyük be a dverem.h fejfájlba!

Egy témakör adatait és kezelő függvényeit egyébként is szokás a C-ben külön forrásfájlban (úgy nevezett implementációs fájlban) elhelyezni, vagy a lefordított változatot külön könyvtárfájlba tenni.

A dologhoz mindig tartozik egy fejfájl is, mely tartalmazza legalább a témakör más forrásmodulból is elérhető adatainak deklarációit, és kezelő függvényeinek prototípusait. Implementációs fájl esetén a fejfájlban még típusdefiníciók, szimbolikus állandók, makrók stb. szoktak lenni.

/* DVEREM.H: double verem push, pop és clear
            fuggvenyenek prototipusai. */
int clear(void);
double push(double x);
double pop(void);
/* DVEREM.C: double verem push, pop és clear
            fuggvenyekkel. */
#define MERET 128 /* A verem merete. */
static int vmut;  /* A veremmutato. */
static double v[MERET]; /* A verem. */
int clear(void){
  vmut=0;
  return MERET; }
double push(double x){
  if(vmut<MERET) return v[vmut++]=x;
  else return x+1.; }
double pop(void){
  if(vmut>0) return v[--vmut];
  else return 0.; }

A vmut veremmutató, és a v verem statikus ugyan, de lokális a dverem.c fordítási egységre. Látszik, hogy a push x paraméterével tölti a vermet, és sikeres esetben ezt is adja vissza. Ha a verem betelt, más érték jön vissza tőle. A pop visszaszolgáltatja a legutóbb betett értéket, ill. az üres veremből mindig zérussal tér vissza. A clear törli a veremmutatót, s ez által a vermet, és visszaadja a verem maximális méretét.

Kódoljuk le végre az eredetileg kitűzött feladatot!

/* PELDA20.C: Valos szamok atlaga, és az ez alatti, ill.
              feletti elemek szama. */
#include <stdio.h>
#include <stdlib.h> /* Az atof miatt! */
#define INP 60      /* Az input puffer merete. */
int getline(char s[],int lim);
#include <ctype.h>
int lebege(char s[]);
#include <float.h>
double gyujto(double a);
#include "dverem.h"
void main(void){
  int max=clear(),/* A verem max. merete. */
      i=0, alatt, felett;
  char s[INP+1]; /* Az input puffer. */
  double a;
  printf("Szamsorozat atlaga alatti és feletti "
        "elemeinek szama.\nA megadast ures "
        "sorral kell befejezni!\n\n");
  while(printf("%4d. elem: ", i+1), i<max &&
    getline(s,INP)>0)
    if(lebege(s)){
      push(a=atof(s));
      printf("Az osszeg:%30.6f\n\n", a=gyujto(a));
      ++i;}
    printf("\nAz atlag: %30.6f\n", a/=i);
    for(max=alatt=felett=0; max<i; ++max){
      double b=pop();
      if(b<a) ++alatt;
      else if(b>a) ++felett; }
    printf("Az atlag alattiak szama: %8d.\n"
          "Az atlag felettiek szama:%8d.\n",
          alatt, felett); }

Vigyázat: a példa a pelda20.c-ből és a dverem.c-ből képzett projekt segítségével futtatható csak!

Élettartam (lifetime, duration)

Az élettartam attribútum szorosan kötődik a tárolási osztályhoz, s az a periódus a program végrehajtása közben, míg a deklarált azonosítóhoz objektumot allokál a fordító a memóriában, azaz amíg a változó vagy a függvény létezik. Megkülönböztethetünk

  • fordítási idejű és
  • futásidejű objektumokat.

A változók például a típusoktól és a típusdefinícióktól eltérően futás időben valós, allokált memóriával rendelkeznek. Három fajta élettartam van.

Statikus (static vagy extern) élettartam

Az ilyen objektumokhoz a memória hozzárendelés a program futásának megkezdődésekor történik meg, s az allokáció marad is a program befejeződéséig. Minden függvény statikus élettartamú objektum bárhol is definiálják őket. Az összes fájl hatáskörű változó is ilyen élettartamú. Más változók a static vagy az extern tárolási osztály specifikátorok explicit megadásával tehetők ilyenné. A statikus élettartamú objektumok minden memória bitje (a függvényektől eltekintve) zérus kezdőértéket kap explicit inicializálás hiányában.

Ne keverjük össze a statikus élettartamot a fájl (globális) hatáskörrel, ui. egy objektum lokális hatáskörrel is lehet statikus élettartamú, csak deklarációjában meg kell adni expliciten a static tárolási osztály kulcsszót.

Lokális (auto vagy register) élettartam

Ezek az objektumok akkor jönnek létre (allokáció) a veremben vagy regiszterben, amikor a vezérlés belép az őket magába foglaló blokkba vagy függvénybe, s meg is semmisülnek (deallokáció), mihelyt kikerül a vezérlés innét. A lokális élettartamú objektumok lokális hatáskörűek, és mindig explicit inicializálásra szorulnak, hisz létrejövetelük helyén "szemét" van. Ne feledjük, hogy a függvényparaméterek is lokális élettartamúak!

Az auto tárolási osztály specifikátor deklarációban való kiírásával expliciten lokális élettartamúvá tehetünk egy változót, de erre többnyire semmi szükség sincs, mert blokkon vagy függvényen belül deklarált változók esetében az alapértelmezett tárolási osztály amúgy is az auto.

A lokális élettartamú objektum egyben lokális hatáskörű is, hisz az őt magába foglaló blokkon kívül nem létezik. A dolog megfordítása nem igaz, mert lokális hatáskörű objektum is lehet statikus élettartamú.

Ha egy regiszterben is elférő változót (például char, short stb. típusút) expliciten register tárolási osztályúnak deklarálunk, akkor a fordító ehhez hozzáérti automatikusan az auto kulcsszót is, hisz a változókat csak addig tudja regiszterben elhelyezni, míg azok el nem fogynak, s ezután a veremben allokál nekik memóriát.

Dinamikus élettartam

Az ilyen objektumokhoz a C-ben például a malloc függvénnyel rendelhetünk memóriát a heap-en, amit aztán a free-vel felszabadíthatunk. (A heap-nek többféle jelentése is van, itt azt a memóriaterületet értjük alatta, amiből a programozó tetszőleges célra explicit módon lefoglalhat valamennyit, felhasználhatja azt, majd végül fel kell szabadítania.) Miután a memóriaallokáláshoz könyvtári függvényeket használunk, és ezek nem részei a nyelvnek, így a C-ben nincs is dinamikus élettartam igazából.

Hatáskör (scope) és láthatóság (visibility)

A hatáskör - érvényességi tartománynak is nevezik - az azonosító azon tulajdonsága, hogy vele az objektumot a program mely részéből érhetjük el. Ez is a deklaráció helyétől és magától a deklarációtól függő attribútum. Felsoroljuk őket!

Blokk (lokális, belső) hatáskör

A deklarációs ponttól indul és a deklarációt magába foglaló blokk végéig tart. Az ilyen hatáskörű változókat szokás belső változóknak is nevezni. Lokális hatáskörűek a függvények formális paraméterei is, s hatáskörük a függvénydefiníció teljes blokkja. A blokk hatáskörű azonosító hatásköre minden, a kérdéses blokkba beágyazott blokkra is kiterjed. Például:

int fv(float lo){
  double szamar; /* A lokális hatáskör itt indul. */
  /* . . . */
  long double oszver; /* Az oszver hatásköre innét
    (deklarációs pont) indul és a függvénydefiníció
    végéig tart. Szóval nem lehetne a szamar változót
    az oszver-rel inicializálni. */
  if (/* feltétel */){
    char lodarazs = 'l'; /* A lodarazs hatásköre
                            ez a belső blokk. */
    /* . . . */
  } /* A lodarazs hatáskörének vége. */
  /* . . . */
} /* A lo, szamar és oszver hatáskörének vége. */
Függvény hatáskör

Ilyen hatásköre csak az utasítás címkének van. Az utasítás címke ezen az alapon: függvényen belüli egyedi azonosító, melyet egy olyan végrehajtható utasítás elé kell írni kettőspontot közbeszúrva, melyre el kívánunk ágazni. Például:

int fv(float k){
  int i, j;
  /* . . . */
  cimke: utasítás;
  /* . . . */
  if (/* feltétel */) goto cimke;
  /* . . . */ }
Függvény prototípus hatáskör

Ilyen hatásköre a prototípusban deklarált paraméterlista azonosítóinak van, melyek tehát a függvény prototípussal be is fejeződnek. Például a következő függvénydefinícióban az i, j és k azonosítóknak van függvény prototípus hatásköre:

void fv(int i, char j, float k);

Az i, j és k ilyen megadásának semmi értelme sincs. Az azonosítók teljesen feleslegesek. A

void fv(int, char, float);

ugyanennyit "mondott" volna. Függvény prototípusban neveket akkor célszerű használni, ha azok leírnak valamit. Például a

double KamatOsszeg(double osszeg, double kamat, int evek);

az osszeg kamatos kamatát közli evek évre.

Fájl (globális, külső) hatáskör

A minden függvény testén kívül deklarált azonosítók rendelkeznek ilyen hatáskörrel, mely a deklarációs pontban indul és a forrásfájl végéig tart. Ez persze azt is jelenti, hogy a fájl hatáskörű objektumok a deklarációs pontjuktól kezdve minden függvényből és blokkból elérhetők. A globális változókat szokás külső változóknak is nevezni. Például a g1, g2 és g3 változók ilyenek:

int g1 = 7; /* g1 fájl hatásköre innét indul. */
void main(void) { /* ... */ }
float g2; /* g2 fájl hatásköre itt startol. */
void fv1(void) { /* ... */ }
double g3 = .52E-40;/* Itt kezdődik g3 hatásköre */
void fv2(void) { /* ... */ }
/* Itt van vége a forrásfájlnak és a g1, g2 és g3 külső
  változók hatáskörének. */
Láthatóság

A forráskód azon régiója egy azonosítóra vonatkozóan, melyben legális módon elérhető az azonosítóhoz kapcsolt objektum. A hatáskör és a láthatóság többnyire fedik egymást, de bizonyos körülmények között egy objektum ideiglenesen rejtetté válhat egy másik ugyanilyen nevű azonosító feltűnése miatt. A rejtett objektum továbbra is létezik, de egyszerűen az azonosítójával hivatkozva nem érhető el, míg a másik ugyanilyen nevű azonosító hatásköre le nem jár. Például:

{ int i; char c = 'z'; /* Az i és c hatásköre indul. */
  i = 3; /* int i-t értük el. */
  /* . . . */
  { double i = 3.5e3; /* double i hatásköre itt kezdődik,
                        s elrejti int i-t, bár */
    /* . . . */      /* hatásköre nem szűnik meg. */
    c = 'A'; /* char c látható és hatásköre
                itt is tart. */
  } /* A double i hatáskörének vége */
  /* int i és c hatáskörben és láthatók. */
  ++i;
} /* int i és char c hatáskörének vége. */
Névterület (name space)

Az a "hatáskör", melyen belül az azonosítónak egyedinek kell lennie. Más névterületen konfliktus nélkül létezhet ugyanilyen azonosító, a fordító képes megkülönböztetni őket. A névterületek típusait később részletezzük.

Kapcsolódás (linkage)

A kapcsolódást csatolásnak is nevezik. A végrehajtható program úgy jön létre, hogy

  • több, különálló fordítási egységet fordítunk,
  • aztán a kapcsoló-szerkesztővel (linker) összekapcsoltatjuk az eredmény .obj fájlokat, más meglévő tárgymodulokat és a könyvtárakból származó tárgykódokat.

Probléma akkor van, ha ugyanaz az azonosító különböző hatáskörökkel deklarált - például más-más forrásfájlban - vagy ugyanolyan hatáskörrel egynél többször is deklarált.

A kapcsoló-szerkesztés az a folyamat, mely az azonosító minden előfordulását korrekt módon egy bizonyos objektumhoz vagy függvényhez rendeli. E folyamat során minden azonosító kap egy kapcsolódási attribútumot a következő lehetségesek közül:

  • külső (external) kapcsolódás,
  • belső (internal) kapcsolódás vagy
  • nincs (no) kapcsolódás.

Ezt az attribútumot a deklarációk elhelyezésével és formájával, ill. a tárolási osztály (static vagy extern) explicit vagy implicit megadásával határozzuk meg.

Lássuk a különféle kapcsolódások részleteit!

A külső kapcsolódású azonosító minden példánya ugyanazt az objektumot vagy függvényt reprezentálja a programot alkotó minden forrásfájlban és könyvtárban. A belső kapcsolódású azonosító ugyanazt az objektumot vagy függvényt jelenti egy és csak egy fordítási egységben (forrásfájlban). A belső kapcsolódású azonosítók a fordítási egységre, a külső kapcsolódásúak viszont az egész programra egyediek. A külső és belső kapcsolódási szabályok a következők:

  • Bármely objektum vagy függvényazonosító fájl hatáskörrel belső kapcsolódású, ha deklarációjában expliciten előírták a static tárolási osztályt.
  • Az explicit módon extern tárolási osztályú objektum vagy függvényazonosítónak ugyanaz a kapcsolódása, mint bármely látható fájl hatáskörű deklarációjának. Ha nincs ilyen látható fájl hatáskörű deklaráció, akkor az azonosító külső kapcsolódású lesz.
  • Ha függvényt explicit tárolási osztály specifikátor nélkül deklarálnak, akkor kapcsolódása olyan lesz, mintha kiírták volna az extern kulcsszót.
  • Ha fájl hatáskörű objektumazonosítót deklarálnak tárolási osztály specifikátor nélkül, akkor az azonosító külső kapcsolódású lesz.

A fordítási egység belső kapcsolódásúnak deklarált azonosítójához egy és csak egy külső definíció adható meg. A külső definíció olyan külső deklaráció, mely az objektumhoz vagy függvényhez memóriát is rendel. Ha külső kapcsolódású azonosítót használunk kifejezésben (a sizeof operandusától eltekintve), akkor az azonosítónak csak egyetlen külső definíciója létezhet az egész programban.

A kapcsolódás nélküli azonosító egyedi entitás. Ha a blokkban az azonosító deklarációja nem vonja maga után az extern tárolási osztály specifikátort, akkor az azonosítónak nincs kapcsolódása, és egyedi a függvényre.

A következő azonosítóknak nincs kapcsolódása:

  • Bármely nem objektum vagy függvénynévvel deklarált azonosítónak. Ilyen például a típusdefiníciós (typedef) azonosító.
  • A függvényparamétereknek.
  • Explicit extern tárolási osztály specifikátor nélkül deklarált, blokk hatáskörű objektumazonosítóknak.
Függvények

A függvényekkel kapcsolatos alapfogalmakat tisztáztuk már a Bevezetés és Alapismeretek leckékben, de fussunk át rajtuk még egyszer!

A függvénynek kell legyen definíciója, és lehetnek deklarációi. A függvény definíciója deklarációnak is minősül, ha megelőzi a forrásszövegben a függvényhívást. A függvénydefinícióban van a függvény teste, azaz az a kód, amit a függvény meghívásakor végrehajt a processzor.

A függvénydefiníció rögzíti a függvény nevét, visszatérési értékének típusát, tárolási osztályát és más attribútumait. Ha a függvénydefinícióban a formális paraméterek típusát, sorrendjét és számát is előírják, függvény prototípusnak nevezzük. A függvény deklarációjának meg kell előznie a függvényhívást, melyben aktuális paraméterek vannak. Ez az oka annak, hogy a forrásfájlban a szabvány függvények hívása előtt behozzuk a prototípusaikat tartalmazó fejfájlokat (#include).

A függvényparamétereket argumentumoknak is szokták nevezni.

A függvényeket a forrásfájlokban szokás definiálni, vagy előrefordított könyvtárakból lehet bekapcsoltatni (linkage). Egy függvény a programban többször is deklarálható, feltéve, hogy a deklarációk kompatibilisek. A függvény prototípusok használata a C-ben ajánlatos, mert a fordítót így látjuk el elegendő információval ahhoz, hogy ellenőrizhesse

  • a függvény nevét (a függvények adott azonosító),
  • a paraméterek számát, típusát és sorrendjét (típuskonverzió lehetséges), valamint
  • a függvény által visszaadott érték típusát (típuskonverzió itt is lehet).

A függvényhívás átruházza a vezérlést a hívó függvényből a hívott függvénybe úgy, hogy az aktuális paramétereket is - ha vannak - átadja érték szerint. Ha a hívott függvényben return utasításra ér a végrehajtás, akkor visszakapja a vezérlést a hívó függvény egy visszaadott értékkel együtt (ha megadtak ilyet!).

Egy függvényre a programban csak egyetlen definíció lehetséges. A deklarációk (prototípusok) kötelesek egyezni a definícióval.

Függvénydefiníció

A függvénydefiníció specifikálja a függvény nevét, a formális paraméterek típusát, sorrendjét és számát, valamint a visszatérési érték típusát, a függvény tárolási osztályát és más attribútumait. A függvénydefinícióban van a függvény teste is, azaz a használatos lokális változók deklarációja, és a függvény tevékenységét megszabó utasítások. A szintaktika:

fordítási-egység:
  külső-deklaráció
  fordítási-egység külső-deklaráció

külső-deklaráció:
  függvénydefiníció
  deklaráció

függvénydefiníció:
  <deklaráció-specifikátorok> deklarátor <deklarációlista> összetett-utasítás

deklarátor:
  <mutató> direkt-deklarátor

direkt-deklarátor:
  direkt-deklarátor(paraméter-típus-lista)
  direkt-deklarátor(<azonosítólista>)

deklarációlista:
  deklaráció
  deklarációlista deklaráció

A külső-deklarációk hatásköre a fordítási egység végéig tart. A külső-deklaráció szintaktikája egyezik a többi deklarációéval, de függvényeket csak ezen a szinten szabad definiálni, azaz tilos függvényben másik függvényt definiálni!

A függvénydefinícióbeliösszetett-utasítás a függvény teste, mely tartalmazza a használatos lokális változók deklarációit, a külsőleg deklarált tételekre való hivatkozásokat, és a függvény tevékenységét megvalósító utasításokat.

Az opcionális deklaráció-specifikátorok és a kötelezően megadandó deklarátor együtt rögzítik a függvény visszatérési érték típusát és nevét. A deklarátor természetesen függvénydeklarátor, azaz a függvénynév és az őt követő zárójel pár. Az első direkt-deklarátor(paraméter-típus-lista) alak a függvény új (modern) stílusú definícióját teszi lehetővé. A deklarátor szintaktikában szereplő direkt-deklarátor a modern stílus szerint a definiálás alatt álló függvény nevét rögzíti, és a kerek zárójelben álló paraméter-típus-lista specifikálja az összes paraméter típusát. Ilyen deklarátor tulajdonképpen a függvény prototípus is. Például:

char fv(int i, double d){
  /* . . . */ }

A második direkt-deklarátor(<azonosítólista>) forma a régi stílusú definíció:

char fv(i, d)
int i;
double d; {
  /* . . . */ }

A továbbiakban csak az új stílusú függvénydefinícióval foglalkozunk, s nem emlegetjük tovább a régit! (A korszerű fordítóprogramok alapértelmezett beállítások mellett már nem is támogatják.)

deklaráció-specifikátorok:
  tárolási-osztály-specifikátor <deklaráció-specifikátorok>
  típusspecifikátor <deklaráció-specifikátorok>
  típusmódosító <deklaráció-specifikátorok>

típusmódosító: (a következők egyike!)
  const
  volatile

A tárolási-osztály-specifikátorok és a típuspecifikátorok definíciói a Típusok és konstansok lecke Deklaráció fejezetében megtekinthetők.

Tárolási osztály

Függvénydefinícióban két tárolási osztály kulcsszó használható: az extern vagy a static. A függvények alapértelmezés szerint extern tárolási osztályúak, azaz normálisan a program minden forrásfájljából elérhetők, de explicit módon is deklarálhatók extern-nek.

Ha a függvény deklarációja tartalmazza az extern tárolási osztály specifikátort, akkor az azonosítónak ugyanaz a kapcsolódása, mint bármely látható, fájl hatáskörű ugyanilyen külső deklarációnak, és ugyanazt a függvényt jelenti. Ha nincs ilyen fájl hatáskörű, látható deklaráció, akkor az azonosító külső kapcsolódású. A fájl hatáskörű, tárolási osztály specifikátor nélküli azonosító mindig külső kapcsolódású. A külső kapcsolódás azt jelenti, hogy az azonosító minden példánya ugyanarra a függvényre hivatkozik, azaz az explicit vagy implicit módon extern tárolási osztályú függvény a program minden forrásfájljában látható. Az extern-től különböző tárolási osztályú, blokk hatáskörű függvénydeklaráció hibát generál.

A függvény explicit módon deklarálható azonban static-nek is, amikor is a rá való hivatkozást az őt tartalmazó forrásfájlra korlátozzuk, azaz a függvény belső kapcsolódású, és csak a definícióját tartalmazó forrásmodulban látható. Az ilyen függvény legelső bekövetkező deklarációjában (ha van ilyen!) és definíciójában is ki kell írni a static kulcsszót.

Akármilyen esetről is van szó azonban, a függvény mindig a definíciós vagy deklarációs pontjától a forrásfájl végéig látható magától.

A visszatérési érték típusa

A visszatérési érték típusa meghatározza a függvény által szolgáltatott érték méretét és típusát. A függvénydefiníció metanyelvi meghatározása az elhagyható deklaráció-specifikátorokkal kezdődik. Ezek közül tulajdonképpen a típusspecifikátor felel meg a visszatérési érték típusának.

E meghatározásokat nézegetve látható, hogy a visszaadott érték típusa bármi lehet eltekintve a tömbtől és a függvénytől (az ezeket címző mutató persze megengedett). Lehet valamilyen aritmetikai típusú, lehet void (nincs visszaadott érték), de el is hagyható, amikor is alapértelmezés az int. Lehet struktúra, unió vagy mutató is, melyekről majd későbbi szakaszokban lesz szó.

A függvénydefinícióban előírt visszaadott érték típusának egyeznie kell a programban bárhol előforduló, e függvényre vonatkozó deklarációkban megadott visszatérési érték típussal. A meghívott függvény akkor ad vissza értéket a hívó függvénynek a hívás pontjára, ha a processzor kifejezéssel ellátott return utasítást hajt végre. A fordító természetesen előbb kiértékeli a kifejezést, és konvertálja - ha szükséges - az értéket a visszaadott érték típusára. A void visszatérésűnek deklarált függvénybeli kifejezéssel ellátott return figyelmeztető üzenetet eredményez, és a fordító nem értékeli ki a kifejezést.

Vigyázat! A függvény típusa nem azonos a visszatérési érték típusával. A függvény típusban ezen kívül benne van még a paraméterek

  • száma,
  • típusai és
  • sorrendje is!
Formális paraméterdeklarációk

A függvénydefiníció metanyelvi meghatározásából következően a modern stílusú direkt-deklarátor(paraméter-típus-lista) alakban, a zárójelben álló paraméter-típus-lista vesszővel elválasztott paraméterdeklarációk sorozata.

paraméter-típus-lista:
  paraméterlista
  paraméterlista, ...

paraméterlista:
  paraméterdeklaráció
  paraméterlista, paraméterdeklaráció

paraméterdeklaráció:
  deklaráció-specifikátor deklarátor
  deklaráció-specifikátor <absztrakt-deklarátor>

absztrakt-deklarátor:
  mutató
  <mutató><direkt-absztrakt-deklarátor>

direkt-absztrakt-deklarátor:
  (absztrakt-deklarátor)
  <direkt-absztrakt-deklarátor>[<konstans-kifejezés>]
  <direkt-absztrakt-deklarátor>(<paraméter-típus-lista>)

A paraméterdeklaráció nem tartalmazhat más tárolási-osztály-specifikátort, mint a register-t. A deklaráció-specifikátor szintaktikabeli típusspecifikátor elhagyható, ha a típus int, és egyébként megadják a register tárolási osztály specifikátort. Összesítve a formális paraméterlista egy elemének formája a következő:

<register> típusspecifikátor <deklarátor>

Az auto-nak deklarált függvényparaméter fordítási hiba!

A C szabályai szerint a paraméter lehet bármilyen aritmetikai típusú. Lehet akár tömb is (formálisan, mert a tömb elemei nem kerülnek átmásolásra), de függvény nem (az ezt címző mutató persze megengedett). A paraméter lehet természetesen struktúra, unió vagy mutató is, melyekről majd későbbi szakaszokban lesz szó. A paraméterlista lehet void is, ami nincs paraméter jelentésű.

A formális paraméterazonosítók nem definiálhatók át a függvény testének külső blokkjában, csak egy ebbe beágyazott belső blokkban, azaz a formális paraméterek hatásköre és élettartama a függvénytest teljes legkülső blokkja. Az egyetlen rájuk is legálisan alkalmazható tárolási osztály specifikátor a register. Például:

int f1(register int i){/* ... */}/* Igény regiszteres
                                    paraméter átadásra. */

A const és a volatile módosítók használhatók a formális paraméter deklarátorokkal. Például a

void f0(double p1, const char s[]){
  /* . . . */
  s[0]='A';
  /* Szintaktikai hiba. */}

const-nak deklarált formális paramétere nem lehet balérték a függvény testében, mert hibaüzenetet okoz.

Ha nincs átadandó paraméter, akkor a paraméterlista helyére a definícióban és a prototípusban a void kulcsszó írandó:

int f2(void){/* ... */} /* Nincs paraméter. */

Ha van legalább egy formális paraméter a listában, akkor az , ...-ra is végződhet:

int f3(char str[], ...){/* ... */}/* Változó számú vagy
                                    típusú paraméter. */

Az ilyen függvény hívásában legalább annyi aktuális paramétert meg kell adni, mint amennyi formális paraméter a ,... előtt van, de természetesen ezeken túl további aktuális paraméterek is előírhatók. A ,... előtti paraméterek típusának és sorrendjének ugyanannak kell lennie a függvény deklarációiban (ha egyáltalán vannak), mint a definíciójában.

A függvény aktuális paraméterei típusának az esetleges szokásos konverzió után hozzárendelés kompatibilisnek kell lennie a megfelelő formális paraméter típusokra. A ... helyén álló aktuális paramétereket nem ellenőrzi a fordító.

Az stdarg.h fejfájlban vannak olyan makrók, melyek segítik a felhasználói, változó számú paraméteres függvények megalkotását! A témára visszatérünk még a Mutatók kapcsán!

A függvény teste

A függvény teste elhagyható deklarációs és végrehajtható utasításokból álló összetett utasítás, azaz az a kód, amit a függvény meghívásakor végrehajt a processzor.

összetett-utasítás:
  {<deklarációlista> <utasításlista>}

A függvénytestben deklarált változók lokálisak, auto tárolási osztályúak, ha másként nem specifikálták őket. Ezek a lokális változók akkor jönnek létre, mikor a függvényt meghívják, és lokális inicializálást hajt rajtuk végre a fordító. A függvény meghívásakor a vezérlést a függvénytest első végrehajtható utasítása kapja meg. void-ot visszaadó függvény blokkjában aztán a végrehajtás addig folytatódik, míg return utasítás nem következik vagy a függvény blokkját záró }-re nem kerül a vezérlés. Ezután a hívási ponttól folytatódik a program végrehajtása.

A "valamit" szolgáltató függvényben viszont lennie kell legalább egy return kifejezés utasításnak, és visszatérés előtt rá is kell, hogy kerüljön a vezérlés. A visszaadott érték meghatározatlan, ha a processzor nem hajt végre return utasítást, vagy a return utasításhoz nem tartozott kifejezés. A kifejezés értékét szükséges esetben hozzárendelési konverziónak veti alá a fordító, ha a visszaadandó érték típusa eltér a kifejezésétől.

Függvény prototípusok

A függvénydeklaráció megelőzi a definíciót, és specifikálja a függvény nevét, a visszatérési érték típusát, tárolási osztályát és a függvény más attribútumait. A függvénydeklaráció akkor válik prototípussá, ha benne megadják az elvárt paraméterek típusát, sorrendjét és számát is. Összegezve: a függvény prototípus csak abban különbözik a definíciótól, hogy a függvény teste helyén egy ; van.

C-ben ugyan nem kötelező, de tegyük magunknak kötelezővé a függvény prototípus használatát, mert ez a következőket rögzíti:

  • A függvény int-től különböző visszatérési érték típusát.
  • Ezt az információt a fordító a függvényhívások paramétertípus és szám megfeleltetés ellenőrzésén túl konverziók elvégzésére is felhasználja.

A paraméterek konvertált típusa határozza meg azokat az aktuális paraméter értékeket, melyek másolatait a függvényhívás teszi ki a verembe. Gondoljuk csak meg, hogyha az int-ként kirakott aktuális paraméter értéket a függvény double-nek tekintené, akkor nem csak e paraméter félreértelmezéséről van szó, hanem az összes többi ezt követő is "elcsúszik"!

A prototípussal a fordító nem csak a visszatérési érték és a paraméterek típusegyezését tudja ellenőrizni, hanem az attribútumokat is. Például a static tárolási osztályú prototípus hatására a függvénydefiníciónak is ilyennek kell lennie. (A függvénydefiníció módosítóinak egyeznie kell a függvénydeklarációk módosítóival!)

A prototípusbeli azonosító hatásköre a prototípus. Prototípus adható változó számú paraméterre, ill. akkor is, ha paraméter egyáltalán nincs.

A komplett paraméterdeklarációk (int a) vegyíthetők az absztrakt-deklarátorokkal (int) ugyanabban a deklarációban. Például:

int add(int a, int);

A paraméter típusának deklarálásakor szükség lehet az adattípus nevének feltüntetésére, mely a típusnév segítségével érhető el. A típusnév az objektum olyan deklarációja, melyből hiányzik az azonosító. A metanyelvi leírás:

típusnév:
  típusspecifikátor-lista<absztrakt-deklarátor>

absztrakt-deklarátor:
  mutató
  <mutató><direkt-absztrakt-deklarátor>

direkt-absztrakt-deklarátor:
  (absztrakt-deklarátor)
  <direkt-absztrakt-deklarátor>[<konstans-kifejezés>]
  <direkt-absztrakt-deklarátor>(<paraméter-típus-lista>)

Az absztrakt-deklarátorban mindig megállapítható az a hely, ahol az azonosítónak lennie kellene, ha a konstrukció deklaráción belüli deklarátor lenne. A következő típusnevek jelentése: int, 10 elemű int tömb és nem meghatározott elemszámú int tömb:

int, int [10], int []

Lássuk be, hogy a függvény prototípus a kód dokumentálására is jó! Szinte rögtön tudunk mindent a függvényről:

void strcopy(char cel[], char forras[]);

A

<típus> fv(void);

olyan függvény prototípusa, melynek nincsenek paraméterei.

Normál esetben a függvény prototípus olyan függvényt deklarál, mely fix számú paramétert fogad. Lehetőség van azonban változó számú vagy típusú paraméter átadására is. Az ilyen függvény prototípus paraméterlistája ...-tal végződik:

<típus> fv(int, long, ...);

A fixen megadott paramétereket fordítási időben ellenőrzi a fordító, s a változó számú vagy típusú paramétert viszont a függvény hívásakor típusellenőrzés nélkül adja át a veremben.

Az stdarg.h fejfájlban vannak olyan makrók, melyek segítik a felhasználói, változó számú paraméteres függvények megalkotását! Nézzünk néhány példa prototípust!

int f(); /* int-et visszaadó függvény, melynek
            paramétereiről nincs információnk. */
int f1(void); /* Olyan int-et szolgáltató függvény,
            melynek nincsenek paraméterei. */
int f2(int, long); /* int-et visszaadó függvény, mely
            elsőnek egy int, s aztán egy long
            paramétert fogad. */
int pascal f3(void); /* Paraméter nélküli, int-et
            szolgáltató pascal függvény. */
int printf(const char [], ...); /* int-tel visszatérő
            függvény egy fix, és nem meghatározott
            számú vagy típusú paraméterrel. */

Készítsünk programot, mely megállapítja az ÉÉÉÉ.HH.NN alakú karakterláncról, hogy érvényes dátum-e!

/* PELDA21.C: Datumellenorzes. */
#include <stdio.h>
#include <stdlib.h> /* Az atoi miatt! */
#define INP 11      /* Az input puffer merete. */
#include <ctype.h>  /* Az isdigit miatt! */
int datume(const char []);
int getline(char [], int);
void main(void){
  char s[INP+1]; /* Az input puffer. */
  printf("Datumellenorzes.\nBefejezes ures sorral!\n");
  while(printf("\nDatum (EEEE.HH.NN)? "),
        getline(s,INP)>0)
    if(datume(s)) printf("Ervenyes!\n");
    else printf("Ervenytelen!\n"); }

Az ÉÉÉÉ.HH.NN alakú dátum érvényességét a kérdésre logikai értékű választ szolgáltató, intdatume(const char s[]) függvény segítségével érdemes eldöntetni.

Követelmények:

  • A karakterláncnak 10 hosszúságúnak kell lennie.
  • A hónapot az évtől elválasztó karakter nem lehet numerikus, és azonosnak kell lennie a hónapot a naptól elválasztóval.
  • A láncbeli összes többi karakter csak numerikus lehet.
  • Csak 0001 és 9999 közötti évet fogadunk el.
  • Az évszám alapján megállapítjuk a február hónap napszámát.
  • A hónapszám csak 01 és 12 közötti lehet.
  • A napszám megengedett értéke 01 és a hónapszám maximális napszáma között van.
int datume(const char s[]){
  static int honap[] =
    { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
  int i, ho;
  if(!s[10] && !isdigit(s[4]) && s[4]==s[7]){
    for(i=0; i<10; ++i){
      if(i==4||i==7) ++i;
      if(!isdigit(s[i])) return 0; }
    if((i=atoi(s))>=1){
      honap[2]=28+(!(i%4)&&i%100 || !(i%400));
      if((ho=10*(s[5]-'0')+s[6]-'0')>=1&&ho<=12&&
        (i=10*(s[8]-'0')+s[9]-'0')>=1&&
        i<=honap[ho]) return 1; } }
  return 0; }
Függvények hívása és paraméterkonverziók

A függvényt aktuális paraméterekkel hívjuk meg. Ezek sorrendjét és típusát a formális paraméterek határozzák meg. A függvényhívás operátor alakja

  utótag-kifejezés(<kifejezéslista>)

kifejezéslista:
  hozzárendelés-kifejezés
  kifejezéslista, hozzárendelés-kifejezés

ahol az utótag-kifejezés egy függvény neve, vagy függvénycímmé értékeli ki a fordító, s ezt hívja meg. A zárójelben álló, elhagyható kifejezéslista tagjait egymástól vessző választja el, és tudjuk, hogy ezek azok az aktuális paraméter kifejezések, melyek értékmásolatait a hívott függvény kapja meg.

Ha az utótag-kifejezés nem deklarált azonosító az aktuális hatáskörben, akkor a fordító implicit módon a függvényhívás blokkjában

extern int azonosító();

módon tekinti deklaráltnak.

A függvényhívás kifejezés értéke és típusa a függvény visszatérési értéke és típusa. Az értéket vissza nem adó függvényt void-nak kell deklarálni, ill. void írandó a kifejezéslista helyére, ha a függvénynek nincs paramétere.

Ha a prototípus paraméterlistája void, akkor a fordító zérus paramétert vár mind a függvényhívásban, mind a definícióban. E szabály megsértése hibaüzenethez vezet.

Az aktuális paraméter kifejezéslista kiértékelési sorrendje nem meghatározott, pontosabban a konkrét fordítótól függ. A más paraméter mellékhatásától függő paraméter értéke így ugyancsak definiálatlan. A függvényhívás operátor egyedül azt garantálja, hogy a fordító a paraméterlista minden mellékhatását realizálja, mielőtt a vezérlést a függvényre adná.

Függvénynek tömb és függvény nem adható át paraméterként, de ezeket címző mutató persze igen. (A korábban elkészített karakterlánc manipuláló függvényeknél, mint pl. a getline, formálisan ugyan tömböt adtunk át, de a fordító valójában olyan kódot generált, ami a karaktertömböt címző mutató értékét adta át, és nem a tömb elemeit. Ennek legfőbb motivációja, hogy a jellemzően nagy méretű adatterületek másolgatása nagyban lelassítaná a program futását. A pontos hívási mechanizmus tárgyalására később még visszatérünk.)

A paraméter lehet aritmetikai típusú. Lehet struktúra, unió vagy mutató is, de ezekkel későbbi szakaszokban foglalkozunk. A paraméter átadása érték szerinti, azaz a függvény az értékmásolatot kapja meg, melyet természetesen el is ronthat a hívás helyén levő eredeti értékre gyakorolt bármiféle hatás nélkül. Szóval a függvény módosíthatja a formális paraméterek értékét.

A fordító kiértékeli a függvényhívás kifejezéslistáját, és szokásos konverziót (egész-előléptetést) hajt végre minden aktuális paraméteren. Ez azt jelenti, hogy a float értékből double lesz, a char és a short értékből int, valamint az unsigned char és az unsigned short értékből unsigned int válik.

Ha van vonatkozó deklaráció a függvényhívás előtt, de nincs benne információ a paraméterekre, akkor a fordító kész az aktuális paraméterek értékével.

Ha deklaráltak előzetesen függvény prototípust, akkor az eredmény aktuális paraméter típusát hasonlítja a fordító a prototípusbeli megfelelő paraméter típusával. Ha nem egyeznek, akkor a deklarált formális paraméter típusára alakítja az aktuális paraméter értékét hozzárendelési konverzióval, és újra a szokásos konverzió következik. A nem egyezés másik lehetséges végkifejlete diagnosztikai üzenet.

A hívásnál a kifejezéslistabeli paraméterek számának egyeznie kell a függvény prototípus vagy definíció paramétereinek számával. Kivétel az, ha a prototípus ...-tal végződik, amikor is a fordító a fix paramétereket az előző pontban ismertetett módon kezeli, s a ... helyén levő aktuális paramétereket úgy manipulálja, mintha nem deklaráltak volna függvény prototípust.

Nem szabványos módosítók, hívási konvenció

A deklaráció deklarátorlistájában a megismert szabványos alaptípusokon, típusmódosítókon kívül minden fordítóprogram rendelkezik még speciális célokat szolgáló, a deklarált objektum tulajdonságait változtató, nem szabványos módosítókkal is.

Teljességre való törekvés nélkül felsorolunk itt néhány ilyen módosítót, melyek közül egyik-másik ki is zárja egymást!

módosító:
  cdecl
  pascal
  interrupt
  fastcall
  stdcall
  export
  near
  far
  huge

Az első néhány módosító a függvény hívási konvencióját határozza meg. Az alapértelmezett hívási konvenció C programokra cdecl.

Ha egy azonosító esetében biztosítani kívánjuk a kis-nagybetű érzékenységet, az aláhúzás karakter (_) név elé kerülését, ill. függvénynévnél a paraméterek jobbról balra való verembe rakását (vagyis mindazt, amit egy C programban a függvények hívásával kapcsolatban általában elvárunk), akkor az azonosító deklarációjában írjuk ki expliciten a cdecl módosítót! Ez a hívási konvenció biztosítja az igazi változó paraméteres függvények írását, hisz a vermet a hívó függvénynek kell rendbe tennie.

A többi hívási konvenció részletei meghaladják a tárgy kitűzött célját, ezért itt csak annyit jegyzünk meg, hogy a hívási konvenciók majd akkor kapnak különös jelentőséget, amikor különféle operációs rendszerekre, fordítóprogramokkal vagy programnyelveken készült tárgymodulokat szeretnénk egyetlen programba szerkeszteni. A különféle környezetekben gyakran eltérő módon valósítják meg a függvények hívását, és ha ezeket szeretnénk együttműködésre bírni, akkor biztosítani kell az azonos módon megvalósított függvényhívási mechanizmust.

Rekurzív függvényhívás

Bármely függvény meghívhatja önmagát közvetlenül vagy közvetve. A rekurzív függvényhívásoknak egyedül a verem mérete szab határt.

Valahányszor meghívják a függvényt, új tároló területet allokál a rendszer az aktuális paramétereknek, az auto és a nem regiszterben tárolt register változóknak. A paraméterek és a lokális változók tehát a veremben jönnek létre a függvénybe való belépéskor, és megszűnnek, mihelyt a vezérlés távozik a függvényből, azaz:

  • valahányszor meghívjuk a függvényt, saját lokális változó és aktuális paraméter másolatokkal rendelkezik, ami
  • biztosítja, hogy a függvény "baj nélkül" meghívhatja önmagát közvetlenül vagy közvetetten (más függvényeken át).

A rekurzívan hívott függvények tulajdonképpen dolgozhatnak dinamikusan kezelt, globális vagy static tárolási osztályú lokális változókkal is. Azt azonban ilyenkor ne felejtsük el, hogy a függvény összes híváspéldánya ugyanazt a változót éri el.

Ha az lenne a feladatunk, hogy írjunk egy olyan függvényt, mely meghatározza n faktoriálisát, akkor valószínűleg így járnánk el:

long double faktor(int n){
  long double N = n<1? 1.L : n;
  while(--n) N*=n;
  return N; }

Látható, hogy a long double ábrázolási formát választottuk, hogy a lehető legnagyobb szám faktoriálisát legyünk képesek meghatározni a C számábrázolási lehetőségeivel. Az algoritmus az egynél kisebb egészek faktoriálisát egynek tekinti, és az ismételt szorzást fordított sorrendben hajtja végre, vagyis: N=n*(n-1)*(n-2)*...*3*2*1.

Írjunk egy rövid keretprogramot, mely bekéri azt az 1 és MAX (fordítási időben változtatható) közti egész számot, melynek megállapíttatjuk a faktoriálisát!

#include <stdlib.h>
#include <limits.h>
#define MAX 40 /* Az input puffer merete. */
int getline(char s[],int n);
/* A decimalis szamjegyek szama maximalisan: */
#define HSZ sizeof(int)/sizeof(short)*5
int egesze(char s[]);
long double faktorialis(int n);
void main(void){
  int n=0; /* A szám. */
  char sor[MAX+1]; /* Input puffer. */
  printf("\t\tRekurziot szemlelteto peldaprogram.\n");
  while(n<1||n>MAX){
    printf("\nMelyik egesz szam faktorialisat szamitsuk?(1-%d)? ",MAX);
    getline(sor,MAX);
    if(egesze(sor)) n=atoi(sor);}
  printf("\nA rekurzioval eloallitott ertek:\n %6d! = %-10.0Lf \n",
        n, faktorialis(n)); }

Ismeretes, hogyha a formátumspecifikációban előírjuk a mezőszélességet, akkor a kijelzés alapértelmezés szerint jobbra igazított. A balra igazítás a szélesség elé írt - jellel érhető el.

A faktoriális-számítás rekurzív megoldása a következő lehetne:

long double faktor(int n){
  if(n <= 1) return 1.L;
  else return (n*faktor(n-1)); }

Látható, hogy a faktor egynél nem nagyobb n paraméter esetén azonnal long double 1-gyel tér vissza. Más esetben viszont meghívja önmagát n-nél eggyel kisebb paraméterrel. Az itt visszakapott értéket megszorozza n-nel, és ez lesz a majdani visszaadott értéke. Nézzük meg asztali teszttel, hogyan történnek a hívások, feltéve, hogy a main a 0-s hívási szint faktor(5)-tel indult!

n:54321
Hívási szint:1 2 3 4 5
Hívás:faktor(4)faktor(3)faktor(2)faktor(1)
Visszatérési szint:0 1 2 3 4
Visszatérési érték:12024621

Cseréljük ki a keretprogramban a faktor függvényt a rekurzív változatra, és próbáljuk ki!

Feladatok

1. Tovább szerettük volna fejleszteni az int datume(const char s[]) függvényt, hogy az évszám, hónapszám és napszám akár egy jegyű is lehessen. Sajnos nem jártunk sikerrel.

/*01*/int datume(const char s[]){
/*02*/  static int honap[] =
/*03*/    { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
/*04*/  int kezd, i, db, ev, ho, nap, elvjel;
/*05*/  kezd = i = 0;
/*06*/  while(isdigit(s[i])) i++;
/*07*/  if((db=i-kezd)<1 || db>4) return 0;
/*08*/  ev = atoi(s[kezd]);
/*09*/  elvjel = s[++i];
/*10*/  kezd = i;
/*11*/  while(isdigit(s[i])) i++;
/*12*/  if((db=i-kezd)<1 || db>2) return 0;
/*13*/  ho = s[kezd]-'0';
/*14*/  if(db==2) ho = ho*10 + s[kezd+1]-'0';
/*15*/  if(ho<1 || ho>12) return 0;
/*16*/  if(ev > 1) honap[2] = 28+(!(ev%4)&&ev%100 || !(ev%400));
/*17*/  if(elvjel != s[i]) return 0;
/*18*/  kezd = ++i;
/*19*/  while(isdigit(s[i])) i++;
/*20*/  if((db=i-kezd)<1 || db>2) return 0;
/*21*/  nap = s[kezd]-'0';
/*22*/  if(db==2) nap = nap*10 + s[kezd+1]-'0';
/*23*/  if(nap<1 || nap>honap[nap]) return 0;
/*24*/  if(elvjel != s[i]) return 0;
/*25*/  return 1; }
Jelölje meg azokat a sorokat, amelyekkel a függvény helyes működésre bírható!
/*02*/  static int honap[12] =
/*08*/  ev = atoi(s);
/*09*/  elvjel = s[i++];
/*17*/  if(s[elvjel] != s[i]) return 0;
/*18*/  kezd = i++;
/*23*/  if(nap<1 || nap>honap[ho]) return 0;
/*23*/  if(nap<1 || nap>honap[ev]) return 0;
/*24*/  if(s[i]) return 0;

2. Készítsen int indexe(char s[], char t[]) és int indexu(char s[], char t[]) függvényeket, melyek meghatározzák és visszaadják a t paraméter karakterlánc s karaktertömbbeli első, illetve utolsó előfordulásának indexét! Próbálja is ki egy rövid tesztprogrammal a függvényeket!

/* INDEXEU.C: Sorban egy adott szoveg elofordulasa
  indexenek megadasa elorol és hatulrol. */
#include <stdio.h>
#include <string.h>
#define MAX 128 /* Az input puffer merete. */
#define AZ "az" /* A keresett szoveg. */
int getline(char s[],int lim) {
  int c,i=0;
  while(--lim>0&&(c=getchar())!=EOF&&c!='\n')s[i++]=c;
  s[i]='\0';
  while(c!=EOF && c!='\n')c=getchar();
  return(i); }
int indexe(char s[],char t[]) {
  /* Visszaadja t s-beli elso
    elofordulásanak indexet, ill. -1-et, ha t nincs s-ben. */
  int i, /* Index az s karakterlancon. */
      j=0, /* Index t-n. Elorol indul. */
      mx=strlen(s); /* Utso index s-ben, ameddig a t-vel
                      valo hasonlitassal el kell menni. */
  for(i=0; i<=mx; ++i) { /* s karakterlancon lepteti i-t. */
    /* Ha a ket karakter egyezik, j-t is leptetjuk t-n. */
    if(s[i]==t[j]) ++j;
    /* Ha a ket karakter elter, visszalepunk t elejere
      (j=0), es i indexet is visszaleptetjuk s-n oda, ahol
      az elobb a ket lanc hasonlitasat kezdtuk, hisz i-t
      ugyis megnoveli a ciklus. */
    else {
      i=i-j;
      j=0;
    }
    /* Ha a ket karakter egyezett, es t karakterlanc veget
      ert, akkor visszaadhato a megtalalasi index. */
    if(!t[j]) return i-j+1;
  }
  return(-1); }
int indexu(char s[],char t[]) {
  /* Visszaadja t s-beli utso
    elofordulasanak indexet, ill. -1-et, ha t nincs s-ben. */
  int i,j,k;
  /* Hatrafele haladunk s karakterlancon, de a
    lanchasonlitasok elore mennek. */
  for(i=strlen(s)-strlen(t); i>=0; --i) {
    for(j=i,k=0; t[k]&&s[j]==t[k]; j++,k++);
    /* Ha a lanchasonlitas t karakterlanc vegen ert veget,
      akkor visszaadhato a megtalalasi index. */
    if(!t[k]) return(i);
  }
  return(-1); }
void main(void) {
  int n; /* Az index. */
  char sor[MAX+1]; /* A beolvasott sor. */
  printf("Gepeljen be egy sort, melyben a(z) \"%s\" szoveg "
        "elso es utolso\nelofordulasat keressuk! Vege: "
        "egy ures sor.\n", AZ);
  while(printf("\nJohet a sor! "), getline(sor,MAX)>0)
    if((n=indexe(sor, AZ))>=0) {
      printf("Az elso elofordulas indexe:%d.\n",n);
      printf("Az utso elofordulas indexe:%d.\n",
            indexu(sor, AZ));
    } else printf("Nincs benn!\n"); }

3. Írja meg a void strrv(char s[]) függvény rekurzív változatát, mely megfordítja a saját helyén a paraméter karakterláncot!

/* STRRV.C: Szovegsor megforditasa a sajat helyen. */
#include <stdio.h>
#include <string.h>
#define MAX 128 /* Az input puffer merete. */
int getline(char s[],int n) {
  int c,i;
  for(i=0; i<n&&(c=getchar())!=EOF&&c!='\n'; ++i) s[i]=c;
  s[i]='\0';
  while(c!=EOF&&c!='\n') c=getchar();
  return(i); }
void csere(int i, int j, char s[]) { /* Rekurziv csere fv.*/
  char k;
  /* Mig az also index kisebb a felsonel: */
  if(i<j) {
    /* Az s karakterlanc ket elemenek csereje: */
    k=s[i];
    s[i]=s[j];
    s[j]=k;
    /* Az also index novelendo, s a felso meg
      csokkentendo: */
    ++i;
    --j;
    /* A ket uj indexszel ujrahivjuk a csere fuggvenyt. */
    csere(i, j, s); } }
void strrv(char s[]) {
  /* s karakterlanc megforditasa a sajat
    helyen. Rekurzivitas konnyen a cserenel biztosithato: */
  if(strlen(s)>1) csere(0, strlen(s)-1, s); }
void main(void) {
  char sor[MAX+1]; /* Az aktualis sor. */
  printf("\n\n\nSzovegsor megforditasa a sajat helyen:\n");
  printf("Utoljara adjon meg egy ures sort is!\n\n");
  /* Sor olvasasa, megforditasa es kijelzese ures sorig: */
  while(getline(sor, MAX)>0){
    strrv(sor);
    printf("%s\n\n", sor); } }

4. Szerettünk volna készíteni egy rekurzív hatványozó függvényt, amely azt használja ki, hogy egy k kitevős hatvány két azonos alapú, de k/2 kitevős hatvány szorzatával is felírható. Feltesszük, hogy a kitevő természetes szám. Ha k páratlan, akkor még egy további szorzásra is szükség van. Pl. 25 = 22 * 22 * 2. A függvényünk sajnos nem lett tökéletes.

/*1*/double hatvany(double a, int k){
/*2*/  cdecl double seged;
/*3*/  if(!k) return 1.;
/*4*/  else if(k==1) return a;
/*5*/  seged = hatvany(a, k%2);
/*6*/  if(k&1) return a*seged*seged;
/*7*/  else return seged*seged; }
Jelölje meg azokat a sorokat, amelyekkel helyes működésre bírható!
/*2*/  auto double seged;
/*5*/  seged = hatvany(a, k/2);
/*6*/  if(k/2) return a*seged*seged;
/*6*/  if(k&&1) return a*seged*seged;