Điểm của con trỏ hàm là gì?


94

Tôi gặp khó khăn khi thấy tiện ích của con trỏ hàm. Tôi đoán nó có thể hữu ích trong một số trường hợp (dù sao thì chúng cũng tồn tại), nhưng tôi không thể nghĩ ra trường hợp nào tốt hơn hoặc không thể tránh khỏi sử dụng con trỏ hàm.

Bạn có thể cho một số ví dụ về việc sử dụng tốt các con trỏ hàm (trong C hoặc C ++) không?


1
Bạn có thể tìm thấy khá nhiều thảo luận về con trỏ hàm trong câu hỏi SO liên quan này .
itmatt

20
@itsmatt: Không hẳn. "TV hoạt động như thế nào?" là một câu hỏi khá khác so với "Tôi phải làm gì với TV?"
sbi

6
Trong C ++, bạn có thể sẽ sử dụng functor ( en.wikipedia.org/wiki/Function_object#In_C_and_C.2B.2B ) để thay thế.
kennytm

11
Trong những ngày đen tối cũ khi C ++ được "biên dịch" sang C, bạn thực sự có thể thấy các phương thức ảo được triển khai như thế nào - đúng vậy, với các con trỏ hàm.
sbk

1
Rất cần thiết khi bạn muốn sử dụng C ++ với C ++ được quản lý hoặc C #, ví dụ: các đại biểu và callbacks
Maher

Câu trả lời:


108

Hầu hết các ví dụ đều có nghĩa là gọi lại : Bạn gọi một hàm f()truyền địa chỉ của một hàm khác g()f()gọi g()cho một số tác vụ cụ thể. Nếu bạn chuyển f()địa chỉ của h()thay vào đó, sau đó f()sẽ gọi lại h()thay thế.

Về cơ bản, đây là một cách để tham số hóa một hàm: Một số phần hành vi của nó không được mã hóa cứng vào f(), mà là hàm gọi lại. Người gọi có thể thực hiện f()hành vi khác nhau bằng cách chuyển các hàm gọi lại khác nhau. Cổ điển là qsort()từ thư viện tiêu chuẩn C lấy tiêu chí sắp xếp của nó làm con trỏ đến một hàm so sánh.

Trong C ++, điều này thường được thực hiện bằng cách sử dụng các đối tượng hàm (còn được gọi là functors). Đây là các đối tượng làm quá tải toán tử gọi hàm, vì vậy bạn có thể gọi chúng như thể chúng là một hàm. Thí dụ:

class functor {
  public:
     void operator()(int i) {std::cout << "the answer is: " << i << '\n';}
};

functor f;
f(42);

Ý tưởng đằng sau điều này là, không giống như một con trỏ hàm, một đối tượng hàm có thể mang không chỉ một thuật toán mà còn mang dữ liệu:

class functor {
  public:
     functor(const std::string& prompt) : prompt_(prompt) {}
     void operator()(int i) {std::cout << prompt_ << i << '\n';}
  private:
     std::string prompt_;
};

functor f("the answer is: ");
f(42);

Một ưu điểm khác là đôi khi việc gọi nội tuyến đến các đối tượng hàm dễ dàng hơn các cuộc gọi thông qua con trỏ hàm. Đây là lý do tại sao sắp xếp trong C ++ đôi khi nhanh hơn sắp xếp trong C.


1
+1, cũng có thể xem câu trả lời này cho một ví dụ khác: stackoverflow.com/questions/1727824/…
sharptooth

Bạn đã quên các hàm ảo, về cơ bản chúng cũng là các con trỏ hàm (cùng với cấu trúc dữ liệu mà trình biên dịch tạo ra). Hơn nữa, trong C thuần túy, bạn có thể tự tạo các cấu trúc này để viết mã hướng đối tượng như được thấy trong lớp VFS (và ở nhiều nơi khác) của nhân Linux.
Florian

2
@krynr: Các hàm ảo là con trỏ hàm chỉ đến những người triển khai trình biên dịch và nếu bạn phải hỏi chúng để làm gì thì có lẽ (hy vọng!) không cần triển khai cơ chế hàm ảo của trình biên dịch.
sbi

@sbi: Tất nhiên là bạn đúng rồi. Tuy nhiên, tôi nghĩ sẽ giúp hiểu được những gì đang xảy ra bên trong phần trừu tượng. Ngoài ra, việc triển khai vtable của riêng bạn trong C và viết mã hướng đối tượng sẽ có một trải nghiệm học tập thực sự tốt.
Florian

điểm đên cho việc cung cấp câu trả lời cho cuộc sống, vũ trụ và tất cả mọi thứ cùng với những gì được hỏi cho bằng OP
simplename

41

Chà, tôi thường sử dụng chúng (chuyên nghiệp) trong bảng nhảy (xem thêm câu hỏi StackOverflow này ).

Bảng nhảy thường được sử dụng (nhưng không riêng) trong các máy trạng thái hữu hạn để biến chúng thành hướng dữ liệu. Thay vì công tắc / trường hợp lồng nhau

  switch (state)
     case A:
       switch (event):
         case e1: ....
         case e2: ....
     case B:
       switch (event):
         case e3: ....
         case e1: ....

bạn có thể tạo một mảng 2d các con trỏ hàm và chỉ cần gọi handleEvent[state][event]


24

Ví dụ:

  1. Phân loại / tìm kiếm tùy chỉnh
  2. Các mẫu khác nhau (như Chiến lược, Người quan sát)
  3. Gọi lại

1
Bàn nhảy là một trong những công dụng quan trọng của nó.
Ashish

Nếu có một số ví dụ hoạt động, điều này sẽ nhận được sự ủng hộ của tôi.
Donal Fellows

1
Chiến lược và trình quan sát có lẽ được triển khai tốt hơn bằng cách sử dụng các hàm ảo, nếu có sẵn C ++. Nếu không thì +1.
Billy ONeal

tôi nghĩ rằng việc sử dụng khôn ngoan của con trỏ chức năng có thể làm cho người quan sát hơn trọng lượng nhỏ gọn và ánh sáng
Andrey

@BillyONeal Chỉ khi bạn kiên định với định nghĩa của GoF, với việc Javaisms của nó bị rò rỉ. Tôi sẽ mô tả std::sort's comptham số như một chiến lược
Caleth

10

Ví dụ "cổ điển" cho tính hữu ích của con trỏ hàm là qsort()hàm thư viện C , thực hiện Sắp xếp nhanh. Để phổ biến cho bất kỳ và tất cả các cấu trúc dữ liệu mà người dùng có thể nghĩ ra, cần có một vài con trỏ void cho dữ liệu có thể sắp xếp và một con trỏ đến một hàm biết cách so sánh hai phần tử của các cấu trúc dữ liệu này. Điều này cho phép chúng tôi tạo ra chức năng lựa chọn của chúng tôi cho công việc và trên thực tế, thậm chí còn cho phép chọn chức năng so sánh tại thời điểm chạy, ví dụ như sắp xếp tăng dần hoặc giảm dần.


7

Đồng ý với tất cả những điều trên, cộng với .... Khi bạn tải động một dll trong thời gian chạy, bạn sẽ cần con trỏ hàm để gọi các hàm.


1
Tôi làm việc này mọi lúc để hỗ trợ Windows XP và vẫn sử dụng các tính năng bổ sung của Windows 7. +1.
Billy ONeal

7

Tôi sẽ đi ngược lại hiện tại ở đây.

Trong C, con trỏ hàm là cách duy nhất để thực hiện tùy chỉnh, vì không có OO.

Trong C ++, bạn có thể sử dụng con trỏ hàm hoặc hàm (đối tượng hàm) cho cùng một kết quả.

Các hàm hàm có một số ưu điểm so với các con trỏ hàm thô, do bản chất đối tượng của chúng, đáng chú ý là:

  • Chúng có thể gây ra một số quá tải operator()
  • Chúng có thể có trạng thái / tham chiếu đến các biến hiện có
  • Chúng có thể được xây dựng tại chỗ ( lambdabind)

Cá nhân tôi thích các bộ chức năng hơn các con trỏ hàm (mặc dù mã viết sẵn), chủ yếu là vì cú pháp cho các con trỏ hàm có thể dễ dàng trở nên phức tạp (từ Hướng dẫn về Con trỏ Hàm ):

typedef float(*pt2Func)(float, float);
  // defines a symbol pt2Func, pointer to a (float, float) -> float function

typedef int (TMyClass::*pt2Member)(float, char, char);
  // defines a symbol pt2Member, pointer to a (float, char, char) -> int function
  // belonging to the class TMyClass

Lần duy nhất tôi từng thấy các con trỏ hàm được sử dụng ở những nơi mà các functors không thể có trong Boost.Spirit. Họ đã hoàn toàn lạm dụng cú pháp để chuyển một số lượng tham số tùy ý làm tham số mẫu duy nhất.

 typedef SpecialClass<float(float,float)> class_type;

Nhưng vì các mẫu đa dạng và lambdas đang ở gần góc, tôi không chắc chúng ta sẽ sử dụng con trỏ hàm trong mã C ++ thuần túy trong thời gian dài.


Chỉ vì bạn không thấy con trỏ hàm của mình không có nghĩa là bạn không sử dụng chúng. Mỗi lần (trừ khi trình biên dịch có thể tối ưu hóa nó đi) bạn gọi một hàm ảo, sử dụng tăng cường bindhoặc functionbạn sử dụng con trỏ hàm. Nó giống như nói rằng chúng tôi không sử dụng con trỏ trong C ++ bởi vì chúng tôi sử dụng con trỏ thông minh. Nhưng dù sao thì, tôi cũng đang chơi.
Florian

3
@krynr: Tôi sẽ lịch sự không đồng ý. Điều quan trọng là những gì bạn thấynhập , đó là cú pháp mà bạn sử dụng. Đối với bạn, tất cả hoạt động như thế nào ở hậu trường không quan trọng: đây là những gì trừu tượng hóa .
Matthieu M.

5

Trong C, cách sử dụng cổ điển là hàm qsort , trong đó tham số thứ tư là con trỏ đến một hàm để sử dụng để thực hiện thứ tự trong sắp xếp. Trong C ++, người ta sẽ có xu hướng sử dụng các hàm (các đối tượng trông giống như các hàm) cho loại điều này.


2
@KennyTM: Tôi đã chỉ ra trường hợp khác duy nhất của điều này trong thư viện tiêu chuẩn C. Các ví dụ bạn trích dẫn là một phần của thư viện bên thứ ba.
Billy ONeal

5

Tôi đã sử dụng con trỏ hàm gần đây để tạo một lớp trừu tượng.

Tôi có một chương trình được viết bằng C thuần túy chạy trên các hệ thống nhúng. Nó hỗ trợ nhiều biến thể phần cứng. Tùy thuộc vào phần cứng tôi đang chạy, nó cần gọi các phiên bản khác nhau của một số chức năng.

Tại thời điểm khởi tạo, chương trình tìm ra phần cứng mà nó đang chạy và điền vào các con trỏ chức năng. Tất cả các quy trình cấp cao hơn trong chương trình chỉ gọi các hàm được tham chiếu bởi con trỏ. Tôi có thể thêm hỗ trợ cho các biến thể phần cứng mới mà không cần chạm vào các quy trình cấp cao hơn.

Tôi đã từng sử dụng các câu lệnh switch / case để chọn các phiên bản chức năng phù hợp, nhưng điều này trở nên không thực tế khi chương trình ngày càng phát triển để hỗ trợ ngày càng nhiều biến thể phần cứng. Tôi đã phải thêm các báo cáo trường hợp khắp nơi.

Tôi cũng đã thử các lớp chức năng trung gian để tìm ra chức năng nào sẽ sử dụng, nhưng chúng không giúp được gì nhiều. Tôi vẫn phải cập nhật các câu lệnh trường hợp ở nhiều nơi bất cứ khi nào chúng tôi thêm một biến thể mới. Với các con trỏ hàm, tôi chỉ phải thay đổi hàm khởi tạo.


3

Giống như Rich đã nói ở trên, con trỏ hàm trong Windows rất bình thường để tham chiếu đến một số địa chỉ lưu trữ hàm.

Khi bạn lập trình C languagetrên nền tảng Windows, về cơ bản, bạn tải một số tệp DLL trong bộ nhớ chính (sử dụng LoadLibrary) và để sử dụng các chức năng được lưu trữ trong DLL, bạn cần tạo các con trỏ hàm và trỏ đến địa chỉ này (sử dụng GetProcAddress).

Người giới thiệu:


2

Con trỏ hàm có thể được sử dụng trong C để tạo ra một giao diện để lập trình. Tùy thuộc vào chức năng cụ thể cần thiết trong thời gian chạy, một triển khai khác có thể được gán cho con trỏ chức năng.


2

Công dụng chính của tôi là CALLBACKS: khi bạn cần lưu thông tin về một hàm để gọi sau .

Giả sử bạn đang viết Bomberman. 5 giây sau khi người này thả bom sẽ phát nổ (gọi explode()hàm).

Bây giờ có 2 cách để làm điều đó. Một cách là "thăm dò" tất cả các quả bom trên màn hình để xem chúng đã sẵn sàng phát nổ trong vòng lặp chính hay chưa.

foreach bomb in game 
   if bomb.boomtime()
       bomb.explode()

Một cách khác là đính kèm một cuộc gọi lại vào hệ thống đồng hồ của bạn. Khi một quả bom được đặt, bạn thêm một lệnh gọi lại để gọi nó là bomb.explode () khi đến đúng thời điểm .

// user placed a bomb
Bomb* bomb = new Bomb()
make callback( function=bomb.explode, time=5 seconds ) ;

// IN the main loop:
foreach callback in callbacks
    if callback.timeToRun
         callback.function()

Đây callback.function()có thể là bất kỳ hàm nào , vì nó là một con trỏ hàm.


Câu hỏi đã được gắn thẻ [C] và [C ++], không phải bất kỳ thẻ ngôn ngữ nào khác. Do đó, việc cung cấp các đoạn mã bằng ngôn ngữ khác là một chút lạc đề.
cmaster - phục hồi monica

2

Sử dụng con trỏ hàm

Để gọi hàm động dựa trên đầu vào của người dùng. Bằng cách tạo một bản đồ chuỗi và con trỏ hàm trong trường hợp này.

#include<iostream>
#include<map>
using namespace std;
//typedef  map<string, int (*)(int x, int y) > funMap;
#define funMap map<string, int (*)(int, int)>
funMap objFunMap;

int Add(int x, int y)
{
    return x+y;
}
int Sub(int x, int y)
{
        return x-y;
}
int Multi(int x, int y)
{
        return x*y;
}
void initializeFunc()
{
        objFunMap["Add"]=Add;
        objFunMap["Sub"]=Sub;
        objFunMap["Multi"]=Multi;
}
int main()
{
    initializeFunc();

    while(1)
    {
        string func;
        cout<<"Enter your choice( 1. Add 2. Sub 3. Multi) : ";
        int no, a, b;
        cin>>no;

        if(no==1)
            func = "Add";
        else if(no==2)
            func = "Sub";
        else if(no==3)
            func = "Multi";
        else 
            break;

        cout<<"\nEnter 2 no :";
                cin>>a>>b;

        //function is called using function pointer based on user input
        //If user input is 2, and a=10, b=3 then below line will expand as "objFuncMap["Sub"](10, 3)"
        int ret = objFunMap[func](a, b);      
        cout<<ret<<endl;
    }
    return 0;
}

Bằng cách này, chúng tôi đã sử dụng con trỏ hàm trong mã công ty thực tế của chúng tôi. Bạn có thể viết số 'n' của hàm và gọi chúng bằng phương pháp này.

ĐẦU RA:

    Nhập lựa chọn của bạn (1. Thêm 2. Phụ 3. Đa): 1
    Nhập 2 không: 2 4
    6
    Nhập lựa chọn của bạn (1. Thêm 2. Phụ 3. Đa): 2
    Nhập 2 không: 10 3
    7
    Nhập lựa chọn của bạn (1. Thêm 2. Phụ 3. Đa): 3
    Nhập 2 không: 3 6
    18

2

Một góc nhìn khác, ngoài những câu trả lời hay khác tại đây:

Trong C, bạn chỉ sử dụng con trỏ hàm, không sử dụng (trực tiếp) hàm.

Ý tôi là, bạn viết các hàm, nhưng bạn không thể thao tác các hàm. Không có biểu diễn thời gian chạy của một hàm mà bạn có thể sử dụng. Bạn thậm chí không thể gọi "một hàm". Khi bạn viết:

my_function(my_arg);

những gì bạn thực sự đang nói là "thực hiện lệnh gọi tới my_functioncon trỏ với đối số được chỉ định". Bạn đang thực hiện cuộc gọi thông qua một con trỏ hàm. Sự phân rã này thành con trỏ hàm có nghĩa là các lệnh sau tương đương với lệnh gọi hàm trước đó:

(&my_function)(my_arg);
(*my_function)(my_arg);
(**my_function)(my_arg);
(&**my_function)(my_arg);
(***my_function)(my_arg);

vân vân (cảm ơn @LuuVinhPhuc).

Vì vậy, bạn đã sử dụng con trỏ hàm làm giá trị . Rõ ràng là bạn muốn có các biến cho các giá trị đó - và đây là nơi tất cả các cách sử dụng metion khác xuất hiện: Đa hình / tùy chỉnh (như trong qsort), lệnh gọi lại, bảng nhảy, v.v.

Trong C ++, mọi thứ phức tạp hơn một chút, vì chúng ta có lambdas và các đối tượng operator(), thậm chí là một std::functionlớp, nhưng nguyên tắc hầu hết vẫn giống nhau.


2
thậm chí nhiều thú vị, bạn có thể gọi hàm như (&my_function)(my_arg), (*my_function)(my_arg), (**my_function)(my_arg), (&**my_function)(my_arg), (***my_function)(my_arg)... bởi vì chức năng phân rã để con trỏ chức năng
phuclv

1

Đối với các ngôn ngữ OO, để thực hiện các lệnh gọi đa hình phía sau hậu trường (điều này cũng có giá trị đối với C cho đến thời điểm nào đó tôi đoán).

Hơn nữa, chúng rất hữu ích để đưa các hành vi khác nhau vào một hàm khác (foo) trong thời gian chạy. Điều đó làm cho hàm foo hàm bậc cao hơn. Bên cạnh tính linh hoạt, điều đó làm cho mã foo dễ đọc hơn vì nó cho phép bạn kéo thêm logic "nếu-khác" ra khỏi nó.

Nó cho phép nhiều thứ hữu ích khác trong Python như trình tạo, bao đóng, v.v.


0

Tôi sử dụng rộng rãi các con trỏ chức năng, để mô phỏng các bộ vi xử lý có mã quang 1 byte. Một mảng 256 con trỏ hàm là cách tự nhiên để thực hiện điều này.


0

Một lần sử dụng con trỏ hàm có thể là nơi chúng ta có thể không muốn sửa đổi mã nơi hàm được gọi (nghĩa là do đó lệnh gọi có thể có điều kiện và trong các điều kiện khác nhau, chúng ta cần thực hiện các loại xử lý khác nhau). Ở đây, các con trỏ hàm rất tiện dụng, vì chúng ta không cần sửa đổi mã tại nơi hàm được gọi. Chúng ta chỉ cần gọi hàm bằng cách sử dụng con trỏ hàm với các đối số thích hợp. Con trỏ hàm có thể được thực hiện để trỏ đến các hàm khác nhau theo điều kiện. (Điều này có thể được thực hiện ở đâu đó trong giai đoạn khởi tạo). Hơn nữa, mô hình trên rất hữu ích, nếu chúng tôi không có đủ khả năng để sửa đổi mã nơi nó được gọi (giả sử đó là một API thư viện, chúng tôi không thể sửa đổi). API sử dụng một con trỏ hàm để gọi hàm thích hợp do người dùng xác định.


0

Tôi sẽ cố gắng đưa ra một danh sách toàn diện ở đây:

  • Gọi lại : Tùy chỉnh một số chức năng (thư viện) với mã do người dùng cung cấp. Ví dụ chính là qsort(), nhưng cũng hữu ích để xử lý các sự kiện (như một nút gọi một cuộc gọi lại khi nó được nhấp vào) hoặc cần thiết để bắt đầu một chuỗi ( pthread_create()).

  • Tính đa hình : Vtable trong một lớp C ++ không là gì ngoài một bảng các con trỏ hàm. Và một chương trình C cũng có thể chọn cung cấp một vtable cho một số đối tượng của nó:

    struct Base;
    struct Base_vtable {
        void (*destruct)(struct Base* me);
    };
    struct Base {
        struct Base_vtable* vtable;
    };
    
    struct Derived;
    struct Derived_vtable {
        struct Base_vtable;
        void (*frobnicate)(struct Derived* me);
    };
    struct Derived {
        struct Base;
        int bar, baz;
    }

    Các nhà xây dựng của Derivedsau đó sẽ thiết lập của nó vtablebiến thành viên cho một đối tượng toàn cầu với việc triển khai các lớp có nguồn gốc của của destructfrobnicate, và mã mà cần thiết để hủy một struct Base*cách đơn giản sẽ gọi base->vtable->destruct(base), mà sẽ gọi phiên bản đúng của destructor, độc lập trong đó có nguồn gốc lớp basethực sự điểm đến .

    Nếu không có con trỏ hàm, tính đa hình sẽ cần được mã hóa bằng một nhóm các cấu trúc chuyển đổi như

    switch(me->type) {
        case TYPE_BASE: base_implementation(); break;
        case TYPE_DERIVED1: derived1_implementation(); break;
        case TYPE_DERIVED2: derived2_implementation(); break;
        case TYPE_DERIVED3: derived3_implementation(); break;
    }

    Điều này trở nên khá khó sử dụng khá nhanh chóng.

  • Mã được tải động : Khi một chương trình tải một mô-đun vào bộ nhớ và cố gắng gọi vào mã của nó, nó phải đi qua một con trỏ hàm.

Tất cả các cách sử dụng con trỏ hàm mà tôi đã thấy đều thuộc một trong ba lớp rộng này.

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.