1
|
g++ -o run.exe main.cpp
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Phân tách file mã nguồn và Makefile.
Lý do mình viết bài này là vì mấy người bạn của mình nhờ mình nói qua một chút về makefile. Thôi thì đằng nào cũng phải làm cái file doc gửi cho "bạn" ấy, mình viết lên đây chia sẻ cùng anh em vậy.
Trong bài này mình sẽ nêu ra 2 vấn đề chính là : Phân tách file mã nguồn và Makefile. Sau khi đọc xong nếu bạn vẫn còn mơ hồ thì xin hỏi mình và Google để tìm hiểu tiếp.
I. Phân tách file mã nguồn.
1. Vì sao phải phân tách file mã nguồn ?! - Dùng lại. - Dễ sửa đổi. - Bảo vệ mã nguồn.
2. Bắt đầu tách. - Trong C/C++ có 2 (và nhiều hơn) loại file : + File mã nguồn thường có dạng 3x.cpp : Chứa định nghĩa các hàm, phương thức (dài dòng và khó hiểu). + File tiêu đề (header) thường có dạng 3x.h hay 3x.hpp, 3x.hxx : Chứa khai báo của các hàm, lớp, phương thức lớp, ... (những thứ ngắn gọn mà bạn nhìn vào cái là biết nó làm gì - không cần biết nó làm việc đó như thế nào). (Mấy loại khác như file object, dinamic_library, static_library, ... sẽ nói sau) Ví dụ luôn cho dễ hiểu - Bình thường bạn hay viết một chương trình kiểu như :
Bây giờ ta sẽ phân tách file này thành 3 file khác nhau là : - anhDiTimEm.cpp : File để chạy hàm main đi timNguoiYeu - timNguoiYeu.h : File chứa hàm timNguoiYeu (string yeuCau) - người ta nhìn vào đây và biết được bạn có một cái hàm với mục đích đi timNguoiYeu, chỉ cần đưa yeuCau vào và nó sẽ trả về danh sách các cô phù hợp. Còn việc tìm như thế nào người ta không cần biết. - timNguoiYeu.cpp : File chứa định nghĩa hàm timNguoiYeu - Đây là nơi bạn tìm cách giải quyết bài toán : Làm sao để tìm được ny sau khi đã có cái yêu cầu kia.
Nội dung 3 file sẽ như sau :
1
|
#include "timNguoiYeu.h"using namespace std;// Định nghĩa hàm.void timNguoiYeu (string yeuCau) { if (yeuCau == "ngon - bổ - rẻ") { cout << "Danh sách người phù hợp đây : " << " Đào - Mơ - Cam - Xoài ..." << endl; }}
|
Vậy đấy : Bây giờ ta thử xem tách file như vậy sẽ có tác dụng như thế nào? (dựa theo 3 cái lý do trên) + Bạn (hay bạn của bạn) viết vài cái hàm và comment đầy đủ là bạn cần đưa vào hàm cái gì và nó trả về cái gì >> Bạn cứ nhìn vào file 3x.h kia và dùng thôi, chả cần quan tâm xem cái hàm ý nó làm ra sao. Lần sau cứ thế mà dùng, chả cần viết lại hay sửa đổi làm gì. + Bạn chỉnh sửa thuật toán để tìm lời giải kia, chả ảnh hưởng gì, miễn sao bạn vẫn để đầu vào và đầu ra như cũ.
+ Bạn viết hàm và chỉ cho người ta xem cái cách dùng kia, phần bạn làm trong file 3x.cpp kia bạn giấu đi (mã hóa chẳng hạn). Người ta chẳng biết bạn đã làm gì, cứ thế dùng thôi.
*** Với chương trình vài nghìn dòng cpp (bài tập lớn chẳng hạn) - Bạn làm sao mà xem hết được cả 1 cái file dài vài nghìn dòng. Cứ sửa chỗ này nó lại hỏng chỗ kia >> mất công mất sức. ![:(](https://lh3.googleusercontent.com/blogger_img_proxy/AEn0k_uWRsYHqT6TV7b-T6PmIjRxed7x2ZRzq_B8IBqKiFy_Fk53FpZiId-QzrrcL4-MM_MpZL51cWUB_p2GLxBkgjxI5QKNbuDss4bU21B6Ksh0uhBgUcZViKBFfolbnGCfCD5rWac=s0-d) ............................. Note : Thông thường các công ti ít khi cho bạn viết 1 file nào quá 500 dòng cpp vì khi xem lại sẽ cực kì khó nhìn và khó hiểu.
Tương tự, ta có thể tách ra rất nhiều file .h và .cpp khác nhau.
Sử dụng :
- Trong main bạn chỉ cần #include "xxxx.jh" - Với xxx là tên cái file .h kia của bạn và dùng những hàm trong đó thôi. (chú ý là tên file nằm trong cặp "" thay vì cặp <> dành cho các file .h của các thư viện chuẩn nhá.)
Nội dung từng file : 1. Đưa tất cả các khai báo hàm vào trong file .h (có thể thêm cái #ifndef _LIBQLTS_H_ .... #define _LIBQLTS_H_ gì đó vào - nếu bạn cần hiểu thêm về nó thì cứ hỏi, mình sẽ nói sau).
2. Đưa tất cả các khai báo hàm vào 1 file .cpp (thường để cùng tên với file .h kia cho dễ nhận biết). Trong đó cần thêm một số thứ như : - #include "xxx.h" - #include <abcde> với abcde là mấy cái thư viện có để bạn dùng các thứ như string, math, cstring, vector, deque, .... Đại loại là mấy cái include kia nó sẽ giống như bên file chứa main của bạn (nếu chỉ tách ra 3 file).
3. Trong file chứa main () thì #include "xxx.h" kia vào.
II. Makefile.
Note : Các bạn nên tìm hiểu về file Object, cách trình biên dịch tạo các loại file (.s, .o, .out, ...) trên google với từ khóa kiểu như : how g++ compile ..v..v.. (Nói qua qua : object file sẽ chứa tất cả thông tin về : các hàm, các đối tượng được tạo ra, các đối số của từng hàm, kiểu trả về của từng hàm ........)
1. Makefile là gì? - Makefile là quá trình sử dụng tiện ích "make" trình biên dịch đọc 1 file có tên là makefile để biết được nó cần biên dịch những cái gì sau khi bạn đã viết xong (những) file mã nguồn. (Cái này mình tự định nghĩa nên nó chả ra làm sao)
2. Makefile để làm gì? - Để chỉ cho thằng trình biên dich kia nó biết tìm những thứ cần thiết để tạo thành một chương trình.
3. Cách makefile. - Lấy lại ví dụ bên trên : bạn có 3 file anhDiTimEm.cpp, timNguoiYeu.cpp, timNguoiYeu.h. Nếu bạn chỉ biên dịch mỗi file chứa main kia thì thằng trình biên dịch nó sẽ chả hiểu cái hàm timNguoiYeu() kia bạn lấy đâu ra. >> Ta cần tạo ra file có tên là makefile (không có phần đuôi nhá) để cho trình biên dịch nó biết mấy cái kia nằm ở đâu.
- Cứ đưa nội dung file lên trước rồi mình sẽ hướng dẫn cách tạo file make.
1
|
run.exe: anhDiTimEm.o timNguoiYeu.o g++ anhDiTimEm.o timNguoiYeu.o -o run.exeanhDiTimEm.o: anhDiTimEm.cpp timNguoiYeu.h g++ -c anhDiTimEm.cpptimNguoiYeu.o: timNguoiYeu.cpp timNguoiYeu.h g++ -c timNguoiYeu.cpp
|
Ở đây ta tạo ra một file chạy có tên run.exe để chạy chương trình. File này phải được tạo thành từ 2 "cái" - 1 cái chứa main và 1 cái chứa cái hàm kia cùng định nghĩa của nó. >> Khi dịch chương trình thì trình biên dịch biết được rằng : - "À, hàm nó nằm trong "cái" timNguoiYeu.o kia. Thằng main nó sẽ sử dụng thằng này." - "Còn mấy cái thằng .o kia sẽ được tạo ra từ mấy cái file .h với .cpp kia.
Theo trên thì như sau : - anhDiTimEm chứa cái hàm ta cần dùng, hàm đó lại nằm trong timNguoiYeu.h nên ta phải "nhìn" vào file .h kia để xem hàm đó như thế nào. (dòng 3, 4) - Sau khi nhìn vào đấy rồi thì lại phải xem xem cái hàm đó được viết như thế nào >> "nhìn" vào file timNguoiYeu.cpp kia. (dòng 5, 6). Bạn nào thích thì đọc, không thì google xem đối số của lệnh có tác dụng gì. (-o kia là linked mấy file object lại với nhau để tạo file thực thi).
4. Cú pháp file make.
1
|
<tên file chạy>: <tab> <tên các file object - object1, 2, ....><tab>g++ <tên các file object bên trên> -o <tên file chạy><tên file object 1>: <tab> <các file mà nó gọi tới - main thì gọi tới timNguoiYeu.h><tab>g++ -c <tên file .cpp><tên file object 2>: <tab> <các file mà nó gọi tới - timNguoiYeu.cpp thì gọi tới timNguoiYeu.h><tab>g++ -c <tên file .cpp>.....v..v.......
|
Cách dễ viết nhất là vẽ ra một cái sơ đồ xem thằng nào gọi tới thằng nào bằng cách cứ nhìn vào mấy cái lệnh #include "xxx.h" kia. File object được tạo ra từ file .cpp. Dễ dàng thấy ta có 2 file .cpp nên sẽ phải tạo ra 2 file object >> liên kết 2 file này lại với nhau ta sẽ được chương trình của mình.
Thêm cái ảnh vào cho nó dễ hiểu. ![:D](https://lh3.googleusercontent.com/blogger_img_proxy/AEn0k_skiAHWQ0K9cvpgg5KrpdhFEnDC91pav3-aZlM-XwltTf4iYNzpy23KWj9PN79EW9hx26zqUjeGazOwXClgkyZAti9EPNPhi5Q9O2_DcV5HnxSXZxL9eIJY-1ta9D1QKYueOQ=s0-d)
ps : - Vừa xem bóng xong, viết tí, nếu không hiểu các bạn cứ comment vào đây, mình sẽ trả lời hoặc gợi ý từ khóa cho các bạn google. - Mình không có năng khiếu sư phạm nên diễn đạt rất lung tung, bài này cũng viết rất ngắn gọn, muốn tìm hiểu sâu hơn xin các bạn cùng thảo luận hoặc google tiếp.
Run : Trên CMD (terminal) chuyển tới thư mục chứa file mã nguồn và file make (cùng thư mục) gõ :
(1 chữ make - đơn giản vậy thôi, sau đó ./run hoặc run.exe).
6. Ví dụ về sử dụng make để biên dịch nhiều file mã nguồn hơn.
Cũng như trên, chúng ta có thể tách thành rất nhiều file khác nhau. Nhưng càng tách nhiều thì tất nhiên độ phức tạp khi make càng cao. Cùng với nó - tính bảo mật, tiện dụng, dễ sửa đổi, linh động đối với chương trình của bạn càng cao.
Vậy chúng ta sẽ cùng tìm hiểu việc make file với số lượng file nhiều hơn một chút để các bạn hình dung được quá trình biên dịch và liên kết các file Object của trình biên dịch (gcc/g++) ra sao.
Không kế thừa và thành phần. Trước hết sử dụng với chương trình chưa có kế thừa (inheritance) hay thành phần (component) Sau đây là file main.cpp trước khi tách file
Ở đây ta có 2 lớp A và B. Hai lớp này riêng rẽ và được sử dụng trong main. Riêng rẽ ở đây nghĩa là : Lớp A không chứa thành phần nào có kiểu B và ngược lại với lớp B. (component).
Khi đó ta có thể tách file main.cpp thành 4 file : a.h, a.cpp, b.h, b.cpp lần lượt chứa khai báo và định nghĩa của từng lớp A, B.
1
|
#ifndef _A_H_#define _A_H_//------------------------------------------------------------------------------// Class A//------------------------------------------------------------------------------class A { public: // Constructors A(); // default constructor A(B *b); // // Public Methods void print(); private: // Attributes B *b; // Private Methods};#endif /* _A_H_ */
|
1
|
#ifndef _B_H_#define _B_H_//------------------------------------------------------------------------------// Class B;//------------------------------------------------------------------------------class B { public: // Constructors B(); // default constructor B(int b); // // Public Methods void print(); private: // Attributes int b; // Private Methods};#endif /* _B_H_ */
|
Ở đây ta có một số cái mới :
1
|
#ifndef _B_H_ // if not define - Nếu chưa có định danh _B_H_ thì#define _B_H_ // Khởi tạo một macro _B_H_ (có thể thêm đường dẫn tới file "CPP/testMake/" vào phía sau)// to do somethings//......................#endif /* _B_H_ */ // end if - Kết thúc tiền xử lý if phía trên
|
Cú pháp trên đây nghĩa là gì? Ứng với mỗi file thư viện người ta định nghĩa cho nó một cái tên, gọi là macro để định danh. Đây là một macro báo cho trình dịch biết cái tên _B_H_ kia đã được định nghĩa chưa, nếu chưa thì ta định nghĩa nó. >> Có macro này giúp ta tránh được việc định nghĩa trùng lặp các macro. Đối với những chương trình rất lớn thì việc trùng lặp là hoàn toàn có thể xảy ra >> trình dịch không biết phải gọi thư viện nào. Nó cũng gần như bắt buộc phải có với tất cả những file thư viện (.h) mà bạn tạo ra. (Khuyên dùng dần cho quen) Tên macro nên đặt trùng với tên file của bạn và tránh tất cả các macro chuẩn của C/C++ như _INC_STDIO_, _INC_MATH_ ....
Make : - Đầu tiên ta nên vẽ sơ đồ lớp ra để dễ tạo file make. - Ta có thể dễ dàng tạo 1 file makefile để biên dịch chương trình này như sau.
1
|
run: main.o a.o b.o g++ -Wall main.o a.o b.o -o runmain.o: main.cpp a.h b.h g++ -Wall -c -g main.cppa.o: a.cpp a.h g++ -Wall -c -g a.cppb.o: b.cpp b.h g++ -Wall -c -g b.cpp
|
Ở đây ta tạo ra 3 file Object (chứa thông tin về các đối tượng được tạo ra) là main.o, a.o, b.o. Vì trong main gọi tới 2 lớp A và B nên main.o phải được tạo ra từ các file main.cpp a.h và b.h Tương tự như bài trước với 2 Object a.o và b.o
Sử dụng Kế thừa và Thành phần.
Giả sử ta có - Lớp A chứa 1 thành phần là một đối tượng của lớp B : ta có cpp của lớp A như sau :
1
|
#ifndef _A_H_#define _A_H_#include "b.h" // Lớp A chứa thành phần là đối tượng thuộc lớp B // >> Thêm định nghĩa lớp B vào lớp A.//------------------------------------------------------------------------------// Class A//------------------------------------------------------------------------------class A { public: A(); // default constructor A(B *b); // void print(); private: // Attributes B *b; };#endif /* _A_H_ */
|
Và trong main
1
|
#include <iostream>#include "a.h" // main chỉ cần gọi tới a.h, khi đó có thể gọi tới hàm trong b.h//------------------------------------------------------------------------------// MAIN//------------------------------------------------------------------------------int main(int argc, char *argv[]){ A* a = new A(b); a->print(); //cin.ignore(80,'\n'); //cin.get(); return 0;}
|
Khi đó ta có make file như sau : . .
1
|
run: main.o a.o b.o #main chứa A nên main phải chứa B g++ -Wall main.o a.o b.o -o runmain.o: main.cpp a.h # main không cần gọi tới "b.h" g++ -Wall -c main.cppa.o: a.cpp a.h b.h # Chú ý lớp A bao gồm 1 thành phần thuộc lớp B. g++ -Wall -c a.cppb.o: b.cpp b.h g++ -Wall -c b.cpp
|
Tương tự ta có thể tạo make file với các lớp kết thừa nhau... Giả sử A thừa kế B Ta cũng có make file giống trường hợp trên.
Thêm về make file : Để makefile dễ chỉnh sửa hơn chúng ta nên đặt những biến hay sử dụng lại một chỗ để tiện dụng.
1
|
# Danh sách đối tượng.OBJS = main.o a.o b.oCC = g++ // Trình biên dịchDEBUG = -g // Trình gỡ rối# In đầy đủ thông tin về quá trình biên dịchCFLAG = -Wall -c $(DEBUG) # In đầy đủ thông tin về quá trình link object LFLAG = -Wall $(DEBUG) run: $(OBJS) $(CC) $(LFLAG) main.o a.o b.o -o runmain.o: main.cpp a.h $(CC) $(CFLAG) main.cppa.o: a.cpp a.h b.h $(CC) $(CFLAG) a.cppb.o: b.cpp b.h $(CC) $(CFLAG) b.cpp
|
Tiện ích thêm vào : 1. Clean : Clean là lệnh chúng ta hay dùng để xóa các file Object sau khi đã biên dịch xong.
1
|
#Thêm vào cuối makefileclean:#Câu lệnh rm với linux và del với Windows<tab>rm -rf *.o *~ #Xóa tất cả các file có đuôi .o và các file có phân cuối là ~ (thường là file tạm của Emacs)
|
Sau khi make file xong ta dùng lệnh
để xóa đi các file Object trung gian kia.
Trên Linux còn rất nhiều tiện ích khác được viết bằng Shell, bạn chỉ cần biết một số cú pháp Shell hay dùng và viết vào trong file make là bạn đã có thể "làm" được 1 số trò hay hay rồi ![:D](https://lh3.googleusercontent.com/blogger_img_proxy/AEn0k_skiAHWQ0K9cvpgg5KrpdhFEnDC91pav3-aZlM-XwltTf4iYNzpy23KWj9PN79EW9hx26zqUjeGazOwXClgkyZAti9EPNPhi5Q9O2_DcV5HnxSXZxL9eIJY-1ta9D1QKYueOQ=s0-d) Ví dụ như : tạo file tar hay sau khi tạo xong file chạy thì ta di chuyển file tạo được đó đi chỗ khác và chạy nó. (cài đặt chương trình) ...v..v... (google để biết thêm nhá).
Cảm ơn các bạn đã đọc tới tận chỗ này. Mọi ý kiến góp ý hay hỏi đáp các bạn cứ comment vào đây. Thân!
|