Hiểu typedefs cho các con trỏ hàm trong C


237

Tôi đã luôn luôn bối rối một chút khi tôi đọc mã của những người khác có typedefs cho con trỏ tới các hàm với các đối số. Tôi nhớ rằng tôi đã mất một thời gian để đi đến một định nghĩa như vậy trong khi cố gắng hiểu một thuật toán số được viết bằng C cách đây một thời gian. Vì vậy, bạn có thể chia sẻ các mẹo và suy nghĩ của mình về cách viết các typedefs tốt cho con trỏ tới các hàm (Do và Do not), tại sao chúng hữu ích và làm thế nào để hiểu công việc của người khác? Cảm ơn!


1
Bạn có thể cung cấp một số ví dụ?
Artelius

2
Bạn không có nghĩa là typedefs cho con trỏ hàm, thay vì macro cho con trỏ hàm? Tôi đã nhìn thấy cái trước nhưng không phải cái sau.
dave4420

Câu trả lời:


296

Hãy xem xét signal()hàm từ tiêu chuẩn C:

extern void (*signal(int, void(*)(int)))(int);

Rõ ràng hoàn toàn rõ ràng - đó là một hàm lấy hai đối số, một số nguyên và một con trỏ tới một hàm lấy một số nguyên làm đối số và không trả về gì, và nó ( signal()) trả về một con trỏ tới một hàm lấy một số nguyên làm đối số và trả về không có gì.

Nếu bạn viết:

typedef void (*SignalHandler)(int signum);

sau đó bạn có thể khai báo signal()là:

extern  SignalHandler signal(int signum, SignalHandler handler);

Điều này có nghĩa là điều tương tự, nhưng thường được coi là hơi dễ đọc hơn. Rõ ràng hơn là hàm lấy một intvà a SignalHandlervà trả về a SignalHandler.

Nó cần một chút để làm quen, mặc dù. Tuy nhiên, một điều bạn không thể làm là viết hàm xử lý tín hiệu bằng cách sử dụng SignalHandler typedefđịnh nghĩa hàm.

Tôi vẫn thuộc trường phái cũ thích gọi một con trỏ hàm là:

(*functionpointer)(arg1, arg2, ...);

Cú pháp hiện đại chỉ sử dụng:

functionpointer(arg1, arg2, ...);

Tôi có thể thấy lý do tại sao điều đó hoạt động - Tôi chỉ muốn biết rằng tôi cần tìm nơi biến được khởi tạo chứ không phải cho một hàm được gọi functionpointer.


Sam nhận xét:

Tôi đã thấy lời giải thích này trước đây. Và sau đó, như trường hợp hiện tại, tôi nghĩ điều tôi không nhận được là sự kết nối giữa hai tuyên bố:

    extern void (*signal(int, void()(int)))(int);  /*and*/

    typedef void (*SignalHandler)(int signum);
    extern SignalHandler signal(int signum, SignalHandler handler);

Hoặc, điều tôi muốn hỏi là, khái niệm cơ bản mà người ta có thể sử dụng để đưa ra phiên bản thứ hai bạn có là gì? Cơ sở kết nối "SignalHandler" và typedef đầu tiên là gì? Tôi nghĩ những gì cần được giải thích ở đây là những gì typedef đang thực sự làm ở đây.

Hãy thử lại lần nữa. Đầu tiên trong số này được nâng thẳng từ tiêu chuẩn C - tôi đã thử lại và kiểm tra xem tôi có đúng dấu ngoặc đơn không (cho đến khi tôi sửa nó - đó là một cookie khó nhớ).

Trước hết, hãy nhớ rằng typedefgiới thiệu một bí danh cho một loại. Vì vậy, bí danh là SignalHandler, và loại của nó là:

một con trỏ tới một hàm lấy một số nguyên làm đối số và không trả về gì.

Phần 'trả về không có gì' được đánh vần void; đối số là một số nguyên là (tôi tin tưởng) tự giải thích. Ký hiệu sau chỉ đơn giản là (hoặc không) cách C đánh vần con trỏ tới hàm lấy các đối số như đã chỉ định và trả về kiểu đã cho:

type (*function)(argtypes);

Sau khi tạo kiểu xử lý tín hiệu, tôi có thể sử dụng nó để khai báo các biến và cứ thế. Ví dụ:

static void alarm_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d)\n", __func__, signum);
}

static void signal_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d) - exiting\n", __func__, signum);
    exit(1);
}

static struct Handlers
{
    int              signum;
    SignalHandler    handler;
} handler[] =
{
    { SIGALRM,   alarm_catcher  },
    { SIGINT,    signal_catcher },
    { SIGQUIT,   signal_catcher },
};

int main(void)
{
    size_t num_handlers = sizeof(handler) / sizeof(handler[0]);
    size_t i;

    for (i = 0; i < num_handlers; i++)
    {
        SignalHandler old_handler = signal(handler[i].signum, SIG_IGN);
        if (old_handler != SIG_IGN)
            old_handler = signal(handler[i].signum, handler[i].handler);
        assert(old_handler == SIG_IGN);
    }

    ...continue with ordinary processing...

    return(EXIT_SUCCESS);
}

Xin lưu ý Làm thế nào để tránh sử dụng printf()trong một bộ xử lý tín hiệu?

Vì vậy, những gì chúng ta đã làm ở đây - ngoài việc bỏ qua 4 tiêu đề cần thiết để làm cho mã được biên dịch sạch sẽ?

Hai hàm đầu tiên là các hàm lấy một số nguyên duy nhất và không trả về gì. Một trong số họ thực sự không quay trở lại nhờ vào exit(1);nhưng người kia sẽ quay lại sau khi in một tin nhắn. Xin lưu ý rằng tiêu chuẩn C không cho phép bạn thực hiện nhiều thao tác xử lý tín hiệu; POSIX hào phóng hơn một chút trong những gì được phép, nhưng chính thức không xử phạt cuộc gọi fprintf(). Tôi cũng in ra số tín hiệu đã nhận được. Trong alarm_handler()hàm, giá trị sẽ luôn luôn SIGALRMlà tín hiệu duy nhất mà nó là một hàm xử lý, nhưng signal_handler()có thể lấy SIGINThoặc SIGQUITlàm số tín hiệu vì cùng một hàm được sử dụng cho cả hai.

Sau đó, tôi tạo một mảng các cấu trúc, trong đó mỗi phần tử xác định một số tín hiệu và trình xử lý sẽ được cài đặt cho tín hiệu đó. Tôi đã chọn lo lắng về 3 tín hiệu; Tôi thường lo lắng SIGHUP, SIGPIPESIGTERMcũng như về việc liệu chúng có được xác định ( #ifdefbiên dịch có điều kiện) không, nhưng điều đó chỉ làm phức tạp mọi thứ. Tôi cũng có thể sử dụng POSIX sigaction()thay vì signal(), nhưng đó là một vấn đề khác; hãy gắn bó với những gì chúng ta bắt đầu.

Các main()lặp chức năng trong danh sách các bộ xử lý được cài đặt. Đối với mỗi trình xử lý, trước tiên, nó gọi signal()để tìm hiểu xem liệu quy trình hiện đang bỏ qua tín hiệu hay không, và trong khi thực hiện, cài đặt SIG_IGNnhư trình xử lý, đảm bảo tín hiệu được bỏ qua. Nếu tín hiệu trước đây không bị bỏ qua, thì nó sẽ gọi signal()lại, lần này để cài đặt trình xử lý tín hiệu ưa thích. (Giá trị còn lại là có lẽ SIG_DFL, xử lý tín hiệu mặc định cho các tín hiệu.) Bởi vì các cuộc gọi đầu tiên 'tín hiệu ()' thiết lập các handler để SIG_IGNsignal()trả về xử lý lỗi trước, giá trị của oldsau khi iftuyên bố phải SIG_IGN- vì thế khẳng định. (Vâng, nó có thể làSIG_ERR nếu có gì đó không ổn - nhưng sau đó tôi sẽ tìm hiểu về điều đó từ việc bắn khẳng định.)

Chương trình sau đó thực hiện công cụ của nó và thoát ra bình thường.

Lưu ý rằng tên của một chức năng có thể được coi là một con trỏ đến một chức năng của loại thích hợp. Khi bạn không áp dụng dấu ngoặc đơn gọi hàm - ví dụ như trong bộ khởi tạo - tên hàm sẽ trở thành một con trỏ hàm. Đây cũng là lý do tại sao việc gọi các hàm thông qua pointertofunction(arg1, arg2)ký hiệu là hợp lý ; Khi bạn nhìn thấy alarm_handler(1), bạn có thể coi đó alarm_handlerlà một con trỏ tới hàm và do đó alarm_handler(1)là một lời gọi của hàm thông qua một con trỏ hàm.

Vì vậy, cho đến nay, tôi đã chỉ ra rằng một SignalHandlerbiến tương đối dễ sử dụng, miễn là bạn có một số loại giá trị phù hợp để gán cho nó - đó là những gì hai hàm xử lý tín hiệu cung cấp.

Bây giờ chúng ta quay trở lại câu hỏi - làm thế nào để hai khai báo signal()liên quan đến nhau.

Hãy xem lại tuyên bố thứ hai:

 extern SignalHandler signal(int signum, SignalHandler handler);

Nếu chúng ta thay đổi tên hàm và kiểu như thế này:

 extern double function(int num1, double num2);

bạn sẽ không gặp vấn đề gì khi diễn giải điều này như là một hàm lấy intdoublelàm đối số và trả về một doublegiá trị (bạn có thể không nên lo lắng nếu điều đó có vấn đề - nhưng có lẽ bạn nên thận trọng khi đặt câu hỏi quá khó như cái này nếu nó là một vấn đề)

Bây giờ, thay vì là một double, signal()hàm lấy một SignalHandlerđối số thứ hai và nó trả về một kết quả như là kết quả của nó.

Các cơ chế mà cũng có thể được coi là:

extern void (*signal(int signum, void(*handler)(int signum)))(int signum);

rất khó để giải thích - vì vậy tôi có thể sẽ làm hỏng nó lên. Lần này tôi đã đưa ra các tên tham số - mặc dù tên không quan trọng.

Nói chung, trong C, cơ chế khai báo là như vậy nếu bạn viết:

type var;

sau đó khi bạn viết varnó đại diện cho một giá trị của cái đã cho type. Ví dụ:

int     i;            // i is an int
int    *ip;           // *ip is an int, so ip is a pointer to an integer
int     abs(int val); // abs(-1) is an int, so abs is a (pointer to a)
                      // function returning an int and taking an int argument

Trong tiêu chuẩn, typedefđược coi là một lớp lưu trữ trong ngữ pháp, thay vì thích staticexternlà các lớp lưu trữ.

typedef void (*SignalHandler)(int signum);

có nghĩa là khi bạn thấy một biến loại SignalHandler(nói alarm_handler) được gọi là:

(*alarm_handler)(-1);

kết quả có type void- không có kết quả. Và (*alarm_handler)(-1);là một lời mời alarm_handler()với tranh luận -1.

Vì vậy, nếu chúng tôi tuyên bố:

extern SignalHandler alt_signal(void);

nó có nghĩa là:

(*alt_signal)();

đại diện cho một giá trị void. Và do đó:

extern void (*alt_signal(void))(int signum);

là tương đương Bây giờ, signal()phức tạp hơn vì nó không chỉ trả về a SignalHandler, mà còn chấp nhận cả int và a SignalHandlerdưới dạng đối số:

extern void (*signal(int signum, SignalHandler handler))(int signum);

extern void (*signal(int signum, void (*handler)(int signum)))(int signum);

Nếu điều đó vẫn làm bạn bối rối, tôi không biết làm cách nào để giúp đỡ - nó vẫn ở một số mức độ bí ẩn đối với tôi, nhưng tôi đã quen với cách nó hoạt động và do đó có thể nói với bạn rằng nếu bạn gắn bó với nó thêm 25 năm nữa hoặc như vậy, nó sẽ trở thành bản chất thứ hai đối với bạn (và thậm chí có thể nhanh hơn một chút nếu bạn khéo léo).


3
Tôi đã thấy lời giải thích này trước đây. Và sau đó, như trường hợp bây giờ, tôi nghĩ cái tôi không nhận được là sự kết nối giữa hai câu lệnh: extern void ( signal (int, void ( ) (int))) (int); / * và * / typedef void (* SignalHandler) (int Signum); tín hiệu bên ngoài SignalHandler (int Signum, bộ xử lý SignalHandler); Hoặc, điều tôi muốn hỏi là, khái niệm cơ bản mà người ta có thể sử dụng để đưa ra phiên bản thứ hai mà bạn có là gì? Cơ sở kết nối "SignalHandler" và typedef đầu tiên là gì? Tôi nghĩ những gì cần được giải thích ở đây là những gì typedef đang thực sự làm ở đây. Thx

6
Câu trả lời tuyệt vời, tôi rất vui vì tôi đã trở lại chủ đề này. Tôi không nghĩ rằng tôi hiểu tất cả mọi thứ, nhưng một ngày nào đó tôi sẽ làm được. Đây là lý do tại sao tôi thích SO. Cảm ơn bạn.
toto

2
Chỉ cần chọn một nit: không an toàn khi gọi printf () và bạn bè bên trong bộ xử lý tín hiệu; printf () không được đăng ký lại (về cơ bản vì nó có thể gọi malloc (), không phải là reentrant)
wildplasser

4
extern void (*signal(int, void(*)(int)))(int);nghĩa là signal(int, void(*)(int))hàm sẽ trả về một con trỏ hàm tới void f(int). Khi bạn muốn chỉ định một con trỏ hàm làm giá trị trả về , cú pháp trở nên phức tạp. Bạn phải đặt loại giá trị trả về ở bên trái và danh sách đối số ở bên phải , trong khi đó là giữa mà bạn đang xác định. Và trong trường hợp này, signal()chính hàm lấy một con trỏ hàm làm tham số của nó, điều này làm phức tạp mọi thứ hơn nữa. Tin tốt là, nếu bạn có thể đọc cái này, Lực lượng đã sẵn sàng với bạn. :).
smwikipedia

1
Trường học cũ về việc sử dụng &trước tên hàm là gì? Nó hoàn toàn không cần thiết; vô nghĩa, thậm chí Và chắc chắn không phải "trường cũ". Trường học cũ sử dụng một tên chức năng đơn giản và đơn giản.
Jonathan Leffler

80

Một con trỏ hàm giống như bất kỳ con trỏ nào khác, nhưng nó trỏ đến địa chỉ của hàm thay vì địa chỉ dữ liệu (trên heap hoặc stack). Giống như bất kỳ con trỏ, nó cần phải được gõ chính xác. Các hàm được xác định bởi giá trị trả về của chúng và các loại tham số mà chúng chấp nhận. Vì vậy, để mô tả đầy đủ một hàm, bạn phải bao gồm giá trị trả về của nó và loại của từng tham số được chấp nhận. Khi bạn gõ một định nghĩa như vậy, bạn đặt cho nó một 'tên thân thiện' để giúp tạo và tham chiếu con trỏ dễ dàng hơn bằng cách sử dụng định nghĩa đó.

Vì vậy, ví dụ giả sử bạn có một chức năng:

float doMultiplication (float num1, float num2 ) {
    return num1 * num2; }

sau đó typedef sau:

typedef float(*pt2Func)(float, float);

có thể được sử dụng để trỏ đến doMulitplicationchức năng này . Nó chỉ đơn giản là xác định một con trỏ tới một hàm trả về một float và nhận hai tham số, mỗi tham số kiểu float. Định nghĩa này có tên thân thiện pt2Func. Lưu ý rằng pt2Funccó thể trỏ đến bất kỳ chức năng nào trả về một float và mất trong 2 float.

Vì vậy, bạn có thể tạo một con trỏ trỏ đến hàm doMultiplication như sau:

pt2Func *myFnPtr = &doMultiplication;

và bạn có thể gọi hàm bằng con trỏ này như sau:

float result = (*myFnPtr)(2.0, 5.1);

Điều này làm cho việc đọc tốt: http://www.newty.de/fpt/index.html


psychotik, cảm ơn! Điều đó rất hữu ích. Các liên kết đến trang web con trỏ chức năng là thực sự hữu ích. Đọc nó bây giờ.

... Tuy nhiên, liên kết newty.de đó dường như không nói về typedefs :( Vì vậy, mặc dù liên kết đó rất tuyệt, nhưng phản hồi trong chủ đề này về typedefs là vô giá!

11
Bạn có thể muốn làm pt2Func myFnPtr = &doMultiplication;thay vì pt2Func *myFnPtr = &doMultiplication;như myFnPtrđã là một con trỏ.
Tamilselvan

1
khai báo pt2Func * myFnPtr = & doMultiplication; thay vì pt2Func myFnPtr = & doMultiplication; ném một cảnh báo.
AlphaGoku

2
@Tamilselvan là chính xác. myFunPtrđã là một con trỏ hàm nên sử dụngpt2Func myFnPtr = &doMultiplication;
Dustin Biser

35

Một cách rất dễ hiểu typedef của con trỏ hàm:

int add(int a, int b)
{
    return (a+b);
}

typedef int (*add_integer)(int, int); //declaration of function pointer

int main()
{
    add_integer addition = add; //typedef assigns a new variable i.e. "addition" to original function "add"
    int c = addition(11, 11);   //calling function via new variable
    printf("%d",c);
    return 0;
}

32

cdecllà một công cụ tuyệt vời để giải mã cú pháp kỳ lạ như khai báo con trỏ hàm. Bạn có thể sử dụng nó để tạo ra chúng là tốt.

Theo như các mẹo để thực hiện các khai báo phức tạp dễ dàng phân tích cú pháp hơn để bảo trì trong tương lai (bởi chính bạn hoặc người khác), tôi khuyên bạn nên tạo typedefcác phần nhỏ và sử dụng các phần nhỏ đó làm các khối xây dựng cho các biểu thức lớn hơn và phức tạp hơn. Ví dụ:

typedef int (*FUNC_TYPE_1)(void);
typedef double (*FUNC_TYPE_2)(void);
typedef FUNC_TYPE_1 (*FUNC_TYPE_3)(FUNC_TYPE_2);

thay vì:

typedef int (*(*FUNC_TYPE_3)(double (*)(void)))(void);

cdecl có thể giúp bạn ra ngoài với những thứ này:

cdecl> explain int (*FUNC_TYPE_1)(void)
declare FUNC_TYPE_1 as pointer to function (void) returning int
cdecl> explain double (*FUNC_TYPE_2)(void)
declare FUNC_TYPE_2 as pointer to function (void) returning double
cdecl> declare FUNC_TYPE_3 as pointer to function (pointer to function (void) returning double) returning pointer to function (void) returning int
int (*(*FUNC_TYPE_3)(double (*)(void )))(void )

Và (trên thực tế) chính xác là cách tôi tạo ra mớ hỗn độn điên rồ đó ở trên.


2
Xin chào Carl, đó là một ví dụ và giải thích rất sâu sắc. Ngoài ra, cảm ơn vì đã cho thấy việc sử dụng cdecl. Nhiều đánh giá cao.

Có cdecl cho windows?
Jack

@Jack, tôi chắc chắn bạn có thể xây dựng nó, vâng.
Carl Norum

2
Ngoài ra còn có cdecl.org cung cấp loại khả năng tương tự nhưng trực tuyến. Hữu ích cho chúng tôi các nhà phát triển Windows.
zaknotzach

12
int add(int a, int b)
{
  return (a+b);
}
int minus(int a, int b)
{
  return (a-b);
}

typedef int (*math_func)(int, int); //declaration of function pointer

int main()
{
  math_func addition = add;  //typedef assigns a new variable i.e. "addition" to original function "add"
  math_func substract = minus; //typedef assigns a new variable i.e. "substract" to original function "minus"

  int c = addition(11, 11);   //calling function via new variable
  printf("%d\n",c);
  c = substract(11, 5);   //calling function via new variable
  printf("%d",c);
  return 0;
}

Đầu ra của nó là:

22

6

Lưu ý rằng, cùng một definer math_func đã được sử dụng để khai báo cả hai hàm.

Cách tiếp cận tương tự của typedef có thể được sử dụng cho cấu trúc bên ngoài. (Sử dụng sturuct trong tệp khác.)


5

Sử dụng typedefs để xác định các loại phức tạp hơn, ví dụ như con trỏ hàm

Tôi sẽ lấy ví dụ về việc xác định máy trạng thái trong C

    typedef  int (*action_handler_t)(void *ctx, void *data);

bây giờ chúng ta đã định nghĩa một kiểu gọi là action_handler có hai con trỏ và trả về một int

xác định máy trạng thái của bạn

    typedef struct
    {
      state_t curr_state;   /* Enum for the Current state */
      event_t event;  /* Enum for the event */
      state_t next_state;   /* Enum for the next state */
      action_handler_t event_handler; /* Function-pointer to the action */

     }state_element;

Con trỏ hàm cho hành động trông giống như một loại đơn giản và typedef chủ yếu phục vụ mục đích này.

Tất cả các trình xử lý sự kiện của tôi bây giờ phải tuân thủ loại được xác định bởi action_handler

    int handle_event_a(void *fsm_ctx, void *in_msg );

    int handle_event_b(void *fsm_ctx, void *in_msg );

Người giới thiệu:

Chuyên gia lập trình C của Linden


4

Đây là ví dụ đơn giản nhất về con trỏ hàm và mảng con trỏ hàm mà tôi đã viết dưới dạng bài tập.

    typedef double (*pf)(double x);  /*this defines a type pf */

    double f1(double x) { return(x+x);}
    double f2(double x) { return(x*x);}

    pf pa[] = {f1, f2};


    main()
    {
        pf p;

        p = pa[0];
        printf("%f\n", p(3.0));
        p = pa[1];
        printf("%f\n", p(3.0));
    }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.