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ỷ.

pdf18 trang | Chuyên mục: Lập Trình Hướng Đối Tượng | Chia sẻ: dkS00TYs | Lượt xem: 7092 | Lượt tải: 0download
Tóm tắt nội dung Constructor & Destructor trong C++, để xem tài liệu hoàn chỉnh bạn click vào nút "TẢI VỀ" ở trên
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:

  • pdfConstructor & Destructor trong C++.pdf