Tại sao file cpp cần file header

Tại sao cần phải táchKhi 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 
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 
#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 
#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