พอยน์เตอร์ของฟังก์ชันใน C ทำงานอย่างไร


1232

ฉันมีประสบการณ์เมื่อเร็ว ๆ นี้กับตัวชี้ฟังก์ชั่นใน C.

ดังนั้นด้วยประเพณีการตอบคำถามของคุณเองฉันจึงตัดสินใจสรุปพื้นฐานเบื้องต้นเล็กน้อยสำหรับผู้ที่ต้องการดำน้ำแบบด่วนในเรื่อง


35
นอกจากนี้ยัง: สำหรับบิตของการวิเคราะห์ในเชิงลึกของตัวชี้ C, ดูblogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge นอกจากนี้การเขียนโปรแกรมจากกราวด์อัพยังแสดงวิธีการทำงานของระดับเครื่อง การทำความเข้าใจ"โมเดลหน่วยความจำ" ของ Cมีประโยชน์มากสำหรับการทำความเข้าใจวิธีการทำงานของพอยน์เตอร์ C
Abbafei

8
ข้อมูลที่ดี ตามชื่อ แต่ผมคาดว่าจะมีจริงๆดูคำอธิบายของวิธีการ "ฟังก์ชั่นการทำงานชี้" ที่ไม่ว่าพวกเขาจะเขียน :)
Bogdan อเล็กซาน

คำตอบ:


1477

พอยน์เตอร์ของฟังก์ชันใน C

เริ่มจากฟังก์ชั่นพื้นฐานที่เราจะชี้ไปที่ :

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

สิ่งแรกที่ให้มีกำหนดตัวชี้ไปยังฟังก์ชั่นที่ได้รับ 2 ที่intและส่งกลับint:

int (*functionPtr)(int,int);

ตอนนี้เราสามารถชี้ไปที่ฟังก์ชั่นของเราได้อย่างปลอดภัย:

functionPtr = &addInt;

ตอนนี้เรามีตัวชี้ไปยังฟังก์ชั่นมาใช้งานกัน:

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

การส่งตัวชี้ไปยังฟังก์ชันอื่นโดยพื้นฐานแล้วจะเหมือนกัน:

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

เราสามารถใช้พอยน์เตอร์ของฟังก์ชั่นในการคืนค่าได้เช่นกัน (พยายามติดตาม, มันยุ่งเหยิง):

// 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;
}

แต่มันก็ดีกว่าการใช้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
ขอบคุณสำหรับข้อมูลที่ดี คุณสามารถเพิ่มข้อมูลเชิงลึกเกี่ยวกับตำแหน่งที่ใช้ตัวชี้ฟังก์ชั่นหรือมีประโยชน์อย่างยิ่งหรือไม่?
Rich.Carpenter

326
"functionPtr = & addInt;" สามารถเขียนได้ (และบ่อยครั้ง) ในฐานะ "functionPtr = addInt;" ซึ่งก็ใช้ได้เช่นกันเนื่องจากมาตรฐานระบุว่าชื่อฟังก์ชันในบริบทนี้ถูกแปลงเป็นที่อยู่ของฟังก์ชัน
hlovdal

22
hlovdal ในบริบทนี้มันน่าสนใจที่จะอธิบายว่านี่คือสิ่งที่ช่วยให้คนหนึ่งที่จะเขียน functionPtr = ******************** addInt;
Johannes Schaub - litb

105
@ Rich.Carpenter ฉันรู้ว่านี้คือ 4 ปีที่ผ่านมาสายเกินไป แต่ฉันคิดของคนอื่น ๆ อาจได้รับประโยชน์จากการนี้: คำแนะนำฟังก์ชั่นที่มีประโยชน์สำหรับการส่งผ่านฟังก์ชั่นเป็นพารามิเตอร์ฟังก์ชั่นอื่นการค้นหาฉันใช้เวลามากเพื่อค้นหาคำตอบนั้นด้วยเหตุผลแปลก ๆ ดังนั้นโดยทั่วไปมันให้ฟังก์ชันการทำงานแบบ C แบบหลอกของ C
giant91

22
@ Rich.Carpenter: ตัวชี้ฟังก์ชันเหมาะสำหรับการตรวจจับ CPU แบบรันไทม์ มีฟังก์ชั่นบางรุ่นหลายรุ่นเพื่อใช้ประโยชน์จาก SSE, popcnt, AVX เป็นต้นเมื่อเริ่มต้นให้ตั้งค่าพอยน์เตอร์ของฟังก์ชันเป็นเวอร์ชันที่ดีที่สุดของแต่ละฟังก์ชันสำหรับ CPU ปัจจุบัน ในรหัสอื่นของคุณเพียงแค่เรียกใช้ตัวชี้ฟังก์ชั่นแทนการมีสาขาแบบมีเงื่อนไขบนคุณสมบัติ CPU ทุกที่ จากนั้นคุณสามารถทำตรรกะที่ซับซ้อนเกี่ยวกับการตัดสินใจได้ดีแม้ว่า CPU นี้จะสนับสนุนpshufbมันช้าดังนั้นการดำเนินการก่อนหน้านี้ยังเร็วกว่า x264 / x265 ใช้สิ่งนี้อย่างกว้างขวางและเป็นโอเพ่นซอร์ส
Peter Cordes

304

ตัวชี้ฟังก์ชั่นใน C สามารถใช้ในการเขียนโปรแกรมเชิงวัตถุใน C

ตัวอย่างเช่นบรรทัดต่อไปนี้เขียนใน C:

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

ใช่->และการขาดการnewประกอบการเป็นผู้ให้ตายแล้วออกไป แต่แน่ใจว่าดูเหมือนจะบ่งบอกว่าเรากำลังตั้งค่าข้อความของบางระดับจะเป็นString"hello"

โดยใช้ตัวชี้ฟังก์ชั่นก็เป็นไปได้ที่จะเลียนแบบวิธีการใน C

สิ่งนี้สำเร็จได้อย่างไร

Stringระดับเป็นจริงstructกับพวงของคำแนะนำการทำงานซึ่งทำหน้าที่เป็นวิธีการวิธีการจำลองแบบ ต่อไปนี้เป็นการประกาศบางส่วนของStringคลาส:

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();

ดังที่เห็นได้ว่าเมธอดของStringคลาสเป็นตัวชี้ฟังก์ชันไปยังฟังก์ชันที่ประกาศ ในการจัดทำอินสแตนซ์ของStringที่newStringฟังก์ชั่นที่เรียกกันในการตั้งค่าตัวชี้ฟังก์ชั่นการทำงานของตน:

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

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

    self->set(self, "");

    return self;
}

ตัวอย่างเช่นgetStringฟังก์ชันที่เรียกใช้โดยการเรียกใช้getเมธอดจะถูกกำหนดดังนี้:

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

สิ่งหนึ่งที่สามารถสังเกตได้คือไม่มีแนวคิดของอินสแตนซ์ของวัตถุและมีวิธีการที่เป็นส่วนหนึ่งของวัตถุดังนั้น "วัตถุตัวเอง" จะต้องผ่านในแต่ละการร้องขอ (และinternalนี่เป็นเพียงการซ่อนstructซึ่งถูกตัดออกจากรายการรหัสก่อนหน้า - เป็นวิธีการซ่อนข้อมูล แต่ไม่เกี่ยวข้องกับตัวชี้ฟังก์ชัน)

ดังนั้นแทนที่จะความสามารถในการทำหนึ่งต้องผ่านในวัตถุในการดำเนินการเกี่ยวกับs1->set("hello");s1->set(s1, "hello")

กับที่คำอธิบายเล็ก ๆ น้อย ๆ ไม่ต้องผ่านในการอ้างอิงถึงตัวเองออกจากวิธีการที่เราจะย้ายไปยังส่วนถัดไปซึ่งเป็นมรดกใน C

สมมติว่าเราต้องการให้ subclass ของString, ImmutableStringพูด เพื่อที่จะทำให้สตริงไม่เปลี่ยนรูปsetวิธีจะไม่สามารถเข้าถึงได้ในขณะที่ยังคงเข้าถึงgetและlengthและบังคับให้ "ตัวสร้าง" ที่จะยอมรับ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);

โดยทั่วไปสำหรับคลาสย่อยทั้งหมดเมธอดที่มีอยู่จะเป็นตัวชี้ฟังก์ชันอีกครั้ง เวลานี้การประกาศสำหรับวิธีการที่ไม่อยู่จึงไม่สามารถเรียกว่าในsetImmutableString

สำหรับการดำเนินการของImmutableStringรหัสที่เกี่ยวข้องเท่านั้นคือฟังก์ชั่น "ตัวสร้าง" ที่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ฟังก์ชันพอยน์เตอร์ไปยังgetและlengthเมธอดอ้างถึงจริง ๆString.getและString.lengthเมธอดโดยการผ่านbaseตัวแปรซึ่งเป็นStringวัตถุที่จัดเก็บไว้ภายใน

การใช้ตัวชี้ฟังก์ชันสามารถรับการสืบทอดของเมธอดจากซูเปอร์คลาส

เรายังสามารถดำเนินการต่อไปความแตกต่างใน C

ตัวอย่างเช่นถ้าเราต้องการเปลี่ยนพฤติกรรมของlengthวิธีการที่จะกลับมา0ตลอดเวลาในImmutableStringชั้นเรียนด้วยเหตุผลบางอย่างสิ่งที่จะต้องทำคือ:

  1. เพิ่มฟังก์ชั่นที่จะทำหน้าที่เป็นlengthวิธีการเอาชนะ
  2. ไปที่ "ตัวสร้าง" และตั้งค่าตัวชี้ฟังก์ชั่นเป็นlengthวิธีการเอาชนะ

การเพิ่มlengthวิธีการแทนที่ในImmutableStringอาจทำได้โดยการเพิ่มlengthOverrideMethod:

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

จากนั้นตัวชี้ฟังก์ชั่นสำหรับlengthวิธีการในตัวสร้างจะถูกเชื่อมโยงกับ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;
}

ตอนนี้แทนที่จะมีพฤติกรรมที่เหมือนกันสำหรับlengthวิธีการในImmutableStringชั้นเรียนเป็นStringชั้นเรียนตอนนี้lengthวิธีการจะอ้างถึงพฤติกรรมที่กำหนดไว้ในlengthOverrideMethodฟังก์ชั่น

ฉันต้องเพิ่มข้อจำกัดความรับผิดชอบที่ฉันยังคงเรียนรู้วิธีการเขียนด้วยสไตล์การเขียนโปรแกรมเชิงวัตถุใน C ดังนั้นอาจมีประเด็นที่ฉันไม่ได้อธิบายได้ดีหรืออาจปิดเครื่องหมายในแง่ของการใช้ OOP ที่ดีที่สุด ใน C. แต่จุดประสงค์ของฉันคือพยายามแสดงให้เห็นถึงการใช้งานตัวชี้ฟังก์ชั่นหลายอย่าง

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีการเขียนโปรแกรมเชิงวัตถุใน C โปรดอ้างอิงคำถามต่อไปนี้:


22
คำตอบนี้น่ากลัว! ไม่เพียง แต่จะบอกเป็นนัยว่า OO ขึ้นอยู่กับเครื่องหมายจุด แต่อย่างใดมันยังกระตุ้นให้ทิ้งขยะลงในวัตถุของคุณ!
Alexei Averchenko

27
นี่คือ OO ทั้งหมดถูกต้อง แต่ไม่ใช่ทุกที่ที่อยู่ใกล้กับ C-style OO สิ่งที่คุณนำไปใช้อย่างเสียหายคือ OO ต้นแบบที่ใช้ Javascript ในรูปแบบ ในการรับ C ++ / Pascal-style OO คุณจะต้อง: 1. มีโครงสร้าง const สำหรับตารางเสมือนของแต่ละคลาสที่มีสมาชิกเสมือน 2. มีตัวชี้ไปยังโครงสร้างนั้นในวัตถุ polymorphic 3. เรียกวิธีการเสมือนผ่านตารางเสมือนจริงและวิธีอื่น ๆ ทั้งหมดโดยตรงโดยปกติแล้วจะทำตามการClassName_methodNameตั้งชื่อฟังก์ชั่นบางอย่าง จากนั้นคุณจะได้รับค่าใช้จ่ายรันไทม์และพื้นที่เก็บข้อมูลเท่ากับที่คุณทำใน C ++ และ Pascal
Reinstate Monica

19
การใช้งาน OO ด้วยภาษาที่ไม่ได้ตั้งใจให้เป็น OO นั้นเป็นความคิดที่ไม่ดีเสมอไป หากคุณต้องการ OO และยังคงมี C เพียงแค่ทำงานกับ C ++
rbaleksandar

20
@rbaleksandar บอกให้ผู้พัฒนาเคอร์เนล Linux ทราบ "ความคิดที่ไม่ดีเสมอ"เป็นความเห็นของคุณอย่างเคร่งครัดซึ่งฉันไม่เห็นด้วยอย่างยิ่ง
Jonathon Reinhart

6
ฉันชอบคำตอบนี้ แต่ไม่ต้องโยน malloc
แมว

227

คำแนะนำเกี่ยวกับการถูกไล่ออก: วิธีใช้งานพอยน์เตอร์ใน GCC บนเครื่อง x86 โดยการคอมไพล์โค้ดของคุณด้วยมือ:

ตัวอักษรสตริงเหล่านี้เป็นไบต์ของรหัสเครื่อง x86 แบบ 32 บิต 0xC3เป็นx86retการเรียนการสอน

โดยปกติแล้วคุณจะไม่เขียนด้วยมือคุณจะเขียนด้วยภาษาแอสเซมบลีแล้วใช้แอสเซมเบลอร์nasmเพื่อรวมเข้าไปในไบนารีแบบเรียบซึ่งคุณ hexdump เป็นตัวอักษรสตริง C

  1. ส่งคืนค่าปัจจุบันของการลงทะเบียน EAX

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. เขียนฟังก์ชั่นการสลับ

    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. เขียนตัวนับ for-loop ที่ 1,000 เรียกใช้ฟังก์ชันในแต่ละครั้ง

    ((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

    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);

โปรดทราบว่าคอมไพเลอร์วางตัวอักษรสตริงใน.rodataส่วน (หรือ.rdataบน Windows) ซึ่งมีการเชื่อมโยงเป็นส่วนหนึ่งของส่วนข้อความ (พร้อมกับรหัสสำหรับฟังก์ชั่น)

ส่วนของข้อความได้รับการอนุญาตให้อ่าน + Exec ดังนั้นการส่งสตริงตัวอักษรไปยังฟังก์ชันพอยน์เตอร์จึงทำงานได้โดยไม่จำเป็นต้องมีmprotect()หรือการVirtualProtect()เรียกของระบบเหมือนที่คุณต้องการสำหรับหน่วยความจำที่จัดสรรแบบไดนามิก (หรือgcc -z execstackเชื่อมโยงโปรแกรมด้วยสแต็ค + ส่วนข้อมูล + ความสามารถในการประมวลผลฮีปเพื่อแฮ็คอย่างรวดเร็ว)


ในการแยกส่วนเหล่านี้คุณสามารถคอมไพล์สิ่งนี้เพื่อวางเลเบลบนไบต์และใช้ disassembler

// 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";

การคอมไพล์ด้วยgcc -c -m32 foo.cและแยกส่วนobjdump -D -rwC -Mintelเราสามารถรับชุดประกอบและพบว่ารหัสนี้ละเมิด ABI โดยการปิดกั้น EBX (การลงทะเบียนที่สงวนไว้สำหรับการโทร) และโดยทั่วไปจะไม่มีประสิทธิภาพ

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

รหัสเครื่องนี้ (อาจ) ทำงานในรหัส 32 บิตบน Windows, Linux, OS X และอื่น ๆ : อนุสัญญาการโทรเริ่มต้นบน OS ทั้งหมดเหล่านี้ส่งผ่าน args บนสแต็กแทนการลงทะเบียนที่มีประสิทธิภาพมากขึ้น แต่ EBX นั้นถูกสงวนไว้สำหรับการโทรในการประชุมปกติทั้งหมดดังนั้นการใช้มันเป็น register register โดยไม่บันทึก / เรียกคืนมันสามารถทำให้ caller crash ได้ง่าย


8
หมายเหตุ: วิธีนี้ใช้ไม่ได้หากเปิดใช้งาน Data Execution Prevention (เช่นใน Windows XP SP2 +) เนื่องจากสตริง C จะไม่ถูกทำเครื่องหมายว่าปฏิบัติการได้
SecurityMatt

5
สวัสดี Matt! ขึ้นอยู่กับระดับการปรับให้เหมาะสม GCC มักจะมีค่าคงที่ของสตริงอยู่ในเซ็กเมนต์ TEXT ดังนั้นสิ่งนี้จะทำงานได้แม้ใน Windows รุ่นที่ใหม่กว่าโดยที่คุณไม่ได้ไม่อนุญาตการเพิ่มประสิทธิภาพประเภทนี้ (IIRC รุ่น MinGW ในช่วงเวลาของการโพสต์ของฉันมานานกว่าสองปีที่ผ่านมา inlines ตัวอักษรของสตริงที่ระดับการเพิ่มประสิทธิภาพการเริ่มต้น)
ลี

10
มีคนช่วยอธิบายสิ่งที่เกิดขึ้นที่นี่ได้ไหม อะไรคือตัวอักษรสตริงที่ดูแปลก ๆ ?
ajay

56
@ jay ดูเหมือนว่าเขากำลังเขียนค่าเลขฐานสิบหกแบบดิบ (ตัวอย่างเช่น '\ x00' เหมือนกับ '/ 0' พวกเขาทั้งคู่เท่ากับ 0) ลงในสตริงจากนั้นหล่อสตริงลงในตัวชี้ฟังก์ชัน C จากนั้นดำเนินการ ตัวชี้ฟังก์ชัน C เพราะเขาเป็นปีศาจ
ejk314

3
สวัสดี FUZxxl ฉันคิดว่ามันอาจแตกต่างกันไปตามคอมไพเลอร์และเวอร์ชั่นของระบบปฏิบัติการ รหัสข้างต้นดูเหมือนว่าจะทำงานได้ดีใน codepad.org; codepad.org/FMSDQ3ME
Lee

115

หนึ่งในสิ่งที่ฉันชอบใช้สำหรับพอยน์เตอร์ของฟังก์ชั่นคือตัววนซ้ำราคาถูกและใช้ง่าย -

#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
คุณควรส่งตัวชี้ไปยังข้อมูลที่ผู้ใช้ระบุหากคุณต้องการแยกเอาท์พุทใด ๆ จากการทำซ้ำ (คิดว่าปิด)
Alexei Averchenko

1
ตกลง ทั้งหมด iterators int (*cb)(void *arg, ...)ของฉันมีลักษณะเช่นนี้ ค่าส่งคืนของตัววนซ้ำยังช่วยให้ฉันหยุดก่อน (ถ้าไม่ใช่ศูนย์)
Jonathon Reinhart

24

พอยน์เตอร์ของฟังก์ชั่นกลายเป็นเรื่องง่ายที่จะประกาศเมื่อคุณมีผู้ประกาศพื้นฐาน:

  • ID: ID: ID เป็น
  • ตัวชี้: *D: D ตัวชี้ไปยัง
  • ฟังก์ชั่น: D(<parameters>): D ฟังก์ชั่นการถ่าย<พารามิเตอร์>กลับ

ในขณะที่ D เป็นตัวประกาศอื่นที่สร้างขึ้นโดยใช้กฎเดียวกัน ในท้ายที่สุดมันจะลงท้ายด้วยID(ดูตัวอย่างด้านล่าง) ซึ่งเป็นชื่อของนิติบุคคลที่ประกาศ ลองสร้างฟังก์ชั่นโดยใช้ตัวชี้ไปยังฟังก์ชั่นที่ไม่ทำอะไรเลยและคืนค่า int และคืนค่าพอยน์เตอร์ให้กับฟังก์ชันที่ใช้ char และ return int ด้วยประเภท defs มันเป็นเช่นนี้

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

อย่างที่คุณเห็นมันเป็นเรื่องง่ายที่จะสร้างมันขึ้นมาโดยใช้ typedefs หากไม่มี typedefs ก็ไม่ยากอย่างใดอย่างหนึ่งกับกฎของนักประกาศด้านบนที่ใช้อย่างสม่ำเสมอ อย่างที่คุณเห็นฉันพลาดส่วนที่ตัวชี้ชี้ไปและสิ่งที่ฟังก์ชันส่งคืน นั่นคือสิ่งที่ปรากฏที่ด้านซ้ายสุดของการประกาศและไม่เป็นที่สนใจ: มันจะถูกเพิ่มเข้าไปในท้ายที่สุดถ้ามีผู้ประกาศขึ้นมาแล้ว มาทำกันเถอะ สร้างขึ้นอย่างต่อเนื่องใช้คำแรก - แสดงโครงสร้างการใช้[และ]:

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

อย่างที่คุณเห็นหนึ่งสามารถอธิบายประเภทได้อย่างสมบูรณ์โดยผนวกท้ายประกาศหนึ่งหลังจากแต่ละอื่น ๆ การก่อสร้างสามารถทำได้สองวิธี หนึ่งคือจากล่างขึ้นบนเริ่มต้นด้วยสิ่งที่ถูกต้อง (ใบไม้) และทำงานผ่านจนถึงตัวระบุ วิธีอื่น ๆ คือจากบนลงล่างเริ่มจากตัวบ่งชี้ทำงานไปจนถึงใบไม้ ฉันจะแสดงทั้งสองวิธี

จากล่างขึ้นบน

การก่อสร้างเริ่มต้นด้วยสิ่งที่อยู่ด้านขวา: สิ่งที่ส่งคืนซึ่งเป็นฟังก์ชั่นการทำความสะอาด เพื่อให้ผู้ประกาศชัดเจนฉันจะให้เลขพวกเขา:

D1(char);

แทรกพารามิเตอร์ char โดยตรงเนื่องจากไม่สำคัญ เพิ่มตัวชี้ไปยัง declarator โดยเปลี่ยนจากD1 *D2โปรดทราบว่าเราต้องล้อมวงเล็บ*D2ไว้ ที่สามารถเป็นที่รู้จักโดยมองถึงความสำคัญของและผู้ประกอบการฟังก์ชั่นการโทร*-operator โดยไม่ต้องวงเล็บของเราคอมไพเลอร์จะอ่านว่ามันเป็น() *(D2(char p))แต่นั่นคงไม่ใช่การแทนที่ D1 ธรรมดา*D2อีกต่อไปแน่นอน วงเล็บอนุญาตให้ใช้กับผู้ประกาศ ดังนั้นคุณไม่ได้ทำอะไรผิดถ้าคุณเพิ่มพวกเขามากเกินไปจริง ๆ

(*D2)(char);

ชนิดส่งคืนเสร็จสมบูรณ์! ทีนี้มาแทนที่D2ฟังก์ชั่นการประกาศฟังก์ชั่นการ<parameters>คืนซึ่งเป็นD3(<parameters>)ที่เราอยู่ในขณะนี้

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

โปรดทราบว่าไม่จำเป็นต้องใช้วงเล็บเนื่องจากเราต้องการ D3เป็น function-declarator และไม่ใช่ตัวประกาศสำหรับตัวชี้ในเวลานี้ ยิ่งใหญ่สิ่งเดียวที่เหลืออยู่คือพารามิเตอร์ของมัน พารามิเตอร์จะทำเหมือนกับที่เราเคยทำชนิดการส่งคืนเพียงกับแทนที่ด้วยchar voidดังนั้นฉันจะคัดลอกมัน:

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

ฉันถูกแทนที่D2ด้วยID1เนื่องจากเราเสร็จสิ้นด้วยพารามิเตอร์นั้น (มันเป็นตัวชี้ไปยังฟังก์ชั่น - ไม่จำเป็นต้องมีตัวประกาศอื่น) ID1จะเป็นชื่อของพารามิเตอร์ ตอนนี้ฉันบอกข้างต้นที่ท้ายหนึ่งเพิ่มประเภทที่ประกาศทั้งหมดเหล่านั้นแก้ไข - คนที่ปรากฏที่ด้านซ้ายสุดของทุกการประกาศ สำหรับฟังก์ชั่นนั้นจะกลายเป็นประเภทส่งคืน สำหรับพอยน์เตอร์ที่ชี้ไปที่ประเภท ฯลฯ ... มันน่าสนใจเมื่อเขียนลงไปมันจะปรากฏขึ้นในลำดับที่ตรงกันข้ามที่อยู่ด้านขวาสุด :) อย่างไรก็ตามการแทนที่มันให้การประกาศที่สมบูรณ์ intแน่นอนทั้งสองครั้ง

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

ฉันได้เรียกตัวระบุของฟังก์ชันID0ในตัวอย่างนั้น

จากบนลงล่าง

สิ่งนี้เริ่มต้นที่ตัวบ่งชี้ทางด้านซ้ายสุดในคำอธิบายของประเภทซึ่งมีผู้ประกาศว่าเมื่อเราเดินผ่านทางขวา เริ่มต้นด้วยฟังก์ชั่นรับ<พารามิเตอร์>กลับมา

ID0(<parameters>)

สิ่งต่อไปในรายละเอียด (หลังจาก "กลับ") เป็นตัวชี้ไปยัง มารวมกัน:

*ID0(<parameters>)

แล้วสิ่งต่อไปคือการถ่าย functon <พารามิเตอร์>กลับ พารามิเตอร์เป็นตัวอักษรธรรมดาดังนั้นเราจึงใส่มันทันทีอีกครั้งเพราะมันเป็นเรื่องเล็กน้อย

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

สังเกตเครื่องหมายวงเล็บที่เราเพิ่มเข้าไปเนื่องจากเราต้องการให้*ผูกก่อนแล้วตาม(char)ด้วย มิฉะนั้นก็จะอ่านฟังก์ชั่นการถ่าย<พารามิเตอร์>ฟังก์ชั่นที่กลับมา ... ไม่อนุญาตให้ใช้ฟังก์ชันที่ส่งคืนฟังก์ชัน

ตอนนี้เราก็ต้องใส่พารามิเตอร์< >ฉันจะแสดงเวอร์ชั่นสั้น ๆ ของการหลุดพ้นเนื่องจากฉันคิดว่าตอนนี้คุณมีความคิดแล้วว่าจะทำอย่างไร

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

เพิ่งintประกาศต่อหน้าผู้ประกาศเหมือนที่เราทำจากล่างขึ้นบนและเราเสร็จแล้ว

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

สิ่งที่ดี

จากล่างขึ้นบนหรือบนลงล่างดีขึ้นหรือไม่ ฉันคุ้นเคยกับการ bottom-up แต่บางคนอาจรู้สึกสะดวกสบายเมื่อใช้จากบนลงล่าง ฉันคิดว่ามันเป็นเรื่องของรสนิยม อนึ่งหากคุณใช้ตัวดำเนินการทั้งหมดในการประกาศนั้นคุณจะได้รับ int:

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

นั่นเป็นคุณสมบัติที่ดีของการประกาศใน C: การประกาศยืนยันว่าหากตัวดำเนินการเหล่านั้นถูกนำมาใช้ในการแสดงออกโดยใช้ตัวระบุมันจะให้ผลเป็นประเภททางด้านซ้ายมือ มันก็เป็นเช่นนั้นสำหรับอาร์เรย์ด้วย

หวังว่าคุณจะชอบการกวดวิชาเล็ก ๆ นี้! ตอนนี้เราสามารถเชื่อมโยงกับสิ่งนี้ได้เมื่อผู้คนสงสัยเกี่ยวกับไวยากรณ์ของฟังก์ชั่นการประกาศที่แปลก ฉันพยายามใส่ตัวซีให้น้อยที่สุดเท่าที่จะทำได้ รู้สึกอิสระที่จะแก้ไข / แก้ไขสิ่งต่าง ๆ ในนั้น


24

การใช้งานที่ดีอีกประการหนึ่งสำหรับตัวชี้ฟังก์ชั่น:
การสลับระหว่างเวอร์ชันอย่างไม่ลำบาก

เป็นประโยชน์อย่างมากเมื่อคุณต้องการฟังก์ชั่นที่แตกต่างกันในเวลาที่ต่างกันหรือขั้นตอนการพัฒนาที่แตกต่างกัน ตัวอย่างเช่นฉันกำลังพัฒนาแอปพลิเคชันบนโฮสต์คอมพิวเตอร์ที่มีคอนโซล แต่ซอฟต์แวร์รุ่นสุดท้ายจะวางบน Avnet ZedBoard (ซึ่งมีพอร์ตสำหรับการแสดงผลและคอนโซล แต่พวกเขาไม่ต้องการ / ต้องการสำหรับ รุ่นสุดท้าย) ดังนั้นในระหว่างการพัฒนาฉันจะใช้printfเพื่อดูสถานะและข้อความแสดงข้อผิดพลาด แต่เมื่อฉันทำเสร็จแล้วฉันไม่ต้องการพิมพ์อะไรเลย นี่คือสิ่งที่ฉันทำ:

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

ในversion.cฉันจะกำหนด 2 ฟังก์ชั่นต้นแบบที่มีอยู่ในversion.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;
}

แจ้งให้ทราบว่าตัวชี้ฟังก์ชันจะเป็นต้นแบบในการversion.hเป็น

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

เมื่อมีการอ้างอิงในแอปพลิเคชันมันจะเริ่มดำเนินการทุกที่ที่มีการชี้ซึ่งยังไม่ได้กำหนด

ในversion.cแจ้งให้ทราบในboard_init()ฟังก์ชั่นที่zprintfได้รับมอบหมายฟังก์ชั่นที่ไม่ซ้ำกัน (ซึ่งตรงกับฟังก์ชั่นลายเซ็น) ขึ้นอยู่กับรุ่นที่กำหนดไว้version.h

zprintf = &printf; zprintf เรียกใช้ printf เพื่อการดีบัก

หรือ

zprintf = &noprint; zprintf เพิ่งกลับมาและจะไม่เรียกใช้รหัสที่ไม่จำเป็น

ใช้รหัสจะมีลักษณะเช่นนี้:

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;
}

โค้ดด้านบนจะใช้printfถ้าอยู่ในโหมดดีบั๊กหรือไม่ทำอะไรเลยถ้าอยู่ในโหมดรีลีส ซึ่งง่ายกว่าการทำโครงการทั้งหมดและการใส่ความคิดเห็นหรือลบรหัส สิ่งที่ฉันต้องทำคือเปลี่ยนเวอร์ชั่นในversion.hและโค้ดจะทำส่วนที่เหลือ!


4
คุณยืนเสียเวลามากในการแสดง แต่คุณสามารถใช้แมโครที่เปิดใช้งานและปิดใช้งานส่วนของรหัสตาม Debug / Release
AlphaGoku

19

ตัวชี้ฟังก์ชั่นมักจะถูกกำหนดโดยtypedefและใช้เป็นค่าพารามิเตอร์และผลตอบแทน

คำตอบข้างต้นมีคำอธิบายแล้วฉันได้ยกตัวอย่าง:

#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 คือการเรียกฟังก์ชั่นที่เลือกในเวลาทำงาน ตัวอย่างเช่นไลบรารีรันไทม์ C มีสองรูทีนqsortและbsearchซึ่งนำตัวชี้ไปยังฟังก์ชันที่ถูกเรียกเพื่อเปรียบเทียบสองรายการที่เรียงลำดับ สิ่งนี้ช่วยให้คุณสามารถเรียงลำดับหรือค้นหาตามลำดับสิ่งใดก็ได้ตามเกณฑ์ที่คุณต้องการใช้

ตัวอย่างพื้นฐานมากหากมีฟังก์ชันหนึ่งที่เรียกว่าprint(int x, int y)ซึ่งจำเป็นต้องเรียกใช้ฟังก์ชัน (อย่างใดอย่างหนึ่งadd()หรือsub()ซึ่งเป็นชนิดเดียวกัน) จากนั้นสิ่งที่เราจะทำเราจะเพิ่มอาร์กิวเมนต์ตัวชี้ฟังก์ชันหนึ่งprint()ฟังก์ชันตามที่แสดงด้านล่าง :

#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;
}

ผลลัพธ์คือ:

ค่าคือ: 410
ค่าคือ: 390


10

การเริ่มต้นจากฟังก์ชั่นเริ่มต้นมีที่อยู่หน่วยความจำบางส่วนจากจุดที่พวกเขาเริ่มดำเนินการ ในภาษาแอสเซมบลีพวกเขาจะถูกเรียกว่าเป็น (เรียกว่า "ที่อยู่หน่วยความจำของฟังก์ชัน") ตอนนี้กลับมาที่ C ถ้าฟังก์ชั่นมีที่อยู่หน่วยความจำก็สามารถจัดการได้โดยพอยน์เตอร์ใน

1. ก่อนอื่นคุณต้องประกาศตัวชี้ไปยังฟังก์ชัน 2. ผ่านที่อยู่ของฟังก์ชันที่ต้องการ

**** หมายเหตุ -> ฟังก์ชั่นควรเป็นประเภทเดียวกัน ****

โปรแกรมอย่างง่ายนี้จะแสดงทุกสิ่ง

#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");
}

ป้อนคำอธิบายรูปภาพที่นี่หลังจากนั้นให้ดูว่าเครื่องเข้าใจพวกเขาได้อย่างไรคำสั่งเครื่องของโปรแกรมด้านบนในสถาปัตยกรรม 32 บิต

พื้นที่เครื่องหมายสีแดงแสดงวิธีการแลกเปลี่ยนที่อยู่และการจัดเก็บใน eax จากนั้นเป็นคำสั่งเรียกพวกเขาบน eax eax มีที่อยู่ที่ต้องการของฟังก์ชั่น


8

ตัวชี้ฟังก์ชั่นเป็นตัวแปรที่มีที่อยู่ของฟังก์ชั่น เนื่องจากมันเป็นตัวแปรพอยน์เตอร์ที่มีคุณสมบัติที่ถูก จำกัด คุณสามารถใช้มันได้เหมือนกับที่คุณทำกับตัวแปรพอยน์เตอร์อื่น ๆ ในโครงสร้างข้อมูล

ข้อยกเว้นเดียวที่ฉันนึกได้คือการใช้ตัวชี้ฟังก์ชั่นเป็นการชี้ไปยังสิ่งอื่นนอกเหนือจากค่าเดียว การทำเลขคณิตของตัวชี้โดยการเพิ่มหรือลดตัวชี้ฟังก์ชั่นหรือการเพิ่ม / ลบออฟเซ็ตให้กับตัวชี้ฟังก์ชั่นนั้นไม่ได้มีประโยชน์ใด ๆ เลยในขณะที่ตัวชี้ฟังก์ชั่นชี้ไปที่สิ่งเดียว

ขนาดของตัวแปรตัวชี้ฟังก์ชั่นจำนวนไบต์ที่ถูกครอบครองโดยตัวแปรอาจแตกต่างกันไปขึ้นอยู่กับสถาปัตยกรรมพื้นฐานเช่น x32 หรือ x64 หรืออะไรก็ตาม

การประกาศสำหรับตัวแปรตัวชี้ฟังก์ชั่นจำเป็นต้องระบุข้อมูลประเภทเดียวกันกับการประกาศฟังก์ชั่นเพื่อให้คอมไพเลอร์ C ทำการตรวจสอบประเภทที่ปกติ หากคุณไม่ได้ระบุรายการพารามิเตอร์ในการประกาศ / คำนิยามของตัวชี้ฟังก์ชั่นคอมไพเลอร์ C จะไม่สามารถตรวจสอบการใช้พารามิเตอร์ มีหลายกรณีที่การขาดการตรวจสอบนี้มีประโยชน์อย่างไรก็ตามโปรดจำไว้ว่าได้ลบตาข่ายนิรภัยออกแล้ว

ตัวอย่างบางส่วน:

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.

คำประกาศสองข้อแรกนั้นค่อนข้างคล้ายกันในเรื่องนี้:

  • funcเป็นฟังก์ชั่นที่รับinta char *และa และคืนค่าint
  • pFuncเป็นตัวชี้ฟังก์ชั่นที่ได้รับการกำหนดที่อยู่ของฟังก์ชั่นที่ใช้intและchar *และส่งกลับint

ดังนั้นจากข้างต้นเราจะมีสายแหล่งที่อยู่ของฟังก์ชั่นที่func()ได้รับมอบหมายให้ตัวแปรชี้ฟังก์ชั่นในขณะที่pFuncpFunc = func;

ขอให้สังเกตไวยากรณ์ที่ใช้กับการประกาศ / การกำหนดตัวชี้ฟังก์ชันซึ่งใช้วงเล็บในการเอาชนะกฎที่มีความสำคัญเหนือธรรมชาติ

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

ตัวอย่างการใช้งานที่แตกต่างกันหลายอย่าง

ตัวอย่างการใช้งานตัวชี้ฟังก์ชั่น:

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

คุณสามารถใช้รายการพารามิเตอร์ความยาวผันแปรได้ในนิยามของตัวชี้ฟังก์ชัน

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

หรือคุณไม่สามารถระบุรายการพารามิเตอร์ได้เลย สิ่งนี้มีประโยชน์ แต่จะช่วยลดโอกาสที่คอมไพเลอร์ C ทำการตรวจสอบในรายการอาร์กิวเมนต์ที่ให้ไว้

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

คุณสามารถใช้สไตล์ C พร้อมกับพอยน์เตอร์ฟังก์ชั่น อย่างไรก็ตามโปรดทราบว่าคอมไพเลอร์ C อาจจะหละหลวมเกี่ยวกับการตรวจสอบหรือให้คำเตือนมากกว่าข้อผิดพลาด

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.

เปรียบเทียบตัวชี้ฟังก์ชั่นกับความเท่าเทียมกัน

คุณสามารถตรวจสอบว่าตัวชี้ฟังก์ชันเท่ากับที่อยู่ฟังก์ชันเฉพาะโดยใช้ifคำสั่งแม้ว่าฉันไม่แน่ใจว่ามีประโยชน์อย่างไร ตัวดำเนินการเปรียบเทียบอื่น ๆ ดูเหมือนจะมีประโยชน์น้อยกว่า

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;
}

Array of Function Pointers

และถ้าคุณต้องการให้อาร์เรย์ของฟังก์ชันพอยน์เตอร์แต่ละองค์ประกอบที่รายการอาร์กิวเมนต์มีความแตกต่างกันคุณสามารถกำหนดตัวชี้ฟังก์ชันด้วยรายการอาร์กิวเมนต์ที่ไม่ได้ระบุ (ไม่ใช่voidซึ่งหมายความว่าไม่มีข้อโต้แย้ง แต่ไม่ได้ระบุ) บางอย่างเช่นต่อไปนี้ อาจเห็นคำเตือนจากคอมไพเลอร์ C นอกจากนี้ยังใช้งานได้กับพารามิเตอร์ตัวชี้ฟังก์ชันกับฟังก์ชัน:

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 namespaceใช้ Global structกับตัวชี้ฟังก์ชั่น

คุณสามารถใช้staticคำสำคัญเพื่อระบุฟังก์ชั่นที่มีชื่อเป็นขอบเขตไฟล์และจากนั้นกำหนดให้ตัวแปรทั่วโลกเป็นวิธีการให้สิ่งที่คล้ายกับการnamespaceทำงานของ C ++

ในไฟล์ส่วนหัวกำหนดโครงสร้างที่จะเป็น namespace ของเราพร้อมกับตัวแปรทั่วโลกที่ใช้มัน

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;

จากนั้นในไฟล์ต้นฉบับ C:

#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};

สิ่งนี้จะถูกใช้โดยการระบุชื่อที่สมบูรณ์ของตัวแปรโกลบอลและชื่อสมาชิกเพื่อเข้าถึงฟังก์ชัน โมดิconstฟายเออร์นี้ใช้กับทั่วโลกเพื่อให้ไม่สามารถเปลี่ยนแปลงได้โดยไม่ได้ตั้งใจ

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

ขอบเขตการใช้งานตัวชี้ฟังก์ชั่น

คอมโพเนนต์ไลบรารี DLL สามารถทำบางสิ่งที่คล้ายกับnamespaceวิธีการลักษณะ C ซึ่งมีการร้องขออินเทอร์เฟซไลบรารีเฉพาะจากวิธีการจากโรงงานในอินเทอร์เฟซไลบรารีซึ่งสนับสนุนการสร้างstructตัวชี้ฟังก์ชันที่ประกอบด้วย .. อินเตอร์เฟสไลบรารีนี้โหลดรุ่น DLL ที่ร้องขอ struct ที่มีตัวชี้ฟังก์ชันที่จำเป็นจากนั้นส่งคืน struct ให้กับผู้เรียกที่ร้องขอเพื่อใช้งาน

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;
}

และสิ่งนี้สามารถใช้เป็นใน:

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

วิธีการเดียวกันนี้สามารถใช้เพื่อกำหนดเลเยอร์ฮาร์ดแวร์ที่เป็นนามธรรมสำหรับโค้ดที่ใช้โมเดลเฉพาะของฮาร์ดแวร์พื้นฐาน ตัวชี้ฟังก์ชั่นจะเต็มไปด้วยฟังก์ชั่นเฉพาะของฮาร์ดแวร์โดยโรงงานเพื่อให้ฟังก์ชั่นเฉพาะของฮาร์ดแวร์ที่ใช้ฟังก์ชั่นที่ระบุในรูปแบบฮาร์ดแวร์นามธรรม สิ่งนี้สามารถใช้เพื่อจัดเตรียมเลเยอร์ฮาร์ดแวร์นามธรรมที่ใช้โดยซอฟต์แวร์ซึ่งเรียกใช้ฟังก์ชันจากโรงงานเพื่อรับฟังก์ชั่นอินเทอร์เฟซฮาร์ดแวร์เฉพาะจากนั้นใช้พอยน์เตอร์ฟังก์ชันที่จัดเตรียมไว้เพื่อดำเนินการสำหรับฮาร์ดแวร์พื้นฐานโดยไม่จำเป็นต้องทราบรายละเอียดการนำไปปฏิบัติ .

ตัวชี้ฟังก์ชั่นเพื่อสร้างผู้ได้รับมอบหมายตัวจัดการและการโทรกลับ

คุณสามารถใช้ตัวชี้ฟังก์ชั่นเป็นวิธีการมอบหมายงานหรือฟังก์ชั่นบางอย่าง ตัวอย่างแบบคลาสสิกใน C คือตัวชี้ฟังก์ชั่นการมอบหมายตัวแทนการเปรียบเทียบที่ใช้กับฟังก์ชันไลบรารีมาตรฐาน C qsort()และbsearch()เพื่อให้ลำดับการเรียงสำหรับการเรียงลำดับรายการหรือดำเนินการค้นหาแบบไบนารีในรายการที่เรียงลำดับ ตัวแทนฟังก์ชั่นการเปรียบเทียบระบุอัลกอริทึมการเปรียบเทียบที่ใช้ในการเรียงลำดับหรือการค้นหาแบบไบนารี

การใช้อื่นคล้ายกับการนำอัลกอริทึมไปใช้กับคอนเทนเนอร์ไลบรารีแม่แบบ C ++ มาตรฐาน

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);

อีกตัวอย่างหนึ่งคือด้วยซอร์สโค้ด GUI ซึ่งมีการลงทะเบียนตัวจัดการสำหรับเหตุการณ์เฉพาะโดยให้ตัวชี้ฟังก์ชั่นซึ่งเรียกจริงเมื่อเหตุการณ์เกิดขึ้น เฟรมเวิร์ก Microsoft MFC พร้อมข้อความแผนที่ใช้สิ่งที่คล้ายกันเพื่อจัดการกับข้อความ Windows ที่ส่งไปยังหน้าต่างหรือเธรด

ฟังก์ชั่นแบบอะซิงโครนัสที่ต้องการการติดต่อกลับคล้ายกับตัวจัดการเหตุการณ์ ผู้ใช้ฟังก์ชั่นอะซิงโครนัสเรียกใช้ฟังก์ชั่นอะซิงโครนัสเพื่อเริ่มต้นการดำเนินการบางอย่างและให้ตัวชี้ฟังก์ชันซึ่งฟังก์ชันอะซิงโครนัสจะเรียกใช้เมื่อการดำเนินการเสร็จสิ้น ในกรณีนี้เหตุการณ์คือฟังก์ชันอะซิงโครนัสที่ทำให้งานเสร็จสมบูรณ์


0

ตั้งแต่คำแนะนำการทำงานมักจะถูกเรียกกลับพิมพ์คุณอาจต้องการที่จะมีลักษณะที่ประเภทเรียกกลับปลอดภัย เช่นเดียวกับจุดเข้าใช้ ฯลฯ ของฟังก์ชันที่ไม่ใช่การเรียกกลับ

C ค่อนข้างไม่แน่นอนและให้อภัยในเวลาเดียวกัน :)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.