Programming

C는 그렇게 어렵지 않습니다 : void (* (* f []) ()) ()

procodes 2020. 5. 16. 11:24
반응형

C는 그렇게 어렵지 않습니다 : void (* (* f []) ()) ()


방금 오늘 사진을 보았고 설명을 부탁드립니다. 여기 그림이 있습니다 :

일부 C 코드

나는 이것이 혼란스럽고 그러한 코드가 실제로 실용적인지 궁금해했다. 나는 사진을 googled 하고이 reddit 항목 에서 다른 사진을 발견 했으며 여기에 그 사진이 있습니다.

흥미로운 설명

그래서이 "나 선식으로 읽는"것은 무엇인가? 이것이 C 컴파일러가 구문 분석하는 방법입니까?
이 이상한 코드에 대한 간단한 설명이 있으면 좋을 것입니다.
이 외에도 이런 종류의 코드가 유용 할 수 있습니까? 그렇다면 언제 어디서?

질문 "나선형 규칙"에 대한,하지만 난 그냥 그 규칙을 읽는 방법이 적용된 방법이나 표현에 대해 물어 아니에요. 나는 그러한 표현의 사용과 나선 규칙의 타당성에 의문을 제기하고 있습니다. 이것에 관해서는, 좋은 답변이 이미 게시되어 있습니다.


복잡한 선언의 의미를 찾는 데 도움이 되는 "시계 방향 / 나선 규칙" 이라는 규칙이 있습니다 .

에서 C-자주 묻는 질문 :

따라야 할 세 가지 간단한 단계가 있습니다.

  1. 미지의 요소로 시작하여 나선형 / 시계 방향으로 움직입니다. 다음 요소를 고려할 때 해당 영어 문장으로 대체하십시오.

    [X]또는 []
    => Array X size of ... 또는 Array undefined size of ...

    (type1, type2)
    => 함수가 type1과 type2를 전달하는 중 ...

    *
    => 포인터 ...

  2. 모든 토큰이 덮힐 때까지 나선형 / 시계 방향으로 계속하십시오.

  3. 항상 괄호 안의 모든 것을 먼저 해결하십시오!

위의 링크에서 예를 확인할 수 있습니다.

또한 다음과 같은 웹 사이트가 있습니다.

http://www.cdecl.org

C 선언을 입력하면 영어 의미를 갖습니다. 에 대한

void (*(*f[])())()

출력 :

f를 함수에 대한 포인터의 배열로 선언하십시오.

편집하다:

Random832 의 주석에서 지적한 것처럼 나선형 규칙은 배열 배열을 처리하지 않으며 이러한 선언에서 (대부분의) 잘못된 결과를 초래합니다. 예를 들어 int **x[1][2];, 나선형 규칙의 []경우 우선 순위가 높은 사실을 무시합니다 *.

배열 배열 앞에 나선 규칙을 적용하기 전에 먼저 명시적인 괄호를 추가 할 수 있습니다. 예를 들면 다음 int **x[1][2];과 같은 것이다 int **(x[1][2]);올바르게 인해 다음에 우선 (또한 유효 C)와 스파이럴 규칙 올바른 영어 선언은 "x는 INT 포인터 포인터의 어레이 (2)의 배열 1"로 판독한다.

이 문제는이에 포함 된 것을 참고 대답 하여 제임스 칸 세이 (지적 haccks 코멘트에).


"나선형"규칙 종류는 다음 우선 순위 규칙에서 제외됩니다.

T *a[]    -- a is an array of pointer to T
T (*a)[]  -- a is a pointer to an array of T
T *f()    -- f is a function returning a pointer to T
T (*f)()  -- f is a pointer to a function returning T

첨자 []와 함수 호출 ()연산자는 단항보다 더 높은 우선 순위를 가지고 있습니다 *그래서, *f()같은 구문 분석 *(f())*a[]같은 구문 분석됩니다 *(a[]).

당신이 배열 또는 함수에 대한 포인터에 대한 포인터를 원한다면, 당신은 명시 적으로 그룹에 필요한 *에서와 같이 식별자, (*a)[]또는 (*f)().

그럼 당신은 그 실현 af단지 식별자보다 더 복잡하게 표현 될 수있다; 으로는 T (*a)[N], a단순한 식별되거나, 그와 같은 함수를 호출 할 수있는 (*f())[N]( a-> f()), 또는이 같은 배열 될 수있다 (*p[M])[N]( a-> p[M]), 또는이 같은 함수 포인터 배열 될 수있다 (*(*p[M])())[N]( a-> (*p[M])()) 기타

간접 연산자 *가 단항 대신 접두어 인 경우 좋을 것이므로 선언을 왼쪽에서 오른쪽으로 읽는 것이 다소 쉬워집니다 ( void f[]*()*();확실히보다 낫습니다 void (*(*f[])())()).

이와 같이 털이 선언 된 경우에는 가장 왼쪽의 식별자 를 찾아 위의 우선 순위 규칙을 적용하여 함수 매개 변수에 재귀 적으로 적용하십시오.

         f              -- f
         f[]            -- is an array
        *f[]            -- of pointers  ([] has higher precedence than *)
       (*f[])()         -- to functions
      *(*f[])()         -- returning pointers
     (*(*f[])())()      -- to functions
void (*(*f[])())();     -- returning void

signal표준 라이브러리 기능은 아마도 이런 종류의 광기에 대한 유형 표본 일 것입니다.

       signal                                       -- signal
       signal(                          )           -- is a function with parameters
       signal(    sig,                  )           --    sig
       signal(int sig,                  )           --    which is an int and
       signal(int sig,        func      )           --    func
       signal(int sig,       *func      )           --    which is a pointer
       signal(int sig,      (*func)(int))           --    to a function taking an int                                           
       signal(int sig, void (*func)(int))           --    returning void
      *signal(int sig, void (*func)(int))           -- returning a pointer
     (*signal(int sig, void (*func)(int)))(int)     -- to a function taking an int
void (*signal(int sig, void (*func)(int)))(int);    -- and returning void

이 시점에서 대부분의 사람들은 "use typedefs"라고 말합니다.

typedef void outerfunc(void);
typedef outerfunc *innerfunc(void);

innerfunc *f[N];

그러나...

식에 어떻게 사용 f 하시겠습니까? 포인터 배열이라는 것을 알고 있지만 올바른 함수를 실행하는 데 어떻게 사용합니까? typedef를 살펴보고 올바른 구문을 퍼즐로 만들어야합니다. 대조적으로, "네이 키드 (naked)"버전은 눈에 띄지 않지만, 표현식에서 사용 하는 방법을 정확하게 알려줍니다 f(즉, (*(*f[i])())();어느 함수도 인수를 사용하지 않는다고 가정).


C에서 선언은 사용법을 반영합니다. 이것이 표준에 정의 된 방식입니다. 선언 :

void (*(*f[])())()

표현식 (*(*f[i])())()이 유형의 결과를 생성 한다고 가정합니다 void. 다음을 의미합니다.

  • f 색인을 생성 할 수 있으므로 배열이어야합니다.

    f[i]
    
  • f역 참조 할 수 있으므로 의 요소는 포인터 여야합니다.

    *f[i]
    
  • 이러한 포인터는 인수를 사용하지 않는 함수에 대한 포인터 여야합니다.

    (*f[i])()
    
  • 해당 함수의 결과는 포인터이어야합니다.

    *(*f[i])()
    
  • 이러한 포인터는 인수를 사용하지 않는 함수에 대한 포인터 이기도 합니다.

    (*(*f[i])())()
    
  • 해당 함수 포인터는 void

"나선형 규칙"은 동일한 것을 이해하는 다른 방법을 제공하는 니모닉입니다.


그래서이 "나 선식으로 읽는"것은 무엇인가?

나선형 규칙을 적용하거나 cdecl사용 하는 것이 항상 유효한 것은 아닙니다. 어떤 경우에는 둘 다 실패합니다. 나선형 규칙은 많은 경우에 작동하지만 보편적이지 않습니다 .

복잡한 선언을 해독하려면 다음 두 가지 간단한 규칙을 기억하십시오.

  • 항상 내부에서 선언을 읽으십시오 . 가장 괄호로 시작하십시오 (있는 경우). 선언되는 식별자를 찾아서 선언을 해독하십시오.

  • 이 항상 선호, 선택의 여지 경우 []()이상* : 만약 *선행 식별자와 []그것을 다음, 식별자는 배열이 아닌 포인터를 나타냅니다. 마찬가지로, *식별자 앞에 식별자가 오면 식별자 ()는 포인터가 아니라 함수를 나타냅니다. (괄호는 항상 정상적인 우선 순위를 무시하는 데 사용할 수있는 []()이상을 *.)

이 규칙은 실제로 식별자의 한 쪽에서 다른쪽으로 지그재그 를하는 것입니다.

이제 간단한 선언을 해독

int *a[10];

적용 규칙 :

int *a[10];      "a is"  
     ^  

int *a[10];      "a is an array"  
      ^^^^ 

int *a[10];      "a is an array of pointers"
    ^

int *a[10];      "a is an array of pointers to `int`".  
^^^      

다음과 같이 복잡한 선언을 해독합시다

void ( *(*f[]) () ) ();  

위의 규칙을 적용하여 :

void ( *(*f[]) () ) ();        "f is"  
          ^  

void ( *(*f[]) () ) ();        "f is an array"  
           ^^ 

void ( *(*f[]) () ) ();        "f is an array of pointers" 
         ^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function"   
               ^^     

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer"
       ^   

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function" 
                    ^^    

void ( *(*f[]) () ) ();        "f is an array of pointers to function returning pointer to function returning `void`"  
^^^^

다음은 진행 방법을 보여주는 GIF입니다 (더 크게 보려면 이미지를 클릭하십시오).

여기에 이미지 설명을 입력하십시오


여기에 언급 된 규칙은 KN KING의 C Programming A Modern Approach 책에서 발췌 한 것입니다 .


이 선언에서는 각 괄호 안의 각 측면에 한 명의 연산자 만 있기 때문에 "나선형"입니다. "나선형"으로 진행한다고 주장하면 int ***foo[][][]실제로 모든 배열 레벨이 포인터 레벨보다 앞에 올 때 선언에서 배열과 포인터 사이를 번갈아 표시하는 것이 좋습니다 .


이와 같은 구조물이 실제 생활에서 어떤 용도로든 사용될 수 있을지 의문입니다. 나는 심지어 일반 개발자 (컴파일러 작성자에게는 OK)의 인터뷰 질문으로 비난했습니다. 대신 typedef를 사용해야합니다.


As a random trivia factoid, you might find it amusing to know that there's an actual word in English to describe how C declarations are read: Boustrophedonically, that is, alternating right-to-left with left-to-right.

Reference: Van der Linden, 1994 - Page 76


Regarding the usefulness of this, when working with shellcode you see this construct a lot:

int (*ret)() = (int(*)())code;
ret();

While not quite as syntactically complicated, this particular pattern comes up a lot.

More complete example in this SO question.

So while the usefulness to the extent in the original picture is questionable (I would suggest that any production code should be drastically simplified), there are some syntactical constructs that do come up quite a bit.


The declaration

void (*(*f[])())()

is just an obscure way of saying

Function f[]

with

typedef void (*ResultFunction)();

typedef ResultFunction (*Function)();

In practice, more descriptive names will be needed instead of ResultFunction and Function. If possible I would also specify the parameter lists as void.


I found method described by Bruce Eckel to be helpful and easy to follow:

Defining a function pointer

To define a pointer to a function that has no arguments and no return value, you say:

void (*funcPtr)();

When you are looking at a complex definition like this, the best way to attack it is to start in the middle and work your way out. “Starting in the middle” means starting at the variable name, which is funcPtr. “Working your way out” means looking to the right for the nearest item (nothing in this case; the right parenthesis stops you short), then looking to the left (a pointer denoted by the asterisk), then looking to the right (an empty argument list indicating a function that takes no arguments), then looking to the left (void, which indicates the function has no return value). This right-left-right motion works with most declarations.

To review, “start in the middle” (“funcPtr is a ...”), go to the right (nothing there – you're stopped by the right parenthesis), go to the left and find the ‘*’ (“... pointer to a ...”), go to the right and find the empty argument list (“... function that takes no arguments ... ”), go to the left and find the void (“funcPtr is a pointer to a function that takes no arguments and returns void”).

You may wonder why *funcPtr requires parentheses. If you didn't use them, the compiler would see:

void *funcPtr();

You would be declaring a function (that returns a void*) rather than defining a variable. You can think of the compiler as going through the same process you do when it figures out what a declaration or definition is supposed to be. It needs those parentheses to “bump up against” so it goes back to the left and finds the ‘*’, instead of continuing to the right and finding the empty argument list.

Complicated declarations & definitions

As an aside, once you figure out how the C and C++ declaration syntax works you can create much more complicated items. For instance:

//: C03:ComplicatedDefinitions.cpp

/* 1. */     void * (*(*fp1)(int))[10];

/* 2. */     float (*(*fp2)(int,int,float))(int);

/* 3. */     typedef double (*(*(*fp3)())[10])();
             fp3 a;

/* 4. */     int (*(*f4())[10])();


int main() {} ///:~ 

Walk through each one and use the right-left guideline to figure it out. Number 1 says “fp1 is a pointer to a function that takes an integer argument and returns a pointer to an array of 10 void pointers.”

Number 2 says “fp2 is a pointer to a function that takes three arguments (int, int, and float) and returns a pointer to a function that takes an integer argument and returns a float.”

If you are creating a lot of complicated definitions, you might want to use a typedef. Number 3 shows how a typedef saves typing the complicated description every time. It says “An fp3 is a pointer to a function that takes no arguments and returns a pointer to an array of 10 pointers to functions that take no arguments and return doubles.” Then it says “a is one of these fp3 types.” typedef is generally useful for building complicated descriptions from simple ones.

Number 4 is a function declaration instead of a variable definition. It says “f4 is a function that returns a pointer to an array of 10 pointers to functions that return integers.”

You will rarely if ever need such complicated declarations and definitions as these. However, if you go through the exercise of figuring them out you will not even be mildly disturbed with the slightly complicated ones you may encounter in real life.

Taken from: Thinking in C++ Volume 1, second edition, chapter 3, section "Function Addresses" by Bruce Eckel.


Remember these rules for C declares
And precedence never will be in doubt:
Start with the suffix, proceed with the prefix,
And read both sets from the inside, out.
-- me, mid-1980's

Except as modified by parentheses, of course. And note that the syntax for declaring these exactly mirrors the syntax for using that variable to get an instance of the base class.

Seriously, this isn't hard to learn to do at a glance; you just have to be willing to spend some time practising the skill. If you're going to maintain or adapt C code written by other people, it's definitely worth investing that time. It's also a fun party trick for freaking out other programmers who haven't learned it.

For your own code: as always, the fact that something can be written as a one-liner does't mean it should be, unless it is an extremely common pattern that has become a standard idiom (such as the string-copy loop). You, and those who follow you, will be much happier if you build complex types out of layered typedefs and step-by-step dereferences rather than relying on your ability to generate and parse these "at one swell foop." Performance will be just as good, and code readability and maintainability will be tremendously better.

It could be worse, you know. There was a legal PL/I statement that started with something like:

if if if = then then then = else else else = if then ...

  • void (*(*f[]) ()) ()

Resolving void >>

  • (*(*f[]) ()) () = void

Resoiving () >>

  • (*(*f[]) ()) = function returning (void)

Resolving * >>

  • (*f[]) () = pointer to (function returning (void) )

Resolving () >>

  • (*f[]) = function returning (pointer to (function returning (void) ))

Resolving * >>

  • f[] = pointer to (function returning (pointer to (function returning (void) )))

Resolving [ ] >>

  • f = array of (pointer to (function returning (pointer to (function returning (void) ))))

I happen to be the original author of the spiral rule that I wrote oh so many years ago (when I had a lot of hair :) and was honored when it was added to the cfaq.

I wrote the spiral rule as a way to make it easier for my students and colleagues to read the C declarations "in their head"; i.e., without having to use software tools like cdecl.org, etc. It was never my intent to declare that the spiral rule be the canonical way to parse C expressions. I am though, delighted to see that the rule has helped literally thousands of C programming students and practitioners over the years!

For the record,

It has been "correctly" identified numerous times on many sites, including by Linus Torvalds (someone whom I respect immensely), that there are situations where my spiral rule "breaks down". The most common being:

char *ar[10][10];

이 스레드에서 다른 사람들이 지적했듯이 배열을 발견 하면 다음과 같이 작성된 것처럼 모든 인덱스 사용한다고 규칙을 업데이트 할 수 있습니다 .

char *(ar[10][10]);

이제 나선형 규칙에 따라 얻을 수 있습니다.

"ar는 char에 대한 포인터의 10x10 2 차원 배열입니다"

나는 나선형 규칙이 C를 배우는 데 유용성을 갖기를 바랍니다.

추신:

나는 "C는 어렵지 않다"이미지를 좋아한다 :)

참고 URL : https://stackoverflow.com/questions/34548762/c-isnt-that-hard-void-f

반응형