Khởi tạo và hủy
-Trongbấtcứmột ngôn ngữlập trình nào trước khi thao tác hayxử lý
trênbấtkỳmột đốitượng nào chúng tabắt buộc phảitạo (khởitạo)
chúng. Điều này là dĩ nhiên!!.
-Sau khikết thúc chương trình hoặc không dùng đếnmột đốitượng nào
đócủa chương trình, chúng ta nên xóa (hủy) đốitượng tránh việc đối
tượng chiếm giữ mãi mãi vùng nhớ đượccấp cho đốitượngbởi chương
trình để những đốitượng khác có thể được khởitạo (tránh lãng phíbộ
nhớ và làmtăng kích thước chương trình khi chạy). Điều này là không
bắt buộc nhưng chắc chắnmột điều lànếu chúng ta không làm điều này
thì chương trìnhcủa chúng tasẽ gây ramộtsốlỗi nghiêm trọng có thể
ảnhhưởng đếnhệ thống.
tự động gọi khi đối tượng được định nghĩa, còn hàm hủy thì ngược lại được trình biên dịch tự động gọi khi đối tượng ra khỏi phạm vi của nó. · Hàm khởi tạo có thể được nạp chồng còn hàm hủy thì không, tức là mỗi lớp có nhiều nhất một hàm hủy. Ví dụ 1: class ViDu { public: ViDu() { cout<<"Constructor"<<endl; } ~ViDu() { cout<<"Destructor"<<endl; } }; int main() { ViDu a; { ViDu b; for (int i=0; i<2; ++i) { ViDu c; getch(); } } getch(); return 0; } Ø Kết quả: Constructor Constructor Constructor Destructor Constructor Destructor Destructor Destructor *Giải thích: Hai dòng đầu tiên là kết quả của khai báo ViDu a, ViDu b được trình biên dịch tự động gọi hàm khởi tạo mặc định a.ViDu(), b.ViDu(). Tương tự cho khai báo ViDu c khi vào vòng lặp for, sau đó đợi nhập một phím bất kỳ, ra khỏi vòng lặp và bắt đầu một vòng lặp mới, khi đó hàm hủy cho đối tượng c được tự động gọi, tương tự cho vòng lặp thứ hai. Tiếp theo hàm hủy của đối tượng b được gọi tự động vì nó đã ra khỏi phạm vi định nghĩa của nó. Cuối cùng, chờ nhập một phím bất kỳ và hàm hủy của đối tượng a được tự động gọi, kết thúc chương trình. Ví dụ 2: class ViDu { private: int* x; public: ViDu(int a=0) { x=new int; *x=a; } ~ViDu() { delete x; } } int main() { ViDu* a=new ViDu; delete a; return 0; } *Giải thích: a được định nghĩa là một con trỏ trỏ đến đối tượng của lớp ViDu, khi đó new ViDu sẽ gọi hàm khởi tạo của lớp với tham số mặc định và đối tượng này được con trỏ a trỏ đến. Nếu trong ví dụ này ta không định nghĩa hàm hủy thì câu lệnh delete a vẫn thu hồi vùng nhớ được cấp phát cho đối tượng được nó trỏ đến nhưng vùng nhớ do thành viên con trỏ của lớp vẫn không được thu hồi. Khi ta định nghĩa hàm hủy thì delete a trước hết sẽ gọi đến hàm hủy của đối tượng thu hồi vùng nhớ của thành viên con trỏ, sau đó sẽ thu hồi vùng nhớ của đối tượng. 2. Cơ chế hoạt động của hàm hủy: -Cũng như hàm khởi tạo, hàm hủy trong C++ được tự động gọi bởi trình biên dịch và không có một sự lựa chọn nào cho lập trình viên. -Hàm hủy thường được sử dụng khi trong lớp có thành viên con trỏ được cấp phát động bởi toán tử new. Khi đó ta phải định nghĩa hàm hủy để thu hồi vùng nhớ mà thành viên con trỏ chiếm giữ trong Heap bằng toán tử delete. Bởi vì khi một vùng nhớ được cấp phát bởi toán tử new thì nó chỉ được thu hồi bởi toán tử delete, trình biên dịch không giúp ta trong trường hợp này. Ví dụ: class A { private: int* x; public: A(int a=0) { x=new int; *x=a; } ~A() { delete x; } }; -Khác với C++, trong ngôn ngữ C# và Java vấn đề không khó khăn như trong C++. Bởi vì, trong C# và Java cung cấp sẵn một cơ chế thu hồi vùng nhớ rất mạnh mẽ và linh hoạt, đó là trình thu gom rác garbage- collector (gc). Vì tất cả đối tượng trong C# và Java đều là kiểu tham chiếu nên khi định nghĩa một đối tượng ta bắt buộc phải khởi tạo chúng, khi đó chúng được cấp phát một vùng nhớ thuộc trình garbage-collector (gc) quản lý và thời gian sống cũng như vị trí của chúng được quản lý bởi trình này, chúng ta chỉ thao tác với đối tượng thông qua tham chiếu đến đối tượng mà thôi. -Trình thu gom gc được tự động gọi mỗi khi máy tính rảnh rỗi hoặc khi định nghĩa một đối tượng hoặc khi một đối tượng ra khỏi phạm vi của mình. Trình thu gom có cách biết khi nào một đối tượng không cần đến nữa trong chương trình và tự động thu hồi vùng nhớ do đối tượng đó chiếm giữ. Trình thu gom có thể tự quyết định di chuyển đối tượng trong bộ nhớ (tránh làm phân mảnh bộ nhớ) để nhường chỗ cấp phát cho những đối tượng mới và nhằm tiết kiệm bộ nhớ. Do vậy mà trong chương trình địa chỉ của đối tượng có thể thay đổi nhưng tham chiếu đến đối tượng đó vẫn không thay đổi. Ví dụ: Trong C#: class A{} class Test { public static void main() { A a=new A(); A b=new A(); a=b; } } *Giải thích: Ở đây phép gán a=b sẽ đưa hai đối tượng a, b trở thành một, tức là cả a và b cùng tham chiếu đến một đối tượng trong bộ nhớ, khi đó đối tượng do a tham chiếu lúc đầu sẽ được trình thu gom thu hồi vùng nhớ. Bởi vì đối tượng đó không có một tham chiếu nào đến nó cả, tức là nó không còn dùng được trong chương trình. C. MỘT SỐ VẤN ĐỀ KHÁC: 1. Hàm khởi tạo: -Giả sử ta có một lớp có constructor đơn giản như sau: class A { public: A() {} }; int main() { A a; return 0; } Khi ta định nghĩa A a, nghĩa là khi biên dịch trình biên dịch tự động thay vào đó là lời gọi hàm A::A() của đối tượng a. Và tất cả các lời gọi hàm thành viên của một đối tượng luôn đi kèm với con trỏ ẩn – this – là một con trỏ ẩn của lớp được trình biên dịch tự động thêm vào, nó chứa địa chỉ của đối tượng đang được gọi, tức là nó trỏ tới chính đối tượng đang chứa nó. 2. Hàm hủy: -Phải chú ý một điều là trình biên dịch không tự động gọi hàm hủy một đối tượng được trỏ bởi con trỏ kiểu void. Khi đó nếu ta không để ý thì vùng nhớ của đối tượng vẫn được thu hồi nhưng vùng nhớ của thành viên con trỏ của đối tượng trỏ đến sẽ không được thu hồi. Ví dụ: class A { private: char* s; public: A(char c='A') { s=new char; *s=c; } ~A() { delete s; } }; int main() { A* a=new A; delete a; void* b=new A('Z'); delete b; } Khi gọi delete a hàm hủy của đối tượng được a trỏ tới sẽ được gọi trước sau đó vùng nhớ do a trỏ tới sẽ được thu hồi, do đó vùng nhớ do a trỏ tới lẫn vùng nhớ do con trỏ thành viên của lớp trỏ tới đều được thu hồi. Trong khi đó, delete b sẽ không gọi đến hàm hủy (vì b có kiểu void*), do đó chỉ thu hồi được vùng nhớ do b trỏ tới. -Trong C++, hàm hủy phải làm tất cả, từ việc thu hồi vùng nhớ của các thành viên đến các việc dọn dẹp đối tượng trước khi hủy luôn đối tượng (ví dụ như xóa hình vẽ của đối tượng trên màn hình). Vì vậy trong C++ hàm hủy quan trọng không kém hàm khởi tạo. -Nhưng trong C# và Java, điều đó không cần thiết lắm vì chúng có cơ chế quản lý và thu hồi bộ nhớ rất linh hoạt (gc) nên hàm hủy chỉ đóng vai trò dọn dẹp đối tượng trước khi trình thu gom thu hồi vùng nhớ của đối tượng. -Một thông báo lỗi sẽ xuất hiện lúc biên dịch khi ta sử dụng goto hoặc switch để nhảy qua câu lệnh mà tại đó đối tượng được định nghĩa. Do vậy, ta không nên dùng goto hoặc định nghĩa đối tượng trong switch. Ví dụ: class A { public: A() {} }; int main() { int i; cin>>i; switch(i) { case 1: A a; break; case 2: //Lỗi A b; break; } if (i) goto Nhay; //Lỗi A a; Nhay: getch(); return 0; } Nếu lớp A của ta không định nghĩa hàm khởi tạo thì sẽ không xuất hiện thông báo lỗi, nhưng khi đó các biến thành viên của lớp sẽ chứa giá trị rác và dẫn đến kết quả không mong muốn khi chạy chương trình. 3. Phạm vi của biến và đối tượng: -Khác với C, biến và đối tượng trong C++ có thể được định nghĩa bất cứ ở đâu trong chương trình. -Biến và đối tượng chỉ tồn tại từ lúc chúng được định nghĩa cho đến khi ra ngoài khối mà chúng được định nghĩa, khối đó được đánh mở đầu và kết thúc bằng cặp ngoặc nhọn {…}. -Đối với đối tượng có hàm hủy, khi ra khỏi phạm vi định nghĩa thì trình biên dịch tự động gọi hàm hủy của chính đối tượng đó rồi thu hồi vùng nhớ của đối tượng (trừ những đối tượng được cấp phát động). -Trong C++, biến đếm được định nghĩa bên trong vòng lặp for (điều mà ở C không có). Ở một số trình biên dịch cũ không cho phép biến được định nghĩa trong vòng lặp for vẫn còn tồn tại sau khi ra khỏi vòng lặp, ví dụ như: for (int i=0; i<10; ++i); cout<<i; sẽ bị báo lỗi khi dịch. Nhưng ở một số trình biên dịch khác như VC++ thì ngược lại, trình biên dịch vẫn cho phép biến được định nghĩa trong vòng lặp vẫn còn tồn tại sau khi ra khỏi vòng lặp. 4. Đối tượng hằng (Constant Object): -Cũng như các biến hằng, các đối tượng hằng cũng được khai báo giống như biến. Khi được khai báo là đối tượng hằng, trình biên dịch sẽ không cho phép bất cứ một sự thay đổi nào trong dữ liệu thành viên của đối tượng trong quá trình sống của nó. Bởi vậy, chúng phải được định nghĩa ngay khi khai báo và chỉ nhưng hàm thành viên hằng mới được tác động lên đối tượng hằng. 5. Hàm thành viên hằng: -Hàm thành viên hằng là hàm thành viên mà nội dung của hàm phải đảm bảo rằng không làm thay đổi bất kỳ thành viên dữ liệu nào của đối tượng. Nó được khai báo bằng từ khóa const đặt phía sau của khai báo hàm. Hàm hằng ngoài việc thao tác được với các đối tượng bình thường thì nó có thể thao tác được với các đối tượng hằng. Ví dụ: class Diem { private: int x; int y; public: Diem(int a=0, int b=0) { x=a; y=b; } void InDiem() const { cout<<x<<" "<<y<<endl; } } int main() { Diem a; const Diem b(1, 3); a.InDiem(); //OK b.InDiem(); //OK return 0; } Nếu hàm InDiem() không phải hàm hằng thì câu lệnh b.InDiem() sẽ bị lỗi!! 6. Con trỏ hằng và hằng con trỏ: -Con trỏ trỏ đến đối tượng hằng (con trỏ hằng) là con trỏ mà không được phép thay đổi giá trị của đối tượng mà nó trỏ tới. Ví dụ: int x=2; int y=1; const int* a=&x; hoặc int const* a=&x; //a là con trỏ hằng x=3; //OK *a=4 //Lỗi a=&y; //OK -Hằng con trỏ là con trỏ mà giá trị của nó được xác định ngay khi khai báo và không được thay đổi giá trị của nó. Tức là hằng con trỏ trỏ đến một đối tượng cố định, không thể thay đổi để nó trỏ tới một đối tượng khác và do đó nó phải được khởi tạo ngay khi khai báo. Ví dụ: int x=3; int y=4; int* const a=&x; *a=5; //OK a=&y; //Lỗi 7. Tham chiếu hằng: -Tham chiếu hằng là là biến mà tham chiếu của nó không được phép thay đổi giá trị. Ví dụ: int x=3; const int& a=x; x=5; //OK a=4; //Lỗi -Bởi vì bản chất của tham chiếu là hằng, luôn gắn liền với một đối tượng và phải được khởi tạo khi khai báo nên không có hằng tham chiếu.
File đính kèm:
- Khởi tạo và hủy.pdf