2013-08-30 89 views
20

Thời gian suy nghĩ - Tại sao bạn vẫn muốn chia nhỏ tệp của mình?

Như tiêu đề cho thấy, vấn đề cuối cùng mà tôi có là nhiều lỗi trình liên kết định nghĩa. Tôi đã thực sự khắc phục vấn đề, nhưng tôi đã không cố định vấn đề một cách chính xác. Trước khi bắt đầu, tôi muốn thảo luận về các lý do để chia nhỏ một tệp lớp thành nhiều tệp. Tôi đã cố gắng để đưa tất cả các kịch bản có thể ở đây - nếu tôi bỏ lỡ bất kỳ, xin vui lòng nhắc nhở tôi và tôi có thể thay đổi. Hy vọng rằng sau đây là đúng:Tách mã lớp C++ thành nhiều tệp, quy tắc là gì?

Lý do 1Để tiết kiệm không gian:

Bạn có một tập tin có chứa các khai báo một lớp học với tất cả các thành viên lớp. Bạn đặt #include guards xung quanh tệp này (hoặC#pragma một lần) để đảm bảo không có xung đột phát sinh nếu bạn #bao gồm tệp trong hai tệp tiêu đề khác nhau sau đó được đưa vào tệp nguồn. Bạn biên dịch một tệp nguồn riêng biệt với việc thực hiện bất kỳ phương thức nào được khai báo trong lớp này, vì nó tải nhiều dòng mã từ tệp nguồn của bạn, làm sạch mọi thứ một chút và giới thiệu một số thứ tự cho chương trình của bạn.

Ví dụ: Như bạn có thể thấy, ví dụ dưới đây có thể được cải thiện bằng cách chia nhỏ việc triển khai các phương thức lớp thành một tệp khác. (Một tập tin cpp)

// my_class.hpp 
#pragma once 

class my_class 
{ 
public: 
    void my_function() 
    { 
     // LOTS OF CODE 
     // CONFUSING TO DEBUG 
     // LOTS OF CODE 
     // DISORGANIZED AND DISTRACTING 
     // LOTS OF CODE 
     // LOOKS HORRIBLE 
     // LOTS OF CODE 
     // VERY MESSY 
     // LOTS OF CODE 
    } 

    // MANY OTHER METHODS 
    // MEANS VERY LARGE FILE WITH LOTS OF LINES OF CODE 
} 

Lý do 2Để ngăn chặn nhiều lỗi định nghĩa mối liên kết:

Có lẽ đây là lý do chính tại sao bạn sẽ chia thực hiện khai báo. Trong ví dụ trên, bạn có thể di chuyển thân phương thức ra bên ngoài lớp. Điều này sẽ làm cho nó trông sạch hơn và có cấu trúc. Tuy nhiên, theo điều này question, ví dụ trên có ẩn số inline chỉ định. Di chuyển việc thực hiện từ bên trong lớp ra bên ngoài lớp, như trong ví dụ bên dưới, sẽ gây ra cho bạn các lỗi trình liên kết, và vì vậy bạn sẽ nội tuyến mọi thứ, hoặc di chuyển các định nghĩa hàm tới một tệp .cpp.

Ví dụ: _ Ví dụ bên dưới sẽ gây ra "nhiều lỗi trình liên kết định nghĩa" nếu bạn không di chuyển định nghĩa hàm sang tệp .cpp hoặc chỉ định hàm làm nội dòng.

// my_class.hpp 
void my_class::my_function() 
{ 
    // ERROR! MULTIPLE DEFINITION OF my_class::my_function 
    // This error only occurs if you #include the file containing this code 
    // in two or more separate source (compiled, .cpp) files. 
} 

Để khắc phục vấn đề này:

//my_class.cpp 
void my_class::my_function() 
{ 
    // Now in a .cpp file, so no multiple definition error 
} 

Hoặc:

// my_class.hpp 
inline void my_class::my_function() 
{ 
    // Specified function as inline, so okay - note: back in header file! 
    // The very first example has an implicit `inline` specifier 
} 

Lý do 3Bạn muốn tiết kiệm không gian, một lần nữa, nhưng lần này bạn đang làm việc với một lớp mẫu:

Nếu chúng tôi đang làm việc với các lớp mẫu, thì chúng tôi không thể di chuyển việc triển khai sang tệp nguồn (tệp .cpp). Điều đó hiện không được cho phép bởi (tôi giả định) hoặc là các trình biên dịch tiêu chuẩn hoặc theo hiện tại. Không giống như ví dụ đầu tiên của Lý do 2, ở trên, chúng tôi được phép đặt triển khai trong tệp tiêu đề.Theo điều này question lý do là các phương thức lớp mẫu cũng ngụ ý các chỉ định inline. Đúng không? (Dường như có ý nghĩa.) Nhưng dường như không ai biết về câu hỏi mà tôi vừa mới tham chiếu!

Vì vậy, hai ví dụ dưới đây giống hệt nhau?

// some_header_file.hpp 
#pragma once 

// template class declaration goes here 
class some_class 
{ 
    // Some code 
}; 

// Example 1: NO INLINE SPECIFIER 
template<typename T> 
void some_class::class_method() 
{ 
    // Some code 
} 

// Example 2: INLINE specifier used 
template<typename T> 
inline void some_class::class_method() 
{ 
    // Some code 
} 

Nếu bạn có một tập tin mẫu lớp header, mà đang trở thành khổng lồ do cho tất cả các chức năng mà bạn có, sau đó tôi tin rằng bạn được phép di chuyển các định nghĩa chức năng vào một tập tin tiêu đề (thường là một tập tin .tpp?) và sau đó #include file.tpp ở cuối tệp tiêu đề của bạn có chứa khai báo lớp. Tuy nhiên, bạn KHÔNG được bao gồm tệp này ở bất kỳ nơi nào khác, do đó, .tpp thay vì .hpp.

Tôi cho rằng bạn cũng có thể làm điều này với các phương thức nội tuyến của một lớp thông thường? Điều đó cũng được cho phép?

Question Time

Vì vậy, tôi đã thực hiện một số báo cáo trên, hầu hết trong số đó liên quan đến cơ cấu các tập tin nguồn. Tôi nghĩ mọi thứ tôi nói đều đúng, bởi vì tôi đã làm một số nghiên cứu cơ bản và "tìm ra một số thứ", nhưng đây là một câu hỏi và vì vậy tôi không biết chắc chắn.

Điều này tóm tắt, là cách bạn sắp xếp mã trong các tệp. Tôi nghĩ rằng tôi đã tìm ra một cấu trúc sẽ luôn hoạt động.

Đây là những gì tôi đã đưa ra. (Đây là "tiêu chuẩn mã lớp tổ chức tập tin/cấu trúc Ed chim", nếu bạn thích Không biết nếu nó sẽ rất hữu ích nào, đó là điểm của yêu cầu..)

  • 1: Khai báo lớp (mẫu hoặc cách khác) trong một tệp .hpp, bao gồm tất cả các phương thức, hàm và dữ liệu của bạn bè.
  • 2: Ở cuối tệp .hpp, #include tệp .tpp chứa việc triển khai bất kỳ phương thức inline nào. Tạo tệp .tpp và đảm bảo tất cả các phương thức được chỉ định là inline.
  • 3: Tất cả các thành viên khác (chức năng phi inline, chức năng người bạn và dữ liệu tĩnh) nên được định nghĩa trong một tập tin .cpp, mà #include s file .hpp ở phía trên để ngăn chặn các lỗi như "lớp ABC đã không được công bố ". Vì mọi thứ trong tệp này sẽ có liên kết bên ngoài, chương trình sẽ liên kết chính xác.

Các tiêu chuẩn như thế này tồn tại trong ngành? Liệu tiêu chuẩn tôi đưa ra có hoạt động trong mọi trường hợp?

+0

btw Tôi đã bỏ qua câu hỏi của bạn vì câu hỏi quá dài. Giới thiệu về hàm mẫu nội tuyến. Từ khóa nội tuyến không bắt buộc (bạn vẫn được phép đặt chúng trong các tệp tiêu đề) và không có bất kỳ hiệu ứng được bảo đảm nào. Sử dụng từ khóa có thể làm cho trình biên dịch có nhiều khả năng nội tuyến hàm, nhưng không có gì là chắc chắn. Các hàm mẫu này không phải là các hàm nội tuyến ngầm. Câu trả lời cho câu hỏi đó là sai, như đã thảo luận trong các bình luận của câu trả lời. –

+0

@NeilKirk Ah cảm ơn, chắc chắn dường như có rất nhiều bất đồng về nó. – user3728501

+0

Chỉ cần bỏ qua đặt nội tuyến ở đó và vấn đề sẽ không bao giờ làm phiền bạn :) –

Trả lời

4

Ba điểm của bạn có âm thanh phù hợp. Đó là cách tiêu chuẩn để làm mọi thứ (mặc dù trước đây tôi chưa từng thấy phần mở rộng .tpp trước đây, thường là nó .inl), mặc dù cá nhân tôi chỉ đặt các hàm nội tuyến ở cuối tệp tiêu đề chứ không phải trong một tệp riêng biệt.

Đây là cách tôi sắp xếp tệp của mình. Tôi bỏ qua tệp kê khai chuyển tiếp cho các lớp đơn giản.

myclass-fwd.h

#pragma once 

namespace NS 
{ 
class MyClass; 
} 

myclass.h

#pragma once 
#include "headers-needed-by-header" 
#include "myclass-fwd.h" 

namespace NS 
{ 
class MyClass 
{ 
    .. 
}; 
} 

MyClass.cpp

#include "headers-needed-by-source" 
#include "myclass.h" 

namespace 
    { 
    void LocalFunc(); 
} 

NS::MyClass::... 

Thay pragma với lính gác tiêu đề theo sở thích ..

Lý do cho phương pháp này là để giảm phụ thuộc tiêu đề, làm chậm thời gian biên dịch trong các dự án lớn. Nếu bạn không biết, bạn có thể chuyển tiếp khai báo một lớp để sử dụng như một con trỏ hoặc tham chiếu. Việc khai báo đầy đủ chỉ cần thiết khi bạn xây dựng, tạo hoặc sử dụng các thành viên của lớp.

Điều này có nghĩa là một lớp khác sử dụng lớp (lấy tham số bằng con trỏ/tham chiếu) chỉ phải bao gồm tiêu đề fwd trong tiêu đề của chính nó. Sau đó, tiêu đề đầy đủ được đưa vào tệp nguồn của lớp thứ hai. Điều này làm giảm đáng kể số lượng rác không cần thiết mà bạn nhận được khi kéo vào một tiêu đề lớn, mà kéo trong một tiêu đề lớn, mà kéo trong một ...

Mẹo tiếp theo là không gian tên không tên (đôi khi được gọi là không gian tên vô danh). Điều này chỉ có thể xuất hiện trong tệp nguồn và nó giống như một vùng tên ẩn chỉ hiển thị với tệp đó. Bạn có thể đặt các hàm cục bộ, các lớp, v.v. ở đây chỉ được sử dụng bởi tệp nguồn. Điều này ngăn ngừa xung đột tên nếu bạn tạo một cái gì đó có cùng tên trong hai tệp khác nhau. (Hai hàm F địa phương chẳng hạn, có thể gây ra lỗi liên kết).

+1

Ah vâng, lời khuyên hay về không gian tên ẩn danh và tệp tiêu đề khai báo chuyển tiếp. Tôi có thể nhìn thấy rất nhiều lần mà tuyên bố về phía trước đặc biệt sẽ hữu ích. Cảm ơn! – user3728501

+0

@NeilKirk: Tại sao bạn bao gồm myclass-fwd.h trong myclass.h? Tôi đã nghĩ rằng đó là dư thừa. – quamrana

+0

@quamrana Không có lý do gì, chỉ để nhất quán. –

1

Lý do chính để tách giao diện khỏi triển khai là do đó bạn không phải biên dịch lại tất cả mã của mình khi có điều gì đó trong quá trình triển khai thay đổi; bạn chỉ phải biên dịch lại các tệp nguồn đã thay đổi.

Đối với "Khai báo lớp (mẫu hoặc cách khác)", templatekhông a class. A template là mẫu cho tạo các lớp. Quan trọng hơn, tuy nhiên, bạn xác định một lớp hoặc một mẫu trong tiêu đề. Định nghĩa lớp bao gồm các khai báo về các hàm thành viên của nó và các hàm thành viên không phải trong nội bộ được định nghĩa trong một hoặc nhiều tệp nguồn. Các hàm thành viên nội dòng và tất cả các hàm mẫu phải được xác định trong tiêu đề, bởi bất kỳ kết hợp nào của định nghĩa trực tiếp và chỉ thị #include mà bạn thích.

+1

"Đối với" Khai báo lớp (mẫu hoặc cách khác) ", một mẫu không phải là một lớp. Mẫu là một mẫu để tạo các lớp." - vâng tôi biết – user3728501

0

Các tiêu chuẩn như thế này tồn tại trong ngành?

Có. Sau đó, một lần nữa, các tiêu chuẩn mã hóa khác với các tiêu chuẩn mà bạn thể hiện cũng có thể được tìm thấy trong ngành công nghiệp. Bạn đang nói về các tiêu chuẩn mã hóa, sau khi tất cả, và các tiêu chuẩn mã hóa từ tốt đến xấu đến xấu.

Tiêu chuẩn tôi đưa ra có hoạt động trong mọi trường hợp không?

Tuyệt đối không. Ví dụ,

template <typename T> class Foo { 
public: 
    void some_method (T& arg); 
... 
}; 

Ở đây, định nghĩa của lớp mẫu Foo không biết một điều về điều đó tham số mẫu T. Nếu như, đối với một số mẫu lớp, các định nghĩa của các phương pháp khác nhau tùy thuộc vào các thông số mẫu? Quy tắc số 2 của bạn không hoạt động ở đây.

Ví dụ khác: Điều gì sẽ xảy ra nếu tệp nguồn tương ứng lớn, hàng nghìn dòng dài hoặc dài hơn? Đôi khi, nó có ý nghĩa để cung cấp việc triển khai trong nhiều tệp nguồn. Một số tiêu chuẩn đi đến mức cực đoan của một chức năng cho mỗi tập tin (ý kiến ​​cá nhân: Yech!).

Ở mức cực khác của tệp nguồn dài hơn một nghìn dòng là lớp không có tệp nguồn. Việc triển khai toàn bộ nằm trong tiêu đề. Có rất nhiều điều để nói cho việc triển khai chỉ tiêu đề. Nếu không có gì khác, nó đơn giản hóa, đôi khi đáng kể, vấn đề liên kết.

+0

Tiêu đề chỉ như sau: Tất cả các phương thức được khai báo và xác định trong các dấu ngoặc nhọn lớp ('{' và '}')? Tôi nhận được quan điểm của bạn về việc ngăn chặn các lỗi liên kết, nhưng bất kỳ thành viên nào phức tạp hơn một dòng mã, ví dụ, phức tạp hơn là chỉ '{return m_time; } ', tạo mã ẩn. – user3728501

+0

Tiêu đề chỉ, như trong tất cả các phương pháp được xác định bên trong một số tập tin tiêu đề. Cho dù đó là bên trong lớp, như là một định nghĩa ngoài dòng nhưng vẫn nằm trong cùng một tệp tiêu đề hoặc một số tệp khác được bao gồm bởi tiêu đề đầu tiên đó là một câu hỏi khác. Điểm mấu chốt: Không có tệp nguồn cần được biên dịch riêng biệt và được liên kết. Rất nhiều Boost, ví dụ, là một thực hiện chỉ tiêu đề. –

+0

Theo một cái gì đó phức tạp hơn '{return something; } 'không thuộc về định nghĩa lớp: * Đó là ý kiến ​​của bạn *. Những người khác có ý kiến ​​khác. Cá nhân tôi không ngại nhìn thấy một đoạn ngắn (nhưng không phải một lớp lót) bên trong định nghĩa lớp. Quy tắc cá nhân của tôi: Nếu tôi đặt một định nghĩa chức năng bên trong lớp nếu tôi nghĩ như vậy sẽ giúp người khác hiểu được lớp đó, đó là ý kiến ​​của tôi. Hãy rất thận trọng với việc đặt các vấn đề về sở thích cá nhân bên trong một tiêu chuẩn mã hóa. Nó sẽ làm cho mọi người bỏ qua các tiêu chuẩn của bạn. –