Tại sao chúng ta cần bao gồm .h trong khi mọi thứ hoạt động khi chỉ bao gồm tệp .cpp?


18

Tại sao chúng ta cần bao gồm cả tệp .h.cpptệp trong khi chúng ta có thể làm cho nó hoạt động chỉ bằng cách bao gồm .cpptệp?

Ví dụ: tạo một file.hkhai báo có chứa, sau đó tạo một file.cppđịnh nghĩa chứa và bao gồm cả hai trong main.cpp.

Ngoài ra: tạo một file.cppkhai báo / định nghĩa có chứa (không có nguyên mẫu) bao gồm nó trong main.cpp.

Cả hai đều làm việc cho tôi. Tôi không thể thấy sự khác biệt. Có lẽ một số cái nhìn sâu sắc về quá trình biên dịch và liên kết có thể giúp đỡ.


Chia sẻ nghiên cứu của bạn giúp mọi người . Hãy cho chúng tôi những gì bạn đã cố gắng và tại sao nó không đáp ứng nhu cầu của bạn. Điều này chứng tỏ rằng bạn đã dành thời gian để cố gắng tự giúp mình, nó giúp chúng tôi tránh nhắc lại các câu trả lời rõ ràng và hầu hết nó giúp bạn có được câu trả lời cụ thể và phù hợp hơn. Xem thêm Cách hỏi
gnat

Tôi mạnh mẽ khuyên bạn nên nhìn vào các câu hỏi liên quan ở đây ở bên. lập trình

Tại sao điều này là trên các lập trình viên, mà không phải trên SO?
Cuộc đua nhẹ nhàng với Monica

@LightnessRacesInOrbit vì đó là câu hỏi về phong cách mã hóa và không phải là câu hỏi "làm thế nào để".
CashCow

@CashCow: Âm thanh như bạn hiểu nhầm SO, đây không phải là trang web "làm thế nào để". Có vẻ như bạn hiểu nhầm câu hỏi này, đây không phải là câu hỏi về phong cách mã hóa.
Cuộc đua nhẹ nhàng với Monica

Câu trả lời:


29

Mặc dù bạn có thể bao gồm .cppcác tệp như bạn đã đề cập, đây là một ý tưởng tồi.

Như bạn đã đề cập, khai báo thuộc về tệp tiêu đề. Chúng không gây ra vấn đề gì khi được bao gồm trong nhiều đơn vị biên dịch vì chúng không bao gồm việc triển khai. Bao gồm định nghĩa của một hàm hoặc thành viên lớp nhiều lần thường sẽ gây ra sự cố (nhưng không phải luôn luôn) vì trình liên kết sẽ bị lẫn lộn và gây ra lỗi.

Điều nên xảy ra là mỗi .cpptệp bao gồm các định nghĩa cho một tập hợp con của chương trình, chẳng hạn như một lớp, nhóm các hàm được tổ chức hợp lý, các biến tĩnh toàn cục (sử dụng một cách tiết kiệm nếu có), v.v.

Mỗi đơn vị biên dịch ( .cpptệp) sau đó bao gồm bất kỳ khai báo nào cần để biên dịch các định nghĩa mà nó chứa. Nó theo dõi các chức năng và các lớp mà nó tham chiếu nhưng không chứa, vì vậy trình liên kết có thể giải quyết chúng sau này khi nó kết hợp mã đối tượng vào một thư viện hoặc thư viện thực thi.

Thí dụ

  • Foo.h -> chứa khai báo (giao diện) cho lớp Foo.
  • Foo.cpp -> chứa định nghĩa (triển khai) cho lớp Foo.
  • Main.cpp-> chứa phương thức chính, điểm vào chương trình. Mã này khởi tạo một Foo và sử dụng nó.

Cả hai Foo.cppMain.cppcần bao gồm Foo.h. Foo.cppcần nó bởi vì nó đang xác định mã hỗ trợ giao diện lớp, vì vậy nó cần biết giao diện đó là gì. Main.cppcần nó bởi vì nó đang tạo ra một Foo và gọi hành vi của nó, vì vậy nó phải biết hành vi đó là gì, kích thước của Foo trong bộ nhớ và cách tìm các chức năng của nó, v.v. nhưng nó chưa cần triển khai thực tế.

Trình biên dịch sẽ tạo Foo.otừ Foo.cppđó chứa tất cả mã lớp Foo ở dạng biên dịch. Nó cũng tạo ra Main.obao gồm phương thức chính và các tham chiếu chưa được giải quyết đến lớp Foo.

Bây giờ đến trình liên kết, kết hợp hai tệp đối tượng Foo.oMain.othành một tệp thực thi. Nó thấy các tham chiếu Foo chưa được giải quyết trong Main.onhưng thấy Foo.ocó chứa các ký hiệu cần thiết, do đó, nó "kết nối các dấu chấm" để nói. Một lệnh gọi hàm Main.ohiện được kết nối với vị trí thực tế của mã được biên dịch để khi chạy, chương trình có thể nhảy đến vị trí chính xác.

Nếu bạn đã bao gồm Foo.cpptệp trong Main.cppđó, sẽ có hai định nghĩa về lớp Foo. Trình liên kết sẽ thấy điều này và nói "Tôi không biết nên chọn cái nào, vì vậy đây là một lỗi." Bước biên dịch sẽ thành công, nhưng liên kết thì không. (Trừ khi bạn không biên dịch Foo.cppnhưng tại sao nó lại nằm trong một .cpptệp riêng ?)

Cuối cùng, ý tưởng về các loại tệp khác nhau không liên quan đến trình biên dịch C / C ++. Nó biên dịch "tệp văn bản" hy vọng chứa mã hợp lệ cho ngôn ngữ mong muốn. Đôi khi nó có thể nói ngôn ngữ dựa trên phần mở rộng tập tin. Ví dụ, biên dịch một .ctệp không có tùy chọn trình biên dịch và nó sẽ giả sử C, trong khi một .cchoặc .cppphần mở rộng sẽ bảo nó giả sử C ++. Tuy nhiên, tôi có thể dễ dàng yêu cầu một trình biên dịch biên dịch một tệp .hhoặc thậm chí .docxlà C ++ và nó sẽ phát ra một .otệp object ( ) nếu nó chứa mã C ++ hợp lệ ở định dạng văn bản thuần túy. Các phần mở rộng này là nhiều hơn cho lợi ích của lập trình viên. Nếu tôi thấy Foo.hFoo.cpp, tôi ngay lập tức giả định rằng cái đầu tiên chứa khai báo của lớp và cái thứ hai chứa định nghĩa.


Tôi không hiểu lý lẽ bạn đang đưa ra ở đây. Nếu bạn chỉ bao gồm Foo.cpptrong Main.cppbạn không cần bao gồm .htệp, bạn có một tệp ít hơn, bạn vẫn giành chiến thắng trong việc chia mã thành các tệp riêng biệt để dễ đọc và lệnh biên dịch của bạn đơn giản hơn. Trong khi tôi hiểu tầm quan trọng của các tệp tiêu đề, tôi không nghĩ đây là nó
Benjamin Gruenbaum

2
Đối với một chương trình tầm thường, chỉ cần đặt tất cả trong một tệp. Thậm chí không bao gồm bất kỳ tiêu đề dự án (chỉ tiêu đề thư viện). Bao gồm các tệp nguồn là một thói quen xấu sẽ gây ra sự cố cho bất kỳ chương trình nào trừ các chương trình nhỏ nhất một khi bạn có nhiều đơn vị biên dịch và thực sự biên dịch chúng một cách riêng biệt.

2
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.Câu quan trọng nhất liên quan đến câu hỏi.
marczellm

@Snowman Đúng, đó là những gì tôi nghĩ bạn nên tập trung vào - nhiều đơn vị biên dịch. Dán mã có / không có tệp tiêu đề để phá mã thành các phần nhỏ hơn là hữu ích và trực giao với mục đích của tệp tiêu đề. Nếu tất cả những gì bạn muốn làm là chia nó thành các tệp tiêu đề, không mua gì cho chúng tôi - một khi chúng tôi cần liên kết động, liên kết có điều kiện và các bản dựng nâng cao hơn thì chúng đột nhiên rất hữu ích.
Benjamin Gruenbaum

Có rất nhiều cách để tổ chức nguồn cho trình biên dịch / liên kết C / C ++. Người hỏi không chắc chắn về quy trình, vì vậy tôi đã giải thích cách thực hành tốt nhất về cách tổ chức mã và cách quy trình hoạt động. Bạn có thể chia tóc về việc có thể tổ chức mã khác nhau, nhưng thực tế vẫn còn, tôi đã giải thích 99% các dự án ngoài kia làm điều đó là cách thực hành tốt nhất vì một lý do. Nó hoạt động tốt, và duy trì sự tỉnh táo của bạn.

9

Đọc thêm về vai trò của bộ tiền xử lý C và C ++ , về mặt khái niệm là "giai đoạn" đầu tiên của trình biên dịch C hoặc C ++ (về mặt lịch sử, đây là một chương trình riêng biệt /lib/cpp, vì lý do hiệu năng, nó được tích hợp bên trong trình biên dịch phù hợp cc1 hoặc cc1plus). Đọc cụ thể tài liệu của cppbộ tiền xử lý GNU . Vì vậy, trong thực tế, trình biên dịch trước tiên về mặt tiền xử lý đơn vị biên dịch của bạn (hoặc đơn vị dịch thuật ) và sau đó làm việc trên biểu mẫu được xử lý trước.

Bạn có thể cần phải luôn luôn bao gồm tệp tiêu đề file.hnếu nó chứa (như được quy định bởi các quy ướcthói quen ):

  • định nghĩa vĩ mô
  • loại định nghĩa (ví dụ như typedef, struct, classvv, ...)
  • định nghĩa về static inlinechức năng
  • khai báo các chức năng bên ngoài.

Lưu ý rằng đó là vấn đề của các quy ước (và thuận tiện) để đặt chúng trong một tệp tiêu đề.

Tất nhiên, việc thực hiện của bạn file.cppcần tất cả các điều trên, vì vậy muốn #include "file.h" lúc đầu.

Đây là một quy ước (nhưng rất phổ biến). Bạn có thể tránh các tệp tiêu đề và sao chép và dán nội dung của chúng vào các tệp thực hiện (ví dụ: đơn vị dịch). Nhưng bạn không muốn điều đó (có lẽ ngoại trừ nếu mã C hoặc C ++ của bạn được tạo tự động; thì bạn có thể tạo chương trình trình tạo thực hiện sao chép và dán đó, bắt chước vai trò của bộ xử lý trước).

Vấn đề là bộ tiền xử lý đang thực hiện các hoạt động chỉ bằng văn bản . Về nguyên tắc, bạn có thể tránh nó hoàn toàn bằng cách sao chép và dán hoặc thay thế nó bằng một "bộ xử lý tiền mã hóa" hoặc trình tạo mã C khác (như gpp hoặc m4 ).

Một vấn đề khác là các tiêu chuẩn C (hoặc C ++) gần đây xác định một số tiêu đề tiêu chuẩn. Hầu hết các triển khai thực sự triển khai các tiêu đề tiêu chuẩn này dưới dạng tệp (cụ thể triển khai) , nhưng tôi tin rằng việc triển khai tuân thủ tiêu chuẩn có thể bao gồm (như #include <stdio.h>đối với C hoặc #include <vector>đối với C ++) với một số thủ thuật ma thuật (ví dụ: sử dụng một số cơ sở dữ liệu hoặc một số thông tin bên trong trình biên dịch).

Nếu sử dụng trình biên dịch GCC (ví dụ gcchoặc g++), bạn có thể sử dụng -Hcờ để được thông báo về mọi sự bao gồm và các -C -Ecờ để có được biểu mẫu được xử lý trước. Tất nhiên, có nhiều cờ trình biên dịch khác ảnh hưởng đến quá trình tiền xử lý (ví dụ: -I /some/dir/để thêm /some/dir/vào tìm kiếm các tệp được bao gồm và -Dđể xác định trước một số macro tiền xử lý, v.v., v.v.).

Lưu ý Các phiên bản tương lai của C ++ (có thể là C ++ 20 , thậm chí muộn hơn) có thể có các mô-đun C ++ .


1
Nếu các liên kết thối, đây sẽ vẫn là một câu trả lời tốt?
Dan Pichelman

4
Tôi đã chỉnh sửa câu trả lời của mình để cải thiện nó và tôi cẩn thận chọn các liên kết -to wikipedia hoặc tới tài liệu GNU - mà tôi tin rằng sẽ có liên quan trong một thời gian dài.
Basile Starynkevitch

3

Do mô hình xây dựng nhiều đơn vị của C ++, bạn cần một cách để có mã chỉ xuất hiện trong chương trình của bạn một lần (định nghĩa) và bạn cần một cách để có mã xuất hiện trong mỗi đơn vị dịch của chương trình (khai báo).

Từ đây ra đời thành ngữ tiêu đề C ++. Đó là quy ước cho một lý do.

Bạn có thể kết xuất toàn bộ chương trình của mình vào một đơn vị dịch thuật, nhưng điều này gây ra vấn đề với việc sử dụng lại mã, kiểm tra đơn vị và xử lý phụ thuộc giữa các mô-đun. Nó cũng chỉ là một mớ hỗn độn lớn.


0

Câu trả lời được chọn từ Tại sao chúng ta cần phải viết một tệp tiêu đề? là một lời giải thích hợp lý, nhưng tôi muốn thêm chi tiết.

Có vẻ như sự hợp lý cho các tệp tiêu đề có xu hướng bị mất trong việc giảng dạy và thảo luận về C / C ++.

Tệp tiêu đề cung cấp một giải pháp cho hai vấn đề phát triển ứng dụng:

  1. Tách giao diện và thực hiện
  2. Cải thiện thời gian biên dịch / liên kết cho các chương trình lớn

C / C ++ có thể mở rộng quy mô từ các chương trình nhỏ đến các chương trình tập tin nhiều triệu, rất lớn. Phát triển ứng dụng có thể mở rộng từ các nhóm của một nhà phát triển đến hàng trăm nhà phát triển.

Bạn có thể đội vài chiếc mũ như một nhà phát triển. Cụ thể, bạn có thể là người sử dụng giao diện cho các hàm và lớp hoặc bạn có thể là người viết giao diện của các hàm và lớp.

Khi bạn đang sử dụng một chức năng, bạn cần biết giao diện chức năng, sử dụng tham số nào, chức năng nào trả về và bạn cần biết chức năng đó làm gì. Điều này dễ dàng được ghi lại trong tệp tiêu đề mà không cần nhìn vào việc thực hiện. Khi bạn đã đọc việc thực hiện printf? Mua chúng tôi sử dụng nó mỗi ngày.

Khi bạn là nhà phát triển giao diện, chiếc mũ sẽ thay đổi hướng khác. Tệp tiêu đề cung cấp khai báo của giao diện công cộng. Tệp tiêu đề xác định những gì một triển khai khác cần để sử dụng giao diện này. Thông tin nội bộ và riêng tư cho giao diện mới này không (và không nên) được khai báo trong tệp tiêu đề. Tệp tiêu đề công khai nên là tất cả mọi người cần sử dụng mô-đun.

Để phát triển quy mô lớn, việc biên dịch và liên kết có thể mất nhiều thời gian. Từ nhiều phút đến nhiều giờ (thậm chí đến nhiều ngày!). Việc chia phần mềm thành các giao diện (tiêu đề) và triển khai (nguồn) cung cấp phương pháp chỉ biên dịch các tệp cần được biên dịch thay vì xây dựng lại mọi thứ.

Ngoài ra, tệp tiêu đề cho phép nhà phát triển cung cấp thư viện (đã được biên dịch) cũng như tệp tiêu đề. Những người dùng khác của thư viện có thể không bao giờ thấy việc triển khai đúng, nhưng vẫn có thể sử dụng thư viện với tệp tiêu đề. Bạn làm điều này mỗi ngày với thư viện chuẩn C / C ++.

Ngay cả khi bạn đang phát triển một ứng dụng nhỏ, sử dụng các kỹ thuật phát triển phần mềm quy mô lớn là một thói quen tốt. Nhưng chúng ta cũng cần nhớ tại sao chúng ta sử dụng những thói quen này.


0

Bạn cũng có thể hỏi tại sao không đặt tất cả mã của bạn vào một tệp.

Câu trả lời đơn giản nhất là bảo trì mã.

Có những lúc hợp lý để tạo một lớp:

  • tất cả được nội tuyến trong một tiêu đề
  • tất cả trong một đơn vị biên dịch và không có tiêu đề nào cả.

Thời điểm hợp lý để hoàn toàn nội tuyến trong một tiêu đề là khi lớp thực sự là một cấu trúc dữ liệu với một vài getters và setters cơ bản và có lẽ là một hàm tạo lấy các giá trị để khởi tạo các thành viên của nó.

(Các mẫu, cần phải được nội tuyến, là một vấn đề hơi khác nhau).

Lần khác để tạo tất cả một lớp trong một tiêu đề là khi bạn có thể sử dụng lớp đó trong nhiều dự án và đặc biệt muốn tránh liên kết trong các thư viện.

Một thời gian khi bạn có thể bao gồm cả một lớp trong một đơn vị biên dịch và hoàn toàn không để lộ tiêu đề của nó là:

  • Một lớp "impl" chỉ được sử dụng bởi lớp mà nó thực hiện. Đây là một chi tiết triển khai của lớp đó và bên ngoài không được sử dụng.

  • Việc triển khai một lớp cơ sở trừu tượng được tạo bởi một loại phương thức nhà máy trả về một con trỏ / tham chiếu / con trỏ thông minh) cho lớp cơ sở. Phương thức nhà máy sẽ được phơi bày bởi chính lớp sẽ không được. (Ngoài ra, nếu lớp có một cá thể tự đăng ký trên một bảng thông qua một cá thể tĩnh, nó thậm chí không cần phải được hiển thị thông qua một nhà máy).

  • Một lớp loại "functor".

Nói cách khác, nơi bạn không muốn ai bao gồm tiêu đề.

Tôi biết những gì bạn có thể nghĩ ... Nếu chỉ vì khả năng bảo trì, bằng cách bao gồm tệp cpp (hoặc tiêu đề hoàn toàn nội tuyến), bạn có thể dễ dàng chỉnh sửa tệp để "tìm" mã và chỉ cần xây dựng lại.

Tuy nhiên "khả năng bảo trì" không chỉ có mã trông gọn gàng. Đó là một vấn đề về tác động của sự thay đổi. Người ta thường biết rằng nếu bạn giữ một tiêu đề không thay đổi và chỉ thay đổi một (.cpp)tệp thực hiện , thì sẽ không bắt buộc phải xây dựng lại nguồn khác vì sẽ không có tác dụng phụ.

Điều này làm cho "an toàn" hơn khi thực hiện các thay đổi như vậy mà không phải lo lắng về hiệu ứng gõ cửa và đó là những gì thực sự có nghĩa là "khả năng duy trì".


-1

Ví dụ của Snowman cần một phần mở rộng nhỏ để cho bạn thấy chính xác lý do tại sao các tệp .h là cần thiết.

Thêm một lớp Bar khác vào vở kịch cũng phụ thuộc vào lớp Foo.

Foo.h -> chứa khai báo cho lớp Foo

Foo.cpp -> chứa định nghĩa (cách đặt) của lớp Foo

Main.cpp -> sử dụng biến của loại Foo.

Bar.cpp -> cũng sử dụng biến loại Foo.

Bây giờ tất cả các tệp cpp cần bao gồm Foo.h Sẽ là một lỗi khi đưa Foo.cpp vào nhiều hơn một tệp cpp khác. Trình liên kết sẽ thất bại vì lớp Foo sẽ được định nghĩa nhiều lần.


1
Đó cũng không phải là nó. Điều đó có thể được giải quyết chính xác giống như cách nó được giải quyết trong .hcác tệp - dù sao cũng có các bộ bảo vệ và chính xác là cùng một vấn đề bạn gặp phải với .hcác tệp. Đây không phải là những gì .htập tin dành cho tất cả.
Benjamin Gruenbaum

Tôi không chắc chắn làm thế nào bạn sẽ sử dụng bao gồm các trình bảo vệ vì chúng chỉ hoạt động trên mỗi tệp (giống như tiền xử lý). Trong trường hợp này vì Main.cpp và Bar.cpp là các tệp khác nhau, Foo.cpp sẽ chỉ được đưa vào một lần trong cả hai (được bảo vệ hoặc không tạo ra bất kỳ sự khác biệt nào). Main.cpp và Bar.cpp đều sẽ biên dịch. Nhưng lỗi liên kết vẫn xảy ra do định nghĩa kép của lớp Foo. Tuy nhiên, đó cũng là sự thừa kế mà tôi thậm chí không đề cập đến. Tuyên bố cho các mô-đun bên ngoài (dll's), v.v.
SkaarjFace

Không, nó sẽ là một đơn vị biên dịch, không có liên kết nào diễn ra cả vì bạn bao gồm .cpptệp.
Benjamin Gruenbaum

Tôi không nói nó sẽ không biên dịch. Nhưng nó sẽ không liên kết cả main.o và bar.o trong cùng một tệp thực thi. Không liên kết những gì là điểm trong biên dịch cả.
SkaarjFace

Uhh ... lấy mã để chạy? Bạn vẫn có thể liên kết nó nhưng chỉ không liên kết các tệp với nhau - thay vào đó chúng là cùng một đơn vị biên dịch.
Benjamin Gruenbaum

-2

Nếu bạn viết toàn bộ mã trong cùng một tệp, thì nó sẽ làm cho mã của bạn trở nên xấu xí.
Thứ hai, bạn sẽ không thể chia sẻ lớp viết của mình với người khác. Theo kỹ thuật phần mềm, bạn nên viết mã khách hàng riêng biệt. Khách hàng không nên biết chương trình của bạn đang hoạt động như thế nào. Họ chỉ cần đầu ra Nếu bạn viết toàn bộ chương trình trong cùng một tệp, nó sẽ rò rỉ bảo mật chương trình của bạn.

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.