c - شرح الدوال في لغة السي




كيف تعمل مؤشرات الدالة في C؟ (8)

كان لي بعض الخبرة في الآونة الأخيرة مع مؤشرات وظيفة في C.

وهكذا ، مع تقليد الإجابة عن أسئلتك ، قررت أن أقدم ملخصًا صغيرًا عن الأساسيات ، لأولئك الذين يحتاجون إلى الغوص السريع في الموضوع.


مؤشرات الدالة في C

لنبدأ بوظيفة أساسية سنشير إليها :

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

أولاً ، دعنا نحدد مؤشرًا إلى دالة تستقبل 2 int s وتعيد دالة 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;
}

استخدام جيد آخر لمؤشرات الدالة:
التبديل بين الإصدارات دون ألم

إنها سهلة الاستخدام عندما تريد وظائف مختلفة في أوقات مختلفة ، أو مراحل مختلفة من التطوير. على سبيل المثال ، أقوم بتطوير تطبيق على جهاز كمبيوتر مضيف يحتوي على وحدة تحكم ، ولكن سيتم طرح الإصدار النهائي من البرنامج على لوحة أفيد ZnetBoard (التي تحتوي على منافذ للشاشات ووحدات التحكم ، ولكنها ليست مطلوبة / مطلوبة الإصدار النهائي). لذا أثناء التطوير ، 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 والرمز سوف تفعل بقية!


أحد الاستخدامات المفضلة لدي لمؤشرات الدالة هو تكرارات رخيصة وسهلة -

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

بدءا من وظيفة الصفر لديه بعض عنوان الذاكرة من حيث تبدأ في التنفيذ. في لغة التجميع يتم استدعائها كـ (استدعاء "عنوان الذاكرة الخاص بالوظيفة"). عد الآن إلى C إذا كانت الدالة تحتوي على عنوان ذاكرة ، فيمكن معالجتها بواسطة المؤشرات في C.So بموجب قواعد 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 على العنوان المطلوب للدالة


دليل الإقلاع: كيفية إساءة استخدام مؤشرات الوظائف في دول مجلس التعاون الخليجي على أجهزة x86 عن طريق تجميع التعليمات البرمجية الخاصة بك باليد:

  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 1000 ، مع استدعاء بعض الوظائف في كل مرة

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

عادة ما يتم تعريف مؤشر الوظيفة من قبل الرموز المميزة لـ 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;
}

مؤشرات الدالة مفيدة في كثير من الحالات ، على سبيل المثال:

  • أعضاء كائنات COM مؤشر إلى الدالة ag: This->lpVtbl->AddRef(This); AddRef هو مؤشر إلى دالة.
  • وظيفة رد الاتصال ، على سبيل المثال وظيفة محددة من قبل المستخدم لمقارنة اثنين من المتغيرات لتمريرها كرد على وظيفة الفرز الخاصة.
  • مفيدة جدا لتنفيذ البرنامج المساعد وتطبيق SDK.

نظرًا لأن مؤشرات الوظائف غالبًا ما تكون مكتوبة بعمليات رد الاتصال ، فقد تحتاج إلى إلقاء نظرة على نوع معاودة الاتصال الآمنة . وينطبق الشيء نفسه على نقاط الدخول ، وما إلى ذلك من الوظائف التي لا تمثل عمليات رد.

C متقلب جدا ومتسامح في نفس الوقت :)







function-pointers