AC Chức năng-Tuyên bố Backgrounder
Trong C, tờ khai chức năng không làm việc như họ làm trong các ngôn ngữ khác: Trình biên dịch C tự nó không tìm kiếm lạc hậu và chuyển tiếp trong tập tin để tìm khai của chức năng từ nơi bạn gọi nó, và nó không quét tệp nhiều lần để tìm ra mối quan hệ: Trình biên dịch chỉ quét chuyển tiếp trong tệp chính xác một lần, từ trên xuống dưới. Việc kết nối các cuộc gọi hàm với các khai báo hàm là một phần của công việc của nhà liên kết và chỉ được thực hiện sau tệp được biên dịch xuống các hướng dẫn lắp ráp nguyên bản. Điều này có nghĩa là khi trình biên dịch quét về phía trước thông qua tệp, lần đầu tiên trình biên dịch gặp tên của một hàm, một trong hai điều phải là trường hợp: Nó hoặc là nhìn thấy khai báo hàm, trong đó trường hợp trình biên dịch biết chính xác hàm này là gì và kiểu nào nó lấy làm đối số và kiểu nó trả về - hoặc đó là lời gọi hàm, và trình biên dịch phải đoán cách hàm sẽ được khai báo cuối cùng.
(Có một tùy chọn thứ ba, trong đó tên được sử dụng trong nguyên mẫu hàm, nhưng chúng tôi sẽ bỏ qua điều đó ngay bây giờ, vì nếu bạn gặp vấn đề này ngay từ đầu, có thể bạn không sử dụng nguyên mẫu.)
Lịch sử Bài học
trong những ngày đầu của C, thực tế là trình biên dịch phải đoán loại là không thực sự là một vấn đề: Tất cả các loại đều hơn hoặc ít hơn như nhau - khá mọi thứ đều là một int hoặc một con trỏ, và chúng có cùng kích thước. (Trong thực tế, trong B, ngôn ngữ trước C, không có loại nào cả; mọi thứ chỉ là int hoặc con trỏ và kiểu của nó được xác định chỉ bằng cách bạn sử dụng nó!) Vậy thì trình biên dịch có thể đoán được hành vi của bất kỳ chỉ dựa trên số tham số đã được truyền: Nếu bạn truyền hai tham số, trình biên dịch sẽ đẩy hai thứ vào ngăn xếp cuộc gọi, và có lẽ callee sẽ có hai đối số được khai báo và tất cả sẽ được xếp hàng. Nếu bạn chỉ truyền một tham số nhưng hàm được mong đợi hai, nó sẽ vẫn sắp xếp công việc và đối số thứ hai sẽ bị bỏ qua/rác. Nếu bạn đã vượt qua ba tham số và hàm được mong đợi hai, nó cũng sẽ sắp xếp công việc, và tham số thứ ba sẽ bị bỏ qua và được dẫm bởi các biến cục bộ của hàm. (Một số mã C cũ vẫn mong đợi các quy tắc đối số không phù hợp này cũng sẽ hoạt động.)
Nhưng có trình biên dịch cho phép bạn chuyển bất cứ thứ gì cho bất kỳ thứ gì không thực sự là một cách hay để thiết kế một ngôn ngữ lập trình. Nó hoạt động tốt trong những ngày đầu bởi vì các lập trình viên C đầu tiên chủ yếu là các trình thuật sĩ, và họ biết không truyền loại sai cho các chức năng, và thậm chí nếu chúng có sai loại, luôn có các công cụ như lint
. kiểm tra mã C của bạn và cảnh báo bạn về những điều như vậy.
Tua tới ngày hôm nay và chúng tôi không hoàn toàn giống nhau trên cùng một chiếc thuyền. C đã lớn lên, và rất nhiều người đang lập trình trong đó không phải là pháp sư, và để thích ứng với họ (và để chứa tất cả những người khác thường xuyên sử dụng lint
anyway), các trình biên dịch đã thực hiện trên nhiều khả năng mà trước đó là một phần của lint
- đặc biệt là phần mà họ kiểm tra mã của bạn để đảm bảo mã an toàn. Các trình biên dịch C ban đầu sẽ cho phép bạn viết int foo = "hello";
và nó sẽ chỉ gán con trỏ cho số nguyên, và điều đó tùy thuộc vào bạn để đảm bảo rằng bạn không làm bất cứ điều gì ngu ngốc. Trình biên dịch C hiện đại phàn nàn lớn tiếng khi bạn nhận được các loại của bạn sai, và đó là một điều tốt.
Loại Xung đột
Vì vậy, những gì đang tất cả điều này đã làm với các lỗi mâu thuẫn kiểu bí ẩn trên dòng khai báo hàm? Như tôi đã nói ở trên, trình biên dịch C vẫn phải biết hoặc đoán tên là gì khi lần đầu tiên họ nhìn thấy tên khi họ quét chuyển tiếp qua tệp: Họ có thể biết ý nghĩa của nó là gì tự khai báo hàm (hoặc một hàm "nguyên mẫu", thêm vào đó ngay), nhưng nếu nó chỉ là một lời gọi hàm, chúng phải đoán. Và, thật đáng buồn, đoán thường sai.
Khi trình biên dịch thấy cuộc gọi của bạn để do_something()
, nó nhìn nó như thế nào được gọi, và nó kết luận rằng do_something()
cuối cùng sẽ được công bố như thế này:
int do_something(char arg1[], char arg2[])
{
...
}
Tại sao nó kết luận rằng? Vì đó là cách bạn gọi là! (Một số trình biên dịch C có thể kết luận rằng đó là int do_something(int arg1, int arg2)
hoặc đơn giản là int do_something(...)
, cả hai đều là xa hơn từ những gì bạn muốn, nhưng điều quan trọng là bất kể trình biên dịch đoán các loại như thế nào, nó đoán chúng khác với sử dụng chức năng thực tế.)
Sau đó, khi trình biên dịch quét về phía trước trong tệp, nó sẽ xem tờ khai thực tế của bạn là char *do_something(char *, char *)
. Khai báo hàm đó thậm chí không gần với khai báo mà trình biên dịch đoán, có nghĩa là dòng nơi trình biên dịch đã biên dịch cuộc gọi được biên dịch sai, và chương trình sẽ không hoạt động. Vì vậy, nó đúng in một lỗi nói với bạn rằng mã của bạn sẽ không hoạt động như được viết.
Bạn có thể thắc mắc, "Tại sao tôi giả sử tôi trả lại int
?" Vâng, nó giả định rằng loại vì không có thông tin ngược lại: printf()
có thể lấy bất kỳ loại nào trong các đối số biến của nó, vì vậy mà không có câu trả lời tốt hơn, int
cũng tốt như dự đoán. (Nhiều trình biên dịch C ban đầu luôn giả định int
đối với mọi loại không xác định và giả sử bạn có nghĩa là ...
cho các đối số cho mọi hàm được khai báo f()
- không phải là void
- đó là lý do tại sao nhiều tiêu chuẩn mã hiện đại đề xuất luôn đặt void
cho đối số nếu có t cho là bất kỳ.)
Cách khắc phục
có hai bản vá phổ biến đối với các lỗi chức năng kê khai.
Các giải pháp đầu tiên, đó là khuyến cáo của nhiều câu trả lời khác ở đây, là để đặt một nguyên mẫu trong mã nguồn trên nơi hàm được đầu tiên gọi. Một nguyên mẫu trông giống như tuyên bố của chức năng, nhưng nó có một dấu chấm phẩy nơi cơ thể nên là:
char *do_something(char *dest, const char *src);
Bằng cách đặt nguyên mẫu đầu tiên, trình biên dịch sau đó biết những gì các chức năng cuối cùng sẽ như thế nào, vì thế nó doesn không phải đoán.Theo quy ước, các lập trình viên thường đặt các nguyên mẫu ở đầu tệp, ngay dưới các câu lệnh #include
, để đảm bảo rằng chúng sẽ luôn được xác định trước bất kỳ tập quán tiềm năng nào của chúng.
Giải pháp khác, cũng hiển thị trong một số mã thực, chỉ đơn giản là sắp xếp lại các hàm của bạn để các khai báo hàm luôn là trước bất kỳ thứ gì gọi chúng! Bạn có thể di chuyển toàn bộ hàm char *do_something(char *dest, const char *src) { ... }
phía trên cuộc gọi đầu tiên đến nó, và trình biên dịch sau đó sẽ biết chính xác chức năng trông như thế nào và sẽ không phải đoán. Trong thực tế, hầu hết mọi người sử dụng nguyên mẫu chức năng, vì bạn cũng có thể lấy nguyên mẫu hàm và di chuyển chúng vào tiêu đề (.h
) để mã trong các tệp .c
khác có thể gọi những chức năng đó. Nhưng một trong hai giải pháp hoạt động, và nhiều codebase sử dụng cả hai.
C99 và C11
Nó rất hữu ích cần lưu ý rằng các quy tắc hơi khác nhau trong các phiên bản mới hơn của tiêu chuẩn C. Trong các phiên bản trước (C89 và K & R), trình biên dịch thực sự sẽ đoán các loại tại thời gian gọi hàm (và K & Trình biên dịch thời đại thường sẽ không cảnh báo bạn nếu chúng sai). Cả C99 và C11 đều yêu cầu khai báo/nguyên mẫu hàm phải đứng trước cuộc gọi đầu tiên và đó là lỗi nếu nó không xảy ra. Nhưng nhiều trình biên dịch C hiện đại - chủ yếu cho khả năng tương thích ngược với mã trước đó - sẽ chỉ cảnh báo về nguyên mẫu bị thiếu và không coi đó là lỗi.
ohhh, một sai lầm què như vậy, nhờ – goe
Mọi người đều làm cho sai lầm què nhân dịp ... –