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

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 のメソッドは class は実際には、宣言された関数への関数ポインタです。 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;
}

注目できることの 1 つは、オブジェクトのインスタンスの概念がなく、実際にはオブジェクトの一部であるメソッドを持つため、呼び出しごとに「自己オブジェクト」を渡す必要があることです。 (そして internal 隠された struct です これは以前のコード リストから省略されていました -- これは情報隠蔽を実行する方法ですが、関数ポインタには関係ありません。)

だから、 s1->set("hello"); できるのではなく s1->set(s1, "hello") でアクションを実行するには、オブジェクトを渡す必要があります .

自分自身への参照を渡さなければならない小さな説明はさておき、次の部分である C での継承 に進みます。 .

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

基本的に、すべてのサブクラスで、使用可能なメソッドは再び関数ポインターです。今回は set の宣言 メソッドが存在しないため、ImmutableString で呼び出すことはできません .

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 でずっと なんらかの理由でクラスになった場合、実行する必要があるのは次のことだけです:

<オール>
  • length のオーバーライドとして機能する関数を追加します。 メソッド。
  • 「コンストラクタ」に移動し、関数ポインタをオーバーライドする 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 の最適な実装方法に関して的外れな点がある可能性があります。しかし、私の目的は、関数ポインターの多くの用途の 1 つを説明することでした。

    C でオブジェクト指向プログラミングを実行する方法の詳細については、次の質問を参照してください:

    • C のオブジェクト指向?
    • C でオブジェクト指向のコードを書くことができますか?

    クビになるためのガイド:x86 マシン上の GCC でコードを手動でコンパイルして関数ポインターを悪用する方法:

    これらの文字列リテラルは、32 ビット x86 マシン コードのバイトです。 0xC3 x86 ret です

    通常はこれらを手で書くのではなく、アセンブリ言語で記述してから nasm のようなアセンブラを使用します。 それを C 文字列リテラルに 16 進ダンプするフラット バイナリにアセンブルします。

    <オール> <リ>

    EAX レジスタの現在の値を返します

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
    
    <リ>

    スワップ関数を書く

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

    for ループ カウンターを 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
    
    <リ>

    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 これは、テキスト セグメントの一部として (関数のコードと共に) リンクされています。

    テキスト セグメントには Read+Exec 権限があるため、mprotect() を必要とせずに文字列リテラルを関数ポインターにキャストできます。 または VirtualProtect() 動的に割り当てられたメモリに必要なようなシステムコール。 (または gcc -z execstack 簡単なハックとして、プログラムをスタック + データ セグメント + ヒープ実行可能ファイルにリンクします。)

    これらを逆アセンブルするには、これをコンパイルしてバイトにラベルを付け、逆アセンブラを使用します。

    // 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 で逆アセンブル 、アセンブリを取得すると、このコードが EBX (呼び出し保存レジスタ) を破壊することによって ABI に違反しており、一般的に非効率的であることがわかります。

    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
    

    このマシン コードは (おそらく) Windows、Linux、OS X などの 32 ビット コードで動作します。これらすべての OS の既定の呼び出し規則は、レジスタでより効率的にではなく、スタックで引数を渡します。しかし、EBX はすべての通常の呼び出し規則で呼び出しが保存されるため、保存/復元せずにスクラッチ レジスタとして使用すると、呼び出し元が簡単にクラッシュする可能性があります。