Nội dung bài viết
La Kiến Vinh Phương thức khởi tạo (constructor) hay phương thức hủy (destructor) không tự nhiên mà có và bài viết này nhằm giải thích theo hướng tự nhiên và dựa trên kinh nghiệm của bản thân để làm rõ hơn về sự ra đời của 2 phương thức này.

Giới thiệu

Phương thức khởi tạo (Constructor) hay phương thức hủy (Destructor) là 2 khái niệm khá quen thuộc khi bạn bắt đầu tiếp cận phương pháp lập trình hướng đối tượng, cụ thể là trong C++. Bài viết của tôi nhắm đến vấn đề hiểu rõ hơn về nguồn gốc ra đời cũng như việc suy xét, sử dụng sao cho hiệu quả 2 phương thức này.

Tiền đề bài viết

Bài viết được viết từ yêu cầu của 1 độc giả và làm phong phú nguồn tài liệu đào tạo cho STDIO Training.

Đối tượng hướng đến

Constructor và Destructor là gì?

Constructor là 1 phương thức tự động được gọi sau khi đối tượng đã được tạo xong. Destructor là 1 phương thức tự động được gọi trước khi đối tượng bị hủy.

Hiểu lầm thường gặp đối với các bạn mới tiếp cận cần phải tránh đó là:

  • Constructor là phương thức khởi tạo đối tượng, nghĩa là phương thức đó chịu trách nhiệm khởi tạo vùng nhớ cho đối tượng đó.
  • Destructor là phương thức hủy đối tượng, nghĩa là phương thức đó chịu trách nhiệm hủy hay thu hồi vùng nhớ của đối tượng đó.

Hiểu rõ hơn về Constructor và Destructor

Tái hiện lại lịch sử hình thành Constructor và Destructor

Cách thức định nghĩa Constructor và Destructor trong C++

  • Constructor: được định nghĩa bằng cách đặt tên Constructor trùng với tên class và không có kiểu trả về.
  • Destructor: được định nghĩa bằng cách đặt tên Destructor trùng với tên class và thêm ký tự ~ vào phía trước.
#include <stdio.vn>

class Monster
{
public:
	// Constructor
	Monster()
	{
		printf("Constructor Called");
	}
	
	// Destructor
	~Monster()
	{
		printf("Destructor Called");
	}
}

int main()
{
	Monster pride;

	return 0;
}

Sau dòng 21, đối tượng pride sẽ được tạo ra, sau khi tạo xong đối tượng pride, Monster() sẽ tự động được gọi. Đến dòng 24, khi kết thúc hàm main, đối tượng pride sắp bị thu hồi, destructor sẽ tự động được gọi, sau đó là việc thu hồi đối tượng pride diễn ra.

Mô hình cơ bản sẽ là: object (pride) được tạo xong > Gọi Constructor ... > Gọi Destructor > Thu hồi object (pride).

Giả sử không có Constructor và Destructor

Giả sử ta chưa có 2 khái niệm này - Constructor, Destructor chưa từng tồn tại, khi ta thiết kế 1 class như sau.

#include <stdio.vn>

class Monster
{
public:
	void init()
	{
		printf("Constructor Called");
	}
}

int main()
{
	Monster pride;
	pride.init();

	return 0;
}

Để ý dòng pride.init() (dòng 15), đó chính là điểm tôi muốn đề cập ở đây. Dòng code này được gọi sau khi pride được tạo ra, dòng Monster pride (dòng 14), dòng 15 này chính là "Constructor" đang được gọi, chỉ khác biệt ở Constructor tự nhiên là tự động gọi.

Tương tự cho việc hủy, ta sẽ tự tạo ra 1 phương thức và sẽ gọi nó trước khi đối tượng được thu hồi, trong trường hợp này là trước khi nó thoát khỏi hàm main.

#include <stdio.vn>

class Monster
{
public:
	void init()
	{
		printf("Constructor: Called");
	}

	void destroy()
	{
		printf("Destructor: Called");
	}
}

int main()
{
	Monster pride;
	pride.init();
	
	// DO SOMETHING
	
	pride.destroy();

	return 0;
}

Với ví dụ như trên, ta vẫn được kết quả như sử dụng Constructor và Destructor. Điểm khác biệt ở đâu?

Điểm khác biệt của phương pháp ta tự tạo và gọi hàm init() và sử dụng Constructor.

Constructor làm nhiều hơn là việc được gọi như một phương thức

Xét ví dụ với danh sách liên kết, thông thường ta tạo 1 loại dữ liệu là Node, Node có phần tử kế tiếp là địa chỉ của Node kế tiếp, khi Node được khởi tạo, thì phần tử kế tiếp là nullptr (biểu thị Node cuối cùng).

Khi chuẩn bị thêm 1 Node mới vào danh sách, ta cần khởi tạo 1 Node sao cho thuộc tính giữ địa chỉ của Node kế tiếp phải là nullptr.

struct Node
{
	Data* _data;
	Node* _next;
};

Giả sử ta sử dụng Node này, khởi tạo Node ta cần

Node* element = new Node();
element->_next = nullptr;

Ta phải đảm bảo rằng _next = nullptr trước đi đưa vào cuối danh sách. Tuy nhiên, nếu như ta sơ sót, nghĩa là ta quên thêm dòng số 2 thì đây không phải là phần tử cuối cùng. Để tránh điều này, ta cần các giá trị khởi tạo mặc định cho Node khi nó được tạo ra.

class Node
{
	Data* _data;
	Node* _next;
	
	Node()
	{
		this->_next = nullptr;
	}
};

Với cách thiết kế này nếu ta vừa khởi tạo Node, thì Constructor của Node sẽ được gọi ngay sau đó.

Node element;

Sau dòng khởi tạo, thì chắc chắn rằng element->_next có giá trị là nullptr. Ta thấy rằng Constructor giúp mọi thứ thuận tiện hơn trong ngữ cảnh này.

Vậy nếu bạn muốn 1 số thuộc tính hay hành động mặc nhiên xảy ra khi đối tượng được tạo ra, có thể sử dụng Constructor. Lưu ý rằng, không phải lúc nào cũng cần có nó.

Bài liên quan

THẢO LUẬN
Dựa trên tình hình đáng buồn của các chương trình máy tính của chúng ta, việc phát triển phần mềm hiển nhiên vẫn là một nghệ thuật hắc ám, và chưa thể được gọi là sự khổ luyện của các kỹ sư. Bill Clinton
ĐÓNG