klasszikus C pointerek, mi a különbség?

 ( tovis | 2008. június 20., péntek - 9:44 )

GNU libc6 tsearch és tfind

void *tsearch(__const void *__key,void **__rootp,__compar_fn_t __compar);

void *tfind(__const void *__key,void *__const *__rootp,__compar_fn_t __compar);

Restellem, de nem igazán értem mi a különbség a

void **__rootp

és a

void *__const *__rootp

pointerek között.

UI: Vannak sejtéseim, de mielőtt a magam "butaságait" eröltetném, szeretném mások definícióját hallani.

Hozzászólás megjelenítési lehetőségek

A választott hozzászólás megjelenítési mód a „Beállítás” gombbal rögzíthető.

Jó. Azt már tudom én hol rontottam el (a pointer értelmezésnbak ehhez semmi köze) Sajna csak ékes angolsággal leírt szöveg van arról milyen egy tétel amit ezek a rutinok kezelnek :)

* Én egy indián vagyok. Minden indián hazudik.

Kezdjük az elején.

void *x;
Ekkor x egy olyan változó lesz, ami egy címet tartalmaz, és a cím egy olyan memóriaterületre mutat, ahol egy "void" típusú érték található vagy egy tömb első eleme, amely tömbnek az elemei "void" típusúak.
Így
v = *x;
értékadás az jelenti, hogy behívjuk azt a "void" típusú változót, ami x-ben lévő címen van, és v-be másoljuk.

void **x;
Ekkor az x-ben lévő cím nem közvetlenül "void" típusú elemre (vagy tömbre) mutat, hanem olyan mutatóra (vagy mutató tömbre), ami (vagy aminek az elemei) már tényleg "void" típusú elemre (vagy tömbre) mutat (vagy mutatnak). Tehát a lényeg, hogy x egy másik mutatóra fog mutatni nem pedig "void" típusú memória területre.
Így
pv = *x;
értékadás azt jelenti, hogy egy olyan változónak adjuk át az x által mutatott memóriaterület tartalmát, amit void *pv; módon deklaráltunk. Azaz a pv-ben lévő cím fog majd egy "void" típusú változóra mutatni.

void *nem_void_tipus *x;
Egy ilyen deklarációt az én fordítóm (g++) nem enged meg.
Sőt ezt sem:
void *void *x;
Ezt a szintaktikát én abszolút nem értem. Nem is tudom jelenlegi fogalmaimmal elképzelni, hogy mi lehet.
Lehet, hogy C-ben megengedett, C++-ban pedig nem?
Ha valaki tudja a választ ezekre a kérdésekre, ne hallgassa el! Én is szívesen okulnék belőle.

Egyébként számomra az is érdekes, hogyan lehet egy változó "void" típusú. Mit takar ez?

Változó!
Mármint nem úgy... :D
Nem void típusú változóról van szó, hanem void típusú mutatóról!
A void típusú mutató az általános mutatótípus, amely infóvesztés nélkül átalakítható bármilyen más típusú mutatóvá, és vissza.
Mivel mindegyik egy memóriacímet tartalmaz (ugyanolyan memóriacímet)...

OK. Így már értem. Kösz a magyarázatot!

Elírás lehet a második. Honnan másoltad?

void *tsearch(const void *key, void **rootp, int(*compar)(const void *, const void *));

void *tfind(const void *key, const void **rootp, int(*compar)(const void *, const void *));

Bocs!

Még egy kérdés. És az alábbi deklaráció mit jelent?

Idézet:
int(*compar)(const void *, const void *)

int(*compar)(const void *, const void *)

Függvényre mutató mutató!
A compar egy olyan függvényt címző mutató, amelynek két const void * argumentuma van, és a visszatérési értéke int típusú.

Azaz például variálhatod, hogy az fsearch függvény milyen függvényt használjon két eleme összehasonlítására.

c-ben lehet a függvényekre mutatni..., (szerepelhetnek tömbben, értékadásban, lehet függvény visszatérési értéke, és argumentuma.
(életemben nem programoztam ilyet)

Azaz a *compar a függvény. Viszont asszem elhagyhatod az argumentumot, mikor használod a tsearch-et (akkor az alapértelmezett compare függvényt használja)
(*compar)(ez, az);
módon kell használni, meghívni majdan a függvényben, aminek átadtad.

Ha tehát van egy olyan függvényem, aminek a deklarációja így néz ki:

   int hasonlit(const int* i1, const int* i2);

akkor ezt beletehetem a tfind függvénybe mint argumentum? Kb. így:

   int* mutat = tfind(a_key, a_rootp, &hasonlit(int1, int2));

Így most helyes vagy sem?

Nem, ez így nem jó.

Először is a compar deklarációja ez:

int (*compar)(const void *, const void *);

Tehát void-nak kell lennie a típusnak, hogy a gcc hiba nélkül lefordítsa. Az intre való konverziót a hasonlít függvényben kell elvégezni.

Továbbá:

int* mutat = tfind(a_key, a_rootp, &hasonlit(int1, int2));

A hasonlít(int1, int2) az egy függvényhívás c-ben, neked nem ezt kell csinálni, hanem a függvény memóriacímét kell átadnod, tehát:

int* mutat = tfind(a_key, a_rootp, hasonlit);

(Nem kell & a hasonlit elé)

Nekem valamiért az rémlik, hogy futás közben már nem változhat. Azaz már fordításkor végleges lesz, hogy adott tsearch hívásakor melyik függvényt használja majd a változók összehasonlítására.
Így van?

Nem; pont azért lehet paraméterként megadni a függvényre mutató mutatót, hogy futás közben is bármikor megváltoztasd. Az, hogy általában egy hívásnál mindig ugyanazt az összehasonlító függvényt adjuk be neki, az nem változtat azon, hogy ugyanannak a tsearch függvénynek egy másik hívása mást használ, ezért paraméterezhető kell legyen.

Ha fordításkor eldőlő dolgok érdekelnek, akkor a C++-t és a sablonokat nézd meg.

Sima c...
Úgy értettem hogy a forrásban egy helyen meghívva a tsearch, mindig ugyanazt a compare függvényt kell hogy használja.
Azaz ott nem lehet a tsearch függvény paramétere például egy tömbben tárolt függvénymutató, ha a tömb indexe futás közben változik.
Kétségtelenül ettől sokkal kevésbe lenne rugalmas a használhatósága, de azért olyat még lehetne csinálni, hogy két helyen hivod meg a tsearch-et, és mindkét helyen más compare függvényt használ.

Szóval hogy az általad írt "általában" helyett mindig lenne.

Érthető hogyan gondoltam, meg mi a különbség a kettő között? Nem tudom eldönteni...

Viszont biztos igazad van, és nem úgy van ahogy nekem rémlett valamiért..., nem is tudom hogy miért. Sohasem használtam c-ben függvénymutatót, csak valamiért az elméletre így emlékeztem c kapcsán.

Nem; bármikor használhat más mutatót. Igen, pl. tömbelem is lehet a mutató. Próbáld ki.

Onnan is meg lehet közelíteni, hogy ahány helyen hívod, de legalábbis ahány mutatóval hívod, annyi példányban kellene le fordítani a tsearch-ot, a különböző callback-ek miatt. Ezt azért nem teszi meg a fordító. Bár maga a visszahívás gyorsabb lenne egy kicsivel, ez tény, de elhanyagolható.

(Nem kell & a hasonlit elé)
Nem kell, de lehet. Ugyanis fuggvenyeknel es fuggvenymutatoknal az * (indirekcio) es a & (cime) operatorok elhagyhatoak.

Nem elírás, te gyakorlatilag ugyanazt írtad.

Az "__const" ugyanazt jelenti, mint a "const" kulcsszó?

__const vagy makro, vagy compiler extenzio, ami valoszinuleg vagy const-kent helyettesitodik be, vagy teljesen eltunik.

Aha, értem. Köszi!

Azaz válaszolva a kérdésedre, a különbség egy plussz const, és hogy az egyik el van írva.

Ha el van írva, és úgy helyes, ahogy te írod, akkor teljesen világos a helyzet. Biztos csodálkoztam volna azon a válaszon, ami a fenti példát meg tudja magyarázni.

Kösz!

Amúgy:
man tsearch
man tfind
-ból másoltam a deklarációkat. Nem tévedhet nagyot. Szerintem első körben ezt használjátok.

A fo kerdesre eddig adott valaszok nem felelnek meg a valosagnak.

void **x;
Itt x egy olyan mutato, amely egy tetszoleges tipusu mutatora mutat.

void *const *x;
Itt x egy olyan mutato, amely egy tetszoleges tipusu konstans mutatora mutat.

Tehat hol kulonbozik a hasznalat:

void **x;
void *const *y;

x = &ptr;         // helyes: x nem konstans
y = &ptr;         // helyes: y nem konstans
*x = &a;          // helyes: x nem konstans mutatora mutat
*y = &a;          // HIBA: y konstans mutatora mutat, nem lehet ertekadas
**x = 3;          // HIBA: **x void tipusu, nem lehet neki erteket adni
**y = 3;          // HIBA: **y void tipusu, nem lehet neki erteket adni
*(int *)*x = 3;   // explicit tipuskonverzio, rendben
*(int *)*y = 3;   // explicit tipuskonverzio, rendben

"void *const *x;
Itt x egy olyan mutato, amely egy tetszoleges tipusu konstans mutatora mutat."
--

Sztem te a
const void **x; -re gondolsz. :)
De próbáld ki:

int main() {

    void *const *y;
    const void *ptr;

    // test.cpp:x: error: invalid conversion from ‘const void**’ to ‘void* const*’
    y = &ptr;        // pedig ez const mutató... :)


    void *aaa;
    void *const *yy = &aaa; // lefordul szépen. pedig az "aaa" nem const.

}

A saját definíciómat - legalábbis C++ esetén - lsd. lentebb.

kl223

quote:

*x = &a;          // helyes: x nem konstans mutatora mutat
*y = &a;          // HIBA: y konstans mutatora mutat, nem lehet ertekadas
**x = 3;          // HIBA: **x void tipusu, nem lehet neki erteket adni
**y = 3;          // HIBA: **y void tipusu, nem lehet neki erteket adni

szoval szerintem erti.

Amugy erdemes azt tudni, h a const balra kot. Kiveve a bal szelen :). Tehat erdemes megszokni a kovetkezo jeloleseket:

// sima valtozok
const int ci;// felrevezeto
int const ic;// konzekvens

// mutatok
// konstanra mutato nem konstans
const int * cip;// felrevezeto
int const * icp;// konzekvens

//nem konstansra mutato konstans
// nincs felrevezeto
int * const ipc;// konzekvens

//konstansra mutato konstans
const int * const cipc;// felrevezeto
int const * const icpc;// konzekvens

"const balra kot. Kiveve a bal szelen"

Ebbe inkább bele sem gondolok. A lényeg, hogy a * melyik oldalán van.

KisKresz

"szoval szerintem erti."

Igen, azt néztem, hogy a kód ott végülis jó. Csak a definíció nem fedte a valóságot abban a formában. (A kódba nem is kötöttem bele.)

kl223

Gondold at megegyszer! Ebben a peldaban ket csillag van, nem egy.

Ne keverd a "void *const *"-ot a "void **const"-tal!

"void *const *rootp"
"void **const rootp"
"const void **rootp"

Ez három dolog, vagy kettő..., az egyik kétféle képpen leírva? :)
Nem bogarásztam végig a példákat, nekem ezek most nem aktuálisak. c sem, de azért érdekelne egy rövid és tömör válasz! :)

Ez harom dolog.

Semmi, benéztem (;

Ez három dolog.
void *const *const rootp = ...;
És még ilyen is van. :)
Meg persze a const előtaggal is lehet kombinálni az első kettődet és ezt is.

Ha három dolog, akkor viszont vagy a témaindító írásába bemásolt, vagy az általam korábban bemásolt tfind deklaráció hibás nem?

Az általam bemásolt nem hibás, ill. én mindenhol így találtam meg.

Lehet persze hogy különböző c-kben, vagy c++-ban más-más..., de nem tartom valószínűnek. Vagy mégis?

Én is így látom.

Nem használtam még ezeket a függvényeket, de szerintem a témaindítóban lévő változat lesz a hibás. Ugye az adatra mutat egy mutató, ennek a mutatónak a címét adjuk át (másszóval a mutatót adjuk át cím szerint).

const void * * rootp
void const * * rootp

a függvény megígéri, hogy nem nyúl az adathoz, bár a mutatót odébb tolhatja rajta.

void * const * rootp

megígéri, hogy nem nyúl a mutatóhoz (amely az adatra mutat). Az adatot azonban felülcsaphatja. Ennek én se nem sok értelmét látnám.

"de szerintem a témaindítóban lévő változat lesz a hibás."

$ grep -A1 tfind /usr/include/search.h 
extern void *tfind (__const void *__key, void *__const *__rootp,
		    __compar_fn_t __compar);

Nem bírtam tovább, megnéztem a tfind forrását.

Van egy mutató (rootp), ami egy konstans mutatóra mutat (pl. root), ami egy struct node_t-re mutat.
Valóban jó, ha levédjük a root mutatót felülírás ellen. A man ezek szerint rosszul mondja.
Jó lenne az adatot is levédeni (a node_t területet), azaz const void *const *rootp lehetne a paraméter (lefordul így).

Tehát mindkettő ,,rossz'', de mindkettő működik. Adalék: még a harmadik (általam javasolt) megoldás sem lenne elég jó, mivel mindegyik csak a gyökérpontot védi meg, a fa többi pontjával úgysem tudnak mit csinálni...

Hát sztem egyik variációra sem igaz, hogy "tetszőleges típusú konstans mutatóra mutat".
De megkérlek, hogy javíts ki, ha tévednék.

Lehet, hogy szavakkal nem ez a legerthetobb megfogalmazasa.

En ugy gondoltam, hogy tetszoleges tipusu [void] konstans mutatora [*const] mutat [*]. Szoval ez egy olyan pointer, ami nem konstans, de konstans altalanos pointerre [void *const] mutat.

Nem láttam egyértelmű választ erre eddig a szálban, szóval (kis fejtágítás ^^):
egy mutató kétféleképpen lehet const:

ad1) úgy, hogy az általa megcímzett memóriát rajta keresztül nem lehet módosítani. Ebben az esetben az, hogy a mutató hova mutat, szabadon módosítható a program futása során. Pl:

int i, j;
const int *ip = &i;   // helyes
ip = &j;              // helyes
*ip = 1;              // fordítási hiba

ad2) úgy, hogy a mutató által megcímzett memóriaterület ugyan módosítható rajta keresztül, de az nem, hogy a mutató hova mutasson. Ekkor értelemszerűen inicializáláskor értéket kell adni a mutatónak. Ennek a szintaxisa az az ominózus *const. Pl:

int i, j;
int *const ip = &i;  // helyes
*ip = 1;             // helyes
ip = &j;             // hibás

A kettőt lehet kombinálni is. Ekkor egy "nagyon const" mutatót kapunk valamire, amin semmit nem lehet módosítani. ;)

C-ben a const-ot simán lehet konvertálni nem const-tá, ott nincs sok értelme.
C++ban viszont van, ott komolyan veszik ezt a kulcsszót.

Egyébként C++ban ezért van értelme egy ilyen definíciójú metódusnak:

class A {
    public:
    void aaa();
    void vmi() const;
};

A fenti deklaráció azt jelenti, hogy a vmi() metódushívás nem fogja megváltoztatni az objektumát, ezért akkor is meghívható, ha az objektumhoz csak const referencián/mutatón keresztül férünk hozzá. Pl:

const A *a = ....; // vhogy értéket kap
a->vmi();          // helyes
a->aaa();          // fordítási hiba

Úgy is mondhatnánk, hogy az aaa() metódusban a "this" mutató vhogy így lehet deklarálva:
A *const this = ...;
...míg a vmi() metódusban vhogy így:
const A *const this = ...;

Üdv,
kl223

...és akkor most jó a témát indító írásban a tfind deklarációja, vagy nem? Mert szerintem az volt a fő kérdés?! :)

C/C++ban alapértelmezés szerint érték szerinti paraméterátadás van.
Magyarul ha átadsz egy függvénynek egy mutatót, akkor a függvény ugyan módosíthatja azt, hogy a mutató hova mutasson, de ez a módosítás nem fog látszódni a függvényen kívül.
Miért? Mert a függvénybeli mutató csak egy lokális másolata az eredetinek. (Természetesen azt a memóriacímet, ahova a pointer mutat, ettől függetlenül lehet valóban módosítani a függvényen belülről, és épp ez a lényeg....)

Ugyanis... mi is a helyzet a "tfind" deklarációban? Ott "pointerre mutató pointerünk" van. Tehát a függvény a sima void **ptr paraméternél képes lenne valóban módosítani a második pointer értékét.
Az a "tfind" deklaráció mindössze annyit köt ki, hogy a függvény _azt_ változatlanul hagyja, hogy az átadott pointerek hova mutatnak. (lényegében ha látod azt a *const-ot egy függvénydeklarációban, az inkább megnyugvásra adhat okot, mint aggódásra. ^.-)

Mutatok példát!

#include <iostream>
#include <cstdio>

using namespace std;

void test1( int **p ) {
    p = 0;
}

void test2( int **p ) {
    *p = 0;
}

int main() {

    int i = 1;
    int *a = &i;
    int **y = &a;

    cout << "orig : " << y << " " << *y << endl;
    test1( y );
    cout << "test1: " << y << " " << *y << endl;
    test2( y );
    cout << "test2: " << y << " " << *y << endl;

    return 0;
}

A program kimenete pedig:

kl223@freelancer /mnt/prog/fun $ ./test
orig : 0xbfed7654 0xbfed7658
test1: 0xbfed7654 0xbfed7658
test2: 0xbfed7654 0
kl223@freelancer /mnt/prog/fun $

Mit kell itt látni? Azt, hogy a "test1" nem módosította a neki átadott paramétert.
A "test2" viszont igen, és ez nem feltétlenül jó.

Ha viszont egy kicsit módosítunk a függvények deklarációján, valahogy így:

void test1( int *const *p ) {
    p = 0;
}

void test2( int *const *p ) {

    // test.cpp:11: error: assignment of read-only location ‘* p’
    *p = 0;
}

Akkor teljesen biztos, hogy a mutatóink nem "sérülnek", mert fordítási hibát fog dobni az a kód, ami megpróbálja módosítani a mutatókban tárolt memóriacímet. (és itt most nem a memóriacímen tárolt adatokról beszélek, hanem lényegében a mutató jobbértékéről - asszem így hívják)

Természetesen a fenti, módosított test{1,2} metódusok a mutatók által megcímzett memóriacímen lévő adatokat ugyanúgy módosíthatják, mint az eredeti példakódban lévők.

Summa summarum: _igen_, jó az a deklaráció. Ne aggódj miatta. :P

Üdv,
kl223

[szerk: legalábbis szintaktikailag jó... hogy a glibc-ben így van-e benne azt nemtom. :)]

Át fogom nézni az összes hozzászólást, és kipróbálok néhány variációt. Valószínűleg az sokat fog segíteni a megértésben.