Làm thế nào để con trỏ hàm trong C làm việc?


1233

Tôi đã có một số kinh nghiệm gần đây với các con trỏ hàm trong C.

Vì vậy, tiếp tục với truyền thống trả lời các câu hỏi của riêng bạn, tôi quyết định làm một bản tóm tắt nhỏ về những điều cơ bản, cho những người cần nhanh chóng tìm hiểu sâu về chủ đề này.


35
Ngoài ra: Để có một chút phân tích chuyên sâu về con trỏ C, hãy xem blog.oracle.com/ksplice/entry/the_ksplice_pulum_challenge . Ngoài ra, Lập trình từ Ground Up cho thấy cách chúng hoạt động ở cấp độ máy. Hiểu "mô hình bộ nhớ" của C rất hữu ích để hiểu cách con trỏ C hoạt động.
Abbafei

8
Thông tin tuyệt vời. Tuy nhiên, theo tiêu đề, tôi dự kiến ​​sẽ thực sự thấy một lời giải thích về cách "con trỏ hàm hoạt động", chứ không phải cách chúng được mã hóa :)
Bogdan Alexandru

Câu trả lời:


1478

Hàm con trỏ trong C

Hãy bắt đầu với một chức năng cơ bản mà chúng ta sẽ chỉ đến :

int addInt(int n, int m) {
    return n+m;
}

Điều đầu tiên, hãy xác định một con trỏ tới một hàm nhận 2 ints và trả về int:

int (*functionPtr)(int,int);

Bây giờ chúng ta có thể trỏ đến chức năng của mình một cách an toàn:

functionPtr = &addInt;

Bây giờ chúng ta có một con trỏ tới hàm, hãy sử dụng nó:

int sum = (*functionPtr)(2, 3); // sum == 5

Truyền con trỏ đến một chức năng khác về cơ bản là giống nhau:

int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}

Chúng ta cũng có thể sử dụng các con trỏ hàm trong các giá trị trả về (cố gắng theo kịp, nó sẽ bị lộn xộn):

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

Nhưng nó đẹp hơn nhiều khi sử dụng 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
Cảm ơn về thông tin tuyệt vời. Bạn có thể thêm một số cái nhìn sâu sắc về nơi con trỏ chức năng được sử dụng hoặc tình cờ là đặc biệt hữu ích?
Rich.Carpenter

327
"functionPtr = & addInt;" cũng có thể được viết (và thường là) là "functionPtr = addInt;" cũng hợp lệ vì tiêu chuẩn nói rằng tên hàm trong ngữ cảnh này được chuyển đổi thành địa chỉ của hàm.
hlovdal

22
hlovdal, trong bối cảnh này, thật thú vị để giải thích rằng đây là điều cho phép người ta viết hàmPtr = ****** / TÌM HIỂU;
Julian Schaub - litb

105
@ Rich.Carpenter Tôi biết rằng đây là 4 năm quá muộn, nhưng tôi cho rằng những người khác có thể được hưởng lợi từ điều này: Con trỏ hàm rất hữu ích để truyền các hàm làm tham số cho các hàm khác . Phải mất rất nhiều tìm kiếm cho tôi để tìm câu trả lời cho một số lý do kỳ lạ. Về cơ bản, nó cung cấp cho C chức năng giả hạng nhất.
giant91

22
@ Rich.Carpenter: con trỏ chức năng rất tốt để phát hiện CPU thời gian chạy. Có nhiều phiên bản của một số chức năng để tận dụng SSE, popcnt, AVX, v.v. Khi khởi động, hãy đặt con trỏ chức năng của bạn thành phiên bản tốt nhất của từng chức năng cho CPU hiện tại. Trong mã khác của bạn, chỉ cần gọi qua con trỏ hàm thay vì có các nhánh có điều kiện trên các tính năng CPU ở mọi nơi. Sau đó, bạn có thể thực hiện logic phức tạp về việc quyết định tốt điều đó, mặc dù CPU này hỗ trợ pshufb, nhưng nó chậm, vì vậy việc thực hiện trước đó vẫn nhanh hơn. x264 / x265 sử dụng rộng rãi và là nguồn mở.
Peter Cordes

304

Các con trỏ hàm trong C có thể được sử dụng để thực hiện lập trình hướng đối tượng trong C.

Ví dụ: các dòng sau được viết bằng C:

String s1 = newString();
s1->set(s1, "hello");

Đúng, việc ->thiếu và thiếu một newtoán tử là một sự cho đi đã chết, nhưng dường như chắc chắn ngụ ý rằng chúng ta đang thiết lập văn bản của một số Stringlớp "hello".

Bằng cách sử dụng con trỏ hàm, nó có thể bắt chước phương pháp trong C .

Làm thế nào là điều này được thực hiện?

Các Stringlớp học thực sự là một structvới một loạt các chức năng gợi ý mà hành động như một cách để các phương pháp mô phỏng. Sau đây là một tuyên bố một phần của Stringlớp:

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

Có thể thấy, các phương thức của Stringlớp thực sự là các con trỏ hàm cho hàm khai báo. Khi chuẩn bị thể hiện của hàm String, newStringhàm được gọi để thiết lập các con trỏ hàm cho các hàm tương ứng của chúng:

String newString()
{
    String self = (String)malloc(sizeof(struct String_Struct));

    self->get = &getString;
    self->set = &setString;
    self->length = &lengthString;

    self->set(self, "");

    return self;
}

Ví dụ, getStringhàm được gọi bằng cách gọi getphương thức được định nghĩa như sau:

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}

Một điều có thể được chú ý là không có khái niệm về một thể hiện của một đối tượng và có các phương thức thực sự là một phần của một đối tượng, do đó, một "đối tượng tự" phải được truyền vào trong mỗi lần gọi. (Và internalđây chỉ là một ẩn structđược bỏ qua khỏi danh sách mã trước đó - đó là một cách thực hiện ẩn thông tin, nhưng điều đó không liên quan đến các con trỏ hàm.)

Vì vậy, thay vì có thể làm s1->set("hello");, người ta phải truyền vào đối tượng để thực hiện hành động trên s1->set(s1, "hello").

Cùng với đó nhỏ giải thích cần phải vượt qua trong một tham chiếu đến chính mình ra khỏi đường đi, chúng tôi sẽ chuyển sang phần tiếp theo, đó là thừa kế trong C .

Hãy nói rằng chúng tôi muốn tạo một lớp con của String, nói một ImmutableString. Để thực hiện chuỗi bất biến, các setphương pháp sẽ không thể truy cập, trong khi duy trì quyền truy cập vào getlength, và buộc các "nhà xây dựng" để chấp nhận một char*:

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

Về cơ bản, đối với tất cả các lớp con, các phương thức có sẵn là một lần nữa hàm con trỏ hàm. Lần này, khai báo cho setphương thức không có mặt, do đó, nó không thể được gọi trong a ImmutableString.

Đối với việc thực hiện ImmutableString, mã chỉ có liên quan là "nhà xây dựng" chức năng, các 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;
}

Trong việc khởi tạo ImmutableString, các hàm con trỏ tới getlengthcác phương thức thực sự tham chiếu đến phương thức String.getString.length, bằng cách đi qua basebiến là một Stringđối tượng được lưu trữ bên trong .

Việc sử dụng một con trỏ hàm có thể đạt được sự kế thừa của một phương thức từ một siêu lớp.

Chúng tôi cũng có thể tiếp tục đa hình trong C .

Ví dụ, nếu chúng ta muốn thay đổi hành vi của lengthphương thức để trả lại 0toàn bộ thời gian trong ImmutableStringlớp vì một số lý do, tất cả những gì sẽ phải được thực hiện là:

  1. Thêm một chức năng sẽ phục vụ như là lengthphương thức ghi đè .
  2. Đi đến "hàm tạo" và đặt con trỏ hàm thành lengthphương thức ghi đè .

Thêm một lengthphương thức ghi đè trong ImmutableStringcó thể được thực hiện bằng cách thêm lengthOverrideMethod:

int lengthOverrideMethod(const void* self)
{
    return 0;
}

Sau đó, con trỏ hàm cho lengthphương thức trong hàm tạo được nối với 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;
}

Bây giờ, thay vì có một hành vi giống hệt nhau cho lengthphương thức trong ImmutableStringlớp là Stringlớp, bây giờ lengthphương thức sẽ đề cập đến hành vi được định nghĩa trong lengthOverrideMethodhàm.

Tôi phải thêm từ chối trách nhiệm rằng tôi vẫn đang học cách viết với phong cách lập trình hướng đối tượng trong C, vì vậy có thể có những điểm mà tôi không giải thích rõ, hoặc có thể bị lạc hướng về cách thực hiện OOP tốt nhất trong C. Nhưng mục đích của tôi là cố gắng minh họa một trong nhiều cách sử dụng con trỏ hàm.

Để biết thêm thông tin về cách thực hiện lập trình hướng đối tượng trong C, vui lòng tham khảo các câu hỏi sau:


22
Câu trả lời này thật kinh khủng! Không chỉ nó ngụ ý rằng OO bằng cách nào đó phụ thuộc vào ký hiệu dấu chấm, nó cũng khuyến khích đưa rác vào các đối tượng của bạn!
Alexei Averchenko

27
Đây là OO, nhưng không phải bất cứ nơi nào gần OO kiểu C. Những gì bạn đã thực hiện một cách đột ngột là OO dựa trên nguyên mẫu kiểu Javascript. Để có OO kiểu C ++ / Pascal, bạn cần phải: 1. Có cấu trúc const cho một bảng ảo của mỗi lớp với các thành viên ảo. 2. Có con trỏ tới cấu trúc đó trong các đối tượng đa hình. 3. Gọi các phương thức ảo thông qua bảng ảo và tất cả các phương thức khác trực tiếp - thường bằng cách tuân theo một số ClassName_methodNamequy ước đặt tên hàm. Chỉ sau đó bạn mới có được thời gian chạy và chi phí lưu trữ giống như bạn làm trong C ++ và Pascal.
Tái lập lại

19
Làm việc OO với một ngôn ngữ không có ý định là OO luôn là một ý tưởng tồi. Nếu bạn muốn OO và vẫn có C, hãy làm việc với C ++.
rbaleksandar

20
@rbaleksandar Hãy nói điều đó với các nhà phát triển nhân Linux. "Luôn luôn là một ý tưởng tồi" hoàn toàn là ý kiến ​​của bạn, mà tôi không đồng ý.
Jonathon Reinhart

6
Tôi thích câu trả lời này nhưng không chọn malloc
mèo

227

Hướng dẫn để bị sa thải: Cách lạm dụng các con trỏ hàm trong GCC trên các máy x86 bằng cách biên dịch mã của bạn bằng tay:

Các chuỗi ký tự này là byte của mã máy 32 bit x86. 0xC3một rethướng dẫn x86 .

Thông thường bạn sẽ không viết chúng bằng tay, bạn sẽ viết bằng ngôn ngữ lắp ráp và sau đó sử dụng một trình biên dịch hợp ngữ nasmđể lắp ráp nó thành một tệp nhị phân phẳng mà bạn viết thành chuỗi chữ C.

  1. Trả về giá trị hiện tại trên thanh ghi EAX

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. Viết hàm hoán đổi

    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. Viết bộ đếm vòng lặp tới 1000, gọi một số hàm mỗi lần

    ((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. Bạn thậm chí có thể viết một hàm đệ quy lên tới 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);
    

Lưu ý rằng trình biên dịch đặt chuỗi ký tự trong .rodataphần (hoặc .rdatatrên Windows), được liên kết như một phần của đoạn văn bản (cùng với mã cho các hàm).

Các đoạn văn bản có đọc + Exec phép, vì vậy đúc xâu để con trỏ chức năng hoạt động mà không cần mprotect()hoặc VirtualProtect()hệ thống các cuộc gọi như bạn cần cho bộ nhớ cấp phát động. (Hoặc gcc -z execstackliên kết chương trình với ngăn xếp + phân đoạn dữ liệu + thực thi heap, như một bản hack nhanh.)


Để phân tách chúng, bạn có thể biên dịch cái này để đặt nhãn trên byte và sử dụng trình phân tách.

// 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";

Biên dịch gcc -c -m32 foo.cvà phân tách với objdump -D -rwC -Mintel, chúng ta có thể nhận được hội đồng và phát hiện ra rằng mã này vi phạm ABI bằng cách ghi đè EBX (một thanh ghi được bảo toàn cuộc gọi) và thường không hiệu quả.

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

Mã máy này (có thể) sẽ hoạt động với mã 32 bit trên Windows, Linux, OS X, v.v .: các quy ước gọi mặc định trên tất cả các hệ điều hành đó chuyển các đối số trên ngăn xếp thay vì hiệu quả hơn trong các thanh ghi. Nhưng EBX được bảo toàn cuộc gọi trong tất cả các quy ước gọi thông thường, do đó, sử dụng nó như một thanh ghi đầu mà không lưu / khôi phục nó có thể dễ dàng khiến người gọi gặp sự cố.


8
Lưu ý: điều này không hoạt động nếu Ngăn chặn thực thi dữ liệu được bật (ví dụ: trên Windows XP SP2 +), vì các chuỗi C thường không được đánh dấu là có thể thực thi được.
SecurityMatt

5
Chào Matt! Tùy thuộc vào mức độ tối ưu hóa, GCC sẽ thường đặt các chuỗi nội tuyến vào phân đoạn TEXT, do đó, điều này sẽ hoạt động ngay cả trên phiên bản mới hơn của cửa sổ với điều kiện bạn không cho phép loại tối ưu hóa này. (IIRC, phiên bản MINGW tại thời điểm bài viết của tôi hơn hai năm trước, inline chuỗi ký tự ở mức tối ưu hóa mặc định)
Lee

10
ai đó có thể vui lòng giải thích những gì đang xảy ra ở đây? Những chuỗi tìm kiếm kỳ lạ đó là gì?
Ajay

56
@ajay Có vẻ như anh ta đang viết các giá trị thập lục phân thô (ví dụ '\ x00' giống như '/ 0', cả hai đều bằng 0) thành một chuỗi, sau đó chuyển chuỗi thành con trỏ hàm C, sau đó thực hiện con trỏ hàm C vì anh ta là ác quỷ.
ejk314

3
xin chào FUZxxl, tôi nghĩ nó có thể thay đổi dựa trên trình biên dịch và phiên bản hệ điều hành. Đoạn mã trên dường như chạy tốt trên codepad.org; codepad.org/FMSDQ3ME
Lee

115

Một trong những cách sử dụng yêu thích của tôi cho các con trỏ hàm là các trình vòng lặp rẻ và dễ dàng -

#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
Bạn cũng nên chuyển một con trỏ tới dữ liệu do người dùng chỉ định nếu bạn muốn trích xuất bất kỳ đầu ra nào từ các lần lặp (nghĩ rằng đóng).
Alexei Averchenko

1
Đã đồng ý. Tất cả các trình lặp của tôi trông như thế này : int (*cb)(void *arg, ...). Giá trị trả về của iterator cũng cho phép tôi dừng sớm (nếu không khác).
Jonathon Reinhart

24

Các con trỏ hàm trở nên dễ khai báo khi bạn có các khai báo cơ bản:

  • id: ID: ID là
  • Con trỏ: *D: D con trỏ đến
  • Chức năng: D(<parameters>): D chức năng lấy <thông số >trở về

Trong khi D là một công cụ khai báo khác được xây dựng bằng các quy tắc tương tự. Cuối cùng, ở đâu đó, nó kết thúc bằng ID(xem ví dụ bên dưới), đó là tên của thực thể được khai báo. Chúng ta hãy thử xây dựng một hàm lấy một con trỏ tới một hàm không lấy gì và trả về int và trả về một con trỏ cho một hàm lấy char và trả về int. Với kiểu defs nó như thế này

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

Như bạn thấy, thật dễ dàng để xây dựng nó bằng cách sử dụng typedefs. Không có typedefs, nó cũng không khó với các quy tắc khai báo ở trên, được áp dụng nhất quán. Như bạn thấy tôi đã bỏ lỡ phần con trỏ trỏ tới, và điều mà hàm trả về. Đó là những gì xuất hiện ở bên trái của tờ khai, và không được quan tâm: Nó được thêm vào cuối nếu đã xây dựng trình khai báo. Hãy làm điều đó. Xây dựng nó một cách nhất quán, từ đầu tiên - hiển thị cấu trúc bằng cách sử dụng []:

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

Như bạn thấy, người ta có thể mô tả một loại hoàn toàn bằng cách nối thêm các khai báo từng cái một. Xây dựng có thể được thực hiện theo hai cách. Một là từ dưới lên, bắt đầu với điều rất đúng (lá) và làm việc theo cách đến định danh. Cách khác là từ trên xuống, bắt đầu từ định danh, làm việc theo cách xuống lá. Tôi sẽ chỉ cả hai cách.

Từ dưới lên

Xây dựng bắt đầu với điều ở bên phải: Điều trả lại, đó là chức năng lấy char. Để giữ cho người khai báo khác biệt, tôi sẽ đánh số chúng:

D1(char);

Chèn tham số char trực tiếp, vì nó tầm thường. Thêm một con trỏ vào khai báo bằng cách thay thế D1bởi *D2. Lưu ý rằng chúng ta phải bọc dấu ngoặc đơn xung quanh *D2. Điều đó có thể được biết bằng cách tra cứu quyền ưu tiên của toán tử *-operatorvà hàm gọi (). Không có dấu ngoặc đơn của chúng tôi, trình biên dịch sẽ đọc nó dưới dạng *(D2(char p)). Nhưng đó sẽ không còn là sự thay thế đơn giản của D1 *D2nữa. Dấu ngoặc luôn được cho phép xung quanh người khai báo. Vì vậy, bạn không làm bất cứ điều gì sai nếu bạn thêm quá nhiều trong số họ, thực sự.

(*D2)(char);

Kiểu trả về đã hoàn tất! Bây giờ, chúng ta hãy thay thế D2bằng hàm khai báo hàm đang <parameters>trả về , đó là D3(<parameters>)cái mà chúng ta đang ở hiện tại.

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

Lưu ý rằng không cần dấu ngoặc đơn, vì chúng ta muốn D3 trở thành một hàm khai báo hàm và không phải là một khai báo con trỏ lần này. Tuyệt vời, điều duy nhất còn lại là các thông số cho nó. Tham số được thực hiện chính xác giống như chúng ta đã thực hiện kiểu trả về, chỉ cần charthay thế bằng void. Vì vậy, tôi sẽ sao chép nó:

(*D3(   (*ID1)(void)))(char)

Tôi đã thay thế D2bởi ID1vì chúng ta đã hoàn thành với tham số đó (nó đã là một con trỏ tới một hàm - không cần một người khai báo khác). ID1sẽ là tên của tham số. Bây giờ, tôi đã nói ở trên ở phần cuối thêm một kiểu mà tất cả những người khai báo đó sửa đổi - kiểu xuất hiện ở bên trái của mỗi khai báo. Đối với các hàm, đó trở thành kiểu trả về. Đối với con trỏ được chỉ vào loại v.v ... Thật thú vị khi viết ra loại, nó sẽ xuất hiện theo thứ tự ngược lại, ở bên phải :) Dù sao, thay thế nó sẽ mang lại tuyên bố hoàn chỉnh. Cả hai lần inttất nhiên.

int (*ID0(int (*ID1)(void)))(char)

Tôi đã gọi định danh của hàm ID0trong ví dụ đó.

Từ trên xuống

Điều này bắt đầu từ mã định danh ở bên trái trong phần mô tả loại, bao bọc người khai báo khi chúng ta đi qua bên phải. Bắt đầu với chức năng lấy <tham số >trả về

ID0(<parameters>)

Điều tiếp theo trong mô tả (sau khi "trở về") là con trỏ tới . Hãy kết hợp nó:

*ID0(<parameters>)

Sau đó, điều tiếp theo là functon lấy <tham số >trở lại . Tham số này là một char đơn giản, vì vậy chúng tôi đưa nó vào ngay lập tức, vì nó thực sự tầm thường.

(*ID0(<parameters>))(char)

Lưu ý các dấu ngoặc đơn mà chúng ta đã thêm, vì chúng ta lại muốn các *liên kết đó trước, và sau đó(char). Nếu không nó sẽ đọc chức năng lấy <thông số >chức năng quay trở lại ... . Noes, chức năng trả về chức năng thậm chí không được phép.

Bây giờ chúng ta chỉ cần đặt <tham số >. Tôi sẽ chỉ ra một phiên bản ngắn của sự biến đổi, vì tôi nghĩ bây giờ bạn đã có ý tưởng làm thế nào để làm điều đó.

pointer to: *ID1
... function taking void returning: (*ID1)(void)

Chỉ cần đặt inttrước những người khai báo như chúng tôi đã làm với từ dưới lên, và chúng tôi đã hoàn thành

int (*ID0(int (*ID1)(void)))(char)

Điều tốt đẹp

Là từ dưới lên hoặc từ trên xuống tốt hơn? Tôi đã quen với việc từ dưới lên, nhưng một số người có thể thoải mái hơn khi từ trên xuống. Đó là vấn đề của hương vị tôi nghĩ. Ngẫu nhiên, nếu bạn áp dụng tất cả các toán tử trong khai báo đó, cuối cùng bạn sẽ nhận được một int:

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

Đó là một thuộc tính đẹp của các khai báo trong C: Khai báo khẳng định rằng nếu các toán tử đó được sử dụng trong một biểu thức bằng cách sử dụng định danh, thì nó sẽ mang lại kiểu bên trái. Nó cũng giống như vậy cho các mảng.

Hy vọng bạn thích hướng dẫn nhỏ này! Bây giờ chúng ta có thể liên kết đến điều này khi mọi người thắc mắc về cú pháp khai báo lạ của các hàm. Tôi đã cố gắng đặt càng ít C bên trong càng tốt. Hãy chỉnh sửa / sửa chữa những thứ trong đó.


24

Một cách sử dụng tốt khác cho con trỏ hàm:
Chuyển đổi giữa các phiên bản không đau

Chúng rất tiện dụng để sử dụng khi bạn muốn các chức năng khác nhau vào các thời điểm khác nhau hoặc các giai đoạn phát triển khác nhau. Chẳng hạn, tôi đang phát triển một ứng dụng trên máy tính chủ có bàn điều khiển, nhưng bản phát hành phần mềm cuối cùng sẽ được đưa lên Avnet ZedBoard (có cổng để hiển thị và bảng điều khiển, nhưng chúng không cần / muốn cho phát hành cuối cùng). Vì vậy, trong quá trình phát triển, tôi sẽ sử dụng printfđể xem trạng thái và thông báo lỗi, nhưng khi tôi hoàn thành, tôi không muốn bất cứ điều gì được in. Đây là những gì tôi đã làm:

phiên bản

// 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

Trong version.ctôi sẽ định nghĩa 2 nguyên mẫu hàm có trongversion.h

phiên bản.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;
}

Lưu ý cách con trỏ hàm được tạo nguyên mẫu version.hnhư

void (* zprintf)(const char *, ...);

Khi nó được tham chiếu trong ứng dụng, nó sẽ bắt đầu thực thi bất cứ nơi nào nó trỏ đến, điều này vẫn chưa được xác định.

Trong version.c, thông báo trong board_init()hàm zprintfđược gán một hàm duy nhất (có chữ ký hàm khớp với) tùy thuộc vào phiên bản được xác định trongversion.h

zprintf = &printf; zprintf gọi printf cho mục đích gỡ lỗi

hoặc là

zprintf = &noprint; zprintf chỉ trả về và sẽ không chạy mã không cần thiết

Chạy mã sẽ như thế này:

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

Đoạn mã trên sẽ sử dụng printfnếu ở chế độ gỡ lỗi hoặc không làm gì nếu ở chế độ phát hành. Điều này dễ dàng hơn nhiều so với việc đi qua toàn bộ dự án và bình luận hoặc xóa mã. Tất cả những gì tôi cần làm là thay đổi phiên bản version.hvà mã sẽ làm phần còn lại!


4
U đứng để mất rất nhiều thời gian hiệu suất. Thay vào đó, bạn có thể sử dụng một macro cho phép và vô hiệu hóa một phần mã dựa trên Gỡ lỗi / Phát hành.
AlphaGoku

19

Con trỏ hàm thường được định nghĩa bởi typedefvà được sử dụng làm giá trị param & return.

Các câu trả lời ở trên đã được giải thích rất nhiều, tôi chỉ đưa ra một ví dụ đầy đủ:

#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

Một trong những ứng dụng lớn cho các con trỏ hàm trong C là gọi một hàm được chọn trong thời gian chạy. Ví dụ, thư viện thời gian chạy C có hai thường trình qsortbsearchlấy một con trỏ tới một hàm được gọi để so sánh hai mục được sắp xếp; điều này cho phép bạn sắp xếp hoặc tìm kiếm, tương ứng, bất cứ điều gì, dựa trên bất kỳ tiêu chí nào bạn muốn sử dụng.

Một ví dụ rất cơ bản, nếu có một hàm được gọi print(int x, int y)lần lượt có thể yêu cầu gọi một hàm ( add()hoặc sub()là cùng loại) thì chúng ta sẽ làm gì, chúng ta sẽ thêm một đối số con trỏ hàm vào print()hàm như dưới đây :

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

Đầu ra là:

giá trị là: 410
giá trị là: 390


10

Bắt đầu từ chức năng đầu có một số Địa chỉ bộ nhớ Từ nơi họ bắt đầu thực thi. Trong ngôn ngữ hội chúng được gọi là (gọi "địa chỉ bộ nhớ của hàm"). Bây giờ hãy quay lại C Nếu hàm có địa chỉ bộ nhớ thì chúng có thể được xử lý bởi Con trỏ trong C.So Theo quy tắc của C

1.Đầu tiên bạn cần khai báo một con trỏ tới hàm 2.Pass Địa chỉ của hàm mong muốn

**** Lưu ý-> các chức năng phải cùng loại ****

Chương trình đơn giản này sẽ minh họa mọi điều.

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

nhập mô tả hình ảnh ở đâySau đó, hãy xem cách máy hiểu chúng. Hướng dẫn về máy của chương trình trên trong kiến ​​trúc 32 bit.

Vùng đánh dấu màu đỏ đang hiển thị cách địa chỉ được trao đổi và lưu trữ trong eax. Sau đó, họ là một hướng dẫn cuộc gọi trên eax. eax chứa địa chỉ mong muốn của hàm.


8

Một con trỏ hàm là một biến chứa địa chỉ của hàm. Vì nó là một biến con trỏ mặc dù với một số thuộc tính bị hạn chế, bạn có thể sử dụng nó giống như bất kỳ biến con trỏ nào khác trong cấu trúc dữ liệu.

Ngoại lệ duy nhất tôi có thể nghĩ đến là coi con trỏ hàm là trỏ đến một thứ khác không phải là một giá trị. Thực hiện số học con trỏ bằng cách tăng hoặc giảm con trỏ hàm hoặc thêm / bớt một phần bù cho con trỏ hàm không thực sự là tiện ích vì con trỏ hàm chỉ trỏ đến một điều duy nhất, điểm vào của hàm.

Kích thước của biến con trỏ hàm, số byte bị chiếm bởi biến, có thể thay đổi tùy thuộc vào kiến ​​trúc cơ bản, ví dụ x32 hoặc x64 hoặc bất cứ điều gì.

Khai báo cho một biến con trỏ hàm cần chỉ định cùng loại thông tin như khai báo hàm để trình biên dịch C thực hiện các loại kiểm tra mà nó thường làm. Nếu bạn không chỉ định danh sách tham số trong khai báo / định nghĩa của con trỏ hàm, trình biên dịch C sẽ không thể kiểm tra việc sử dụng tham số. Có những trường hợp khi thiếu kiểm tra này có thể hữu ích tuy nhiên chỉ cần nhớ rằng một mạng lưới an toàn đã được gỡ bỏ.

Vài ví dụ:

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.

Hai khai báo đầu tiên có phần giống nhau ở chỗ:

  • funclà một hàm lấy một intvà một char *và trả về mộtint
  • pFunclà một con trỏ hàm được gán địa chỉ của hàm lấy một intvà a char *và trả về mộtint

Vì vậy, từ trên chúng ta có thể có một dòng nguồn trong đó địa chỉ của hàm func()được gán cho biến con trỏ hàm pFuncnhư trong pFunc = func;.

Lưu ý cú pháp được sử dụng với khai báo / định nghĩa con trỏ hàm trong đó dấu ngoặc đơn được sử dụng để vượt qua các quy tắc ưu tiên toán tử tự nhiên.

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

Một số ví dụ sử dụng khác nhau

Một số ví dụ về việc sử dụng con trỏ hàm:

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

Bạn có thể sử dụng danh sách tham số độ dài thay đổi trong định nghĩa của một con trỏ hàm.

int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);

Hoặc bạn không thể chỉ định một danh sách tham số. Điều này có thể hữu ích nhưng nó loại bỏ cơ hội cho trình biên dịch C thực hiện kiểm tra trên danh sách đối số được cung cấp.

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

Phong cách C

Bạn có thể sử dụng phôi kiểu C với các con trỏ hàm. Tuy nhiên, hãy lưu ý rằng trình biên dịch C có thể lỏng lẻo về kiểm tra hoặc cung cấp các cảnh báo thay vì lỗi.

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.

So sánh con trỏ hàm với đẳng thức

Bạn có thể kiểm tra xem một con trỏ hàm có bằng một địa chỉ hàm cụ thể bằng cách sử dụng một ifcâu lệnh mặc dù tôi không chắc nó sẽ hữu ích như thế nào. Các toán tử so sánh khác dường như thậm chí còn ít tiện ích hơn.

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

Một mảng các con trỏ hàm

Và nếu bạn muốn có một mảng các con trỏ hàm, mỗi phần tử trong danh sách đối số có sự khác biệt thì bạn có thể định nghĩa một con trỏ hàm với danh sách đối số không xác định (không voidcó nghĩa là không có đối số mà chỉ là không xác định) giống như sau có thể thấy cảnh báo từ trình biên dịch C. Điều này cũng hoạt động cho một tham số con trỏ hàm đến một chức năng:

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

Kiểu C namespaceSử dụng Toàn cầu structvới Chức năng Con trỏ

Bạn có thể sử dụng statictừ khóa để chỉ định một hàm có tên là phạm vi tệp và sau đó gán nó cho biến toàn cục như một cách cung cấp một cái gì đó tương tự như namespacechức năng của C ++.

Trong tệp tiêu đề, xác định một cấu trúc sẽ là không gian tên của chúng ta cùng với một biến toàn cục sử dụng nó.

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;

Sau đó, trong tệp nguồn 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};

Điều này sau đó sẽ được sử dụng bằng cách chỉ định tên đầy đủ của biến cấu trúc toàn cầu và tên thành viên để truy cập hàm. Công cụ constsửa đổi được sử dụng trên toàn cầu để không thể thay đổi ngẫu nhiên.

int abcd = FuncThingsGlobal.func1 (a, b);

Các lĩnh vực ứng dụng của con trỏ hàm

Một thành phần thư viện DLL có thể làm điều gì đó tương tự như cách namespacetiếp cận kiểu C trong đó giao diện thư viện cụ thể được yêu cầu từ phương thức xuất xưởng trong giao diện thư viện hỗ trợ tạo structcon trỏ hàm chứa .. Giao diện thư viện này tải phiên bản DLL được yêu cầu, tạo một cấu trúc với các con trỏ hàm cần thiết, và sau đó trả về cấu trúc cho người gọi yêu cầu sử dụng.

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

và điều này có thể được sử dụng như trong:

LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
//  ....
myLib.Func1();
//  ....
FreeLibraryFunc (&myLib);

Cách tiếp cận tương tự có thể được sử dụng để xác định một lớp phần cứng trừu tượng cho mã sử dụng một mô hình cụ thể của phần cứng cơ bản. Các con trỏ hàm được điền bởi các chức năng cụ thể của phần cứng bởi một nhà máy để cung cấp chức năng cụ thể cho phần cứng thực hiện các chức năng được chỉ định trong mô hình phần cứng trừu tượng. Điều này có thể được sử dụng để cung cấp một lớp phần cứng trừu tượng được sử dụng bởi phần mềm gọi hàm chức năng của nhà máy để có được giao diện chức năng phần cứng cụ thể sau đó sử dụng các con trỏ hàm được cung cấp để thực hiện các hành động cho phần cứng cơ bản mà không cần biết chi tiết triển khai về mục tiêu cụ thể .

Chức năng Con trỏ để tạo Đại biểu, Trình xử lý và Gọi lại

Bạn có thể sử dụng các con trỏ hàm như một cách để ủy thác một số tác vụ hoặc chức năng. Ví dụ kinh điển trong C là con trỏ hàm ủy nhiệm so sánh được sử dụng với các hàm thư viện C chuẩn qsort()bsearch()để cung cấp thứ tự đối chiếu để sắp xếp danh sách các mục hoặc thực hiện tìm kiếm nhị phân trên danh sách các mục được sắp xếp. Đại biểu hàm so sánh chỉ định thuật toán đối chiếu được sử dụng trong sắp xếp hoặc tìm kiếm nhị phân.

Một cách sử dụng khác tương tự như áp dụng thuật toán cho bộ chứa Thư viện Mẫu Tiêu chuẩn 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);

Một ví dụ khác là với mã nguồn GUI trong đó trình xử lý cho một sự kiện cụ thể được đăng ký bằng cách cung cấp một con trỏ hàm thực sự được gọi khi sự kiện xảy ra. Khung Microsoft MFC với bản đồ thông báo của nó sử dụng một cái gì đó tương tự để xử lý các thông báo Windows được gửi đến một cửa sổ hoặc luồng.

Các hàm không đồng bộ yêu cầu gọi lại tương tự như trình xử lý sự kiện. Người dùng hàm không đồng bộ gọi hàm không đồng bộ để bắt đầu một số hành động và cung cấp một con trỏ hàm mà hàm không đồng bộ sẽ gọi sau khi hành động hoàn tất. Trong trường hợp này, sự kiện là hàm không đồng bộ hoàn thành nhiệm vụ của nó.


0

Vì các con trỏ hàm thường được gõ các cuộc gọi lại, bạn có thể muốn xem qua các cuộc gọi lại an toàn kiểu . Điều tương tự cũng áp dụng cho các điểm vào, vv của các hàm không phải là hàm gọi lại.

C khá hay thay đổi và tha thứ cùng một lúc :)

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.