Tại sao nên sử dụng #ifndef CLASS_H và #define CLASS_H trong tệp .h nhưng không phải trong .cpp?


136

Tôi luôn thấy mọi người viết

lớp.h

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

Câu hỏi là, tại sao họ không làm điều đó cho tệp .cpp có chứa các định nghĩa cho các hàm lớp?

Hãy nói rằng tôi có main.cpp, và main.cppbao gồm class.h. Các class.htập tin không có includegì, vậy làm thế nào để main.cppbiết những gì trong class.cpp?


5
"Nhập khẩu" có lẽ không phải là từ bạn muốn sử dụng ở đây. Bao gồm.
Kate Gregory

5
Trong C ++, không có mối tương quan 1-1 giữa các tệp và các lớp. Bạn có thể đặt bao nhiêu lớp vào một tệp mà bạn muốn (hoặc thậm chí chia một lớp thành nhiều tệp, mặc dù điều này hiếm khi hữu ích). Do đó, vĩ mô nên FILE_H, không CLASS_H.
sbi

1
Xem lời khuyên bảo vệ của tôi .

Câu trả lời:


302

Đầu tiên, để giải quyết yêu cầu đầu tiên của bạn:

Khi bạn thấy điều này trong tệp .h :

#ifndef FILE_H
#define FILE_H

/* ... Declarations etc here ... */

#endif

Đây là một kỹ thuật tiền xử lý để ngăn chặn tệp tiêu đề được đưa vào nhiều lần, có thể gây ra sự cố vì nhiều lý do. Trong quá trình biên dịch dự án của bạn, mỗi tệp .cpp (thường) được biên dịch. Nói một cách đơn giản, điều này có nghĩa là trình biên dịch sẽ lấy tệp .cpp của bạn , mở bất kỳ tệp nào #includedbằng cách đó, ghép tất cả chúng thành một tệp văn bản lớn và sau đó thực hiện phân tích cú pháp và cuối cùng nó sẽ chuyển đổi nó thành một số mã trung gian, tối ưu hóa / thực hiện khác các tác vụ và cuối cùng tạo ra đầu ra lắp ráp cho kiến ​​trúc đích. Bởi vì điều này, nếu một tệp #includednhiều lần dưới một .cpptệp, trình biên dịch sẽ nối thêm nội dung tệp của nó hai lần, vì vậy nếu có các định nghĩa trong tệp đó, bạn sẽ gặp lỗi trình biên dịch cho bạn biết rằng bạn đã xác định lại một biến. Khi tệp được xử lý bởi bước tiền xử lý trong quá trình biên dịch, lần đầu tiên nội dung của nó đạt được hai dòng đầu tiên sẽ kiểm tra xem FILE_Hđã được xác định cho bộ tiền xử lý chưa. Nếu không, nó sẽ định nghĩa FILE_Hvà tiếp tục xử lý mã giữa nó và lệnh #endif. Lần tiếp theo nội dung của tập tin được xem bởi bộ xử lý, kiểm tra chống lại FILE_Hsẽ là sai, vì vậy nó sẽ ngay lập tức quét xuống #endifvà tiếp tục sau đó. Điều này ngăn ngừa lỗi xác định lại.

Và để giải quyết mối quan tâm thứ hai của bạn:

Trong lập trình C ++ như một thông lệ chung, chúng tôi tách phát triển thành hai loại tệp. Một là với phần mở rộng là .h và chúng tôi gọi đây là "tệp tiêu đề." Chúng thường cung cấp một khai báo các hàm, lớp, cấu trúc, biến toàn cục, typedefs, macro tiền xử lý và định nghĩa, v.v. Về cơ bản, chúng chỉ cung cấp cho bạn thông tin về mã của bạn. Sau đó, chúng tôi có phần mở rộng .cpp mà chúng tôi gọi là "tệp mã." Điều này sẽ cung cấp các định nghĩa cho các hàm đó, các thành viên lớp, bất kỳ thành viên cấu trúc nào cần định nghĩa, biến toàn cục, v.v. Vì vậy, tệp .h khai báo mã và tệp .cpp thực hiện khai báo đó. Vì lý do này, chúng tôi thường trong quá trình biên dịch biên dịch từng .cpptệp vào một đối tượng và sau đó liên kết các đối tượng đó (vì bạn hầu như không bao giờ thấy một tệp .cpp bao gồm tệp .cpp khác ).

Làm thế nào những giải pháp bên ngoài này được giải quyết là một công việc cho trình liên kết. Khi trình biên dịch của bạn xử lý main.cpp , nó sẽ nhận được các khai báo cho mã trong class.cpp bằng cách bao gồm class.h . Nó chỉ cần biết các hàm hoặc biến này trông như thế nào (đó là những gì một khai báo mang lại cho bạn). Vì vậy, nó biên dịch tệp main.cpp của bạn thành một số tệp đối tượng (gọi nó là main.obj ). Tương tự, class.cpp được biên dịch thành class.objtập tin. Để tạo ra tệp thực thi cuối cùng, một trình liên kết được gọi để liên kết hai tệp đối tượng đó với nhau. Đối với bất kỳ biến hoặc hàm bên ngoài chưa được giải quyết, trình biên dịch sẽ đặt một sơ khai nơi truy cập xảy ra. Sau đó, trình liên kết sẽ lấy sơ khai này và tìm mã hoặc biến trong tệp đối tượng được liệt kê khác và nếu tìm thấy, nó kết hợp mã từ hai tệp đối tượng thành tệp đầu ra và thay thế sơ khai bằng vị trí cuối cùng của hàm hoặc Biến đổi. Bằng cách này, mã của bạn trong main.cpp có thể gọi các hàm và sử dụng các biến trong class.cpp NẾU VÀ CHỈ NẾU NẾU ĐƯỢC GIẢI QUYẾT TRONG class.h .

Tôi hy vọng nó sẽ có ích.


Tôi đã cố gắng để hiểu .h và .cpp trong vài ngày qua. Câu trả lời này đã tiết kiệm thời gian và hứng thú của tôi để tìm hiểu C ++. Được viết tốt. Cảm ơn Justin!
Rajkumar R

bạn thực sự đã giải thích tuyệt vời! Có lẽ câu trả lời sẽ khá tốt nếu đó là với hình ảnh
alamin

13

Đây CLASS_Hlà một người bảo vệ bao gồm ; nó được sử dụng để tránh cùng một tệp tiêu đề được đưa vào nhiều lần (thông qua các tuyến khác nhau) trong cùng một tệp CPP (hoặc chính xác hơn là cùng một đơn vị dịch ), dẫn đến lỗi nhiều định nghĩa.

Bao gồm các vệ sĩ không cần thiết trên các tệp CPP bởi vì theo định nghĩa, nội dung của tệp CPP chỉ được đọc một lần.

Bạn dường như đã giải thích các trình bảo vệ bao gồm có chức năng tương tự như các importcâu lệnh trong các ngôn ngữ khác (như Java); Tuy nhiên, đó không phải là trường hợp. Bản #includethân nó gần tương đương với importcác ngôn ngữ khác.


2
"trong cùng một tệp CPP" nên đọc "trong cùng một đơn vị dịch".
dreamlax

@dreamlax: Điểm hay - đó là những gì ban đầu tôi sẽ viết, nhưng sau đó tôi nhận ra rằng ai đó không hiểu bao gồm các vệ sĩ sẽ chỉ bị nhầm lẫn bởi thuật ngữ "đơn vị dịch thuật". Tôi sẽ chỉnh sửa câu trả lời để thêm "đơn vị dịch" trong ngoặc đơn - đó phải là câu hỏi hay nhất của cả hai thế giới.
Martin B

6

Nó không - ít nhất là trong giai đoạn biên dịch.

Việc dịch chương trình c ++ từ mã nguồn sang mã máy được thực hiện theo ba giai đoạn:

  1. Tiền xử lý - Bộ tiền xử lý phân tích tất cả mã nguồn cho các dòng bắt đầu bằng # và thực thi các lệnh. Trong trường hợp của bạn, nội dung của tệp của bạn class.hđược chèn vào vị trí của dòng #include "class.h. Vì bạn có thể được bao gồm trong tệp tiêu đề của mình ở một số vị trí, nên các #ifndefmệnh đề tránh các lỗi khai báo trùng lặp, vì chỉ thị tiền xử lý không được xác định chỉ lần đầu tiên bao gồm tệp tiêu đề.
  2. Biên dịch - Trình biên dịch hiện dịch tất cả các tệp mã nguồn đã xử lý trước thành các tệp đối tượng nhị phân.
  3. Liên kết - Trình liên kết liên kết (do đó là tên) với các tệp đối tượng. Một tham chiếu đến lớp của bạn hoặc một trong các phương thức của nó (cần được khai báo trong class.h và được định nghĩa trong class.cpp) được phân giải thành phần bù tương ứng trong một trong các tệp đối tượng. Tôi viết 'một trong các tệp đối tượng của bạn' vì lớp của bạn không cần được định nghĩa trong tệp có tên class.cpp, nó có thể nằm trong thư viện được liên kết với dự án của bạn.

Tóm lại, các khai báo có thể được chia sẻ thông qua một tệp tiêu đề, trong khi việc ánh xạ các khai báo thành các định nghĩa được thực hiện bởi trình liên kết.


4

Đó là sự khác biệt giữa khai báo và định nghĩa. Các tệp tiêu đề thường chỉ bao gồm khai báo và tệp nguồn chứa định nghĩa.

Để sử dụng một cái gì đó bạn chỉ cần biết khai báo không phải là định nghĩa. Chỉ người liên kết cần biết định nghĩa.

Vì vậy, đây là lý do tại sao bạn sẽ bao gồm một tệp tiêu đề bên trong một hoặc nhiều tệp nguồn nhưng bạn sẽ không bao gồm một tệp nguồn bên trong một tệp khác.

Ngoài ra bạn có nghĩa là #includevà không nhập khẩu.


3

Điều đó được thực hiện cho các tệp tiêu đề để nội dung chỉ xuất hiện một lần trong mỗi tệp nguồn được xử lý trước, ngay cả khi nó được bao gồm nhiều lần (thường là do nó được bao gồm từ các tệp tiêu đề khác). Lần đầu tiên bao gồm, biểu tượng CLASS_H(được gọi là bảo vệ bao gồm ) chưa được xác định, vì vậy tất cả nội dung của tệp được bao gồm. Làm điều này xác định ký hiệu, vì vậy nếu nó được bao gồm lại, nội dung của tệp (bên trong khối #ifndef/ #endif) sẽ bị bỏ qua.

Không cần phải làm điều này cho chính tệp nguồn vì (thông thường) không được bao gồm bởi bất kỳ tệp nào khác.

Đối với câu hỏi cuối cùng của bạn, class.hnên chứa định nghĩa của lớp và khai báo của tất cả các thành viên, các hàm liên quan và bất cứ điều gì khác, để bất kỳ tệp nào có chứa nó đều có đủ thông tin để sử dụng lớp. Việc thực hiện các chức năng có thể đi trong một tệp nguồn riêng biệt; bạn chỉ cần khai báo để gọi cho họ.


2

main.cpp không cần phải biết những gì trong class.cpp . Nó chỉ cần biết các khai báo của các hàm / lớp mà nó sẽ sử dụng và các khai báo này nằm trong class.h .

Trình liên kết liên kết giữa các vị trí nơi các hàm / lớp được khai báo trong class.h được sử dụng và các cài đặt của chúng trong class.cpp


1

.cppcác tập tin không được bao gồm (sử dụng #include) vào các tập tin khác. Vì vậy, họ không cần bao gồm bảo vệ. Main.cppsẽ biết tên và chữ ký của lớp mà bạn đã triển khai class.cppchỉ vì bạn đã chỉ định tất cả những điều đó class.h- đây là mục đích của tệp tiêu đề. (Tùy thuộc vào bạn để đảm bảo class.hmô tả chính xác mã bạn triển khai class.cpp.) Mã thực thi trong class.cppsẽ được cung cấp cho mã thực thi main.cppnhờ vào nỗ lực của trình liên kết.


1

Nhìn chung, các mô-đun mã như .cppcác tệp được biên dịch một lần và được liên kết trong nhiều dự án, để tránh việc biên dịch logic lặp đi lặp lại không cần thiết. Ví dụ, g++ -o class.cppsẽ tạo ra class.ocái mà sau đó bạn có thể liên kết từ nhiều dự án để sử dụng g++ main.cpp class.o.

Chúng tôi có thể sử dụng #includenhư trình liên kết của chúng tôi, như bạn có nghĩa là ngụ ý, nhưng điều đó sẽ thật ngớ ngẩn khi chúng tôi biết cách liên kết đúng bằng trình biên dịch của chúng tôi với ít thao tác gõ phím và ít lặp lại lãng phí trong quá trình biên dịch, thay vì mã của chúng tôi có nhiều lần nhấn phím hơn và lãng phí hơn sự lặp lại của việc biên soạn ...

Tuy nhiên, các tệp tiêu đề vẫn được yêu cầu đưa vào từng dự án, bởi vì điều này cung cấp giao diện cho mỗi mô-đun. Nếu không có các tiêu đề này, trình biên dịch sẽ không biết về bất kỳ ký hiệu nào được giới thiệu bởi các .otệp.

Điều quan trọng là phải nhận ra rằng các tệp tiêu đề là những gì giới thiệu định nghĩa của các ký hiệu cho các mô-đun đó; một khi điều đó được nhận ra thì sẽ có ý nghĩa rằng nhiều thể vùi có thể gây ra việc xác định lại các ký hiệu (gây ra lỗi), vì vậy chúng tôi sử dụng các vệ sĩ để ngăn chặn các định nghĩa lại đó.


0

bởi vì Headerfiles định nghĩa những gì lớp chứa (Thành viên, cấu trúc dữ liệu) và các tệp cpp thực hiện nó.

Và tất nhiên, lý do chính cho điều này là bạn có thể bao gồm một tệp .h nhiều lần trong các tệp .h khác, nhưng điều này sẽ dẫn đến nhiều định nghĩa của một lớp, không hợp lệ.

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.