Hàm khởi tạo và hủy bỏ
Phần này không chỉ cung cấp một điểm thống
nhất đơn để vào hàm thư viện, nhưng nó cũng ẩn đi
tên của những hàm trong tên lớp.Trong chương 5 ,
quyền truy nhập sẽ được giới thiệu. Phần này cung
cấp cách thiết kế lớp để thiết lập rõ ràng ranh giới
cho sự định đoạt những người lập trình nào thì được
phép hoạt động và kết thúc phạm vi hoạt động của
mình. Nó có nghĩa là những cơ cáu nội bộ của sự
hoạt động một kiêủ dữ liệu thì dươí quyền điều khiển
và định đoạt của người thiết kế lớp, và sự rõ ràng của
nó đến nhưng người lập trình nào có thể và nên chú ý
tới.
hile(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; } ///:~ Và khi gọi hàm cleanup( ) sẽ loại bỏ giá trị vừa tạo,nhưng hàm hủy vẫn tiếp tục được gọi một cách tự động khi intStash và stringStash ra khỏi phạm vi hoạt động. Trong ví dụ Stask chúng ta thấy rõ:Chỉ sử dụng kiểu nội tại nếu không gọi hàm hủy,nếu cố tình sao chép lớp đối tượng vào Stask nó có thể thi hành không đúng.Thư viện chuẩn của C++ có thể sao chép đối tượng chính xác vào nơi chứa chúng, nhưng dây là một quá trình diễn ra rối rắm và phức tạp.Trong những ví dụ stack tiếp theo ta sẽ dùng con trỏ. STACK với hàm tạo và hàm hủy Trong stack với hàm tạo và hàm hủy sẽ làm việc một cách hiệu quả với new và delete. // 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ỉ stack dùng hàm tạo và hàm hủy mà cả lớp 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( ) khởi tạo con trỏ data và next vì vậy trong Stack::push( ) có khai báo : head = new Link(dat,head); Không chỉ cho phép tạo liên kết mới(tạo dối tượng với từ khóa new,đống thời nó còn khởi tạo một con trỏ cho liên kết này. Tại sao hàm hủy của Link không làm mọi thứ,tại sao nó không delete con trỏ data?Có 2 vấn đề.Stack là một con trỏ, chúng ta không thể delete một con trỏ không có giá trị trả về nếu nó không trỏ đến bất kì đối tượng nào.Nhưng ngoài ra,hàm hủy Link xóa con trỏ data, pop()sẽ không trả về con trỏ để xóa một đối tượng,điều này chắc chắn sẽ gây ra sự cố.Điều này thỉnh thoảng xem như vấn đề của quyền sở hữu: Link và Stack chỉ giữ những con trỏ,nhưng nó không chịu trách nhiệm phải hủy chúng.Điều này có nghĩa là bạn phải cẩn thận.Ví dụ,nếu bạn không pop() và delete tất cả những con trỏ được tạo ra trên Stack,chúng sẽ không tự động được giải phóng bởi hàm hủy của Stack.Điều này có gây ra vân đề và dẫn tới việc lãng phí bộ nhớ,vì vậy việc hiểu biết về giải phóng đối tượng có thể làm chương trình hoạt động tốt hay mắc phải những sai sót.Điều này cũng giải thích tại sao Stack::~Stack( )in thông báo lỗi nếu đối tượng Stack không được giải phóng trước hàm hủy. Bởi vì phân công và giải phóng của đốit tượng Link bị ẩn bên dưới Stack-nó là một bên dưới bộ thực thi-chúng ta không thể nhận biết được nó trong những chương trình,mặc dù ta chịu trách nhiệm giải phóng con trỏ: //: 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 đều đã được lấy ra và delete,nhưng nếu ngược lại,ta phải tạo một thông báo require( )rằng đó chính là lỗ hổng bộ nhớ. Khởi tạo khối tập hợp Một khối tập hợp thì giống như là một lượng nhóm lại với nhau.Định nghĩa này bao gồm cả khối tập hợp các kiểu nhóm chung với nhau, như là class và struct.Một mảng là một khối tập hợp nhưng cùng một kiểu dữ liệu. Khi ta tạo ra một đối tượng là một khối các tập hợp, người biên dịch phải khởi tạo chúng một cách cẩn thận.Điều này tùy thuộc vào khối tập hợp ta đang đề cập đến, nhưng trong mọi trường hợp những thành phần trong đó phải được bao bọc bởi dấu{...}.Ví dụ cho một mảng tĩnh: int a[5] = { 1, 2, 3, 4, 5 }; Nếu ta khởi tạo hơn số thành phần của mảng, trình biên dịch sẽ thông báo lỗi.Nếu khởi tạo ít hơn thì sao? Ví dụ: int b[6] = {0}; Ở đây, trình biên dịch sẽ gán giá trị đầu tiên cho phần tử đầu tiên của mảng,và gán 0 cho tất cả những phần tử mảng còn lại.Điều này sẽ không xảy ra nếu ta định rõ một mảng mà không khởi tạo.Vì vậy cách diễn đạt ở trên là cách ngắn gọn, để tạo ra một mảng các số 0. Điều lưu ý thứ hai về mảng là khả năng ”tự động đếm”, trong đó trình biên dịch sẽ xác định kích thước mảng dựa trên các số được khởi tạo: int c[] = { 1, 2, 3, 4 }; Nếu bạn muốn tăng số phần tử của mảng, hãy thêm giá trị khởi tạo khác. Nếu muốn tăng số câu lệnh, cần phải có một vài thay đổi, ta cần phải sửa lại một vài lỗi.Nhưng làm thế nào ta có thể xác định kích thước của mảng? Ta dùng sizeof c /sizeof *c(kích thước của mảng chia cho kích thước phần tử đầu tiên của mảng). for(int i = 0; i < sizeof c / sizeof *c; i++) c[i]++; Vì structures đồng thời là một khối tập hợp, chúng có thể được khởi tạo theo những cách tương tự.Vì một struct trong C có những thành viên là dạng public, nên chúng có thể được tạo một cách trực tiếp: struct X { int i; float f; char c; }; X x1 = { 1, 2.2, 'c' }; Nếu bạn có một mảng như các đối tượng, bạn có thể khởi tạo cho chúng bằng cách dùng: X x2[3] = { {1, 1.1, 'a'}, {2, 2.2, 'b'} }; Ở ví dụ trên, đối tượng thứ 3 sẽ được gán là 0. Nếu bất kì thành phần dữ liệu nào là private (một trường hợp đặc thù trong một lớp C++), hay là kiểu public, nhưng nó sẽ khác nếu ở đó là một hàm tạo.Ở ví dụ trên, khởi tạo một cách trực tiếp thành phần của khối đối tượng, nhưng hàm khởi tạo một cách khởi tạo theo một giao diện hình thức.Vì vậy nếu có một struct như sau: struct Y { float f; int i; Y(int a); }; Bạn phải chỉ rõ hàm tạo gọi.Cách tốt nhất là: Y y1[] = { Y(1), Y(2), Y(3) }; Bạn phải tạo ra ba đối tượng và lời gọi hàm tạo.Bất kể là một struct với thành phần public hay một class với thành phần dữ liệu private, tất cả những sự khởi tạo phải thông qua hàm tạo,nếu bạn đang sử dụng khởi tạo khối dữ liệu. Ở ví dụ thứ hai cho thấy những đối số của hàm khởi tạo: //: 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ó giống như hàm tạo được gọi cho mỗi đối tượng trong mảng. Hàm tạo mặc định Một hàm tạo mặc định là một hàm tạo được gọi mà không có đối số.Một hàm tạo mặc định được dùng để tạo ra một “ đối tượng vanilla”, nhưng nó đồng thời cũng quan trọng khi trình biên dịch tạo ra một đối tượng nhưng không đưa vào bất kì chi tiết nào.Ví dụ, nếu bạn có struct Y được định nghĩa trước và dùng nó: Y y2[2] = { Y(1) }; Trình biên dịch sẽ không thể tìm thấy một hàm tạo mặc định.Đối tượng thứ hai trong mảng muốn tạo ra sẽ không có đối số, và trình biên dịch sẽ tìm kiếm một hàm tạo mặc định cho nó.Thực ra, nếu bạn định nghĩa một cách đơn giản một mảng đối tượng Y, Y y3[7]; Sẽ có rắc rối bởi vì nó phải có hàm tạo mặc định để khởi tạo mỗi đối tượng trong mảng.Vấn đề tương tự sẽ xảy ra nếu bạn tạo ra một đối tượng riêng như là: Y y4; Hãy nhớ, nếu bạn có một hàm tạo, trình biên dịch sẽ luôn tạo giá trị dù trong bất kì tình huống nào. Hàm tạo mặc định rất quan trọng, nếu không có một hàm tạo cho một structure(struct hay class), trình biên dịch sẽ tự động tạo nó cho bạn. // Automatically-generated default constructor class V { int i; // private }; // No constructor int main() { V v, v2[10]; } ///:~ Nếu có một hàm tạo được định nghĩa, thì sẽ không có hàm tạo mặc định. Có thể bạn nghĩ rằng trình biên dịch hàm tạo làm việc khôn ngoan, cũng như vùng nhớ của đối tượng là 0.Nhưng không phải – điều đó sẽ tạo thêm chi phí hoạt động cho hệ thống nhưng không phải do lập trình viên tạo ra.Nếu bạn muốn bộ nhớ khởi tạo giá trị 0, bạn phải viết một hàm mặc định rõ ràng để thực hiện điều này. Mặc dù trình biên dịch sẽ tạo một hàm tạo mặc định cho bạn, nhưng bộ khởi tạo này thường không phải điều bạn muốn.Vì vậy bạn nên cẩn thận khi thực hiện nó.Nói chung, bạn nên định nghĩa hàm tạo một cách rõ ràng và không nên để trình biên dịch làm điều đó. SUMARRY C++ cung cấp cho ta cơ chế khá phức tạp nhưng mạnh mẽ về tầm quan trọng của việc khởi tạo và giải phóng.Một trong những vấn mà C++ cải tiến từ C chính là việc khởi tạo giá trị.Và một trong những lỗi khó phát hiện chính là việc giải phóng bộ nhớ không đúng cách.Vì hàm tạo và hàm hủy cho phép việc khởi tạo và giải phóng một cách thích hợp(trình biên dịch sẽ không cho phép một đối tượng được tạo và hủy nếu không gọi hàm tạo và hàm hủy thích hợp). Khởi tạo một khối tập hợp được xây dựng theo cách tương tự, nó giúp bạn tránh những lỗi khi xây dựng kiểu dữ liệu tập hợp và làm cho code ngắn gọn hơn. Việc tạo và hủy là một trong những phần quan trọng giúp cho chương trình thực hiện trơn tru hơn trong C++.
File đính kèm:
- Hàm khởi tạo và hủy bỏ.pdf