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


1232

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

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


35
또한 C 포인터에 대한 심층 분석에 대해서는 blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge를 참조하십시오 . 또한 Ground Up 프로그래밍 은 기계 수준에서 작동하는 방식을 보여줍니다. 이해 C의 "메모리 모델은" C 작업을 포인터하는 방법을 이해하는 데 매우 유용합니다.
Abbafei

8
훌륭한 정보. 제목으로, 나는 "함수 포인터가 어떻게 작동하는지"에 대한 설명을 실제로 보게 될 것이라고 예상했을 것이다. :)
Bogdan Alexandru

답변:


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 의사 일류 기능을 제공합니다.
giant91

22
@ Rich.Carpenter : 함수 포인터는 런타임 CPU 감지에 좋습니다. SSE, popcnt, AVX 등을 활용하기 위해 여러 기능 버전의 일부 기능이 있어야합니다. 시작할 때 기능 포인터를 현재 CPU에 대한 각 기능의 최고 버전으로 설정하십시오. 다른 코드에서는 어디에서나 CPU 기능에 조건 분기를 사용하는 대신 함수 포인터를 통해 호출하십시오. 그런 다음이 CPU가을 지원하더라도 pshufb느리게 결정하기에 대한 복잡한 논리를 수행 할 수 있으므로 속도가 느리므로 이전 구현이 여전히 빠릅니다. x264 / x265는 이것을 광범위하게 사용하며 오픈 소스입니다.
Peter Cordes

303

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에서 객체 지향 프로그래밍을 수행하는 방법에 대한 자세한 내용은 다음 질문을 참조하십시오.


22
이 대답은 끔찍하다! OO가 어떻게 든 점 표기법에 의존한다는 것을 의미 할뿐만 아니라 정크를 객체에 넣는 것을 권장합니다!
Alexei Averchenko

27
OO는 괜찮지 만 C 스타일 OO 근처는 아닙니다. 잘못 구현 한 것은 Javascript 스타일 프로토 타입 기반 OO입니다. C ++ / Pascal 스타일 OO를 가져 오려면 다음을 수행해야합니다. 1. 가상 멤버 가있는 각 클래스 의 가상 테이블에 대한 const 구조체를 갖습니다 . 2. 다형성 객체에서 해당 구조체에 대한 포인터를 갖습니다. 3. 가상 테이블을 통해 가상 메서드를 호출하고 다른 모든 메서드를 직접 호출합니다. 일반적으로 ClassName_methodName함수 명명 규칙을 따릅니다. 그래야 C ++ 및 Pascal에서와 동일한 런타임 및 스토리지 비용이 발생합니다.
Reinstate Monica

19
OO가 아닌 언어로 OO를 작업하는 것은 항상 나쁜 생각입니다. OO를 원하지만 여전히 C가 있으면 C ++로 작업하십시오.
rbaleksandar

20
@rbaleksandar 리눅스 커널 개발자에게 알려주십시오. "항상 나쁜 생각" 은 엄밀히 동의하지 않는 당신의 의견입니다.
Jonathon Reinhart

6
나는이 답변을 좋아하지만 malloc을 캐스팅하지 않습니다
cat

227

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


8
참고 : C 문자열은 일반적으로 실행 가능으로 표시되지 않기 때문에 데이터 실행 방지가 활성화 된 경우 (예 : Windows XP SP2 +에서) 작동하지 않습니다.
SecurityMatt

5
안녕 매트! 최적화 수준에 따라 GCC는 종종 문자열 상수를 TEXT 세그먼트에 인라인하므로 이러한 유형의 최적화를 허용하지 않으면 최신 버전의 창에서도 작동합니다. (IIRC, 2 년 전 필자의 게시물 당시 MINGW 버전은 기본 최적화 수준에서 문자열 리터럴을 인라인합니다)
Lee

10
누군가 여기서 무슨 일이 일어나고 있는지 설명해 주시겠습니까? 이상한 이상한 문자열 리터럴은 무엇입니까?
ajay

56
@ajay 원시 16 진수 값 (예 : '\ 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
동의했다. 모든 반복자는 다음과 같습니다 int (*cb)(void *arg, ...). 반복자의 반환 값을 사용하면 일찍 중지 할 수 있습니다 (0이 아닌 경우).
Jonathon Reinhart

24

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

  • 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 내부를 넣으려고했습니다. 내용을 자유롭게 편집 / 수정하십시오.


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.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것입니다. 코드는 나머지를 수행합니다!


4
U는 많은 성능 시간을 잃게됩니다. 대신 디버그 / 릴리스를 기반으로 코드 섹션을 활성화 및 비활성화하는 매크로를 사용할 수 있습니다.
AlphaGoku

19

함수 포인터는 일반적으로로 정의되며 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;
}

14

C에서 함수 포인터의 큰 용도 중 하나는 런타임에 선택된 함수를 호출하는 것입니다. 예를 들어, C 런타임 라이브러리는 두 개의 루틴이 qsortbsearch두 항목 정렬되고 비교 호출되는 함수에 대한 포인터를 가지고; 이를 통해 사용하려는 기준에 따라 각각을 정렬하거나 검색 할 수 있습니다.

매우 기본적인 예, 함수 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로 돌아옵니다. 함수에 메모리 주소가 있으면 C의 포인터에 의해 조작 될 수 있습니다. 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");
}

여기에 이미지 설명을 입력하십시오그 후 machine이 어떻게 그들을 이해하는지 볼 수 있습니다 32 비트 아키텍처에서 위 프로그램의 machine instruction에 대한 간략한 설명.

빨간색 표시 영역은 주소가 교환되고 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소요 함수 intchar *와 다시 표시는int
  • pFunc는 a int와 a 를 가져 와서 a 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;
}

함수 포인터의 배열

그리고 인수 목록에 차이점이있는 각 요소에 함수 포인터 배열을 원한다면 인수 목록이 지정되지 않은 ( 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.
}

함수 포인터와 함께 namespace전역 struct을 사용하는 C 스타일

당신이 사용할 수있는 static이름이 파일 범위이며, 다음에 비슷한 제공하는 방법으로 전역 변수에이를 할당하는 기능을 지정하는 키워드를 namespaceC ++의 기능을.

헤더 파일에서 네임 스페이스가 될 구조체와이를 사용하는 전역 변수를 정의하십시오.

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 라이브러리 구성 요소 namespacestruct포함 된 함수 포인터 의 작성을 지원하는 라이브러리 인터페이스의 팩토리 메소드에서 특정 라이브러리 인터페이스가 요청되는 C 스타일 접근법 과 유사한 작업을 수행 할 수 있습니다 .이 라이브러리 인터페이스는 요청 된 DLL 버전을로드하고 작성합니다. 필요한 함수 포인터를 가진 구조체, 그리고 요청한 호출자에게 구조체를 반환합니다.

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.