Tại sao có tệp tiêu đề và tệp .cpp? [đóng cửa]


484

Tại sao C ++ có tệp tiêu đề và tệp .cpp?



đó là một mô hình OOP phổ biến, .h là một khai báo lớp và cpp là định nghĩa. Một người không cần biết nó được thực thi như thế nào, anh ấy / cô ấy chỉ nên biết giao diện.
Manish Kakati

Đây là phần tốt nhất của giao diện tách c ++ khỏi thực hiện. Nó luôn luôn tốt hơn là giữ tất cả mã trong một tệp, chúng tôi có giao diện riêng biệt. Một số lượng mã luôn ở đó như hàm nội tuyến là một phần của tệp tiêu đề. Có vẻ tốt khi nhìn thấy một tệp tiêu đề hiển thị danh sách các hàm được khai báo và các biến lớp.
Miank

Đôi khi các tệp tiêu đề rất cần thiết cho việc biên dịch - không chỉ là tùy chọn tổ chức hoặc cách phân phối các thư viện được biên dịch trước. Giả sử bạn có cấu trúc trong đó game.c phụ thuộc vào BOTH vật lý.c và math.c; vật lý.c cũng phụ thuộc vào math.c. Nếu bạn đã bao gồm các tệp .c và quên các tệp .h mãi mãi, bạn sẽ có các khai báo trùng lặp từ math.c và không có hy vọng biên dịch. Đây là điều có ý nghĩa nhất đối với tôi tại sao các tệp tiêu đề lại quan trọng. Hy vọng nó sẽ giúp người khác.
Samy Bencherif

Tôi nghĩ rằng nó phải được thực hiện với thực tế là chỉ cho phép các ký tự chữ và số trong phần mở rộng. Tôi thậm chí không biết điều đó có đúng không, chỉ là đoán
dùng12211554

Câu trả lời:


201

Chà, lý do chính sẽ là để tách giao diện khỏi việc thực hiện. Tiêu đề tuyên bố "những gì" một lớp (hoặc bất cứ điều gì đang được triển khai) sẽ làm, trong khi tệp cpp định nghĩa "cách" nó sẽ thực hiện các tính năng đó.

Điều này làm giảm sự phụ thuộc để mã sử dụng tiêu đề không nhất thiết phải biết tất cả các chi tiết triển khai và bất kỳ lớp / tiêu đề nào khác chỉ cần cho điều đó. Điều này sẽ giảm thời gian biên dịch và cả số lượng biên dịch lại cần thiết khi có gì đó trong quá trình thực hiện thay đổi.

Nó không hoàn hảo và bạn thường sử dụng các kỹ thuật như Thành ngữ Pimpl để phân tách giao diện và triển khai đúng cách, nhưng đó là một khởi đầu tốt.


178
Không thực sự đúng. Tiêu đề vẫn chứa một phần chính của việc thực hiện. Từ khi nào các biến riêng tư là một phần của giao diện của lớp? Chức năng thành viên tư nhân? Vậy thì cái quái gì họ đang làm trong tiêu đề hiển thị công khai? Và nó rơi xa hơn với các mẫu.
jalf

13
Đó là lý do tại sao tôi nói rằng nó không hoàn hảo, và thành ngữ Pimpl là cần thiết cho sự tách biệt nhiều hơn. Các mẫu là một loại sâu hoàn toàn khác nhau - ngay cả khi từ khóa "xuất khẩu" được hỗ trợ đầy đủ trong hầu hết các trình biên dịch, nó vẫn sẽ cho tôi cú pháp đường chứ không phải là phân tách thực sự.
Joris Timmermans

4
Làm thế nào để các ngôn ngữ khác xử lý này? ví dụ - Java? Không có khái niệm tệp tiêu đề trong Java.
Lazer

8
@Lazer: Java đơn giản hơn để phân tích cú pháp. Trình biên dịch Java có thể phân tích cú pháp một tệp mà không cần biết tất cả các lớp trong các tệp khác và kiểm tra các loại sau. Trong C ++, rất nhiều cấu trúc không rõ ràng nếu không có thông tin về kiểu, vì vậy trình biên dịch C ++ cần thông tin về các kiểu được tham chiếu để phân tích một tệp. Đó là lý do tại sao nó cần tiêu đề.
Niki

15
@nikie: "Dễ dàng" phân tích cú pháp phải làm gì với nó? Nếu Java có một ngữ pháp ít nhất là phức tạp như C ++, thì nó vẫn có thể sử dụng các tệp java. Trong cả hai trường hợp, những gì về C? C dễ phân tích cú pháp, nhưng sử dụng cả tệp tiêu đề và tệp c.
Thomas Eding

609

Biên dịch C ++

Một phần tổng hợp trong C ++ được thực hiện theo 2 giai đoạn chính:

  1. Đầu tiên là việc biên dịch các tệp văn bản "nguồn" thành các tệp "đối tượng" nhị phân: Tệp CPP là tệp được biên dịch và được biên dịch mà không có bất kỳ kiến ​​thức nào về các tệp CPP khác (hoặc thậm chí là các thư viện), trừ khi được cung cấp cho nó thông qua khai báo thô hoặc bao gồm tiêu đề. Tệp CPP thường được biên dịch thành tệp .OBJ hoặc .O "đối tượng".

  2. Thứ hai là liên kết với nhau của tất cả các tệp "đối tượng" và do đó, việc tạo tệp nhị phân cuối cùng (có thể là thư viện hoặc tệp thực thi).

HPP phù hợp ở đâu trong tất cả quá trình này?

Một tệp CPP đơn độc kém ...

Quá trình biên dịch của mỗi tệp CPP độc lập với tất cả các tệp CPP khác, điều đó có nghĩa là nếu A.CPP cần một ký hiệu được xác định trong B.CPP, như:

// A.CPP
void doSomething()
{
   doSomethingElse(); // Defined in B.CPP
}

// B.CPP
void doSomethingElse()
{
   // Etc.
}

Nó sẽ không được biên dịch vì A.CPP không có cách nào để biết "doS SomethingElse" tồn tại ... Trừ khi có một tuyên bố trong A.CPP, như:

// A.CPP
void doSomethingElse() ; // From B.CPP

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

Sau đó, nếu bạn có C.CPP sử dụng cùng một ký hiệu, thì bạn sao chép / dán khai báo ...

SAO CHÉP / PASTE!

Vâng, có một vấn đề. Sao chép / dán là nguy hiểm, và khó bảo trì. Điều đó có nghĩa là sẽ rất tuyệt nếu chúng ta có cách nào đó để KHÔNG sao chép / dán và vẫn khai báo biểu tượng ... Làm thế nào chúng ta có thể làm điều đó? Bằng cách bao gồm một số tệp văn bản, thường được thêm vào bởi .h, .hxx, .h ++ hoặc, ưu tiên của tôi cho các tệp C ++, .hpp:

// B.HPP (here, we decided to declare every symbol defined in B.CPP)
void doSomethingElse() ;

// A.CPP
#include "B.HPP"

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

// B.CPP
#include "B.HPP"

void doSomethingElse()
{
   // Etc.
}

// C.CPP
#include "B.HPP"

void doSomethingAgain()
{
   doSomethingElse() ; // Defined in B.CPP
}

Làm thế nào để includelàm việc?

Về bản chất, bao gồm một tệp sẽ phân tích cú pháp và sau đó sao chép-dán nội dung của nó trong tệp CPP.

Ví dụ: trong đoạn mã sau, với tiêu đề A.HPP:

// A.HPP
void someFunction();
void someOtherFunction();

... nguồn B.CPP:

// B.CPP
#include "A.HPP"

void doSomething()
{
   // Etc.
}

... sẽ trở thành sau khi đưa vào:

// B.CPP
void someFunction();
void someOtherFunction();

void doSomething()
{
   // Etc.
}

Một điều nhỏ - tại sao lại bao gồm B.HPP trong B.CPP?

Trong trường hợp hiện tại, điều này là không cần thiết và B.HPP có doSomethingElsekhai báo hàm và B.CPP có doSomethingElseđịnh nghĩa hàm (chính nó là một khai báo). Nhưng trong trường hợp tổng quát hơn, trong đó B.HPP được sử dụng để khai báo (và mã nội tuyến), không thể có định nghĩa tương ứng (ví dụ: enums, structs, v.v.), vì vậy có thể cần bao gồm nếu B.CPP sử dụng những tuyên bố từ B.HPP. Nói chung, đó là "hương vị tốt" cho một nguồn để mặc định bao gồm tiêu đề của nó.

Phần kết luận

Do đó, tệp tiêu đề là cần thiết, bởi vì trình biên dịch C ++ không thể tìm kiếm các khai báo ký hiệu một mình, và do đó, bạn phải giúp nó bằng cách bao gồm các khai báo đó.

Một từ cuối cùng: Bạn nên đặt các bộ bảo vệ tiêu đề xung quanh nội dung của các tệp HPP của bạn, để đảm bảo nhiều thể vùi sẽ không phá vỡ bất cứ điều gì, nhưng tất cả, tôi tin rằng lý do chính cho sự tồn tại của các tệp HPP đã được giải thích ở trên.

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_

hoặc thậm chí đơn giản hơn

#pragma once

// The declarations in the B.hpp file

2
@nimcap :: You still have to copy paste the signature from header file to cpp file, don't you?Không cần. Miễn là CPP "bao gồm" HPP, trình biên dịch trước sẽ tự động thực hiện sao chép-dán nội dung của tệp HPP vào tệp CPP. Tôi cập nhật câu trả lời để làm rõ điều đó.
paercebal

7
@Bob : While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself. Không, nó không. Nó chỉ biết các loại được cung cấp bởi người dùng, sẽ mất một nửa thời gian để đọc giá trị trả về. Sau đó, chuyển đổi ngầm xảy ra. Và sau đó, khi bạn có mã : foo(bar), bạn thậm chí không thể chắc chắn foolà một hàm. Vì vậy, trình biên dịch phải có quyền truy cập vào thông tin trong các tiêu đề để quyết định xem nguồn có biên dịch chính xác hay không ... Sau đó, khi mã được biên dịch, trình liên kết sẽ chỉ liên kết các lệnh gọi với nhau.
paercebal

3
@Bob: [tiếp tục] ... Bây giờ, trình liên kết có thể thực hiện công việc được thực hiện bởi trình biên dịch, tôi đoán, sau đó sẽ làm cho tùy chọn của bạn có thể. (Tôi đoán đây là chủ đề của đề xuất "mô-đun" cho tiêu chuẩn tiếp theo). Seems, they're just a pretty ugly arbitrary design.: Nếu C ++ đã được tạo ra vào năm 2012, thực sự. Nhưng hãy nhớ rằng C ++ được xây dựng dựa trên C vào những năm 1980 và tại thời điểm đó, các ràng buộc khá khác biệt vào thời điểm đó (IIRC, người ta đã quyết định cho các mục đích thông qua để giữ các liên kết giống như C).
paercebal

1
@paercebal Cảm ơn bạn đã giải thích và ghi chú, paercebal! Tại sao tôi không chắc chắn, đó foo(bar)là một hàm - nếu nó được lấy làm con trỏ? Trong thực tế, nói về thiết kế xấu, tôi đổ lỗi cho C, không phải C ++. Tôi thực sự không thích một số ràng buộc của C thuần túy, chẳng hạn như có các tệp tiêu đề hoặc có các hàm trả về một và chỉ một giá trị, trong khi nhận nhiều đối số trên đầu vào (không cảm thấy tự nhiên khi đầu vào và đầu ra hoạt động theo cách tương tự ; tại sao nhiều đối số, nhưng đầu ra duy nhất?) :)
Boris Burkov

1
@Bobo :: Why can't I be sure, that foo(bar) is a functionfoo có thể là một loại, vì vậy bạn sẽ có một hàm tạo lớp được gọi. In fact, speaking of bad design, I blame C, not C++: Tôi có thể đổ lỗi cho C rất nhiều thứ, nhưng đã được thiết kế vào những năm 70 sẽ không phải là một trong số đó. Một lần nữa, các ràng buộc của thời điểm đó ... such as having header files or having functions return one and only one value: Tuples có thể giúp giảm thiểu điều đó, cũng như chuyển các đối số bằng cách tham chiếu. Bây giờ, cú pháp để truy xuất nhiều giá trị được trả về là gì và nó có đáng để thay đổi ngôn ngữ không?
paercebal

93

Bởi vì C, nơi khái niệm bắt nguồn, đã 30 tuổi và trước đó, đó là cách khả thi duy nhất để liên kết mã với nhau từ nhiều tệp.

Ngày nay, đó là một vụ hack khủng khiếp phá hủy hoàn toàn thời gian biên dịch trong C ++, gây ra vô số phụ thuộc không cần thiết (vì các định nghĩa lớp trong tệp tiêu đề phơi bày quá nhiều thông tin về việc triển khai), v.v.


3
Tôi tự hỏi tại sao các tệp tiêu đề (hoặc bất cứ thứ gì thực sự cần thiết cho việc biên dịch / liên kết) không chỉ đơn giản là "tự động tạo"?
Mateen Ulhaq

54

Bởi vì trong C ++, mã thực thi cuối cùng không mang bất kỳ thông tin ký hiệu nào, nó ít nhiều là mã máy thuần túy.

Vì vậy, bạn cần một cách để mô tả giao diện của một đoạn mã, tách biệt với chính mã. Mô tả này là trong tập tin tiêu đề.


16

Bởi vì C ++ thừa hưởng chúng từ C. Thật không may.


4
Tại sao thừa kế C ++ từ C là không may?
Lokesh

3
@Lokesh Bởi vì hành lý của nó :(
陳力

1
Làm thế nào điều này có thể là một câu trả lời?
Shuvo Sarker

14
@ShuvoSarker vì như hàng ngàn ngôn ngữ đã chứng minh, không có lời giải thích kỹ thuật nào cho C ++ khiến các lập trình viên viết chữ ký hàm hai lần. Câu trả lời cho "tại sao?" là "lịch sử".
Boris

15

Bởi vì những người thiết kế định dạng thư viện không muốn "lãng phí" dung lượng cho thông tin hiếm khi được sử dụng như macro C tiền xử lý và khai báo hàm.

Vì bạn cần thông tin đó để thông báo cho trình biên dịch của mình "chức năng này sẽ khả dụng sau khi trình liên kết thực hiện công việc của nó", họ đã phải đưa ra một tệp thứ hai nơi thông tin được chia sẻ này có thể được lưu trữ.

Hầu hết các ngôn ngữ sau C / C ++ đều lưu trữ thông tin này trong đầu ra (ví dụ mã byte Java) hoặc chúng không sử dụng định dạng được biên dịch trước, luôn được phân phối ở dạng nguồn và biên dịch nhanh chóng (Python, Perl).


Sẽ không hoạt động, tài liệu tham khảo theo chu kỳ. Ieyou không thể xây dựng a.lib từ a.cpp trước khi xây dựng b.lib từ b.cpp, nhưng bạn cũng không thể xây dựng b.lib trước a.lib.
MSalters

20
Java đã giải quyết điều đó, Python có thể làm điều đó, bất kỳ ngôn ngữ hiện đại nào cũng có thể làm được. Nhưng tại thời điểm C được phát minh, RAM rất đắt và khan hiếm, nó không phải là một lựa chọn.
Aaron Digulla

6

Đó là cách tiền xử lý khai báo giao diện. Bạn đặt giao diện (khai báo phương thức) vào tệp tiêu đề và triển khai vào cpp. Các ứng dụng sử dụng thư viện của bạn chỉ cần biết giao diện mà chúng có thể truy cập thông qua #include.


4

Thường thì bạn sẽ muốn có một định nghĩa về giao diện mà không phải gửi toàn bộ mã. Ví dụ: nếu bạn có một thư viện dùng chung, bạn sẽ gửi một tệp tiêu đề với nó xác định tất cả các chức năng và ký hiệu được sử dụng trong thư viện dùng chung. Nếu không có tệp tiêu đề, bạn sẽ cần gửi nguồn.

Trong một dự án, các tệp tiêu đề được sử dụng, IMHO, cho ít nhất hai mục đích:

  • Rõ ràng, bằng cách giữ cho các giao diện tách biệt với việc thực hiện, việc đọc mã sẽ dễ dàng hơn
  • Thời gian biên dịch. Bằng cách chỉ sử dụng giao diện khi có thể, thay vì thực hiện đầy đủ, thời gian biên dịch có thể giảm xuống vì trình biên dịch có thể chỉ cần tham chiếu đến giao diện thay vì phải phân tích mã thực tế (mà theo ý tưởng, chỉ cần thực hiện một lần duy nhất).

3
Tại sao các nhà cung cấp thư viện không thể gửi tệp "tiêu đề" được tạo? Tệp "tiêu đề" miễn phí tiền xử lý sẽ cho hiệu năng tốt hơn nhiều (trừ khi việc triển khai thực sự bị hỏng).
Tom Hawtin - tackline

Tôi nghĩ nó không liên quan nếu tệp tiêu đề được tạo hoặc viết bằng tay, câu hỏi không phải là "tại sao mọi người lại tự viết tệp tiêu đề?", Đó là "tại sao chúng ta có tệp tiêu đề". Điều tương tự cũng xảy ra đối với các tiêu đề miễn phí tiền xử lý. Chắc chắn, điều này sẽ nhanh hơn.

-5

Trả lời câu trả lời của MadKeithV ,

Điều này làm giảm sự phụ thuộc để mã sử dụng tiêu đề không nhất thiết phải biết tất cả các chi tiết triển khai và bất kỳ lớp / tiêu đề nào khác chỉ cần cho điều đó. Điều này sẽ giảm thời gian biên dịch và cũng là lượng biên dịch lại cần thiết khi có gì đó trong quá trình thực hiện thay đổi.

Một lý do khác là một tiêu đề cung cấp một id duy nhất cho mỗi lớp.

Vì vậy, nếu chúng ta có một cái gì đó như

class A {..};
class B : public A {...};

class C {
    include A.cpp;
    include B.cpp;
    .....
};

Chúng tôi sẽ có lỗi, khi chúng tôi cố gắng xây dựng dự án, vì A là một phần của B, với các tiêu đề, chúng tôi sẽ tránh được loại đau đầu này ...

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.