Constructor & Destructor trong C++
Bởi vì class chứa những dữ liệu phức tạp bên trong, bao gồm dữ liệu và phương thức
nên việc khởi tạo và dọn dẹp đối tượng phức tạp hơn những cấu trúc đơn giản.
Constructor và destructor là những hàm đặc biệt dùng để khởi tạo và dọn dẹp đối
tượng. Constructor liên quan đến việc cấp phát (allocate) và khởi tạo (initialize) cho đối
tượng. Destructor liên quan đến việc huỷ bộ nhớ đã cấp phát (deallocate) đối tượng.
Giống như những phương thức khác, constructor và destructor được tạo trong phần
khai báo của class. Chúng có thể được cài đặt inline hoặc từ bên ngoài. Constructor và
destructor có những điểm chung sau :
Constructor và destructor đều không có kiểu trả về và chúng không thể trả về bất
cứ kiểu nào.
Trong C++, constructor và destructor không thể được khai báo với từ khoá static,
const hay volatile. Riêng constructor không thể được khai báo với từ khoá
virtual. Destructor có thể là virtual. Các lớp kế thừa từ nó sẽ thực thi destructor
của riêng mình.
Constructor và destructor thường là public. Nếu được khai báo protected thì chỉ
có các lớp kế thừa và friend có thể dùng để tạo đối tượng.
Dù có thể có constructor và destor không tham số nhưng luôn có 1 tham số
ngầm định là this khi gọi 2 hàm này.
Các lớp kế thừa không thể kế thừa hoặc overload các constructor và destructor
của lớp cha. Nhưng chúng luôn gọi constructor và destructor của lớp cha mỗi khi
chúng được tạo và huỷ.
default constructor sẽ bị vô hiệu hoá. Constructor là 1 hàm public, do đó nếu khai báo constructor thành private trình biên dịch sẽ báo lỗi rằng chúng ta đang cố truy cập đến 1 thành viên private của 1 lớp. Giống như các hàm thành viên khác, các constructor được phép có tham số mặc định Sample(int x = 0, char ch = ‘a’) {} Constructor gọi 1 constructor khác Trong 1 lớp có nhiều constructor, chúng ta có thể từ constructor này gọi 1 constructor khác mà không cần phải tạo lại một đối tượng khác class Sample { Sample() { Sample(3); // call Sample( int ) and print 3 } Sample( int x ) { cout<<x<<endl; } }; void main() { Sample s; // call Sample() } Đây là 1 trường hợp mà chúng ta có thể chủ động gọi constructor. Ta cũng có thể gọi destructor theo cách trên. Khởi tạo theo kiểu Initialization Aggregate Như đã biết C++ cho phép ta khai báo như sau để khởi tạo các thành viên của 1mảng hoặc struct. struct S { int a, b; }; void main() { int a[5] = {1, 2, 3, 4, 5}; //1 int b[5] = {1, 2, 3}; //2 S ss[3] = { {1, 2}, {3, 4}, {5, 6} }; //3 S xx[3] = { {1, 2} }; //4 } Trong trường hợp 1, cả 5 phần tử của a đều được gán trước bởi người lập trình. Trường hợp b là 1 dạng của trường hợp a nhưng thiếu 2 phần tử; lúc đó trình biên dịch sẽ tự động gán 0 vào 2 phần tử cuối. Tương tự đối với các trường hợp 3 và 4. Vậy thì cú pháp tiện dụng trên có áp dụng được với class không và nó có phá vỡ các constructor không khi ta đã cố ý gán giá trị cho các phần tử mà không thông qua constructor? Cú pháp trên vẫn áp dụng được cho class trong trường hợp tất cả các dữ liệu thành viên của của class đều là public (giống như struct) và không có constructor nào trong class class Sample { public: int a, b; }; void main() { Sample s[2] = { {1, 2}, {3, 4} }; } Nhưng nếu chúng ta chỉnh ít nhất 1 thành viên của Sample thành private lúc đó việc khởi tạo như trên sẽ không hợp lệ. Và trong trường hợp Sample có constructor thì việc khai báo như trên sẽ gặp lỗi. Thay vào đó chúng ta phải khai báo bằng constructor tương ứng. class Sample { public: int a, b; Sample(int _a, int _b) { a = _a; b = _b; } }; void main() { Sample s1[2] = { Sample(1, 2), Sample(3, 4) }; Sample* s2[2] = { new Sample(1, 2), new Sample(3, 4)}; } Như đã thấy, việc gọi constructor trong 1 danh sách khởi tạo được thực hiện bằng cách gọi hàm constructor tương ứng (tên lớp + danh sách tham số). Nhưng nếu chúng ta gọi thế này thì điều gì sẽ xảy ra? Sample s1[2] = { Sample(1, 2) } Rõ ràng trong danh sách khởi tạo, s1[0] đã được khai báo còn s1[1] đã bị bỏ qua. Đối với các kiểu dữ liệu cơ bản thì những phần tử bị thiếu này sẽ được gán thành 0 tương ứng. Nhưng trong trường hợp này trình biên dịch sẽ báo lỗi vì nó không tìm được 1 default constructor không tham số để khởi tạo s1[1]. Vậy ta phải khai báo thêm 1 constructor không tham số cho Sample để nó dùng làm default constructor. Copy Constructor Copy Constructor là 1 loại constructor đặc biệt. Copy constructor xuất hiện khi ta có nhu cầu sao chép dữ liệu từ 1 đối tượng này sang 1 đối tượng khác cùng lớp. Các trường hợp này có thể là : Khi 1 đối tượng đã được 1 tạo ra sẵn và 1 đối tượng khác được gán bằng đối tượng này. Sample A; Sample B = A; Xảy ra copy constructor của B khi B sao chép từ A Khi 1 đối tượng được truyền theo tham trị đến 1 hàm void Func(Sample s) { } ... Sample S1; Func(S1); Xảy ra copy constructor cho tham số s của hàm Func khi tham số này copy từ biến S1. Khi 1 đối tượng được return bởi 1 hàm. Sample Func() { Sample S; return S; } Copy constructor sẽ được gọi khi chương trình chạy đến return s; Khi 1 đối tượng được thow/catch khi handling exception try { ... Sample s; throw s; } catch(Sample exception) { ... }; Tại dòng throw s; và lúc catch(Sample exception) copy constructor sẽ được gọi. Khi 1 đối tượng được đặt trong phần khởi tạo của 1 mảng đối tượng khác Sample s1, s2; Sample arr[2] = { s1, s2 }; Đối tượng arr[0] đang chép từ s1 và arr[1] đang chép từ s2 nên copy constructor sẽ được gọi. Nói chung, các trường hợp sau là biến thể của trường hợp 1 Cú pháp của copy constructor Copy constructor thực chất cũng chỉ là 1 constructor bình thường với ít nhất 1 tham số dạng type& name được đặt đầu tiên. class Sample { ... Sample(Sample& copyFromMe) {} Sample(Sample& copyFromMe, int x) {} }; Các tham số đều được truyền theo tham chiếu để đảm bảo rằng tham số copyFromeMe ở trên được truyền theo địa chỉ chứ không phải là sao chép cả tham số, bởi vì như vậy, giống như đệ quy, ta lại gọi hàm conpy constructor 1 lần nữa. Nên đặt tham số này là const để tránh việc thay đổi nguồn sao chép. Chúng ta nên dùng copy constructor khi cần 1 bản sao sâu ( deep copy ) của 1 đối tượng, nghĩa là ta cần sao chép bộ nhớ động, nếu không thì ta không cần viết bởi trình biên dịch luôn tạo sẵn 1 copy constructor mặc định sao chép lần lượt toàn bộ thành viên của lớp (sao chép cạn – shallow copy). Sao chép cạn : Sao chép sâu : Khi ta không chỉ định một copy constructor nào cho class thì trình biên dịch sẽ tự động tạo 1 default copy constructor, copy chính xác từng byte dữ liệu(member-wise clone) từ đối tượng nguồn đến đối tượng đích. Nhưng nếu trong class có dữ liệu động thì nó chỉ tạo ra 1 bản copy cạn. Các biến thể khác của copy constructor (như hàm Sample(Sample& copyFromMe, int x)) chỉ được gọi khi ta yêu cầu chúng 1 cách tường minh (xem Constructor gọi 1 constructor) Khi dùng copy constructor ta nên chú ý các điểm sau: Xoá dữ liệu động của đích trước khi copy từ nguồn class Sample { int *x, size; Sample(int count) { x = new int[size = count]; } Sample(const Sample& copyFromMe) { delete[] x; x = new int[size = copyFromMe.size]; memcpy(x, copyFromMe.x, size*sizeof(int)); } }; void main() { Sample s1(2), s2(4); s1 = s2; } Ban đầu, s1 chứa 1 biến trỏ đến 1 khối int[2] trên heap, s2 chứa 1 biến trỏ đến 1 khối int[4] khác trên heap. Khi ta copy s2 sang s1 thì ta phải xoá vùng nhớ int[2] của s1 trước rồi mới tạo vùng nhớ mới cho s1. Tránh việc tự copy chính mình Việc gán s1 = s1 là 1 việc hoàn toàn hợp lệ. Nhưng xảy ra vấn đề là khi copy constructor được gọi, nó sẽ xoá vùng nhớ của chính nó, rồi lại tạo 1 vùng nhớ mới bằng kích thước vùng nhớ cũ. Đến đây sẽ xảy ra vấn đề khi sao chép bởi vùng nhớ cũ đã bị xoá, nên việc sao chép sẽ dẫn tới lỗi. Nên kiểm tra trước xem đối tượng có trỏ đến chính mình hay không khi thực hiện copy constructor class Sample { int *x, size; Sample(int count) { x = new int[size = count]; } Sample(const Sample& copyFromMe) { if(this != ©FromMe) { delete[] x; x = new int[size = copyFromMe.size]; memcpy(x, copyFromMe.x, size*sizeof(int)); } } }; Toán tử gán bằng và copy constructor Về bản chất, toán tử gán bằng tương tự như copy constructor nhưng có 1 vài điểm khác biệt giữa chúng. Copy constructor có thể được gọi khi đối tượng vừa được khai báo, chưa có dữ liệu. Toán tử gán bằng xảy ra khi đối tượng đã có sẵn dữ liệu class Sample { int x; Sample(int _x) { x = _x; } Sample(Sample& copyFromMe) { x = copyFromMe.x; } Sample operator=(Sample& rvalue) { x = rvalue.x; } } void main() { Sample s1(5), s2(10); Sample s2 = s1; //call copy constructor s2 = s1; //call operator= } Toán tử gán bằng có thể tạo thành 1 chuỗi biểu thức liên tục do có kiểu trả về còn copy constructor thì không. // s2 = s3 : assignment operator; // Sample s1 = s2 : copy constructor Sample s1 = s2 = s3; Destructor Định nghĩa Destructor làm công việc ngược lại với constructor. Nó giải phóng bộ nhớ khi đối tượng bị huỷ bỏ. Destructor có tên trùng với tên class nhưng có dấu ~ ở trước. Nó không có kiểu trả về, không có tham số (và do đó trong mỗi class chỉ có 1 destructor). Destructor không thể được khai báo const, volatile, virtual hay static. Destructor phát sinh khi : Biến đối tượng ra khỏi tầm của nó class Sample { public: Sample() { cout<<”created”<<endl; } ~Sample() { cout<<”destroyed”<<endl; } }; void main() { Sample s1; { Sample s2; } } Khi chạy đoạn code trên ta sẽ được created created destroyed destroyed Khi s2 ra khỏi scope của mình thì destructor cho s2 sẽ được gọi. Tương tự đối với s1.Xét đoạn code sau: void main() { { Sample s1; goto out; } out: //something } Trong đoạn code này, destructor của s1 vẫn được gọi dù có lệnh nhảy goto. Tương tự đối với các lệnh nhảy khác : break, throw Chú ý nếu s1 là con trỏ thì destructor sẽ không được tự động gọi khi ra khỏi scope mà phải nhờ vào trường hợp thứ 2. Đối tượng được huỷ bởi hàm delete Khi gọi delete lên 1 class có destructor thì nó được gọi và đối tượng bị huỷ ngay lập tức. void main() { Sample* s1 = new Sample(); delete s1; //call destructor } Khi chúng ta gọi destructor 1 cách tường minh thì không có nghĩa là đối tượng đã bị huỷ bỏ ngay lúc ta gọi destructor. Thực chất đối tượng vẫn tồn tại cho đến khi nó thật sự bị huỷ bỏ. Lúc đó destructor sẽ được tự động gọi. class Sample { public: Sample() { cout<<”created”<<endl; } ~Sample() { cout<<”destroyed”<<endl; } void print() { cout<<”Hello world”<<endl; } } void main() { Sample s1; s1.print(); s1.~Sample(); s1.print(); } Khi chạy đoạn code trên ta được created Hello world destroyed Hello world destroyed Rõ ràng lời gọi tường minh s1.~Sample() không có tác dụng,. Trừ khi trong class có bộ nhớ động và trong destructor có lời gọi huỷ bộ nhớ động này, lúc đó s1.~Sample() sẽ xoá bộ nhớ động này nên chương trình có thể hoạt động không chính xác về sau.
File đính kèm:
- Constructor & Destructor trong C++.pdf