C++ vì sao phải sử dụng file header


Khi lập trình với bất cứ ngôn ngữ nào thì các bạn cũng đều gặp phải một vấn đề là khi chương trình bắt đầu lớn thì sẽ khó quản lý mã nguồn. Đó là lúc bạn muốn chia chúng thành các file riêng, mỗi file làm một nhiệm vụ riêng và khi cần thì “include” chúng vào chương trình.

Trong C++ hoặc các ngôn ngữ lập trình hướng đối tượng như Java, C#, chúng ta còn muốn chia mỗi lớp vào một file riêng, như vậy sẽ tiện hơn thay vì tống tất cả các lớp vào một chỗ.

Việc tách ra cũng giúp bạn dễ dàng hơn trong việc tái sử dụng mã nguồn, thay vì mỗi lần ‘tái sử dụng’ bạn phải copy-paste mã nguồn thì bây giờ bạn chỉ cần ‘include’ những file nào cần sử dụng là có thể dùng.

Chương trình khi chưa tách

Ví dụ sau đây là một chương trình đơn giản (được tôi thiết kế theo mô hình MVC – tôi sẽ có một bài viết về MVC ở dịp khác) cho phép thiết lập giá trị cho một biến, tăng, giảm giá trị biến đó và hiển thị giá trị biến đó ra màn hình.

/* MVC example */ #include <iostream> using namespace std; class Model { int value; public: Model() { value = 0; } int getValue() { return value; } void setValue(int); void decrease() { value--; } void increase() { value++; } }; void Model::setValue(int value) { this->value = value; } class View { Model *model; void showValue(); public: View(Model *); void showMenu(); void process(); }; View::View(Model *model) { this->model = model; } void View::showValue() { if (model != NULL) { cout << "n============"; cout << "n====" << model->getValue() << "===="; cout << "n============n"; } } void View::showMenu() { cout << "n------------------------n"; cout << "1 - Setn"; cout << "2 - Increasen"; cout << "3 - Decreasen"; cout << "4 - Exitn"; cout << "Choose: "; } void View::process() { int choice; do { showMenu(); cin >> choice; switch (choice) { case 1: cout << "Enter a integer value: "; int value; cin >> value; model->setValue(value); showValue(); break; case 2: model->increase(); cout << "Increasing successful!"; showValue(); break; case 3: model->decrease(); cout << "Decreasing successful!"; showValue(); break; case 4: break; default: cout << "Please choose number form 1 to 4!n"; break; } } while (choice != 4); } int main() { Model* model = new Model; View view(model); view.process(); delete model; return 0; }

Các bạn save file này với tên là main.cpp rồi biên dịch bằng g++ và chạy như sau:

$ g++ main.cpp $ ./a.out

Như các bạn thấy, chương trình này có 2 lớp là Model và View và đều được định nghĩa và cài đặt trong một file duy nhất. Mặc dù chương trình đơn giản nhưng do tất cả nằm trong cùng một file nên vẫn cảm thấy rối rối thế nào đó đúng không nào.

Bắt đầu tách

Bây giờ tôi sẽ bắt đầu tách chương trình này ra để mỗi lớp đặt trên mỗi file riêng. Cụ thể thì sau khi tách tôi sẽ có được 3 file chính là: main.cpp, Model.cpp (chứa dữ liệu) và View.cpp (đảm nhận việc hiển thị).

Header file (.h)

Để bắt đầu tách, bạn cần sử dụng header file, header file cho phép bạn định nghĩa các thành phần của chương trình ở các file riêng, và khi cần sử dụng lại thì có thể gọi dễ dàng bằng cách include vào chương trình như sau:

#include "filename.h"

Việc include này thực chất là nhúng toàn bộ nội dung của filename.h vào chương trình hiện có.

Ở đây, tôi sẽ tạo 2 header file là Model.h và View.h để khai báo các lớp Model và View:

Model.h

#ifndef MODEL_H #define MODEL_H class Model { int value; public: Model(); void setValue(int); int getValue(); void increase(); void decrease(); }; #endif

View.h

#ifndef VIEW_H #define VIEW_H #include "Model.h" class View { Model *model; public: View(Model *); void showValue(); void showMenu(); void process(); }; #endif

Như các bạn thấy, file Model.h khai báo lớp Model, file View.h khai báo lớp View. Trong lớp View có sử dụng con trỏ Model *model cho nên cần phải include header Model.h vào như ở dòng thứ 4: #include “Model.h”

Các bạn có thể thấy 2 dòng đầu tiên và dòng cuối cùng của mỗi header file trông lạ lạ. Đó là include guard, nó được dùng để đảm bảo rằng một header file chỉ được include một lần trong chương trình, nếu header file được include nhiều hơn 1 lần thì sẽ xảy ra tình trạng một lớp, biến,… đã định nghĩa rồi lại được định nghĩa lần nữa và chương trình sẽ báo lỗi: previous definition.

Sử dụng include guard rất đơn giản, chỉ cần tạo header file của bạn với cấu trúc như sau:

#ifndef FILENAME_H #define FILENAME_H // đặt code của bạn vào đây #endif

Những lưu ý khi sử dụng header file:

  • Luôn luôn sử dụng include guard
  • Chỉ khai báo chứ không cài đặt giá trị cho biến, trừ trường hợp nó là hằng
  • Chỉ khai báo chứ không cài đặt hàm trong header file (nên cài đặt hàm ở file .cpp riêng) để giúp chương trình dễ đọc hơn
  • Mỗi header file nên làm một nhiệm vụ riêng
  • Cố gắng hạn chế include các header file khác trong header file của bạn

Tạo file cài đặt cho các lớp trong header file

Như đã nói ở trên, header file chỉ nên đảm nhận việc khai báo, còn việc cài đặt được đặt vào một file .cpp riêng.

Ở đây tôi tạo file Model.cpp để cài đặt cho lớp được định nghĩa trong Model.h: (lưu ý là bạn có thể đặt tên file .cpp là gì cũng được nhưng nên đặt giống với file header mà nó cài đặt cho để tiện quản lý)

Model.cpp

#include "Model.h" Model::Model() { value = 0; } void Model::setValue(int value) { this->value = value; } int Model::getValue() { return value; } void Model::increase() { value++; } void Model::decrease() { value--; }

Tương tự đối với file View.h cũng có file View.cpp để cài đặt:

View.cpp

#include <iostream> #include "Model.h" #include "View.h" using namespace std; View::View(Model *model) { this->model = model; } void View::showValue() { cout << "===============n"; cout << "====" << model->getValue() << "====n"; cout << "================n"; } void View::showMenu() { cout << "n==============n"; cout << "0 - Show valuen"; cout << "1 - Set valuen"; cout << "2 - Increasen"; cout << "3 - Decreasen"; cout << "4 - Exitn"; cout << "Choose: "; } void View::process() { int choice = -1; do { showMenu(); cin >> choice; switch (choice) { case 0: showValue(); break; case 1: cout << "Enter a integer number: "; int tmp; cin >> tmp; model->setValue(tmp); showValue(); break; case 2: model->increase(); showValue(); break; case 3: model->decrease(); showValue(); break; case 4: break; default: cout << "Please choose from 0 to 4!n"; break; } } while (choice != 4); }

Sử dụng code ở các file riêng

Như đã nói ở trên, để tái sử dụng code ở các file riêng các bạn chỉ cần include header file vào chương trình.

Ở đây tôi có file main.cpp sử dụng 2 lớp Model và View để hoàn thiện thành một chương trình nên tôi include Model.h và View.h vào:

main.cpp

/* Seprate class in file */ #include <iostream> #include "Model.h" #include "View.h" using namespace std; int main() { Model *model = new Model(); View view(model); view.process(); return 0; }

Biên dịch chương trình

Khác với trường hợp chương trình chỉ có một file, các bạn chỉ cần gõ: g++ filename.cpp là xong. Với chương trình có nhiều file nếu các bạn thực hiện cách biên dịch tương tự thì sẽ không thành công, ví dụ nếu tôi biên dịch mỗi file main.cpp thì sẽ gặp lỗi như sau:

$ g++ main.cpp /tmp/ccPzJkJB.o: In function `main': main.cpp:(.text+0x1d): undefined reference to `Model::Model()' main.cpp:(.text+0x35): undefined reference to `View::View(Model*)' main.cpp:(.text+0x41): undefined reference to `View::process()' collect2: ld returned 1 exit status

Vậy phải biên dịch thế nào? Cũng đơn giản thôi, các bạn biên dịch như sau:

$ g++ Model.cpp View.cpp main.cpp

hoặc cũng có thể biên dịch từng file ra object file rồi sử dụng object file để biên dịch tiếp:

$ g++ -c Model.cpp # được object file Model.o $ g++ -c View.cpp # được object file View.o $ g++ -c main.cpp # được object file main.o $ g++ main.o Model.o View.o # được file thực thi a.out

hoặc cũng có thể tạo Makefile như sau:

all : main.o Model.o View.o g++ main.o Model.o View.o main.o : main.cpp g++ -c main.cpp Model.o : Model.cpp g++ -c Model.cpp View.o : View.cpp g++ -c View.cpp clean : rm *.o ; rm a.out

Rồi biên dịch bằng cách gọi:

$ make

Để dọn dẹp folder, gọi:

$ make clean

Cuối cùng, để chạy chương trình ta gõ:

$ ./a.out

This entry is part 11 of 11 in the series Hướng đối tượng C++

87 / 100

Tách code C++ thành các file.hfile.cpp? Chắc hẳn các bạn đã từng nghe qua. Trước tới giờ, chúng ta thường chỉ viết các đoạn code ngắn, hàm đơn giản hay chỉ là một chương trình nhỏ nên chỉ cần viết 1 file.cpp là đủ. Nhưng khi xây dựng một chương trình lớn thì có nên làm như vậy?

Nếu bạn đọc đang có nhu cầu học lập trình hướng đối tượng C++, hãy tham khảo ngay series Lập trình hướng đối tượng với C++ của LTKK nhé.

Tại sao phải tách file?

Khi lưu toàn bộ code vào một file main sẽ gây ra rất nhiều vấn đề bất lợi, có thể kể đến như: Chương trình trở nên quá dài; khó quản lý, sử dụng. Giả sử ta khi muốn tìm một hàm nào đấy để chỉnh sửa thì sẽ làm tiêu tốn thời gian vì có quá nhiều hàm và không biết nó ở đâu.

Việc tách code C++ cũng như tách code ở các ngôn ngữ khác sẽ giúp:

  • Dễ quản lí, bảo trì source code.
  • Giúp code dễ đọc, dễ hiểu và dễ sử dụng.
  • Tái sử dụng các hàm đã viết.

Tách code C++ như thế nào?

Hãy cùng xem xét ví dụ sau:

File: main.cpp

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

#include <iostream>

#include <string>

#include <cmath>

using namespace std;

// Các hàm tính toán - Math

int UCLN(int a, int b){

    if(a == b)

        return a;

    else if(a > b)

        return UCLN(a - b, b);

    else

        return UCLN(a, b - a);

}

int BCNN(int a, int b){

    int uc = UCLN(a, b);

    return a * b / uc;

}

bool laSoNguyenTo(int n){

    if(n < 2)

        return false;

    else if(n == 2)

        return true;

    else{

        int num = sqrt(n);

        for(int i = 2; i <= num; i++){

            if(n % i == 0)

                return false;

        }

        return true;

    }

}

// Các hàm về chuỗi - string

string inHoa(string xau){

    for(int i = 0; i < xau.size(); i++){

        if(xau[i] >= 'a' && xau[i] <= 'z')

            xau[i] = xau[i] - 'a' + 'A';

    }

    return xau;

}

string inThuong(string xau){

    for(int i = 0; i < xau.size(); i++){

        if(xau[i] >= 'A' && xau[i] <= 'Z')

            xau[i] = xau[i] - 'A' + 'a';

    }

    return xau;

}

int main(){

    cout << UCLN(18, 27) << endl;

    cout << inHoa("Lap Trinh Khong Kho") <<  endl;

    return 0;

}

Có thể thấy: Chương trình được viết vào một file main.cpp duy nhất. Và các hàm (function) của mình có 2 loại. “Loại 1: Các hàm về tính toán” và “Loại 2: Các hàm về xâu chuỗi”. Mình sẽ tách chương trình trên thành các file sau:

      • Header.h – Các thư viện dùng chung như “iostream”,…
      • Math_Fn.h và Math_Fn.cpp – Các hàm về tính toán
      • String_Fn.h và String_Fn.cpp – Các hàm về xâu, chuỗi
      • main.cpp – Hàm main, chạy code

Lưu ý:

  • Việc đặt tên các file .h và .cpp trùng nhau chỉ để dễ sử dụng.
  • file .h dùng để khai báo hàm và file .cpp dùng để viết hàm.
  • Có thể tách file thành các cách khác nhau, đây chỉ là một ví dụ nhỏ.

Mã code các file

Một chút về file Header (.h): Bạn có thể tưởng tượng đây là một file thư viện mà mình tự viết ra. Nó chứa các khai báo hàm, định nghĩa marco và có thể chia sẽ sang các file khác. (Trong các trình biên dịch sẽ có các thư viện – file Header được nhà sản xuất viết sẵn như string.h, time.h, math.h, …)

Note: Mỗi file .h đều phải có dòng code “#pragma once” ở đầu.

a) File: Header.h

Đối với mình, đây sẽ là file include các thư viện mà tất cả các file khác đều phải có. Phổ biến nhất là <iostream>. Nhằm mục đích tái sử dụng, không cần include một thư viện nhiều lần. Ngoài ra có thể khai báo các các class, struct cần thiết vào. Thậm chí là cả using namespace std

File: Header.h

#pragma once

#include <iostream>

using namespace std;

b) File: Math_Fn.h và Math_Fn.cpp

File: Math_Fn.h

#pragma once

#include "Header.h"

#include <cmath>

// Các hàm về tính toán - Math

int UCLN(int a, int b);

int BCNN(int a, int b);

bool laSoNguyenTo(int n);

Mình đã include file Header.h nên trong file này sẽ có luôn thư viện <iostream> và cả using namespace std. Tiếp theo, chỉ cần thêm các thư viện cần thiết (cmath) và khai báo các hàm tính toán.

File: Math_Fn.cpp

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

#include "Math_Fn.h"

int UCLN(int a, int b) {

    if (a == b)

        return a;

    else if (a > b)

        return UCLN(a - b, b);

    else

        return UCLN(a, b - a);

}

int BCNN(int a, int b) {

    int uc = UCLN(a, b);

    return a * b / uc;

}

bool laSoNguyenTo(int n) {

    if (n < 2)

        return false;

    else if (n == 2)

        return true;

    else {

        int num = sqrt(n);

        for (int i = 2; i <= num; i++) {

            if (n % i == 0)

                return false;

        }

        return true;

    }

}

Ở file .cpp, mình chỉ cần include file “Math_Fn.h” và viết các hàm đã khai báo trước đó.

c) File: String_Fn.h và String_Fn.cpp

Hoàn toàn tương tự với Math_Fn. Đây sẽ là file lưu các hàm về xâu, chuỗi.

File: String_Fn.h

#pragma once

#include "Header.h"

#include <string>

// Các hàm về chuỗi - string

string inHoa(string xau);

string inThuong(string xau);

File: String_Fn.cpp

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

#include "String_Fn.h"

// Các hàm về chuỗi - string

string inHoa(string xau) {

    for (int i = 0; i < xau.size(); i++) {

        if (xau[i] >= 'a' && xau[i] <= 'z')

            xau[i] = xau[i] - 'a' + 'A';

    }

    return xau;

}

string inThuong(string xau) {

    for (int i = 0; i < xau.size(); i++) {

        if (xau[i] >= 'A' && xau[i] <= 'Z')

            xau[i] = xau[i] - 'A' + 'a';

    }

    return xau;

}

d) File: main.cpp

Include tất cả các file .h (header) đã viết vào, rồi viết hàm main.

File: main.cpp

// include các file header đã viết vào

#include "Header.h"

#include "Math_Fn.h"

#include "String_Fn.h"

int main() {

    cout << UCLN(18, 27) << endl;

    cout << inHoa("Lap Trinh Khong Kho") << endl;

    return 0;

}

Tách code C++ trong Visual Studio

Hiện nay Visual Studio đang có rất nhiều phiên bản như 2012, 2017, 2019. Trong bài này mình sẽ sử dụng phiên bản mới nhất hiện tại – Visual Studio 2022. Việc tách file sẽ không có sự khác biệt giữa các phiên bản nên các bạn đừng lo lắng nhé! Hướng dẫn dưới đây sẽ chỉ bạn từng bước cách tách code C++ trên Visual Studio.

B1: Tạo một project mới (Create a new project) rồi chọn Empty Project C++

B2: Các bạn để ý ở góc phải – Solution Expoerer sẽ có:

    • Header Files: Folder lưu các file Header (.h)
    • Source Files: Folder lưu các file code (.cpp)

B3: Click chuột phải vào folder muốn thêm file => Chọn Add => New Item…

B4: Chọn file phù hợp

    • C++ file đối với folder “Source Files”
    • Header file đối với foder “Header Files”
    • Đặt tên rồi chọn Add

Và đây là kết quả: Nội dung từng file thì mình đã đề cập ở phần 3

Tách code C++ trong Visual Studio Code

Đây là một editor do Microsoft phát triển dưới dạng mã nguồn mở (opensourse) khá xịn xò được rất nhiều lập trình viên yêu thích. Hiện tại VS Code là Editor được sử dụng phổ biến nhất.

Hãy đảm bảo bạn đã cài đặt “Code Runner” và đã chạy được bằng Terminal ngay trên Visual Studio Code nhé! Hướng dẫn dưới đây sẽ chỉ bạn từng bước cách tách code C++ trên Visual Studio Code.

Tham khảo thêm: Hướng dẫn cài đặt VS Code học C++

Bước 1: Tạo một folder và lưu toàn bộ các file vào trong đó – bao gồm cả file .h và file .cpp

Bước 2: Vào Settings => Tìm kiếm “run code”

Bước 3: Ở Extension => Chọn Run Code con… (23) => Tìm kiếm Code-runner: Executor Map => Chọn Edit in settings.json

Bước 4: Ở mục cpp: Các bạn sửa $fileName thành *.cpp

Bước 5: Lưu lại và quay trở lại file main.cpp để chạy code. Khi bạn đã làm qua một lần rồi thì lần tiếp theo chỉ cần làm 2 bước: B1 và B5 thôi nhé !!! Và đây là kết quả:

Câu hỏi thường gặp (FAQ)

Dưới đây là các câu hỏi thường gặp liên quan tới vấn đề tách code C++ thành file .h và .cpp do mình tổng hợp.

Việc đổi tên thành *.cpp sẽ thay đổi những gì?

Hãy để ý ở Visual Studio, tất cả các file (.h và .cpp) sẽ đều được lưu chung vào trong một folder. Việc đổi từ $fileName sang *.cpp cũng tương tự vậy. Tức là toàn bộ các file trong cùng một project phải lưu cùng một folder thì mới chạy được trên Visual Studio Code. Nói cách khác, các file không liên quan với nhau mà để chung một folder thì khi chạy sẽ lỗi.

Khi mới bắt đầu nên tách file như thế nào?

Ở phía trên, mình đưa ra một ví dụ khá tổng quát. Tuy nhiên, khi mới bắt đầu tập tành tách file thì các bạn chỉ nên tách file thành 2 file:

      • file .h: include tất cả các thư viện, using name space, khai báo tất cả các hàm
      • file .h: include file .h, viết các hàm và viết hàm main.

Rồi sau đó các bạn hãy từ từ nâng số file lên – 3 file, 4 file và cuối cùng là tách file theo chức năng từng file để dễ quản lí, sử dụng nhé.

Tại sao phải khai báo hàm?

Giả sử bạn có hai hàm: function1 và fuction2. Khi ở trong function2 thì bạn có thể sử dụng function1 nếu muốn. Nhưng khi ở fuction1 thì bạn không thể dùng function2. Bởi vì trình biên dịch sẽ chạy từ trên xuống dưới, khi chưa chạy xong function1 thì function2 không tồn tại để sử dụng.

Việc khai báo hàm sẽ giúp chúng ta khắc phục điều đó. Các bạn có thể sử dụng các hàm một cách thoải mái mà không cần lo lắng đến thứ tự.

Viết hàm trong file header (.h) luôn được không?

Được, nhưng không nên. Vốn dĩ file .h sinh ra là để khai báo (thư viện, using name, class, struct, hàm – function,…). Còn việc lưu code thuộc về các source file .cpp. Do đó khi các bạn viết hàm trong file .h luôn thì thỉnh thoảng chương trình sẽ chạy sai hoặc không chạy được.

Tại sao phải có “#pragma once” ở đầu file.h?

Khi bạn include một file header (.h) thì tức là bạn đã include “toàn bộ những gì mà file header đó include”. Hãy nhìn hình dưới đây:

Như vậy ở file main.cpp, ta đã include iostream 2 lần. Điều này sẽ gây ra xung đột và có thể khiến chương trình không thể chạy được. Và thêm “#pragma once” ở đầu giúp chúng ta khắc phục điều đó, nó mang ý nghĩa include thư viện một lần, tránh sự trùng lặp.

Bản MinGW mà mình đang dùng?

MinGW là một phần mềm mã nguồn mở, một trình biên dịch ngôn ngữ C/C++ rất phổ biến trên Windows. Đối với Visual Studio thì có sẵn trình biên dịch nhưng các editor như VSCode, sublime text,… thì không. Do đó bạn phải cài thêm mới chạy code được.

Hiện tại mình đang sử dụng phiên bản MinGW mới nhất 9.2.0. Các bạn có thể download tại MinGW và cách cài đặt MinGW tại Hướng dẫn cài đặt MinGW.

Lời cảm ơn

Cảm ơn các bạn đã đọc bài viết hướng dẫn tách code C++ của mình, nếu thấy hay thì hãy chia sẽ để ủng hộ LTKK nhé! Và nếu bạn có câu hỏi hay thắc mắc gì thì hãy để lại bình luận nhé, hoặc có thể đưa lên group Lập Trình Không Khó để mọi người cùng thảo luận. Mình sẽ cố gắng update câu trả lời sớm nhất có thể.

Cảm ơn các bạn rất nhiều!!!!!

Video liên quan

Chủ đề