Slide Trình bày chương 6 trong cuốn Thinking in C++
Việc thiết kế lớp có thể bảo đảm cho sự tạo lập của mỗi đối tượng bởi cung cấp một hàm đặc biệt gọi là hàm tạo.
Nếu một lớp có một hàm tạo, bộ biên dịch sẽ tự động gọi nó tại thời điểm một đối tượng được tạo ra trước khi ta có bất kì thao tác nào trên đối tượng đó.
g như bất cứ hàm nào, hàm khởi tạo có thể có thông số cho phép ta xác định những giá trị ban đầu cho đối tượng . Cả hàm tạo và hàm hủy là 2 kiểu hàm khác thường: chúng không có giá trị trả về. Nó khác với trả về giá trị void , trong đó hàm không trả về gì cả nhưng bạn vẫn có lựa chọn để làm một điều gì đó khác. Còn hàm khởi tạo và hàm hủy cũng không trả về gì cả nhưng bạn không còn sự lựa chọn nào khác. 2. BẢO ĐẢM SỰ GIẢI PHÓNG BỘ NHỚ BẰNG HÀM HỦY Tại sao cần phải dùng hàm hủy ? Giống như hàm khởi tạo tên lớp được dùng cho tên hàm. Tuy nhiên hàm hủy được phân biệt bằng dấu ~. Đây là một khai báo cho hàm hủy : class Y { public: ~Y(); }; Hàm hủy được gọi tự động khi đối tượng ra khỏi phạm vi của nó. Bạn có thể thấy nơi mà hàm khởi tạo được gọi tại lúc xác định một đối tượng. Nhưng bằng chứng duy nhất cho việc gọi một hàm hủy là dấu ngoặc đóng phạm vi bao quanh đối tượng “ } “. Hàm hủy vẫn được gọi cả khi bạn dùng goto để nhảy ra khỏi phạm vi của nó. Kết quả xuất ra màn hình : before opening brace after Tree creation Tree height is 12 before closing brace inside Tree destructor Tree height is 16 after closing brace. 3. SỰ LOẠI BỎ CỦA KHỐI ĐỊNH NGHĨA Trong C bạn phải luôn xác định tất cả các biến tại vị trí bắt đầu của khối, sau dấu “{ “. Thực ra điều này không phải lúc nào cũng thuận tiện cho người lập trình vì mỗi lần ta cần thêm một biến mới ta phải quay trở lại vị trí bắt dầu của khối đó. Code sẽ dễ dàng đọc hơn khi sự xác định biến gần với vị trí mà nó được sử dụng. Nếu như một hàm khởi tạo tồn tại nó phải được gọi khi đối tượng được tạo ra. Tuy nhiên nếu hàm tạo mang một hay nhiều tham số khởi tạo. Làm cách nào ta có được những thông tin khởi tạo tại vị trí bắt đầu của một phạm vi? Tuy nhiên C++ bảo đảm khi một đối tượng được tạo nó sẽ được khởi tạo đồng thời, nên không có đối tượng nào không được khởi tạo khi chạy xung quanh hệ thống. C không quan tâm tới điều này nên C khuyến khích phải xác định các biến tại điểm bắt đầu của một khối trước khi ta có những thông tin khởi tạo cần thiết. Nói chung C++ không cho phép tạo một đối tượng trước khi ta có những thông tin khởi tạo. Vì vậy ngôn ngữ có lẽ không khả thi nếu ta xác định biến tại điểm bắt đầu của một phạm vi. Thực ra nó khuyến khích việc xác định một đối tượng gần với vị trí nó được dùng như có thể. Nên bạn có thể chờ tới khi bạn có đủ thông tin cho một biến trước khi xác định nó, nên ta có thể luôn xác định và khởi tạo cùng một lúc. for Loop(vòng lặp) Trong C++ ta sẽ luôn thấy biến đếm vòng lặp for được xác định ngay cạnh biểu thức for : for(int j = 0; j < 100; j++) { cout << "j = " << j << endl;} for(int i = 0; i < 100; i++) { cout << "i = " << i << endl; } Điều này tránh gây ra sự nhầm lẫn cho những người mới lập trình. storage allocation (điều phối sự lưu trữ): hàm khởi tạo sẽ không được gọi cho tới khi đối tượng được định nghĩa. Bộ biên dịch cũng kiểm tra để chắc rằng ta không đặt việc định nghĩa đối tượng ở nơi mà dòng xử lí bỏ qua nó, như là trong khai báo switch hoặc một nơi nào đó mà goto có thể nhảy băng qua. Dòng khai báo trong đoạn code dưới đây đưa ra cảnh báo hoặc lỗi: 4. STACK VỚI HÀM TẠO VÀ HÀM HỦY Thực hiện lại danh sách liên kết bên trong stack với hàm khởi tạo và hàm hủy cho thấy như thế nào hai hàm đó làm việc với new và delete. Dưới đây là header file : Không chỉ stack có hàm khởi tạo và hàm hủy mà cả struct Link cũng có. Hàm khởi tạo Link::Link đơn giản chỉ khởi tạo data và con trỏ next, nên trong Stack::Push dòng : head = new Link(dat,head); không chỉ phân phối một link mới mà còn khởi tạo con trỏ cho link đó. Tại sao hàm hủy của link lại không làm gì cả – cách cụ thể, tại sao nó không delete con trỏ data ? Điều này thỉnh thoảng ám chỉ một vấn đề của việc sở hữu: Link và stack chỉ chứa con trỏ ,nhưng nó không chịu trách nhiệm xóa chúng. nếu như bạn không pop() và delete tất cả những con trỏ trên stack,chúng sẽ không tự động xóa bởi hàm hủy. Đây có lẽ là điều khó chịu và dẫn tới sự rò rỉ bộ nhớ, nên cần biết cái gì chịu trách nhiệm việc hủy một đối tượng là một sự khác biệt giữa một chương trình thành công và một chương trình có lỗi. Đó là lí do tại sao stack::~stack in ra một thông báo lỗi nếu như đối tượng stack không rỗng khi hủy : Stack::~Stack() { require(head == 0, "Stack not empty");} * Dưới đây là file thi hành test3.cpp : 5. GỘP CHUNG SỰ KHỞI TẠO: Khái niệm này ám chỉ việc kết hợp nhiều loại dữ liệu với nhau, giống như struct và class. Một mảng là một sự gộp chung kiểu dữ liệu duy nhất. Khi ta tạo một đối tượng bằng việc gộp chung chúng lại, tất cả những gì ta phải làm là tạo ra một phép gán, và việc khởi tạo sẽ được chú ý bởi trình biên dịch. Tính chất của nó phụ thuộc vào kiểu của tập mà ta giải quyết ,nhưng trong tất cả trường hợp mà những thành phần này được gán phải được bao quanh bởi dấu ngoặc xoắn. int a[5] = { 1, 2, 3, 4, 5 }; Một cách ngắn gọn thứ hai cho mảng là “automatic counting”, trong đó bạn để cho trình biên dịch xác định kích thước của mảng dựa trên khối lượng của các khởi tạo: int c[] = { 1, 2, 3, 4 }; Bây giờ nếu như ta quyết định thêm một thành phần khác vào mảng, ta chỉ đơn giản thêm một khởi tạo khác. Nếu như ta thử đưa ra nhiều sự khởi tạo hơn những thành phần trong mảng, bộ biên dịch sẽ thông báo lỗi. Nhưng điều gì sẽ xảy ra nếu như ta đưa ra ít sự khởi tạo hơn. ví dụ như: int b[6] = {0}; Ở đây trình biên dịch sẽ dùng giá trị khởi tạo đầu tiên cho thành phần đầu tiên của mảng, và sau đó dùng zero cho tất cả những thành phần còn lại và không được khai báo. Biểu thức ở trên là một cách ngắn gọn để khởi tạo một mảng bằng zero mà không dùng hàm lập for mà không gây ra bất cứ lỗi nào. Nếu như ta có thể thuyết lập được code của mình sao cho mà nó cần phải được thay đổi trong một hoàn cảnh xấu nào đó. Ta có thể giảm được phát sinh lỗi trong việc sửa đổi. Nhưng bằng cách nào ta xác định được kích thước của mảng? Biểu thức: sizeof c / sizeof *c là một thủ thuật để không cần phải sửa đổi nếu như kích thước của mảng thay đổi: for(int i = 0; i < sizeof c / sizeof *c; i++) c[i]++; Bởi vì cấu trúc cũng là một khối tập hợp, nên chúng có thể được khởi tạo bằng cách tương tự. Bởi vì struct của C có tất cả các thành viên là kiểu public, nên nó có thể được gán trực tiếp: struct X { int i; float f; char c; }; X x1 = { 1, 2. 2, 'c' }; Nếu như ta có một mảng những đối tượng như vậy, ta có thể khởi tạo chúng bằng cách dùng một tập hợp lồng vào nhau của những dấu ngoặc móc cho mỗi đối tượng: X x2[3] = { {1, 1. 1, 'a'}, {2, 2. 2, 'b'} }; Ở đây, đối tượng thứ 3 được khởi tạo bằng zero. Trong ví dụ trên, những khởi tạo được gán trực tiếp cho những phần tử của một tập hợp, nhưng hàm khởi tạo là một cách của việc ép buộc sự khởi tạo xảy ra thông qua một giao diện chính thức. Ở đây hàm khởi tạo phải được gọi để diễn tả sự khởi tạo. Nếu như ta có một struct như dưới đây: struct Y { float f; int i; Y(int a);}; Ta phải chỉ ra hàm khởi tạo. Sự tiếp cận tốt nhất được trình bày dưới đây:Y y1[] = { Y(1), Y(2), Y(3) }; Ta có được 3 đối tượng và 3 hàm khởi tạo được gọi. Bất cứ khi nào ta có một hàm khởi tạo được gọi, cho dù nó là một struct với tất cả một thành viên public hay là class với kiểu dữ liệu thành viên private, tất cả sự khởi tạo phải thông qua hàm khởi tạo kể cả khi nó dùng sự khởi tạo gộp chung 6. SỰ KHỞI TẠO MẶC ĐỊNH Sự khởi tạo mặc định có thể được gọi mà không có tham số. Ví dụ nếu như ta lấy một struct Y được định nghĩa lúc trước và dùng nó trong một sự xác định như sau : Y y2[2] = { Y(1) }; Trình biên dịch sẽ phàn nàn nó không tìm thấy một hàm khởi tạo mặc định. Đối tượng thứ hai trong mảng muốn được tạo mà không có tham số, và đó là lúc mà trình biên dịch dùng tới hàm khởi tạo. Vấn đề tương tự xảy ra nếu như ta tạo ra một đối tượng như sau :Y y4; Hàm khởi tạo mặc định thì quá quan trọng, nên nếu ( và chỉ nếu ) không có hàm khởi tạo trong một cấu trúc ( struct hay class ), trình biên dịch sẽ tự động tạo một cái cho bạn. Nếu bất kì hàm khởi tạo nào được xác định, tuy nhiên, và không có hàm khởi tạo mặc định, thì thể hiện của V ở trên sẽ sinh ra lỗi biên dịch. Bạn có thể nghĩ trình biên dịch – hàm khởi tạo tổng hợp nên làm một vài sự khởi tạo thông minh, như thiết lập tất cả giá trị của đối tượng là zero. Nhưng không phải vậy – nó sẽ vượt ra khỏi sự điều khiển của người lập trình. Nếu như ta muốn bộ nhớ được khởi tạo tới zero, ta phải tự làm điều đó bằng cách viết hàm khởi tạo mặc định. Dầu cho trình biên dịch tạo một hàm khởi tạo mặc định cho bạn, hành vi của trình biên dịch – hàm khởi tạo tổng hợp hiếm khi được như bạn muốn. Nói chung, ta nên xác định hàm khởi tạo một cách rõ ràng và không cho phép trình biên dịch làm nó cho ta.
File đính kèm:
- Slide Trình bày chương 6 trong cuốn Thinking in C++.ppt