Sự khác nhau giữa file .h và file .cpp


Câu hỏi:

Trả lời:

Ai cũng biết là ta có thể viết code trong nhiều file .c/.cpp khác nhau, các trình biên dịch cho phép điều này nhằm giúp ta tổ chức mã nguồn dễ hơn, hãy thử tưởng tượng những khó khăn nếu mỗi chương trình phải viết trong một file, chưa kể đến quá trình merge code, team tầm 5 người thì có lẽ thời gian merge còn lâu hơn thời gian code nữa.

Tuy nhiên bạn không thể gọi một hàm chứa trong một file .cpp khác, đơn giản là vì trình biên dịch không biết hàm đó có kiểu trả về là gì, cũng như danh sách tham số là gì, vì không biết nên nó không thể xác định được lời gọi hàm của bạn có chính xác hay chưa. Để hỗ trợ gọi một hàm từ một file khác, C/C++ cho phép bạn khai báo (declaration) mà không cần định nghĩa (define) thân hàm.

Hình 1

Trong hình minh họa trên, ở dòng 7 bạn có thể thấy một declaration. Dòng này chỉ giúp ta biết được hàm isPalindrome có kiểu trả về, cũng như danh sách tham số là gì.

Dòng declaration này sẽ không sinh ra bất kỳ code nào trong file thực thi.

Khi có một lời gọi hàm đến isPalindrome (như trong ví dụ bên dưới), trình dịch sẽ sinh ra code để gọi hàm. Code gọi hàm sẽ bao gồm các lệnh để thiết lập các tham số và lệnh CALL đến isPalindrome.

Hình 2

Dòng 15 được dịch thành:

Hình 3

Tuy nhiên vì hàm isPalindrome được khai báo ở một file mã nguồn khác nên trình biên dịch vẫn chưa biết được hàm cần gọi nằm ở đâu, nơi chứa địa chỉ hàm isPalindrome trong lời gọi call sẽ để trống và được đánh dấu lại (*).

File mã nguồn của bạn lúc này đã được dịch nhưng chưa chạy được, ta gọi là objective file, và thường có phần mở rộng là .o hoặc .obj.

Sau khi tất cả các file .cpp đã được dịch, trình biên dịch sẽ kết nối các file obj lại, nó sẽ duyệt lại tất cả những lệnh call chưa có địa chỉ và tìm hàm tương ứng, vì lúc này tất cả mã nguồn đã được biên dịch nên trình dịch đã biết chính xác hàm isPalindrome nằm ở đâu (hoặc báo lỗi nếu không tìm thấy define của nó). Chỉ đến khi tất cả các lời gọi hàm đều đã được xử lý thì file thực thi hoàn chỉnh mới được tạo ra, và chỉ lúc này nó mới có thể chạy được. Quá trình kết nối và phân giải các lời gọi còn thiếu này ta gọi là liên kết (link).

Như vậy tới đây ta đã biết cách trình biên dịch (và liên kết) làm việc như thế nào, cách này cũng được áp dụng khi các bạn sử dụng một hàm trong thư viện có sẵn của C++: các hàm thư viện đã được biên dịch sẽ được link vào trong chương trình của bạn, các lời gọi hàm sẽ được cập nhật để địa chỉ nó gọi đến là địa chỉ của hàm mà bạn đang sử dụng.

Vậy còn file .h để làm gì?

Như ở trên đã nói, để gọi một hàm ta cần có declaration của nó, như vậy trước khi gọi đến isPalindrome, ta phải viết một dòng tương tự dòng 7 trong hình 1:

Nếu bạn cần gọi đến hàm này trong 100 file mã nguồn khác nhau, bạn sẽ cần 100 dòng như vậy trong mỗi file, nếu hàm này thay đổi, ví dụ thêm một tham số, bạn sẽ phải sửa lại cả 100 declaration đó. Tất nhiên trong thực tế người ta không làm vậy, người ta sẽ tạo ra một file, trong đó có declaration của hàm isPalindrome, mỗi khi cần dùng, họ sẽ sử dụng macro #include để chèn nội dung file đó vào. File này người ta gọi là header và thường có phần mở rộng là .h hoặc .hpp.

Macro #include chỉ đơn giản đọc nội dung file và chèn vào đúng vị trí nó được khai báo.

Nhờ cách này, mỗi khi thay đổi, bạn chỉ cần sửa nội dung file .h là đủ, tất cả file mã nguồn có #include file này sẽ thay đổi theo và được biên dịch lại. Cũng nhờ cách này, bạn có thể cho phép người sử dụng biết cách dùng một thư viện bạn tạo ra mà không cần chia sẻ lại mã nguồn, bạn chỉ cần viết một file .h với các hàm mà bạn muốn chia sẻ là được.

Và nếu hàm của bạn có sử dụng một cấu trúc dữ liệu tự định nghĩa, chúng cũng cần được khai báo trong file header, vì tất nhiên nếu không có các khai báo đó bạn sẽ không thể gọi được hàm.

Có một vấn đề với file .h và #include, đó là việc #include có thể bị trùng lặp. Giả sử bạn có math.h chứa khai báo các hàm toán học, ptb1.h chứa các hàm giải phương trình bậc 1, ptb2.h chứa các hàm giải phương trình bậc 2. Cả ptb1.h và ptb2.h đều #include math.h, nếu chương trình giải toán của bạn #include cả ptb1.h và ptb2.h sẽ dẫn đến math.h bị #include hai lần, hay nói cách khác, các declaration trong math.h xuất hiện trong file mã nguồn hai lần (sau khi xử lý các macro).

Để tránh điều này người ta sử dụng quy ước tạo ra các macro trùng với tên file để xác định xem file này đã được #include trước đó hay chưa. Trong hình 1 bạn có thể thấy người ta dùng #ifndef (If Not DEFined)để xác định xem đã có macro PALINDROME_H hay chưa, nếu chưa thì sẽ định nghĩa một macro mới và khai báo các hàm, ngược lại không làm gì cả.

Nếu tự viết các file .h bạn cũng cần tuân theo quy ước này.

Ngoài function, bạn cũng có thể áp dụng với các biến bằng cách sử dụng từ khóa extern.

Ví dụ được sử dụng trong bài: https://github.com/edumentab/cpp-project-example

Leave a comment