1. Homepage of Dr. Zoltán Porkoláb
    1. Home
    2. Archive
  2. Teaching
    1. Timetable
    2. Imperative programming (BSc)
    3. Multiparadigm programming (MSc)
    4. C programming (BSc for physicists)
    5. Project tools (BSc)
    6. Bolyai College
    7. C++ (for foreign studenst)
    8. Software technology lab
    9. BSc and MSc thesis
  3. Research
    1. Templight
    2. CodeChecker
    3. CodeCompass
    4. Projects
    5. Publications (up to 2011)
    6. PhD students
  4. Affiliations
    1. Dept. of Programming Languages and Compilers
    2. Ericsson Hungary Ltd

Imperatív programozás 8.

Függvények, paraméterátadás

A függvények és eljárások, gyakori összefoglaló nevükön alprogramok az imperatív nyelvek alapvető építőkövei. Levetővé teszik, hogy a nagyobb, komplex programokat kisebb, könnyebben karbantartható, egyszerűbb részekre bontsuk fel, eltakarva az implementációs részleteket a külvilág elől. Segítségükkel újra fel tudjuk használni a már megírt algoritmusainkat, az esetleges különbségeket a paraméterekkel kifejezve.

Függvények egy csoportját külön fordítási egységekbe szervezhetjük és könyvtárakat is építhetünk belőlük.

A C nyelvet úgy tervezték, hogy könnyen írhassunk függvényeket és azok futási időben kis költséggel végrehajthatók legyenek.

A függvények deklarációjában használt paraméter neveket szokás formális paraméternek nevezni, míg a függvény meghívásakor ténylegesen átadott értékek az aktuális paraméter. A függvény nevét a teljes paraméterlistával a függvény prototípusának (signature) nevezzük.

C-ben az összes alprogramot egységesen függvénynek nevezzük. Amennyiben nem adnak vissza értéket (eljárás), akkor azt a void visszatérő típussal jelöljük.

Függvénydeklaráció C-ben

A visszatérő érték és a paraméterlista típusa része a függvény típusának. Ahol a függvénynek nincsen visszatérő értéke a void típust használjuk. Tömbök nem használhatóak visszatérő típusként.

int f(void);    /* függvény paraméter nélkül, int visszatérő típussal  */
int *fp(void);  /* függvény paraméter nélkül, int* visszatérő típussal */

A deklarációkban használt formális paraméternevek csak leíró szerepűek, ténylegesen nem használja fel a fordító, és el is hagyhatjuk őket.

1
2
int f(int *x, int *y); /* x és y neveknek nincsen szerepe, el is hagyható */
int f(int *, int *);   /* ekvivalens a fentivel */

A C nagyon régi, ANSI szabvány előtti verziójában nem használtunk prototípusos deklarációkat. Az evvel való kompatibilitás miatt az ANSI C-ben is elhagyhatjuk a paraméterek specifikációját:

1
2
int f(void);      /* függvény pontosan nulla paraméterrel */ 
int g();          /* függvény paraméter-specifikáció nélkül */

Itt f() és g() különböznek. Tudjuk, hogy f() pontosan nulla paraméterrel rendelkezik, de semmit sem tudunk g() paramétereinek számáról vagy típusáról. A g() deklarációja reverz kompatibilis az ANSI előtti C-vel, de ilyet új kód írásakor ne használjunk.

Megjegyzés

C++-ban mindig alkalmaznunk kell a prototípusos deklarációt. Ott a g() jelölés ekvivalens a g(void) jelöléssel és a pontosan nulla paramétert jelenti. Abban az esetben, ha nem tudjuk a paraméterek számát, vagy típusát, mint pl a printf esetében, használjuk az ellipsis jelölést:

1
2
int printf(const char *format, ...); 
int fprintf(FILE *stream, const char *format, ...); 

Ennek a jelölésnek az a jelentése, hogy nulla vagy több további paraméter ismeretlen típussal. Ilyen függvényeket nem egyszerű implementálni, az ilyen változó paraméterlistájú (variadic parameter) függvényeket a <stdarg.h> headerfájl va_ makróival írhatunk.

Függvényhívás C-ben

Amikor egy f függvényt meghívunk, a hívásnak meg kell felelnie a deklaráció prototípusával:

  • A paraméterek száma ugyanannyi.
  • A hívás minden aktuális paramétere olyan típusú, hogy értékül adható legyen a deklaráció formális paraméter típusának.

Ha a paraméterek száma nem felel meg a deklarációnak, akkor a viseledés nemdefiniált. Ha a deklaráció az ellipsis jelölést használja, vagy a típusok nem kompatiilisak, a viselkedés szintén nemdefiniált.

Ha a deklaráció nem prototípusos (azaz üres nyitó-csukó zárójeles, pl. void f() ), akkor a függvényhíváskor az ún. default promóciók történnek meg, mint az egész típusok long vagy int-re és a lebegőpontos típusok long double vagy double konverziója.

Ha a deklaráció prototípusos, azaz felsoroltuk az egyes paramétereket és típusait, akkor az aktuális paraméter értékek pontosan úgy konvertálódnak a formális paraméterekre, mint ha értékadás történne. Az ellipsis-től kezdve ez a konverzió megáll, és onnan csak a default promóciók történnek meg.

1
2
3
4
5
6
7
double fahr2cels(double);  /* prototípusos deklaráció */
double cels2fahr();        /* nem prototípusos deklaráció */
/* ... */
float f = 3.14;
printf("%f\n", fahr2cels(36));  /* ok, 36 double-ra konvertálódik */
printf("%f\n", cels2fahr(f));   /* ok, float -> double promóció   */
printf("%f\n", fahr2cels(36));  /* hiba, int paraméter adódik át  */

Két további konverzió történhet még meg:

  • signedunsigned konverzió, ha az érték reprezentálható mindkét típusban

  • void *char * konverzió.

Egy függvényhívás szekvecia-pont, azaz először azaktuális paraméterek értékelődnek ki nemdefiniált sorrendben, és a függvény törzsének végrehajtása csak azután kezdőthet. A paraméterek kiértékelésének egymás közötti sorrendje viszont definiálatlan. (A paramétereket elválasztó vessző nem a vessző operátor.)

( *t[f1()] ) ( f2(), f3(), f4() );

Az f1, f2, f3, f4 függvények akármilyen sorrendben meghívódhatnak.

A rekurzív függvényhívások akár direkt akár indirek módon megtörténhetnek:

1
2
3
4
5
6
7
int factorial(int n)
{
  if ( 1 == n ) 
    return 1;
  else 
    return n * factorial(n-1);
}

Ha ezt a függvényt egynél kisebb paraméterrel hívjuk meg, akkor végtelen rekurzió következik be, ami futási idejű hiba.

Paraméterátadás

A programozási nyelvek az idők során számos módszert dolgoztak ki a paraméterek átadására.

Cím szerinti

A cím szerinti (call by address, call by reference) paraméterátadás esetében az aktuális és formális paraméterek memória-lokációja megegyezik, az alprogram paramétere lényegében a híváskor átadott tárterület (változó) szinonímája (álneve). Minden módosítás, amit az alprogramban a paraméteren végzünk valójában az átadott aktuális paraméteren történik meg és azonnal látszik. Ez a legegyszerűbben implementálható és az egyik legrégibb paraméterátadási módszer.

Előnye, hogy az alprogram a paramétereken keresztük kétirányú kommunikációt folytathat a hívóval: onnan információkat kaphat és visszafelé is adhat. A paraméterek módosítása valójában a hívó változóinak módosítása. Ez néha zavaró is lehet, különösen ha mind az aktuális és a formális paraméter is látható/haszálható az alprogramban. Bár a cím szerinti paraméterátadás olcsón implementálható, hiszen nincsenek másolások, mint az érték szerintiben, az aliasing lehetősége miatt a fordítóprogram csak óvatosabban optimalizálhat.

Ugyancsak problémát jelenthet, ha kifejezéseket vagy konstansokat (pl. 42) adunk át paraméterként, hiszen ezeknek eredetileg nincsen memóriaterületük.

Érték szerint

Az érték szerinti (call by value) paraméterátadás során a függvény formális paramétereit úgy tekintjük, mintha azok a függvény lokális változói lennének. A függvényhívás során ezeket a “lokális változókat” az aktuális paraméter értékekkel inicializáljuk: lényegében azok értékeit másoljuk a formális paraméterekbe. Mindez azt jelenti, hogy az alprogram végrehajtása során az aktuális és formális paraméter jól elkülönül. Ennek nemcsak az az előnye, hogy könyebben érvelhetünk a program működéséről, de gyakran a fordítóprogram is hatékonyabb kódot fordíthat, ha nem lehetséges alisaing.

A módszer előnye, hogy a paramétereket úgy kezelhetjük, mint a lokális változókat. Hátránya, hogy az alprogramban a paraméterek módosítását nem tudják továbbítani a hívó eljárás felé. Ezt gyakran úgy kerüljük meg, hogy paraméterként eleve a módosítandó területre hivatkozó mutatót adjuk át. Pont így működik pl. a C scanf függvénye.

Eredmény szerinti

Az eredmény szerinti (call by result) paraméterátadás nagyon hasonló az érték szerintihez, de az alprogramból történő visszatérés pillanatában az alprogramban létező másolat (a formális paraméter) értéke visszamásolódik az aktuális paraméterbe. Így a függvény végrehajtása során külön-külön tárterületet használ a formális és az aktuális paraméter, de az alprogram általi módosítások a visszatérés pillanatában láthatóvá válnak a hívó számára. Más mellett ilyen paraméterátadási módot is használ az ADA output és inout paraméterek esetében.

Név szerinti

A név szerinti (call by name) paraméterátadást elsősorban az Algol 60 és a Simula 67 alkalmazta. Ebben az esetben nem az aktuális paraméter memória területét vagy pillanatnyi értékét használjuk fel a paraméterátadáskor, hanem magát a kifejezést, amit a programozó beírt a függvényhíváskor. Amikor az alprogram végrehajtása során hivatkozunk a formális paraméterre, akkor újra kiértékeljük az átadott kifejezés pillanatnyi értékét, és azt használjuk.

Implemetációja gyakran úgy történt, hogy a kifejezést kiszámoló kis eljárást, ún. closure-t adtunk át, és ezt hajtódott végre a paraméter minden meghivatkozásakor. A mai nyelvekben ritkán alkalmazzuk, ha mégis valami hasonlóra van szükségünk, akkor pl. C++-ban lamdba kifejezést adunk át paraméterként.

Az egyes programozási nyelvek stratégiái

A FORTRAN és számos azt követő nyelv cím szerinti paraméterátadást alkalmazott. Amikor nem változót, hanem egy kifejezést adtunk át, akkor azt egy temporális tárterületen számoltuk ki és ennek a területnek címét adtuk át. A C programozási nyelv érték szerinti paraméretátadást használ. A C++ alapértelmezésben ugyancsak érték szerinti, de a referencia típusú paraméterek esetében lényegében a cím szerinti paraméterátadással dolgozik. Ugyanígy a Pascal is használja mindkét módszert: az alapértelmezés az érték szerinti-nek felel meg, viszont a var kulcsszó alkalmazásával lényegében cím szerint adhatunk át paramétereket. Az Algol 60 és Simula 67 érték és név szerinti paraméterátadást használt. Javaban a beépített típusok érték szerint adódnak át, de a class típusok referencia szerint, ilyenkor lényegében olyan pointerek adódnak át, amik a tényleges objektumra mutatnak - hatásában ez leginkább a cím szerinti paraméterátadásnak felel meg. Az ADA nyelv érték és eredmény szerinti paraméterátadást használ.

Paraméterátadás C-ben

A C programozási nyelvben az aktuális paraméterek érték szerint adódnak át, azaz a kifejezés értéke bemásolódik a formális paraméter területére, pont úgy, mintha a függvényben definiált lokális változó lenne, amit az aktuális paraméterből inicializálnánk.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void increment(int i)
{
  ++i;
  printf("i in increment() = %d\n",i);
} 
int main()
{
  int i = 0;
  increment(i);
  increment(i);
  printf("i in main() = %d\n",i);
  return 0;    
}
$ gcc -ansi -pedantic -Wall -W f.c 
$ ./a.out 
i in increment() = 1
i in increment() = 1
i in main() = 0

A cím szerinti paraméterátadást szimulálhatjuk avval, ha a változó helyett annak címét adjuk áát paraméternek:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
void increment(int *ip)
{
  ++*ip;
  printf("i in increment() = %d\n",*ip);
} 
int main()
{
  int i = 0;
  increment(&i);
  increment(&i);
  printf("i in main() = %d\n",i);
  return 0;    
}
$ gcc -ansi -pedantic -Wall -W f.c 
$ ./a.out 
i in increment() = 1
i in increment() = 2
i in main() = 2

A tömb paramétereket az első elemre mutató pointer értékként adjuk át:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int f( int *t, int i)
{
  return t[i];
}
int main()
{
  int arr[] = {1, 2, 3, 4};
  printf("%d\n", f(arr,1));
  printf("%d\n", f(&arr[0],1));
  return 0;
}
$ gcc -ansi -std=c99 -Wall -W a.c
$ ./a.out 
2
2

Ezért ezek a deklarációk ekvivalensek:

1
2
3
4
5
6
int f( int *t, int i)   { return t[i]; }
int f( int t[], int i)  { return t[i]; }
int f( int t[4], int i) { return t[i]; }

/* a tömbhatárokat nem ellenőrzi a fordító, ezért ez is lefordul: */
int f( int t[4], int i)  { return t[6]; }  /* de lehet, hogy hibásan működik */

A visszatérő típus

A visszatérő értékek a függvény visszatérő típusára knvertálódik:

1
2
3
4
5
6
double f(void)
{
  int i;
  // ...
  return i;   // converted to double
}

A main függvény paraméterei

A main() függvényt az alábbi módokon lehet definiálni:

1
2
3
4
5
int main(void) { /* ... */ }
int main( int argc, char *argv[]) { /* ... */ }

/* ha az operációs rendszer támogatja (pl. UNIX) */
int main( int argc, char *argv[], char *envp[]) { /* ... */ }

Ha argc definiált, akkor argv[argc] nullpointer. Ha argc
nagyobb nullánál, akkor argv[0] a program neve, ahogy azt meghívták, és argv[1] … argv[argc-1] a program operációs rendszertől kapott paraméterei. Az argv[i] paraméterek NUL azaz ‘\0’ karakterrel terminált karaktertömbök.

Normális esetben az argc mindig nagyobb, mint 0.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main(int argc, char *argv[])
{
  printf("name of the program = %s\n", argv[0]);
  for (int i = 1; i < argc; ++i)
    printf("argv[%d] = %s\n", i, argv[i]);
  return 0;    
}
$ gcc -ansi -std=c99 -Wall -W -o mainpars mainpars.c 
$ ./mainpars
name of the program = ./mainpars
$ ./mainpars first second third
name of the program = ./mainpars
argv[1] = first
argv[2] = second
argv[3] = third

A hívási verem szerkezete

alt text

Függvénymutatók

Függvényere is állíthatunk mutatókat, az ilyen pointerek típusához hozzátartozik a teljes prototípus és a vissztérő érték típusa is.

A függvénymutató értékeket a (deklarált) függvénynevekből képezzük:

double sin(double); /* vagy #include <math.h> */
...
double f(double (*par)(double))
{
  double (*fp)(double);   /* fp egy mutató double(double) függvényre */
  fp = par;
  
  if ( NULL != fp )
  {
    (*fp)(.5)       /* sin(.5) meghívása */
      fp (.5)       /* ekvivalens a fentivel */
  }
}

Az előző increment-es példa pointerrel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
void increment(int *ip)
{
  ++*ip;
  printf("i in increment() = %d\n",*ip);
} 
int main()
{
  void (*fp)(int *); /* mutató void (int*) függvényre */
  void (*gp)();      /* mutató ismeretlen paraméterlistájú függvényre */
  int i = 0;
  fp = increment;
  gp = fp;
  (*fp)(&i);    /* increment() meghívása */
    fp (&i);    /* ugyanaz egyszerűbben  */
    gp (&i);    /* increment() mghívása, de nem ellenőrzi a paramétereket */
  printf("i in main() = %d\n",i);
  return 0;    
}
$ gcc -ansi -pedantic -Wall -W f.c 
$ ./a.out 
i in increment() = 1
i in increment() = 2
i in increment() = 3
i in main() = 3

Egy függvénymutató:

  • mutathat a kompatibilis típusú függvényre
  • értékül adható egy kompatibilis függvénymutatónak
  • összehasonlítható a nullpointerrel
  • meghívható a mutatott függvény, ha nem nullpointer

Tömbök, pointerek, pointer aritmetika

Tömbök

A tömbök a memóriában folytonosan (azaz egymást követő bájtokon) elhelyezkedő azonos típusú elemek véges sorozata. A tömb egyes elemeit a tömb elejétől számított pozíciójuk, az ún. tömbindex alapján érhetjük el. A legtöbb programozási nyelvben, így a C, C++, Java, C# esetében is az indexeket nullától kezdjük, azaz a tömb legelső eleme a 0 indexű, de egyes nyelveknél, pl. FORTRAN-ban és Pascal-ban a programozó adhatja meg az alsó és felső indexeket is. Alapértelmezésként FORTRAN-ban 1-től kezdődnek az indexek.

/* C nyelv, 10 darab int, elemek: t[0] .. t[9] */
int  daysInMonth[12];
daysInMonth[2] = 31;  /* marcius */      
c FORTRAN nyelv, MONTH(1) .. MONTH(12) 
      INTEGER DAYSINMONTH(12)  
      DAYSINMONTH(3) = 31  ! MARCIUS
(* pascal nyelv, elemek: month[-2] .. month[9] *)
type
 monthtype = array [-2..9] of integer; 
var
 month : monthtype;
begin
 month[0] := 31  (* marcius *)
end;
# Python

A tömbök egyes nyelvekben “tudják” a méretüket, ilyen pl. a Java és a C#. Ennek nyilván többletköltsége van, ezért a C és C++ futási időben nem tartja számon ezt az információt. Fordítási időben azonban ezekben a nyelvekben megtudhatjuk a tömbök méretét, a sizeof operátor segítségével. A t tömb elemeinek számát pl. lekérdezhetjük a sizeof(t)/sizeof(t[0]) kifejezéssel. Vigyázzunk azonban, hogy ez nem fog működni, ha t függvényparaméter (lásd ott).

A tömbök Javaban, C#-ban, Pascalban, és sok más nyelven lehetnek többdimenziósak is. A C (és C++) nyelv ezt úgy oldja meg, hogy az egydimenziós tömbök elemei maguk is lehetnek (fixméretű) tömbök:

/* C nyelv, 4x2 darab short, elemek: s[0][0] .. s[0][1] .. s[3][1] */
short  s[4][2];
s[1][0] = 1212;  /* a tömb elejétől (1x2+0) * sizeof(short) távolságra */ 

Láthatjuk, hogy a C ún. sorfolytonos ábrázolást alkalmaz, azaz a tömb sorai egymás után tárolódnak. Ilyenkor egy t[I][J][K] többdimenziós tömb (i, j, … ,k)-ik elemének eléréséhez elég a J…K dimenziókat tudni, a legkülsőbb (azaz a “legbaloldalabbi”) I dimenzió méretére nincsen szükségünk.

alt text

A tömbökkel C-ben nem lehet műveleteket végezni, kivéve az egyes tömbelemek elérését az index operátor segítségével. Ugyanakkor a kifejezésekben a tömb neve automatikusan az első elemre mutató pointer értékre konvertálódik.

Az index operátor

A C nyelvben egy t tömb i-edik elemének elérésére a t[i] index operátor használható. A C nyelv sem fordítási, sem futási időben nem ellenőrzi, hogy az index érvényes-e, azaz a tömb egy valós elemét címzi-e meg. Ha hibát vétünk, a program viselkedése nemdefiniált, “szerencsés” esetben elszáll a programunk, de az is lehet, hogy hibásan továbbműködik.

C++-ban különösen a többdimenziós tömbök helyett érdemesebb az std::vector vagy (C++11-től kezdve) az std::array osztályokat használni.

Mutatók

A mutató (pointer) egy olyan programkontrukció, ami egy memória-területet azonosít (pl. egy változó címét). A mutatót képzelhetjük úgy, mintha egy memória-címet tartalmazna (valójában ennél lehetnek bonyolultabb, platformfüggő megvalósítások).

Vannak programozási nyelvek (pl. Pascal, Java), amelyek csak a dimnamikus memóriaterületre (lásd 5-ik előadás) engednek mutatót állítani, a C (és a C++, ADA) viszont engedi, hogy tetszőleges a program címterében levő memóriaterületre (így statikus élettartamú globális, vagy a veremben létrejött automatikus változókra) pointert állítsunk. Pointerekre is állíthatunk pointereket.

alt text

A mutató típusa viszont fontos: hatással van arra, hogyan működnek a pointerek. Az általános, bármire mutató pointereket void * típussal hozzuk létre.

int  *pi;   /* mutató int típusra                  */
int **ppi;  /* mutató int tpusra mutató pointerre  */
void *pv;   /* általános mutató: "pointer to void" */

Műveletek mutatókkal

Mutató értékeket a & cím (address) operátorral hozhatunk létre. Van egy speciális érték, a null pointer, ami azt jelzi, hogy a pointerünk éppen nem mutat semmire. A null pointert a NULL makrónévvel jelöljük, a null pointer elágazásokban, ciklusokban, stb. hamis értékre értékelődik ki, minden más pointer értéket igaznak tekintünk. Egyes függvények, mint pl. a malloc, null pointer visszaadásával jelzik, ha hibaesemény történt.

A mutatóra a * indirekció (dereference) operátort alkalmazva azt a memóriaterületet kapjuk, amire a pointer mutat. Ezzel a memóriaterülettel utána a típusának megfelelő műveleteket végezhetjük el. A null pointer indirekciója nemdefiniált viselkedés - súlyos futási hibát eredményez.

Egy adott típusra mutató pointert össze lehet hasonlítani egy ugyanolyan típusú mutatóval vagy a null pointerrel az == egyenlőség és a != nemegyenlő operátorokkal. Két null pointer érték mindig megegyezik és sohasem egyenlő nem null pointer értékekkel.

A tömbökre mutató pointerek speciálisan viselkednek. Ha két, ugyanolyan típusú mutató ugyanazon tömb elemeire mutat, akkor használhatjuk a <, <=, >, >= operátorokat is. Azt a pointert tekintjük kisebbnek, amelyik a kisebb indexű tömbelemre mutat.

A tömbökre mutató pointerekhez hozzáadhatunk egész számokat is, ezt hívjuk pointer aritmikának. Az összeadás (és kivonás) hatása függ a pointer típusától: ha ptr egy T típusú tömbre mutat, akkor ptr + i az i tömbelemmel, azaz i*sizeof(T) bájttal odébb mutat.

double   points[10];   /* 10 elemű double tömb */
double  *curPoint = &points[4];  /* mutató az 5-ik elemre */
void *pv;   /* általános mutató: "pointer to void" */

alt text

Ha két mutató ugyanolyan típusú és ugyanarra a tömbre mutatnak, akkor kivonhatjuk őket egymásból, és az eredmény egy (előjeles) egész, amelyik a mutatott elemek indexei közötti (előjeles) különbséget adja meg. Fontos tehát, hogy nem a köztes bájtok, hanem a köztes tömbelemek számát kapjuk meg. Ezért is fontos, hogy a pointereinket helyes típusra deklaráljuk. Mivel egy void * pointer nem mutathat tömbre, ezért a pointer aritmetika sem alkalmazható void * típusú pointerekre.

Mutatók és tömbök kapcsolata

Minden tömb neve az első (azaz nullás indexű) elemre mutató pointer értékre konvertálódik. Ezért a tömbök neveit is lehet pointerként használni. Ennek egy speciális esete, amikor egy tömböt függvényparaméterként adunk át. Ilyenkor valójában a függvény első elemére mutató pointer értékét adjuk át a hívott függvénynek. A függvény paraméter-deklarációjában egyaránt használhatjuk a pointer vagy tömb deklarációs formát - a fordítóprogram minden tömb formájú paramétert pointernek fog tekinteni.

void f( int t[10]);   /* ezek a deklarációk mind ugyanazt jelentik */
void f( int t[]);     /*                                           */
void f( int *t);      /* és ennek a pointernek tekinti a fordító   */

Index operátor alkalmazása pointerekre

Kényelmi okokból a nem void* mutatókra is alkalmazhatjuk az index operátort. Ebben az esetben a ptr[i] kifejezés értéke megegyezik a *(ptr+i) kifejezéssel. Az i értéke lehet nulla vagy negatív is. A fordítóprogram semmilyen ellenőrzést sem végez arra nézve, hogy az így kapott memória pozíció érvényes ill. helyes-e. A legtöbb C fordító elfogadja a 2[“Hello”] kifejezést, amit ekvivalensnek tekint *(2+”Hello”) azaz *(“Hello”+2) azaz “Hello”[2]-vel, és az első ‘l’ karaktert fogja jelenteni.

1
2
3
4
5
6
7
8
9
10
11
12
double  tomb[10];        /* 10 elemű double tömb */
double *ptr = &tomb[0];  /* első elemre mutató pointer */
double *qtr = &tomb[5];  /* a hatodik elemre mutató pointer */
/* ez itt mind igaz allitas */
assert(  tomb    == &tomb[0] );
assert(  tomb+5  == &tomb[5] );
assert(  *ptr    ==  ptr[0]  );
assert( *(ptr+5) ==  ptr[5]  );
assert(  tomb    ==  ptr     );
assert(  tomb[5] ==  ptr[5]  );
assert( &tomb[5] ==  qtr     );
assert(   ptr - qtr ==  -5   );

Bár kényelmi okokból a C kifejezésekben a tömböket és pointereket hasonlóan használhatjuk a tömbök és pointerek nem ekvivalensek. A pointer egy fix méretű ojbektum, amely egy másik memóriaterületre történő hivatkozást (vagy a nullpointer értéket) tartalmaz. A tömb pedig azonos típusú elemek véges sorozata. A fordító más kódot generál tomb[i]-ből, mint ptr[i]-ből.

Az alábbi, két forrásfájlból álló példa jól mutatja a jelenséget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* source1.c */
#include <stdio.h>

int t[] = {1,2,3};   /* három elemű int tömb */

extern void f( int *par);
extern void g( int *par);

int main()
{
  int *p = t;          /* p = &t[0] */
  printf("%d",t[1]);   /* 2 */
  printf("%d",p[1]);   /* 2 */
  f(t);
  g(t);
}
void f( int *par)
{
  printf("%d",t[1]);   /* 2 */
  printf("%d",p[1]);   /* 2 */
}

/* ------------------------------- */

/* source2.c */
#include <stdio.h> 

extern  int *t;  /* Ez a hiba! source1.c-ben a t név egy tömb, 
                    nem pedig pointer 
extern int t[];     -vel jól működne a program               */              

void g( int *par)
{
  printf("%d",par[1]); /* 2 */
  printf("%d",t[1]);   /* itt a program valószínűleg elszáll */
}