C'deki işlev işaretçileri nasıl çalışır?


1233

Son zamanlarda C fonksiyon göstergeleri ile biraz deneyimim oldu.

Bu yüzden, kendi sorularınızı yanıtlama geleneğine devam ederek, konuya hızlı bir şekilde dalmak isteyenler için temel bilgilerin küçük bir özetini yapmaya karar verdim.


35
Ayrıca: C işaretçilerin biraz derinlemesine analizi için bkz. Blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . Ayrıca, Sıfırdan Programlama , makine düzeyinde nasıl çalıştıklarını gösterir. Anlamak C'nin "bellek modeli" C çalışmalarını işaret eden anlayış nasıl çok yararlıdır.
Abbafei

8
Harika bilgi. Bununla birlikte, başlığa göre, gerçekten nasıl kodlandıklarını değil, "fonksiyon işaretleyicilerinin nasıl çalıştığına" ilişkin bir açıklama görmeyi beklerdim :)
Bogdan Alexandru

Yanıtlar:


1478

C'deki fonksiyon göstergeleri

İşaret edeceğimiz temel bir işlevle başlayalım :

int addInt(int n, int m) {
    return n+m;
}

İlk olarak, 2 ints alan ve aşağıdakileri döndüren bir işleve bir işaretçi tanımlayalım int:

int (*functionPtr)(int,int);

Şimdi fonksiyonumuza güvenle işaret edebiliriz:

functionPtr = &addInt;

Şimdi işleve bir işaretçi olduğuna göre, onu kullanalım:

int sum = (*functionPtr)(2, 3); // sum == 5

İşaretçiyi başka bir işleve geçirmek temelde aynıdır:

int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}

İşlev değerlerini dönüş değerlerinde de kullanabiliriz (devam etmeye çalışın, dağınık olur):

// this is a function called functionFactory which receives parameter n
// and returns a pointer to another function which receives two ints
// and it returns another int
int (*functionFactory(int n))(int, int) {
    printf("Got parameter %d", n);
    int (*functionPtr)(int,int) = &addInt;
    return functionPtr;
}

Ancak aşağıdakileri kullanmak çok daha hoştur typedef:

typedef int (*myFuncDef)(int, int);
// note that the typedef name is indeed myFuncDef

myFuncDef functionFactory(int n) {
    printf("Got parameter %d", n);
    myFuncDef functionPtr = &addInt;
    return functionPtr;
}

19
Harika bilgi için teşekkürler. İşlev işaretleyicilerinin nerede kullanıldığı veya özellikle yararlı olduğu konusunda bir fikir verebilir misiniz?
Rich.Carpenter

326
"functionPtr = & addInt;" ayrıca "functionPtr = addInt;" standart da bu bağlamdaki bir işlev adının işlevin adresine dönüştürüldüğünü söylediği için de geçerlidir.
hlovdal

22
hlovdal, bu bağlamda birisinin fonksiyon yazmasını sağlayan şeyin bu olduğunu açıklamak ilginçtirPtr = ****************** addInt;
Johannes Schaub - litb

105
@ Rich.Carpenter Bunun 4 yıl çok geç olduğunu biliyorum, ama diğer insanların bundan fayda sağlayabileceğini düşünüyorum: İşlev işaretçileri işlevleri parametre olarak diğer işlevlere geçirmek için kullanışlıdır . Tuhaf bir nedenden dolayı bu cevabı bulmak beni çok fazla araştırdı. Temel olarak, C sözde birinci sınıf işlevsellik verir.
giant91

22
@ Rich.Carpenter: fonksiyon işaretçileri çalışma zamanı CPU tespiti için iyidir. SSE, popcnt, AVX, vb. Avantajlarından yararlanmak için bazı işlevlerin birden fazla sürümüne sahip olun. Başlangıçta, işlev işaretçilerinizi geçerli CPU için her işlevin en iyi sürümüne ayarlayın. Diğer kodunuzda, her yerde CPU özelliklerinde koşullu dallara sahip olmak yerine işlev işaretçisini arayın. Daha sonra, bu CPU desteklese bile pshufb, yavaş karar vermesi konusunda karmaşık bir mantık yapabilirsiniz , bu nedenle önceki uygulama hala daha hızlıdır. x264 / x265 bunu yoğun olarak kullanır ve açık kaynak kodludur.
Peter Cordes

304

C'deki fonksiyon göstergeleri C'de nesne yönelimli programlama yapmak için kullanılabilir.

Örneğin, aşağıdaki satırlar C ile yazılır:

String s1 = newString();
s1->set(s1, "hello");

Evet, ->bir newoperatörün eksikliği ve yokluğu ölü bir verimdir, ancak elbette bir Stringsınıfın metnini olacak şekilde ayarladığımızı ima ediyor gibi görünüyor "hello".

İşlev işaretçileri kullanarak , C'deki yöntemleri taklit etmek mümkündür .

Bu nasıl başarılır?

StringSınıf aslında bir olduğunu structbenzetmek yöntemlere için bir yol olarak hareket işlev işaretçileri bir grup ile. StringSınıfın kısmi bir açıklaması aşağıdadır :

typedef struct String_Struct* String;

struct String_Struct
{
    char* (*get)(const void* self);
    void (*set)(const void* self, char* value);
    int (*length)(const void* self);
};

char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);

String newString();

Görülebileceği gibi, Stringsınıfın yöntemleri aslında bildirilen işleve işlev işaretçileridir. Örneğini hazırlarken String, newStringfonksiyon kendi fonksiyonları için fonksiyon işaretçileri kurmak amacıyla denir:

String newString()
{
    String self = (String)malloc(sizeof(struct String_Struct));

    self->get = &getString;
    self->set = &setString;
    self->length = &lengthString;

    self->set(self, "");

    return self;
}

Örneğin getString, getyöntem çağrıldığında çağrılan işlev aşağıdaki gibi tanımlanır:

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}

Fark edilebilecek bir şey, bir nesnenin bir örneğinin ve aslında bir nesnenin parçası olan yöntemlere sahip olmanın hiçbir konsepti olmadığıdır, bu nedenle her çağrımda bir "kendi nesnesi" iletilmelidir. (Ve daha önce kod listesinden çıkarılmış olan internalgizli bir structşeydir - bilgi gizleme gerçekleştirmenin bir yoludur, ancak bu işlev işaretçileriyle ilgili değildir.)

Dolayısıyla, s1->set("hello");eylemi yerine getirmek için, nesnenin içinden geçmek gerekir s1->set(s1, "hello").

Bu küçük açıklamanın kendinize bir referans göndermesi gerektiğinde , C'deki miras olan bir sonraki bölüme geçeceğiz .

Diyelim ki bir alt sınıf yapmak istiyoruz String, diyelim ki ImmutableString. Dize değişmez hale getirmek için, seterişim korurken yöntem, erişilemez getve lengthve kabul etmek için "yapıcı" zorlamak char*:

typedef struct ImmutableString_Struct* ImmutableString;

struct ImmutableString_Struct
{
    String base;

    char* (*get)(const void* self);
    int (*length)(const void* self);
};

ImmutableString newImmutableString(const char* value);

Temel olarak, tüm alt sınıflar için, mevcut yöntemler bir kez daha fonksiyon işaretçileridir. Bu kez, setyöntemin bildirimi mevcut değildir, bu nedenle a ImmutableString.

Uygulanmasına gelince ImmutableString, ilgili tek kod "yapıcı" fonksiyonudur newImmutableString:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = self->base->length;

    self->base->set(self->base, (char*)value);

    return self;
}

ImmutableStringİşlevinin somutlaştırılmasında işlev, getve lengthyöntemlerine işaret eder String.getve dahili olarak saklanan bir nesne olan değişkenden String.lengthgeçerek ve yöntemine başvurur .baseString

Bir fonksiyon işaretçisinin kullanılması, bir yöntemin bir üst sınıftan miras alınmasını sağlayabilir.

C de polimorfizme devam edebiliriz .

Örneğin , herhangi bir nedenle sınıfta her zaman lengthgeri dönmek için yöntemin davranışını değiştirmek istersek , yapılması gereken tek şey:0ImmutableString

  1. Geçersiz kılma lengthyöntemi olarak işlev görecek bir işlev ekleyin .
  2. "Kurucu" ya gidin ve işlev işaretçisini geçersiz kılma lengthyöntemine ayarlayın.

Üzerine bir geçersiz kılma lengthyöntemi eklemek ImmutableStringaşağıdakiler eklenerek yapılabilir lengthOverrideMethod:

int lengthOverrideMethod(const void* self)
{
    return 0;
}

Ardından, lengthyapıcıdaki yöntemin işlev işaretçisi aşağıdakilere bağlanır lengthOverrideMethod:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = &lengthOverrideMethod;

    self->base->set(self->base, (char*)value);

    return self;
}

Şimdi, sınıftaki lengthyöntem için ImmutableStringsınıf olarak özdeş bir davranışa sahip olmak yerine String, şimdi lengthyöntem lengthOverrideMethodişlevde tanımlanan davranışa atıfta bulunacaktır .

Hala bir nesne yönelimli programlama stiliyle nasıl yazacağımı öğrendiğime dair bir feragatname eklemeliyim, bu yüzden muhtemelen iyi açıklayamadığım noktalar var ya da OOP'u en iyi nasıl uygulayabileceğim konusunda işaretsiz olabilir Ama amacım fonksiyon göstergelerinin birçok kullanımından birini göstermeye çalışmaktı.

C'de nesne yönelimli programlamanın nasıl yapılacağı hakkında daha fazla bilgi için lütfen aşağıdaki sorulara bakın:


22
Bu cevap korkunç! Sadece OO'nun bir şekilde nokta gösterimine bağlı olduğu anlamına gelmez, aynı zamanda nesnelerinize önemsiz şeyler koymayı da teşvik eder!
Alexei Averchenko

27
Bu iyi OO, ama C tarzı OO yakınında hiçbir yerde değil. Kırık uyguladığınız şey Javascript tarzı prototip tabanlı OO. C ++ / Pascal tarzı OO elde etmek için yapmanız gerekenler: 1. Sanal üyelere sahip her sınıfın sanal tablosu için bir const yapınız olmalıdır . 2. Polimorfik nesnelerde bu yapıya işaret edin. 3. Sanal yöntemleri sanal tablo aracılığıyla ve diğer tüm yöntemleri doğrudan çağırın - genellikle bazı ClassName_methodNameişlev adlandırma kurallarına bağlı kalarak. Ancak o zaman C ++ ve Pascal ile aynı çalışma zamanı ve depolama maliyetlerini elde edersiniz.
Monica

19
OO'nun OO olması amaçlanmayan bir dilde çalışmak her zaman kötü bir fikirdir. OO istiyorsanız ve hala C sadece C ++ ile çalışmak.
rbaleksandar

20
@rbaleksandar Bunu Linux çekirdek geliştiricilerine anlatın. "Her zaman kötü bir fikir" kesinlikle katılmıyorum, kesinlikle katılmıyorum.
Jonathon Reinhart

6
Bu yanıtı beğendim ama malloc kullanmıyorum
kedi

227

İşten çıkarılma kılavuzu: Kodunuzu elle derleyerek x86 makinelerinde GCC'deki işlev işaretleyicilerini kötüye kullanma:

Bu dize değişmezleri, 32 bit x86 makine kodunun baytlarıdır. 0xC3olan bir x86 rettalimat .

Normalde bunları elle yazmazsınız, montaj dilinde yazarsınız ve daha sonra nasmbir C dizgi hazır bilgisine çevirdiğiniz düz bir ikili dosyaya monte etmek gibi bir montajcı kullanırsınız .

  1. EAX kaydındaki geçerli değeri döndürür

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. Takas işlevi yazma

    int a = 10, b = 20;
    ((void(*)(int*,int*))"\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b")(&a,&b);
    
  3. Her seferinde bir işlev çağırarak 1000'e bir for-loop sayacı yazın

    ((int(*)())"\x66\x31\xc0\x8b\x5c\x24\x04\x66\x40\x50\xff\xd3\x58\x66\x3d\xe8\x03\x75\xf4\xc3")(&function); // calls function with 1->1000
  4. 100'e kadar yinelenen bir işlev bile yazabilirsiniz

    const char* lol = "\x8b\x5c\x24\x4\x3d\xe8\x3\x0\x0\x7e\x2\x31\xc0\x83\xf8\x64\x7d\x6\x40\x53\xff\xd3\x5b\xc3\xc3 <- Recursively calls the function at address lol.";
    i = ((int(*)())(lol))(lol);
    

Derleyicilerin , metin segmentinin bir parçası olarak (işlevler koduyla birlikte) bağlı olan .rodatabölüme (veya .rdataWindows'a) dize değişmezleri yerleştirdiğini unutmayın .

İşlev işaretçileri için dize hazır döküm gerek kalmadan çalışır böylece metin segmenti Oku + Exec iznine sahip mprotect()veya VirtualProtect()dinamik olarak ayrılan bellek için gerekiyordu gibi aramaları sistemi. (Veya gcc -z execstackprogramı hızlı bir hack olarak stack + data segment + yığın yürütülebilir ile bağlar.)


Bunları sökmek için baytlara bir etiket koymak ve bir sökücü kullanmak için bunu derleyebilirsiniz.

// at global scope
const char swap[] = "\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b";

Derleme gcc -c -m32 foo.cve sökme ile objdump -D -rwC -Mintelmontajı alabilir ve bu kodun EBX'i (çağrı korumalı bir kayıt) klonlayarak ABI'yi ihlal ettiğini ve genellikle verimsiz olduğunu öğrenebiliriz.

00000000 <swap>:
   0:   8b 44 24 04             mov    eax,DWORD PTR [esp+0x4]   # load int *a arg from the stack
   4:   8b 5c 24 08             mov    ebx,DWORD PTR [esp+0x8]   # ebx = b
   8:   8b 00                   mov    eax,DWORD PTR [eax]       # dereference: eax = *a
   a:   8b 1b                   mov    ebx,DWORD PTR [ebx]
   c:   31 c3                   xor    ebx,eax                # pointless xor-swap
   e:   31 d8                   xor    eax,ebx                # instead of just storing with opposite registers
  10:   31 c3                   xor    ebx,eax
  12:   8b 4c 24 04             mov    ecx,DWORD PTR [esp+0x4]  # reload a from the stack
  16:   89 01                   mov    DWORD PTR [ecx],eax     # store to *a
  18:   8b 4c 24 08             mov    ecx,DWORD PTR [esp+0x8]
  1c:   89 19                   mov    DWORD PTR [ecx],ebx
  1e:   c3                      ret    

  not shown: the later bytes are ASCII text documentation
  they're not executed by the CPU because the ret instruction sends execution back to the caller

Bu makine kodu (muhtemelen) Windows, Linux, OS X ve benzeri sistemlerde 32 bit kodda çalışacaktır: tüm bu işletim sistemlerindeki varsayılan arama kuralları, kayıtlarda daha verimli bir şekilde değil, yığınları yığın üzerinde geçirir. Ancak EBX, tüm normal çağrı kurallarında çağrı korumalıdır, bu yüzden kaydetmeden / geri yüklemeden bir sıfırlama kaydı olarak kullanmak, arayanın kolayca çökmesini sağlayabilir.


8
Not: C dizeleri normalde yürütülebilir olarak işaretlenmediği için Veri Yürütme Engellemesi etkinleştirilmişse (örn. Windows XP SP2 + 'da) bu çalışmaz.
SecurityMatt

5
Merhaba Matt! Optimizasyon seviyesine bağlı olarak, GCC genellikle dizgi sabitlerini TEXT segmentine yerleştirir, bu nedenle bu tür bir optimizasyona izin vermemeniz koşuluyla pencerelerin daha yeni sürümlerinde bile çalışır. (IIRC, iki yıl önce yayınım sırasındaki MINGW sürümü, varsayılan optimizasyon düzeyinde dize değişmezlerini sıralar)
Lee

10
Birisi burada neler olduğunu açıklayabilir mi? Bu garip görünümlü dizgi değişmezleri neler?
ajay

56
@ajay Görünüşe göre ham onaltılık değerleri yazıyor (örneğin, '\ x00' '/ 0' ile aynı, ikisi de 0'a eşit) bir dizeye, sonra dizeyi bir C işlev işaretçisine çeviriyor ve C işlevi işaretçisi çünkü o şeytan.
ejk314

3
merhaba FUZxxl, ben derleyici ve işletim sistemi sürümüne göre değişebilir düşünüyorum. Yukarıdaki kod codepad.org üzerinde iyi çalışıyor gibi görünüyor; codepad.org/FMSDQ3ME
Lee

115

İşlev işaretçileri için en sevdiğim kullanımlardan biri ucuz ve kolay yineleyiciler -

#include <stdio.h>
#define MAX_COLORS  256

typedef struct {
    char* name;
    int red;
    int green;
    int blue;
} Color;

Color Colors[MAX_COLORS];


void eachColor (void (*fp)(Color *c)) {
    int i;
    for (i=0; i<MAX_COLORS; i++)
        (*fp)(&Colors[i]);
}

void printColor(Color* c) {
    if (c->name)
        printf("%s = %i,%i,%i\n", c->name, c->red, c->green, c->blue);
}

int main() {
    Colors[0].name="red";
    Colors[0].red=255;
    Colors[1].name="blue";
    Colors[1].blue=255;
    Colors[2].name="black";

    eachColor(printColor);
}

7
Yinelemelerden herhangi bir çıktıyı bir şekilde çıkarmak istiyorsanız, kullanıcı tarafından belirtilen verilere bir işaretçi iletmelisiniz (kapanışları düşünün).
Alexei Averchenko

1
Kabul. Benim Yineleyicilerin Tüm şuna benzer: int (*cb)(void *arg, ...). Yineleyicinin dönüş değeri de erken durmamı sağlar (sıfırdan farklıysa).
Jonathon Reinhart

24

Temel tanımlayıcılara sahip olduğunuzda işlev işaretleyicilerinin bildirilmesi kolaylaşır:

  • id: ID: İD a,
  • İşaretçi: *D: D işaretçi
  • Fonksiyon: D(<parameters>): D fonksiyon alma <parametreleri >dönen

D ise aynı kurallar kullanılarak yapılmış başka bir bildiricidir. Sonunda, bir yerlerde, IDbeyan edilen varlığın adı olan (bir örnek için aşağıya bakın) ile biter . Hiçbir şey almayan ve int döndüren bir işleve bir işaretçi alarak ve bir işaret ve int döndüren bir işleve bir işaretçi döndürerek bir işlev oluşturmaya çalışalım. Type-defs ile böyle

typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);

Gördüğünüz gibi, typedefs kullanarak bunu oluşturmak oldukça kolay. Typedefs olmadan, tutarlı bir şekilde uygulanan yukarıdaki beyan kuralları ile zor değildir. Gördüğünüz gibi işaretçinin işaret ettiği bölümü ve işlevin döndürdüğü şeyi kaçırdım. Deklarasyonun en solunda görünen şey budur ve ilgi çekici değildir: Birisi zaten beyancıyı oluşturduysa eklenir. Hadi bunu yapalım. Tutarlı bir şekilde oluşturmak, ilk kelime - [ve kullanarak yapıyı göstermek ]:

function taking 
    [pointer to [function taking [void] returning [int]]] 
returning
    [pointer to [function taking [char] returning [int]]]

Gördüğünüz gibi, biri birbiri ardına bildiriciler ekleyerek bir türü tamamen tanımlayabilir. İnşaat iki şekilde yapılabilir. Biri aşağıdan yukarıya, en doğru şeyle (yapraklar) başlayıp tanımlayıcıya kadar ilerliyor. Diğer yol, tanımlayıcıdan başlayarak, yapraklara kadar aşağı doğru çalışan yukarıdan aşağıyadır. Her iki yolu da göstereceğim.

Altüst

İnşaat sağdaki şeyle başlar: Döndürülen şey, char alan işlevdir. Beyanları ayrı tutmak için, onları numaralandıracağım:

D1(char);

Önemsiz olduğundan char parametresini doğrudan ekledi. Değiştirerek Bildiricisi için bir işaretçi ekleme D1ile *D2. Parantezleri etrafa sarmamız gerektiğini unutmayın *D2. Bu, *-operatorve fonksiyon çağrısı operatörünün önceliğine bakarak bilinebilir (). Parantezlerimiz olmasaydı, derleyici bunu okurdu *(D2(char p)). Ancak bu *D2, elbette artık D1'in basit bir yerine geçmeyecekti . Bildirgelerin çevresinde parantezlere her zaman izin verilir. Yani aslında çok fazla eklerseniz yanlış bir şey yapmazsınız.

(*D2)(char);

Dönüş türü tamamlandı! Şimdi, geriD2 dönüşü alan işlev bildirim işleviyle<parameters> değiştirelim , ki D3(<parameters>)şu anda bulunduğumuz durum.

(*D3(<parameters>))(char)

Bu sefer bir işaretçi değil, bir işlev bildirici olmak istediğimiz D3 için parantez gerekmediğine dikkat edin . Harika, tek şey bunun için parametreler. Parametre, dönüş türünü yaptığımızla tamamen aynıdır, sadece ile chardeğiştirilir void. Bu yüzden kopyalayacağım:

(*D3(   (*ID1)(void)))(char)

Ben yerini ettik D2tarafından ID1(- Başka Bildiricisi gerek zaten bir işleve bir işaretçi var) o parametreyle bittiğinde beri. ID1parametrenin adı olacaktır. Şimdi, sonunda sonunda tüm bu bildirgenin değiştirdiği türü ekler - her bildirimin en solunda görünen bir ekler. Fonksiyonlar için, dönüş türü haline gelir. Vb işaret sivri işaretçiler için ... Bu tür yazarken ilginç, en sağda ters sırayla görünecektir :) Neyse, onun yerine tam beyan verir. intElbette iki kere .

int (*ID0(int (*ID1)(void)))(char)

ID0Bu örnekte işlevin tanımlayıcısını çağırdım .

Yukarıdan aşağıya

Bu, türün açıklamasının en solundaki tanımlayıcıdan başlar ve sağa doğru yürürken bu bildiriciyi sarar. Parametreleri döndüren fonksiyon ile başlayın<>

ID0(<parameters>)

Açıklamasındaki bir sonraki şey ("geri döndükten sonra") göstericiydi . Bunu dahil edelim:

*ID0(<parameters>)

Sonra bir sonraki şey parametreler geri dönen functon<> oldu . Parametre basit bir karakterdir, bu yüzden hemen tekrar koyarız, çünkü gerçekten önemsizdir.

(*ID0(<parameters>))(char)

Biz bu olaya istiyoruz çünkü biz ekledi parantez Not *ilk bağlamalar ve ardından(char) . Aksi takdirde okurdu alarak işlevi <parametreleri >... fonksiyonunu dönen . Gürültü, işlev döndüren işlevlere izin verilmez.

Şimdi sadece <parametreler koymamız gerekiyor >. Şimdi türetmenin kısa bir versiyonunu göstereceğim, çünkü şimdiye kadar bunu nasıl yapacağınız hakkında bir fikriniz olduğunu düşünüyorum.

pointer to: *ID1
... function taking void returning: (*ID1)(void)

Sadece intaşağıdan yukarıya doğru yaptığımız gibi deklaratörlerin önüne koy ve bitirdik

int (*ID0(int (*ID1)(void)))(char)

Güzel şey

Aşağıdan yukarıya veya yukarıdan aşağıya daha iyi mi? Aşağıdan yukarıya alışkınım, ancak bazı insanlar yukarıdan aşağıya daha rahat olabilir. Bence bu bir zevk meselesi. Bu arada, bu bildirideki tüm operatörleri uygularsanız, bir int elde edersiniz:

int v = (*ID0(some_function_pointer))(some_char);

Bu, C'deki bildirimlerin güzel bir özelliğidir: Bildirim, bu işleçlerin tanımlayıcıyı kullanarak bir ifadede kullanılması durumunda, en soldaki türü verdiğini varsayar. Diziler için de böyle.

Umarım bu küçük öğreticiyi beğenmişsinizdir! Şimdi insanlar işlevlerin tuhaf beyan sözdizimini merak ettiğinde buna bağlanabiliriz. Mümkün olduğunca az C iç parçası koymaya çalıştım. İçindeki şeyleri düzenlemek / düzeltmek için çekinmeyin.


24

İşlev işaretçileri için başka bir iyi kullanım:
Ağrısız sürümler arasında geçiş yapma

Farklı zamanlarda farklı işlevler veya farklı gelişim aşamaları istediğinizde kullanmak için çok kullanışlıdırlar. Örneğin, bir konsolu olan bir ana bilgisayarda bir uygulama geliştiriyorum, ancak yazılımın son sürümü bir Avnet ZedBoard'a konacak (ekranlar ve konsollar için bağlantı noktaları var, ancak bunlar için gerekli değil / aranıyor son sürüm). Bu yüzden geliştirme sırasında printfdurum ve hata mesajlarını görüntülemek için kullanacağım , ancak işim bittiğinde hiçbir şeyin yazdırılmasını istemiyorum. İşte yaptığım şey:

version.h

// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION


// Define which version we want to use
#define DEBUG_VERSION       // The current version
// #define RELEASE_VERSION  // To be uncommented when finished debugging

#ifndef __VERSION_H_      /* prevent circular inclusions */
    #define __VERSION_H_  /* by using protection macros */
    void board_init();
    void noprintf(const char *c, ...); // mimic the printf prototype
#endif

// Mimics the printf function prototype. This is what I'll actually 
// use to print stuff to the screen
void (* zprintf)(const char*, ...); 

// If debug version, use printf
#ifdef DEBUG_VERSION
    #include <stdio.h>
#endif

// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

#ifdef INVALID_VERSION
    // Won't allow compilation without a valid version define
    #error "Invalid version definition"
#endif

Gelen version.cI2 fonksiyonu tanımlayacak Mevcut in prototipversion.h

version.c

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

Fonksiyon işaretçisi içinde prototyped nasıl Bildirimi version.holarak

void (* zprintf)(const char *, ...);

Uygulamada referans alındığında, henüz tanımlanmamış olan herhangi bir yere işaret etmeye başlar.

Gelen version.c, içinde bildirim board_init()fonksiyonu zprintftanımlanır sürümüne bağlı (işlev imza maçları) benzersiz bir işlevi atanırversion.h

zprintf = &printf; zprintf hata ayıklama amacıyla printf'i çağırır

veya

zprintf = &noprint; zprintf sadece döner ve gereksiz kod çalıştırmaz

Kodu çalıştırmak şöyle görünecektir:

mainProg.c

#include "version.h"
#include <stdlib.h>
int main()
{
    // Must run board_init(), which assigns the function
    // pointer to an actual function
    board_init();

    void *ptr = malloc(100); // Allocate 100 bytes of memory
    // malloc returns NULL if unable to allocate the memory.

    if (ptr == NULL)
    {
        zprintf("Unable to allocate memory\n");
        return 1;
    }

    // Other things to do...
    return 0;
}

Yukarıdaki kod printfhata ayıklama modunda kullanılırsa veya serbest bırakma modundaysa hiçbir şey yapmaz. Bu, tüm projeyi gözden geçirip kodu yorumlamaktan veya silmekten çok daha kolaydır. Tek yapmam gereken sürümü değiştirmek version.hve kod gerisini yapacak!


4
U çok fazla performans süresi kaybetmeye stand. Bunun yerine, Debug / Release tabanlı bir kod bölümünü etkinleştiren ve devre dışı bırakan bir makro kullanabilirsiniz.
AlphaGoku

19

İşlev işaretçisi genellikle tarafından tanımlanır typedefve param & return değeri olarak kullanılır.

Yukarıda verilen cevaplar çoktan açıklanmıştı, sadece tam bir örnek verdim:

#include <stdio.h>

#define NUM_A 1
#define NUM_B 2

// define a function pointer type
typedef int (*two_num_operation)(int, int);

// an actual standalone function
static int sum(int a, int b) {
    return a + b;
}

// use function pointer as param,
static int sum_via_pointer(int a, int b, two_num_operation funp) {
    return (*funp)(a, b);
}

// use function pointer as return value,
static two_num_operation get_sum_fun() {
    return &sum;
}

// test - use function pointer as variable,
void test_pointer_as_variable() {
    // create a pointer to function,
    two_num_operation sum_p = &sum;
    // call function via pointer
    printf("pointer as variable:\t %d + %d = %d\n", NUM_A, NUM_B, (*sum_p)(NUM_A, NUM_B));
}

// test - use function pointer as param,
void test_pointer_as_param() {
    printf("pointer as param:\t %d + %d = %d\n", NUM_A, NUM_B, sum_via_pointer(NUM_A, NUM_B, &sum));
}

// test - use function pointer as return value,
void test_pointer_as_return_value() {
    printf("pointer as return value:\t %d + %d = %d\n", NUM_A, NUM_B, (*get_sum_fun())(NUM_A, NUM_B));
}

int main() {
    test_pointer_as_variable();
    test_pointer_as_param();
    test_pointer_as_return_value();

    return 0;
}

14

C'deki işlev işaretçileri için büyük kullanımlardan biri, çalışma zamanında seçilen bir işlevi çağırmaktır. Örneğin, C çalışma zamanı kitaplığında iki yordam vardır qsortve bsearchbunlar sıralanan iki öğeyi karşılaştırmak için çağrılan bir işleve işaret eder; bu, kullanmak istediğiniz ölçütlere göre sırasıyla herhangi bir şeyi sıralamanıza veya aramanıza olanak tanır.

Çok temel bir örnek, bir işlevi print(int x, int y)çağırmayı gerektirebilecek bir işlev varsa (ya add()da sub(), aynı türden) o zaman ne yapacağız, print()aşağıda gösterildiği gibi işleve bir işlev işaretçisi argümanı ekleyeceğiz :

#include <stdio.h>

int add()
{
   return (100+10);
}

int sub()
{
   return (100-10);
}

void print(int x, int y, int (*func)())
{
    printf("value is: %d\n", (x+y+(*func)()));
}

int main()
{
    int x=100, y=200;
    print(x,y,add);
    print(x,y,sub);

    return 0;
}

Çıktı:

değer: 410
değer: 390


10

Sıfırdan başlayarak çalıştırmaya başladıkları yerden bazı bellek adresleri vardır. Montaj Dilinde ("fonksiyonun bellek adresi" olarak adlandırılır) olarak adlandırılır.

1.İlk olarak işlev için bir işaretçi bildirmeniz gerekir 2.İstediğiniz işlevin Adresini girin

**** Not-> fonksiyonlar aynı tipte olmalıdır ****

Bu Basit Program Her Şeyi Gösterecektir.

#include<stdio.h>
void (*print)() ;//Declare a  Function Pointers
void sayhello();//Declare The Function Whose Address is to be passed
                //The Functions should Be of Same Type
int main()
{
 print=sayhello;//Addressof sayhello is assigned to print
 print();//print Does A call To The Function 
 return 0;
}

void sayhello()
{
 printf("\n Hello World");
}

resim açıklamasını buraya girinBundan sonra, makinenin onları nasıl anladığını görelim. 32 bit mimaride yukarıdaki programın makine talimatını görebilirsiniz.

Kırmızı işaret alanı, adresin nasıl değiştirildiğini ve eax'ta nasıl saklandığını gösteriyor. O zaman onların eax bir çağrı talimatı. eax, işlevin istenen adresini içerir.


8

İşlev işaretçisi, işlevin adresini içeren bir değişkendir. Bazı sınırlı özelliklere sahip bir işaretçi değişkeni olduğu için, veri yapılarında diğer işaretçi değişkenlerinde olduğu gibi kullanabilirsiniz.

Düşünebildiğim tek istisna, işlev işaretçisini tek bir değerden başka bir şeye işaret ediyormuş gibi davranmaktır. Bir işlev işaretçisini arttırarak veya azaltarak veya işlev işaretçisine bir ofset ekleyerek / çıkararak işaretçi aritmetiği yapmak, işlev işaretçisi yalnızca tek bir şeyi, bir işlevin giriş noktasını işaret ettiği için gerçekten herhangi bir yardımcı program değildir.

Bir işlev işaretçisi değişkeninin boyutu, değişken tarafından işgal edilen bayt sayısı, temeldeki mimariye, örneğin x32 veya x64'e veya başka bir şeye bağlı olarak değişebilir.

Bir işlev işaretçisi değişkeni için bildirim, C derleyicisinin normalde yaptığı denetim türlerini yapabilmesi için işlev bildirimi ile aynı tür bilgileri belirtmesi gerekir. İşlev işaretçisinin bildiriminde / tanımında bir parametre listesi belirtmezseniz, C derleyicisi parametrelerin kullanımını kontrol edemez. Bu kontrol eksikliğinin yararlı olabileceği durumlar vardır, ancak bir güvenlik ağının kaldırıldığını unutmayın.

Bazı örnekler:

int func (int a, char *pStr);    // declares a function

int (*pFunc)(int a, char *pStr);  // declares or defines a function pointer

int (*pFunc2) ();                 // declares or defines a function pointer, no parameter list specified.

int (*pFunc3) (void);             // declares or defines a function pointer, no arguments.

İlk iki beyanda biraz benzer:

  • funca intve a alan char *veint
  • pFunc, a intve a alan bir işlevi char *döndüren veint

Dolayısıyla yukarıdakilerden, fonksiyonun adresinin func()fonksiyon işaretçisi değişkenine pFuncolduğu gibi atandığı bir kaynak hattına sahip olabiliriz pFunc = func;.

Doğal işleç öncelik kurallarının üstesinden gelmek için parantezin kullanıldığı bir işlev işaretçisi bildirimi / tanımıyla kullanılan sözdizimine dikkat edin.

int *pfunc(int a, char *pStr);    // declares a function that returns int pointer
int (*pFunc)(int a, char *pStr);  // declares a function pointer that returns an int

Birkaç Farklı Kullanım Örneği

İşlev işaretçisinin kullanımına bazı örnekler:

int (*pFunc) (int a, char *pStr);    // declare a simple function pointer variable
int (*pFunc[55])(int a, char *pStr); // declare an array of 55 function pointers
int (**pFunc)(int a, char *pStr);    // declare a pointer to a function pointer variable
struct {                             // declare a struct that contains a function pointer
    int x22;
    int (*pFunc)(int a, char *pStr);
} thing = {0, func};                 // assign values to the struct variable
char * xF (int x, int (*p)(int a, char *pStr));  // declare a function that has a function pointer as an argument
char * (*pxF) (int x, int (*p)(int a, char *pStr));  // declare a function pointer that points to a function that has a function pointer as an argument

İşlev işaretçisi tanımında değişken uzunluklu parametre listelerini kullanabilirsiniz.

int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);

Veya bir parametre listesi belirtemezsiniz. Bu yararlı olabilir, ancak C derleyicisinin sağlanan argüman listesinde kontrol yapma fırsatını ortadan kaldırır.

int  sum ();      // nothing specified in the argument list so could be anything or nothing
int (*psum)();
int  sum2(void);  // void specified in the argument list so no parameters when calling this function
int (*psum2)(void);

C tarzı oyuncular

İşlev işaretçileri olan C stili dökümleri kullanabilirsiniz. Bununla birlikte, bir C derleyicisinin hatalar yerine denetimler konusunda gevşek olabileceğini veya uyarı verebileceğini unutmayın.

int sum (int a, char *b);
int (*psplsum) (int a, int b);
psplsum = sum;               // generates a compiler warning
psplsum = (int (*)(int a, int b)) sum;   // no compiler warning, cast to function pointer
psplsum = (int *(int a, int b)) sum;     // compiler error of bad cast generated, parenthesis are required.

İşlev İşaretçisini Eşitlikle Karşılaştırma

Bir işlev işaretçisi bir ifdeyim kullanarak belirli bir işlev adresine eşit olup olmadığını kontrol edebilirsiniz, ancak bunun ne kadar yararlı olacağından emin değilim. Diğer karşılaştırma operatörleri daha az faydaya sahip gibi görünmektedir.

static int func1(int a, int b) {
    return a + b;
}

static int func2(int a, int b, char *c) {
    return c[0] + a + b;
}

static int func3(int a, int b, char *x) {
    return a + b;
}

static char *func4(int a, int b, char *c, int (*p)())
{
    if (p == func1) {
        p(a, b);
    }
    else if (p == func2) {
        p(a, b, c);      // warning C4047: '==': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
    } else if (p == func3) {
        p(a, b, c);
    }
    return c;
}

Bir İşlev İşaretçisi Dizisi

Ve her biri bağımsız değişken listesinin farklı olduğu bir işlev işaretçisi dizisine sahip olmak istiyorsanız, bağımsız değişken listesi belirtilmemiş bir işlev işaretçisi tanımlayabilirsiniz ( voidbu hiçbir bağımsız değişken anlamına gelmez, yalnızca belirtilmemiş) C derleyicisinden gelen uyarıları görebilir. Bu, bir işleve bir işlev işaretçisi parametresi için de çalışır:

int(*p[])() = {       // an array of function pointers
    func1, func2, func3
};
int(**pp)();          // a pointer to a function pointer


p[0](a, b);
p[1](a, b, 0);
p[2](a, b);      // oops, left off the last argument but it compiles anyway.

func4(a, b, 0, func1);
func4(a, b, 0, func2);  // warning C4047: 'function': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
func4(a, b, 0, func3);

    // iterate over the array elements using an array index
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++) {
    func4(a, b, 0, p[i]);
}
    // iterate over the array elements using a pointer
for (pp = p; pp < p + sizeof(p)/sizeof(p[0]); pp++) {
    (*pp)(a, b, 0);          // pointer to a function pointer so must dereference it.
    func4(a, b, 0, *pp);     // pointer to a function pointer so must dereference it.
}

C stili namespaceGlobal'i structİşlev İşaretçileriyle Kullanma

Sen kullanabilirsiniz staticAdını dosya kapsamı ve daha sonra benzer bir şey sağlayan bir yolu olarak küresel bir değişkene atar bir işlev belirtmek için anahtar kelime namespaceC ++ işlevselliği.

Bir başlık dosyasında, onu kullanan global bir değişkenle birlikte ad alanımız olacak bir yapı tanımlayın.

typedef struct {
   int (*func1) (int a, int b);             // pointer to function that returns an int
   char *(*func2) (int a, int b, char *c);  // pointer to function that returns a pointer
} FuncThings;

extern const FuncThings FuncThingsGlobal;

Sonra C kaynak dosyasında:

#include "header.h"

// the function names used with these static functions do not need to be the
// same as the struct member names. It's just helpful if they are when trying
// to search for them.
// the static keyword ensures these names are file scope only and not visible
// outside of the file.
static int func1 (int a, int b)
{
    return a + b;
}

static char *func2 (int a, int b, char *c)
{
    c[0] = a % 100; c[1] = b % 50;
    return c;
}

const FuncThings FuncThingsGlobal = {func1, func2};

Bu daha sonra, işleve erişmek için genel yapı değişkeninin tam adını ve üye adını belirterek kullanılır. constDeğiştirici o kazara değiştirilemez küresel vb kullanılır.

int abcd = FuncThingsGlobal.func1 (a, b);

Fonksiyon İşaretleyicilerinin Uygulama Alanları

Bir DLL kitaplığı bileşeni, içeren bir işlev işaretçisi namespaceoluşturmayı destekleyen bir kitaplık arabiriminde fabrika yönteminden belirli bir kitaplık arabiriminin istendiği C stili yaklaşımına benzer bir şey yapabilir struct. Bu kitaplık arabirimi, istenen DLL sürümünü yükler, gerekli işlev işaretçileri olan bir yapı oluşturur ve ardından, yapıyı kullanım için istekte bulunan arayana döndürür.

typedef struct {
    HMODULE  hModule;
    int (*Func1)();
    int (*Func2)();
    int(*Func3)(int a, int b);
} LibraryFuncStruct;

int  LoadLibraryFunc LPCTSTR  dllFileName, LibraryFuncStruct *pStruct)
{
    int  retStatus = 0;   // default is an error detected

    pStruct->hModule = LoadLibrary (dllFileName);
    if (pStruct->hModule) {
        pStruct->Func1 = (int (*)()) GetProcAddress (pStruct->hModule, "Func1");
        pStruct->Func2 = (int (*)()) GetProcAddress (pStruct->hModule, "Func2");
        pStruct->Func3 = (int (*)(int a, int b)) GetProcAddress(pStruct->hModule, "Func3");
        retStatus = 1;
    }

    return retStatus;
}

void FreeLibraryFunc (LibraryFuncStruct *pStruct)
{
    if (pStruct->hModule) FreeLibrary (pStruct->hModule);
    pStruct->hModule = 0;
}

ve bu şu şekilde kullanılabilir:

LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
//  ....
myLib.Func1();
//  ....
FreeLibraryFunc (&myLib);

Aynı yaklaşım, temel donanımın belirli bir modelini kullanan kod için soyut bir donanım katmanı tanımlamak için kullanılabilir. İşlev işaretçileri, soyut donanım modelinde belirtilen işlevleri uygulayan donanıma özgü işlevleri sağlamak için fabrika tarafından donanıma özgü işlevlerle doldurulur. Bu, belirli bir donanım işlev arabirimini elde etmek için fabrika işlevini çağıran ve daha sonra belirli bir hedefle ilgili uygulama ayrıntılarını bilmeye gerek kalmadan temel donanım için eylemleri gerçekleştirmek üzere sağlanan işlev işaretçileri kullanan yazılım tarafından kullanılan soyut bir donanım katmanı sağlamak için kullanılabilir. .

Temsilci, İşleyici ve Geri Arama oluşturmak için İşaretçiler İşlev

İşlev işaretçileri, bazı görevleri veya işlevleri temsil etmenin bir yolu olarak kullanabilirsiniz. C'deki klasik örnek, Standart C kitaplığı işlevleriyle kullanılan qsort()ve bsearch()bir öğe listesini sıralamak veya sıralı bir öğe listesi üzerinde ikili arama yapmak için harmanlama sırası sağlamak için kullanılan karşılaştırma temsilcisi işlev işaretçisidir . Karşılaştırma işlevi temsilcisi, sıralamada veya ikili aramada kullanılan harmanlama algoritmasını belirtir.

Başka bir kullanım, bir C ++ Standart Şablon Kitaplığı kapsayıcısına algoritma uygulanmasına benzer.

void * ApplyAlgorithm (void *pArray, size_t sizeItem, size_t nItems, int (*p)(void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for ( ; pList < pListEnd; pList += sizeItem) {
        p (pList);
    }

    return pArray;
}

int pIncrement(int *pI) {
    (*pI)++;

    return 1;
}

void * ApplyFold(void *pArray, size_t sizeItem, size_t nItems, void * pResult, int(*p)(void *, void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for (; pList < pListEnd; pList += sizeItem) {
        p(pList, pResult);
    }

    return pArray;
}

int pSummation(int *pI, int *pSum) {
    (*pSum) += *pI;

    return 1;
}

// source code and then lets use our function.
int intList[30] = { 0 }, iSum = 0;

ApplyAlgorithm(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), pIncrement);
ApplyFold(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), &iSum, pSummation);

Başka bir örnek, olay gerçekleştiğinde gerçekten çağrılan bir işlev işaretçisi sağlayarak belirli bir olay için bir işleyicinin kaydedildiği GUI kaynak kodudur. Microsoft MFC çerçevesi ileti eşlemeleriyle, bir pencereye veya iş parçacığına teslim edilen Windows iletilerini işlemeye benzer bir şey kullanır.

Geri arama gerektiren asenkron işlevler, olay işleyiciye benzer. Eşzamansız işlevin kullanıcısı, bir eylem başlatmak için eşzamansız işlevi çağırır ve eylem tamamlandığında eşzamansız işlevin çağıracağı bir işlev işaretçisi sağlar. Bu durumda olay, görevini tamamlayan eşzamansız işlevdir.


0

İşlev işaretçileri genellikle geri aramalar yazdığından, güvenli tür geri aramalara göz atmak isteyebilirsiniz . Aynı durum geri arama olmayan işlevler vb. Giriş noktaları için de geçerlidir.

C aynı zamanda oldukça kararsız ve affedicidir :)

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.