Lập trình hướng đối tượng - Chương 6: Tạo và hủy
Như ta đã biết, trong C++ đã có sự cải thiện đáng kểhơn C đó là có thể gợp các kiểu dữ
liệu lại thành một cấu trúc, đó là lớp, một loại kiểu dữ liệu quan trọng điển hình trong C++.
Trong phần này chúng ta sẽ xem xét kĩ hơn về những vấn đề bên trong một lớp. Ở trong
phần trước đã đề cập đến việc xét quyền hạn cho lập trình người dùng (client programmer),
những gì người dùng được phép truy cập sử dụng và những gì bị hạn chế. ðiều này có nghĩa
là nội bộ cơ chế bên trong do người thiết kế lớp thiết lập.
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ịchcó 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.
h stringStash(sizeof(char) * bufsize); ifstream in("Stash2Test.cpp"); assure(in, " Stash2Test.cpp"); string line; while(getline(in, line)) stringStash.add((char*)line.c_str()); int k = 0; char* cp; while((cp = (char*)stringStash.fetch(k++))!=0) cout << "stringStash.fetch(" << k << ") = " << cp << endl; } ///:~ Bạn cũng chú ý rằng hàm cleanup() ñã bị loại bỏ, nhưng hàm hủy vẫn ñược gọi khi intStash và stringStash thoát khỏi vùng hoạt ñộ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 ///:~ 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ớ ñó 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 //{L} Stack3 //{T} Stack3Test.cpp // Constructors/destructors #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 << *s << endl; delete s; } } ///:~ Trong trường hợp này, tất cả những dòng trong textline ñược ñưa ra và xóa, nhưng nếu không, bạn sẽ nhận ñược từ hàm require() lời thông báo có nghĩa rằng…tràn bộ nhớ. Khởi tạo tập hợp (Aggrerate Initialization) Một tập hợp, là một ñống cái gì ñó gợp chung với nhau. ðịnh nghĩa này bao gồm nhiều kiểu gộp vào nhau (như struct và class). Mảng là tập họp của một kiểu. Khởi tạo tập hợp có thể dễ bị lỗi và dài dòng. Khởi tạo tập họp trong C++ sẽ an toàn hơn. Khi bạn khởi tạo một ñối tượng là một tập hợp, tất cả việc bạn cần phải làm chỉ là gán giá trị cho nó, còn việc tạo nó thì trình biên dịch ñã làm. Ví dụ tạo một mảng hoàn toàn ñơn giản int a[5] = { 1, 2, 3, 4, 5 }; Nếu bạn thử thêm vào nhiều giá trị hơn mảng cho phép, trình biên dịch sẽ báo lỗi, còn nếu ít hơn thì không vấn ñề gì. Ví dụ: int b[6] = {3}; Trong trường hợp này, trình biên dịch sẽ nhận giá trị ñầu tiên cho phần tử ñầu tiên của mảng, cụ thể trong trường hợp này là 3 cho phần từ b[0], các phần tử còn lại ñều nhận giá trị 0. Chú ý trường hợp này chỉ xảy ra nếu khi khai báo mảng có ít nhất một phần tử, còn nếu bạn khai báo một mảng không có giá trị nào như. Vì vậy, cách trên là một cách gọn ñể tạo một mảng toàn giá trị không mà không cần dùng vòng lặp nào, và không thể có một lỗi nào xảy ra. Một cách khác ñể mảng tự ñộng ñếm số phần tử, với cách này, trình biên dịch sẽ xác ñịnh ñộ lớn của mảng: int c[] = { 1, 2, 3, 4 }; Bây giờ nếu bạn muốn thêm một thuộc tính cho mảng, bạn chỉ cần thêm một khai báo. Nếu bạn cần ñiều chỉnh code ở chỗ nào ñó, phần trăm bị lỗi của bạn sẽ giảm ñi. Nhưng làm sao ñể xác ñịnh số phần tử của mảng. Bạn hãy sử dụng toán tử sizeof c/sizeof*c, dù ñộ lớn của mảng có thay ñổi bạn vẫn có thể xác ñịnh ñúng số phần tử for(int i = 0; i < sizeof c / sizeof *c; i++) c[i]++; Kiểu cấu trúc cũng là tập họp, trong C kiểu struct, tất cả ñiều ñược khai báo dưới dạng public: struct X { int i; float f; char c; }; X x1 = { 1, 2.2, 'c' }; Nếu bạn có một mảng ñối tượng, bạn có thể khai báo gộp bằng cách sử dụng dấu ngoặc nhọn cho mỗi ñối tượng X x2[3] = { {1, 1.1, 'a'}, {2, 2.2, 'b'} }; ở ñây, ñối tượng thứ ba ñược tạo là 0 Nếu kiểu là private, hoặc là public nhưng nằm trong hàm tạo, mọi thứ ñều khác hẳn. Trong ví dụ trên, người lập trình ñã gán trực tiếp cho tập họp, nhưng hàm tạo vẫn thực hiện bình thường. Ý là, hàm tạo luôn ñược gọi trong việc khởi tạo. Vì vậy nếu bạn có struct như vầy struct Y { float f; int i; Y(int a); }; Bạn phải chỉ ra việc gọi hàm tạo. Cách tốt nhất là làm rõ ràng như sau: Y y1[] = { Y(1), Y(2), Y(3) }; Bạn tạo ba ñối tượng và ba hàm tạo ñược gọi. Dù cho bạn có hàm tạo hay không, dù struct với kiểu public hay class với kiểu private, khi ñối tượng ñược tạo ra ñều phải thông qua hàm tạo, khởi tạo tập họp cũng không ngoại lệ ðây là ví dụ về hàm tạo có tham số: //: C06:Multiarg.cpp // Multiple constructor arguments // with aggregate initialization #include 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:
- Lập trình hướng đối tượng - Chương 6 Tạo và hủy.pdf