Chuyển tới nội dung chính

Hàm khởi tạo và hàm hủy trong C++

Hàm khởi tạo (constructor) chạy tự động khi tạo đối tượng, dùng để gán trạng thái ban đầu. Hàm hủy (destructor) chạy tự động khi hủy đối tượng, dùng để dọn tài nguyên. Đây là nền của RAII (tài nguyên gắn với vòng đời đối tượng).

Đặt vấn đề

Không có hàm khởi tạo, mỗi đối tượng mới sinh ra là một quả bom hẹn giờ. Bạn khai báo TaiKhoan tk; và các thuộc tính kiểu cơ bản của nó chứa rác: bất cứ giá trị nào còn sót trong vùng nhớ đó từ lần dùng trước. Số dư có thể là hai tỉ, có thể là âm. Bạn phải nhớ gọi tay một hàm khoiTao() ở mọi nơi tạo đối tượng, và chỉ cần quên một chỗ là hỏng.

Ở đầu kia của vòng đời, vấn đề còn tệ hơn. Giả sử đối tượng mở một tệp, cấp phát một mảng động, hay giữ một kết nối mạng. Ai đóng tệp? Ai gọi delete? Nếu câu trả lời là "lập trình viên phải nhớ gọi donDep()", bạn đã thua từ đầu: người ta sẽ quên, và ngay cả người không quên cũng thua khi hàm thoát sớm bằng return hoặc khi một ngoại lệ nhảy qua đoạn dọn dẹp.

C++ giải quyết cả hai đầu bằng một ý tưởng: gắn việc chuẩn bị và việc dọn dẹp vào vòng đời của đối tượng. Hàm khởi tạo chạy tự động lúc đối tượng sinh ra, hàm hủy chạy tự động lúc nó chết, và trình biên dịch bảo đảm hàm hủy chạy trong mọi đường thoát, kể cả khi có ngoại lệ. Bạn không cần nhớ gì cả. Đó là RAII.

Ý tưởng cốt lõi

  • Constructor File(string t) đảm bảo đối tượng sinh ra là đã hợp lệ.
  • Destructor ~File() chạy khi đối tượng ra khỏi phạm vi (hoặc bị delete).
  • Thứ tự hủy ngược thứ tự tạo (vào sau ra trước).

Mã nguồn

#include <iostream>
#include <string>
using namespace std;

class File {
public:
string ten;
File(string t) : ten(t) { cout << "Mo " << ten << "\n"; } // khởi tạo
~File() { cout << "Dong " << ten << "\n"; } // hủy
};

int main() {
File a("data.txt");
{
File b("log.txt"); // sống trong khối {}
} // b ra khỏi khối -> hủy
cout << "...\n";
} // a ra khỏi main -> hủy

Thử ngay: xem khởi tạo và hủy chạy từng bước

Vòng đời đối tượng: tạo và hủy C++
1class File {
2public:
3 string ten;
4 File(string t) : ten(t) { cout << "Mo " << ten << "\n"; }
5 ~File() { cout << "Dong " << ten << "\n"; }
6};
7
8int main() {
9 File a("data.txt");
10 {
11 File b("log.txt");
12 }
13 cout << "...\n";
14}
File a("data.txt")File::File()
Ngăn xếp stack
main()
#1File
ten"data.txt"
Bộ nhớ động heap
(trống)
Tạo a: hàm KHỞI TẠO File(string) chạy ngay, gán ten = "data.txt" và in "Mo data.txt".
Kết quảMo data.txt
1/5

Các loại hàm khởi tạo

Một lớp có thể có nhiều hàm khởi tạo, phân biệt nhau bằng danh sách tham số (nạp chồng). Ba loại hay gặp nhất:

  • Mặc định: không tham số, Diem(). Dùng khi tạo Diem d; hay khi tạo mảng đối tượng.
  • Có tham số: Diem(int x, int y). Dùng khi biết trước giá trị ban đầu.
  • Sao chép (copy constructor): Diem(const Diem& khac). Chạy khi tạo một đối tượng mới từ một đối tượng đã có, gồm cả lúc truyền theo giá trị và lúc trả về theo giá trị.
#include <iostream>
using namespace std;

class Diem {
public:
int x, y;
Diem() : x(0), y(0) { cout << "ctor mac dinh\n"; } // default
Diem(int a, int b) : x(a), y(b) { cout << "ctor tham so\n"; } // parameterized
Diem(const Diem& k) : x(k.x), y(k.y) { cout << "ctor sao chep\n"; } // copy
};

int main() {
Diem a; // "ctor mac dinh"
Diem b(3, 4); // "ctor tham so"
Diem c = b; // "ctor sao chep": c is built from b, this is NOT an assignment
cout << c.x << "," << c.y << "\n"; // 3,4
}

Chú ý dòng Diem c = b;. Dấu bằng ở đây gây hiểu lầm: đó không phải phép gán mà là khởi tạo, nên hàm khởi tạo sao chép chạy chứ không phải toán tử gán.

Viết một hàm khởi tạo là mất hàm khởi tạo mặc định

Nếu lớp không có hàm khởi tạo nào, trình biên dịch tự sinh cho bạn một hàm mặc định rỗng. Nhưng chỉ cần bạn tự viết một hàm khởi tạo có tham số, hàm mặc định đó biến mất. Từ đó Diem d; không còn biên dịch được, và mảng Diem ds[10]; cũng hỏng theo. Nếu vẫn cần nó, hãy viết thêm hoặc khai báo Diem() = default;.

Danh sách khởi tạo

Hai cách viết dưới đây trông giống nhau nhưng chạy khác nhau:

Sach(string t) { tua = t; } // (1) tua is default-built first, then ASSIGNED
Sach(string t) : tua(t) { } // (2) tua is BUILT directly from t

Cách (1) làm hai việc: trước khi thân hàm chạy, tua đã được xây bằng hàm khởi tạo mặc định của string (thành chuỗi rỗng), rồi thân hàm mới gán đè lên. Cách (2) xây thẳng tua từ t, bỏ hẳn bước thừa.

Với thành viên kiểu int hay double, khác biệt này gần như bằng không, nên nhiều người tưởng hai cách như nhau. Nhưng với thành viên là string, vector, hay bất cứ lớp nào có hàm khởi tạo và hàm hủy thật, cách (1) trả giá gấp đôi: một lần dựng đối tượng rỗng vô ích, rồi một lần gán đè để vứt luôn cái vừa dựng. Khi lớp có nhiều thành viên nặng và hàm khởi tạo bị gọi trong vòng lặp, khoản lãng phí đó cộng dồn rất nhanh. Đó là lý do danh sách khởi tạo được xem là mặc định nên theo, còn gán trong thân hàm là ngoại lệ chỉ dùng khi thật sự cần tính toán gì đó trước khi đặt giá trị.

Quan trọng hơn, có ba trường hợp bắt buộc dùng danh sách khởi tạo vì phép gán không thể thay thế:

  • Thành viên là hằng: const int ma;. Hằng chỉ được đặt giá trị đúng một lần lúc sinh ra.
  • Thành viên là tham chiếu: Lop& lop;. Tham chiếu phải gắn với đối tượng ngay lúc ra đời và không thể gắn lại.
  • Thành viên là đối tượng của lớp không có hàm khởi tạo mặc định. Không có gì để xây nó trước, nên buộc phải xây thẳng từ danh sách.
#include <iostream>
#include <string>
using namespace std;

class Lop {
public:
string ten;
Lop(const string& t) : ten(t) {} // no default constructor here
};

class SinhVien {
private:
const int maSo; // const: must be set in the init list
Lop& lop; // reference: must be bound in the init list
string ten;
public:
SinhVien(int ma, Lop& l, const string& t)
: maSo(ma), lop(l), ten(t) {} // the only legal way to set these three
void in() const {
cout << maSo << " " << ten << " lop " << lop.ten << "\n";
}
};

int main() {
Lop k24("K24-CNTT");
SinhVien sv(1001, k24, "An");
sv.in(); // 1001 An lop K24-CNTT
}
Thành viên khởi tạo theo thứ tự khai báo, không theo thứ tự trong danh sách
class Mang {
int* p; // declared FIRST, so it is initialized FIRST
int n; // declared second
public:
Mang(int soPhanTu) : n(soPhanTu), p(new int[n]) { } // BUG: n is still garbage here
};

Đọc danh sách khởi tạo, ta tưởng n được đặt bằng soPhanTu trước, rồi mới cấp phát new int[n]. Sai. Thứ tự trong danh sách không quyết định gì cả: thành viên luôn được khởi tạo theo thứ tự khai báo trong lớp. Ở đây p khai báo trước nên new int[n] chạy trước, lúc đó n còn là rác, và bạn cấp phát một mảng với kích thước ngẫu nhiên.

Cách tránh: luôn viết danh sách khởi tạo theo đúng thứ tự khai báo thành viên, và bật cảnh báo -Wreorder (có sẵn trong -Wall) để trình biên dịch nhắc bạn khi hai thứ tự lệch nhau.

Hàm hủy và thứ tự gọi

Hàm hủy có tên là dấu ngã cộng tên lớp, ~File(). Nó không có tham số, không có kiểu trả về, và mỗi lớp chỉ có đúng một hàm hủy (không nạp chồng được). Nó chạy khi:

  • đối tượng cục bộ ra khỏi phạm vi,
  • đối tượng cấp phát động bị delete,
  • một ngoại lệ được ném và ngăn xếp được tháo gỡ,
  • đối tượng thành viên bị hủy cùng đối tượng chứa nó.

Thứ tự đầy đủ của một vòng đời:

  1. Cấp phát bộ nhớ cho đối tượng.
  2. Các thành viên được khởi tạo, theo thứ tự khai báo trong lớp.
  3. Thân hàm khởi tạo chạy.
  4. Đối tượng sống và làm việc.
  5. Thân hàm hủy chạy.
  6. Các thành viên bị hủy, theo thứ tự ngược với thứ tự khai báo.

Nói ngắn gọn: xây từ trong ra, dỡ từ ngoài vào. Giữa các đối tượng cùng phạm vi thì cũng vậy, cái tạo sau bị hủy trước.

Xem chạy từng bước: thứ tự dựng và hủy theo chuỗi kế thừa

Bản trace ở đầu bài cho thấy thứ tự dựng và hủy giữa các đối tượng cùng phạm vi. Bản trace sau đây đi vào một đối tượng duy nhất nhưng có kế thừa: hàm dựng chạy theo chuỗi cha trước con, còn hàm hủy chạy ngược lại con trước cha. Vì đối tượng lớp con bị delete qua con trỏ lớp cha nên hủy tử lớp cơ sở phải là virtual.

Dựng cha rồi con, hủy con rồi cha C++
1#include <iostream>
2using namespace std;
3struct A {
4 A() { cout << "A()"; }
5 virtual ~A() { cout << "~A()"; }
6};
7struct B : A {
8 B() { cout << "B()"; }
9 ~B() override { cout << "~B()"; }
10};
11int main() {
12 A* p = new B();
13 delete p;
14}
new B()A::A() chạy trước
Ngăn xếp stack
main()
Bộ nhớ động heap
#1Btĩnh: A
new B() dựng phần LỚP CHA trước: A::A() chạy, in "A()". Lúc này phần riêng của B chưa được dựng.
Kết quảA()
1/6

Khi nào cần tự viết hàm hủy

Đây là câu hỏi thực tế nhất của bài, và câu trả lời gọn hơn nhiều sinh viên tưởng: chỉ khi lớp của bạn sở hữu một tài nguyên thô mà không ai khác dọn hộ. Tài nguyên thô là bộ nhớ cấp bằng new, tệp mở bằng fopen, kết nối mạng, khóa, cửa sổ đồ họa.

Nếu lớp chỉ chứa int, string, vector, hay các đối tượng đã tự lo phần dọn dẹp, thì đừng viết hàm hủy. Hàm hủy do trình biên dịch sinh ra vốn đã gọi hàm hủy của từng thành viên, và string với vector tự giải phóng bộ nhớ của chúng.

Lớp dưới đây sở hữu một mảng động, nên nó cần hàm hủy thật.

#include <iostream>
using namespace std;

class MangDong {
private:
int* du_lieu; // owned raw resource
int n;
public:
MangDong(int soPhanTu) : du_lieu(new int[soPhanTu]), n(soPhanTu) {
for (int i = 0; i < n; ++i) du_lieu[i] = 0;
cout << "Cap phat " << n << " so\n";
}
~MangDong() { // destructor: release what the constructor acquired
delete[] du_lieu; // use delete[] for new[], not plain delete
cout << "Giai phong " << n << " so\n";
}
void dat(int i, int v) { if (i >= 0 && i < n) du_lieu[i] = v; }
int lay(int i) const { return (i >= 0 && i < n) ? du_lieu[i] : 0; }
};

int main() {
{
MangDong m(5); // "Cap phat 5 so"
m.dat(2, 42);
cout << m.lay(2) << "\n"; // 42
} // leaving the block: "Giai phong 5 so" runs automatically
cout << "Ra khoi khoi\n";
}
Sở hữu con trỏ thô thì phải lo cả sao chép

Lớp MangDong ở trên còn một cái bẫy: nếu ai đó viết MangDong m2 = m1;, hàm khởi tạo sao chép mặc định sẽ chép giá trị con trỏ, khiến hai đối tượng cùng trỏ vào một mảng. Khi cả hai bị hủy, delete[] chạy hai lần trên cùng vùng nhớ và chương trình sập. Quy tắc kinh nghiệm (rule of three): lớp nào cần hàm hủy tự viết thì gần như chắc chắn cũng cần hàm khởi tạo sao chép và toán tử gán tự viết. Cách tránh toàn bộ rắc rối này: dùng std::vector hoặc std::unique_ptr thay cho con trỏ thô, khi đó bạn không cần viết hàm hủy nào cả.

Vì sao hàm hủy lớp cơ sở nên là ảo

Có một tình huống mà thứ tự hủy tưởng như đã rõ lại đổ vỡ hoàn toàn: khi bạn xóa một đối tượng lớp con thông qua một con trỏ kiểu lớp cơ sở. Đây là cách viết cực kỳ thường gặp trong đa hình, ta giữ mọi đối tượng trong một Co* rồi delete chúng theo cùng một lối. Nhưng nếu hàm hủy của lớp cơ sở không ảo, delete p chỉ nhìn vào kiểu tĩnh của p, gọi đúng hàm hủy của lớp cơ sở và bỏ qua hàm hủy lớp con. Phần tài nguyên mà lớp con cấp phát không được dọn, và ta có một rò rỉ ngay giữa mã trông rất sạch sẽ.

#include <iostream>
using namespace std;

struct Co {
// ~Co() { cout << "huy Co\n"; } // non-virtual: ~Con() would be skipped
virtual ~Co() { cout << "huy Co\n"; } // virtual: dispatches to the real type
};
struct Con : Co {
int* tai_nguyen;
Con() : tai_nguyen(new int[100]) {}
~Con() override { delete[] tai_nguyen; cout << "huy Con\n"; }
};

int main() {
Co* p = new Con(); // a base pointer holding a derived object
delete p; // with virtual ~Co: prints "huy Con" then "huy Co"
}

Với hàm hủy ảo, delete p tra bảng phương thức ảo, thấy kiểu động là Con, nên chạy ~Con() trước (giải phóng mảng động), rồi mới tới ~Co(). Đúng quy tắc dỡ từ ngoài vào. Nếu gỡ chữ virtual đi, chỉ ~Co() chạy, mảng một trăm phần tử rò rỉ, và tệ hơn, theo chuẩn C++ việc delete một đối tượng lớp con qua con trỏ cơ sở không có hàm hủy ảo là hành vi không xác định, nghĩa là trình biên dịch được phép làm bất cứ điều gì.

Lớp nào định làm cơ sở đa hình thì phải có hàm hủy ảo

Gốc rễ của lỗi này là sự lệch pha giữa kiểu tĩnh và kiểu động: delete qua con trỏ cơ sở chỉ biết kiểu tĩnh, còn đối tượng thật lại thuộc lớp con. Chỉ hàm hủy ảo mới bắc được cây cầu đó. Quy tắc thực hành: hễ một lớp có bất kỳ hàm ảo nào, hoặc bạn định cho người khác kế thừa và xóa qua con trỏ cơ sở, hãy khai báo hàm hủy của nó là virtual. Ngược lại, một lớp không bao giờ dùng làm cơ sở thì không cần hàm hủy ảo, vì bảng phương thức ảo cũng tốn thêm bộ nhớ cho mỗi đối tượng.

Tóm tắt

  • Hàm khởi tạo bảo đảm đối tượng sinh ra là đã hợp lệ. Có mặc định, có tham số, và sao chép.
  • Tự viết một hàm khởi tạo thì hàm mặc định biến mất, trừ khi khai báo = default.
  • Ưu tiên danh sách khởi tạo. Bắt buộc dùng nó với thành viên const, tham chiếu, và lớp không có hàm khởi tạo mặc định.
  • Thành viên khởi tạo theo thứ tự khai báo, hủy theo thứ tự ngược lại.
  • Hàm hủy chạy tự động ở mọi đường thoát. Đó là điều làm RAII an toàn hơn việc gọi tay.
  • Chỉ tự viết hàm hủy khi lớp sở hữu tài nguyên thô. Có vector hay string lo giúp thì đừng viết.

Bài tập thực hành

Bài tập 1: tham số mặc định cho hàm khởi tạo

Viết lớp TaiKhoan (tài khoản) có hai trường: ten (string) và soDu (int). Thêm hàm khởi tạo nhận tensoDu, trong đó soDu có giá trị mặc định là 0. Khi tạo đối tượng, in ra dòng Mo tai khoan <ten> voi so du <soDu>. Hãy tạo một tài khoản chỉ truyền tên và một tài khoản truyền cả hai tham số, rồi quan sát kết quả.

Lời giải chi tiết

Tham số mặc định cho phép gọi hàm khởi tạo mà không cần truyền đủ mọi đối số. Khi khai báo TaiKhoan(string t, int s = 0), trình biên dịch sẽ tự điền 0 cho soDu nếu lời gọi bỏ trống tham số này. Lưu ý chỉ đặt giá trị mặc định ở khai báo (trong khai báo lớp), không lặp lại nó ở phần định nghĩa để tránh lỗi định nghĩa hai lần.

Trong main, lời gọi TaiKhoan a("An") dùng giá trị mặc định nên soDu bằng 0, còn TaiKhoan b("Binh", 500) truyền đủ nên soDu bằng 500.

#include <iostream>
#include <string>
using namespace std;

class TaiKhoan {
public:
string ten;
int soDu;
// Default argument 0 for soDu, declared only here
TaiKhoan(string t, int s = 0) : ten(t), soDu(s) {
cout << "Mo tai khoan " << ten << " voi so du " << soDu << "\n";
}
};

int main() {
TaiKhoan a("An"); // soDu uses default value 0
TaiKhoan b("Binh", 500); // soDu is 500
}

Ý chính: tham số mặc định gắn ở khai báo hàm khởi tạo giúp một hàm khởi tạo phục vụ nhiều cách gọi khác nhau.

Bài tập 2: đếm thứ tự hủy

Viết lớp Buoc (bước) lưu một số nguyên thuTu. Hàm khởi tạo in Tao buoc <thuTu>, hàm hủy in Huy buoc <thuTu>. Trong main, tạo ba đối tượng Buoc mang số 1, 2, 3 theo đúng thứ tự đó trong cùng một phạm vi. Hãy dự đoán và giải thích thứ tự các dòng Huy được in ra.

Lời giải chi tiết

Các đối tượng cục bộ trong cùng một phạm vi bị hủy theo thứ tự ngược với thứ tự tạo (vào sau ra trước). Vì ta tạo lần lượt bước 1, 2, 3 nên khi kết thúc phạm vi, trình biên dịch hủy bước 3 trước, rồi bước 2, cuối cùng bước 1.

Do đó phần in Tao đi theo thứ tự 1, 2, 3, còn phần in Huy đi theo thứ tự 3, 2, 1.

#include <iostream>
using namespace std;

class Buoc {
public:
int thuTu;
Buoc(int n) : thuTu(n) { cout << "Tao buoc " << thuTu << "\n"; }
~Buoc() { cout << "Huy buoc " << thuTu << "\n"; }
};

int main() {
Buoc b1(1);
Buoc b2(2);
Buoc b3(3);
// Destruction order is reverse of creation: 3, 2, 1
}

Ý chính: trong một phạm vi, đối tượng tạo sau bị hủy trước, nên thứ tự hủy luôn ngược thứ tự tạo.

Bài tập 3: lớp Timer kiểu RAII

Viết lớp Timer thể hiện một tài nguyên theo phong cách RAII: nhận một tên nhan (string) trong hàm khởi tạo và in Bat dau <nhan>, còn hàm hủy in Ket thuc <nhan>. Mục tiêu là khi một Timer được đặt ở đầu một khối {}, nó tự động in dòng kết thúc ngay khi khối đóng lại, kể cả khi giữa khối có nhiều lệnh khác. Hãy viết main đặt một Timer bao quanh một đoạn công việc giả lập (vài dòng cout) trong một khối lồng, và một Timer khác ở phạm vi ngoài.

Lời giải chi tiết

Ý tưởng RAII là gắn việc mở và đóng một tài nguyên vào vòng đời của đối tượng: mở trong hàm khởi tạo, đóng trong hàm hủy. Nhờ vậy ta không cần nhớ gọi tay lệnh đóng, vì hàm hủy chạy tự động khi đối tượng ra khỏi phạm vi, kể cả khi thoát sớm.

Đặt Timer cong_viec("cong viec") ở đầu khối lồng. Mọi lệnh trong khối chạy xong thì dấu } đóng khối kích hoạt hàm hủy của cong_viec, in Ket thuc cong viec. Timer ngoài cùng (tong) hủy cuối cùng khi main kết thúc.

#include <iostream>
#include <string>
using namespace std;

class Timer {
public:
string nhan;
Timer(string n) : nhan(n) { cout << "Bat dau " << nhan << "\n"; }
~Timer() { cout << "Ket thuc " << nhan << "\n"; }
};

int main() {
Timer tong("tong"); // outer-scope timer
{
Timer cong_viec("cong viec"); // RAII guard for the block
cout << "Lam viec 1\n";
cout << "Lam viec 2\n";
} // cong_viec destroyed here -> prints "Ket thuc cong viec"
cout << "Da xong khoi trong\n";
} // tong destroyed here -> prints "Ket thuc tong"

Ý chính: đặt một đối tượng RAII ở đầu khối khiến phần dọn dẹp tự chạy đúng lúc khối kết thúc, giúp mã an toàn và gọn hơn.

Bài tập 4: thứ tự tạo và hủy của đối tượng thành viên

Viết lớp DongCo in Tao dong co trong hàm khởi tạo và Huy dong co trong hàm hủy. Viết lớp Banh tương tự với Tao banhHuy banh. Viết lớp Xe có hai thành viên theo đúng thứ tự khai báo: một DongCo rồi một Banh. Thân hàm khởi tạo của Xe in Tao xe, thân hàm hủy in Huy xe. Dự đoán chính xác sáu dòng được in ra khi tạo một Xe trong main, rồi giải thích.

Lời giải chi tiết

Áp dụng sáu bước của vòng đời. Khi tạo Xe, các thành viên được khởi tạo trước theo thứ tự khai báo (DongCo rồi Banh), sau đó mới tới thân hàm khởi tạo của Xe. Khi hủy, thân hàm hủy của Xe chạy trước, rồi các thành viên bị hủy theo thứ tự ngược lại (Banh rồi DongCo).

Vậy thứ tự in là: Tao dong co, Tao banh, Tao xe, Huy xe, Huy banh, Huy dong co. Cách nhớ: bạn lắp linh kiện trước rồi mới ráp thành xe, khi tháo thì gỡ xe ra trước rồi mới tháo linh kiện. Nhờ vậy thân hàm hủy của Xe vẫn dùng được các thành viên, vì chúng chưa bị hủy.

#include <iostream>
using namespace std;

class DongCo {
public:
DongCo() { cout << "Tao dong co\n"; }
~DongCo() { cout << "Huy dong co\n"; }
};

class Banh {
public:
Banh() { cout << "Tao banh\n"; }
~Banh() { cout << "Huy banh\n"; }
};

class Xe {
private:
DongCo dongCo; // declared first -> built first, destroyed last
Banh banh; // declared second -> built second, destroyed first
public:
Xe() { cout << "Tao xe\n"; } // body runs AFTER both members are built
~Xe() { cout << "Huy xe\n"; } // body runs BEFORE any member is destroyed
};

int main() {
Xe x;
// Tao dong co / Tao banh / Tao xe / Huy xe / Huy banh / Huy dong co
}

Ý chính: đối tượng thành viên xây trước thân hàm khởi tạo và hủy sau thân hàm hủy, nên trong cả hai thân hàm bạn luôn dùng được thành viên một cách an toàn.

Tự kiểm tra

Chương trình in ra theo thứ tự nào?
Mo data.txt
Mo log.txt
Dong log.txt
...
Dong data.txt

b sinh sau nhưng hủy trước (vì ra khỏi khối {} trước), a hủy cuối cùng khi kết thúc main.

Câu hỏi tự kiểm

Kiểm tra nhanh: khởi tạo và hủy0/4 đúngchưa trả lời
  1. 1Các đối tượng cục bộ trong cùng một phạm vi bị hủy theo thứ tự nào?
  2. 2Với các trường int* p; int n; hàm dựng viết : n(soPhanTu), p(new int[n]) có lỗi gì?
  3. 3Trường hợp nào BẮT BUỘC dùng danh sách khởi tạo, không thể gán trong thân hàm dựng?
  4. 4Khi nào nên tự viết hàm hủy?
Ghi nhớ

Constructor chạy khi tạo, destructor chạy khi hủy (ra khỏi phạm vi hoặc delete). Hủy theo thứ tự ngược tạo. Đây là cơ chế dọn tài nguyên an toàn của C++.