C'deki işlev işaretçileri için typedef'leri anlama


237

Diğer insanların kodlarını argümanlarla işlevler için typedefs vardı her zaman biraz güdük olmuştur. Bir süre önce C ile yazılmış sayısal bir algoritmayı anlamaya çalışırken böyle bir tanıma ulaşmanın biraz zaman aldığımı hatırlıyorum. Peki, işlevlere işaretçiler (Yapılacaklar ve Yapılmayacaklar) için iyi tip tanımlamaları nasıl yazacağınıza dair ipuçlarınızı ve düşüncelerinizi paylaşabilir misiniz, neden yararlıdırlar ve başkalarının çalışmalarını nasıl anlarsınız? Teşekkürler!


1
Bazı örnekler verebilir misiniz?
Artelius

2
İşlev işaretçileri için makrolar yerine işlev işaretçileri için typedefs demek istemiyor musunuz? İlkini gördüm, ikincisini görmedim.
dave4420

Yanıtlar:


297

signal()C standardındaki işlevi düşünün :

extern void (*signal(int, void(*)(int)))(int);

Tamamen anlaşılır bir şekilde açık - tamsayıyı bağımsız değişken olarak alan ve hiçbir şey döndürmeyen bir işleve iki bağımsız değişken, bir tamsayı ve bir işaretçi alan bir işlevdir ve ( signal()) bir tamsayıyı bağımsız değişken olarak alan ve döndüren bir işleve bir işaretçi döndürür hiçbir şey değil.

Eğer yazarsanız:

typedef void (*SignalHandler)(int signum);

bunun yerine şu şekilde beyan edebilirsiniz signal():

extern  SignalHandler signal(int signum, SignalHandler handler);

Bu aynı şey anlamına gelir, ancak genellikle okunması biraz daha kolay olarak kabul edilir. Fonksiyonun inta SignalHandlerve a alıp a döndürdüğü daha açıktır SignalHandler.

Yine de alışmak biraz zaman alıyor. Yapamayacağınız bir şey SignalHandler typedef, işlev tanımında kullanarak bir sinyal işleyici işlevi yazmaktır .

Ben hala bir işlev işaretçisi olarak çağırmayı tercih eski okuldayım:

(*functionpointer)(arg1, arg2, ...);

Modern sözdizimi sadece şunu kullanır:

functionpointer(arg1, arg2, ...);

Neden işe yaradığını görebiliyorum - Ben sadece bir fonksiyon yerine değişkenin başlatıldığı yeri aramak gerektiğini bilmek istiyorum functionpointer.


Sam yorumladı:

Bu açıklamayı daha önce görmüştüm. Ve sonra, şimdi olduğu gibi, almadım iki ifade arasındaki bağlantı olduğunu düşünüyorum:

    extern void (*signal(int, void()(int)))(int);  /*and*/

    typedef void (*SignalHandler)(int signum);
    extern SignalHandler signal(int signum, SignalHandler handler);

Ya da sormak istediğim şey, sahip olduğunuz ikinci versiyonu bulmak için kullanabileceğiniz temel kavram nedir? "SignalHandler" ı ve ilk typedef'i bağlayan temel unsur nedir? Burada açıklanması gereken şey, typedef'in aslında burada yaptığı şey.

Tekrar deneyelim. Bunlardan birincisi doğrudan C standardından kaldırılır - tekrar yazdım ve parantezlerin doğru olduğunu kontrol ettim (düzeltene kadar değil - hatırlanması zor bir çerez).

Her şeyden önce, typedefbir tür için bir takma ad getirdiğini unutmayın . Yani, takma ad SignalHandlerve türü:

bağımsız değişkeni bağımsız değişken olarak alan ve hiçbir şey döndürmeyen bir işleve işaretçi.

'Hiçbir şey döndürmez' bölümü hecelenir void; bir tamsayı olan argüman kendime açıklayıcıdır (güveniyorum). Aşağıdaki gösterim, basitçe (veya değil) C'nin belirtildiği gibi argümanları alma ve verilen türü döndürme işlevine işaretçiyi nasıl yazdığını gösterir:

type (*function)(argtypes);

Sinyal işleyici türünü oluşturduktan sonra bunu değişkenleri bildirmek için kullanabilirim. Örneğin:

static void alarm_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d)\n", __func__, signum);
}

static void signal_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d) - exiting\n", __func__, signum);
    exit(1);
}

static struct Handlers
{
    int              signum;
    SignalHandler    handler;
} handler[] =
{
    { SIGALRM,   alarm_catcher  },
    { SIGINT,    signal_catcher },
    { SIGQUIT,   signal_catcher },
};

int main(void)
{
    size_t num_handlers = sizeof(handler) / sizeof(handler[0]);
    size_t i;

    for (i = 0; i < num_handlers; i++)
    {
        SignalHandler old_handler = signal(handler[i].signum, SIG_IGN);
        if (old_handler != SIG_IGN)
            old_handler = signal(handler[i].signum, handler[i].handler);
        assert(old_handler == SIG_IGN);
    }

    ...continue with ordinary processing...

    return(EXIT_SUCCESS);
}

Lütfen dikkat Bir sinyal işleyicide kullanmaktan nasıl kaçınılır printf()?

Peki, burada ne yaptık - kodun temiz bir şekilde derlenmesi için ihtiyaç duyulacak 4 standart üstbilgiyi atlayın?

İlk iki işlev, tek bir tamsayı alan ve hiçbir şey döndürmeyen işlevlerdir. Biri aslında sayesinde exit(1);geri dönmüyor, diğeri ise bir mesaj yazdırdıktan sonra geri dönüyor. C standardının bir sinyal işleyici içinde çok fazla şey yapmanıza izin vermediğini unutmayın; POSIX , izin verilen şeylerde biraz daha cömerttir, ancak resmi olarak çağrıya izin vermez fprintf(). Alınan sinyal numarasını da yazdırıyorum. İşlevde, alarm_handler()değer her zaman SIGALRMbir işleyici olduğu tek sinyal olduğu gibi olacaktır, ancak her ikisi için de aynı işlev kullanıldığı için sinyal numarası olarak veya signal_handler()alabilir .SIGINTSIGQUIT

Daha sonra, her elemanın bir sinyal numarasını ve bu sinyal için kurulacak işleyiciyi tanımladığı yapılar dizisi oluşturuyorum. Yaklaşık 3 sinyal için endişelenmeyi seçtim; Sık sık dert ediyorum SIGHUP, SIGPIPEve SIGTERMçok onlar (tanımlanmıştır olmadığı konusunda #ifdefkoşullu derleme), ama bu sadece bir şeyler zorlaştırmaktadır. Muhtemelen sigaction()bunun yerine POSIX kullanırdım signal(), ama bu başka bir sorun; Başladığımız şeye sadık kalalım.

main()İşleyicileri listesinin üzerine fonksiyon yineler kurulacak. Her işleyici için, ilk signal()olarak işlemin şu anda sinyali yoksayıp almadığını öğrenmek için arama yapar ve bunu yaparken SIG_IGN, işleyicinin olarak yüklenerek sinyalin yok sayılmasını sağlar. Sinyal daha önce yoksayılmamışsa signal(), bu kez tercih edilen sinyal işleyiciyi kurmak için tekrar arar . (Diğer değer tahminen olduğu SIG_DFLsinyali için varsayılan sinyal işleyici.) İçin ilk çağrı 'sinyali ()' olarak işleyici ayarladığımızdan SIG_IGNve signal()önceki hata işleyicisi, değerini verir oldsonra ifdeyim olmalıdır SIG_IGN- bu nedenle iddiayı. (Şey, olabilirSIG_ERR eğer bir şey önemli ölçüde yanlış giderse - ama o zaman bunu iddia ateşlemesinden öğrenirdim.)

Program daha sonra işlerini yapar ve normal olarak çıkar.

Bir işlevin adının, uygun türde bir işleve bir işaretçi olarak kabul edilebileceğini unutmayın. İşlev çağrısı parantezlerini (örneğin, başlatıcılarda olduğu gibi) uygulamadığınızda, işlev adı bir işlev işaretçisi haline gelir. Bu nedenle pointertofunction(arg1, arg2)gösterim yoluyla işlevleri çağırmak da mantıklıdır ; gördüğünüzde alarm_handler(1), alarm_handlerbunun işleve bir işaretçi olduğunu ve bu nedenle alarm_handler(1)bir işlev işaretçisi aracılığıyla bir işlevin çağrılması olduğunu düşünebilirsiniz .

Şimdiye kadar, bir SignalHandlerdeğişkenin, atamak için doğru değer türlerinden bazılarına sahip olduğunuz sürece, göreceli olarak basit olduğunu gösterdim - bu, iki sinyal işleyici işlevinin sağladığı şeydir.

Şimdi şu soruya dönüyoruz - iki beyan signal()birbiriyle nasıl ilişkilidir?

İkinci beyanı inceleyelim:

 extern SignalHandler signal(int signum, SignalHandler handler);

İşlev adını ve türünü şu şekilde değiştirirsek:

 extern double function(int num1, double num2);

bunu bir intve a işlevlerini doubleargüman olarak alan ve bir doubledeğer döndüren bir işlev olarak yorumlamakta sorun yaşamazsınız (belki de sorunluysa bu konuda karar vermemelisiniz - ama belki de bu kadar zor sorular sormakta dikkatli olmalısınız) bu bir sorun ise).

Şimdi, yerine olmanın double, signal()fonksiyon alır SignalHandlerikinci argüman olarak ve onun sonucunda bir döndürür.

Bunun da işlenebileceği mekanik:

extern void (*signal(int signum, void(*handler)(int signum)))(int signum);

açıklamak zor - bu yüzden muhtemelen berbat edeceğim. Bu kez parametrelerin isimlerini verdim - isimler kritik olmasa da.

Genel olarak, C'de, beyan mekanizması şöyle yazar:

type var;

o zaman yazdığınızda varverilen bir değeri temsil eder type. Örneğin:

int     i;            // i is an int
int    *ip;           // *ip is an int, so ip is a pointer to an integer
int     abs(int val); // abs(-1) is an int, so abs is a (pointer to a)
                      // function returning an int and taking an int argument

Standartta, typedefdilbilgisinde bir depolama sınıfı olarak ele alınır, daha ziyade staticve externdepolama sınıflarıdır.

typedef void (*SignalHandler)(int signum);

şu şekilde SignalHandlerçağrılmış bir tür değişken (alarm_handler) gördüğünüzde:

(*alarm_handler)(-1);

sonuç var type void- sonuç yok. Ve argümanla (*alarm_handler)(-1);çağrılmasıdır .alarm_handler()-1

Yani, eğer beyan edersek:

extern SignalHandler alt_signal(void);

demek oluyor:

(*alt_signal)();

bir boşluk değerini temsil eder. Ve bu nedenle:

extern void (*alt_signal(void))(int signum);

eşdeğerdir. Şimdi, signal()daha karmaşık çünkü sadece a döndürmekle kalmıyor SignalHandler, aynı zamanda hem int hem de a SignalHandlerdeğişkenlerini kabul ediyor :

extern void (*signal(int signum, SignalHandler handler))(int signum);

extern void (*signal(int signum, void (*handler)(int signum)))(int signum);

Bu hala sizi şaşırtıyorsa, nasıl yardım edeceğimden emin değilim - hala benim için gizemli, ama nasıl çalıştığına alıştım ve bu yüzden size 25 yıl daha yapışırsanız söyleyebilirim ya da öyleyse, bu sizin için ikinci bir doğa olacaktır (ve belki de akıllıysanız biraz daha hızlı).


3
Bu açıklamayı daha önce görmüştüm. Ve sonra, şimdi olduğu gibi, almadım iki ifade arasındaki bağlantıydı: extern void ( sinyal (int, void ( ) (int))) (int); / * ve * / typedef void (* SignalHandler) (int signum); harici SignalHandler sinyali (int signum, SignalHandler işleyicisi); Veya sormak istediğim şey, sahip olduğunuz ikinci versiyonu bulmak için kullanabileceğiniz temel kavram nedir? "SignalHandler" ı ve ilk typedef'i bağlayan temel unsur nedir? Burada açıklanması gereken şey, typedef'in aslında burada yaptığı şey. Thx

6
Harika cevap, bu konuya geri döndüğüme sevindim. Her şeyi anladığımı sanmıyorum, ama bir gün anlayacağım. Bu yüzden SO'yu seviyorum. Teşekkür ederim.
toto

2
Sadece bir nit seçmek için: bir sinyal işleyici içindeki printf () ve arkadaşlarını aramak güvenli değildir; printf () yeniden girilmez (temel olarak, reentrant olmayan malloc () öğesini çağırabildiği için)
wildplasser

4
extern void (*signal(int, void(*)(int)))(int);Araçlar signal(int, void(*)(int))için bir fonksiyon, işaretçi döndürür void f(int). Dönüş değeri olarak bir işlev işaretçisi belirtmek istediğinizde , sözdizimi karmaşıklaşır. Dönüş değeri türünü sola , argüman listesini sağa , tanımladığınız orta konum olarak yerleştirmeniz gerekir. Ve bu durumda, signal()fonksiyonun kendisi parametre olarak bir fonksiyon işaretçisini alır, bu da işleri daha da karmaşık hale getirir. İyi haber şu ki, bunu okuyabilirseniz , Kuvvet zaten sizinle. :).
smwikipedia

1
Eski okul &bir işlev adının önünde kullanma hakkında ne düşünüyor ? Tamamen gereksiz; anlamsız. Ve kesinlikle "eski okul" değil. Eski okul, basit ve basit bir işlev adı kullanır.
Jonathan Leffler

80

Bir işlev işaretçisi diğer işaretçiler gibidir, ancak veri adresi (yığın veya yığın üzerinde) yerine bir işlevin adresini gösterir. Herhangi bir işaretçi gibi, doğru yazılması gerekir. İşlevler, dönüş değerleri ve kabul ettikleri parametre türleri ile tanımlanır. Bu nedenle, bir işlevi tam olarak tanımlamak için, dönüş değerini eklemeniz gerekir ve her parametrenin türü kabul edilir. Böyle bir tanım yazdığınızda, bu tanımı kullanarak işaretçiler oluşturmayı ve referans oluşturmayı kolaylaştıran 'kolay bir ad' verirsiniz.

Örneğin, bir fonksiyonunuz olduğunu varsayalım:

float doMultiplication (float num1, float num2 ) {
    return num1 * num2; }

sonra aşağıdaki typedef:

typedef float(*pt2Func)(float, float);

bu doMulitplicationişleve işaret etmek için kullanılabilir . Bir şamandıra döndüren ve her biri şamandıra tipi olmak üzere iki parametre alan bir işleve bir işaretçi tanımlar. Bu tanımın kolay adı vardır pt2Func. pt2FuncBir şamandıra döndüren ve 2 şamandıra alan HERHANGİ bir işlevi işaret edebileceğini unutmayın .

Böylece doMultiplication işlevini aşağıdaki gibi gösteren bir işaretçi oluşturabilirsiniz:

pt2Func *myFnPtr = &doMultiplication;

ve bu işaretçiyi kullanarak işlevi aşağıdaki gibi çağırabilirsiniz:

float result = (*myFnPtr)(2.0, 5.1);

Bu iyi bir okuma yapar: http://www.newty.de/fpt/index.html


psikotik, teşekkürler! Bu yardımcı oldu. İşlev işaretçileri web sayfasına bağlantı gerçekten yararlıdır. Şimdi okuyorum.

... Ancak, bu newty.de bağlantısı hiç typedefs hakkında konuşmak gibi görünmüyor :( Yani bu bağlantı harika olsa da, bu konuda typedefs ile ilgili cevaplar paha biçilmez!

11
Sen yapmak istediği belki pt2Func myFnPtr = &doMultiplication;yerine pt2Func *myFnPtr = &doMultiplication;olarak myFnPtrzaten göstericisidir.
Tamilselvan

1
pt2Func * bildirme myFnPtr = & doMultiplication; pt2Func yerine myFnPtr = & doMultiplication; bir uyarı verir.
AlphaGoku

2
@Tamilselvan doğru. myFunPtrzaten bir fonksiyon göstergesidir, bu yüzden kullanınpt2Func myFnPtr = &doMultiplication;
Dustin Biser

35

İşlev işaretçisinin typedef değerini anlamanın çok kolay bir yolu:

int add(int a, int b)
{
    return (a+b);
}

typedef int (*add_integer)(int, int); //declaration of function pointer

int main()
{
    add_integer addition = add; //typedef assigns a new variable i.e. "addition" to original function "add"
    int c = addition(11, 11);   //calling function via new variable
    printf("%d",c);
    return 0;
}

32

cdeclişlev işaretçisi bildirimleri gibi garip sözdizimini deşifre etmek için harika bir araçtır. Bunları oluşturmak için de kullanabilirsiniz.

Karmaşık bildirimleri gelecekteki bakım için (kendiniz veya başkaları tarafından) ayrıştırmayı kolaylaştıracak ipuçlarına gelince typedef, küçük parçaları silmenizi ve bu küçük parçaları daha büyük ve daha karmaşık ifadeler için yapı taşları olarak kullanmanızı öneririm . Örneğin:

typedef int (*FUNC_TYPE_1)(void);
typedef double (*FUNC_TYPE_2)(void);
typedef FUNC_TYPE_1 (*FUNC_TYPE_3)(FUNC_TYPE_2);

ziyade:

typedef int (*(*FUNC_TYPE_3)(double (*)(void)))(void);

cdecl bu konuda size yardımcı olabilir:

cdecl> explain int (*FUNC_TYPE_1)(void)
declare FUNC_TYPE_1 as pointer to function (void) returning int
cdecl> explain double (*FUNC_TYPE_2)(void)
declare FUNC_TYPE_2 as pointer to function (void) returning double
cdecl> declare FUNC_TYPE_3 as pointer to function (pointer to function (void) returning double) returning pointer to function (void) returning int
int (*(*FUNC_TYPE_3)(double (*)(void )))(void )

Ve aslında bu çılgın karışıklığı tam olarak nasıl yarattığım.


2
Merhaba Carl, bu çok anlayışlı bir örnek ve açıklama. Ayrıca, cdecl kullanımını gösterdiğin için teşekkürler. Çok takdir etmek.

Windows için CDECL var mı?
Jack

@Jack, eminim inşa edebilirsin, evet.
Carl Norum

2
Aynı türden yetenekler sağlayan ancak çevrimiçi olan cdecl.org da var . Bizim için Windows geliştiricileri yararlı.
zaknotzach

12
int add(int a, int b)
{
  return (a+b);
}
int minus(int a, int b)
{
  return (a-b);
}

typedef int (*math_func)(int, int); //declaration of function pointer

int main()
{
  math_func addition = add;  //typedef assigns a new variable i.e. "addition" to original function "add"
  math_func substract = minus; //typedef assigns a new variable i.e. "substract" to original function "minus"

  int c = addition(11, 11);   //calling function via new variable
  printf("%d\n",c);
  c = substract(11, 5);   //calling function via new variable
  printf("%d",c);
  return 0;
}

Bunun çıktısı:

22

6

Her iki işlevi bildirmek için aynı math_func tanımlayıcısının kullanıldığını unutmayın.

Aynı tipel yaklaşımı harici yapı için de kullanılabilir (başka bir dosyada sturuct kullanılarak).


5

İşlev işaretçileri gibi daha karmaşık türleri tanımlamak için typedefs kullanın

C'de bir durum makinesi tanımlama örneğini ele alacağım

    typedef  int (*action_handler_t)(void *ctx, void *data);

şimdi action_handler adında iki işaretçi alan ve bir int döndüren bir tür tanımladık

durum makinenizi tanımlayın

    typedef struct
    {
      state_t curr_state;   /* Enum for the Current state */
      event_t event;  /* Enum for the event */
      state_t next_state;   /* Enum for the next state */
      action_handler_t event_handler; /* Function-pointer to the action */

     }state_element;

Eylemin işlev işaretçisi basit bir türe benziyor ve typedef öncelikle bu amaca hizmet ediyor.

Tüm olay işleyicilerim artık action_handler tarafından tanımlanan türe uymalıdır

    int handle_event_a(void *fsm_ctx, void *in_msg );

    int handle_event_b(void *fsm_ctx, void *in_msg );

Referanslar:

Linden tarafından uzman C programlama


4

Bu, alıştırma olarak yazdığım işlev işaretçileri ve işlev işaretçi dizilerinin en basit örneğidir.

    typedef double (*pf)(double x);  /*this defines a type pf */

    double f1(double x) { return(x+x);}
    double f2(double x) { return(x*x);}

    pf pa[] = {f1, f2};


    main()
    {
        pf p;

        p = pa[0];
        printf("%f\n", p(3.0));
        p = pa[1];
        printf("%f\n", p(3.0));
    }
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.