Còn cái cuối cùng nữa là copy constructor, bạn chỉ cần nhớ copy constructor dùng để tránh hiện tượng dùng chung vùng nhớ và một vùng nhớ bị xóa 2 lần. Và copy constructor chỉ cần thiết khi lớp định nghĩa có thành phần dữ liệu động. Thêm nữa là operator= (assignment operator). Khi nào có thành phần dữ liệu động, để an toàn cứ auto viết thêm operator= với copy constructor.
Đi vào vấn đề…
Đầu tiên mình muốn làm rõ cái này xí, giả sử ta khai báo:
-
PhanSo a;
-
PhanSo b(a);
hoặc PhanSo b = a;
-
PhanSo b; b = a;
Ở dòng 1. Đối tượng a thuộc kiểu kiểu dữ liệu PhanSo đã được tạo ra, được tạo ra mà không có đối số, tức là nó đã được tạo ra bằng constructor mặc định, không có gì để bàn cãi.
Ở dòng 2 thì 2 cái khai báo đó là như nhau, đối tượng b cũng thuộc kiểu dữ liệu PhanSo được tạo, và nó gán bằng a. Đúng không? Thực ra đó không phải là phép gán bằng (assignment operator). Đó là copy constructor. Đối tượng b được tạo ra bằng copy constructor với đối số truyền vào là a. Phép gán bằng được thực hiện thực ra là ở dòng 3. dòng 2 và dòng 3 thực ra là khác nhau. Bạn có thể chạy chương trình dưới đây để kiểm chứng.
struct DayNhauHoc
{
DayNhauHoc() { }
DayNhauHoc(const DayNhauHoc & dnh) { cout << “copy constructor” << endl; }
void operator=(const DayNhauHoc & dnh) { cout << “assignment operator” << endl; }
};
int main()
{
DayNhauHoc dnh1;
DayNhauHoc dnh2 = dnh1;
DayNhauHoc dnh3; dnh3 = dnh1;
}
Thực ra cái đoạn ở trên không liên quan đến việc tại sao ta lại cần một copy constructor đâu ><
Giờ mới vào vấn đề nè. Copy constructor là gì. Copy constructor nó cũng chỉ là một constructor như bao constructor khác, cũng chỉ để tạo đối tượng. Nhưng thay vì tạo một đối tượng không có giá trị thì nó sẽ tạo một đối tượng dựa vào một đối tượng đã tồn tại trước đó vậy thôi. Khi ta không khai báo constructor, chương trình tự tạo cho ta constructor mặc định với đ có gì trong đó :)), Ngoài ra, chương trình sẽ luôn tự tạo cho ta thêm constructor copy nữa. Và copy constructor nó làm cái chuyện đó là sao chép tương ứng từng thành phần dữ liệu một của đối tượng đối số truyền vào vào đối tượng *this sắp được tạo ra đó. Oke, trong đa số trường hợp thì constructor copy của hệ thống tự tạo đó là ổn. Đơn giản là copy dữ liệu thôi mà. Tuy nhiên, khi lớp định nghĩa có thành phần dữ liệu động, sẽ nảy sinh vấn đề vùng nhớ bị xóa 2 lần. Tức là sao?
Giả sử ta có một lớp đa thức với các thành phần dữ liệu là một mảng động dùng để chứa hệ số như bên dưới, có phương thức cộng hai đa thức:
class DaThuc
{
float *heso;
int bac;
public:
DaThuc Cong(const DaThuc &daThuc2) const
{
DaThuc result;
// Cộng hai đa thức this và daThuc2 lại thành result;
return result;
}
};
Khi return result;
tức là đã có gọi copy constructor. Tức là sao? Ví dụ
DaThuc a, b, c;
a = b.Cong©;
Cái dòng 2, đối tượng b sẽ gọi hàm Cong với đối số là c. Trong phương thức này, để tính ra kết quả ta cần tạo ra một đối tượng result
, cái này thì cũng không có gì để nói. Sau khi tính toán xong, vấn đề là nó sẽ return result
để trả về giá trị cho cái b.Cong(c)
. Ở đây, bản chất b.Cong(c)
cũng là một đối tượng mới được tạo ra bằng copy constructor, copy của cái thằng result
. Sau đó cái đối tượng b.Cong(c)
đó lại gán cho đối tượng a bằng operator=. Đó là những gì diễn ra ở 2 hàm trên. Đó là lý do tại sao ta cần copy constructor và operator=. Nhưng như mình đã nói ở trên chương trình luôn tự tạo 2 cái đó cho ta, vậy copy constructor mặc định của hệ thống có vấn đề gì? Vấn đề ở đây là nó copy toàn bộ dữ liệu của đối tượng đối số vào đối tượng được khởi tạo. Giả sử DaThuc a(b);
. Nó sẽ copy toàn bộ thành phần dữ liệu của b vào a, trong đó có con trỏ int *heso
. Con trỏ đó có gì, có địa chỉ của mảng hệ số, tức là 2 đối tượng đó dùng chung một mảng hệ số vì cùng trỏ tới cùng một mảng hệ số mà. Khi hệ số của đối tượng a thay đổi thì của b cũng thay đổi và ngược lại. Trong khi chúng ta chỉ muốn copy của thằng b vào thằng a cái mảng đó rồi sau đó đường ai nấy đi, dùng riêng. Và, ““giả sử”” ta có câu lệnh delete[] heso
trong destructor. Khi đối tượng a hết phạm vi hoạt động, mảng hệ số được giải phóng => của b cũng bị giải phóng => chương trình gặp lỗi. Đó là lý do ta cần viết riêng copy constructor sao cho đối tượng a được tạo ra với một mảng heso khác, chỉ copy giá trị của từng phần tử thuộc mảng từ đối tượng b thôi chứ không dùng luôn mảng đó, (chỉ được copy thành cái khác rồi dùng chứ không được dùng chung). Tương tự ta cần thêm cái operator=
Dài quá… Hy vọng bạn hiểu :3