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

