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

