














Preview text:
lO M oARcPSD| 45467232
Bài số 1: Tổng quan lập trình hướng đối tượng
1. Trắc nghiệm (2 điểm)
a. Lợi ích chính của OOP là gì?
A. Tăng hiệu năng chương trình B. Giảm số dòng lệnh
C. Tăng tính mô-đun, bảo trì và tái sử dụng D. Không cần viết hàm
b. Câu lệnh nào dùng để tạo đối tượng từ lớp trong C++? A. class a = new A(); B. A a; C. A = new object(); D. object A = new A(); 2. Tự luận (3 điểm)
Trình bày đặc điểm của lập trình hướng đối tượng và so sánh với lập trình cấu trúc. 3. Lập trình (5 điểm)
Viết chương trình định nghĩa lớp SinhVien gồm: mã SV, tên SV, điểm KTLT. Nhập danh sách
sinh viên và in danh sách ra màn hình.
Bài số 2: Kế thừa và tính trừu tượng trong C++
1. Trắc nghiệm (2 điểm)
a. Trong kế thừa, từ khóa nào giúp lớp con kế thừa công khai lớp cha? A. inherit B. extends C. public D. private
b. Tính trừu tượng thể hiện rõ nhất qua đâu? A. Hàm thành viên tĩnh B. Hàm ảo
C. Lớp trừu tượng (abstract class) D. Giao diện 2. Tự luận (3 điểm)
Trình bày vai trò của tính trừu tượng trong thiết kế phần mềm. So sánh lớp trừu tượng và lớp giao diện. 3. Lập trình (5 điểm)
Xây dựng lớp trừu tượng NhanVien với hàm ảo tinhLuong(). Hai lớp con:
- NVCongNhat: lương = số ngày * 300k
- NVSanPham: lương = số sản phẩm * 200k
Viết chương trình chính quản lý danh sách nhân viên. lO M oARcPSD| 45467232
Bài số 3: Đa hình và hàm ảo
1. Trắc nghiệm (2 điểm)
a. Hàm ảo thuần túy được khai báo như thế nào? A. virtual void func(); B. void func() = 0; C. virtual void func() = 0; D. abstract func();
b. Đa hình động được thực hiện qua? A. Hàm main B. Hàm tạo
C. Hàm ảo và con trỏ lớp cha D. Kế thừa nhiều lớp 2. Tự luận (3 điểm)
Phân biệt đa hình tĩnh và đa hình động. Ưu nhược điểm của mỗi loại. 3. Lập trình (5 điểm)
Tạo lớp Cha có hàm ảo tinhTien(). Hai lớp con: - ThanhToanTienMat - ThanhToanChuyenKhoan
Ghi đè hàm tinhTien với chiết khấu khác nhau. Viết chương trình quản lý và gọi hàm bằng con trỏ lớp cha.
Bài số 4: Quản lý bộ nhớ và con trỏ trong OOP
1. Trắc nghiệm (2 điểm)
a. Trong C++, từ khóa nào được dùng để cấp phát động bộ nhớ? A. malloc B. alloc C. new D. pointer
b. Khi nào cần dùng destructor?
A. Khởi tạo đối tượng B. Gán đối tượng C. Giải phóng tài nguyên D. Sửa dữ liệu 2. Tự luận (3 điểm) lO M oARcPSD| 45467232
Giải thích sự khác biệt giữa constructor, destructor và khi nào dùng cấp phát động (dynamic allocation). 3. Lập trình (5 điểm)
Viết chương trình quản lý sinh viên với mảng cấp phát động. Lớp SinhVien gồm: tên, điểm TB.
Nhập N sinh viên và xuất danh sách.
Bài số 5: Mô hình hóa hệ thống bằng lớp và đối tượng 1. Trắc nghiệm (2 điểm) a. UML là gì?
A. Ngôn ngữ lập trình hướng đối tượng B. Công cụ phân tích dữ liệu
C. Ngôn ngữ mô hình hóa hướng đối tượng D. Một IDE
b. Quan hệ "has-a" trong OOP thể hiện qua? A. Kế thừa B. Giao diện
C. Thành phần lớp là đối tượng lớp khác D. Tính đa hình 2. Tự luận (3 điểm)
Trình bày các quan hệ giữa các lớp trong UML: kế thừa, kết hợp, tập hợp. 3. Lập trình (5 điểm)
Thiết kế hệ thống quản lý thư viện gồm các lớp: - Sach: mã sách, tên sách
- NhaXuatBan: tên NXB, năm thành lập
- Mỗi sách có 1 NXB → dùng quan hệ "has-a"
Nhập danh sách sách và in thông tin ra màn hình. Giải
Bài số 1: Tổng quan lập trình hướng đối tượng 1. Trắc nghiệm:
a. C → Tăng tính mô-đun, bảo trì và tái sử dụng b. B → A a; 2. Tự luận:
Đặc điểm của lập trình hướng đối tượng (OOP): Đóng gói (Encapsulation) Kế thừa (Inheritance) Đa hình (Polymorphism)
Trừu tượng hóa (Abstraction) lO M oARcPSD| 45467232 3. Lập trình: #include using namespace std; class SinhVien { private: string maSV, tenSV; float diemKTLT; public:
void nhap() { cout << "Ma SV: "; cin
>> maSV; cin.ignore(); cout <<
"Ten SV: "; getline(cin, tenSV); cout <<
"Diem KTLT: "; cin >> diemKTLT; }
void xuat() { cout << "Ma: " << maSV << ", Ten: " << tenSV << ", Diem: " << diemKTLT << endl; } }; int main() { int n;
cout << "Nhap so SV: "; cin >> n;
SinhVien* ds = new SinhVien[n]; for (int i =
0; i < n; i++) { cout << "\nNhap SV thu "
<< i + 1 << ":\n"; ds[i].nhap(); } lO M oARcPSD| 45467232
cout << "\nDanh sach sinh vien:\n";
for (int i = 0; i < n; i++) { ds[i].xuat(); } delete[] ds; return 0; }
Bài số 2: Kế thừa và tính trừu tượng 1. Trắc nghiệm: a. C → public
b. C → Lớp trừu tượng (abstract class) 2. Tự luận:
Vai trò của trừu tượng: Ẩn chi tiết cài đặt
Giúp thiết kế hệ thống linh hoạt, dễ mở rộng
-Đơn giản hóa độ phức tạp: Tính trừu tượng giúp ẩn đi các chi tiết triển khai phức tạp bên
trong, chỉ hiển thị những thông tin và chức năng cần thiết cho người sử dụng. Điều này giúp
người lập trình tập trung vào "cái gì" mà không cần quan tâm đến "như thế nào".
-Tăng tính mô-đun hóa: Bằng cách định nghĩa các giao diện trừu tượng, chúng ta có thể chia hệ
thống thành các module độc lập. Mỗi module có thể được phát triển và kiểm thử riêng biệt, sau
đó tích hợp lại với nhau.
-Dễ bảo trì và mở rộng: Khi các chi tiết triển khai được ẩn đi, việc thay đổi hoặc nâng cấp một
phần của hệ thống sẽ ít ảnh hưởng đến các phần khác, giúp giảm thiểu rủi ro và chi phí bảo trì.
Đồng thời, việc thêm các chức năng mới cũng dễ dàng hơn.
-Thúc đẩy tái sử dụng mã: Các lớp trừu tượng hoặc giao diện định nghĩa một khuôn mẫu
chung, cho phép các lớp con triển khai theo cách riêng của mình nhưng vẫn tuân thủ một cấu
trúc nhất định. Điều này thúc đẩy việc tái sử dụng mã và giảm sự trùng lặp.
-Tạo ra một kiến trúc rõ ràng: Tính trừu tượng giúp định nghĩa các tầng (layers) và thành phần
(components) trong kiến trúc phần mềm một cách rõ ràng, dễ hiểu và dễ quản lý. lO M oARcPSD| 45467232 3. Lập trình: #include using namespace std; class NhanVien { public:
virtual float tinhLuong() = 0; virtual void nhap() = 0;
virtual void xuat() = 0; virtual ~NhanVien() {} };
class NVCongNhat : public NhanVien { private: int soNgay; public: void nhap() {
cout << "Nhap so ngay lam: "; cin >> soNgay; } lO M oARcPSD| 45467232 float tinhLuong() override { return soNgay * 300000; }
void xuat() { cout << "NVCongNhat - Luong: " << tinhLuong() << endl; } };
class NVSanPham : public NhanVien { private: int soSP; public:
void nhap() { cout << "Nhap so san pham: "; cin >> soSP; } float tinhLuong() override { return soSP * 200000; }
void xuat() { cout << "NVSanPham - Luong: " << tinhLuong() << endl; } }; int main() { int n;
cout << "Nhap so NV: "; cin >> n; NhanVien* ds[100];
for (int i = 0; i < n; i++) { int loai;
cout << "\nNhan vien thu " << i + 1 << " (1-Cong Nhat, 2-San Pham): "; cin >> loai;
if (loai == 1) ds[i] = new NVCongNhat; else ds[i] = new NVSanPham; ds[i]- >nhap(); }
cout << "\nDanh sach nhan vien:\n";
for (int i = 0; i < n; i++) { ds[i]- >xuat(); delete ds[i]; } return 0; }
Bài số 3: Đa hình và hàm ảo 1. Trắc nghiệm:
a. C → virtual void func() = 0;
b. C → Hàm ảo và con trỏ lớp cha lO M oARcPSD| 45467232 2. Tự luận:
Phân biệt đa hình tĩnh và đa hình động. Ưu nhược điểm của mỗi loại.
Đa hình (Polymorphism) là một trong những trụ cột của OOP, cho phép một giao diện duy nhất
có thể được sử dụng cho nhiều kiểu dữ liệu khác nhau. Có hai loại đa hình chính:
1. Đa hình tĩnh (Static Polymorphism - Compile-time Polymorphism)
Định nghĩa: Là dạng đa hình xảy ra trong quá trình biên dịch (compile-time). Trình biên dịch sẽ
xác định phiên bản hàm nào được gọi dựa trên kiểu dữ liệu hoặc số lượng/kiểu đối số. Cách thực hiện:
Nạp chồng hàm (Function Overloading): Nhiều hàm có cùng tên nhưng khác nhau về số lượng hoặc kiểu đối số.
Nạp chồng toán tử (Operator Overloading): Định nghĩa lại ý nghĩa của các toán tử cho các kiểu
dữ liệu do người dùng định nghĩa.
Mẫu hàm (Function Templates): Viết một hàm tổng quát có thể hoạt động với nhiều kiểu dữ liệu khác nhau. Ưu điểm:
Hiệu năng cao: Vì việc gọi hàm được xác định tại thời điểm biên dịch, không có chi phí bổ sung trong thời gian chạy.
Kiểm tra lỗi sớm: Lỗi xảy ra nếu không tìm thấy hàm phù hợp sẽ được báo trong quá trình biên dịch. Nhược điểm:
Ít linh hoạt: Không thể thay đổi hành vi của hàm trong thời gian chạy. Các quyết định về hành vi
phải được đưa ra trước khi chương trình chạy.
Khó mở rộng: Khi muốn thêm một hành vi mới, có thể phải thay đổi mã nguồn hiện có hoặc tạo
thêm các phiên bản nạp chồng.
2. Đa hình động (Dynamic Polymorphism - Run-time Polymorphism)
Định nghĩa: Là dạng đa hình xảy ra trong quá trình thực thi chương trình (run-time). Trình biên
dịch không biết phiên bản hàm nào sẽ được gọi cho đến khi chương trình chạy, mà việc này
được quyết định dựa trên kiểu đối tượng thực tế mà con trỏ hoặc tham chiếu trỏ tới. Cách thực hiện:
Ghi đè hàm (Function Overriding): Hàm ảo (virtual function) trong lớp cơ sở được ghi đè
(override) trong các lớp dẫn xuất.
Hàm ảo thuần túy (Pure Virtual Function) và lớp trừu tượng (Abstract Class): Buộc các lớp con
phải triển khai một hàm cụ thể. Ưu điểm:
Tính linh hoạt cao: Cho phép hành vi của đối tượng thay đổi trong thời gian chạy, tùy thuộc vào
loại đối tượng thực tế.
Khả năng mở rộng tốt: Dễ dàng thêm các lớp con mới mà không cần thay đổi mã của lớp cha
hoặc các lớp con khác, chỉ cần đảm bảo các lớp con mới ghi đè hàm ảo.
Code dễ đọc và quản lý hơn: Khi làm việc với các đối tượng thuộc cùng một hệ thống phân cấp,
bạn có thể tương tác với chúng thông qua con trỏ hoặc tham chiếu đến lớp cơ sở. Nhược điểm: lO M oARcPSD| 45467232
Hiệu năng thấp hơn một chút: Do cần tra cứu bảng hàm ảo (vtable) trong thời gian chạy để xác
định phiên bản hàm cần gọi, có một chi phí nhỏ.
Lỗi có thể xảy ra trong thời gian chạy: Nếu việc triển khai hàm ảo không đúng hoặc có lỗi logic,
lỗi chỉ phát hiện được khi chương trình chạy.
Phân biệt đa hình tĩnh và đa hình động:
Đặc điểm l Đa hình tĩnh (compile-time) l Đa hình động (run-time)
Thời điểm xác định l Khi biên dịch l Khi chương trình chạy
Hình thức l Nạp chồng hàm, nạp chồng toán tử l Hàm ảo
Ưu điểm l Hiệu suất cao, rõ ràng l Linh hoạt, hỗ trợ kế thừa tốt
Nhược điểm l Kém linh hoạt l Hiệu suất thấp hơn do gọi gián tiếp 3. Lập trình: #include using namespace std; class ThanhToan { public:
virtual float tinhTien(float soTien) = 0; virtual ~ThanhToan() {} };
class ThanhToanTienMat : public ThanhToan { public:
float tinhTien(float soTien) override {
return soTien * 0.95; // chiết khấu 5% } };
class ThanhToanChuyenKhoan : public ThanhToan { public:
float tinhTien(float soTien) override {
return soTien * 0.98; // chiết khấu 2% } }; int main() { ThanhToan* p; float soTien; int luaChon; lO M oARcPSD| 45467232
cout << "Nhap so tien: "; cin >> soTien; cout << "Chon
phuong thuc (1: Tien mat, 2: Chuyen khoan): "; cin >> luaChon; if (luaChon == 1) p = new ThanhToanTienMat; else
p = new ThanhToanChuyenKhoan;
cout << "So tien sau khi thanh toan: " << p->tinhTien(soTien) << endl; delete p; return 0; }
Bài số 4: Quản lý bộ nhớ và con trỏ trong OOP 1. Trắc nghiệm: a. C → new
b. C → Giải phóng tài nguyên 2. Tự luận: Constructor vs Destructor:
Constructor: Hàm khởi tạo, gọi khi tạo đối tượng.
Destructor: Hàm hủy, gọi khi đối tượng bị hủy (dùng giải phóng tài nguyên như bộ nhớ, file...).
Dynamic Allocation (Cấp phát động):
Dùng khi số lượng đối tượng không biết trước lúc biên dịch (runtime). Sử
dụng từ khóa new, giải phóng bằng delete hoặc delete[].
Giải thích sự khác biệt giữa constructor, destructor và khi nào dùng cấp phát động (dynamic allocation). 1. Constructor (Hàm tạo)
Định nghĩa: Constructor là một hàm thành viên đặc biệt của một lớp, được gọi tự động khi một
đối tượng của lớp đó được tạo ra. Tên của constructor phải giống hệt tên của lớp và nó không
có kiểu trả về (kể cả void). Mục đích:
Khởi tạo đối tượng: Đảm bảo rằng đối tượng được khởi tạo ở trạng thái hợp lệ ngay sau khi nó được tạo.
Cấp phát tài nguyên: Thực hiện cấp phát bộ nhớ động, mở tệp, kết nối cơ sở dữ liệu, hoặc bất
kỳ tài nguyên nào khác mà đối tượng cần.
Ví dụ: Khi bạn khai báo SinhVien sv; hoặc SinhVien* sv = new SinhVien();, constructor của lớp SinhVien sẽ được gọi. 2. Destructor (Hàm hủy)
Định nghĩa: Destructor cũng là một hàm thành viên đặc biệt của một lớp, được gọi tự động khi
một đối tượng của lớp đó bị hủy (ví dụ: khi đối tượng ra khỏi phạm vi, khi chương trình kết thúc, lO M oARcPSD| 45467232
hoặc khi delete một đối tượng được cấp phát động). Tên của destructor là dấu ngã (~) theo sau
là tên lớp (ví dụ: ~ClassName()) và nó không có kiểu trả về, cũng không nhận đối số. Mục đích:
Giải phóng tài nguyên: Thực hiện các công việc dọn dẹp, như giải phóng bộ nhớ động đã được
cấp phát bởi constructor hoặc các phương thức khác, đóng tệp, đóng kết nối cơ sở dữ liệu,
v.v., để tránh rò rỉ tài nguyên.
Đảm bảo tài nguyên được trả lại hệ thống: Tránh tình trạng "rò rỉ bộ nhớ" (memory leak) hoặc
các tài nguyên khác bị chiếm dụng mà không được giải phóng.
Ví dụ: Khi sv trong SinhVien sv; ra khỏi phạm vi, hoặc khi bạn gọi delete sv; cho một đối tượng
được cấp phát động, destructor của lớp SinhVien sẽ được gọi.
Khi nào dùng cấp phát động (Dynamic Allocation)?
Cấp phát động (sử dụng new và delete trong C++) là quá trình cấp phát bộ nhớ trong thời gian
chạy (run-time) từ heap (vùng nhớ tự do), thay vì cấp phát trong thời gian biên dịch
(compile-time) từ stack. Chúng ta nên dùng cấp phát động trong các trường hợp sau:
Kích thước dữ liệu không biết trước khi biên dịch: Khi bạn cần tạo một mảng hoặc một cấu trúc
dữ liệu mà kích thước của nó chỉ được xác định trong thời gian chạy (ví dụ: người dùng nhập số lượng phần tử).
Dữ liệu cần tồn tại ngoài phạm vi hàm: Khi một đối tượng cần tồn tại ngay cả sau khi hàm tạo ra
nó đã kết thúc. Các đối tượng được cấp phát trên stack sẽ tự động bị hủy khi hàm kết thúc,
nhưng đối tượng trên heap vẫn tồn tại cho đến khi được delete thủ công.
Tạo các cấu trúc dữ liệu linh hoạt: Ví dụ như danh sách liên kết, cây, đồ thị, nơi các nút được
thêm hoặc xóa một cách linh hoạt trong thời gian chạy.
Đa hình động (Dynamic Polymorphism): Khi bạn muốn sử dụng con trỏ hoặc tham chiếu của
lớp cơ sở để trỏ đến các đối tượng của lớp dẫn xuất và gọi các hàm ảo (như trong Bài 3). Điều
này yêu cầu các đối tượng được cấp phát động.
Khi làm việc với các tài nguyên lớn: Đối với các đối tượng quá lớn không thể cấp phát trên
stack (stack có giới hạn kích thước).
Lưu ý quan trọng: Khi sử dụng new để cấp phát bộ nhớ động, bạn phải luôn sử dụng delete để
giải phóng bộ nhớ đó khi không còn cần đến nữa để tránh rò rỉ bộ nhớ. Đối với mảng cấp phát
động (new Type[size]), bạn phải dùng delete[] để giải phóng. 3. Lập trình: #include using namespace std; class SinhVien { private: string ten; float diemTB; public:
void nhap() { cout << "Ten SV: "; cin.ignore();
getline(cin, ten); cout << "Diem TB: "; cin >> diemTB; } lO M oARcPSD| 45467232
void xuat() { cout << "Ten: " << ten << ", Diem TB: " << diemTB << endl; } }; int main() { int n;
cout << "Nhap so sinh vien: "; cin >> n;
SinhVien* ds = new SinhVien[n];
for (int i = 0; i < n; i++) { cout <<
"\nNhap SV thu " << i + 1 << ":\n"; ds[i].nhap(); }
cout << "\nDanh sach sinh vien:\n";
for (int i = 0; i < n; i++) { ds[i].xuat(); } delete[] ds; return 0; }
Bài số 5: Mô hình hóa hệ thống bằng lớp và đối tượng 1. Trắc nghiệm:
a. C → Ngôn ngữ mô hình hóa hướng đối tượng
b. C → Thành phần lớp là đối tượng lớp khác 2. Tự luận: Quan hệ trong UML:
Loại quan hệ l Ý nghĩa l Ký hiệu UML
Kế thừa (generalization) l Lớp con kế thừa lớp cha l Tam giác trắng
Kết hợp (association) l Một lớp liên kết với lớp khác (có thể 1-nhiều) l Mũi tên đơn
Tập hợp (aggregation) l Một lớp chứa lớp khác (có thể độc lập) l Hình thoi rỗng
Thành phần (composition) l Lớp chứa lớp khác (phụ thuộc sống chết) l Hình thoi đen Trình bày
các quan hệ giữa các lớp trong UML: kế thừa, kết hợp, tập hợp.
Trong UML (Unified Modeling Language), các quan hệ giữa các lớp là cách thể hiện sự tương
tác và phụ thuộc giữa chúng trong một hệ thống. Dưới đây là ba quan hệ cơ bản:
1. Kế thừa (Inheritance / Generalization) lO M oARcPSD| 45467232
Ý nghĩa: Thể hiện mối quan hệ "là một loại" (is-a kind of). Một lớp con kế thừa các thuộc tính và
phương thức từ lớp cha. Lớp con mở rộng hoặc ghi đè các hành vi của lớp cha.
Ký hiệu UML: Một đường liền nét với mũi tên rỗng (hình tam giác) trỏ từ lớp con đến lớp cha. Ví
dụ: Dog kế thừa từ Animal (A Dog is a kind of Animal). Đặc điểm: Giúp tái sử dụng mã.
Tạo ra một hệ thống phân cấp các lớp.
Thúc đẩy tính đa hình động (khi sử dụng hàm ảo). 2. Kết hợp (Association)
Ý nghĩa: Thể hiện mối quan hệ chung giữa hai lớp, trong đó một lớp "có" hoặc "sử dụng" một
hoặc nhiều đối tượng của lớp khác. Đây là mối quan hệ yếu hơn so với tập hợp hoặc hợp thành.
Ký hiệu UML: Một đường thẳng liền nét giữa hai lớp, có thể có mũi tên chỉ hướng của sự tương
tác, và có thể có bội số (multiplicity) ở hai đầu (ví dụ: 1, 0..*, 1..*).
Ví dụ: Student kết hợp với Course (A Student takes many Courses). Đặc điểm:
Các đối tượng có thể tồn tại độc lập với nhau.
Không có sự sở hữu mạnh mẽ về vòng đời. 3. Tập hợp (Aggregation)
Ý nghĩa: Là một dạng đặc biệt của kết hợp, thể hiện mối quan hệ "một phần của" (part-of)
nhưng yếu hơn hợp thành. Một đối tượng lớn hơn (whole) chứa các đối tượng nhỏ hơn (parts),
nhưng các đối tượng con có thể tồn tại độc lập nếu đối tượng cha bị hủy.
Ký hiệu UML: Một đường liền nét với một hình thoi rỗng ở phía lớp "toàn thể" (whole) và một
đường thẳng đến lớp "thành phần" (part).
Ví dụ: Library tập hợp Book (A Library has Books). Nếu thư viện đóng cửa, các cuốn sách vẫn tồn tại. Đặc điểm:
Mối quan hệ "có" (has-a) nhưng các thành phần không bị phụ thuộc vào vòng đời của đối tượng chứa nó.
Các thành phần có thể được chia sẻ giữa nhiều đối tượng "toàn thể".
4. Hợp thành (Composition) - (Thường được xem xét cùng với Tập hợp)
Ý nghĩa: Là một dạng mạnh nhất của kết hợp, thể hiện mối quan hệ "là một phần không thể
tách rời của" (is-a part-of, mandatory). Các đối tượng con không thể tồn tại độc lập với đối
tượng cha. Nếu đối tượng cha bị hủy, các đối tượng con cũng bị hủy theo.
Ký hiệu UML: Một đường liền nét với một hình thoi đặc (đen) ở phía lớp "toàn thể" (whole) và
một đường thẳng đến lớp "thành phần" (part).
Ví dụ: House hợp thành Room (A House has Rooms). Nếu căn nhà bị phá hủy, các phòng cũng
không còn tồn tại. Đặc điểm:
Mối quan hệ "có" (has-a) mạnh mẽ, vòng đời của các thành phần phụ thuộc hoàn toàn vào đối tượng chứa nó.
Các thành phần không thể được chia sẻ. 3. Lập trình: lO M oARcPSD| 45467232 #include using namespace std; class NhaXuatBan { private: string tenNXB; int namTL; public:
void nhap() { cout << "Ten NXB: "; cin.ignore();
getline(cin, tenNXB); cout << "Nam thanh lap: "; cin >> namTL; } void xuat() {
cout << "NXB: " << tenNXB << ", Nam TL: " << namTL; } }; class Sach { private: string maSach, tenSach;
NhaXuatBan nxb; // Quan hệ "has-a" public:
void nhap() { cout << "Ma sach: "; cin
>> maSach; cin.ignore(); cout <<
"Ten sach: "; getline(cin, tenSach); cout
<< "---Nhap thong tin NXB---\n"; nxb.nhap(); }
void xuat() { cout << "\nMa: " << maSach << ", Ten: " <<
tenSach << ", "; nxb.xuat(); cout << endl; } }; int main() { int n; cout << "Nhap so sach: "; cin >> n; Sach* ds = new Sach[n];
for (int i = 0; i < n; i++) { cout << "\nNhap
sach thu " << i + 1 << ":\n"; ds[i].nhap(); }
cout << "\nDanh sach sach:\n";
for (int i = 0; i < n; i++) { ds[i].xuat(); } lO M oARcPSD| 45467232 delete[] ds; return 0; }