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.
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.
Yanıtlar:
İşaret edeceğimiz temel bir işlevle başlayalım :
int addInt(int n, int m) {
return n+m;
}
İlk olarak, 2 int
s 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;
}
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.
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 new
operatörün eksikliği ve yokluğu ölü bir verimdir, ancak elbette bir String
sı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?
String
Sınıf aslında bir olduğunu struct
benzetmek yöntemlere için bir yol olarak hareket işlev işaretçileri bir grup ile. String
Sı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, String
sınıfın yöntemleri aslında bildirilen işleve işlev işaretçileridir. Örneğini hazırlarken String
, newString
fonksiyon 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
, get
yö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 internal
gizli 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, set
erişim korurken yöntem, erişilemez get
ve length
ve 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, set
yö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, get
ve length
yöntemlerine işaret eder String.get
ve dahili olarak saklanan bir nesne olan değişkenden String.length
geçerek ve yöntemine başvurur .base
String
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 length
geri dönmek için yöntemin davranışını değiştirmek istersek , yapılması gereken tek şey:0
ImmutableString
length
yöntemi olarak işlev görecek bir işlev ekleyin .length
yöntemine ayarlayın.Üzerine bir geçersiz kılma length
yöntemi eklemek ImmutableString
aşağıdakiler eklenerek yapılabilir lengthOverrideMethod
:
int lengthOverrideMethod(const void* self)
{
return 0;
}
Ardından, length
yapı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 length
yöntem için ImmutableString
sınıf olarak özdeş bir davranışa sahip olmak yerine String
, şimdi length
yöntem lengthOverrideMethod
iş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:
ClassName_methodName
işlev adlandırma kurallarına bağlı kalarak. Ancak o zaman C ++ ve Pascal ile aynı çalışma zamanı ve depolama maliyetlerini elde edersiniz.
İş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. 0xC3
olan bir x86 ret
talimat .
Normalde bunları elle yazmazsınız, montaj dilinde yazarsınız ve daha sonra nasm
bir C dizgi hazır bilgisine çevirdiğiniz düz bir ikili dosyaya monte etmek gibi bir montajcı kullanırsınız .
EAX kaydındaki geçerli değeri döndürür
int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
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);
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
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 .rodata
bölüme (veya .rdata
Windows'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 execstack
programı 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.c
ve sökme ile objdump -D -rwC -Mintel
montajı 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.
İş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);
}
int (*cb)(void *arg, ...)
. Yineleyicinin dönüş değeri de erken durmamı sağlar (sıfırdan farklıysa).
Temel tanımlayıcılara sahip olduğunuzda işlev işaretleyicilerinin bildirilmesi kolaylaşır:
ID
: İD a,*D
: D işaretçiD(<parameters>)
: D fonksiyon alma <
parametreleri >
dönenD ise aynı kurallar kullanılarak yapılmış başka bir bildiricidir. Sonunda, bir yerlerde, ID
beyan 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.
İ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 D1
ile *D2
. Parantezleri etrafa sarmamız gerektiğini unutmayın *D2
. Bu, *-operator
ve 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 char
değiştirilir void
. Bu yüzden kopyalayacağım:
(*D3( (*ID1)(void)))(char)
Ben yerini ettik D2
tarafından ID1
(- Başka Bildiricisi gerek zaten bir işleve bir işaretçi var) o parametreyle bittiğinde beri. ID1
parametrenin 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. int
Elbette iki kere .
int (*ID0(int (*ID1)(void)))(char)
ID0
Bu örnekte işlevin tanımlayıcısını çağırdım .
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 int
aşağıdan yukarıya doğru yaptığımız gibi deklaratörlerin önüne koy ve bitirdik
int (*ID0(int (*ID1)(void)))(char)
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.
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 printf
durum 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:
// 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.c
I2 fonksiyonu tanımlayacak Mevcut in prototipversion.h
#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.h
olarak
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 zprintf
tanı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:
#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 printf
hata 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.h
ve kod gerisini yapacak!
İşlev işaretçisi genellikle tarafından tanımlanır typedef
ve 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 ∑
}
// test - use function pointer as variable,
void test_pointer_as_variable() {
// create a pointer to function,
two_num_operation sum_p = ∑
// 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;
}
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 qsort
ve bsearch
bunlar 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
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");
}
Bundan 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.
İş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:
func
a int
ve a alan char *
veint
pFunc
, a int
ve 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 pFunc
olduğ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 if
deyim 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 ( void
bu 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 namespace
Global'i struct
İşlev İşaretçileriyle Kullanma
Sen kullanabilirsiniz static
Adı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 namespace
C ++ 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. const
Değ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 namespace
oluş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.
İş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 :)