Tại sao C ++ có tệp tiêu đề và tệp .cpp?
Tại sao C ++ có tệp tiêu đề và tệp .cpp?
Câu trả lời:
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.
Một phần tổng hợp trong C ++ được thực hiện theo 2 giai đoạn chính:
Đầ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".
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?
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 ...
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
}
include
là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.
}
Trong trường hợp hiện tại, điều này là không cần thiết và B.HPP có doSomethingElse
khai 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ó.
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
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 đó.
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 foo
là 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.
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).
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?) :)
Why can't I be sure, that foo(bar) is a function
foo 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?
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.
Bởi vì C ++ thừa hưởng chúng từ C. Thật không may.
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).
Đó 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.
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:
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 ...