----> Lời khuyên cho các bạn bắt đầu học lập trình: nếu có tham khảo code trên mạng thì các bạn nên xem nó và tự viết lại rồi tìm hiểu xem từng phần trong code ý nó có mục đích là gì chứ đừng nên copy và paste một cách máy móc mà mình chả hiểu được code ý nó viết gì.
Bài này mình sẽ dành để viết về constructor trong C++. Tại sao phải dùng constructor, dùng nó như thế nào, và những vấn đề cần lưu ý khi sử dụng constructor sẽ là những nội dung chính được đưa ra.
1. Vấn đề đặt ra
Giả sử ta tạo ra một lớp Rectangle (hình chữ nhật) như sau:
#include <string>
using namespace std;
// class definition
class Rectangle{
private:
int width; // chiều rộng
int height; // chiều cao
public:
// set width & height
void set_width(int); // nhập chiều rộng
void set_height(int); // nhập chiều cao
// get width & height
int get_width(); // lấy chiều rộng
int get_height(); // lấy chiều cao
// calculate area
int area(); // tính diện tích
};
// member function definitions
// set width
void Rectangle::set_width(int a){
width=a;
}
// set height
void Rectangle::set_height(int b){
height=b;
}
// get width
int Rectangle::get_width(){
return width;
}
// get height
int Rectangle::get_height(){
return height;
}
// calculate area
int Rectangle::area(){
return height*width;
}
Điều gì sẽ xảy ra khi ta gọi hàm tính diện tích area trước khi thiết lập chiều rộng và chiều cao cho hình chữ nhật như trong đoạn chương trình sau:
cout << my_rectangle.area() << endl; // in ra màn hình diện tích của my_rectangle
Giá trị thu được trên màn hình có thể là một số âm ! Câu lệnh thứ nhất khai báo đối tượng my_rectangle, chương trình sẽ cấp phát bộ nhớ cho các thành phần dữ liệu widthvà height, giả sử width rơi vào ô nhớ mà trước đó có lưu trữ giá trị 20, còn height rơi vào ô nhớ trước đó có lưu trữ giá trị -3. Ngay sau đó, câu lệnh thứ hai yêu cầu tính diện tích của my_rectangle rồi hiển thị ra màn hình, và kết quả ta thu được là diện tích my_rectangle bằng -60 ! Để đảm bảo mọi đối tượng đều được khởi tạo hợp lệ trước khi nó được sử dụng trong chương trình, C++ cung cấp một giải pháp đó là hàm tạo (constructor).
2. Hàm tạo (constructor)
Constructor là một hàm thành viên đặc biệt có nhiệm vụ thiết lập những giá trị khởi đầu cho các thành phần dữ liệu khi đối tượng được khởi tạo. Nó có tên giống hệt tên lớp để compiler có thể nhận biết được nó là constructor chứ không phải là một hàm thành viên giống như các hàm thành viên khác. Trong constructor ta có thể gọi đến các hàm thành viên khác. Một điều đặc biệt nữa là constructor không có giá trị trả về, vì vậy không được định kiểu trả về nó, thậm chí là void. Constructor phải được khai báo public. Constructor được gọi duy nhất một lần khi đối tượng được khởi tạo. Những lớp không khai báo tường minh constructor trong định nghĩa lớp, như lớp Rectangle ở trên của chúng ta, trình biên dịch sẽ tự động cung cấp một “constructor mặc định" (default constructor). Construtor mặc định này không có tham số, và cũng không làm gì cả. Nhiệm vụ của nó chỉ là để lấp chỗ trống. Nếu lớp đã khai báo constructor tường minh rồi thì default constructor sẽ không được gọi. Bây giờ ta sẽ trang bị constructor cho lớp Rectangle:
private:
int width;
int height;
public:
// constructor
Rectangle();
/* các hàm khác khai báo ở chỗ này */
};
// member function definitions
// constructor
Rectangle::Rectangle(){
width=0;
height=0;
}
/* các hàm khác định nghĩa ở đây */
Khi đó câu lệnh
sẽ tạo ra một đối tượng my_rectangle có width=0 và height=0.
3. Thiết lập giá trị bất kỳ cho các thành phần dữ liệu khi khởi tạo đối tượng
Một vấn đề được đặt ra là có thể khởi tạo những giá trị nhau khác cho các đối tượng ngay lúc khai báo không? Giống như với kiểu int:
int b=100;
int c=1000;
C++ hoàn toàn cho phép chúng ta làm điều này. Có một số cách để thiết lập những giá trị khác nhau cho các thành phần dữ liệu trong khi khai báo.
Cách thứ nhất: viết thêm một hàm tạo nữa có tham số.
C++ hoàn toàn không giới hạn số lượng constructor. Chúng ta thích viết bao nhiêu constructor cũng ok. Đây chính là khả năng cho phép quá tải hàm của C++ (function overloading), trong trường hợp của ta là quá tải hàm tạo. Tức là cùng một tên hàm nhưng có thể định nghĩa theo nhiều cách khác nhau để dùng cho những mục đích khác nhau. Để quá tải một hàm (bất kỳ) ta chỉ cần cho các hàm khác nhau về số lượng tham số , kiểu tham số còn giữ nguyên tên hàm. Tạm thời cứ thế đã, tớ sẽ đề cập rõ hơn trong một bài riêng cho functions. Bây giờ ta sẽ bổ sung thêm một constructor nữa vào định nghĩa lớp Rectangle:
private:
int width;
int height;
public:
// constructor
Rectangle(); // hàm tạo không có tham số
Rectangle(int, int); // hàm tạo với hai tham số
/* các hàm khác khai báo ở chỗ này */
};
// member function definitions
// constructor with no parameters
Rectangle::Rectangle(){
width=0;
height=0;
}
// constructor with two parameters
Rectangle::Rectangle(int a, int b){
width=a;
height=b;
}
/* các hàm khác định nghĩa ở đây */
Bây giờ ta sẽ test bằng chương trình sau:
Rectangle rectB(3,4); // gọi hàm tạo có tham số
cout << rectA.area() << endl; // kết quả là 0
cout << rectB.area() << endl; // kết quả là 12
C++ sẽ tự nhận biết để gọi constructor phù hợp. Trong đoạn chương trình trên, câu lệnh thứ nhất khởi tạo đối tượng rectA nhưng không kèm theo truyền tham số vào, nên compiler sẽ gọi tới hàm tạo thứ nhất, tức hàm tạo không có tham số. Sau câu lệnh này rectA đều có width và height đều bằng 0. Câu lệnh thứ hai khởi tạo đối tượngrectB, nhưng đồng thời truyền vào hai đối số là 3 và 4. Do đó compiler sẽ gọi đến hàm tạo thứ hai. Sau câu lệnh này rectB có width=3 còn height=4. Và kết quả ta được diện tích thằng rectA là 0, còn rectB là 12.
Cách thứ hai: dùng đối số mặc định (default arguments)
Chúng ta vẫn làm việc với lớp Rectangle ở trên và sẽ chỉ dùng một hàm tạo nhưng “chế biến” nó một chút:
private:
int width;
int height;
public:
// constructor
Rectangle(int =0, int =0); // hàm tạo với đối số mặc định
/* các hàm khác khai báo ở chỗ này */
};
// member function definitions
// constructor with default arguments
Rectangle::Rectangle(int a, int b){
width=a;
height=b;
}
/* các hàm khác định nghĩa ở đây */
Chúng ta chú ý đến khai báo của hàm tạo:
Khai báo này cho biết, khi khai báo đối tượng, nếu đối số nào bị khuyết (tức không được truyền vào) thì sẽ được mặc định là 0. Và để đảm bảo không xảy ra sự nhập nhằng, C++ yêu cầu tất cả những đối số mặc định đều phải tống sang bên phải nhất (rightmost), tức ngoài cùng bên phải. Vì vậy:
Rectangle rectB(4); // sẽ gán width=4, height=0
Rectangle rectC(2,6); // sẽ gán width=2, height=6
Chú ý: giá trị mặc định (ví dụ int =0) chỉ được viết lúc khai báo hàm, chứ không phải lúc định nghĩa hàm. Nếu ta viết lại những giá trị mặc định này trong danh sách tham số lúc định nghĩa hàm sẽ gây lỗi biên dịch.
Rectangle::Rectangle(int a=0, int b=0){ // error
width=a;
height=b;
}
4. Hàm tạo mặc định
Như đã nói ở trên, nếu ta không cung cấp hàm tạo cho lớp thì compiler sẽ làm điều đó thay chúng ta. Nó sẽ cung cấp một hàm tạo không tham số và không làm gì cả ngoài việc lấp chỗ trống. Đôi khi hàm tạo không có tham số do người dùng định nghĩa cũng được gọi là hàm tạo mặc định (hay ngầm định). Chúng ta xem xét chuyện gì sẽ xảy ra nếu như không có hàm tạo ngầm định khi khai báo một mảng các đối tượng. Ví dụ vẫn là lớp Rectangle với hàm tạo hai tham số:
private:
int width;
int height;
public:
// constructor
Rectangle(int, int); // hàm tạo với hai tham số
/* các hàm khác khai báo ở chỗ này */
};
// member function definitions
// constructor with 2 parameters
Rectangle::Rectangle(int a, int b){
width=a;
height=b;
}
/* các hàm khác định nghĩa ở đây */
Nếu như ta khai báo một mảng tầm chục thằng Rectangle thì chuyện gì sẽ xảy ra?
Rectangle rect_array[10]; // chục thằng thì có vấn đề - error
Điều này là do ta cần khai báo 10 thằng Rectangle nhưng lại không cung cấp đủ tham số cho chúng, vì hàm tạo yêu cầu hai tham số cần phải được truyền vào. Giải quyết chuyện này bằng cách bổ sung thêm một hàm tạo không có tham số hoặc chỉnh lại tất cả các tham số của hàm tạo hai tham số bên trên thành dạng đối số mặc định là ok.