C에서 함수 포인터는 어떻게 작동합니까? 최근 C에서 함수

최근 C에서 함수 포인터에 대한 경험이있었습니다.

그래서 나는 당신 자신의 질문에 답하는 전통을 가지고, 주제에 빠르게 뛰어 들어야하는 사람들을 위해 아주 기본적인 것에 대한 작은 요약을 만들기로 결정했습니다.



답변

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

답변

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클래스 의 메소드 는 실제로 선언 된 함수에 대한 함수 포인터입니다. 인스턴스의 제조에서 StringnewString기능들은 각각의 기능에 대한 함수 포인터를 설정하기 위해 호출된다 :

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

주목할 수있는 것은 객체의 인스턴스에 대한 개념이없고 실제로 객체의 일부인 메소드를 가지므로 각 호출마다 “자체 객체”를 전달해야한다는 것입니다. (그리고 internalstruct의 코드 목록에서 생략 된 것은 숨겨져 있습니다. 정보 숨기기를 수행하는 방법이지만 함수 포인터와 관련이 없습니다.)

따라서 할 수있는 것이 아니라 s1->set("hello");객체를 전달하여에 대한 작업을 수행해야합니다 s1->set(s1, "hello").

그 작은 설명이 방해가되지 않는 자신을 언급해야하므로 다음 부분으로 넘어갑니다 .C의 상속입니다 .

하자 우리의 서브 클래스를 만들고 싶어 말을 String을 말한다 ImmutableString. 문자열을 변경할 수 없도록하기 위해 and에 set대한 액세스를 유지 하면서이 메소드에 액세스 할 수 없으며 “생성자”가 다음을 승인하도록합니다 .getlengthchar*

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할 때 getand length메소드에 대한 함수 포인터는 실제로 내부에 저장된 객체 인 변수를 통해 String.getand 및 String.length메소드를 참조 합니다.baseString

함수 포인터를 사용하면 수퍼 클래스에서 메소드를 상속받을 수 있습니다.

우리는 C 에서 다형성을 계속할 수 있습니다 .

예를 들어 어떤 이유로 클래스의 모든 시간 length을 반환 하도록 메서드 의 동작을 변경하려면 다음을 수행해야합니다.0ImmutableString

  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는 AS 클래스 String클래스 지금 length에있어서, 상기에 정의 된 동작을 참조한다 lengthOverrideMethod기능.

C에서 객체 지향 프로그래밍 스타일로 작성하는 방법을 여전히 배우고 있다는 고지 사항을 추가해야하므로 아마도 잘 설명하지 않았거나 OOP를 구현하는 가장 좋은 방법이라는 점에서 잘못되었을 수 있습니다 그러나 C의 목적은 함수 포인터를 많이 사용하는 방법 중 하나를 설명하는 것이 었습니다.

C에서 객체 지향 프로그래밍을 수행하는 방법에 대한 자세한 내용은 다음 질문을 참조하십시오.


답변

해고 가이드 : x86 머신에서 GCC에서 함수 포인터를 남용하는 방법 : 직접 코드를 컴파일하여 :

이 문자열 리터럴은 32 비트 x86 기계 코드의 바이트입니다. 0xC3있다 하여 x86의 ret명령 .

일반적으로 직접 작성하지는 않고 어셈블리 언어로 작성한 다음 nasmC 문자열 리터럴에 16 진 덤프하는 플랫 바이너리로 어셈블러를 사용합니다 .

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

컴파일러는 문자열 리터럴을 .rodata섹션 (또는 .rdataWindows)에 배치하며 텍스트 세그먼트의 일부로 링크됩니다 (함수 코드와 함께).

텍스트 세그먼트에는 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는 모든 일반적인 호출 규칙에서 호출 보존되므로 저장 / 복원하지 않고 스크래치 레지스터로 사용하면 호출자가 쉽게 충돌 할 수 있습니다.


답변

함수 포인터를 가장 좋아하는 용도 중 하나는 저렴하고 쉬운 반복자입니다.

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

답변

기본 선언자가 있으면 함수 포인터를 쉽게 선언 할 수 있습니다.

  • id : ID: ID는
  • 포인터 : *D: D 포인터
  • 기능 : D(<parameters>): D 기능 <매개 변수 >반환

D는 동일한 규칙을 사용하여 작성된 또 다른 선언자입니다. 결국 어딘가 ID에서 선언 된 엔터티의 이름 인 (아래 예제 참조)로 끝납니다 . 아무것도 취하지 않고 int를 리턴하는 함수에 대한 포인터를 취하고 char을 취하고 int를 리턴하는 함수에 대한 포인터를 리턴하는 함수를 작성해 봅시다. type-def를 사용하면 다음과 같습니다.

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

보시다시피 typedef를 사용하여 쉽게 구축 할 수 있습니다. typedef가 없으면 위의 선언자 규칙을 일관성있게 적용하는 것이 어렵지 않습니다. 보시다시피 포인터가 가리키는 부분과 함수가 반환하는 부분을 놓쳤습니다. 이것이 선언의 맨 왼쪽에 표시되며 관심이 없습니다. 선언자가 이미 작성된 경우 끝에 추가됩니다. 그걸하자. 일관되게 꾸준히 빌드하면 [and ]:을 사용하여 구조를 보여줍니다 .

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

보시다시피, 선언자를 하나씩 추가하여 유형을 완전히 설명 할 수 있습니다. 건설은 두 가지 방법으로 수행 할 수 있습니다. 하나는 매우 올바른 것 (잎)으로 시작하여 식별자까지 진행하는 상향식입니다. 다른 방법은 식별자에서 시작하여 잎까지 내려가는 하향식입니다. 두 가지 방법을 모두 보여 드리겠습니다.

상향식

건설은 오른쪽에있는 것부터 시작합니다 : 반환 된 것은 char를 취하는 함수입니다. 선언자를 구별하기 위해 번호를 매길 것입니다.

D1(char);

char 매개 변수는 사소하므로 직접 삽입했습니다. 대체하여 선언자에 대한 포인터를 추가 D1하여 *D2. 우리는 괄호를 감싸 야합니다 *D2. *-operator그리고 함수 호출 연산자 의 우선 순위를 찾아서 알 수 있습니다 (). 괄호가 없으면 컴파일러는로 읽습니다 *(D2(char p)). 그러나 그것은 *D2더 이상 D1을 더 이상 대체하지는 않을 것 입니다. 선언자는 항상 괄호를 사용할 수 있습니다. 실제로 너무 많이 추가해도 아무런 문제가 없습니다.

(*D2)(char);

반품 유형이 완료되었습니다! 자, 불러들이는 대신, D2함수 선언자에 의해 기능을 복용 <parameters>반환 하고, D3(<parameters>)우리가 지금에있는.

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

우리가 이후에는 괄호가 필요하지 않습니다 유의 원하는 D3 기능 – 선언자 아니라이 시간 선언자 포인터가 될 수 있습니다. 위대한 것은 남은 것은 매개 변수입니다. 매개 변수는로 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)

좋은 것

상향식 또는 하향식이 더 낫습니까? 나는 상향식에 익숙하지만 어떤 사람들은 하향식에 더 편할 수 있습니다. 내가 생각하는 맛의 문제입니다. 또한 해당 선언에 모든 ​​연산자를 적용하면 int를 얻게됩니다.

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

그것은 C에서 선언의 좋은 속성입니다. 선언은 이러한 연산자가 식별자를 사용하는 표현식에 사용되면 맨 왼쪽에 유형을 산출한다고 주장합니다. 배열도 마찬가지입니다.

이 작은 튜토리얼을 좋아 하셨기를 바랍니다! 이제 사람들이 함수의 이상한 선언 구문에 대해 궁금해 할 때 이것을 연결할 수 있습니다. 가능한 적은 C 내부를 넣으려고했습니다. 내용을 자유롭게 편집 / 수정하십시오.


답변

함수 포인터에 대한 또 다른 좋은 사용법 :
고통없이 버전 간 전환

서로 다른 시간에 다른 기능이나 다른 개발 단계를 원할 때 사용하기에 매우 편리합니다. 예를 들어, 콘솔이있는 호스트 컴퓨터에서 응용 프로그램을 개발하고 있지만 소프트웨어의 최종 릴리스는 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.cI는 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것입니다. 코드는 나머지를 수행합니다!


답변

함수 포인터는 일반적으로로 정의되며 typedefparam & return 값으로 사용됩니다.

위의 답변은 이미 많은 설명을 들었습니다.

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