2010-01-22 38 views
10

Tôi chỉ mới bắt đầu quấn đầu quanh các con trỏ hàm trong C. Để hiểu cách truyền con trỏ hàm hoạt động, tôi đã viết chương trình sau đây. Về cơ bản nó tạo ra một con trỏ hàm tới một hàm lấy một tham số, đưa nó tới một con trỏ hàm với ba tham số và gọi hàm, cung cấp ba tham số. Tôi đã tò mò điều gì sẽ xảy ra:Điều gì sẽ xảy ra nếu tôi gán con trỏ hàm, thay đổi số tham số

#include <stdio.h> 

int square(int val){ 
    return val*val; 
} 

void printit(void* ptr){ 
    int (*fptr)(int,int,int) = (int (*)(int,int,int)) (ptr); 
    printf("Call function with parameters 2,4,8.\n"); 
    printf("Result: %d\n", fptr(2,4,8)); 
} 


int main(void) 
{ 
    printit(square); 
    return 0; 
} 

Việc biên dịch và chạy mà không có lỗi hoặc cảnh báo (gcc -Wall trên Linux/x86). Kết quả đầu ra trên hệ thống của tôi là:

Call function with parameters 2,4,8. 
Result: 4 

Vì vậy, dường như các đối số thừa được bỏ qua một cách đơn giản.

Bây giờ tôi muốn hiểu điều gì đang thực sự xảy ra ở đây.

  1. Tính hợp pháp: Nếu tôi hiểu câu trả lời cho Casting a function pointer to another type chính xác, đây chỉ là hành vi không xác định. Vì vậy, thực tế là điều này chạy và tạo ra một kết quả hợp lý chỉ là may mắn tinh khiết, đúng không? (hoặc độc đáo trên một phần của các nhà biên dịch biên dịch)
  2. Tại sao gcc không cảnh báo tôi về điều này, ngay cả với Tường? Đây có phải là một cái gì đó trình biên dịch chỉ không thể phát hiện? Tại sao?

Tôi đến từ Java, nơi kỹ thuật đánh máy là rất chặt chẽ hơn, vì vậy hành vi này làm tôi bối rối một chút. Có lẽ tôi đang trải qua một cú sốc văn hóa :-).

+0

Nếu điều đó đã là một cú sốc văn hóa cho bạn, hãy chờ cho đến khi bạn phát hiện ra rằng trong C++ con trỏ đến các hàm thành viên có thể lớn hơn void *, mặc dù chúng không mang con trỏ này với chúng ... – OregonGhost

Trả lời

11

Các thông số bổ sung sẽ không bị hủy. Chúng được đặt đúng trên ngăn xếp, như thể cuộc gọi được thực hiện đến một chức năng có ba tham số. Tuy nhiên, vì hàm của bạn chỉ quan tâm đến một tham số, nó chỉ trông ở trên cùng của ngăn xếp và không chạm vào các tham số khác.

Thực tế là cuộc gọi này làm việc là tinh khiết may mắn, dựa trên hai sự kiện:

  • kiểu của tham số đầu tiên là như nhau cho các chức năng và con trỏ đúc. Nếu bạn thay đổi hàm để lấy một con trỏ thành chuỗi và cố gắng in chuỗi đó, bạn sẽ nhận được một sự cố tốt đẹp, vì mã sẽ cố gắng trỏ con trỏ đến địa chỉ bộ nhớ 2.
  • quy ước gọi được sử dụng theo mặc định là dành cho người gọi để dọn dẹp ngăn xếp. Nếu bạn thay đổi quy ước gọi, để callee dọn dẹp ngăn xếp, bạn sẽ kết thúc với người gọi đẩy ba tham số trên ngăn xếp và sau đó làm sạch callee (hoặc đúng hơn là cố gắng) một tham số. Điều này có thể dẫn đến sự tham nhũng ngăn xếp.

Không có cách nào trình biên dịch có thể cảnh báo bạn về các vấn đề tiềm ẩn như thế này vì một lý do đơn giản - trong trường hợp chung, nó không biết giá trị của con trỏ tại thời gian biên dịch. nó chỉ vào. Hãy tưởng tượng rằng con trỏ hàm trỏ đến một phương thức trong một bảng ảo lớp được tạo ra trong thời gian chạy? Vì vậy, nó bạn nói với trình biên dịch nó là một con trỏ đến một hàm với ba tham số, trình biên dịch sẽ tin bạn.

+0

@ JasonC yep, chính xác những gì tôi đã nói trong điểm đạn thứ hai của tôi.:-) –

1
  1. Admitedly Tôi không biết chắc chắn, nhưng bạn chắc chắn không muốn tận dụng lợi thế của hành vi đó nếu nó may mắn hoặc nếu nó biên dịch cụ thể.

  2. Nó không đáng bị cảnh báo vì diễn viên rõ ràng. Bằng cách truyền, bạn đang thông báo cho trình biên dịch mà bạn biết rõ hơn. Cụ thể, bạn đang tạo một void* và như vậy bạn đang nói "lấy địa chỉ được biểu thị bằng con trỏ này và làm cho nó giống với con trỏ khác" - diễn viên đơn giản thông báo cho trình biên dịch rằng bạn chắc chắn tại địa chỉ đích, trên thực tế, giống nhau. Mặc dù ở đây, chúng tôi biết điều đó không chính xác.

2
  1. Vâng, đó là hành vi không xác định - bất cứ điều gì có thể xảy ra, kể cả nó xuất hiện để "làm việc".

  2. Bộ đúc ngăn trình biên dịch đưa ra cảnh báo. Ngoài ra, trình biên dịch không có yêu cầu để chẩn đoán có thể gây ra hành vi không xác định. Lý do cho điều này là hoặc là không thể làm như vậy, hoặc làm như vậy sẽ là quá khó khăn và/hoặc gây ra nhiều chi phí.

0

tôi nên làm mới bộ nhớ của tôi về bố trí nhị phân của quy ước gọi C tại một số điểm, nhưng tôi khá chắc chắn đây là những gì đang xảy ra:

  • 1: Nó không phải là tinh khiết may mắn . Quy ước gọi C được xác định rõ và dữ liệu bổ sung trên ngăn xếp không phải là yếu tố cho trang web cuộc gọi, mặc dù nó có thể bị ghi đè bởi callee vì callee không biết về nó.
  • 2: Một diễn viên "cứng", sử dụng dấu ngoặc đơn, cho trình biên dịch biết bạn đang làm gì.Vì tất cả các dữ liệu cần thiết là trong một đơn vị biên dịch, trình biên dịch có thể đủ thông minh để tìm ra điều này rõ ràng là bất hợp pháp, nhưng các nhà thiết kế của C không tập trung vào việc đánh dấu trường hợp góc không thể xác minh được. Một cách đơn giản, các quỹ tín thác biên dịch mà bạn biết những gì bạn đang làm
+0

bỏ phiếu mà không có bất kỳ lời giải thích nào! –

+0

Đó là may mắn. Số lượng tham số không phải là điều quan trọng duy nhất, loại kích thước và ngữ nghĩa của các tham số được truyền và dự kiến ​​cũng rất quan trọng. Việc đúc từ f1 (char *) sang f2 (int, int int) chẳng hạn sẽ gây ra vấn đề. Hoặc đúc từ f1 (MYVERYLARGESTRUCT) đến f2 (char). –

+0

Tôi không ủng hộ mã này nói chung, nhưng nó không phải là may mắn tinh khiết những gì đã xảy ra. Quy ước gọi là 'C' (__cdecl trong MS C++) đảm bảo rằng tham số đầu tiên của một cuộc gọi phương thức sẽ luôn là thông số đầu tiên được tìm thấy bởi calleee, bất kể có bao nhiêu tham số tiếp theo. Hành vi này hoàn toàn có thể đoán trước và lặp lại, điều ngược lại với "may mắn" trong đầu tôi. –

0

Để trả lời câu hỏi của bạn (có lẽ dại dột trong trường hợp nhiều người lập trình C/C++!):

  1. tinh khiết may mắn - bạn có thể dễ dàng lấy mẫu ngăn xếp và ghi đè con trỏ trả về mã thực thi tiếp theo. Vì bạn đã chỉ định con trỏ hàm với 3 tham số, và gọi con trỏ hàm, hai tham số còn lại bị 'loại bỏ' và do đó, hành vi là không xác định. Hãy tưởng tượng nếu tham số thứ 2 hoặc thứ 3 đó có chứa lệnh nhị phân và bật ra khỏi ngăn xếp thủ tục cuộc gọi ....

  2. Không có cảnh báo nào khi bạn đang sử dụng con trỏ void * và truyền con trỏ. Đó là một mã khá hợp pháp trong mắt trình biên dịch, ngay cả khi bạn đã chỉ định rõ ràng công tắc -Wall. Trình biên dịch giả định bạn biết bạn đang làm gì! Đó là bí mật.

Hy vọng điều này sẽ giúp, Trân trọng, Tom.

+0

họ nên xây dựng một trình biên dịch biết rõ hơn là tin tưởng con người. :-) –

+0

@Franci: Đó là bản chất của C, bạn cung cấp một cú pháp trông hợp pháp, trình biên dịch sẽ vui vẻ chấp nhận nó ... xem tại http://www.ioccc.org để xem ý tôi là gì. – t0mm13b

11

Nếu bạn lấy một chiếc xe và đúc nó như một cái búa trình biên dịch sẽ đưa bạn vào từ của bạn rằng chiếc xe là một cái búa nhưng điều này không biến chiếc xe thành một cái búa. Trình biên dịch có thể thành công trong việc sử dụng chiếc xe để lái một chiếc đinh nhưng đó là việc thực hiện phụ thuộc vào tài sản tốt. Nó vẫn là một điều không khôn ngoan để làm.

2

Hành vi phạm tội tệ nhất của diễn viên của bạn là truyền con trỏ dữ liệu đến con trỏ hàm. Nó tồi tệ hơn thay đổi chữ ký vì không có gì đảm bảo rằng kích thước của con trỏ hàm và con trỏ dữ liệu bằng nhau.Và trái với rất nhiều lý thuyết hành vi không xác định, điều này có thể gặp phải trong tự nhiên, ngay cả trên các máy nâng cao (không chỉ trên các hệ thống nhúng).

Bạn có thể gặp phải các con trỏ kích thước khác nhau dễ dàng trên các nền tảng nhúng. Thậm chí còn có các bộ xử lý nơi con trỏ dữ liệu và con trỏ hàm làm việc với những thứ khác nhau (RAM cho một, ROM cho cái kia), cái gọi là kiến ​​trúc Harvard. Trên x86 ở chế độ thực, bạn có thể có 16 bit và 32 bit hỗn hợp. Watcom-C có một chế độ đặc biệt cho bộ mở rộng DOS, nơi các con trỏ dữ liệu rộng 48 bit. Đặc biệt với C nên biết rằng không phải mọi thứ đều là POSIX, vì C có thể là ngôn ngữ duy nhất có sẵn trên phần cứng kỳ lạ.

Một số trình biên dịch cho phép các mô hình bộ nhớ hỗn hợp trong đó mã được đảm bảo nằm trong kích thước 32 bit và dữ liệu có thể định địa chỉ với con trỏ 64 bit hoặc ngược lại.

Chỉnh sửa: Kết luận, không được đưa con trỏ dữ liệu vào con trỏ hàm.

+0

+1 Thú vị, tôi không biết về con trỏ dữ liệu phân biệt này <-> con trỏ hàm. Bạn có một số tài liệu tham khảo hữu ích để đọc thêm? Ngoài ra, điều gì sẽ tương đương với void * cho các con trỏ hàm? Hay không có con trỏ hàm "generic", như void * cho con trỏ dữ liệu? – sleske

+0

Chuẩn C không đảm bảo kích thước ''sizeof (void *) == (void (*) (void))''; may mắn thay, POSIX. –

+0

Mục 6.3.2.3 'con trỏ' nói: "Một con trỏ tới một hàm của một kiểu có thể được chuyển đổi thành con trỏ tới một hàm của kiểu khác và ngược lại, kết quả sẽ so sánh với con trỏ ban đầu. được sử dụng để gọi một hàm có kiểu không tương thích với kiểu được chỉ định, hành vi không xác định. " Lưu ý rằng câu hỏi được gọi một cách rõ ràng hành vi không xác định - hãy coi chừng ma quỷ mũi. Một đoạn tương tự (trước đó) về con trỏ đến các đối tượng. Tại điểm không, tiêu chuẩn C nói rằng các con trỏ tới các hàm có thể được chuyển đổi thành con trỏ tới các đối tượng hoặc ngược lại. –

2

Hành vi được xác định theo quy ước gọi điện. Nếu bạn sử dụng một quy ước gọi điện thoại nơi người gọi đẩy và bật ngăn xếp, thì nó sẽ hoạt động tốt trong trường hợp này vì nó chỉ có nghĩa là có thêm một vài byte trên ngăn xếp trong suốt cuộc gọi. Tôi không có gcc tiện dụng vào lúc này, nhưng với trình biên dịch microsoft, mã này:

int (__cdecl * fptr)(int,int,int) = (int (__cdecl *) (int,int,int)) (ptr); 

Việc lắp ráp sau đây được tạo ra cho các cuộc gọi:

push  8 
push  4 
push  2 
call  dword ptr [ebp-4] 
add   esp,0Ch 

Note 12 byte (0Ch) được thêm vào ngăn xếp sau cuộc gọi. Sau này, ngăn xếp là tốt (giả sử callee là __cdecl trong trường hợp này vì vậy nó không cố gắng cũng làm sạch ngăn xếp). Nhưng với mã sau:

int (__stdcall * fptr)(int,int,int) = (int (__stdcall *) (int,int,int)) (ptr); 

add esp,0Ch không được tạo trong hội đồng. Nếu callee là __cdecl trong trường hợp này, stack sẽ bị hỏng.

+0

+1 Cảm ơn thông tin về quy ước gọi điện. Tôi không biết rằng có nhiều quy ước. – sleske

Các vấn đề liên quan