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 thay vì gò ép trong khuôn khổ.

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.

Tôi muốn khẳng định cho bạn 1 điều, khi bạn học 1 kiến thức của người khác, thì kiến thức đó được hình thành từ kinh nghiệm và các vấn đề mà họ đã giải quyết và gặp phải trước đó, nếu bạn chưa bao giờ gặp thì bạn vẫn không thể hiểu được vì sao có nó.

Tiền đề bài viết

Bài viết này nhằm tặng cho bạn Nguyễn Minh Triết, đã thông qua STDIO Fanpage và có nhu cầu cần STDIO có thêm bài viết này để chia sẻ cho các bạn khác. Tôi xem như bài viết này là món quà tặng riêng cho bạn.

ss_1

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

Nguyễn Minh Triết, và các độc giả phải biết qua về Constructor, Destructor trong class C++.

Biết thêm về danh sách liên kết.

Constructor và Destructor là gì?

Nói vắn tắt như sau:

  • 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 (thu hồi).

Một hiểu lầm thường gặp đối với các bạn mới tiếp cận đó 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 đó. <= SAI
  • 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 đó. <= SAI

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ên Constructor trùng với tên class và không có kiểu trả về, có thể overload.
  • Destructor: được định nghĩa bằng cách, 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;

}

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 23, khi sắp kết thúc hàm main, tức là đố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à: pride được tạo xong > Gọi Constructor ... > Gọi Destructor > Thu hồi 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();
}

Bạn hãy để ý dòng code 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), và tôi muốn bạn hiểu rằng, dòng 15 này chính là "Constructor" đang được gọi, chỉ khác biệt ở Constructor tự nhiên là, nó được tự động gọi.

Tương tự cho việc hủy, ta sẽ tự động 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();
}

Với code ví dụ như trên, ta vẫn được kết quả giống như khi sử dụng Constructor và Destructor.

Vậy điểm khác biệt hay vấn đề nằm ở đâu? Tại sao những người đặt nền móng cho ngôn ngữ hay phương pháp lập trình này lại đưa ra các khái niệm đó?

Đ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 tự thiết kế, thông thường ta tạo 1 loại dữ liệu là Node và có phần tử kế tiếp là địa chỉ của Node kế tiếp, nếu như Node kế tiếp là NULL (hoặc nullptr C++11) thì đó chính là Node cuối cùng trong danh sách.

Do đó, khi ta 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à NULL.

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

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

class element;
element._next = NULL;

// hoặc

Node* pelement = new Node();
pelement->_next = NULL;

Xét các dòng code trên ta thấy rằng, vấn đề nảy sinh là lúc này ta phải đảm bảo rằng _next = NULL 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 hoặc dòng số 7 và dĩ nhiên là, có lúc ta sẽ quên và nó mang đến rủi ro tiềm ẩn (chúng ta không chắc rằng trình biên dịch sẽ làm điều này thay chúng ta).

Và chúng ta đặt ra vấn đề "phải chi mà có cách để 1 Node khi vừa sinh ra đời thì bản chất của nó đã tồn tại như vậy", và Constructor lúc này sẽ mang ý nghĩa đó. Ta viết lại thiết kết của struct Node như sau

class Node
{
	Data* _data;
	Node* _next;
	
	Node()
	{
		this._next = NULL;
	}
};

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 đó mà không cần ta phải nhớ để rồi quên.

Node element;

Sau dòng khởi tạo, thì chắc chắn rằng element._next có giá trị là NULL.

THẢO LUẬN
ĐÓNG