Lập trình hướng đối tượng - Chương 6: Tạo và hủy

 

Cùng với đó, việc kiểm soát truy cập đã cải thiện hơn và dễ dàng sử dụng. Khái niệm “Kiểu dữ liệu mới” mà họ cung cấp có vài điểm tiến bộ hơn kiểu dữ liệu đã có trong C. Trình biên dịch C++ có thể kiểm tra kiểu dữ liệu một cách chặt chẽ và đảm bảo với một cấp độ an toàn hơn khi sử dụng kiểu dữ liệu.

 

Khi nói đến an toàn, có nhiều điều mà trình biên dịch có thể làm cho chúng ta hơn là C đã cung cấp. Khi lập trình với trình biên dịch C++ bạn sẽ cảm thấy mình dễ thấy lỗi hơn so với C, thông thường là trước khi biên dịch chương trình, trình biên dịch sẽ báo lỗi (error) hay cảnh báo (warning) và chĩ những chỗ lỗi cho bạn. Do vậy, bạn sẽ dễ dàng sữa lỗi và chương trình của bạn sẽ có thể chạy được nhanh hơn (do tìm thấy lỗi và fix nhanh hơn)

 

Hai trong số vấn đề an toàn được đưa ra ở đây là tạo và hủy. Phần lớn lỗi trong C xảy ra là do người sử dụng quên tạo hay hủy một biến

Trong C++, khái niệm về tạo và hủy lả rất quan trọng. Việc này cần thiết để loại bỏ những lỗi nhỏ xảy ra khi người dùng quên đi các hoạt động này. Chương này sẽ giới thiệu các tính năng trong C++ giúp đảm bảo cho việc tạo và hủy.

 

pptx19 trang | Chuyên mục: Lập Trình Hướng Đối Tượng | Chia sẻ: dkS00TYs | Lượt xem: 1464 | Lượt tải: 0download
Tóm tắt nội dung Lập trình hướng đối tượng - Chương 6: Tạo và hủy, để xem tài liệu hoàn chỉnh bạn click vào nút "TẢI VỀ" ở trên
 xảy ra nếu nó can thiệp vào phần cứng, hay hiện một cái gì đó lên màn hình, hay việc cấp phát bộ nhớ trên heap quá lớn? Nếu bạn chỉ cần biết ma-ke-no (mặc kệ nó), thì bạn sẽ không bao giờ đạt được cảnh giới cao về việc kết cấu chặt chẽ. Trong C++, việc hủy cũng quan trọng không kém gì việc tạo, do đó bạn cần phải biết đến Hàm Hủy. Cú pháp của hàm hủy cũng gần như tương tự với cú pháp của hàm tạo: tên hàm giống với tên lớp. Tuy nhiên hàm hủy được bắt đầu với một dấu ngã (~). Và, hàm hủy hàm hủy không được quyền có tham số. Bên dưới là một vì dụ về hàm hủy: class Y {	 public:	 ~Y();	 };   Trình biên dịch sẽ tự động gọi hàm hủy khi đối tượng thoát khỏi vùng hoạt động (scope). Như bạn đã biết ở trên, hàm tạo được gọi mỗi khi đối tượng được định nghĩa, còn hảm hủy chỉ được gọi khi gặp dấu ngoặc nhọn ( } ) kết thúc vùng hoạt động của đối tượng. Nếu bạn dùng lệnh goto để nhảy ra khỏi vùng hoạt động thì hàm hủy vẫn không được gọi, do nó vẫn chưa gặp dấu ngoặc nhọn, bạn nên chú ý điều này, và những hàm khác trong thư viện của C như setjmp() hay longjmp() cũng không thể gọi được hàm hủy. Hàm Hủy Ví dụ sau sẽ minh họa rõ hơn về cách hoạt động của hàm tạo và hàm hủy: //: C06:Constructor1.cpp/ / Constructors & destructors #include using namespace std;  class Tree { int height; public: Tree(int initialHeight); // Constructor ~Tree(); // Destructor void grow(int years); void printsize(); };  Tree::Tree(int initialHeight) { height = initialHeight; }  Tree::~Tree() { cout #include using namespace std;  class G { int i; public: G(int ii); };  G::G(int ii) { i = ii; }  int main() { cout > retval; require(retval != 0); int y = retval + 3; G g(y); } ///:~  Trong phần chương trình trên, bạn có thể thấy rằng trong hàm main(), biến retval được định nghĩa sau khi hàm cout được thực hiện, và sau đó y và g được định nghĩa. Nếu bạn làm như trên đối với C chứ không phải C++, hiễn nhiên bạn sẽ gặp error trước khi chương trình của bạn chạy được, chính xác là sau lúc bạn biên dịch chương trình chừng ½ giây. Cải thiện việc khai báo biến trong C++ Lời khuyên: mặc dù khai báo biến hay đối tượng ở đầu chương trình hay trước khi bạn sử dụng chĩ là “phong cách lập trình”, người thế này, người thế khác. Nhưng tôi khuyên bạn nên sử dụng cách mà từ nảy giờ tôi đã đề cập đến, không phải vô ý mà C++ có tiến bộ hơn C về việc khai báo biến ở bất cứ nơi đâu trong chương trình cũng được. Tất nhiên nó phải có ích lợi thì ta mới sử dụng chứ. Nếu mới viết chương trình bạn dự định có những biến mà bạn suy nghĩ sẽ sử dụng, lỡ thiếu thì bạn phải quay lên trên, thêm vào, còn nếu dư thì đôi khi bạn mặc kệ nó, vùng nhớ sẽ cấp phát cho nó, nhưng không làm gì cả. Việc cần biến hay đối tượng mới khai báo cũng giúp cho việc giảm đi những biến chưa cần tới mà đã cấp phát trong bộ nhớ. Còn nữa, đối với người đọc code của bạn, mỗi khi người ta thấy một biến mới, thì họ phải kéo lên tận trên cùng để coi cái đó là biến gì…ôi, mệt lắm. Nhưng đôi khi cứ hai ba dòng lại int, int, int thì nhìn cũng rối ^_^. Vòng lặp for Trong C++, bạn sẽ thường gặp vòng lặp for với cách định nghĩa như sau: for(int j = 0; j bạn không chủ động bỏ bớt đi được nếu không cần nó nữa, nó chỉ bị xóa bỏ khi chương trình kết thúc, vì thế chương trình của bạn nên có bộ nhớ tĩnh càng nhỏ càng tốt, chỉ khai báo trong bộ nhớ tĩnh những cái cần thiết." Ghi chú : đối với nhiều người "vùng memory dùng để cấp phát động" đều được gọi là heap, nghĩa là vùng "Free Store" + "Heap" đều được gọi chung là heap . Điều đó cũng chấp nhận được. Còn ở đây chúng ta tách biệt "Free Store" và Heap để chỉ rõ một vùng  dùng new/delete còn vùng kia dủng malloc/free . Với  2 vùng "Free Store" + "Heap", "bạn cấp phát nó khi chương trình đang chạy, có thể loại bỏ nó khi cần, cho phép bạn chủ động quản lý bộ nhớ, nhưng có nhược điểm là nếu bạn "quên" không loại bỏ nó sẽ có nguy cơ rò rỉ bộ nhớ và thậm chí ảnh hưởng đến toàn bộ hệ thống." Stack với hàm tạo và hàm hủy Sau đây là danh sách liên kết (bên trong Stack) với hàm tạo và hàm hủy cho thấy việc tạo và hủy gọn gàng với new và delete. Dưới đây là file Header: //: C06:Stack3.h // With constructors/destructors #ifndef STACK3_H #define STACK3_H  class Stack { struct Link { 	void* data; 	Link* next; 	Link(void* dat, Link* nxt); 	~Link(); }* head; public: 	Stack(); 	~Stack(); 	void push(void* dat); 	void* peek(); 	void* pop(); }; #endif // STACK3_H ///:~ Stack với hàm tạo và hàm hủy Không chỉ có hàm tạo và hàm hủy mà còn đưa thêm vào struct Link: 	//: C06:Stack3.cpp {O} 	// Constructors/destructors 	#include "Stack3.h“ 	#include "../require.h“ 	using namespace std;  	Stack::Link::Link(void* dat, Link* nxt) { data = dat; next = nxt;}  	Stack::Link::~Link() { }  	Stack::Stack() { head = 0; }  	void Stack::push(void* dat) { head = new Link(dat,head);}  	void* Stack::peek() { 	 require(head != 0, "Stack empty"); 	 return head->data; 	}  	void* Stack::pop() { 	 if(head == 0) return 0; 	 void* result = head->data; 	 Link* oldHead = head; 	 	 head = head->next; 	 delete oldHead; 	 return result; 	}  	Stack::~Stack() { require(head == 0, "Stack not empty"); } ///:~   Hàm tạo Link::Link() đơn giản tạo ra con trỏ data và next, còn trong Stack::Push(), dòng: 	head = new Link(dat,head);  không chỉ cấp phát vùng nhớ và còn tạo ra con trỏ cho vùng nhớ đó Stack với hàm tạo và hàm hủy Bạn có thể thấy kì kì tại sao hàm hủy của Link lại không làm gì, nói chính xác – tại sao không delete con trỏ data? Có hai vấn đề ở đây. Trong Stack, bạn có thể không đúng khi delete con trỏ void nếu nó là đối tượng. Còn nữa, nếu hàm hủy Link xóa con trỏ data, pop() sẽ trả về là một con trỏ bị xóa, điều này chắc chắn lỗi. Cái này cần nói đến vấn đề kiểm soát của: Link và Stack,chỉ lưu trữ con trỏ, nhưng không có trách nhiệm xóa đi chúng. Vậy thì bạn cần phải biết trách nhiệm đó phải thuộc về ai. Ví dụ như, nếu bạn không pop() và delete tất cả con trỏ trên Stack, thì hàm hủy sẽ không tự động xóa các con trỏ trên Stack. Chú ý điều này sẽ dẫn đến vấn đề tràn bộ nhớ, vì vậy biết được ai có bổn phận hủy đối tượng sẽ tạo ra sự khác biết giữa một chương trình hoàn chỉnh và, chương trình…gà – Đó là tại sao Stack::Stack() báo lỗi nếu đối tượng Stack không trống khi hủy. Do việc cấp phát và hủy trong Link là ẩn bên trong Stack, phần đó thực hiện ẩn – bạn không thể thấy khi test chương trình. Mặc dù bạn có trách nhiệm xóa con trỏ trở lại bằng pop() //: C06:Stack3Test.cpp #include "Stack3.h“ #include "../require.h“ #include #include #include using namespace std;  int main(int argc, char* argv[]) { requireArgs(argc, 1); // File name is argument ifstream in(argv[1]); assure(in, argv[1]); Stack textlines; string line; // Read file and store lines in the stack: while(getline(in, line)) textlines.push(new string(line)); // Pop the lines from the stack and print them: string* s; while((s = (string*)textlines.pop()) != 0) { cout using namespace std;  class Z { int i, j; public: Z(int ii, int jj); void print(); };  Z::Z(int ii, int jj) { i = ii; j = jj;}  void Z::print() { cout << "i = " << i << ", j = " << j << endl;}  int main() { 	Z zz[] = { Z(1,2), Z(3,4), Z(5,6), Z(7,8) }; 	for(int i = 0; i < sizeof zz / sizeof *zz; i++) 	zz[i].print(); } ///:~ Chú ý rằng nó trông như hàm tạo được gọi rõ ràng đối với mỗi đối tượng Hàm tạo mặc định Hàm tạo mặc định là hàm tạo được gọi mà không cần tham số. Ví dụ nếu bạn lấy struct Y ở trên và sử dụng nó như thế này: Y y2[2] = { Y(1) };   Trình biên dịch sẽ báo rằng không tìm thấy hàm tạo mặc định. Đối tượng thứ hai trong mảng muốn được tạo không cần tham số, và lúc đó trình biên dịch sẽ tìm hàm tạo mặc định. Thật ra, bạn cỏ thể định nghĩa đơn giản một mảng đối tượng Y y3[7];   Trình biên dịch sẽ báo rằng bạn cần phải có một hàm tạo mặc định cho mỗi đối tượng trong mảng.   Vấn đề tương tự nếu bạn tạo một đối tượng như vầy:   Y y4;   Nhớ rằng, nếu bạn có một hàm tạo, trình biên dịch chắc chắn sẽ gọi hàm tạo đó, hãy cẩn thận trường hợp này. Hàm tạo mặc định sẽ quan trọng khi (và chỉ khi) không có hàm tạo trong cấu trúc (class hay struct), khi đó trình biên dịch sẽ tự động tạo một hàm tạo cho bạn. Vì thế, đoạn code sau vẫn chạy: //: C06:AutoDefaultConstructor.cpp/ / Automatically-generated default constructor  class V { int i; // private}; // No constructor int main() { V v, v2[10]; } ///:~   Tuy nhiên, nếu có một hàm tạo nào đó được định nghĩa, và không có hàm tạo mặc định, thì hiễn nhiên, trường hợp của lớp V ở trên sẽ gây lỗi Có thể bạn nghĩ trình biên dịch tạo ra hàm tạo, nên nó sẽ khởi tạo đối tượng thông minh hơn, vd như chĩnh vùng nhơ của tất cả đối tượng là không. Nhưng điều đó không có – trình biên dịch có thể cấp vùng nhờ nhìu hơn nhưng lập trình viên không thể kiểm soát được. Nếu bạn muốn vùng nhớ được tạo là không, bạn phải tự làm điều đó bằng cách viết ra một hàm tạo rõ ràng. Mặc dù trình biên dịch sẽ tạo hàm tạo mặc định cho bạn, thường thì hàm tạo mặc định hiếm khi đúng với những gì bạn muốn. Bạn nên sử dụng tính năng này như một mạng lưới an toàn, nhưng đừng nên lạm dụng nó. Nói chung, bạn nên định nghĩa rõ ràng hàm tạo chứ đừng nên để trình biên dịch tự động tạo hàm tạo nó cho bạn. Tóm tắt Các cơ chế trong C++ cung cấp cho bạn một gợi ý mạnh về việc tạo và hủy. Khi Stroustrup thiết kế C++, một trong những quan sát đầu tiên của anh về năng suất trong C là phần quan trọng trong vấn đề lập trình gây ra do khởi tạo biến không thích hợp. Những vấn đề này gây ra những lỗi khó phát hiện, và cũng tương tự cho việc hủy không đúng cách. Bởi vì hàm tạo và hàm hủy đảm bảo cho bạn tính đúng đắn trong việc tạo và hủy (trình biên dịch sẽ không cho phép bạn tạo và hủy đối tượng khi hàm tạo và hàm hủy được gọi không đúng), bạn sẽ có toàn quyền kiểm soát và an toàn hơn Khởi tạo tổng hợp bao gồm nhiều kiểu tương tự - điều này giúp bạn ngăn bạn phạm một số sai lầm trong tập hợp và làm cho code của bạn gọn gàng hơn Vấn đề an toàn trong C++ là một vấn đề lớn. Tạo và hủy là một trong những phần đó, bạn có thể tìm hiểu thêm về vấn đề này trong quyển Thinking in C++ 

File đính kèm:

  • pptxLập trình hướng đối tượng - Chương 6 Tạo và hủy.pptx