You are on page 1of 118

Nhóm NMTP

(Tài liệu lưu hành nội bộ)

Ngôn ngữ lập trình

C++
Ngôn ngữ lập trình

C++

Đặng Nguyên Phương

TPHCM − 02/2018
“C makes it easy to shoot yourself in the foot;
C++ makes it harder, but when you do it blows your whole leg off.”

— Bjarne Stroustrup
Mục lục

Lời nói đầu 5

1 Giới thiệu ngôn ngữ lập trình C++ 7


1.1 Ngôn ngữ lập trình . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
1.2 Lập trình hướng đối tượng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.3 Ngôn ngữ C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.3.1 Giới thiệu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.3.2 Lịch sử phát triển . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.3.3 Chuẩn C++11 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.4 Trình biên dịch C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.4.1 Các trình biên dịch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.4.2 Các trình thông dịch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5 Hướng dẫn cài đặt và sử dụng một số trình biên dịch C++ . . . . . . . . . . . . . . . 13
1.5.1 Visual C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.5.2 GCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

2 Một số khái niệm C++ cơ bản 17


2.1 Cấu trúc đơn giản của một chương trình C++ . . . . . . . . . . . . . . . . . . . . . . 17
2.2 Các lệnh xuất nhập dữ liệu cơ bản . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.3 Biến . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3.1 Các kiểu dữ liệu của biến . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3.2 Phạm vi của biến . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.3.3 Lvalue và rvalue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.3.4 Định nghĩa kiểu tự động . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.4 Hằng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.4.1 Các loại hằng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
2.4.2 Cách định nghĩa hằng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.5 Toán tử . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
2.6 Các hàm toán học . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.7 Các chỉ thị tiền xử lý . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.8 Không gian tên . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.9 Thư viện chuẩn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28

3 Cấu trúc điều khiển 29


3.1 Cấu trúc điều khiển if...else... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
3.2 Cấu trúc lựa chọn switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.3 Cấu trúc lặp for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
3.4 Cấu trúc lặp while . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.5 Các lệnh cho vòng lặp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34

1
Mục lục 2

4 Mảng và chuỗi 37
4.1 Mảng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.1.1 Khai báo mảng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.1.2 Thao tác với các phần tử trong mảng . . . . . . . . . . . . . . . . . . . . . . 38
4.1.3 Mảng nhiều chiều . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.2 Chuỗi kí tự . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.2.1 Chuỗi kí tự theo kiểu C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.2.2 Lớp chuỗi kí tự . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
4.2.3 Mảng chứa các chuỗi kí tự . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
4.2.4 Chuỗi thô . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45

5 Hàm 47
5.1 Khai báo hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.1.1 Cú pháp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
5.1.2 Khai báo hai hay nhiều hàm có cùng tên . . . . . . . . . . . . . . . . . . . . . 48
5.1.3 Nguyên mẫu hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
5.2 Đối số của hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.2.1 Giá trị mặc định . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.2.2 Cách truyền đối số . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.3 Hàm đệ quy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
5.4 Hàm nội tuyến . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
5.5 Kĩ thuật nạp chồng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.5.1 Nạp chồng hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.5.2 Nạp chồng toán tử . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
5.6 Các hàm lambda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53

6 Con trỏ 57
6.1 Thao tác với con trỏ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.1.1 Khai báo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
6.1.2 Con trỏ NULL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
6.1.3 Con trỏ void . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
6.1.4 Phép tính số học với con trỏ . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
6.1.5 Phép so sánh với con trỏ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
6.2 Con trỏ và tham chiếu . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
6.3 Con trỏ và hằng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
6.4 Con trỏ và mảng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
6.4.1 Con trỏ và địa chỉ mảng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
6.4.2 Mảng con trỏ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
6.5 Con trỏ và hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
6.5.1 Truyền con trỏ cho hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
6.5.2 Trả về con trỏ từ hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
6.5.3 Con trỏ hàm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
6.6 Bộ nhớ động . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
6.6.1 Toán tử cấp phát bộ nhớ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
6.6.2 Hàm cấp phát bộ nhớ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
6.6.3 Các vùng bộ nhớ trên máy tính . . . . . . . . . . . . . . . . . . . . . . . . . . 67
6.6.4 Các lỗi thường gặp khi sử dụng bộ nhớ động . . . . . . . . . . . . . . . . . . 67
6.7 Smart pointer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67

7 Tập tin 71
7.1 Thao tác với tập tin theo kiểu C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
7.1.1 Mở/đóng tập tin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
7.1.2 Đọc dữ liệu từ tập tin văn bản . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3 MỤC LỤC

7.1.3 Ghi dữ liệu ra tập tin văn bản . . . . . . . . . . . . . . . . . . . . . . . . . . 74


7.1.4 Đọc/ghi dữ liệu với tập tin nhị phân . . . . . . . . . . . . . . . . . . . . . . . 74
7.2 Thao tác với tập tin theo kiểu C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
7.2.1 Mở/đóng tập tin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
7.2.2 Kiểm tra trạng thái của tập tin . . . . . . . . . . . . . . . . . . . . . . . . . . 77
7.2.3 Đọc/ghi dữ liệu với tập tin văn bản . . . . . . . . . . . . . . . . . . . . . . . 78
7.2.4 Đọc/ghi dữ liệu với tập tin nhị phân . . . . . . . . . . . . . . . . . . . . . . . 79

8 Lớp và đối tượng 81


8.1 Dữ liệu tự định nghĩa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
8.2 Dữ liệu có cấu trúc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
8.3 Lớp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
8.4 Hàm bạn và lớp bạn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
8.5 Lớp dẫn xuất . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
8.6 Một số vấn đề thêm về lớp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
8.6.1 Con trỏ this . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
8.6.2 Hàm ảo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
8.6.3 Lớp cơ sở trừu tượng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
8.6.4 Hàm khởi tạo sao chép . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
8.6.5 Sao chép đối tượng . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
8.6.6 Danh sách khởi tạo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90

9 Thư viện chuẩn 91


9.1 Template . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
9.2 Các thành phần chính của STL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
9.3 Container . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
9.3.1 Vector . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
9.3.2 Map . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
9.4 Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
9.5 Functor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
9.6 Algorithm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96

10 Một số vấn đề nâng cao 99


10.1 Cách sử dụng một số từ khóa . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
10.1.1 Từ khoá const . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
10.1.2 Từ khoá extern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
10.1.3 Từ khoá static . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
10.2 Định dạng dòng dữ liệu xuất . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
10.3 Cách tổ chức mã nguồn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
10.3.1 Dự án . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
10.3.2 File header . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
10.4 Một số nâng cấp, bổ sung trong C++11 . . . . . . . . . . . . . . . . . . . . . . . . . 105
10.4.1 Tính toán song song . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
10.4.2 Các hàm begin() và end() tổng quát . . . . . . . . . . . . . . . . . . . . . . . 106
10.4.3 Alternative function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
10.4.4 Khởi tạo theo kiểu danh sách . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
10.4.5 Các hàm default và delete . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
10.4.6 Ủy nhiệm hàm khởi tạo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
10.4.7 Tham chiếu rvalue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
10.4.8 Các hàm emplace . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

Tài liệu tham khảo 111


Mục lục 4
LỜI NÓI ĐẦU

Ngôn ngữ lập trình C/C++ là một trong những ngôn ngữ lập trình phổ biến nhất trên thế giới,
được ứng dụng trong nhiều ngành khác nhau, và là nền tảng để xây dựng nên một số hệ điều
hành, ngôn ngữ lập trình khác (xem danh sách một số ứng dụng tiêu biểu của C++ tại đây
http://www.stroustrup.com/applications.html). Trong lĩnh vực vật lý hạt nhân và hạt cơ
bản, ngôn ngữ lập trình C++ được sử dụng để xây dựng các chương trình tính toán lý thuyết, mô
phỏng và xử lý số liệu, chẳng hạn như
• LISE: chương trình tính toán phản ứng hạt nhân
http://lise.nscl.msu.edu/lise.html
• GALPROP: chương trình tính toán cho vật lý thiên văn
http://galprop.stanford.edu/
• PYTHIA: chương trình tạo các event cho vật lý hạt cơ bản
http://home.thep.lu.se/~torbjorn/Pythia.html
• GEANT4: chương trình mô phỏng vận chuyển hạt bằng phương pháp Monte Carlo
http://geant4.cern.ch/
• ROOT: chương trình xử lý số liệu
http://root.cern.ch/drupal/
• ...
Tài liệu này được biên soạn nhằm giúp các bạn sinh viên chuyên ngành vật lý hạt nhân và hạt cơ
bản có thể tìm hiểu về ngôn ngữ lập trình C++ và ứng dụng của nó vào giải quyết các bài toán
chuyên ngành. Phần đầu của tài liệu sẽ trình bày một số kiến thức cơ bản về ngôn ngữ lập trình
C++ cũng như lịch sử phát triển của nó nhằm giúp cho các bạn có một cái nhìn tổng quát về ngôn
ngữ lập trình này. Một số khái niệm cơ bản của kĩ thuật lập trình hướng đối tượng cũng được đề
cập đến trong phần này.
Các Chương 2 - 4 là phần dành cho các bạn mới bắt đầu làm quen với việc lập trình C++. Một số
kiến thức cơ bản về ngôn ngữ lập trình C++ sẽ được trình bày trong các chương này chẳng hạn
như các khái niệm về biến (variable), hằng (constant), toán tử (operator ); các cấu trúc điều khiển;
và đặc biệt là các kiểu dữ liệu mảng (array) và chuỗi (string). Đây chính là các khái niệm cơ bản
mà các bạn phải nắm vững nếu muốn viết một chương trình C++.
Đối với các bạn đã biết căn bản về C++, các bạn có thể chuyển thẳng vào Chương 5 để làm quen
với các kiến thức về hàm cũng như về con trỏ (Chương 6). Sau đó, các bạn sẽ được làm quen với
các thao tác sử lý tập tin (file) trong Chương 7. Các kiến thức về lớp (class) và đối tượng (object)
sẽ được trình bày trong Chương 8.
Trong Chương 9, tác giả sẽ trình bày các kiến thức về template và đặc biệt là một số kiến thức
cơ bản về thư viện chuẩn của C++. Chương cuối sẽ được dành cho việc giới thiệu những kiến thức

5
LỜI NÓI ĐẦU 6

nâng cao cũng như những cập nhật mới nhất của chuẩn C++11 là tiêu chuẩn đang được sử dụng
trong tài liệu này.
Mục đich của tài liệu là giới thiệu nhưng kiến thức, khái niệm cơ bản nhất của C++ do đó nội dung
của nó khá cô đọng. Mặc dù vẫn còn rất nhiều khái niệm chưa được trình bày trong tài liệu này,
nhưng hi vọng nó sẽ giúp ích cho các bạn sinh viên có mong muốn sử dụng thành thạo một ngôn
ngữ lập trình mạnh trong công việc tính toán, xử lý số liệu hay mô phỏng.

Đặng Nguyên Phương


Chương 1
GIỚI THIỆU NGÔN NGỮ
LẬP TRÌNH C++

1.1 Ngôn ngữ lập trình


Ngôn ngữ lập trình là một tập hợp (bộ) các quy tắc (rule) nhằm giúp người dùng hướng dẫn máy
tính cách thức hoạt động.

Ngôn ngữ lập trình được chia làm 5 thế hệ (Hình 1.1):

• Thế hệ 1: ngôn ngữ máy (machine language), phụ thuộc vào từng loại máy, đây là ngôn ngữ
lập trình có hiệu quả cao tuy nhiên lại khó hiểu và khó sử dụng đối với người dùng.

• Thế hệ 2: hợp ngữ (assembly language) được xem là ngôn ngữ cấp thấp, sử dụng các thuật
nhớ (mnemonic) thay thế cho mã máy (binary), do đó dễ viết hơn so với ngôn ngữ máy. Các
chương trình hợp ngữ thường phụ thuộc chặt chẽ vào một kiến trúc máy tính xác định. Việc
chuyển đổi giữa hợp ngữ và mã máy được thực hiện thông qua các trình hợp dịch (assembler )
và trình phân dịch (disassembler ).

• Thế hệ 3: các ngôn ngữ lập trình có cấu trúc (structured language) hay ngôn ngữ lập trình
thủ tục (procedural language), các chương trình được viết dưới dạng mã nguồn (source code)
và được biên dịch thành ngôn ngữ máy. Các chương trình thực hiện việc biên dịch này được
gọi là các trình biên dịch (compiler ), hoặc trong một số trường hợp ta còn sử dụng các trình
thông dịch (interpreter ). Đặc điểm của các ngôn ngữ thế hệ thứ 3 này so với các ngôn ngữ
thế hệ trước đó là chúng độc lập với kiến trúc máy tính. Các ngôn ngữ tiêu biểu có thể kể
đến chẳng hạn như Fortran, Basic, Pascal, C/C++,...

• Thế hệ 4: các ngôn ngữ lập trình theo lĩnh vực (domain-specific language), thường tập trung
vào một lĩnh vực cụ thể hơn so với các ngôn ngữ thế hệ thứ 3 (thường là các ngôn ngữ đa
mục đích − general purpose). Đồng thời, các chương trình thuộc ngôn ngữ thế hệ này cũng
đòi hỏi lập trình ít hơn để hoàn thành công việc so với ngôn ngữ thế hệ thứ 3. Các ngôn ngữ
tiêu biểu có thể kể đến chẳng hạn như Mathematica, Matlab, Python, Ruby, Perl,...

• Thế hệ 5: các ngôn ngữ ứng dụng (applicative language) hay ngôn ngữ khai báo (declarative
language), thường tập trung giải quyết những vấn đề cụ thể mà không cần đến người lập
trình chuyên nghiệp, không quá chú trọng đến thuật toán xứ lý vấn đề. Các ngôn ngữ tiêu
biểu có thể kể đến chẳng hạn như Lisp, Scheme, SML, Prolog,...

Các ngôn ngữ lập trình cũng được chia là hai nhóm chính:

7
1.2. Lập trình hướng đối tượng 8

Hình 1.1: Sơ đồ cây của các ngôn ngữ lập trình

• Nhóm ngôn ngữ truyền thống: xây dựng một chuỗi các bước thực hiện, nhóm này bao gồm
các ngôn ngữ trong 3 thế hệ đầu tiên1 .
• Nhóm ngôn ngữ hướng đối tượng: tạo ra các đối tượng thay vì chuỗi các bước thực hiện, bao
gồm một số ngôn ngữ thế hệ 3, các ngôn ngữ thế hệ 4 và 5.

1.2 Lập trình hướng đối tượng


Lập trình hướng đối tượng (object-oriented programming − OOP) là kĩ thuật lập trình hỗ trợ công
nghệ đối tượng. Kĩ thuật lập trình này giúp tăng năng suất và giảm nhẹ các thao tác viết mã cho
người lập trình. Nó cũng giúp cho người lập trình tạo ra các ứng dụng mà các yếu tố bên ngoài
có thể tương tác với các chương trình đó giống như là tương tác với các đối tượng vật lý, điều này
giúp đơn giản hóa độ phức tạp khi bảo trì cũng như mở rộng phần mềm bằng cách cho phép lập
trình viên tập trung vào các đối tượng phần mềm ở bậc cao hơn.
Đối tượng (object) là sự kết hợp giữa mã và dữ liệu hình thành nên một đơn vị duy nhất, đơn vị
này tương đương với một chương trình con. Các đối tượng bao gồm hai thành phần chính:
• Các phương thức (method ): là phương tiện để sử dụng một đối tượng, các phương thức thường
là các hàm.
• Các thuộc tính (attribute): dùng để mô tả các tính chất của đối tượng, thường là các biến,
tham số hay hằng nội tại (các dữ liệu nội tại).
Mỗi phương thức hay mỗi dữ liệu nội tại cùng với các tính chất được định nghĩa (bởi người lập
trình) được xem là một đặc tính của đối tượng (xem Hình 1.2). Trong thực tế, các đối tượng thường
được trừu tượng hóa qua việc định nghĩa các lớp (class).
1
Nhóm ngôn ngữ truyền thống đôi khi còn được chia thành hai nhóm:
– Lập trình tuyến tính (tuần tự): chương trình được thực hiện tuần tự từ đầu đến cuối, lệnh này kế tiếp lệnh kia
cho đến khi kết thúc chương trình.
– Lập trình cấu trúc (thủ tục): chương trình chính được chia nhỏ thành các chương trình con và mỗi chương
trình con thực hiện một công việc xác định.
9 CHƯƠNG 1. GIỚI THIỆU NGÔN NGỮ LẬP TRÌNH C++

Hình 1.2: Minh họa đối tượng; một đối tượng, chẳng hạn như một chiếc xe hơi, sẽ có các thông tin
(thuộc tính) như mẫu xe, năm sản xuất, màu sắc và các hành động (phương thức) như khởi động,
chạy và dừng.

Mỗi đối tượng có một tên riêng biệt và tất cả các tham chiếu đến đối tượng đó được tiến hành qua
tên của nó. Như vậy, mỗi đối tượng có khả năng nhận vào các thông báo, xử lý dữ liệu (bên trong
của nó), và gửi ra hay trả lời đến các đối tượng khác hay đến môi trường.

Một số tính chất chính của lập trình hướng đối tượng:

• Tính trừu tượng (abstraction): khả năng của chương trình bỏ qua hay không chú ý đến một
số khía cạnh của thông tin mà nó đang trực tiếp làm việc lên. Mỗi đối tượng có thể hoàn tất
các công việc một cách nội bộ và liên lạc với các đối tượng khác mà không cần cho biết làm
cách nào đối tượng tiến hành được các thao tác. Tính trừu tượng còn thể hiện qua việc một
đối tượng ban đầu có thể có một số đặc điểm chung cho nhiều đối tượng khác như là sự mở
rộng của nó nhưng bản thân đối tượng ban đầu này có thể không có các biện pháp thi hành.
Tính trừu tượng này thường được xác định trong khái niệm gọi là lớp trừu tượng (abstract
class) hay lớp cơ sở trừu tượng (abstract base class).

• Tính đóng gói (encapsulation): không cho phép người sử dụng các đối tượng thay đổi trạng
thái nội tại của một đối tượng, chỉ có các phương thức nội tại của đối tượng cho phép thay
đổi trạng thái của nó. Việc cho phép môi trường bên ngoài tác động lên các dữ liệu nội tại
của một đối tượng theo cách nào là hoàn toàn tùy thuộc vào người lập trình. Đây là tính
chất đảm bảo sự toàn vẹn của đối tượng.

• Tính kế thừa (inheritance): cho phép một đối tượng có thể có sẵn các đặc tính mà đối tượng
khác đã có thông qua kế thừa. Điều này cho phép các đối tượng chia sẻ hay mở rộng các đặc
tính sẵn có mà không cần phải tiến hành định nghĩa lại. Nếu ta có một lớp B kế thừa từ lớp
A thì A được gọi là lớp cơ sở (base class) và B được gọi là lớp dẫn xuất (derived class).

• Tính đa hình (polymorphism): thể hiện thông qua việc gửi các thông điệp (message), các
phương thức dùng trả lời cho một thông điệp sẽ tùy theo đối tượng mà thông điệp đó được
gửi tới sẽ có các phản ứng khác nhau. Người lập trình có thể định nghĩa một đặc tính (chẳng
hạn như một phương thức) cho một loạt các đối tượng gần nhau, khi thi hành chương trình
sẽ tự động kiểm tra xem đối tượng là thuộc kiểu lớp nào sau đó sẽ gọi phương thức tương
ứng với lớp đó.

Ưu điểm của lập trình hướng đối tượng:

• Dữ liệu được đóng gói vào trong các đối tượng, nếu muốn truy nhập vào dữ liệu phải thông
qua các phương thức được cho phép của đối tượng, do đó sẽ giảm thiểu nguy cơ dữ liệu bị
thay đổi tự do trong chương trình.
1.3. Ngôn ngữ C++ 10

• Khi thay đổi cấu trúc dữ liệu của một đối tượng, không cần thay đổi mã nguồn của các đối
tượng khác, điều này cũng hạn chế sự ảnh hưởng xấu của việc thay đổi dữ liệu đến các đối
tượng khác trong chương trình và giúp cho việc bảo trì, nâng cấp chương trình dễ dàng hơn.

• Nhờ nguyên tắc kế thừa, ta có thể sử dụng lại mã nguồn, tiết kiệm tài nguyên, chi phí thời
gian.

Tuy nhiên phương pháp lập trình hướng đối tượng cũng có những nhược điểm:

• Khái niệm hướng đối tượng còn tương đối mới lạ đối với một số người, do đó tốn không ít
thời gian để tìm hiểu và xây dựng một chương trình hướng đối tượng.

• Các chương trình hướng đối tượng thông thường đòi hỏi việc viết mã nguồn dài hơn so với
các chương trình thủ tục thông thường.

• Thời gian thực thi các chương trình hướng đối tượng nói chung cũng chậm hơn so với các
chương trình thủ tục.

1.3 Ngôn ngữ C++


1.3.1 Giới thiệu

C++ là một ngôn ngữ lập trình đa mục đích (general purpose) được Bjarne Stroustrup phát triển
từ những năm 1980 tại Bell Labs. Đây là một trong những ngôn ngữ lập trình phổ biến nhất trên
thế giới (cùng với C, Objective-C, và Java). Các đặc trưng của ngôn ngữ này gồm có:

• Dạng tự do (free-form): có thể sử dụng khoảng trắng tùy ý

• Kiểu tĩnh (static type): tất cả các biến phải được gán kiểu trước khi biên dịch, và kiểu của
biến sẽ được kiểm tra trong thời gian biên dịch (compile-time)2

• Hỗ trợ lập trình thủ tục (procedural programing) hay lập trình cấu trúc (structured program-
ming): phân chia công việc chính thành các công việc nhỏ hơn và giao cho một hàm đảm
nhiệm, chương trình chính sẽ gọi các hàm này vào những thời điểm cần thiết.

• Dữ liệu trừu trượng (abstract data type): có sử dụng các kiểu dữ liệu do người dùng tự định
nghĩa.

• Lập trình hướng đối tượng (object-oriented programming): coi chương trình là tập hợp của
các đối tượng (object) có quan hệ nào đó với nhau, mỗi đối tượng có dữ liệu và phương thức
(method ) của riêng mình.

• Lập trình đa hình (polymorphism): một biến có thể có nhiều kiểu (thông qua con trỏ) hoặc
có thể dùng nhiều hàm có cùng tên.

Ngôn ngữ C++ gồm có hai thành phần chính

• Phần ngôn ngữ cốt lõi (core language) bao gồm ngôn ngữ lập trình, một số thư viện gốc và
các danh định (identifier ) được biết đến với tên gọi “từ khóa” (keyword ).

• Thư viện chuẩn C++ (Standard Template Library − STL) là một tập hợp các lớp và các hàm
được viết bằng ngôn ngữ cốt lõi. Thư viện này cung cấp các container, hàm để làm tiện ích,
các đối tượng hàm, các dãy kí tự tổng quát và các dòng dữ liệu (bao gồm I/O tương tác và
tập tin), hỗ trợ một số tính năng ngôn ngữ (bao gồm cả thư viện chuẩn C).
2
ngược lại là kiểu động (dynamic type), các biến có thể mang bất kì kiểu nào, việc gán kiểu cho biến sẽ được thực
hiện trong thời gian chạy chương trình (run-time)
11 CHƯƠNG 1. GIỚI THIỆU NGÔN NGỮ LẬP TRÌNH C++

1.3.2 Lịch sử phát triển


Nói đến C++ ta cần phải nói đến ngôn ngữ đã làm nền tảng cho nó, đó chính là ngôn ngữ lập trình
C. Ngôn ngữ này được phát triển đầu tiên bởi Dennis Ritchie trong khoảng những năm 1969 đến
1972 tại Phòng Thí nghiệm AT&T Bell. Đây là một ngôn ngữ lập trình tương đối nhỏ gọn, vận
hành gần với phần cứng và có tính linh hoạt cao. Đến khoảng cuối thập niên 1970 và thập niên
1980, ngôn ngữ C đã được sử dụng rộng rãi trong các hệ thống máy tính từ mainframe cho đến
microcomputer, các lập trình viên sử dụng nó để viết đủ thứ loại chương trình từ các phần mềm
hệ thống cho đến các chương trình ứng dụng.
Trong khoảng thời gian 1979-1980, Bjarne Stroustrup đã tạo ra một loại ngôn ngữ nâng cao bằng
cách thêm các tính năng của ngôn ngữ Simula 3 vào trong C, ngôn ngữ này ban đầu được đặt tên
là “C with classes” (C với các lớp).
Năm 1983, tên “C with classes” được đổi thành C++ (++ là toán tử tăng thêm 1 đơn vị trong C,
ngụ ý rằng đây là bản nâng cao của C). So với C, C++ tăng cường thêm nhiều tính năng, chẳng
hạn như khái niệm lớp (class), hàm ảo, các kiểu tham chiếu, các hàm nội tuyến (inline), các đối
số mặc định, không gian tên (namespace), khả năng kiểm soát bộ nhớ của lưu trữ tự do, kiểm soát
kiểu,...
Năm 1989 phiên bản C++ 2.0 phát hành. Các tính năng mới được bổ sung bao gồm đa kế thừa,
lớp trừu tượng, các hàm tĩnh, hàm thành viên hằng,...
Năm 1998, hội đồng tiêu chuẩn hoá C++ đã soạn thảo tiêu chuẩn ISO/IEC 14882:1998 cho C++
(được biết đến với tên gọi C++98). Trong những năm tiếp theo, hội đồng tiếp tục xử lí các vấn đề
gặp phải và ban hành một phiên bản hiệu chỉnh ISO/IEC 14882:2003 của chuẩn C++ trong năm
2003 (C++03).
Năm 2005, bản báo cáo “Library Technical Report 1” (TR1) được phát hành, phiên bản tiêu chuẩn
C++ tương ứng (C++TR1) ra đời vào năm 2007.
Năm 2011, chuẩn C++11 đã chính thức được ban hành với khá nhiều thay đổi quan trọng như tính
năng khởi tạo theo danh sách, lập trình song song, các hàm lambda,...
Các chuẩn tiếp theo của C++ dự kiến sẽ được thông qua vào năm 2014 và 2017 (với tên gọi tương
ứng là C++14 và C++17)4 .

1.3.3 Chuẩn C++11


Chuẩn C++11 (trước đây gọi là C++0x) là tiêu chuẩn mới nhất của ngôn ngữ lập trình C++ tính
cho tới thời điểm hiện nay. Tên gọi C++11 của tiêu chuẩn này được dựa vào năm xuất bản của
bản chi tiết kỹ thuật (specification). Tiêu chuẩn ISO/IEC 14882:2011 cho C++ được ban hành vào
tháng 9/2011.
Mặc dù chuẩn C++98 vẫn còn là một tiêu chuẩn rất được phổ biến (được nhiều người dùng sử dụng
để viết các chương trình, được sử dụng trong các khóa dạy về lập trình C++, lập trình hướng đối
tượng trong các trường đại học), mục đích của việc đưa ra các chuẩn C++ mới, cụ thể là C++11 là
nhằm
• Đạt được hiệu năng cao hơn về tốc độ, khả năng sử dụng bộ nhớ, tận dụng các cấu trúc phần
cứng mới như các bộ xử lý đa lõi (multi-core),...
• Hiệu quả lập trình cao hơn qua việc cung cấp các lớp, cú pháp và kỹ thuật lập trình mới
nhằm đẩy mạnh xu hướng lập trình tổng quát (generic programming)5 .
3
được xem là một trong những ngôn ngữ lập trình hướng đối tượng đầu tiên, được phát triển vào khoảng những
năm 1960 bởi Ole-Johan Dahl và Kristen Nygaard
4
Bản nháp của chuẩn C++14 đã được hoàn thành vào giữa tháng 8 năm 2014
5
Một tư duy lập trình mở, trên quan điểm tổng quát hóa tất cả những gì có thể nhằm đưa ra một khuôn mẫu giải
1.4. Trình biên dịch C++ 12

• Vẫn tương thích với các phiên bản C++ cũ (C++98, C++03).
• Không đưa thêm vào quá nhiều các khái niệm mới hoặc các tính năng chưa ổn định.
• Làm cho ngôn ngữ C++ trở nên dễ học và dễ dạy hơn bằng các giảm thiểu các tính năng mà
cần các lập trình viên chuyên nghiệp.
• ...
Chuẩn C++11 cũng chính là chuẩn được sử dụng trong tài liệu này. Các nâng cấp của chuẩn này
so với các chuẩn trước đó sẽ được trình bày trong một chương riêng.

1.4 Trình biên dịch C++


1.4.1 Các trình biên dịch
Trình biên dịch (compiler ) là một chương trình máy tính làm công việc dịch một chuỗi các câu lệnh
được viết bằng một ngôn ngữ lập trình, hay còn gọi mã nguồn (source code), thành một chương
trình tương đương nhưng ở dưới dạng một ngôn ngữ máy tính mới (ngôn ngữ đích) và thường là
ngôn ngữ ở cấp thấp hơn, ví dụ như ngôn ngữ máy. Chương trình mới được dịch này được gọi mã
đối tượng (object code).
Trình biên dịch đầu tiên cho “C with classes” là Cfront, được xây dựng từ một trình biên dịch C
có tên là CPre. Nhiệm vụ chính của nó là dịch mã nguồn của “C with classes” ra mã nguồn của
C thông thường. Một điểm thú vị của trình biên dịch này là nó được viết phần lớn bằng “C with
classes”, điều này làm cho nó trở thành một trình biên dịch tự lập (self-hosting compiler ) có nghĩa
là trình biên dịch có thể tự biên dịch được chính nó. Cfront bị ngừng phát triển từ năm 1993 do
những khó khăn trong việc tích hợp các tính năng mới vào trong nó, dù sao đi nữa thì nó vẫn có
những đóng góp quan trọng cho việc xây dựng các trình biên dịch sau này.
Ngày nay, chúng ta không chỉ có một mà là nhiều trình biên dịch C++ khác nhau (xem Hình 1.3),
một số trình biên dịch thông dụng nhất hiện nay có thể kể đến chẳng hạn như:
• Microsoft C++ : trình biên dịch của hãng Microsoft, thường được tích hợp trong gói Visual
Studio
http://msdn.microsoft.com/en-us/vstudio/
• Intel C++ : trình biên dịch của hãng Intel
https://software.intel.com/en-us/c-compilers
• GCC (GNU compiler collection): tập hợp các trình biên dịch GNU, không chỉ có trình biên
dịch cho C++ mà còn có cả cho các ngôn ngữ khác như Fortran, Java, Ada,...
https://gcc.gnu.org/
• C++Builder: được phát triển từ trình biên dịch Borland C++ của hãng Borland
http://www.embarcadero.com/products/cbuilder
• Clang: trình biên dịch có sử dụng các thư viện LLVM (Low Level Virtual Machine)6 và được
phát triển bởi Apple cùng các công ty như Google, Sony, Intel,...
http://clang.llvm.org/
• Xem thêm thông tin về các trình biên dịch khác tại
http://en.wikipedia.org/wiki/Category:C%2B%2B_compilers
pháp cho nhiều bài toán lập trình cụ thể, nhằm làm giảm lượng mã nguồn cần viết, tăng khả năng tận dụng chương
trình được viết và làm cho chương trình có tính khả chuyển cao
6
LLVM là một dự án cung cấp cơ sở hạ tầng cho trình biên dịch (compiler infrastructure) được thiết kế để tối
ưu thời gian biên dịch (compile-time), thời gian liên kết (link-time), thời gian chạy (run-time) và thời gian nghỉ
(idle-time) cho chương trình. Các thư viện trong LLVM được viết bằng C++ và có thể được sử dụng cho nhiều ngôn
ngữ khác nhau chẳng hạn như Python, Fortran, Lisp, Ada, Ruby, Haskell,...
13 CHƯƠNG 1. GIỚI THIỆU NGÔN NGỮ LẬP TRÌNH C++

Hình 1.3: Bảng liệt kê một số trình biên dịch thông dụng

1.4.2 Các trình thông dịch


Bên cạnh các trình biên dịch, chúng ta còn sử dụng cả các trình thông dịch (interpreter ) cho C++.
Các trình thông dịch sẽ biên dịch mã nguồn theo từng phân đoạn, sau đó thực thi các đoạn mã đã
được biên dịch. Điều này khác hoàn toàn so với các trình biên dịch (biên dịch hoàn toàn mã nguồn
rồi mới thực thi chương trình). Các trình thông dịch C++ có thể kể đến như:
• UnderC: trình thông dịch gọn nhẹ, có thể tương thích phần lớn chuẩn ISO
http://home.mweb.co.za/sd/sdonovan/underc.html
• Cint: trình thông dịch được tạo bởi Masa Goto và được tích hợp trong ROOT
http://root.cern.ch/drupal/content/cint
• Cling: trình thông dịch được xây dựng dựa trên cơ sở các thư viện của LLVM và Clang, được
tích hợp trong ROOT6 thay thế cho Cint
http://root.cern.ch/drupal/content/cling

1.5 Hướng dẫn cài đặt và sử dụng một số trình biên dịch C++
Phần này sẽ hướng dẫn các bạn cách thức cài đặt và sử dụng của 2 trong số những trình biên dịch
C++ thông dụng nhất hiện nay là VC++ và GCC.

1.5.1 Visual C++


Microsoft Visual C++ (thường được gọi là MSVC hay VC++) là một sản phẩm môi trường phát
triển tích hợp (Integrated Development Environment − IDE) của hãng Microsoft cho các ngôn ngữ
lập trình C, C++ và C++/CLI.

Cách cài đặt


Microsoft Visual C++ là một phần của bộ công cụ Microsoft Visual Studio, tuy nhiên ta chỉ cần
tải phiên bản miễn phí Visual C++ tại http://www.microsoft.com/express/Downloads là được.
1.5. Hướng dẫn cài đặt và sử dụng một số trình biên dịch C++ 14

Trong phần hướng dẫn này, phiên bản Visual C++ 2010 Express được sử dụng. Các bước thực hiện
việc cài đặt như sau:
• Sau khi download thành công file setup (vc_web.exe), double click để chạy chương trình cài
đặt và làm theo các bước hướng dẫn của chương trình.
• Sau khi cài đặt hoàn tất, Microsoft còn có yêu cầu đăng kí để được sử dụng sản phẩm. Vào
Start → All Programs, mở Microsoft Visual C++ 2010 Express, trên thanh menu nhấp vào
Help → Register Product và làm theo các hướng dẫn đăng kí.
• Sau khi cài đặt xong, chương trình sẽ tạo một file vcvars32.bat (nằm trong thư mục bin),
chung ta cần chạy file này để thiết lập các giá trị cho các biến môi trường PATH, LIB và
INCLUDE.

Cách thức biên dịch


Để biên dịch một file mã nguồn với VC++, ta thực hiện như sau
• Chọn Start → All Programs → Microsoft Visual C++ 2010 Express, ta sẽ mở được IDE của
VC++ (như Hình 1.4).
• Tạo ứng dụng bằng cách chọn File→New →Project

• Ở mục Visual C++ chọn Win32 Console Application, nhập tên của project, chọn OK rồi
Finish.
• File mã nguồn có tên gọi <project.cpp> sẽ được tạo ra, chúng ta sẽ viết mã nguồn vào trong
file này.
• Trên thanh menu Debug, chọn Debug (F5) hoặc Build (F7) để biên dịch. File thực thi tạo ra
sẽ được chứa trong thư mục <project>\Debug.

Hình 1.4: Giao diện của Visual C++ 2010 Express

Ngoài ra, việc biên dịch các file mã nguồn C++ còn có thể được thực hiện trên giao diện dòng lệnh
Command Prompt với file cl.exe, cú pháp cho lệnh biên dịch như sau
cl [ options ] < source codes >

Một số tùy chọn để điều khiển quá trình biên dịch


/Fo tạo ra tập tin đối tượng (.obj)
/Wall hiển thị tất cả thông điệp cảnh báo (warning)
15 CHƯƠNG 1. GIỚI THIỆU NGÔN NGỮ LẬP TRÌNH C++

/LD tạo ra thư viện động


/I directory thêm thư mục chứa các header cần thiết trong quá trình biên dịch
/On biên dịch với chế độ tối ưu

Ví dụ 1.1: Biên dịch và thực thi một chương trình ‘helloworld’ với CL
$ cl helloworld . cpp

Để thực thi chương trình ứng dụng helloworld.exe vừa được tạo ra, ta gõ lệnh
$ helloworld

1.5.2 GCC

Trong hầu hết các phiên bản Linux, trình biên dịch được mặc định cài đặt sẵn là GCC (GNU
Compiler Collection), đây là một bộ các trình biên dịch có khả năng biên dịch nhiều ngôn ngữ
khác nhau như C (gcc), C++ (g++), Fortran (gfortran),...

Tên gốc của GCC là GNU C Compiler do ban đầu nó chỉ hỗ trợ dịch ngôn ngữ lập trình C. Phiên
bản đầu tiên GCC 1.0 được phát hành vào năm 1987, sau đó được mở rộng hỗ trợ dịch C++ vào
tháng 12 cùng năm đó. Sau đó, GCC được phát triển cho các ngôn ngữ lập trình Fortran, Pascal,
Objective C, Java, and Ada,... Bảng dưới trình bày một số trình biên dịch thông dụng nhất trong
GCC.
Ngôn ngữ Trình biên dịch
C gcc
C++ g++
Fortran gfortran
Pascal gpc
Java gcj
Ada gnat
D gdc
VHDL ghdl

Các bạn có thể tham khảo thêm thông tin về các trình biên dịch này tại https://gcc.gnu.org/.

Cách cài đặt trên Linux

Tuỳ thuộc vào bản phân phối Linux mà chúng ta có thể cài đặt với những lệnh khác nhau

• CentOS/RedHat
$ yum install gcc gcc - c ++

• Debian/Ubuntu
$ sudo apt - get install gcc

Các trình biên dịch GCC thường được đặt trong thư mục /usr/bin hay /usr/local/bin. Để kiểm
tra phiên bản GCC vừa được cài đặt, ta gõ lệnh
$ gcc -- version
1.5. Hướng dẫn cài đặt và sử dụng một số trình biên dịch C++ 16

Cách cài đặt trên Windows


Có nhiều cách để cài đặt GCC trên môi trường Windows, trong đó có hai cách phổ biến là cài đặt
thông qua Cygwin7 và MinGW8
• Cygwin: để cài đặt với Cygwin chúng ta tiến hành như sau
– Tải chương trình cài đặt Cygwin tại https://cygwin.com/install.html.
– Chạy chương trình cài đặt, có thể bấm Next để giữ các tuỳ chọn mặc định. Cho đến
khi hiện ra cửa sổ “Select Pakages”, di chuyển đến mục Devel, mở ra và chọn gcc-g++,
binutils; bấm Next để tiếp tục công việc cài đặt.
• MinGW: để cài đặt với MinGW chúng ta tiến hành như sau
– Tải chương trình cài đặt MinGW tại http://sourceforge.net/projects/mingw/files/
(file có tên mingw-get-setup.exe) và chạy chương trình để cài đặt.
– Trong quá trình cài đặt, lưu ý cài đặt các gói mingw32-gcc, mingw32-gcc-g++ và
mingw32-binutils.
– Nhấp chuột phải vào biểu tượng My Computer, chọn tab Advanced rồi chọn Environment
Variables. Ở mục System Variables chọn biến PATH và thêm đường dẫn tới thư mục
bin của MinGW (ví dụ C:\MinGW\bin).

Cách thức biên dịch


Cú pháp để biên dịch một chương trình C++ với GCC như sau
g ++ [ options ] < source codes >

Một số tùy chọn để điều khiển quá trình biên dịch


-c tạo ra tập tin đối tượng (.o)
-o filename tạo ra tập tin output có tên là filename
-g biên dịch ở chế độ debug (báo lỗi khi có lỗi xảy ra)
-Wall hiển thị tất cả thông điệp cảnh báo (warning)
-l directory thêm thư viện trong quá trình biên dịch
-I directory thêm thư mục chứa các header cần thiết trong quá trình biên dịch
-L directory thêm thư mục chứa các thư viện cần thiết trong quá trình biên dịch
-On biên dịch với chế độ tối ưu, n = 1,2,3 (thông thường là 2)
Trong quá trình biên dịch, nếu không có yêu cầu cụ thể, trình biên dịch sẽ mặc định tìm kiếm
các tập tin header và thư viện trong cùng thư mục với tập tin mã nguồn và các thư mục như
/usr/include hay /usr/lib.
Ví dụ 1.2: Biên dịch và thực thi một chương trình ‘helloworld’ với GCC
$ g ++ -o helloworld helloworld . cpp
$ ./ helloworld
Trong đó, helloworld.cpp là tập tin chứa mã nguồn của chương trình, tùy chỉnh -o cho ta xác định
trước tên của tập tin ứng dụng được biên dịch ra, trong trường hợp này là helloworld. Để thực thi
chương trình ứng dụng vừa được tạo ra, ta gõ lệnh
$ ./ helloworld

7
Cygwin là một bộ các công cụ cung cấp các chức năng tương tự như một môi trường Linux cho hệ điều hành
Windows.
8
MinGW (Minimalist GNU for Windows) là một bản phân phối các trình biên dịch của GNU trên môi trường
Windows.
Chương 2
Một số khái niệm C++ cơ bản

Trong chương này, một số khái niệm cơ bản về C++ sẽ được trình bày nhằm giúp cho người đọc
mới bắt đầu làm quen với ngôn ngữ sẽ dễ dàng nắm bắt hơn những kiến thức được trình bày ở
những phần sau.

2.1 Cấu trúc đơn giản của một chương trình C++
Cấu trúc phổ biến của một file C++ là như sau
# include < header >
int main ()
{
// Noi dung
}

• Các dòng bắt đầu bằng kí tự ‘#’ được gọi là các chỉ thị tiền xử lý (preprocessor directive),
dùng để báo hiệu cho trình biên dịch. Ví dụ như lệnh #include <header> sẽ báo cho trình
biên dịch sử dụng các file header trong khi biên dịch chương trình.
• Kí tự ‘//’ có tác dụng comment toàn bộ kí tự sau nó trên cùng 1 dòng, trong trường hợp ta
muốn comment nhiều hơn 1 dòng thì ta sử dụng cặp kí tự ‘/*’ và ‘*/’ để mở đầu và kết thúc.
• Hàm main() là hàm chính của chương trình, đây là nơi mà chương trình bắt đầu thi hành
các lệnh. Hàm này bắt buộc phải có khi muốn biên dịch một chương trình viết bằng C++.
Hàm main() có thể được đặt ở bất kì vị trí nào trong chương trình (đầu, cuối hoặc giữa) và
luôn là hàm được thực hiện đầu tiên khi chạy chương trình.

2.2 Các lệnh xuất nhập dữ liệu cơ bản


Các lệnh xuất nhập dữ liệu cơ bản gồm có
• cin: lệnh nhập dữ liệu (từ bàn phím)
• cout: lệnh xuất dữ liệu (lên màn hình)
• cerr/clog: lệnh xuất báo lỗi (lên màn hình), cerr không lưu trong bộ đệm (buffer ) còn clog
thì có
Ví dụ 2.1: Tạo file ví dụ helloworld.cpp

17
2.3. Biến 18

# include < iostream >

int main ()
{
std :: cout << " Hello World ! " << std :: endl ;
return 0;
}

• Toán tử << được gọi là toán tử chèn vì nó chèn dữ liệu đi sau nó vào dòng dữ liệu đứng
trước.
• Để xuống dòng ta có thể sử dụng kí tự \n hoặc tham số endl
cout << " Hello World !\ n " ;

• Để sử dụng được các lệnh cin và cout, ta cần phải khai báo thư viện iostream
# include < iostream >

Chúng ta sử dụng không gian tên std:: trước các lệnh này (sẽ bàn cụ thể hơn ở các phần sau).

2.3 Biến
Biến (variable) có thể được xem như là vùng nhớ chứa dữ liệu tạm thời trong khi thực thi chương
trình. Các dữ liệu được lưu trữ trong biến có thể là các giá trị số, chuỗi kí tự,... và các dữ liệu này
có thể thay đổi được trong quá trình thực thi chương trình.

2.3.1 Các kiểu dữ liệu của biến


Bộ nhớ của máy tính được tổ chức thành các byte1 (kí hiệu là B), đây là lượng bộ nhớ nhỏ nhất
mà chúng ta có thể quản lí. Để xây dựng các kiểu dữ liệu phức tạp hơn, chúng ta sẽ gộp nhiều byte
lại với nhau
Các kiểu biến được chia làm 4 nhóm chính là nhóm các biến kiểu kí tự (character ), kiểu số nguyên
(integer ), kiểu số thực (real ) và kiểu luận lý (boolean). Danh sách các kiểu dữ liệu trong C++ gồm có
Nhóm Kiểu dữ liệu Kích thước (B)
char 1
char16_t 2
Kí tự
char32_t 4
wchar_t kích thước lớn nhất cho kiểu char
int 2
short 2
Số nguyên2
long 4
long long 8
float 4
Số thực double 8
long double 10
Luận lý bool
Trống void 0
Con trỏ trống decltype(nullptr) 0
1
Một byte gồm 8 bit (kí hiệu b), mỗi bit có thể nhận 2 giá trị 0 hoặc 1
2
Các kiểu số nguyên có thể là số có dấu hay không dấu tuỳ theo miền giá trị mà chúng ta cần biểu diễn. Vì vậy
khi xác định một kiểu số nguyên chúng ta đặt từ khoá signed hoặc unsigned trước tên kiểu dữ liệu. Nếu ta không
chỉ rõ signed hoặc unsigned nó sẽ được coi là có dấu.
19 CHƯƠNG 2. MỘT SỐ KHÁI NIỆM C++ CƠ BẢN

Để có thể sử dụng một biến trong C++, đầu tiên chúng ta cần phải khai báo biến đó. Cách thức
khai báo một biến là ghi ra tên kiểu (vd: int, float,...) và sau đó là tên của biến.
Một tên biến được xem là hợp lệ khi nó là một chuỗi gồm các chữ cái, chữ số hoặc kí tự gạch dưới
(không chứa kí tự trống hoặc kí tự đặc biệt). Chiều dài của tên biến là không giới hạn. Một số lưu
ý khi đặt tên cho biến
• Tên biến thường bắt đầu bằng một chữ cái.
• Các tên bắt đầu bằng kí tự gạch dưới ‘_’ thường được dành cho các liên kết bên ngoài
(external link ).
• Không bao giờ bắt đầu tên biến bằng một chữ số.
• Không được đặt trùng tên biến với các từ khóa của C++ (xem danh sách tại http://en.
cppreference.com/w/cpp/keyword).
• C++ phân biệt chữ hoa và chữ thường.
Ví dụ 2.2: Khai báo hai biến kiểu nguyên x, y và một biến kiểu kí tự a
int x ;
int y ;
char a ;
Trong trường hợp nhiều biến có cùng kiểu, ta có thể khai báo
int x , y ;
Khi khai báo một biến, giá trị của nó mặc nhiên là không xác định. Trong trường hợp muốn khởi
tạo giá trị ban đầu cho biến ngay khi khai báo biến (ví dụ khởi tạo giá trị ban đầu cho biến y là
5), ta có thể sử dụng một trong những cách sau đây
int x = 5;
int x (5) ;
int x {5};
int x {}; // x = 0
int x = int () ; // x = 0

2.3.2 Phạm vi của biến


Mỗi biến khi được khai báo sẽ có các phạm vi khác nhau, dưới đây là một số loại pham vi của biến
mà chúng ta thường gặp
• Biến toàn cục (global variable) có thể được sử dụng ở bất kì đâu trong chương trình, ngay
sau khi nó được khai báo.
• Biến cục bộ (local variable) tầm hoạt động của biến bị giới hạn trong phần mã (thường là
hàm hoặc vòng lặp) mà nó được khai báo.
• Biến ngoài (external variable) không những được dùng trong một file mã nguồn mà còn trong
tất cả các file được liên kết trong chương trình (thường được khai báo với từ khóa extern3 ).
• Biến tự động (automatic variable) là các biến cục bộ mà sự tồn tại của nó kết thúc ngay khi
việc thực thi kết thúc.
• Biến tĩnh (static variable) là các biến mà tồn tại đến cuối chương trình (thường được khai
báo với từ khóa static4 ).
Ví dụ 2.3: Minh hoạ biến toàn cục và cục bộ
3
Xem Phần 10.1.2
4
Xem Phần 10.1.3
2.3. Biến 20

int g ; // bien toan cuc , duoc su dung trong toan bo file nay

int main ()
{
int x ; // bien cuc bo , chi duoc su dung trong ham main ()
return 0;
}

Ví dụ 2.4: Minh hoạ biến ngoài


Giả sử chúng ta có một file global.h có khai báo biến ngoài
// global . h
extern int g = 1;

Trong trường hợp file global.h được sử dụng trong file source.cpp, chúng ta có thể sử dụng biến
ngoài này
// source . cpp
# include < iostream >
# include " global . h "

int main ()
{
std :: cout << " Gia tri cua bien g : " << g << std :: endl ;
return 0;
}

Mặc dù biến g không được khai báo trực tiếp trong file source.cpp nhưng kết quả xuất ra vẫn là
Gia tri cua bien g : 1

Ví dụ 2.5: Minh hoạ biến tĩnh


void func () {
static int x = 0;
x = x + 1;
}

int main ()
{
func () ; // x = 1
func () ; // x = 2
func () ; // x = 3
return 0;
}

Trong ví dụ này, mặc dù chỉ được khai báo một cách cục bộ bên trong hàm func() nhưng do x là
một biến tĩnh nên nó có tác dụng trong toàn bộ chương trình, giá trị của nó không bị khởi tạo lại
mỗi khi gọi hàm func().

Ví dụ 2.6: Minh hoạ biến tự động


for ( int i = 0; i < 5; ++ i ) {
int n = 0;
std :: cout << ++ n << std :: endl ;
}

Trong ví dụ này, biến n được khởi tạo lại trong mỗi vòng lặp, do đó giá trị in ra luôn là 1.
21 CHƯƠNG 2. MỘT SỐ KHÁI NIỆM C++ CƠ BẢN

2.3.3 Lvalue và rvalue


Lvalue và rvalue là các giá trị tương ứng nằm ở bên trái và bên phải của toán tử gán (dấu ‘=’).
Các lvalue đại diện cho các đối tượng chiếm những vị trí xác định trong bộ nhớ (có địa chỉ), và
thông thường là các biến. Ngược lại, rvalue không đại diện cho các đối tượng chiếm vị trí trong bộ
nhớ. Việc hiểu rõ ý nghĩa của hai khái niệm này đôi khi giúp chúng ta tránh được những lỗi xảy
ra trong quá trình viết chương trình.
Ví dụ 2.7: Minh hoạ một số lỗi thường gặp liên quan đến lvalue và rvalue
x = 1; // OK
1 = x; // ERROR
( x + 1) = 2; // ERROR

int a = 1; // a la lvalue
int b = 1; // b la lvalue
int c = a + b ; // a va b duoc chuyen thanh rvalue , tra ve rvalue

int a ; // a la lvalue
int b ; // b la lvalue
int c = a + b ; // a va b khong chuyen duoc thanh rvalue --> ERROR

2.3.4 Định nghĩa kiểu tự động


Trong các chuẩn C++ cũ, ta cần phải định nghĩa kiểu của một đối tượng khi khai báo nó, tuy nhiên
điều này có thể không cần thực hiện trong C++11 với từ khóa auto5 , phương pháp này được gọi
là định nghĩa kiểu tự động (automatic type deduction).
Ví dụ 2.8: Minh hoạ định nghĩa kiểu tự động
auto x = 0; // x co kieu int do 0 co kieu int
auto c = 'a '; // kieu char
auto d = 0.5; // kieu double

Để lấy thông tin về kiểu của đối tượng ta sử dụng từ khóa decltype
auto m = 100; // m co kieu la int
decltype (100) n = 100; // gan kieu int ( la kieu cua 100) cho n

2.4 Hằng
Hằng (constant) là bất kì một biểu thức nào mang một giá trị cố định.

2.4.1 Các loại hằng


Các loại hằng thông dụng chẳng hạn như
• Các số
123
0.545
0 x4b
3.0 e -12

5
Từ khóa auto không phải là từ khóa mới, nó được dùng trong những chuẩn C/C++ trước đó nhằm để chỉ rằng
các đối tượng được tạo ra trong vùng nhớ động và sẽ tự động được giải phóng khỏi bộ nhớ chương trình khi không
dùng đến nữa, C++11 đã sử dụng lại từ khóa này với ý nghĩa hoàn toàn mới.
2.5. Toán tử 22

• Kí tự, chuỗi kí tự
'a '
" Good morning ! "

• Mã điều khiển: \n (xuống dòng), \b (backspace), \r (lùi về đầu dòng), \t (tab), \v (căn thẳng
theo chiều dọc), \f (sang trang), \a (kêu bíp), \’ (dấu nháy đơn), \” (dấu nháy kép),...

2.4.2 Cách định nghĩa hằng

Để định nghĩa các hằng, ta có thể

• Sử dụng chỉ thị tiền xử lý #define. Khi khai báo hằng bằng #define, chương trình sẽ thay
thế các tên hằng bằng giá trị của nó tại bất kì chỗ nào chúng xuất hiện. Các hằng số được
định nghĩa theo cách này ược coi là các hằng số macro.

• Sử dụng tiền tố const và khai báo các hằng với một kiểu xác định như là làm với một biến.

Ví dụ 2.9: Minh hoạ sử dụng chỉ thị tiền xử lý #define


# define PI 3.14159265
float r = 2.0; // khai bao ban kinh r
float c = 2 * PI * r ; // tinh chu vi

Ví dụ 2.10: Minh hoạ sử dụng từ khoá const


const float PI = 3.14159265;
const ZIP = 12440;

Trong trường hợp kiểu dữ liệu không được chỉ rõ trình biên dịch sẽ coi nó là kiểu int.

2.5 Toán tử
Dưới dây là một số các toán tử thông dụng

Toán tử gán (=) dùng để gán giá trị cho biến

Ví dụ 2.11: Minh hoạ toán tử gán


int x;
x = 1; // gan gia tri 1 cho bien x
int y = 2;
x = y ; // gan gia tri cua bien y cho bien x

Các toán tử số học dùng để tính toán các giá trị, gồm có: +, −, *, /, % (chia lấy dư)

Ví dụ 2.12: Minh hoạ các toán tử số học


int a = 10 , b = 3;
int c = a - b ; // c = 7
int d = a / b ; // d = 3
int e = a % b ; // e = 1
23 CHƯƠNG 2. MỘT SỐ KHÁI NIỆM C++ CƠ BẢN

Các toán tử tăng giảm tăng hoặc giảm giá trị của biến đi 1 đơn vị, gồm có
++ tăng lên 1 đơn vị
- - giảm đi 1 đơn vị
Ví dụ 2.13: Minh hoạ các toán tử tăng giảm
int a = 5;
a ++; // tuong duong voi a = a + 1
Lưu ý rằng có sự khác biệt giữa việc đặt toán tử tăng giảm phía trước hoặc phía sau của biến.
Trong trường hợp đặt phía trước, giá trị của biến sẽ được tăng/giảm trước khi thực hiên câu lệnh,
còn trường hợp đặt phía sau thì giá trị của biến sẽ được tăng/giảm sau khi hoàn tất câu lệnh, ví
dụ như
int a = 5;
int b = a ++; // a = 5 , b = 4
int b = ++ a ; // a = 5 , b = 5

Các toán tử gán phức hợp gán giá trị cho biến cùng lúc với thực hiện một toán tử số học lên
nó, tương dương với việc gán giá trị của phép toán lên toán hạng đầu tiên, các toán tử này gồm có
+= tăng lên một lượng
-= giảm đi một lượng
*= nhân thêm một lượng
/= chia cho một lượng
Ví dụ 2.14: Minh hoạ các toán tử gán phức hợp
int a = 10 , b = 3;
a -= 2; // tuong duong voi a = a - 2
b *= a +1; // tuong duong voi b = b * ( a +1)

Các toán tử so sánh so sánh giá trị giữa hai biểu thức với nhau, gồm có
== so sánh bằng
> so sánh lớn hơn
< so sánh nhỏ hơn
>= so sánh lớn hơn hoặc bằng
<= so sánh nhỏ hơn hoặc bằng
!= so sánh khác (không bằng)
Ví dụ 2.15: Minh hoạ các toán tử so sánh
int a = 7;
( a == 7) // true
( a < 8) // true
( a == 1) // false
( a != 3) // true
( a *1 >= 7) // true

Các toán tử logic thực hiện các phép toán logic, gồm có: && (and ), || (or ), ! (not)
Ví dụ 2.16: Minh hoạ các toán tử logic
int a = 7;
( a == 7) // true
!( a == 7) // false
( a == 7 && 0) // false
2.6. Các hàm toán học 24

( a == 7 || 0) // true

Toán tử điều kiện (?) có cấu trúc như sau


điều kiện ? kết quả 1 : kết quả 2
(nếu điều kiện cho giá trị true thì trả về kết quả 1, còn nếu false thì trả về kết quả 2)
Ví dụ 2.17: Minh hoạ toán tử điều kiện
c = ( a == b ? 2 : 3) // neu a == b thi c = 2 , con khong thi c = 3

Toán tử dấu phẩy (,) dùng để phân chia hai hay nhiều biểu thức trong trường hợp cần sử dụng
duy nhất một biểu thức
Ví dụ 2.18: Minh hoạ toán tử dấu phẩy
int a , b = 3;
int c = ( a = 1 , a + b ) ; // dau tien gan a = 1 , roi sau do c = a + b

Các toán tử thao tác bit thực hiện thao tác trên các bit, gồm có: & (logical and ), | (logical
or ), ∧ (logical xor ), ∼ (logical not), >> (dịch bit sang phải), << (dịch bit sang trái)
Ví dụ 2.19: Minh hoạ toán tử thao tác bit
int a = 6; // 6 = 0000 0110
int b = 10; // 10 = 0000 1010
int c = a & b ; // 2 = 0000 0010

Toán tử chuyển đổi kiểu dữ liệu sử dụng cặp ngoặc tròn ()


Ví dụ 2.20: Minh hoạ toán tử chuyển đổi kiểu dữ liệu
int i ;
float f = 3.14;
i = ( int ) f ;
// hoac
i = int ( f ) ;

Toán tử kích thước dữ liệu sizeof()


Ví dụ 2.21: Minh hoạ toán tử kích thước dữ liệu
int i ;
sizeof ( i ) ;

2.6 Các hàm toán học


Danh sách các hàm toán học thông dụng có thể được xem ở đây http://en.cppreference.com/
w/cpp/numeric/math.
Để sử dụng các hàm này, ta cần khai báo thư viện cmath (hoặc math.h)6 . Dưới đây là một số
hàm thông dụng
6
Lý do vì sao lại có tới 2 thư viện để lựa chọn sẽ được giải thích trong Phần 9.2.
25 CHƯƠNG 2. MỘT SỐ KHÁI NIỆM C++ CƠ BẢN

sqrt(x) hàm căn bậc hai


pow(x,a) hàm luỹ thừa xa
exp(x) hàm ex
abs(x)/fabs(x) hàm lấy giá trị tuyệt đối
log(x)/log10(x) hàm logarit tự nhiên và logarit cơ số 10
sin(x)/cos(x),... các hàm sin(), cos(),...
ceil(x)/floor(x) hàm làm tròn lên/xuống
Ví dụ 2.22: Minh hoạ các hàm toán học
float x = 4.; // x = 4.0
float y = sqrt ( x ) ; // y = 2.0
y = pow (x ,3) ; // y = 64.0

2.7 Các chỉ thị tiền xử lý


Bộ tiền xử lý (preprocessor ) được ra đời do sự hạn chế của bộ nhớ máy tính trước đây, không thể
chứa hết toàn bộ chương trình nguồn để dịch, do vậy một số ngôn ngữ thế hệ thứ 3 như C/C++
đã chia giai đoạn xử lý ra làm 2 phần: giai đoạn tiền xử lý sẽ xử lý các file nguồn, ghi nhận các
tùy chọn (option) cho việc biên dịch; và giai đoạn xử lý sẽ thực hiện việc biên dịch mã nguồn.
Các chỉ thị tiền xử lý trong C/C++ đề bắt đầu bằng kí tự ‘#’, các chỉ thị này không được xem
như là dòng lệnh nên không có dấu ‘;’ ở cuối dòng. Trong phần trên ta đã làm quen với các chỉ thị
#include và #define, phần này sẽ trình bày chi tiết hơn về một số chỉ thị thông dụng

Các chỉ thị định nghĩa (#define và #undef) dùng để định nghĩa hay hủy bỏ định nghĩa các
macro.
Ví dụ 2.23: Minh hoạ các chỉ thị định nghĩa
# define SIZE 100
int table1 [ SIZE ]; // tuong duong voi khai bao : int table1 [100]
# undef SIZE // bo dinh nghia macro SIZE
# define SIZE 200 // dinh nghia lai macro SIZE
int table2 [ SIZE ]; // table2 [200]
Ta cũng có thể định nghĩa một macro hàm có tham số
# define getmax (a , b ) a > b ? a : b

Trong định nghĩa macro hàm, toán tử # dùng để báo rằng tham số sẽ được thay thế bởi một chuỗi
# define str ( x ) # x
std :: cout << str ( hello ) ; // tuong duong std :: cout << " hello ";
và toán tử ## dùng để nối hai tham số (không có khoảng trắng ở giữa)
# define glue (a , b ) a ## b
glue (c , out ) << " hello " ; // tuong duong cout << " hello ";

Các chỉ thị điều kiện (#ifdef, #ifndef, #if, #endif, #else, #elif) cho phép bao gồm hoặc
loại bỏ một phần các dòng lệnh nếu những điều kiện được thỏa.
Ví dụ 2.24: Minh hoạ các chỉ thị điều kiện
# ifdef SIZE
int table [ SIZE ]; // neu macro SIZE da duoc dinh nghia thi khai bao table [
SIZE ]
# endif
2.7. Các chỉ thị tiền xử lý 26

# ifndef SIZE
# define SIZE 100 // neu macro SIZE chua duoc dinh nghia thi dinh nghia no
# endif

Các chỉ thị #ifdef, #ifndef có thể được thay thế bởi các toán tử đặc biệt defined và !defined
# if defined ROW_SIZE
# define COL_SIZE ROW_SIZE
# elif ! defined BUFFER_SIZE
# define COL_SIZE 128
# else
# define COL_SIZE BUFFER_SIZE
# endif

Chỉ thị điều khiển dòng (#line) khi trình biên dịch phát hiện ra lỗi trong quá trình biên dịch,
nó sẽ hiển thị thông báo lỗi với tham chiếu đến dòng lệnh gây lỗi trong file. Chỉ thị #line sẽ giúp
ta điều khiển thông tin này.
Ví dụ 2.25: Minh hoạ chỉ thị điều khiển dòng
int main () { // Dong 1
# line 1 " main . cpp : error detected " // Dong 2
int a = 10; // Dong 3
int b 20; // Dong 4
return 0; // Dong 5
} // Dong 6
Trong ví dụ trên ta thấy lỗi xảy ra tại dòng 4, chương trình sẽ hiện câu thông báo lỗi như định
nghĩa trong chỉ thị #line và vị trí dòng có lỗi là 2 (do ta cung cấp giá trị 1 cho chỉ thị #line, giá
trị 1 nay sẽ được gán cho dòng tiếp theo sau nó như là vị trí tham chiếu, dòng xảy ra lỗi sẽ ở vị trí
tham chiếu số 2).

Chỉ thị lỗi (#error) dùng để thoát khỏi quá trình biên dịch khi nó được tìm thấy.
Ví dụ 2.26: Minh hoạ chỉ thị lỗi
# ifndef __cplusplus
# error A C ++ compiler is required !
# endif
Trình biên dịch sẽ in ra báo lỗi và thoát khỏi quá trình biên dịch nếu như macro __cplusplus
không đươc định nghĩa (macro này được định nghĩa một cách mặc định bởi tất cả các trình biên
dịch C++).

Chỉ thị tùy biến (#pragma) cho trình biên dịch biết cách dịch chương trình theo một số “tùy
chọn” đặc biệt (tùy thuộc vào từng nền tảng hệ điều hành và trình biên dịch). Nếu trình biên dịch
không hỗ trợ các tùy chọn đó nó sẽ bỏ qua (không gây lỗi biên dịch). Dưới đây là một số ví dụ cho
chỉ thị #pragma
Ví dụ 2.27: Minh hoạ chỉ thị lỗi
Báo cho trình biên dịch include các file header 1 lần duy nhất, cho chúng được khai báo bao nhiêu
lần đi nữa
# pragma once
Loại bỏ cảnh báo (số 4018) khi biên dịch
# pragma warning ( disable : 4018 )
Thêm các thư viện vào danh sách các thư viện phụ thuộc (dependency) khi biên dịch
27 CHƯƠNG 2. MỘT SỐ KHÁI NIỆM C++ CƠ BẢN

# pragma comment ( lib , " kernel32 " )


# pragma comment ( lib , " user32 " )

2.8 Không gian tên


Không gian tên (namespace) cho phép chúng ta gộp một nhóm các lớp, các đối tượng toàn cục và
các hàm dưới một cái tên, hoặc được dùng để phân chia phạm vi hoạt động của các đối tượng. Cú
pháp của nó như sau
namespace <không gian tên> {
... khai báo ...
}
Giả sử như ta có hai hàm có cùng tên func() trong một chương trình, để giúp cho trình biên dịch
phân biệt được hàm nào được gọi ta sử dụng không gian tên như sau
// Khong gian ten dau tien
namespace first {
void func () {
std :: cout << " Inside first space " << std :: endl ;
}
}

// Khong gian ten thu hai


namespace second {
void func () {
std :: cout << " Inside second space " << std :: endl ;
}
}

Để gọi hàm với không gian tên tương ứng ta sử dụng kí hiệu ‘::’
int main ()
{
// Goi ham voi khong gian ten thu nhat
first :: func () ;
// Goi ham voi khong gian ten thu hai
second :: func () ;
return 0;
}

Để có thể truy xuất trực tiếp mà không cần đặt thông qua không gian tên, ta sử dụng từ khóa
using
int main ()
{
using namespace first ;
// Goi ham voi khong gian ten thu nhat
func () ;
// Goi ham voi khong gian ten thu hai
second :: func () ;
return 0;
}

Chúng ta cũng có thể định nghĩa lại các không gian tên đã khai báo, chẳng hạn như
namespace dau_tien = first ;
2.9. Thư viện chuẩn 28

Tất cả định nghĩa của các lớp, đối tượng và hàm của thư viện chuẩn (STL) đều được định nghĩa
trong không gian tên std, chẳng hạn như các lệnh cin và cout mà ta nói ở trên, nếu viết đầy đủ
thì sẽ là
std :: cout << " Nhap ten cua ban : " ;
std :: cin >> ten ;
std :: cout << " Xin chao " << ten << " ! " << std :: endl ;
hoặc ta cũng có thể lược đi bằng cách sử dụng từ khóa using
using namespace std ;
cout << " Nhap ten cua ban : " ;
cin >> ten ;
cout << " Xin chao " << ten << " ! " << endl ;

2.9 Thư viện chuẩn


Thư viện chuẩn (Standard Template Library − STL) được xây dựng đầu tiên bởi Alexander
Stepanov (1979) với mục đích phát triển phương pháp lập trình tổng quát. Thư viện đầu tiên
được xây dựng với ngôn ngữ lập trình Ada bởi Stepanov và Musser năm 1987, tuy nhiên ngôn ngữ
Ada lại không được phát triển mạnh nên họ đã chuyển sang ngôn ngữ C++. Với sự giúp đỡ của
Meng Lee và Andrew Koenig, bộ thư viện chuẩn đã được hoàn thiện và đưa vào chuẩn ANSI/ISO
C++ từ năm 1994.
Một số thành phần chính của STL gồm có
• Container : là các đối tượng chứa những đối tượng khác (cấu trúc dữ liệu của template), vd:
vector, set, map,...
• Iterator : giống con trỏ, dùng để truy nhập các phần tử dữ liệu của các container.
• Algorithm: các thuật toán để thao tác dữ liệu như tìm kiếm (find), sắp xếp (sort),...
• Functor : các toán tử hàm.
• Adapter : các bộ thương thích container phục vụ cho những nhu cầu đặc biệt.
• Ultility: các ứng dụng của STL, gồm có hai phần: các ứng dụng cho ngôn ngữ (lập trình) và
ứng dụng đa mục đích.
• Numeric: bộ các hàm và kiểu dữ liệu phục vụ cho việc tính toán, xử lý các mảng số.
• String: thư viện chuỗi.
• Stream: các lớp hỗ trợ các phép toán cho dòng xuất nhập.
• Thư viện C : các thư viện ngôn ngữ C.
• ...
Ngoại trừ lớp string, tất cả các thành phần còn lại của STL đều dưới dạng các template.
Lưu ý: các file thư viện chuẩn của C++ đều không có phần mở rộng .h, ví dụ
# include < string > // thu vien chuan C ++
# include < string .h > // thu vien chuan C

Các thư viện chuẩn C được đưa vào trong bộ thư viện chuẩn C++ STL với kí tự ’c’ ở đầu
# include < cstring > // thu vien chuan C ++ cua cac ham C
Chương 3
Cấu trúc điều khiển

Khi xây dựng chương trình, đôi lúc ta cần thực hiện một nhóm các lệnh tương ứng với một điều
kiện nào đó hay là lặp lại nhóm lệnh một số lần nhất định. Trong trường hợp đó, ta sẽ sử dụng cấu
trúc điều khiển. Nhóm các lệnh (nhiều hơn 1) sẽ được gộp lại với nhau bằng cặp ngoặc nhọn { }

3.1 Cấu trúc điều khiển if...else...


Cấu trúc này có dạng
if(điều kiện) {
các lệnh thực thi 1
}
hoặc
if(điều kiện) {
các lệnh thực thi 1
} else {
các lệnh thực thi 2
}
Nếu điều kiện trả về giá trị true thì các lệnh thực thi 1 sẽ được thực hiện. Trong trường hợp có
thêm tuỳ chọn else thì các lệnh thực thi 2 sẽ được thực hiện nếu giá trị trả về của điều kiện là
false (xem Hình 3.1).
Ví dụ 3.1: Kiểm tra xem x là số chẵn hay số lẻ
int x = 5;
if ( x %2 == 0) {
std :: cout << " So chan " << std :: endl ;
} else {
std :: cout << " So le " << std :: endl ;
}

Ngoài ra, chúng ta cũng có thể sử dụng nhiều khối lệnh if...else... liên tiếp nhau.
Ví dụ 3.2: Thông báo học lực của học sinh
float diem = 6.5;
if ( diem >= 8.0) {
std :: cout << " Hoc sinh gioi " << std :: endl ;
} else if ( diem >= 6.5) {
std :: cout << " Hoc sinh kha " << std :: endl ;
} else if ( diem >= 5.0) {

29
3.2. Cấu trúc lựa chọn switch 30

Hình 3.1: Lưu đồ của cấu trúc điều khiển if...else...

std :: cout << " Hoc sinh trung binh " << std :: endl ;
} else {
std :: cout << " Hoc sinh kem " << std :: endl ;
}

3.2 Cấu trúc lựa chọn switch


Cấu trúc này có dạng
switch (biểu thức) {
case <giá trị 1>:
các lệnh;
break;
case <giá trị 2>:
các lệnh;
break;
...
default:
các lệnh;
}
Với cấu trúc switch, giá trị thu được từ biểu thức sẽ được so sánh với các giá trị 1, 2,...; với mỗi
giá trị được kiểm tra cho kết quả true, các lệnh tương ứng sẽ được thực hiện (xem Hình 3.2). Nếu
không có giá trị nào so sánh cho kết quả true, chương trình sẽ thực hiện khối lệnh default (nằm
ở cuối cấu trúc switch).
Một số lưu ý khi sử dụng cấu trúc switch
• Chỉ dùng so sánh giá trị của biểu thức với các hằng, không so sánh được với các biến.
• Trong trường hợp cẩn kiểm tra với các khoảng giá trị, chúng ta nên sử dụng chuỗi các lệnh
if..else....
• Trong trường hợp không có lệnh break ở cuối các khối lệnh case, chương trình sẽ tiếp tục
thực hiện các khối lệnh tiếp theo.
Ví dụ 3.3: Kiểm tra xem x là số chẵn hay số lẻ
31 CHƯƠNG 3. CẤU TRÚC ĐIỀU KHIỂN

Hình 3.2: Lưu đồ của cấu trúc lựa chọn switch

int x = 5;
switch ( x %2) {
case 0:
std :: cout << " So chan " << std :: endl ;
break ;
case 1:
std :: cout << " So le " << std :: endl ;
break ;
default :
std :: cout << " Khong xac dinh " << std :: endl ;
}

3.3 Cấu trúc lặp for


Cấu trúc này có dạng
for(giá trị ban đầu; điều kiện; bước nhảy) {
các lệnh thực thi
}
Quy trình của vòng lặp này như sau (xem Hình 3.3)
• Ban đầu, biểu thức giá trị ban đầu sẽ được thực hiện duy nhất một lần đều tiên, biểu thức
này sẽ gán các giá trị ban đầu cho các biến điều khiển vòng lặp.
• Sau đó, biểu thức điều kiện sẽ được đánh giá, nếu giá trị trả về là true thì các lệnh thực thi
bên trong vòng lặp sẽ được thực hiện.
• Sau khi thực hiện các lệnh bên trong vòng lặp, lệnh bước nhảy sẽ được thực hiện và cập nhật
giá trị mới cho các biến điều khiển vòng lặp.
• Biểu thức điều kiện lại được đánh giá một lần nữa, nếu giá trị trả về vẫn là true, các lênh
bên trong vòng lặp tiếp tục được thực hiện một lần nữa. Cứ như vậy cho đến khi giá trị trả
về của điều kiện là false, vòng lặp sẽ ngừng lại.
3.3. Cấu trúc lặp for 32

Hình 3.3: Lưu đồ của cấu trúc lặp for

Ví dụ 3.4: In ra các số tăng dần từ 0 đến 4


for ( int i =0; i < 5; ++ i ) {
std :: cout << i << std :: endl ;
}

Ví dụ 3.5: Vòng lặp for cho hai biến


for ( int i = 0 , j = 10; i != j ; ++ i , --j ) {
std :: cout << " i = " << i << " \ t j = " << j << std :: endl ;
}

Trong trường hợp chúng ta muốn tạo ra một vòng lặp vô hạn với for, chúng ta có thể viết như sau
(lưu ý là trong cặp ngoặc tròn luôn tồn tại hai dấu ‘;’ cho dù có biểu thức hay không)
for (;;) {
// cac lenh trong vong lap
}

Với trường hợp sử dụng vòng lặp vô hạn, chúng ta cần thêm vào bên trong vòng lặp ít nhất một
điều kiện để thoát (ngưng) vòng lặp (xem Phần 3.5), nếu không vòng lặp sẽ không thể ngừng và
gây ra lỗi cho chương trình.

Theo chuẩn C++11, chúng ta còn có thể tạo vòng lặp for theo danh sách
for ( int i : {1 , 3 , 6 , 9}) {
// cac lenh trong vong lap
}
33 CHƯƠNG 3. CẤU TRÚC ĐIỀU KHIỂN

3.4 Cấu trúc lặp while


Cấu trúc vòng lặp while có 2 dạng (xem Hình 3.4)
while(điều kiện) {
các lệnh thực thi
}
hoặc
do {
các lệnh thực thi
} while(điều kiện);
Với vòng lặp while, điều kiện sẽ được kiểm tra, nếu kết quả trả về là true thì các lệnh thực thi
bên trong vòng lặp sẽ được thực hiện. Sau mỗi lần lặp, điều kiện sẽ được kiểm tra lại, vòng lặp sẽ
ngừng khi giá trị trả về của điều kiện là false.
Bên cạnh dạng vòng lặp while thông thường, chúng ta còn có dạng vòng lặp do...while.... Điểm
khác biệt giữa hai dạng này là trong dạng do...while... các lệnh bên trong vòng lặp sẽ được thực
thi một lần trước khi kiểm tra điều kiện (lưu ý rằng có dấu ‘;’ ở cuối vòng lặp do...while...).

Hình 3.4: Lưu đồ của cấu trúc lặp while

Ví dụ 3.6: In ra các số giảm dần từ 5 đến 0


int x = 5;
while ( x > 0) {
std :: cout << x - - << std :: endl ;
}

hoặc ta có thể viết như sau


int x = 5;
do {
std :: cout << x - - << std :: endl ;
} while ( x > 0) ;

Trong trường hợp chúng ta muốn tạo ra một vòng lặp vô hạn với while, chúng ta có thể gán cho
giá trị điều kiện là 1
3.5. Các lệnh cho vòng lặp 34

while (1) {
// cac lenh trong vong lap
}
hoặc true
while ( true ) {
// cac lenh trong vong lap
}
Cũng tương tự như với trường hợp vòng lặp vô hạn for, chúng ta cần thêm vào bên trong vòng
lặp một điều kiện để thoát (xem Phần 3.5).

3.5 Các lệnh cho vòng lặp


Dưới đây là một số lệnh cho vòng lặp và cấu trúc điều khiển
• Lệnh continue được dùng để kết thúc lần lặp hiện tại và chuyển sang lần lặp tiếp theo
• Lệnh break được dùng để kết thúc hoàn toàn vòng lặp.
• Lệnh goto được dùng để nhảy tới một vị trí nào đó trong chương trình, vị trí này được đánh
dấu bởi ‘nhãn’ (label ). Khi sử dụng lệnh goto, người dùng cần cẩn thận vì có thể gây ra các
lỗi không đáng có, chỉ nên giới hạn việc sử dụng nó trong cùng 1 khối lệnh, đặc biệt là khi
có sự tham gia của các biến cục bộ.
Ví dụ 3.7: Minh hoạ lệnh continue
for ( int i = 0; i < 5; ++ i ) {
if ( i == 3) continue ;
std :: cout << i << std :: endl ;
}
Trong ví dụ này, các số được in ra sẽ là 0, 1, 2, 4 (không có số 3).
Ví dụ 3.8: Minh hoạ lệnh break
for ( int i = 0; i < 5; ++ i ) {
if ( i == 3) break ;
std :: cout << i << std :: endl ;
}
Trong ví dụ này, các số được in ra sẽ là 0, 1, 2 (không có từ 3 trở đi).
Thông qua 2 ví dụ ở trên, chúng ta có thể thấy được là khi giá trị i = 3, đối với lệnh continue
chương trình sẽ bỏ qua không xuất ra giá trị của i và chuyển tới lần lặp tiếp theo. Trong khi đó,
đối với lệnh break, chương trình sẽ thoát ra khỏi vòng lặp, do đó các giá trị i từ 3 trở đi sẽ không
được xuất ra màn hình.
Ví dụ 3.9: Lệnh break với vòng lặp vô hạn
Như đã nói ở phần trên, đối với các vòng lặp vô hạn chúng ta cần đặt điều kiện để thoát ra khỏi
vòng lặp. Trong trường hợp này, chúng ta có thể sử dụng lệnh break
int x = 5;
while (1) {
std :: cout << x - - << std :: endl ;
if ( x <= 2) break ;
}
Trong ví dụ này, các số được in ra sẽ là 5, 4 và 3 (không có từ 2 trở đi).
Ví dụ 3.10: Minh hoạ lệnh goto
35 CHƯƠNG 3. CẤU TRÚC ĐIỀU KHIỂN

for ( int i = 0; i < 5; ++ i ) {


std :: cout << i ;
if ( i == 3) goto mylabel ;
std :: cout << " != 3 " ;
mylabel :
std :: cout << std :: endl ;
}
Trong ví dụ này, với các giá trị i 6= 3, chương trình sẽ in thêm dòng chữ “! = 3”. Còn khi giá trị
i = 3, lệnh goto sẽ bỏ qua dòng in ra đó để chuyển tới lệnh xuống hàng kế tiếp.
3.5. Các lệnh cho vòng lặp 36
Chương 4
Mảng và chuỗi

Trong chương trước, chúng ta đã làm quen với các kiểu dữ liệu cơ bản của một biến (phần tử).
Tiếp theo sau, chương này được dùng để trình bày các vấn đề liên quan đến các thao tác xử lý một
tập hợp các phần tử có cùng kiểu dữ liệu, tập hợp này được gọi là mảng. Trong trường hợp mảng
chỉ chứa toàn dữ liệu dạng kí tự, nó được gọi là một chuỗi (kí tự).

4.1 Mảng
Mảng (array) là một dãy các phần tử có cùng kiểu được đặt liên tiếp trong bộ nhớ và có thể truy
xuất đến từng phần tử bằng cách thêm một chỉ số vào sau tên của mảng.
Hình 4.1 minh hoạ cấu trúc cơ bản của một mảng. Lưu ý rằng chỉ số của phần tử đầu tiên trong
mảng luôn là 0, nếu mảng có kích thước là N thì các phần tử sẽ có chỉ số từ 0 đến (N−1).

Hình 4.1: Minh hoạ cấu trúc cơ bản của mang[6] chứa các phần tử là số nguyên. Mảng này gồm
có 6 phần tử, mỗi phần tử trong mảng chứa một giá trị nguyên khác nhau, phần tử đầu tiên có chỉ
số là 0, các phần tử kế tiếp có chỉ số từ 1 đến 5.

4.1.1 Khai báo mảng


Cách thức khai báo mảng như sau
<kiểu biến> <tên mảng> [kích thước]
hoặc
<kiểu biến> *<tên mảng>

Ví dụ 4.1: Minh hoạ khai báo mảng


// Khai bao mang so nguyen A co kich thuoc 100
int A [100];
// Khai bao mang so nguyen A co kich thuoc khong xac dinh
int * A ;

37
4.1. Mảng 38

Ta cũng có thể khởi tạo giá trị của các phần tử trong mảng ngay khi khai báo bằng những cách
sau
int A [10] = {1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10};
int A [10] = {} // tat ca cac phan tu deu bang 0
int A [10] = {1 , 2} // hai phan tu dau mang gia tri 1 va 2 , cac phan tu sau
deu bang 0
int A [] = {1 , 2} // mang co kich thuoc la 2

4.1.2 Thao tác với các phần tử trong mảng


Để gán hoặc truy xuất giá trị của một phần tử trong mảng, ta chỉ cần gọi tên mảng cùng với chỉ
số của phần tử đó.
Ví dụ 4.2: Minh hoạ gán và truy xuất phần tử trong mảng
# include < iostream >

int main ()
{
float A [3];
A [0] = 1.5;
A [1] = -2.9;
A [2] = 3.0;

// xuat gia tri cac phan tu trong mang


for ( int i = 0; i < 3; ++ i )
std :: cout << " Phan tu thu " << i +1 << " co gia tri la " << A [ i ] << std
:: endl ;

return 0;
}

Với vòng lặp for, chúng ta có thể lặp theo khoảng giá trị (range-based loop) đối với mảng
float A [3];
A [0] = 1.5;
A [1] = -2.9;
A [2] = 3.0;

for ( auto n : A )
std :: cout << n << std :: endl ;

4.1.3 Mảng nhiều chiều


Cách thức khai báo mảng nhiều chiều trong C++ như sau
<kiểu biến> <tên mảng> [kích thước 1] [kích thước 2] ... [kích thước N]

Ví dụ 4.3: Minh hoạ mảng nhiều chiều


int A [3][10];
double x [20][60][60];

Dạng đơn giản nhất của mảng nhiều chiều là mảng hai chiều, thực chất đây là một dãy các mảng
một chiều. Chúng ta cũng có thể xem mảng hai chiều như là một bảng có các hàng và cột (xem
Hình 4.2).
39 CHƯƠNG 4. MẢNG VÀ CHUỖI

Hình 4.2: Minh hoạ cấu trúc cơ bản của mộ mảng 2 chiều A[3][4] chứa các phần tử là số nguyên,
chỉ số đầu tương ứng với chỉ số dòng, chỉ số sau tương ứng với chỉ số cột.

Ta có thể khởi tạo giá trị cho mảng 2 chiều với một trong những cách sau
int A [3][4] = {
{2 , -1 , 3 , 14} ,
{4 , 51 , 26 , 73} ,
{ -48 , 39 , 100 , 11}
};

int A [3][4] = {2 , -1 , 3 , 14 , 4 , 51 , 26 , 73 , -48 , 39 , 100 , 11};

int A [][4] = { // co the bo qua kich thuoc dau tien cua mang khi khai bao
{2 , -1 , 3 , 14} ,
{4 , 51 , 26 , 73} ,
{ -48 , 39 , 100 , 11}
};

int A [3][4] = {0}; // mang gom cac phan tu co gia tri bang 0

Để truy cập tới từng phần tử trong mảng 2 chiều, chúng ta có thể sử dụng vòng lặp for để duyệt
qua các chỉ số dòng và cột của mảng
for ( int row = 0; row < Nrows ; ++ row ) // duyet qua cac chi so dong
for ( int col = 0; col < Ncols ; ++ col ) // duyet qua cac chi so cot
A [ row ][ col ] = ....
hoặc áp dụng cách lặp theo khoảng
for ( auto row : A ) // duyet qua cac dong trong mang
for ( auto element : row ) // duyet qua cac phan tu trong 1 dong
element = ....

Ví dụ 4.4: Minh hoạ mảng 2 chiều


# include < iostream >

int main ()
{
// khoi tao mang 2 chieu
int A [3][4] = {
{2 , -1 , 3 , 14} ,
{4 , 51 , 26 , 73} ,
{ -48 , 39 , 100 , 11}
};

// xuat gia tri cac phan tu trong mang


for ( int i = 0; i < 3; ++ i ) {
for ( int j = 0; j < 4; ++ j ) {
std :: cout << " A [ " << i << " ][ " << j << " ] = " << A [ i ][ j ] << std ::
endl ;
4.2. Chuỗi kí tự 40

}
}
return 0;
}

4.2 Chuỗi kí tự
Chuỗi (string) là một mảng (dãy) bao gồm các kí tự (character ). Trong C++ có hai dạng cuỗi kí tự
• Chuỗi kí tự theo kiểu C (C-style character string)
• Chuỗi kí tự với lớp string trong C++

4.2.1 Chuỗi kí tự theo kiểu C


Trong ngôn ngữ C truyền thống không có kiểu dữ liệu cơ bản để lưu các chuỗi kí tự, do đó để người
ta thường sử dụng mảng có kiểu char.
Ví dụ 4.5: Khai báo chuỗi kí tự tên có độ dài cực đại là 20 kí tự
char ten [20];

Nội dung của chuỗi kí tự luôn được kết thúc với kí tự null (\0).

Khởi tạo chuỗi kí tự


Để khởi tạo nội dung cho chuỗi kí tự, ta có thể thực hiện theo cách sau
char ten [] = { 'P ' , 'h ' , 'u ' , 'o ' , 'n ' , 'g ' , ' \0 ' };

hoặc
char ten [] = " Phuong " ;

Lưu ý rằng việc gán nhiều hằng cho các phần tử trong mảng chỉ hợp lệ khi khởi tạo mảng. Thao
tác gán chỉ có thể được thực hiện với tứng phần tử một trong khi thực thi chương trình.

Gán giá trị cho chuỗi kí tự


Để có thể gán giá trị cho một chuỗi kí tự, chúng ta có thể sử dụng hàm strcpy() (được định nghĩa
trong thư viện cstring hoặc string.h)
strcpy ( ten , " Phuong " ) ;

Một cách khác để gán giá trị cho một chuỗi kí tự là sử dụng trực tiếp dòng nhập dữ liệu (cin)
cin.getline (char buffer[], int length, char delimiter = ‘\n’)
Trong đó buffer là bộ đệm (mảng), length là độ dài cực đại của mảng và delimiter là kí tự được
dùng để kết thúc việc nhập (mặc định là \n).
char ten [100];
std :: cout << " Nhap ten : " ;
std :: cin . getline ( ten ,100) ;
Trong trường hợp chỉ muốn nhận các từ đơn (không có khoảng trắng ) thay vì cả câu, ta có thể
sử dụng
std :: cin >> ten ;
41 CHƯƠNG 4. MẢNG VÀ CHUỖI

Chuyển đổi chuỗi kí tự

Khi ta có chuỗi kí tự là một số (vd: “1234”), ta có thể chuyển đổi chuỗi này sang số bằng cách sử
dụng các hàm có trong thư viện cstdlib (stdlib.h)
• atoi: chuyển chuỗi thành kiểu int
• atol: chuyển chuỗi thành kiểu long
• atof: chuyển chuỗi thành kiểu float
Ví dụ 4.6: Nhập chuỗi kí tự tương ứng với giá tiền và chuyển thành số
char mybuffer [100];
float price ;
std :: cout << " Enter price : " ;
std :: cin . getline ( mybuffer ,100) ;
price = std :: atof ( mybuffer ) ;

Thao tác trên chuỗi kí tự

Một số hàm khác trong thư viện cstring (string.h) thường hay được sử dụng để thao tác trên
chuỗi bên cạnh hàm strcpy() gồm có
• strcat(): gắn thêm chuỗi string2 vào phía cuối của chuỗi string1 và trả về string
char* strcat (char* string1, const char* string2)
• strcmp(): so sánh hai chuỗi string1 và string2, trả về 0 nếu hai chuỗi là bằng nhau
int strcmp (const char* string1, const char* string2)
• strlen(): Trả về độ dài của chuỗi
size_t strlen (const char* string)
Ví dụ 4.7: Minh hoạ các thao tác trên chuỗi
# include < iostream >
# include < cstring >

int main ()
{
char string1 [20] = " Hello " ;
char string2 [20] = " World " ;
char string3 [20];
int length ;

// gan chuoi string1 vao string3


strcpy ( string3 , string1 ) ;
std :: cout << string << std :: endl ;

// noi hai chuoi string1 va string2


strcat ( string1 , string2 ) ;
std :: cout << string1 << std :: endl ;

// do dai cua chuoi string1 sau khi noi


length = strlen ( string1 ) ;
std :: cout << length << std :: endl ;

return 0;
}
4.2. Chuỗi kí tự 42

4.2.2 Lớp chuỗi kí tự


Thư viện chuẩn của C++ có cung cấp lớp string để định nghĩa các chuỗi, để làm được điều này
ta sử dụng thư viện string (lưu ý rằng thư viện này khác với thư viện string.h 1 )

Khai báo chuỗi kí tự


Kiểu chuỗi kí tự với lớp string có dạng
string <tên chuỗi>
Ví dụ 4.8: Minh hoạ khai báo chuỗi kí tự
std :: string str = " Hello World " ;
hoặc
std :: string str ( " Hello World " ) ;

Gán giá trị cho chuỗi kí tự


Để gán giá trị cho một chuỗi kí tự ta làm như sau
str = " Hello World " ;
hoặc gán giá trị từ một chuỗi khác
std :: string str1 = " Hello World " ;
str = str1 ;

Ta cũng có thể sử dụng phương thức assign() (dùng được với cả string lẫn char*)
std :: string str1 = " Hello World " ;
str . assign ( str1 ) ;

char str2 [20] = " Hello World " ;


str . assign ( str2 ) ;

Ví dụ 4.9: Minh hoạ phương thức assign()


# include < iostream >
# include < string >

int main ()
{
std :: string str ;
std :: string base = " Ngon ngu lap trinh C ++ " ;

str . assign ( base ) ;


std :: cout << str << std :: endl ;

str . assign ( base ,9 ,11) ;


std :: cout << str << std :: endl ; // " lap trinh C "

str . assign ( " ngon ngu " ,4) ;


std :: cout << str << std :: endl ; // " ngon "

str . assign ( " lap trinh " ) ;


std :: cout << str << std :: endl ; // " lap trinh "
1
Xem Phần 9.2.
43 CHƯƠNG 4. MẢNG VÀ CHUỖI

str . assign (10 , '* ') ;


std :: cout << str << std :: endl ; // "**********"

str . assign < int >(10 ,0 x2D ) ;


std :: cout << str << std :: endl ; // " - - - - - - - - - -"

str . assign ( base . begin () +5 , base . end () -10) ;


std :: cout << str << std :: endl ; // " ngu lap "

return 0;
}

Chuyển đổi giữa string và char*

Cách chuyển đổi giữa string (chuỗi kí tự theo chuẩn C++) và char* (chuỗi kí tự theo chuẩn C)
như sau
std :: string str = " Hello " ;
const char * c = str . c_str () ; // chuyen tu string thanh char *
std :: string str1 ( c ) ; // chuyen tu char * thanh string

Thao tác trên chuỗi kí tự

Một số thao tác thông dụng trên chuỗi gồm có


• Nối chuỗi
std :: string str3 = str1 + str2 ; // cong hai chuoi
std :: cout << " str1 + str2 : " << str3 << std :: endl ;

• Độ dài chuỗi
int len = str3 . size () ; // kich thuoc chuoi
std :: cout << " Do dai duoi str3 : " << len << std :: endl ;

• So sánh chuỗi
std :: string str1 ( " mau do " ) ;
std :: string str2 ( " mau xanh " ) ;
if ( str1 . compare ( str2 ) !=0)
std :: cout << " Hai chuoi khong giong nhau " << std :: endl ;

• Nhập chuỗi (lưu ý khác với cin.getline() ở trên)


std :: string ten ;
std :: cout << " Nhap ten : " ;
std :: getline ( std :: cin , ten ) ;

• Chèn, lấy, xóa chuỗi con


std :: string name = " Dang Phuong " ;
std :: string middlename = " Nguyen " ;
std :: string fullname = name . insert (4 , middlename ) ; // chen chuoi
std :: string givenname = fullname . substr (12 ,6) ; // lay chuoi
std :: string surname = fullname . erase (4 ,13) ; // xoa chuoi
4.2. Chuỗi kí tự 44

• Tìm, thay thế chuỗi


std :: string str = " Hello World ! " ;
std :: size_t found = str . find ( " World " ) ; // tra ve vi tri tu chuoi con
if ( found != std :: string :: npos )
std :: cout << " ' World ' found at : " << found << std :: endl ;
str . replace (6 , 5 , " Phuong " ) ; // thay the chuoi con

Lưu ý: size_t là kiểu số nguyên không dấu của giá trị trả về của toán tử sizeof. Trong
trường hợp này ta có thể thay thế bằng khai báo kiểu int, tuy nhiên điểm khác biệt giữa
hai kiểu này nằm ở điều kiện của kiểu size_t là phải có kích thước (miền giá trị) đủ lớn để
chứa đối tượng có kích thước lớn nhất mà hệ thống có thể xử lý được (ví dụ một mảng tĩnh
có kích thước 8GB), trong khi kiểu int thi không nhất thiết.

Ví dụ 4.10: Minh hoạ các thao tác trên chuỗi


# include < iostream >
# include < string >

int main ()
{
std :: string string1 = " Hello " ;
std :: string string2 = " World " ;
std :: string string3 ;
int length ;

// gan chuoi string1 vao string3


string3 = string1 ;
std :: cout << string3 << std :: endl ;

// noi hai chuoi string1 va string2


string3 = string1 + string2 ;
std :: cout << string3 << std :: endl ;

// do dai cua chuoi string sau khi noi


length = string3 . size () ;
std :: cout << length << std :: endl ;

return 0;
}

4.2.3 Mảng chứa các chuỗi kí tự

Mảng chứa các chuỗi kí tự được khai báo tương tự như các mảng thông thường, ví dụ
std :: string str [3] = { " Hello " , " ABC " , " 123 " };

Để truy cập tới tất cả các phần tử trong mảng, ta có thể sử dụng các vòng lặp, chẳng hạn như
for ( i = 0; i < 3; ++ i )
std :: cout << str [ i ] << std :: endl ;

hoặc
for ( auto i : str )
std :: cout << i << std :: endl ;
45 CHƯƠNG 4. MẢNG VÀ CHUỖI

4.2.4 Chuỗi thô


Chuỗi thô (raw string) là chuỗi mà không thực hiện các kí tự điều khiển (chẳng hạn như \n, \t,...).
Chuỗi thô được bắt đầu bằng kí tự R``( và kết thúc với '').
Để dễ so sánh, ta bắt đầu với một chuỗi thông thường
std :: string str = " First line .\ nSecond line .\ nEnd of message .\ n " ;
std :: cout << str << std :: endl ;
Câu lệnh trên sẽ in ra 3 dòng (được phân cách bởi kí tự xuống dòng \n), tuy nhiên nếu ta sử dụng
chuỗi thô
std :: string str = R " ( First line .\ nSecond line .\ nEnd of message .\ n ) " ;
std :: cout << str << std :: endl ;
Câu lệnh trên chỉ in ra một dòng duy nhất với nội dung y hệt như những gì ta viết.
4.2. Chuỗi kí tự 46
Chương 5
Hàm

Trong C++, hàm là một khối lệnh dùng để thực thi một tác vụ nào đó và trả kết quả về khi được
gọi. Việc sử dụng hàm sẽ giúp cho chương trình của chúng ta được gọn gàng, rõ ràng và dễ sửa lỗi
hơn, đặc biệt đối với các chương trình sử dụng một tác vụ nào đó nhiều hơn một lần.

5.1 Khai báo hàm


5.1.1 Cú pháp

Cách thức khai báo một hàm như sau


<kiểu dữ liệu trả về> <tên hàm> (<đối số 1>, <đối số 2>,...)
{
<nội dung của hàm>
}
Các đối số (argument hay parameter ) được khai báo với kiểu dữ liệu tương ứng ở phía trước và
được cách nhau bởi dấu phẩy (‘,’). Các đối số này được dùng để truyền thông tin từ nơi gọi hàm
và chúng được sử dụng như các biến thông thường bên trong hàm.
Dữ liệu được trả về thông qua lệnh return, nếu không trả về dữ liệu thì khai báo kiểu dữ liệu trả
về là void.
Ví dụ 5.1: Khai báo hàm tính tổng của hai số nguyên
# include < iostream >

int tong ( int x , int y ) // ham tinh tong hai so nguyen


{
return ( x + y ) ;
}

int main ()
{
int x = 5 , y = 6;
std :: cout << " Tong cua hai so x va y la : " << tong (x , y ) << std :: endl ;
return 0;
}

Trong ví dụ này, chúng ta khai báo hai hàm tong() và main(). Đầu tiên chương trình sẽ thực thi
hàm main() (mặc định), khi thực hiện đến lời gọi hàm tong(x,y) chương trình sẽ thực thị hàm

47
5.1. Khai báo hàm 48

tong() với các đối số x và y tương ứng được truyền cho hàm. Giá trị tổng được trả về thông qua
lệnh return (x+y);.
Ví dụ 5.2: Khai báo hàm tính tổng hai số nguyên nhưng không trả về giá trị
# include < iostream >

void tong ( int x , int y ) // ham tinh tong va in ra gia tri


{
std :: cout << " Tong cua hai so x va y la : " << x + y << std :: endl ;
}

int main ()
{
int x = 5 , y = 6;
tong (x , y ) ;
return 0;
}

5.1.2 Khai báo hai hay nhiều hàm có cùng tên


Ta có thể khai báo hai hay nhiều hàm có cùng tên nếu chúng có số lượng đối số, kiểu dữ liệu của
đối số hay kiểu dữ liệu trả về khác nhau. Trình biên dịch sẽ lựa chọn gọi hàm nào bằng cách phân
tích kiểu đối số khi hàm được gọi (xem thêm trong Phần 5.5).
Ví dụ 5.3: Khai báo hai hàm tính tổng cho 2 số nguyên và hai số thực
# include < iostream >

int tong ( int x , int y ) // tinh tong hai so nguyen


{
return ( x + y ) ;
}

void tong ( float x , float y ) // tinh tong hai so thuc


{
std :: cout << " Tong cua hai so x va y la : " << x + y << std :: endl ;
}

int main ()
{
int x = 5 , y = 6;
std :: cout << " Tong cua hai so x va y la : " << tong (x , y ) << std :: endl ;

float z = 1.0 , w = -3.5;


tong (z , w ) ;

return 0;
}

5.1.3 Nguyên mẫu hàm


Trên nguyên tắc, một hàm phải được khai báo trước khi nó được gọi, điều này cũng đồng nghĩa
với việc hàm main() phải được đặt cuối chương trình để có thể gọi được tất cả các hàm đã khai
báo. Tuy nhiên, để tránh việc phải viết tất cả mã chương trình trước khi chúng có thể được dùng
trong hàm main() hay bất kì một hàm nào khác, ta có thể sử dụng cách khai báo nguyên mẫu
(prototype) cho hàm. Cách khai báo này đủ để cho trình biên dịch có thể biết các đối số và kiểu dữ
49 CHƯƠNG 5. HÀM

liệu trả về của hàm. Khai báo nguyên mẫu không bao gồm phần thân hàm và được kết thúc bằng
dấu ‘;’.
Ví dụ 5.4: Khai báo nguyên mẫu cho hàm tính tổng hai số nguyên
# include < iostream >

int tong ( int x , int y ) ; // khai bao nguyen mau

void main ()
{
int x = 5 , y = 6;
cout << " Tong cua hai so x va y la : " << tong (x , y ) << endl ;
}

int tong ( int x , int y ) // dinh nghia ham


{
return ( x + y ) ;
}

5.2 Đối số của hàm


5.2.1 Giá trị mặc định
Khi định nghĩa một hàm chúng ta có thể chỉ định những giá trị mặc định (default value) sẽ được
truyền cho các đối số trong trường hợp chúng bị bỏ qua khi hàm được gọi.
int tong ( int x = 0 , int y = 1)

Khi gọi hàm, ta không nhất thiết phải khai báo đầy đủ tất cả các đối số. Ví dụ như hàm tong(x,y)
có thể được gọi chỉ với 1 đối số tong(2), khi đó giá trị trả về sẽ là 3 (x = 2 được truyền cho hàm
và y = 1 được thiết lập mặc định).
Ví dụ 5.5: Khai báo giá trị mặc định cho đối số của hàm tính tổng hai số nguyên
# include < iostream >

int tong ( int x = 0 , int y = -1) // dinh nghia ham


{
return ( x + y ) ;
}

void main ()
{
int x = 5 , y = 6;
tong (x , y ) ; // 11
tong ( x ) ; // 4
tong ( y ) ; // 5
}

5.2.2 Cách truyền đối số


Trong phần lớn các trường hợp gọi hàm, các đối số truyền cho hàm đều là các giá trị (chứ không
phải là bản thân các biến), cách truyền này được gọi là cách truyền đối số theo dạng tham trị (by
value). Do đó khi thay đổi giá trị của các đối số này bên trong hàm thì các biến đó vẫn không bị
thay đổi. Để thay đổi được giá trị của biến bên ngoài hàm ta cần phải truyền bản thân biến đó.
Việc này có thể được thực hiện bằng cách truyền đối số dưới dạng tham chiếu (by reference), khi
5.2. Đối số của hàm 50

đó bất kì sự thay đổi nào của đối số đó bên trong hàm sẽ ảnh hưởng trực tiếp lên biến. Để khai
báo đối số theo dạng tham chiếu, ta sử dụng kí tự ‘&’ đặt trước tên biến được truyền.
Ví dụ 5.6: Hàm hoán đổi giá trị hai biến
# include < iostream >

void hoandoi ( int & x , int & y )


{
int tmp = x ;
x = y;
y = tmp ;
}

int main ()
{
int x = 1 , y = 4;
hoandoi (x , y ) ;
std :: cout << " x = " << x << " , y = " << y << std :: endl ;
return 0;
}

Ngoài ra còn một cách truyền đối số khác đó là truyền dưới dạng tham biến (by variable) hay còn
gọi là truyền bằng con trỏ (by pointer ). Cách truyền này được thực hiện bằng cách sử dụng kí tự
‘*’ đặt trước tên biến (sẽ bàn kĩ hơn trong Phần 6.5.1).
int hoandoi ( int *x , int * y )
{
int tmp = * x ;
*x = *y;
* y = tmp ;
}

Trong trường hợp muốn truyền tham số là mảng cho hàm thì chúng ta cần phải chỉ định trong
phần đối số kiểu dữ liệu cơ bản của mảng, tên mảng và cặp ngoặc vuông (trống hoặc có giá trị kích
thước mảng) khi khai báo đối số của hàm.
void function ( int arg []) // mang 1 chieu
void function ( int arg [10]) // mang 1 chieu
void function ( int arg [][3][4]) // mang nhieu chieu
void function ( int arg [10][3][4]) // mang nhieu chieu

Ví dụ 5.7: Tính tổng các phần tử của mảng


# include < iostream >

int tong ( int arg [] , int size )


{
int sum = 0;
for ( int i = 0; i < size ; ++ i ) {
sum += arg [ i ];
}
return sum ;
}

int main ()
{
int a [5] = {1 , -2 , 3 , -4 , 5};
int s = tong (a ,5) ;
std :: cout << " Gia tri tong la : " << s << std :: endl ;
51 CHƯƠNG 5. HÀM

return 0;
}

5.3 Hàm đệ quy


Hàm đệ quy (recursive function) là hàm gọi chính nó trong quá trình thực thi. Các hàm này thích
hợp cho một số mục đích cụ thể chẳng hạn như sắp xếp dãy số, tính giai thừa của một số,...
Ví dụ 5.8: Tính giai thừa của một số nguyên
# include < iostream >

int giaithua ( int n ) // ham tinh giai thua


{
if ( n > 1)
return ( n * giaithua (n -1) ) ; // goi lai ham tinh giai thua voi (n -1)
else
return 1;
}

int main ()
{
int n ;
std :: cout << " Nhap mot so nguyen : " ;
std :: cin >> n ;
std :: cout << n << " ! = " << giaithua ( n ) << std :: endl ;
return 0;
}

5.4 Hàm nội tuyến


Thông thường một hàm có được là nội tuyến (inline function) hay không sẽ do trình biên dịch
quyết định. Tuy nhiên khi đặt từ khóa inline trước khai báo của một hàm, trình biên dịch sẽ xem
hàm này như là nội tuyến và thay thế lời gọi hàm bằng mã lệnh của hàm khi chương trình được
dịch, hay nói một cách khác là trình biên dịch sẽ chèn phần thân hàm vào vị trí mà nó được sử
dụng.
inline int tong ( int x =0 , int y =1)

Cách này thường được dùng cho các hàm thực thi thường xuyên nhằm loại bỏ thời gian quá dụng
(overhead ) khi gọi hàm, tuy nhiên đôi khi cũng gây ra vấn đề khi biên dịch nếu số lượng hàm nội
tuyến quá lớn.
Ví dụ 5.9: Tìm giá trị lớn nhất giữa hai số nguyên
# include < iostream >

inline int max ( int x , int y )


{
return ( x > y ) ? x : y ;
}

int main ()
{
std :: cout << " Gia tri lon nhat cua (2 , -1) : " << max (2 , -1) << std :: endl ;
std :: cout << " Gia tri lon nhat cua (5 ,10) : " << max (5 ,10) << std :: endl ;
return 0;
5.5. Kĩ thuật nạp chồng 52

5.5 Kĩ thuật nạp chồng

Trong C++ ta có thể xây dựng các hàm hoặc toán tử có cùng tên nhưng khác nhau về nội dung
hoặc chức năng thực hiện nhiệm vụ, cách thức này được gọi là nạp chồng hay tái định nghĩa
(overloading).

Khi chúng ta gọi các hàm hay toán tử có cùng tên với nhau, trình biên dịch sẽ xác định hàm (toán
tử) nào là phù hợp nhất bằng cách so sánh các đối số được truyền cho hàm (toán tử) với các kiểu
đối số được khai báo trong phần định nghĩa hàm (toán tử).

5.5.1 Nạp chồng hàm

Ta có thể định nghĩa nhiều hàm có cùng tên với nhau, nhưng phải có các đối số khác nhau (về kiểu
dữ liệu hay số lượng đối số)1 . Phương pháp này được gọi là phương pháp nạp chồng hàm (function
overloading).

Ví dụ 5.10: Các hàm tính tổng

# include < iostream >

int tong ( int a , int b )


{
return ( a + b ) ;
}

int tong ( int a , int b , int c )


{
return ( a + b + c ) ;
}

float tong ( float a , float b )


{
return ( a + b ) ;
}

int main ()
{
int a = 2 , b = 3 , c = -1;
float d = 0.5 , e = -1.2;
std :: cout << " Tong cua hai so nguyen : " << tong (a , b ) << std :: endl ;
std :: cout << " Tong cua ba so nguyen : " << tong (a ,b , c ) << std :: endl ;
std :: cout << " Tong cua hai so thuc : " << tong (d , e ) << std :: endl ;
return 0;
}

1
Còn một khái niệm tái định nghĩa (hay nạp chồng) hàm khác nữa gọi là overriding, điểm khác biệt giữa hai khái
niệm này là overloading sử dụng cùng một tên hàm nhưng khác nhau các đối số, còn overriding có cùng tên hàm
lẫn đối số tuy nhiên nội dung hàm lại khác. Do đó, overriding thường được sử dụng trong việc tái định nghĩa các
phương thức của lớp dẫn xuất so với lớp cơ sở (xem phần ??)
53 CHƯƠNG 5. HÀM

5.5.2 Nạp chồng toán tử

Nạp chồng toán tử (operator overloading) cho phép định nghĩa lại một toán tử (ví dụ như các toán
tử +, −, *, /,...) cho một kiểu dữ liệu được định nghĩa bởi người dùng.
Ví dụ 5.11: Định nghĩa phép cộng phân số
# include < iostream >

struct Phanso { // dinh nghia phan so


int tuso , mauso ;
};

// Dinh nghia lai toan tu '+ '


Phanso operator +( const Phanso &a , const Phanso & b )
{
Phanso c ;
c . tuso = a . tuso * b . mauso + b . tuso * a . mauso ;
c . mauso = a . mauso * b . mauso ;
return c ;
}

int main ()
{
Phanso a , b , c ;
a . tuso = -1; a . mauso = 2;
b . tuso = 5; b . mauso = 3;
c = a + b ; // thuc hien phep cong phan so
std :: cout << " Tong cua hai phan so la : " << c . tuso << " / " << c . mauso <<
std :: endl ;
return 0;
}

5.6 Các hàm lambda


C++11 cung cấp các hỗ trợ cho các hàm vô danh (anonymous function) hay còn gọi là các hàm
lambda (lambda function)2 , nói cách khác đây là các hàm mà có thân hàm nhưng không có tên
hàm. Các hàm lambda có vai trò quan trọng trong lập trình hướng hàm (function programming),
nó có thể được sử dụng để thay thế cho các đối tượng hàm (functor )3 nhằm tránh việc phải tạo
các lớp và định nghĩa hàm.
Một hàm lambda đơn giản có dạng như sau
[các biến ngoài và kiểu] (các tham số) -> <kiểu trả về> { ...nội dung... }
Ví dụ 5.12: Minh hoạ hàm lambda
# include < iostream >

int main ()
{
auto func = [] () { std :: cout << " Hello world " << std :: endl ; };
func () ; // goi ham
return 0;
}

2
Còn gọi là biểu thức lambda (lambda expression)
3
Xem Phần ??
5.6. Các hàm lambda 54

Trong ví dụ trên, ta thấy có xuất hiện cặp ngoặc vuông [], đó là cặp ngoặc capture specification,
nó cho phép một hàm lambda có thể sử dụng các biến ngoài theo cách mà một hàm bình thường
tác động lên các biến nằm trong phạm vi hoạt động của nó. Đồng thời đây cũng là dấu hiệu của
hàm lambda, báo cho trình biên dịch biết là ta đang khai báo một hàm lambda. Cặp ngoặc tròn ()
theo sau cung cấp danh sách các đối số, do ta không trả về bất cứ kiểu dữ liệu nào nên sẽ không
có dấu ->, và cuối dùng là phần thân hàm (xuất ra dòng "Hello world").
Ví dụ 5.13: Hàm lambda tính tổng hai số nguyện
# include < iostream >

int main ()
{
auto func = []( int x , int y ) { return x + y ; }; // tinh tong hai so
int result = func (2 ,3) ;
std :: cout << result << std :: endl ;
return 0;
}

Ví dụ cách trả về giá trị trong với hàm lambda


[] () { return 1; } // tra ve mot so nguyen
[] () -> int { return 1; } // khai bao kieu du lieu tra ve la so nguyen

Ví dụ 5.14: Hàm lambda tính tổng hai số nguyện


# include < iostream >

int main ()
{
int result = []( int x , int y ) -> int { return x + y ; }(2 ,4) ;
std :: cout << result << std :: endl ;
return 0;
}

Ta cũng có thể sử dụng hàm lambda bên trong một hàm lambda khác.
Ví dụ 5.15: Minh hoạ sử dụng lambda bên trong hàm lambda khác
# include < iostream >

int main ()
{
int result = []( int m , int n ) -> int { return m + n ; } ([]( int a ) -> int
{ return a ; }(2) ,[]( int a ) -> int { return a ; }(3) ) ;
std :: cout << result << std :: endl ;
return 0;
}

Một trong những ưu điểm của hàm lambda là nó giảm bớt việc định nghĩa các lớp hoặc hàm, đặc
biệt khi dùng với các algorithm trong thư viện chuẩn STL (xem Phần 9.6).
Ví dụ 5.16: Định nghĩa hàm max() trả về giá trị lớn nhất trong hai số nguyên
# include < iostream >

// Khai bao ham max ()


int max ( int i , int j ) { return (i > j ? i : j ) ; }
55 CHƯƠNG 5. HÀM

int main ()
{
const int N = 5;
int a [ N ] = {1 , 3 , -3 , 0 , 9};

// Tim so lon nhat trong mang a [5]


int sln = a [0];
for ( int i = 1; i < N ; ++ i )
sln = max ( sln , a [ i ]) ;
std :: cout << " So lon nhat trong day so la : " << sln << std :: endl ;

return 0;
}
Ta có thể bỏ qua định nghĩa hàm max() khi sử dụng hàm lambda
# include < iostream >

int main ()
{
const int N = 5;
int a [ N ] = {1 , 3 , -3 , 0 , 9};

// Tim so lon nhat trong mang a [5]


int sln = a [0];
for ( int i = 1; i < N ; ++ i )
sln = []( int i , int j ) -> int { return (i > j ? i : j ) ; } ( sln , a [ i ]) ;
std :: cout << " So lon nhat trong day so la : " << sln << std :: endl ;

return 0;
}
5.6. Các hàm lambda 56
Chương 6
Con trỏ

Như chúng ta đã biết, bộ nhớ máy tính có thể xem như là một dãy gồm các ô nhớ (có kích thước
1 byte), mỗi ô có một địa chỉ xác định. Các biến chính là các ô nhớ mà chúng ta có thể truy xuất
dưới các tên. Khi chúng ta khai báo một biến thì nó phải được lưu trữ trong một vị trí cụ thể trong
bộ nhớ, vị trí của biến được lưu trữ sẽ do trình biên dịch và hệ điều hành quyết định. Để có thể
biết được chính xác vị trí (hay địa chỉ) của biến đó ở đầu trong bộ nhớ, chúng ta cần sử dụng một
công cụ hỗ trợ, đó chính là con trỏ.

6.1 Thao tác với con trỏ


6.1.1 Khai báo
Con trỏ (pointer ) là một biến lưu trữ địa chỉ của một biến hoặc một vùng biến khác (xem Hình 6.1).
Để lấy địa chỉ của một biến, người ta sử dụng toán tử &. Cách thức khai báo con trỏ như sau
<kiểu dữ liệu> *<tên con trỏ>

Trong đây chúng ta sẽ làm quen với hai toán tử


• Toán tử lấy địa chỉ (&): được dùng như là một tiền tố của biến và có thể được hiểu là “địa
chỉ của”, vd: &a có thể được hiểu là “địa chỉ của biến a”.
• Toán tử tham chiếu (*): truy xuất trực tiếp đến giá trị được lưu trữ trong biến được trỏ bởi
nó và có thể được hiểu là “giá trị được trỏ bởi”, vd: *pointer có thể hiểu là “giá trị được trỏ
bởi pointer”.
Ví dụ 6.1: Minh hoạ mối quan hệ giữa biến và con trỏ
int a = 3; // gia su bien a co dia chi la 1011
int b, *p;
p = &a; // p = 1011
b = a; // b = 3

Ví dụ 6.2: Thay đổi giá trị của biến a từ 3 thành 5


int a = 3;
int * p ; // Khai bao bien con tro p
p = & a ; // p chua dia chi cua bien a
* p = 5; // gan gia tri cua bien ung voi dia chi ma p tro toi la 5 ( a = 5)

Ta cũng có thể khởi tạo con trỏ ngay khi định nghĩa

57
6.1. Thao tác với con trỏ 58

Hình 6.1: Minh hoạ con trỏ: biến a có địa chỉ trong bộ nhớ là 1011 và có giá trị là 3, biến con trỏ
p có giá trị là địa chỉ của biến a.

double a = 5.43;
double * p = & a ;

6.1.2 Con trỏ NULL


Con trỏ NULL hay còn gọi là con trỏ rỗng (null pointer )1 là con trỏ mà không trỏ tới một địa chỉ
nào xác định. Khi khai báo con trỏ NULL, ta sử dụng giá trị NULL2 hoặc 0.
Ví dụ 6.3: Minh hoạ con trỏ NULL
int * p = 0;
float * f = NULL ;

if ( p ) // tra ve true neu p khong la con tro null


if (! p ) // tra ve true neu p la con tro null

Ngoài ra, chuẩn C++11 có định nghĩa một từ khóa mới là nullptr có kiểu là nullptr_t tương
thích với tất cả các con trỏ và kiểu bool nhưng không tương thích với các kiểu cơ bản như int,
long, double,...
Ví dụ 6.4: Minh hoạ nullptr
void f ( int ) ; // ham thu 1
void f ( char *) ; // ham thu 2
// Chuan C ++ cu
f (0) ; // 0 la kieu int hay con tro null ? Ham nao duoc goi ?
// Chuan C ++11
f ( nullptr ) // con tro null , goi ham thu 2

6.1.3 Con trỏ void


Con trỏ void (void pointer ) hay còn gọi là con trỏ tổng quát (generic pointer ) là một dạng đặc
biệt của con trỏ mà có thể trỏ đến bất kì kiểu dữ liệu nào. Để khai báo con trỏ void, ta sử dụng từ
khoá void thay cho kiểu dữa liệu.
Ví dụ 6.5: Minh hoạ con trỏ void
void * p ;

int n = 10;
1
Cần phân biệt với con trỏ void (xem Phần 6.1.3).
2
NULL có kiểu int và giá trị là 0. Con trỏ NULL được định nghĩa trong nhiều thư viện, chẳng hạn như thư viện
iostream.
59 CHƯƠNG 6. CON TRỎ

float x = -2.;

p = &n; // p tro den bien n co kieu int


p = &x; // p tro den bien x co kieu float

Tuy nhiên, do con trỏ void không biết kiểu dữ liệu mà nó trỏ tới nên ta không thể lấy nội dung của
địa chỉ mà nó trỏ tới (dereference) một cách trực tiếp được. Vì vậy, ta cần phải chuyển đổi kiểu
trước khi sử dụng
# include < iostream >

int main ()
{
int n = 10;

void * vp = & n ;
int * ip = ( int *) vp ; // chuyen doi kieu ( void *) thanh ( int *)

std :: cout << * ip << std :: endl ;


return 0;
}

6.1.4 Phép tính số học với con trỏ


Việc thực hiện các phép tính số học với con trỏ hơi khác so với các kiểu dữ liệu số khác. Chúng ta
chỉ được sử dụng phép cộng và trừ, và kết quả của phép tính phụ thuộc vào kích thước của kiểu
dữ liệu mà biến con trỏ trỏ tới. Điều này có nghĩa là kích thước tính bằng byte của kiểu dữ liệu nó
trỏ tới sẽ được cộng (hoặc trừ) thêm vào biến con trỏ.
Ví dụ 6.6: Minh hoạ phép toán đối với con trỏ
int * p1 = & a1 ; // gia su gia tri cua p1 la 1000 ( dia chi cua a1 )
long * p2 = & a2 ; // gia su gia tri cua p2 la 2000 ( dia chi cua a2 )
p1 ++; // gia tri cua p1 luc nay la 1001 ( kieu int co kich thuoc 1 byte )
p2 ++; // gia tri cua p2 luc nay la 2002 ( kieu long co kich thuoc 2 byte )
p1 - -; // gia tri cua p1 luc nay tro lai la 1000

6.1.5 Phép so sánh với con trỏ


Chúng ta cũng có thể sử dụng các phép so sánh (==, <, >) đối với các con trỏ. Việc so sánh này
sẽ có ý nghĩa nếu các con trỏ có liên quan với nhau (chẳng hạn như trỏ tới các phần tử trong vùng
1 mảng,...).
Ví dụ 6.7: Minh hoạ phép so sánh đối với con trỏ
# include < iostream >

int main ()
{
int a [] = {1 , 10 , 100};
int * p ;

// gan dia chi cua phan tu dau tien cho con tro
p = & a [0];
int i = 0;
while ( p <= & a [2]) { // thuc hien phep so sanh de kiem tra xem toi vi
tri cuoi cung trong mang hay chua
6.2. Con trỏ và tham chiếu 60

std :: cout << " a [ " << i << " ] = " ;


std :: cout << * p << std :: endl ;
p ++;
i ++;
}
return 0;
}

6.2 Con trỏ và tham chiếu


Cả hai dường như được dùng để làm những điều tương tự nhau, đó là cho phép ta truy cập đến
các đối tượng một cách gián tiếp. Tuy nhiên chúng vẫn có những khác biệt, chẳng hạn như
• Phải khởi tạo giá trị tham chiếu
int n = 10;
int & r = n ; // tham chieu den a ( OK )
int & r ; // tham chieu khong duoc khoi tao ( ERROR )

• Không (nên) có tham chiếu NULL, một tham chiếu phải luôn chỉ đến một đối tượng nào đó.
Tuỳ trường hợp trình biên dịch có thể báo lỗi hoặc không.
int * p ; // con tro null ( OK )
int & r = * p ; // tham chieu den con tro null ( WARN )

• Đối với con trỏ, ta cần phải kiểm tra nó có phải là con trỏ NULL hay không trước khi sử
dụng, còn tham chiếu thì không có tham chiếu NULL nào được tạo ra do đó nếu có tham
chiếu thì ta có thể sử dụng nó mà không cần kiểm tra
void f ( float * p ) {
if ( p ) { // kiem tra con tro null
std :: cout << * p ;
}
}

void f ( float & r ) {


std :: cout << rd ; // khong can kiem tra tham chieu
}

• Con trỏ có thể được chỉ định để truy cập đến nhiều đối tượng khác nhau, còn một tham chiếu
thì chỉ truy cập đến đối tượng đã được khởi tạo cho nó.
int a = 10 , b = 5;
int & r = a ; // r tham chieu den a
int * p = a ; // p tro den a
r = b; // r van tham chieu den a
p = &b; // p tro den b

6.3 Con trỏ và hằng


Mối quan hệ giữa con trỏ và hằng có hai kiểu3
• Con trỏ tới giá trị hằng hay còn gọi là con trỏ hằng (pointer to constant) là con trỏ mà
không cho phép thay đổi nội dung của dữ liệu thông qua con trỏ. Cách khai báo con trỏ hằng
như sau
3
Lưu ý vị trí của dấu ‘*’ trong các cách khai báo.
61 CHƯƠNG 6. CON TRỎ

const <kiểu dữ liệu> * <tên biến>


hoặc
<kiểu dữ liệu> const * <tên biến>
Địa chỉ bộ nhớ được lưu trữ bởi con trỏ hằng không thể được gán cho một con trỏ thông
thường khác4 .
• Con trỏ có địa chỉ là hằng hay còn gọi là hằng con trỏ (constant pointer ) là con trỏ mà
luôn trỏ tới một địa chỉ không đổi. Cách khai báo hằng con trỏ như sau
<kiểu dữ liệu> * const <tên biến> = <địa chỉ bộ nhớ>
Do con trỏ này luôn trỏ tới một địa chỉ không đổi, địa chỉ này cần được khai báo ngay lúc
khởi tạo con trỏ.
• Trong trường hợp muốn khai báo kết hợp cả hai kiểu (con trỏ hằng trỏ tới một địa chỉ hằng),
ta khai báo như sau
const <kiểu dữ liệu> * const <tên biến> = <địa chỉ bộ nhớ>
hoặc
<kiểu dữ liệu> const * const <tên biến> = <địa chỉ bộ nhớ>
Ví dụ 6.8: Minh hoạ con trỏ hằng
int a ;
const int * p = & a ; // p la con tro hang
* p = 10; // a = 10 ( khong the gan gia tri cho a thong qua p -->
ERROR )

Ví dụ 6.9: Minh hoạ hằng con trỏ


int a , b ;
int * const p = & a ; // p la hang con tro
p = &b; // khong the gan dia chi cua b cho con tro p --> ERROR

6.4 Con trỏ và mảng


6.4.1 Con trỏ và địa chỉ mảng
Trong thực tế, địa chỉ của một mảng tương đương với địa chỉ phần tử đầu tiên của nó, giống như
một con trỏ tương đương với địa chỉ của phần tử đầu tiên mà nó trỏ tới, chẳng hạn như ta có thể
khai báo mảng có kích thước không xác định theo cách sau
int * n ; // mang n chua so nguyen
char * s ; // mang ( chuoi ) s chua ki tu

Ví dụ 6.10: Khai báo một mảng chứa biến kiểu số nguyên, địa chỉ của con trỏ trỏ tới cũng chính
là địa chỉ của phần tử đầu tiên trong mảng
# include < iostream >

int main ()
{
int n [] = {1 , 10 , 100};
std :: cout << " Dia chi phan tu dau tien : " << & n [0] << std :: endl ;
std :: cout << " Dia chi cua mang : " << n << std :: endl ;
}

4
Để chuyển từ con trỏ hằng thành con trỏ thường ta sử dụng const_cast (xem Phần )
6.4. Con trỏ và mảng 62

Tuy nhiên con trỏ và mảng không phải hoàn toàn có thể hoán đổi cho nhau được, chẳng hạn như
int a [10];
int * p = a ; // lenh gan nguoc lai a = p la sai vi a [0] la hang con tro

Chúng ta cũng có thể gán giá trị cho mảng thông qua con trỏ
a [5] = 10; // phan tu thu 5 cua mang a co gia tri la 10
*( a +5) = 10; // tuong duong a [5] = 10 ( a cung la con tro )

Ví dụ 6.11: Gán giá trị cho mảng


# include < iostream >

int main ()
{
int a [] = {10 , 100 , 200};

// Gan gia tri thong qua con tro


for ( int i = 0; i < 3; ++ i ) {
*( a + i ) = i *3;
}

// In ra gia tri cua mang


for ( int i = 0; i < 3; ++ i ) {
std :: cout << a [ i ] << std :: endl ;
}

return 0;
}

6.4.2 Mảng con trỏ


Cũng tương tự như với các biến, chúng ta có thể tạo một mảng chứa các con trỏ, chẳng hạn như
int * p [5]; // mang chua 5 con tro toi du lieu so nguyen

Ngoài ra, chúng ta cũng có thể xem mảng N chiều như là một mảng một chiều chứa các con trỏ
trỏ tới các mảng (N−1) chiều
int ** p ; // mang 2 chieu
float * a [5]; // mang 2 chieu co 5 dong

Ví dụ 6.12: Minh hoạ mảng con trỏ


# include < iostream >

int main ()
{
int a [] = {0 , 1 , 10 , 100};
int * p []= {a , a +1 , a +2 , a +3}; // mang con tro p [] chua cac dia chi cua
mang a []
for ( int i = 0; i < 4; ++ i ) {
std :: cout << * p [ i ] << std :: endl ; // xuat ra noi dung cua mang a []
}
return 0;
}
63 CHƯƠNG 6. CON TRỎ

6.5 Con trỏ và hàm


6.5.1 Truyền con trỏ cho hàm
Để truyền con trỏ cho một hàm, ta chỉ cần khai báo đối số của hàm có kiểu con trỏ. Trong trường
hợp muốn truyền đối số theo kiểu tham biến (xem Phần 5.2.2) ta cần truyền địa chỉ của biến cho
hàm khi gọi.
Ví dụ 6.13: Hoán đổi giá trị của hai biến
# include < iostream >

void hoandoi ( int * x , int * y )


{
int tmp = * x ;
*x = *y;
* y = tmp ;
}

int main ()
{
int x = 1 , y = 4;
hoandoi (& x ,& y ) ; // truyen dia chi cua bien
std :: cout << " x = " << x << " , y = " << y << std :: endl ;
return 0;
}

Chúng ta cũng có thể truyền đối số là mảng cho hàm dưới dạng con trỏ.
Ví dụ 6.14: Tính giá trị trung bình của mảng
# include < iostream >

float trungbinh ( float * a , int N )


{
float tong = 0.;
for ( int i = 0; i < N ; ++ i ) {
tong += a [ i ];
}
return tong / N ;
}

int main ()
{
float a [] = { -1.0 , 2.4 , 10.8 , -4.1};
float tb = trungbinh (a ,4) ;
std :: cout << " Gia tri trung binh cua mang la : " << tb << std :: endl ;
return 0;
}

6.5.2 Trả về con trỏ từ hàm


Việc trả về con trỏ cũng tương tự như với việc truyền con trỏ cho hàm.
Ví dụ 6.15: Sao chép mảng
# include < iostream >

float * saochep ( float a [4])


{
6.5. Con trỏ và hàm 64

static float b [4];


for ( int i = 0; i < 4; ++ i ) {
b [ i ] = a [ i ];
}
return b ;
}

int main ()
{
float mang [] = { -1.0 , 2.4 , 10.8 , -4.1};
float * mang1 = saochep ( mang ) ;
for ( int i = 0; i < 4; ++ i ) {
std :: cout << " Phan tu " << i +1 << " la : " << mang1 [ i ] << std :: endl ;
}
return 0;
}

6.5.3 Con trỏ hàm


Việc sử dụng con trỏ hàm (function pointer ) sẽ giúp chúng ta truyền một hàm như là một tham
số đến một hàm khác. Để có thể khai báo một con trỏ trỏ tới một hàm chúng ta phải khai báo nó
như là khai báo mẫu của một hàm nhưng phải bao trong một cặp ngoặc đơn () tên của hàm và
chèn kí tự ‘*’ đằng trước.
Ví dụ 6.16: Hàm tính tổng hoặc hiệu của hai số nguyên
# include < iostream >

// ham tinh tong


int tong ( int a , int b )
{
return ( a + b ) ;
}

// ham tinh hieu


int hieu ( int a , int b )
{
return (a - b ) ;
}

int (* tru ) ( int , int ) = hieu ; // con tro ham cua ham hieu ()

// ham goi con tro cua tong () hoac hieu ()


int tinh ( int x , int y , int (* ham ) ( int , int ) )
{
int g ;
g = (* ham ) (x , y ) ;
return g ;
}

int main ()
{
int x = 10 , y = 3;
int z = tinh (x , y , & tong ) ;
std :: cout << " Tong cua hai so la : " << z << std :: endl ;
z = tinh (x , y , tru ) ;
std :: cout << " Hieu cua hai so la : " << z << std :: endl ;
return 0;
}
65 CHƯƠNG 6. CON TRỎ

Trong ví dụ trên, chúng ta xây dựng một hàm tinh() để tính tổng hoặc hiệu của hai số nguyên x
và y. Hàm tinh() này sẽ gọi giá trị của hai biến x,y và đồng thời cũng gọi con trỏ của các hàm
tong() và hieu() tương ứng. Việc truyền con trỏ cho hàm tinh() có thể được thực hiện bằng
cách truyền địa chỉ của hàm (vd: &tong) hoặc tạo một con trỏ cho hàm ((*tru)(int,int)= hieu)
và gọi con trỏ này.

6.6 Bộ nhớ động


Khi thực thi chương trình, máy tính sẽ sử dụng bộ nhớ để lưu trữ các dữ liệu như các biến, mảng
và các đối tượng mà chúng ta khai báo, kích cỡ của chúng là cố định và không thể thay đổi trong
thời gian thực thị chương trình. Đôi khi cách làm này gây lãng phí hoặc thiếu bộ nhớ, do đó chúng
ta cần sử dụng bộ nhớ theo cách mà kích cỡ của nó chỉ có thể được xác định khi chương trình chạy.
Để làm được điều này chúng ta sẽ sử dụng các bộ nhớ động (dynamic memory).

6.6.1 Toán tử cấp phát bộ nhớ


Việc cấp phát bộ nhớ động (dynamic memory allocation) trong C++ được thực hiện qua hai toán
tử new và delete
• Toán tử new được dùng để cấp phát bộ nhớ, có cú pháp như sau
<con trỏ> = new <kiểu dữ liệu>
hoặc <con trỏ> = new <kiểu dữ liệu>[kích thước]
int * p1 = new int ;
int * p2 = new int [10]; // danh vung nho cho 10 phan tu va tra ve 1
con tro tro den dau khoi du lieu
int * p3 = new int (10) ; // khoi tao gia tri cua p3 bang 10

Tất cả các biến cấp phát động mới đều được đặt trong vùng nhớ free store. Trong trường hợp
hệ điều hành hết bộ nhớ để cấp phát, một con trỏ null (0) sẽ được trả về
int * p = new int ;
if ( p == NULL ) {
std :: cout << " Khong du bo nho " << std :: endl ;
}

• Toán tử delete được dùng để giải phóng bộ nhớ được cấp phát khi nó không cần dùng đến
nữa, có cú pháp như sau
delete <con trỏ>
hoặc delete [] <con trỏ>
delete p ;

Ví dụ 6.17: Cấp phát và giải phóng bộ nhớ cho mảng với new/delete
# include < iostream >

int main ()
{
int n ;
int * a ;

std :: cout << " Nhap kich thuoc mang : " ;


std :: cin >> n ;
6.6. Bộ nhớ động 66

a = new int [ n ]; // cap phat bo nho cho mang

// Gan gia tri cho cac phan tu trong mang


for ( int i = 0; i < n ; ++ i ) {
a [ i ] = i *10;
std :: cout << " a [ " << i << " ] = " << a [ i ] << std :: endl ;
}
delete a ; // giai phong bo nho dong

return 0;
}

6.6.2 Hàm cấp phát bộ nhớ


Bên cạnh các toán tử new/delete, chúng ta cũng có thể sử dụng các hàm trong thư viện cstdlib
(stdlib.h) để cấp phát bộ nhớ động
• Hàm malloc là một hàm tổng quát để cấp phát bộ nhớ động cho con trỏ
int * p1 , * p2 ;
p1 = ( int *) malloc (10) ; // cap phat cho con tro p1 mot khoi nho co
kich thuoc 10 byte
p2 = ( int *) malloc (5 * sizeof ( int ) ) ;

• Hàm calloc tương tự như malloc


int * p1 , * p2 ;
p1 = ( int *) calloc (3 ,10) ; // cap phap cho con tro p1 mot khoi nho
co 3 phan tu , moi phan tu co kich thuoc 10 byte
p2 = ( int *) calloc (3 , 5 * sizeof ( int ) ) ;
Hàm calloc khởi tạo tất cả các phần tử của nó về 0.
• Hàm realloc thay đổi kích thước của khối nhớ đã được cấp phát cho một con trỏ
int * p = malloc (10) ; // cap phat 10 byte cho p
p = realloc (p ,12) ; // tang bo nho cua p len them 2 byte

• Hàm free giải phóng một khối nhớ động đã được cấp phát
free ( p ) ;

Lưu ý: khi sử dụng, các toán tử new/delete sẽ gọi các hàm khởi tạo và hủy của kiểu dữ liệu, còn
các hàm malloc/free thì không. Ngoài ra không nên trộn lẫn các cách cấp phát và thu hồi bộ nhớ
với nhau (ví dụ như new dùng với free() hay malloc() dùng với delete), điều này có thể gây
nên các lỗi trong quá trình thực thi.
Ví dụ 6.18: Cấp phát và giải phóng bộ nhớ cho mảng với malloc()/free()
# include < iostream >

int main ()
{
int n ;
int * a ;

std :: cout << " Nhap kich thuoc mang : " ;


std :: cin >> n ;
a = ( int *) malloc ( n * sizeof ( int ) ) ; // cap phat bo nho cho mang
67 CHƯƠNG 6. CON TRỎ

// Gan gia tri cho cac phan tu trong mang


for ( int i = 0; i < n ; ++ i ) {
a [ i ] = i *10;
std :: cout << " a [ " << i << " ] = " << a [ i ] << std :: endl ;
}
free ( a ) ; // giai phong bo nho dong

return 0;
}

6.6.3 Các vùng bộ nhớ trên máy tính


• Const data: chứa các chuỗi kí tự tường minh (ví dụ như “hello”) và các dữ liệu mà giá trị của
nó đã được biết tại thời gian biên dịch (compiler-time). Đây là vùng chỉ có thể đọc, không
thể thay đổi giá trị tại vùng nhớ đó.
• Stack : là vùng nhớ tạm để lưu biến nằm trong hàm và địa chỉ trả về sau khi kết thúc hàm.
Cấp phát trong vùng stack nhanh hơn so với vùng cấp phát động (free store và heap).
• Free store: là vùng nhớ dùng để cấp phát động thông qua các toán tử new/delete.
• Heap: là vùng nhớ dùng để cấp phát động thông qua các hàm malloc/free.
• Global/static: bộ nhớ tĩnh, chứa các biến static, global,... được tính toán và cấp phát 1 lần
ngay khi khởi động chương trình và giữ nguyên trong suốt thời gian chương trình chạy, nó
chỉ bị xóa bỏ khi chương trình kết thúc. Khi lập trình nên lưu ý giữ cho bộ nhớ tĩnh càng
nhỏ càng tốt, chỉ khai báo trong bộ nhớ tĩnh những cái cần thiết.

6.6.4 Các lỗi thường gặp khi sử dụng bộ nhớ động


• Memory leak : xảy ra khi chương trình không thể thu hồi (hay truy cập) một vùng nhớ đã
cấp phát mặc dù không còn sử dụng. Nó xảy ra khi tất cả con trỏ trỏ đến vùng nhớ đó bị
thay đổi giá trị trước khi vùng nhớ được giải phóng.
• Dangling pointer : xảy ra khi tồn tại con trỏ trỏ tới một vùng nhớ chưa được cấp phát, hoặc
đã bị thu hồi.

6.7 Smart pointer


Smart pointer không phải là khái niệm mới được đưa vào chuẩn C++11, tuy nhiên có nhiều hỗ trợ
mới cho smart pointer được đưa vào trong chuẩn này cho nên nó sẽ được trình bày ở đây.
Như ta đã biết, một trong những vấn đề đau đầu nhất mà người dùng C++ gặp phải đó là vấn đề
quản lý bộ nhớ, giải phóng các vùng nhớ mà không còn cần dùng nữa. Nếu mỗi tài nguyên của
chúng ta chỉ được sử dụng bởi duy nhất một đối tượng, thì ta chỉ việc thực hiện giải phóng tài
nguyên đó trong hàm hủy của đối tượng. Nhưng trong trường hợp ta có các tài nguyên chia sẻ,
được sử dụng bởi nhiều đối tượng. Khi đó ta sẽ không biết phải thực hiện giải phóng tài nguyên
đó ở đâu, hay nói cách khác là sẽ không biết đối tượng nào chịu trách nhiệm giải phóng tài nguyên
đó. Do vậy, để giải quyết được vấn đề này thì ta phải sử dụng đến một khái niệm là quyền sở hữu
(ownership).
Khi một đối tượng sở hữu tài nguyên, thì nó sẽ có quyền sử dụng, hủy chuyển quyền sở hữu tài
nguyên đó cho một đối tượng khác. Ngoài ra đối tượng sở hữu tài nguyên còn có nghĩa vụ phải hủy
đi tài nguyên mà mình sở hữu khi tài nguyên đó không còn được dùng đến nữa.
Để cụ thể hóa khái niệm quyền sở hữu này ta cần tới một loại con trỏ mới, đó là con trỏ “thông
minh” (smart pointer ). Đây là các đối tượng có cách làm việc giống như các con trỏ thông thường
6.7. Smart pointer 68

(raw pointer ) trong C/C++ 5 nhưng có bổ sung thêm các khả năng như tự động quản lý bộ nhớ,
kiểm tra truy xuất ngoài vùng được cấp phát,. . .
Chức năng quản lý các đối tượng được tạo ra bằng toán tử new, các đối tượng sẽ được tự động xóa
bỏ theo một cách thích hợp nhất (dựa trên cơ chế về quyền sở hữu) và các lập trình viên sẽ không
cần phải quan tâm tới việc quyết định xem cần loại bỏ chúng khi nào và ở đâu trong chương trình
của mình. Nếu không có các smart pointer, chúng ta chỉ có một cách duy nhất để loại bỏ các đối
tượng cấp phát động bằng toán tử delete, ví dụ như
type * p = new type () ;
// p = ...
delete p ;

Nhưng không phải lúc nào chúng ta cũng thực hiện được điều này một cách tường minh: một lệnh
gán con trỏ sai sẽ làm mất địa chỉ của biến đã cấp phát, hoặc chương trình gặp lỗi không mong
muốn trước khi có thể thực hiện lệnh delete, cả hai khả năng đều dẫn tới lỗi memory leak. Với
các smart pointer, công việc dọn dẹp này sẽ được thực hiện một cách tự động.
SmartPointer < type > p ( new type () ) ;;
// p = ...
// ham huy se duoc goi tu dong tuy theo tung loai smart pointer

Trong C++11 có 3 loại smart pointer6 chính là:


• unique_ptr đại diện cho quyền sở hữu duy nhất, nghĩa là tài nguyên mà nó trỏ tới chỉ có thể
được sở hữu bởi duy nhất một đối tượng (tương đương với auto_ptr trong các chuẩn trước
C++11).
• shared_ptr đại diện cho quyền sở hữu chia sẻ, nghĩa là tài nguyên mà nó trỏ tới là tài nguyên
chia sẻ, có thể được sở hữu bởi nhiều đối tượng.
• weak_ptr đại diện cho quyền sở hữu yếu, nghĩa là đối tượng nắm trong tay con trỏ này trỏ
tới một tài nguyên thì nó chỉ có quyền được sử dụng tài nguyên đó, chứ không có quyền và
nghĩa vụ hủy đi tài nguyên.
Để sử dụng các smart pointer ta cần khai báo thư viện memory. Một số phương thức chính của
smart pointer gồm có
• Hoán đổi
std :: shared_ptr < int > foo ( new int (10) ) ;
std :: shared_ptr < int > bar ( new int (20) ) ;

foo . swap ( bar ) ;

std :: cout << " * foo : " << * foo << '\ n ';
std :: cout << " * bar : " << * bar << '\ n ';

• Tạo quyền sở hữu


std :: shared_ptr < int > sp ; // con tro rong
sp . reset ( new int ) ; // tao quyen so huu cho con tro
* sp =10;
std :: cout << * sp << '\ n ';

5
Để đạt được điều này, người ta đã tạo ra các đối tượng để “đóng gói” các con trỏ thông thường, các đối tượng
này còn được gọi là proxy object. Để các đối tượng này hành xử như một con trỏ thật sự thì các toán tử đặc trưng
của con trỏ thông thường như các toán tử tham chiếu * và -> đều được tái định nghĩa lại trong các đối tượng này.
6
Từ chuẩn C++03 trở về trước chỉ có duy nhất một loại smart pointer là auto_ptr.
69 CHƯƠNG 6. CON TRỎ

sp . reset ( new int ) ; // xoa bo doi tuong dang duoc quan ly , tao con tro
moi
* sp =20;
std :: cout << * sp << '\ n ';

Các bạn có thể tìm hiểu thêm các phương thức khác của smart pointer tại:
http://en.cppreference.com/w/cpp/memory/unique_ptr
http://en.cppreference.com/w/cpp/memory/shared_ptr
http://en.cppreference.com/w/cpp/memory/weak_ptr
6.7. Smart pointer 70
Chương 7
Tập tin

Tập tin (file) là một trong những hình thức lưu trữ dữ liệu phổ biến, có hai loại tập tin chính
• Tập tin văn bản (text file): là loại tập tin chỉ lưu trữ thuần túy văn bản, các kí tự được biểu
diễn bằng mã ASCII của nó, và người dùng có thể dễ dàng đọc được nội dung của tập tin
này.
• Tập tin nhị phân (binary file): là loại tập tin chứa các đoạn mã nhị phân, người dùng không
thể đọc được nội dung các tập tin này.
Các thao tác trên tập tin có thể được thực hiện theo chuẩn C (sử dụng các hàm trong thư viện
cstdio) hoặc theo chuẩn C++ (các hàm trong các thư viện dòng xuất nhập iostream).

7.1 Thao tác với tập tin theo kiểu C


Việc thao tác với tập tin theo chuẩn C được thực hiện thông qua thư viện cstdio (stdio.h)1 với
con trỏ của đối tượng FILE2
Các hàm chính trong thư viện cstdio gồm có
Tên hàm Chức năng
Các hàm chung
fopen Mở tập tin
fclose(all) Đóng (tất cả các) tập tin
fflush(all) Làm sạch vùng đệm của (tất cả các) tập tin đang mở
remove Xóa tập tin
feof Kiểm tra xem tới cuối tập tin chưa
Xử lý tập tin văn bản
fprintf Ghi ra tập tin
fscanf Đọc từ tập tin
fputc/fgetc Ghi (đọc) 1 kí tự ra (từ) tập tin
fputs/fgets Ghi (đọc) 1 chuỗi ra (từ) tập tin
Xử lý tập tin nhị phân
fwrite Ghi ra tập tin
fread Đọc từ tập tin
fseek Di chuyển con trỏ tới vị trí trong tập tin
ftell Cho biết vị trí hiện tại của con trỏ trong tập tin
1
C Standard Input and Output Library
2
Đây là một kiểu đối tượng trong chuẩn C chứa các thông tin để điều khiển xuất nhập cho tập tin.

71
7.1. Thao tác với tập tin theo kiểu C 72

Các bạn có thể tham khảo thêm thông tin tại http://en.cppreference.com/w/cpp/io/c.

7.1.1 Mở/đóng tập tin


Việc mở/đóng tập tin được thực hiện thông qua các hàm fopen() và fclose().
Ví dụ 7.1: Minh họa thao tác mở/đóng tập tin
# include < iostream >
# include < cstdio >

int main ()
{
FILE * fp ; // Khai bao con tro FILE *
fp = std :: fopen ( " file . txt " ," w " ) ; // Mo mot file co ten la file . txt de
ghi du lieu

if ( fp == NULL ) // Kiem tra tap tin


std :: cout << " Khong mo duoc tap tin " << std :: endl ;
else {
std :: cout << " Tap tin mo thanh cong " << std :: endl ;
std :: fclose ( fp ) ; // Dong file . txt
}
return 0;
}
Trong ví dụ trên chúng ta thấy *fp là một con trỏ của một đối tượng FILE. Khi gọi hàm fopen(),
hàm này sẽ trả về con trỏ tương ứng với đối tượng FILE. Trong trường hợp không mở được tập tin
yêu cầu, chương trình sẽ trả về một con trỏ rỗng (NULL).
Trong hàm fopen(), bên cạnh việc khai báo tên tập tin cần mở, chúng ta còn cần khai báo thêm
kiểu (mode) thao tác với tập tin này. Trong C/C++ có 3 kiểu thao tác sau:
• r (read ): tập tin được mở ra chỉ dùng để đọc (lấy thông tin), tập tin này phải tồn tại trước
đó.
• w (write): tập tin được mở ra chỉ dùng để ghi (xuất thông tin). Trong trường hợp có một
tập tin cùng tên đang tồn tại, thông tin trong tập tin này sẽ bị xóa bỏ, tập tin được xem như
hoàn toàn mới.
• a (append ): tập tin được mở để thêm nội dung vào cuối tập tin. Trong trường hợp không có
tập tin nào cùng tên đang tồn tại, nó sẽ được tạo mới.
Trong trường hợp có thêm dấu ‘+’ phía sau kí tự chỉ kiểu thao tác, chức năng cập nhật (update)
sẽ được thêm vào, ví dụ như
fp = std :: fopen ( " file . txt " ," r + " ) ;
Tập tin file.txt sẽ được mở để đọc thông tin và cập nhật nội dung (vừa đọc, vừa ghi ra tập tin).
Trong trường hợp muốn mở tập tin nhị phân, chúng ta cần thêm kí tự "b" vào phía sau các kí tự
thao tác, ví dụ như
fp1 = std :: fopen ( " file1 " ," rb " ) ;
fp2 = std :: fopen ( " file2 " ," a + b " ) ;

Để xóa tập tin, chúng ta sử dụng hàm remove().


Ví dụ 7.2: Minh họa thao tác xóa tập tin
if ( std :: remove ( " file . txt " ) != 0 )
std :: cout << " Khong xoa duoc tap tin " << std :: endl ;
73 CHƯƠNG 7. TẬP TIN

else
std :: cout << " Tap tin da duoc xoa " << std :: endl ;

7.1.2 Đọc dữ liệu từ tập tin văn bản


Để đọc dữ liệu từ một tập tin văn bản, ta sử dụng các hàm
• Đọc chuỗi theo định dạng không có khoảng trắng từ tập tin
int fscanf(FILE* fp, const char* format)
• Đọc một kí tự từ tập tin
int fgetc(FILE* fp)
int getc(FILE* fp)
• Đọc chuỗi tối đa n−1 kí tự từ tập tin (kết thúc bằng kí tự xuống dòng)
char* fgets(char* buf, int n, FILE* fp)
Ví dụ 7.3: Đọc các số thực từ tập tin so.txt vào mảng
# include < iostream >
# include < cstdio >

int main ()
{
FILE * fp ;
float a [100];
fp = std :: fopen ( " so . txt " ," r " ) ;

// Doc so vao mang a []


int n = 0;
while (! std :: feof ( fp ) ) { // Lap cho den cuoi tap tin
if ( std :: fscanf ( fp , " % f " , & a [ n ]) > 0 )
n ++;
}
std :: fclose ( fp ) ;

// Xuat ket qua ra man hinh


for ( int i = 0; i < n ; ++ i )
std :: cout << " a [ " << i << " ] = " << a [ i ] << std :: endl ;

return 0;
}

Ví dụ 7.4: Đọc chuỗi từ tập tin chuoi.txt


# include < iostream >
# include < cstdio >

int main ()
{
FILE * fp ;
char s [100];
fp = std :: fopen ( " chuoi . txt " ," r " ) ;
std :: fgets (s , 100 , fp ) ; // doc chuoi
std :: cout << s << std :: endl ;
std :: fclose ( fp ) ;
return 0;
}
7.1. Thao tác với tập tin theo kiểu C 74

Bên cạnh việc kiểm tra cuối tập tin bằng hàm feof(), ta cũng có thể sử dụng macro EOF3 .
Ví dụ 7.5: Đọc từng kí tự từ tập tin chuoi.txt
# include < iostream >
# include < cstdio >

int main ()
{
FILE * fp ;
char s [100];
fp = std :: fopen ( " chuoi . txt " ," r " ) ;

int c ; // luu y c co kieu int , khong phai char , de so sanh voi EOF
while ( ( c = std :: fgetc ( fp ) ) != EOF ) {
std :: putchar ( c ) ; // ham ghi ki tu ra man hinh
}
std :: fclose ( fp ) ;
return 0;
}

7.1.3 Ghi dữ liệu ra tập tin văn bản


Để ghi dữ liệu ra một tập tin văn bản, ta sử dụng các hàm
• Ghi chuỗi theo định dạng ra tập tin
int fscanf(FILE* fp, const char* format)
• Ghi một kí tự ra tập tin
int fputc(int c, FILE* fp)
int putc(int c, FILE* fp)
• Ghi chuỗi ra tập tin
char* fputs(const char* buf, FILE* fp)
Ví dụ 7.6: Ghi các số thực từ mảng ra tập tin so.txt
# include < iostream >
# include < cstdio >

int main ()
{
FILE * fp ;
float a [] = {1.0 , 2.3 , -2.1 , 4.9};
fp = std :: fopen ( " so . txt " ," w " ) ;
std :: fputs ( " Cac so tu mang :\ n " , fp ) ;
// Ghi ra tap tin
for ( int i = 0; i < 4; ++ i )
std :: fprintf ( fp , " %.2 f \ n " , a [ i ]) ;
std :: fclose ( fp ) ;
return 0;
}

7.1.4 Đọc/ghi dữ liệu với tập tin nhị phân


Việc đọc/ghi dữ liệu với tập tin nhị phân được thực hiện thông qua các hàm fread() và fwrite()
size_t fread(void* p, size_t size, size_t count, FILE* fp)
3
Biểu thức hằng có kiểu int và mang giá trị âm.
75 CHƯƠNG 7. TẬP TIN

size_t fwrite(const void* p, size_t size, size_t count, FILE* fp)


Ví dụ 7.7: Ghi các kí tự ra tập tin kitu
# include < iostream >
# include < cstdio >

int main ()
{
FILE * fp ;
char c [] = { 'a ' , 'b ' , 'c ' , 'd ' , 'e ' };
fp = std :: fopen ( " kitu " , " wb " ) ;
std :: fwrite ( c , sizeof ( char ) , sizeof ( c ) , fp ) ; // ghi cac ki tu ra tap
tin
std :: fclose ( fp ) ;
return 0;
}

Ví dụ 7.8: Đọc các kí tự từ tập tin kitu


# include < iostream >
# include < cstdio >

int main ()
{
FILE * fp ;
fp = std :: fopen ( " kitu " , " rb " ) ;

// Lay kich thuoc cua tap tin


std :: fseek ( fp , 0 , SEEK_END ) ; // den vi tri cuoi tap tin
long size = std :: ftell ( fp ) ; // tra ve vi tri cuoi , cung la kich thuoc
tap tin
std :: rewind ( fp ) ; // tro ve vi tri dau tap tin

// Doc cac ki tu vao mang


char * c = new char ( size ) ;
std :: fread (c , sizeof ( char ) , size , fp ) ;
std :: fclose ( fp ) ;

// In ra ket qua
for ( int i = 0; i < size ; ++ i )
std :: cout << c [ i ] << std :: endl ;
delete c ;

return 0;
}

Các hàm fseek() và ftell() cũng được sử dụng để di chuyển và xác định vị trí hiện tại của chúng
ta trong quá trình thao tác với tập tin
int fseek(FILE* fp, long offset, int origin)
long ftell(FILE* fp)
Trong đó, origin là vị trí tham chiếu cho offset, có 3 giá trị chính: SEEK_SET (vị trí đầu tập tin),
SEEK_CUR (vị trí hiện tại) và SEEK_END (vị trí cuối tập tin).
Ví dụ 7.9: Ghi các kí tự và số ra tập tin kitu
# include < iostream >
# include < cstdio >

int main ()
7.2. Thao tác với tập tin theo kiểu C++ 76

{
FILE * fp ;
fp = std :: fopen ( " kitu " , " wb " ) ;
std :: fputs ( " abcdef " , fp ) ; // Ghi chuoi " abcdef " ra tap tin
std :: fseek ( fp , 2 , SEEK_SET ) ; // Di chuyen den vi tri ki tu thu 3 tinh
tu dau tap tin
std :: fputs ( " 123 " , fp ) ; // Ghi de chuoi "123" --> " ab123f "
std :: fclose ( fp ) ;
return 0;
}

7.2 Thao tác với tập tin theo kiểu C++


Trong C++ chúng ta có thể thực hiện các thao tác xuất/nhập với tập tin tương tự như với giao
diện màn hình máy tính. Cũng giống như các lớp istream (chứa đối tượng cin) và ostream (chứa
đối tượng cout), đối với thao tác trên tập tin, C++ cũng có
• ifstream: lớp gồm các phương thức đọc tập tin4
• ofstream: lớp gồm các phương thức ghi tập tin5
• fstream: lớp gồm các phương thức đọc/ghi tập tin6

Tên phương thức Chức năng


Các hàm chung
open Mở tập tin
close Đóng (tất cả các) tập tin
is_open Kiểm tra xem tập tin đã mở hay chưa
flush Làm sạch vùng đệm của tập tin đang mở
good Kiểm tra xem tình trạng tập tin có tốt để xử lý hay không
eof Kiểm tra xem tới cuối tập tin chưa
Xử lý tập tin văn bản
<< Ghi ra tập tin
>> Đọc từ tập tin
getline Đọc 1 dòng dữ liệu từ tập tin
get Đọc 1 kí tự hoặc chuỗi từ tập tin
put Ghi 1 kí tự ra tập tin
Xử lý tập tin nhị phân
write Ghi ra tập tin
read Đọc từ tập tin
seekg/seekp Di chuyển con trỏ tới vị trí trong tập tin
tellg/tellp Cho biết vị trí hiện tại của con trỏ trong tập tin

7.2.1 Mở/đóng tập tin


Đầu tiên chúng ta cần khai báo các đối tượng dòng (stream object) tương ứng với các tập tin mà
chúng ta muốn thao tác, ví dụ như
std :: ifstream input_file ; // tap tin dau vao ( input )
std :: ofstream output_file ; // tap tin dau ra ( output )

4
Được kế thừa từ lớp istream.
5
Được kế thừa từ lớp ostream.
6
Được kế thừa từ lớp iostream.
77 CHƯƠNG 7. TẬP TIN

Việc mở/đóng tập tin được thực hiện thông qua các phương thức open() và close()
void open(const char* filename, ios_base::openmode mode)
void close()
Trong đó, đối số mode mặc định cho đối tượng ifstream là ios_base::in và cho đối tượng
ofstream là ios_base::out. Ngoài ra chúng ta còn có các giá trị khác như
• binary: thao tác với tập tin nhị phân.
• ate (at end ): vị trí đầu ra bắt đầu ở cuối tập tin.
• app (append ): tất cả thao tác đầu ra đều được thực hiện ở cuối tập tin (thêm nội dung vào
cuối tập tin).
• trunc (truncate): tất cả nội dung có sẵn trước đó trong tập tin đều bị xoá bỏ.
Ví dụ 7.10: Minh họa thao tác mở/đóng tập tin
# include < iostream >
# include < fstream >

int main ()
{
std :: ofstream ofs ; // doi tuong dong dau ra ( xuat noi dung ra tap tin )
ofs . open ( " out . txt " , std :: ofstream :: out | std :: ofstream :: app ) ; // mo tap
tin out . txt de xuat du lieu , du lieu duoc them vao cuoi tap tin
ofs . close () ; // dong tap tin
return 0;
}

Chúng ta cũng có thể khai báo tên tập tin ngay khi tạo đối tượng dòng thay vì sử dụng phương
thức open()
std :: ofstream ofs ( " out . txt " ) ;

7.2.2 Kiểm tra trạng thái của tập tin


Để kiểm tra xem thao tác mở tập tin đã thành công hay chưa, chúng ta có thể sử dụng phương
thức is_open()
if ( ofs . is_open () ) {
// thuc hien thao tac xu ly tap tin
}

Ngoài ra, chúng ta còn một số phương thức khác để kiểm tra trạng thái của tập tin:
• good(): trả về true nếu không có bit nào bị lỗi và dòng đã sẵn sàng (is_open()).
• bad(): trả về true nếu có bit bị lỗi.
• fail(): trả về true nếu có bit bị lỗi hoặc tập tin không sử dụng được (invalid ).
• eof(): trả về true nếu con trỏ đã tới cuối tập tin.
Thay vì phương thức good, chúng ta cũng có thể sử dụng cách sau để kiểm tra tập tin
if ( ofs ) // tra ve true neu tap tin good
if (! ofs ) // tra ve true neu tap tin khong good

Ví dụ 7.11: Minh họa kiểm tra tập tin


7.2. Thao tác với tập tin theo kiểu C++ 78

# include < iostream >


# include < fstream >

int main ()
{
std :: ifstream ifs ( " input . txt " ) ;
if ( ifs . good () ) {
std :: cout << " Tap tin duoc mo thanh cong " << std :: endl ;
ifs . close () ;
} else
std :: cout << " Co loi xay ra khi mo tap tin " << std :: endl ;
return 0;
}

7.2.3 Đọc/ghi dữ liệu với tập tin văn bản


Để đọc/ghi dữ liệu với các tập tin văn bản, chúng ta sử dụng các toán tử ‘«’ và ‘»’ như với cin
và cout.
Ví dụ 7.12: Minh họa ghi dữ liệu ra tập tin
# include < iostream >
# include < fstream >

int main ()
{
std :: string texts [] = { " Hello " , " ABC " , " 123 " };
std :: ofstream ofs ;
ofs . open ( " out . txt " ) ;
if ( ofs . is_open () ) {
for ( auto text : texts )
ofs << text ;
ofs . close () ;
}
return 0;
}

Ví dụ 7.13: Minh họa đọc chuỗi kí tự từ tập tin văn bản


# include < iostream >
# include < fstream >

int main ()
{
string line ;
std :: ifstream ifs ( " input . txt " ) ;
if ( ifs . is_open () ) {
while (! ifs . eof ) { // lap cho den cuoi tap tin
ifs >> line ; // doc chuoi tu tap tin
std :: cout << line ; // xuat chuoi ra man hinh
}
ifs . close () ;
}
return 0;
}

Ngoài ra, chúng ta cũng có thể sử dụng các hàm getline(), get() và put() để đọc một dòng hay
đọc/ghi một kí tự ra tập tin
79 CHƯƠNG 7. TẬP TIN

istream& getline (istream& is, string& str, char delim)


istream& get (char& c)
ostream& put (char c)

Ví dụ 7.14: Minh họa đọc một dòng kí tự từ tập tin văn bản
# include < iostream >
# include < fstream >

int main ()
{
string line ;
std :: ifstream ifs ( " input . txt " ) ;
if ( ifs . is_open () ) {
while ( getline ( ifs , line ) ) // lap cho den dong cuoi cua tap tin
std :: cout << line ; // xuat chuoi ra man hinh
ifs . close () ;
}
return 0;
}

Ví dụ 7.15: Minh họa ghi kí tự ra tập tin văn bản


# include < iostream >
# include < fstream >

int main ()
{
std :: ofstream ofs ( " out . txt " ) ;
if ( ofs . is_open () ) {
ofs . put ( 'a ') ;
ofs . close () ;
}
return 0;
}

7.2.4 Đọc/ghi dữ liệu với tập tin nhị phân


Việc đọc/ghi dữ liệu với tập tin nhị phân được thực hiện thông qua các phương thức read() và
write()
istream& read (char* s, streamsize n)
ostream& write (const char* s, streamsize n);)
Ví dụ 7.16: Minh họa đọc chuỗi kí tự từ tập tin nhị phân
# include < iostream >
# include < fstream >

int main () {
std :: streampos size ;
char * char_block ;

ifstream ifs ( " input " , ios :: in | ios :: binary | ios :: ate ) ; // mo tap tin
nhi phan , vi tri con tro nam o cuoi tap tin
if ( ifs . is_open () ) {
size = input . tellg () ; // xac dinh kich thuoc tap tin ( vi tri con tro
o cuoi tap tin )
7.2. Thao tác với tập tin theo kiểu C++ 80

char_block = new char [ size ]; // tao chuoi ki tu co kich thuoc bang


kich thuoc tap tin
ifs . seekg (0 , ios :: beg ) ; // di chuyen con tro len dau tap tin
ifs . read ( char_block , size ) ; // doc toan bo ki tu trong tap tin
ifs . close () ;
delete char_block ; // giai phong bo nho char_block
}
return 0;
}

Các hàm seekg()/seekp() và tellg()/tellp() được sử dụng để di chuyển và xác định vị trí hiện
tại của chúng ta trong quá trình thao tác với tập tin đầu vào/đầu ra
streampos tellg()
streampos tellp()
istream& seekg (streamoff off, ios_base::seekdir way)
ostream& seekp (streamoff off, ios_base::seekdir way)
Trong đó, way là vị trí tham chiếu cho off (offset), có 3 giá trị chính: ios_base::beg (vị trí đầu
tập tin), ios_base::cur (vị trí hiện tại) và ios_base::end (vị trí cuối tập tin).
Ví dụ 7.17: Thay đổi chuỗi kí tự ghi ra tập tin
# include < iostream >
# include < fstream >

int main ()
{
std :: ofstream outfile ;
outfile . open ( " vidu . txt " ) ;

outfile . write ( " Hello world ! " ,12) ;


int pos = outfile . tellp () ;
outfile . seekp ( pos -7) ;
outfile . write ( " Phuong " ,6) ;

outfile . close () ;
return 0;
}
Chương 8
Lớp và đối tượng

Bên cạnh các kiểu dữ liệu có sẵn trong C++, người dùng cũng có thể tự mình định nghĩa các kiểu
dữ liệu riêng, đặc biệt là các kiểu dữ liệu có cấu trúc (structured data). Các kiểu dữ liệu có cấu
trúc này giúp người dùng có thể nhóm (group) các thành phần dữ liệu khác nhau vào trong cùng
một kiểu dữ liệu duy nhất.

8.1 Dữ liệu tự định nghĩa


C++ cho phép chúng ta định nghĩa các kiểu dữ liệu của riêng mình dựa trên các kiểu dữ liệu đã có.
Để có thể làm việc đó chúng ta sẽ sử dụng từ khoá typedef, dạng thức như sau
typedef <kiểu dữ liệu đã có> <kiểu dữ liệu mới>
Ví dụ 8.1: Minh hoạ kiểu dữ liệu tự định nghĩa
typedef char * chuoi ;
chuoi loichao ;

8.2 Dữ liệu có cấu trúc


Một cấu trúc dữ liệu là một tập hợp của những kiểu dữ liệu khác nhau được gộp lại với một cái
tên duy nhất. Để định nghĩa các kiểu dữ liệu có cấu trúc ta có thể sử dụng các từ khóa struct,
union hay enum,...
Struct: dạng thức của nó như sau
struct <tên kiểu cấu trúc> {
<kiểu dữ liệu> phần tử 1;
<kiểu dữ liệu> phần tử 2;
...
} <tên biến>;
Ví dụ 8.2: Minh hoạ định nghĩa dữ liệu có cấu trúc
struct sinhvien { // dinh nghia cau truc
char ten [100];
int tuoi ;
}
sinhvien A ; // khai bao bien
sinhvien B , C ;

81
8.2. Dữ liệu có cấu trúc 82

Để truy xuất các phần tử của biến, ta sử dụng dấu ‘.’, ví dụ:
A . tuoi = 20;

Ta cũng có thể định nghĩa các cấu trúc lồng nhau, chẳng hạn như
struct diem {
float toan ;
float anhvan ;
}
struct sinhvien {
char ten [100];
int tuoi ;
diem ketqua ; // bien ketqua thuoc kieu " diem "
}
sinhvien A ;

Sau khai báo trên chúng ta có thể truy xuất


A . ketqua . toan
A . ketqua . anhvan

Ngoài ra, chúng ta cũng có thể sử dụng con trỏ để khai báo biến có cấu trúc
sinhvien * pA = & A ;

Để truy cập phần tử của biến con trỏ ta sử dụng dấu ->
pA - > ketqua . toan

Union: có cấu trúc tương tự struct

union <tên kiểu cấu trúc> {


<kiểu dữ liệu> phần tử 1;
<kiểu dữ liệu> phần tử 2;
...
} <tên biến>;

Tuy nhiên điểm khác biệt của union so với struct là tất cả các phần tử của union đều chiếm cùng
một chỗ trong bộ nhớ, kích thước của nó là kích thước của phần tử lớn nhất. Do đó bất kì một sự
thay đổi nào đối với một phần tử sẽ ảnh hưởng tới tất cả các phần tử còn lại.

Enum: kiểu dữ liệu liệt kê dùng để tạo ra các kiểu dữ liệu chứa một cái gì đó hơi đặc biệt một
chút (không phải kiểu số hay kiểu kí tự), cấu trúc của nó như sau

enum <tên kiểu cấu trúc> {


giá trị 1,
giá trị 2,
...
} <tên biến>;

Ví dụ 8.3: Minh hoạ kiểu dữ liệu enum


enum colors { black , blue , green , cyan , red , purple , yellow , white };
colors mycolor ;
mycolor = blue ;
if ( mycolor == green ) mycolor = red ;
83 CHƯƠNG 8. LỚP VÀ ĐỐI TƯỢNG

Trên thực tế kiểu dữ liệu liệt kê được dịch là một số nguyên, giá trị tương ứng với phần tử đầu
tiên là 0 và các giá trị tiếp theo cứ thế tăng lên 1. Ví dụ trong kiểu dữ liệu mà chúng ta định nghĩa
ở trên, black tương đương với 0 , blue tương đương với 1 , green tương đương với 2 và cứ tiếp tục
như thế. Ngoài ra, chúng ta có thể chỉ định giá trị tương ứng bằng cách gán
enum colors { black =2 , blue , green , cyan , red , purple , yellow , white };

8.3 Lớp
Lớp (class)1 là một kiểu dữ liệu do người dùng tự định nghĩa, có thể bao gồm trong nó các dữ liệu
và hàm. Cách thức khai báo một lớp như sau
class <tên của lớp> {
<kiểu thuộc tính>:
dữ liệu 1;
dữ liệu 2;
hàm 1;
....
} <biến đối tượng>;
Các thành viên (member ) của lớp được chia làm 2 loại: các thành phần chứa dữ liệu của lớp được
gọi là các thuộc tính (attribute) của lớp, các thành phần này thường là các biến; các thành phần
chỉ hành động của lớp được gọi là các phương thức (method ) của lớp, các thành phần này thường
là các hàm.
Thuộc tính của dữ liệu và hàm có các dạng: private (chỉ được truy xuất bởi các thành viên trong
cùng lớp), protected (có thể được truy xuất bởi các thành viên trong cùng lớp hoặc từ các lớp kế
thừa), hoặc public (có thể được truy xuất từ bên ngoài lớp).
Ví dụ 8.4: Minh hoạ khai báo một lớp
class Sinhvien { // Tao mot lop co ten la Sinhvien
private :
char ten [100];
int tuoi ;
float diemthi ;
public :
Sinhvien () {}; // Constructor
~ Sinhvien () {}; // Destructor
void ketqua () {
if ( diemthi >= 5)
cout << " Dau " << endl ;
else
cout << " Rot " << endl ;
}
};

Hàm khởi tạo (constructor ) của một lớp sẽ được gọi tự động khi khai báo một đối tượng mới thuộc
lớp đó, ngược lại hàm hủy (destructor ) sẽ được gọi tự động khi đối tượng đó kết thúc hoạt động
hoặc được giải phóng khỏi bộ nhớ. Một số đặc tính của hàm khởi tạo và hàm hủy:
• Hàm khởi tạo phải có tên trùng với tên của lớp; hàm hủy có tên bắt đầu bằng ∼ và theo sau
là tên của lớp tương ứng.
1
Trong C++, struct và class là gần như nhau, điểm khác biệt duy nhất giữa chúng là các thành viên (member )
của struct được mặc định là dạng public còn các thành viên của class thì mặc định là private. Tuy nhiên, struct trong
C không có hàm thành viên (member function) và chúng ta cũng không thể khởi tạo giá trị trực tiếp cho các biến
thành viên trong C như trong C++ được.
8.4. Hàm bạn và lớp bạn 84

• Cả hai hàm không có giá trị trả về và đều có thuộc tính public.
• Một lớp có thể có nhiều hàm khởi tạo nhưng chỉ có duy nhất một hàm hủy.
Nếu định nghĩa thuộc tính hoặc phương thức bên ngoài phạm vi định nghĩa lớp, ta phải dùng chỉ
thị phạm vi được thể hiện qua dấu ::
class Sinhvien { // Tao mot lop co ten la Sinhvien
private :
char ten [100];
int tuoi ;
float diemthi ;
public :
Sinhvien () {}; // Constructor
~ Sinhvien () {}; // Destructor
void ketqua () ;
};

void Sinhvien :: ketqua () {


if ( diemthi >= 5)
std :: cout << " Dau " << std :: endl ;
else
std :: cout << " Rot " << std :: endl ;
}

Muốn truy xuất các thành viên của đối tượng thuộc một lớp nào đó, ta có thể sử dụng toán tử dấu
. đặt ngay sau đối tượng đó
void main ()
{
Sinhvien A ; // Doi tuong A thuoc lop Sinhvien
A . ten = " Nguyen Van A " ;
A . diemthi = 6.5;
A . ketqua () ;
}

Chú ý: cần phân biệt giữa toán tử dấu chấm (.) dùng để truy cập các thành phần của đối tượng
và toán tử mũi tên (->) dùng để truy cập các thành phần của con trỏ tới đối tượng. Ta có mối
quan hệ như sau
A - > ten = (* A ) . ten

8.4 Hàm bạn và lớp bạn


Nếu một thành viên của lớp được quy định là private hoặc protected thì chỉ có các hàm thành viên
của lớp mới có quyền truy cập đến nó. Nếu một hàm không phải là thành viên của lớp muốn truy
cập đến, thì nó phải là hàm bạn (friend function) hoặc là phương thức của một lớp bạn (friend
class) của lớp đó.
• Hàm bạn: có thể được khai báo nhờ từ khóa friend
class Sinhvien { // Tao mot lop co ten la Sinhvien
private :
char ten [100];
int tuoi ;
float diemthi ;
public :
Sinhvien () {}; // Constructor
~ Sinhvien () {}; // Destructor
85 CHƯƠNG 8. LỚP VÀ ĐỐI TƯỢNG

// Khai bao ham ban


// ham ketqua () luc nay khong phai la thanh vien cua lop Sinhvien
friend void ketqua ( Sinhvien ) ;
};

void ketqua ( Sinhvien A ) {


if ( A . diemthi >= 5)
std :: cout << " Dau " << std :: endl ;
else
std :: cout << " Rot " << std :: endl ;
}

• Lớp bạn: tương tự như hàm bạn với từ khóa friend


class Sinhvien { // Tao mot lop co ten la Sinhvien
private :
char ten [100];
int tuoi ;
float diemthi ;
public :
Sinhvien () {}; // Constructor
~ Sinhvien () {}; // Destructor
// Khai bao lop ban
friend class Kiemtra ;
};

class Kiemtra { // Tao mot lop co ten la Kiemtra


public :
void ketqua ( Sinhvien A ) ;
};

void Kiemtra :: ketqua ( Sinhvien A ) {


if ( A . diemthi >= 5)
std :: cout << " Dau " << std :: endl ;
else
std :: cout << " Rot " << std :: endl ;
}

8.5 Lớp dẫn xuất


Lớp dẫn xuất (derived class) hay còn gọi là lớp con (subclass) là một lớp được kế thừa từ một lớp
khác, cú pháp khai báo lớp này như sau
class <tên lớp dẫn xuất>: <từ khóa dẫn xuất> <tên lớp cơ sở> {
....
};
Trong đó từ khóa dẫn xuất quy định tính chất kế thừa của lớp dẫn xuất từ lớp cơ sở (base class)
• private: các thành viên private của lớp cơ sở không thể truy cập từ lớp dẫn xuất, các thành
viên protected và public của lớp cơ sở trở thành các thành viên private của lớp dẫn xuất.
• protected: các thành viên private của lớp cơ sở không thể truy cập từ lớp dẫn xuất, các
thành viên protected và public của lớp cơ sở trở thành các thành viên protected của lớp dẫn
xuất.
• public: các thành viên private của lớp cơ sở không thể truy cập từ lớp dẫn xuất, các thành
viên protected và public của lớp cơ sở trở thành các thành viên của lớp dẫn xuất với thuộc
tính không thay đổi.
8.6. Một số vấn đề thêm về lớp 86

Ví dụ 8.5: Minh hoạ lớp dẫn xuất


// Lop Sinhvien la dan xuat cua lop Nghenghiep
class Sinhvien : public Nghenghiep {
. . .
}

Khi một đối tượng của lớp dẫn xuất được tạo ra thì hàm khởi tạo của lớp cơ sở được áp dụng
trước rồi mới tới hàm khỏi tạo của lớp dẫn xuất, còn khi đối tượng kết thúc thì hàm hủy của lớp
dẫn xuất được áp dụng trước rồi mới tới hàm hủy của lớp cơ sở.
Về mặt dữ liệu, một lớp dẫn xuất bao giờ cũng chứa toàn bộ dữ liệu của lớp cơ sở, do đó ta có thể
thực hiện phép gán một đối tượng thuộc lớp dẫn xuất cho đối tượng thuộc lớp cơ sở (nhưng không
thể theo chiều ngược lại)
Sinhvien A ;
Nghenghiep B ;
B = A ; // dung
A = B ; // sai

Đa kế thừa một lớp có thể được dẫn xuất từ những lớp cơ sở khác nhau với những kiểu dẫn
dẫn xuất khác nhau, cú pháp như sau
class <tên lớp dẫn xuất>: <từ khóa dẫn xuất 1> <tên lớp cơ sở 1>, .... ,
<từ khóa dẫn xuất N> <tên lớp cơ sở N> {
............... nội dung .................
};
Ví dụ 8.6: Minh hoạ đa kế thừa
// Lop Sinhvien la dan xuat cua hai lop Nghenghiep va Truong
class Sinhvien : public Nghenghiep , public Truong {
. . .
}

Hàm khởi tạo và hủy trong đa kế thừa được khai báo tương tự như trong đơn kế thừa, ngoại trừ
việc phải sắp xếp thứ tự gọi hàm của các lớp cơ sở. Thông thường, thứ tự gọi hàm của các lớp cơ
sở nên tuân theo thứ tự dẫn xuất từ các lớp cơ sở trong đa kế thừa.

8.6 Một số vấn đề thêm về lớp


8.6.1 Con trỏ this
Trong định nghĩa hàm thành viên đôi khi ta cần tham chiếu đến chính đối tượng đang gọi tới nó,
để thực hiện điều này ta có thể sử dụng toán tử this.
Giả sử ta có một lớp
class MyClass {
public :
void showData () const ;
private :
int data ;
};

Hàm thành viên showData() có thể truy cập đến biến thành viên data theo một trong hai cách
sau
87 CHƯƠNG 8. LỚP VÀ ĐỐI TƯỢNG

std :: cout << data ;


// hoac
std :: cout << this - > data ;

Lưu ý: con trỏ this không được dùng cho các phương thức tĩnh (static member function)2 trong
lớp.

8.6.2 Hàm ảo
Hàm ảo (virtual function) hay phương thức ảo (virtual method ) là những hàm hoặc phương thức
mà cách thức hoạt động của nó có thể được tái định nghĩa trong các lớp thừa kế. Hàm ảo được
khai báo với từ khóa virtual.
Ví dụ 8.7: Minh hoạ hàm ảo
Giả sử ta có hai lớp Base và Derived như sau
class Base {
public :
const char * getName () { return " lop co so " ; }
};

class Derived : public Base {


public :
const char * getName () { return " lop dan xuat " ; }
};

int main ()
{
Derived c ;
Base & b = c ;
std :: cout << " b la " << b . getName () << std :: endl ;
}

Kết quả thu được sẽ là


b la lop co so

Đó là bởi vì b là một con trỏ của lớp Base do đó nó sẽ gọi hàm Base::getName() mặc dù cho nó
được trỏ tới đối tượng thuộc lớp Derived. Bây giờ nếu ta thêm từ khóa virtual trước phương
thức Base::getName()
class Base {
public :
virtual const char * getName () { return " lop co so " ; }
};

Lúc này phương thức Base::getName() sẽ là phương thức ảo, chương trình sẽ tìm kiếm xem có
lớp dẫn xuất nào của lớp Base tái định nghĩa lại phương thức này hay không. Kết quả thu được sẽ

b la lop dan xuat

Nếu phần khai báo hàm ảo kết thúc với = 0, nó được gọi là hàm ảo thuần túy (pure virtual function)
class Base {
...
virtual void f () = 0;
2
Xem trong Phần 10.1.3
8.6. Một số vấn đề thêm về lớp 88

...
}

Các hàm ảo phải được định nghĩa (không chỉ là khai báo) ở trong lớp mà nó xuất hiện lần đầu tiên.
Những lớp dẫn xuất từ lớp đó sẽ tiến hành tái định nghĩa những hàm này nếu cần thiết. Nhưng
nếu ta khai báo chúng như các hàm ảo thuần túy, thì ta không phải định nghĩa chúng trong lớp
mà ta khai báo.

8.6.3 Lớp cơ sở trừu tượng


Lớp cơ sở trừu tượng (abstract base class) là lớp được tạo ra chỉ để làm cơ sở cho khác lớp khác.
Đây là lớp chứa các khai báo về các thuộc tính (biến) cũng như các phương thức (hàm) thành
viên, tuy nhiên không có đối tượng nào sẽ được tạo ra bằng cách gọi lớp này. Các lớp cơ sở trừu
tượng chỉ chứa các hàm ảo thuần túy, các hàm này được khai báo trong lớp nhưng không được
định nghĩa, và bắt buộc phải được định nghĩa lại trong lớp dẫn xuất.
class Base { // lop co so truu tuong
public :
virtual const char * getName () = 0; // ham ao thuan tuy
};

class Derived : public Base { // lop dan xuat


public :
const char * getName () { return " lop dan xuat " ; } // tai dinh nghia (
bat buoc )
};

8.6.4 Hàm khởi tạo sao chép


Hàm khởi tạo sao chép (copy constructor ) là hàm khởi tạo mà tạo ra đối tượng bằng cách khởi tạo
giá trị ban đầu của nó với một đối tượng khác thuộc cùng lớp. Nếu hàm khởi tạo sao chép không
được định nghĩa trong lớp, trình biên dịch sẽ tự động tạo nó một cách mặc định. Tuy nhiên nếu
trong lớp có biến con trỏ hay có cấp phát bộ nhớ động thì cần phải có hàm khởi tạo sao chép, hàm
này có dạng thông dụng như sau
<tên lớp> (const <tên lớp> &<đối tượng>) { . . . noi dung . . . }
Hàm khởi tạo copy thường được dùng để
• Khởi tạo giá trị ban đầu của đối tượng bằng một đối tượng khác cùng loại
• Sao chép đối tượng và truyền nó như là một đối số cho hàm
• Sao chép đối tượng và trả nó về từ hàm
Ví dụ 8.8: Hàm khởi tạo sao chép
class Line {
public :
Line ( int len ) ; // ham khoi tao don gian
Line ( const Line & obj ) ; // ham khoi tao sao chep
~ Line () ; // ham huy
private :
int * ptr ;
};

Line :: Line ( int len ) {


ptr = new int ;
89 CHƯƠNG 8. LỚP VÀ ĐỐI TƯỢNG

* ptr = len ;
}

Line :: Line ( const Line & obj ) {


ptr = new int ;
* ptr = * obj . ptr ; // sao chep gia tri
}

Line ::~ Line () {


delete ptr ;
}

int main ( )
{
Line line1 (10) ;
Line line2 = line1 ; // dong thoi cung goi ham khoi tao sao chep
return 0;
}

8.6.5 Sao chép đối tượng


Có hai cách thức sao chép đối tượng chính
• Sao chép ngoài (shallow copy) chỉ sao chép cấu trúc của đối tượng chứ không tạo ra một bản
sao hoàn toàn khác trong chương trình. Ví dụ như ta có một tập hợp A gồm các phần tử {a,
b, c}, B là một sao chép ngoài của A, tập hợp B này cũng có các phần tử như tập hợp A.
Tuy nhiên nếu ta xóa phần tử a trong tập hợp A thì ở phần tử B cũng chỉ còn {b, c} do nó
cùng chứa tại một vị trí trong bộ nhớ chứ không phải tạo một bản sao hoàn chỉnh của các
phần tử bên trong của tập hợp.
• Sao chép hoàn toàn (deep copy) sao chép không chỉ cấu trúc mà mọi thứ trong đối tượng đó
thành một đối tượng hoàn toàn mới. Trở lại với ví dụ ở trên, tập hợp B sẽ chứa tất cả các đối
tượng {a, b, c} nhưng nó là một thực thể độc lập so với tập hợp A. Điều đó cũng có nghĩa là
nếu ta xóa đi phần từ bên tập hợp A thì trong tập hợp B vẫn không có gì thay đổi.
Ví dụ 8.9: Minh hoạ sao chép dữ liệu
MyString str1 ( " Welcome " ) ;
MyString str2 = str1 ; // shallow copy ( goi ham khoi tao sao chep )

MyString str1 ( " Welcome " ) ;


MyString str2 ;
str2 = str1 ; // deep copy ( goi toan tu gan )

Bởi vì C++ không biết về lớp mà chúng ta xây dựng nên các hàm khởi tạo sao chép và toán tử gán
mặc định sẽ là shallow copy (hay còn gọi là memberwise copy). Ta cũng có thể thực hiện deep copy
bằng cách xây dựng tường minh hàm khởi tạo sao chép, chẳng hạn như trong ví dụ về hàm khởi
tạo sao chép phía trên
Line :: Line ( const Line & obj ) {
ptr = new int ;
* ptr = * obj . ptr ; // deep copy
}

Còn shallow copy sẽ là


Line :: Line ( const Line & obj ) {
* ptr = * obj . ptr ; // shallow copy
}
8.6. Một số vấn đề thêm về lớp 90

8.6.6 Danh sách khởi tạo


Giả sử ta có một lớp
class MyClass {
public :
MyClass ( int x ) { data = x }; // ham khoi tao
private :
int data ;
};

Thay vì thực hiện lệnh gán bên trong hàm khởi tạo, ta có thể sử dụng danh sách khởi tạo (con-
structor initialization list) như sau
class MyClass {
public :
MyClass ( int x ) : data ( x ) { } // danh sach khoi tao
private :
int data ;
};

Nếu ta có thành viên của lớp có cùng tên với đối số ví dụ như
class MyClass {
public :
MyClass ( std :: string data ) { this - > data = data ; }
private :
std :: string data ;
};

tương đương với danh sách khởi tạo


class MyClass {
public :
MyClass ( std :: string data ) : data ( data ) { }
private :
std :: string data ;
};

Nếu có nhiều thành viên trong lớp


MyClass ( std :: string data ) : data ( data ) , var ( var ) { }

Trong trường hợp thành viên hằng, việc sử dụng danh sách khởi tạo là cần thiết do thành viên này
chỉ được định nghĩa duy nhất 1 lần đầu tiên
class MyClass {
public :
MyClass () : data (10) { }
// chuong trinh se bao loi neu khai bao nhu ben duoi
// MyClass () : { data = 10 }
private :
const int data ;
};
Chương 9
Thư viện chuẩn

Thư viện chuẩn (Standard Template Library − STL) được xây dựng đầu tiên bởi Alexander
Stepanov (1979) với mục đích phát triển phương pháp lập trình tổng quát (generic programming).
Thư viện đầu tiên được xây dựng với ngôn ngữ lập trình Ada bởi Stepanov và Musser năm 1987,
tuy nhiên ngôn ngữ Ada lại không được phát triển mạnh nên họ đã chuyển sang ngôn ngữ C++.
Với sự giúp đỡ của Meng Lee và Andrew Koenig, bộ thư viện chuẩn đã được hoàn thiện và đưa vào
chuẩn ANSI/ISO C++ từ năm 1994. Ngoại trừ lớp string, tất cả các thành phần còn lại của STL
đều dưới dạng các template.

9.1 Template
Template thường được sử dụng trong lập trình tổng quát (generic programming), đây là một từ
khóa của C++ nhằm thông báo cho trình biên dịch biết rằng các hàm, các lớp, hay các xử lý liên
quan sẽ được quyết định sử dụng kiểu dữ liệu nào cho phù hợp trong khi biên dịch. Khi đó trình
biên dịch sẽ để lại các xử lý đó tới khi có dữ liệu thực sự được tạo ra và sẽ tạo ra một khuôn mẫu
chung cho các xử lý. Khi xử lý đó được gọi với những kiểu dữ liệu đã xác định thì trình biên dịch
lúc đó sẽ tự biết dùng xử lý nào với kiểu dữ liệu được truyền vào một cách phù hợp nhất.
Giả sử ta có hai hàm tìm số lớn hơn trong hai số
// Voi kieu int
int maxInt ( int a , int b ) {
return ( a > b ? a : b ) ;
}

// Voi kieu double


int maxDouble ( double a , double b ) {
return ( a > b ? a : b ) ;
}

Cả hai hàm đều có chung một cách giải quyết vấn đề, chỉ khác ở kiểu dữ liệu được truyền vào và
trả về. Mặc dù ta có thể sử dụng hàm maxDouble() cho cả hai xử lý tuy nhiên kiểu dữ liệu trả về
không được như mong muốn. Để tránh điều đó, ta có thể gom hai kiểu dữ liệu thành 1 kiểu duy
nhất và xây dựng 1 hàm duy nhất
template < class T >
T maxNumber ( T a , T b ) {
return ( a > b ? a : b ) ;
}

91
9.2. Các thành phần chính của STL 92

Và ta có thể khai báo kiểu dữ liệu cụ thể sau đó


int main () {
int a = 10 , b = 5;
std :: cout << maxNumber < int > (a , b ) << std :: endl ; // voi kieu int

double c = 3.3 , d = 2.0;


std :: cout << maxNumber < double > (c , d ) << std :: endl ; // voi kieu double

return 0;
}

Để tránh nhầm lẫn với từ khóa class trong khai báo lớp, ta có thể sử dụng từ khóa typename
template < typename T >
T maxNumber ( T a , T b ) {
return ( a > b ? a : b ) ;
}

9.2 Các thành phần chính của STL


Một số thành phần chính của STL gồm có

• Container : là các đối tượng chứa những đối tượng khác (cấu trúc dữ liệu của template), vd:
vector, set, map,...

• Iterator : giống con trỏ, dùng để truy nhập các phần tử dữ liệu của các container.

• Algorithm: các thuật toán để thao tác dữ liệu như tìm kiếm (find), sắp xếp (sort),...

• Functor : các toán tử hàm.

• Adapter : các bộ thương thích container phục vụ cho những nhu cầu đặc biệt.

• Ultility: các ứng dụng của STL, gồm có hai phần: các ứng dụng cho ngôn ngữ (lập trình) và
ứng dụng đa mục đích.

• Numeric: bộ các hàm và kiểu dữ liệu phục vụ cho việc tính toán, xử lý các mảng số.

• String: thư viện chuỗi.

• Stream: các lớp hỗ trợ các phép toán cho dòng xuất nhập.

• Thư viện C : các thư viện ngôn ngữ C.

• ...

Lưu ý: các file thư viện chuẩn của C++ đều không có phần mở rộng .h, ví dụ
# include < string > // thu vien chuan C ++
# include < string .h > // thu vien chuan C

Các thư viện chuẩn C được đưa vào trong bộ thư viện chuẩn C++ STL với kí tự ’c’ ở đầu
# include < cstring > // thu vien chuan C ++ cua cac ham C

Phần dưới sẽ trình bày một số lớp thông dụng của STL.
93 CHƯƠNG 9. THƯ VIỆN CHUẨN

9.3 Container
Container hay còn gọi là các lớp chứa, là các lớp template có cấu trúc dùng để lưu trữ các kiểu dữ
liệu khác nhau. Có hai loại container chính

• Container tuyến tính (sequential container ) có cấu trúc dữ liệu tuyến tính, ví dụ vector,
list,...

• Container liên kết (asociative container ) có cấu trúc dữ liệu phi tuyến, ví dụ map, set,...

Ngoài ra chúng ta còn có các bộ tương thích container (container adapter ) như stack, queue,...

Các phương thức chính của container gồm có

size() số lượng phần tử


empty() trả về 1 nếu container rỗng, 0 nếu ngược lại
max_size() số lượng phần tử tối đa được cấp phát
== trả về 1 nếu hai container giống nhau
=! trả về 1 nếu hai container khác nhau
begin() trả về con trỏ đến phần tử đầu tiên của container
end() trả về con trỏ đến phần tử cuối cùng của container
front() trả về tham chiếu đến phần tử đầu tiên của container
back() trả về tham chiếu đến phần tử cuối cùng của container
swap() hoán đổi hai container

Dưới đây sẽ trình bày một số loại container thông dụng.

9.3.1 Vector

Vector định nghĩa một mảng động trong C++ (tương tự như array), đặc điểm của vector là có thể
tự động cấp phát bộ nhớ. Cách thức khai báo một vector như sau

vector <kiểu dữ liệu> tên_vector (kích thước)


hoặc vector<kiểu dữ liệu> tên_vector

Ví dụ 9.1: Khai báo vector


std :: vector < int > v ;
std :: vector < float > v1 (100) ;
std :: vector < float > v2 ( v1 ) ; // v2 la copy cua v1
vector < float > v3 ( v1 . begin () , v1 . begin () + 5) ; // v3 la copy cua 5 phan tu
dau tien v1

Lưu ý rằng ta cần khai báo #include<vector> để có thể sử dụng được kiểu vector.

Để đưa giá trị vào phần tử trong vector ta sử dụng phương thức push_back() (để bỏ phần tử cuối
cùng của vector ta sử dụng pop_back()) và để truy xuất giá trị phần tử ta sử dụng phương thức
at() (hoặc dùng cặp ngoặc vuông [] như trong array).
std :: vector < int > myvector ;
int number ;
std :: cout << " Nhap vao mot so nguyen : " ;
std :: cin >> number ;
myvector . push_back ( number ) ;
std :: cout << myvector . at (0) << std :: endl ;
9.4. Iterator 94

Để cấp phát vùng nhớ cho vector ta có thể sử dụng phương thức reserve() (trong trường hợp ta
biết chắc kích thước vùng nhớ cần thiết cho vector), việc làm này sẽ giúp hạn chế vector tự cấp
phát vùng nhớ không cần thiết1 .
std :: vector < int > myvector ;
std :: vector < int >:: size_type sz ; // size_type tuong tu nhu size_t
sz = myvector . capacity () ; // kich thuoc vung nho cap phat cho vector
myvector . reserve (100) ; // cap phat vung nho co kich thuoc 100

Cách thức tạo một vector 2 chiều


vector < vector <int > > matr ;
vector <int > newColumn ;
matr . push_back ( newColumn ) ;
matr . at ( rowNumber ) . push_back ( intValue ) ;

9.3.2 Map
Map cho phép ta lấy tương ứng giữa một giá trị với một giá trị khác, hai giá trị này tạo thành một
cặp giá trị. Trong đó giá trị đầu của cặp là key, giá trị của key là duy nhất (không có 2 key cùng
xuất hiện trong 1 map).
std :: map < char , int > m ;
m [ 'a ' ]=10;
m [ 'b ' ]=30;
m [ 'c ' ]=50;
m [ 'd ' ]=70;
std :: map < char , int > m1 ( m . begin () ,m . end () ) ;
std :: map < char , int > m2 ( m ) ;

Tìm key trong map


std :: map < string , int > diem = {{ " A " , 10} ,{ " B " ,8}};
auto search = diem . find ( " A " ) ;
if ( search != diem . end () ) {
std :: cout << " Tim thay diem cua " << search - > first << " la " << search - >
second << std :: endl ;
} else {
std :: cout << " Khong tim thay " << std :: endl ;
}

9.4 Iterator
Iterator là khái niệm sử dụng để chỉ một con trỏ trỏ đến các phần tử trong 1 container, mỗi
container có một loại iterator khác nhau.
Trong thư viện STL thì người ta tích hợp lớp đối tượng iterator cùng với các container, ta có thể
khai báo iterator như là một phần nằm trong container, các phương thức begin() và end() sẽ trả
lại con trỏ kiểu đối tượng iterartor đến phần tử đầu tiên và cuối cùng của container.
Có tất cả 5 loại iterator
1
Khi một vector được khai báo, một vùng nhớ sẽ được cấp phát cho vector. Khi vector này đã chiếm đầy vùng
nhớ được cấp phát mà vẫn tiếp tục nhận thêm phần tử, một vùng nhớ mới sẽ được tạo ra (thường có kích thước gấp
đôi vùng nhớ cũ), hệ điều hành sẽ copy dữ liệu ở vùng nhớ cũ sang vùng nhớ mới và tiếp tục công việc nhận thêm
phần tử. Quá trình cấp phát vùng nhớ mới này tiếp diễn liên tục và đôi khi sẽ làm chậm chương trình, đặc biệt trong
trường hợp các vector có kích thước lớn, thậm chí có thể gây ra lỗi memory leak nghiêm trọng.
95 CHƯƠNG 9. THƯ VIỆN CHUẨN

• Random access iterator (RandIter): chứa và nhận giá trị, các thành phần có thể truy xuất
ngẫu nhiên.
• Bidirectional iterator (BiIter): chứa và nhận giá trị, di chuyển tới trước và sau.
• Forward iterator (ForIter): chứa và nhận giá trị, chỉ cho phép di chuyển tới.
• Input iterator (InIter): nhận nhưng không chứa giá trị, chỉ cho phép di chuyển tới.
• Output iterator (OutIter) chứa nhưng không nhận giá trị, chỉ cho phép di chuyển tới.
Ta có thể truy xuất đến các thành phần của container bằng cách sử dụng iterator
< container > coll ;
for ( < container >:: iterator it = coll . begin () ; it != coll . end () ; ++ it )
{
... noi dung ...
}

Ví dụ 9.2: Sử dụng iterator


std :: vector < int > v ;
std :: vector < int >:: iterator it ;

// Them phan tu vao vector


v . push_back (1) ;
v . push_back (4) ;
v . push_back (8) ;

for ( it = v . begin () ; it != v . end () ; ++ it ) {


std :: cout < < * it << std :: endl ;
}

Nếu một container được khai báo với từ khóa const thì chúng ta phải dùng const_iterator thay
vì iterator
const vector < string > v ;
vector < string >:: const_iterator i = v . begin () ;

9.5 Functor
Functor hay function object (đối tượng hàm) là một đối tượng được sử dụng như một hàm (hay
nói cách khác đó là ta có một lớp mà có định nghĩa operator() trong lớp đó). Vì là một đối tượng
nên các functor có trạng thái, trong khi các hàm bình thường thì không, do đó các functor có thể
ứng xử khác nhau tùy vào trạng thái, tạo nên sự linh hoạt hơn so với các hàm bình thường.
Ví dụ:
class add_x {
private :
int x ;
public :
add_x ( int x ) : x ( x ) {}
int operator () ( int y ) { return x + y ; }
};

Chúng ta có thể thay đổi trạng thái của functor


add_x add12 (12) ; // tao phuong thuc add12 () : cong 12 vao voi doi so
add_x add30 (30) ; // tao phuong thuc add30 () : cong 30 vao voi doi so
int i = add12 (8) ; // i = 20
9.6. Algorithm 96

hoặc truyền functor như là một đối số (hàm std::transform sẽ biết tự động gọi add_x::operator
())
std :: vector < int > in ; // gia su cac vector nay chua cac phan tu
std :: vector < int > out ;
// out [ i ] == in [ i ] + 1
std :: transform ( in . begin () , in . end () , out . begin () , add_x (1) ) ;

Một số functor cơ bản (để sử dụng được cần khai báo #include <functional>) như cộng (plus),
trừ (minus), nhân (muliplies),...
int first []={1 ,2 ,3 ,4 ,5};
int second []={10 ,20 ,30 ,40 ,50};
int results [5];
std :: transform ( first , first +5 , second , results , std :: plus < int >() ) ;

9.6 Algorithm
Nhằm giúp cho người dùng không phải viết lại những giải thuật quá cơ bản như sắp xếp, thay thế,
tìm kiếm,... thư viện STL có cung cấp một số thuật toán cơ bản, để sử dụng các thuật toán này
cần khai báo #include <algorithm>
• Các thuật toán tìm kiếm
// find
int myints [] = { 10 , 20 , 30 , 40 };
int * p ;
p = std :: find ( myints , myints +4 , 30) ;
std :: cout << " Phan tu tim thay : " << * p << std :: endl ;

// find_if
bool IsOdd ( int i ) { return (( i %2) ==1) ; }
std :: vector < int >:: iterator it = std :: find_if ( myvector . begin () ,
myvector . end () , IsOdd ) ;
std :: cout << " Phan tu le dau tien duoc tim thay la " << * it << std ::
endl ;

// search
std :: vector < int >:: iterator it = std :: search ( myvector . begin () ,
myvector . end () , myints , myints +4) ;
std :: cout << " Mang myints duoc tim thay tai vi tri " << ( it - myvector .
begin () ) << std :: endl ;

• Các thuật toán sắp xếp


sort ( v . begin () ,v . end () ) ; // sap xep theo thu tu tang dan
sort ( v . begin () ,v . end () , greater < int >() ) ; // sap xep theo yeu cau

• Các thuật toán trên tập hợp


// includes
int A [] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 };
int B [] = { 1 , 4 , 7 };
const int N1 = sizeof ( A ) / sizeof ( int ) ;
const int N2 = sizeof ( B ) / sizeof ( int ) ;
if ( std :: includes (A , A + N1 , B , B + N2 ) )
std :: cout << " A chua B " << std :: endl ;
97 CHƯƠNG 9. THƯ VIỆN CHUẨN

// set_union , set_intersection , set_difference


int first [] = {5 ,10 ,15 ,20 ,25};
int second [] = {50 ,40 ,30 ,20 ,10};

vector < int > v (10) ;


vector < int >:: iterator it ;

sort ( first , first +5) ;


sort ( second , second +5) ;

vector < int >:: iterator union_it = set_union ( first , first +5 , second ,
second +5 , v . begin () ) ; // hop
vector < int >:: iterator inter_it = set_intersection ( first , first +5 ,
second , second +5 , v . begin () ) ; // giao
vector < int >:: iterator diff_it = set_difference ( first , first +5 , second ,
second +5 , v . begin () ) ; // lay ra cac phan tu khong giong nhau

• Các thuật toán tìm min/max


const int x = min (3 , 9) , y = max (3 , 9) ; // min / max truong hop co 2 so

int A [] = {3 ,4 ,2 ,6 ,3 ,1 ,2 ,3 ,2 ,3 ,4 ,5 ,6 ,4 ,3 ,2 ,1};
int N = sizeof ( A ) / sizeof (* A ) ;
std :: cout << " So nho nhat trong mang : " << * min_element (A , A + N ) << std
:: endl ;
std :: cout << " So lon nhat trong mang : " << * max_element (A , A + N ) << std
:: endl ;

• Thuật toán biến đổi


int increase ( int i ) { return ++ i ;}
vector < int > v2 ;
v2 . resize ( v1 . size () ) ;
// tang tat ca phan tu v1 len 1 don vi va gan vao v2
transform ( v1 . begin () , v1 . end () , v2 . begin () , increase ) ;

• Thuật toán copy


int a [] = {1 , 2 , 3 , 4 , 5 , 6};
int n = sizeof ( a ) / sizeof (* a ) ;
vector < int > v1 (a , a + n ) ;
vector < int > v2 ( n ) ;
copy ( v1 . begin () , v1 . end () , v2 . begin () ) ;
copy ( V . begin () , V . end () , ostream_iterator < int >( cout , " " ) ) ;
9.6. Algorithm 98
Chương 10
Một số vấn đề nâng cao

10.1 Cách sử dụng một số từ khóa


10.1.1 Từ khoá const
Đây là từ khóa dùng để khai báo hằng, một số công dụng của từ khóa này chẳng hạn như
Khai báo hằng số
const float PI = 3.14;

Việc gọi hàm Function() sẽ không làm thay đổi đối tượng thuộc lớp Class
void Class :: Function () const

Việc truyền đối số cho hàm Function() sẽ không làm thay đổi argument
void Class :: Function ( Argument const & argument )

Không thể thay đổi đối tượng được trả về bởi hàm Function()
Result const & Class :: Function ()

Để loại bỏ ảnh hưởng của const, ta có thể dùng từ khóa mutable đặt trước đối tượng.

10.1.2 Từ khoá extern


Đây là từ khóa dùng để báo cho trình biên dịch biết một đối tượng đã được định nghĩa trong file
khác và được dùng như là một biến toàn cục trong toàn bộ chương trình. Chẳng hạn như ta có hai
file
/* File1 . cpp */
int x = 1;

/* File2 . cpp */
extern int x ; // bao cho trinh bien dich biet bien x da duoc khai bao o
noi khac
int main ()
{
std :: cout << x << std :: endl ;
return 0;
}

99
10.1. Cách sử dụng một số từ khóa 100

Trong trường hợp biến cục bộ có tên trùng với tên của biến toàn cục trong chương trình, thì đó
biến cục bộ sẽ được ưu tiên sử dụng. Tuy nhiên, nếu ta muốn sử dụng biến toàn cục thì ta có thể
sử dụng toàn tử :: để báo cho trình biên dịch biết biến mà nó đang sử dụng là biến toàn cục.
int x = 1;

int main ()
{
int x = 5;
std :: cout << :: x << std :: endl ; // xuat ra gia tri la 1
return 0;
}

Trong trường hợp khi dùng cụm từ extern ``C'' sẽ báo cho trình biên dịch biết rằng các hàm
trong danh sách phải được biên dịch theo dạng C, tức là không được dùng các phương pháp thay
đổi tên hàm (name mangling) của C++. Lý do chính phải dùng lệnh này là khi biên dịch mã nguồn
C++ thành các thư viện động, các hàm có thể được gọi bởi một ngôn ngữ khác (chẳng hạn như
Python) nên nếu biên dịch theo kiểu C++ thì ngôn ngữ kia không nối (link ) được1 . Nếu ta chỉ cần
nối các module C++ với nhau thôi thì không cần sử dụng lệnh này.
Ví dụ 10.1: Một số cách sử dụng extern ``C''
extern " C " void f ( int ) ;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
extern " C "
{
void g ( char ) ;
int i ;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
extern " C " {
# include " C_header . h "
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ifdef __cplusplus
extern " C " {
# endif
/* ... */
# ifdef __cplusplus
}
# endif

10.1.3 Từ khoá static


Khi sử dụng từ khóa này để khai báo một biến bên trong hàm, biến này sẽ tồn tại cho đến cuối
chương trình (biến tĩnh)
for ( int x =0; x <10; x ++) {
for ( int y =0; y <10; y ++) {
static int number = 0;
number ++;
}
}

Công dụng thứ hai của từ khóa static là ở bên trong phần định nghĩa lớp. Các thành viên của lớp
được khai báo với từ khóa static ở phía trước được gọi là các thành viên tĩnh (static member ),
1
Xem thêm trong phần 6.1 của tài liệu “Giới thiệu ngôn ngữ lập trình Python” (http://goo.gl/OyjKiB)
101 CHƯƠNG 10. MỘT SỐ VẤN ĐỀ NÂNG CAO

dữ liệu của các thành viên tĩnh của lớp được xem như là toàn cục của lớp đó. Do vậy mọi sự thay
đổi dữ liệu thành viên tĩnh của đối tượng này đều có tác dụng lên toàn bộ các dữ liệu thành viên
tĩnh của các đối tượng khác.
class Item {
private :
static int next_item ;
public :
int getItem () { return next_item ++; }
};

int Item :: next_item = 1;

int main ()
{
Item first ;
Item second ;
std :: cout << first . getItem () << std :: endl ; // next_item = 2
std :: cout << second . getItem () << std :: endl ; // next_item = 3
return 0;
}

Lưu ý: các phương thức tĩnh trong lớp chỉ được truy cập đến các thành viên dữ liệu tĩnh của lớp
đó.

10.2 Định dạng dòng dữ liệu xuất


Để định dạng các dòng dữ liệu xuất, chúng ta có thể sử dụng thư viện iomanip (bằng cách thêm
vào dòng chỉ thị tiền xử lý #include<iomanip>). Dưới đây là một số định dạng thông dụng trong
C++
• Định dạng độ rộng (width) của dữ liệu xuất (std::setw(n))
# include < iostream >
# include < iomanip >

int main () {
std :: cout << std :: setw (8) << " 1000000\ n " ;
std :: cout << std :: setw (8) << " 1\ n " ;
std :: cout << std :: setw (8) << " 10000\ n " ;
std :: cout << std :: setw (8) << " 1000000000\ n " ;
return 0;
}

/* Ket qua :
1000000
1
10000
1000000000
*/

• Định dạng trình bày dữ liệu (std::fixed, std::scientific)


# include < iostream >

int main () {
double a = 3.14159;
double b = 1.0 e -5;
10.2. Định dạng dòng dữ liệu xuất 102

std :: cout . precision (3) ; // dinh dang do chinh xac ( so chu so sau dau
thap phan )

std :: cout << " fixed :\ n " << std :: fixed ;


std :: cout << a << '\ n ' << b << '\ n ';

std :: cout << '\ n ';

std :: cout << " scientific :\ n " << std :: scientific ;


std :: cout << a << '\ n ' << b << '\ n ';
return 0;
}

/* Ket qua :
fixed :
3.142
0.000

scientific :
3.142 e +00
1.000 e -05
*/

• Tự động điền kí tự vào khoảng trống được tạo ra bởi std::setw(n) (std::setfill(ch))

# include < iostream >


# include < iomanip >

int main () {
std :: cout << std :: setfill ( ' - ') << std :: setw (8) ;
std :: cout << 100 << std :: endl ;
return 0;
}

/* Ket qua :
- - - - -100
*/

• Định dạng thập phân (std::dec), bát phân (std::oct), thập lục phân (std::hex)

# include < iostream >

int main () {
int n = 100;
std :: cout << std :: dec << n << '\ n ';
std :: cout << std :: hex << n << '\ n ';
std :: cout << std :: oct << n << '\ n ';
return 0;
}

/* Ket qua :
100
64
144
*/
103 CHƯƠNG 10. MỘT SỐ VẤN ĐỀ NÂNG CAO

10.3 Cách tổ chức mã nguồn


10.3.1 Dự án
Đối với một chương trình C++ đơn giản thì toàn bộ nội dung của chương trình có thể được chứa
trong một file duy nhất. Tuy nhiên đối với các chương trình lớn có cấu trúc phức tạp, thường là
các dự án (project), ta cần phải chia nhỏ mã nguồn ra thành nhiều file để quản lý. Việc chia nhỏ
này có nhiều lợi ích
• Tăng tốc độ biên dịch: hầu hết các trình biên dịch làm việc trên 1 file trong 1 lúc, do đó cho
dù ta chỉ thực hiện một thay đổi nhỏ trong file, ta cũng phải biên dịch lại toàn bộ file. Mặt
khác, nếu ta chia mã nguồn ra thành nhiều file, thì chỉ yêu cầu file có sự thay đổi được biên
dịch lại.
• Tăng cường sự tổ chức: việc phân chia mã nguồn một cách hợp lý sẽ giúp ta dễ dàng hơn
trong việc tìm kiếm những hàm, biến, định nghĩa các cấu trúc/lớp,. . . đặc biệt khi ta cần
xem lại 1 đoạn mã để tìm kiếm cái gì đó.
• Giảm bớt sự viết lại : nếu mã nguồn được phân chia cẩn thận thành những đoạn độc lập với
nhau, ta có thể sử dụng lại chúng trong các dự án khác, tiết kiệm thời gian phải viết lại lần
sau.
• Chia sẻ mã nguồn giữa các dự án: tương tự như việc giảm bớt sự viết lại, lợi ích của việc chia
sẽ file này so với sử dụng copy-paste là bất cứ lỗi nào được sửa trong 1 hay nhiều file trong
1 dự án sẽ có tác dụng với những dự án khác, và tất cả các dự án có thể chắc chắn được cập
nhật bản mới nhất.
• Phân chia trách nhiệm giữa các lập trình viên: trong những dự án lớn thật sự thường có
nhiều hơn 1 lập trình viên cùng viết chung mã nguồn. Do đó, ta sẽ phải chia làm nhiều file
để mỗi lập trình viên có thể làm việc trên từng phần riêng biệt mà không ảnh hưởng đến các
lập trình viên khác.

10.3.2 File header


Thông thường mã nguồn được phân chia theo dạng các ‘module’ (đôi khi 1 module có thể chia ra
thành 2 hay nhiều file), tạo những file mới với những tên có nghĩa sẽ giúp người dùng biết nội dung
bên trong khi nhìn lướt qua. Thông thường trong C++, người ta chia mỗi lớp vào một file riêng,
như vậy sẽ tiện hơn thay vì cho tất cả vào một chỗ, và các file này thường có tên trùng với tên của
lớp. Để tách theo cách này, ta cần sử dụng các file header (thường có đuôi .h, .hpp, .hxx ), đây là
các file cho phép định nghĩa các thành phần của chương trình ở các file riêng, và khi cần sử dụng
lại thì có thể gọi dễ dàng bằng cách đưa vào chương trình qua lệnh #include.
Nội dung của file header thường gồm có
• Các chỉ thị tiền xử lý
• Định nghĩa lớp hoặc cấu trúc, template
• Định nghĩa kiểu typedef
• Định nghĩa hàm
• Biến toàn cục
• Hằng số
Giả sử chúng ta muốn định nghĩa một lớp MyClass, ta sẽ tạo một file header MyClass.h có nội
dung như sau
10.3. Cách tổ chức mã nguồn 104

class MyClass {
public :
void func () ;
int data ;
};

File mã nguồn MyClass.cpp có nội dung


# include " MyClass . h "

void MyClass :: func () {


std :: cout << " Hello " << std :: endl ;
}

Mỗi file mã nguồn của lớp MyClass cần phải #include ``MyClass.h''. Lưu ý rằng ta nên dùng
ngoặc kép “ ” hơn là ngoặc nhọn <> khi include những file header, dấu ngoặc kép sẽ cho trình biên
dịch biết phải tìm kiếm header trong thư mục chương trình trước, rồi mới đến các header chuẩn
của trình biên dịch.
File mã nguồn chính main.cpp có nội dung
# include " MyClass . h " // dinh nghia MyClass

int main ()
{
MyClass a ;
a . func () ;
return 0;
}

Những file header sẽ trở thành phần chung giữa các module hay lớp con. Bằng cách #include
một header, ta có thể truy xuất đến toàn bộ những định nghĩa cấu trúc, tên hàm, hàng số. . . của
module hoặc lớp tương ứng.
Một số lỗi thường gặp khi biên dịch với header2
• Không tìm thấy định nghĩa cần thiết: điều này sẽ làm cho file mã nguồn không biên dịch
được bởi vì có một số những định nghĩa chưa được khai báo do không được include từ các
file header. Giả sử ta có 3 file (Header1.h, Header2.h và File.cpp) như sau
/* Header1 . h */
class ClassOne { ... };

/* Header2 . h */
# include " Header1 . h "
class ClassTwo { ... };

/* File . cpp */
# include " Header2 . h "
ClassOne myClassOne ;
ClassTwo myClassTwo ;

Trong trường hợp này, File.cpp sẽ biên dịch tốt, vì đã include Header2.h và gián tiếp include
Header1.h, có nghĩa là File.cpp có thể truy xuất đến lớp ClassOne. Nhưng nếu một thời gian
sau, ai đó cho rằng Header2.h không cần thiết phải include Header1.h và xoá dòng include
đó đi, hậu quả là File.cpp sẽ không thể biên dịch được. Do đó, ta cần phải dứt khoát khi
2
Cách thức biên dịch chương trình với file header được hướng dẫn ở Phần 6 trong tài liệu “Nhập môn hệ điều
hành Linux” (http://goo.gl/11gQjF)
105 CHƯƠNG 10. MỘT SỐ VẤN ĐỀ NÂNG CAO

include bất kì header nào cần cho file mã nguồn để biên dịch, không nên chỉ dựa vào những
file header include gián tiếp mà có thể sẽ thay đổi.
Tuy nhiên việc này đôi khi lại dẫn tới một số lỗi sẽ được trình bày tiếp theo sau.
• Phụ thuộc vòng tròn: khi những header xuất hiện khi cần include lẫn nhau để làm việc một
cách có dụng ý. Chẳng hạn như ta có hai lớp ClassOne và ClassTwo phụ thuộc lẫn nhau
/* Header1 . h */
# include " Header2 . h "
class ClassOne { ClassTwo two ; };

/* Header2 . h */
# include " Header1 . h "
class ClassTwo { ClassOne one ; };

Thật ra lớp ClassOne không cần phải biết chi tiết của lớp ClassTwo, do nó chỉ sử dụng con
trỏ của lớp ClassTwo chứ không sử dụng toàn bộ đối tượng của lớp này. Con trỏ không cần
biết nó chỉ đến đâu, do đó ta không cần phải định nghĩa cấn trúc hoặc lớp để chứa con trỏ.
Điều này có nghĩa là dòng #include ở đây là không cần thiết.
Tuy nhiên, khi trình biên dịch hoạt động, khi biên dịch lớp ClassOne nó sẽ cần tới ClassTwo
mà lúc này chưa được biên dịch do nó cũng cần tới ClassOne được biên dịch (nên nhớ là trình
biên dịch mỗi lúc chỉ hoạt động với 1 file), do đó sẽ gây ra lỗi “Undeclared identifier”. Để khắc
phục lỗi này, thay vì sử dụng include, ta sẽ sử dụng khai báo trước (forward declaration)
/* Header1 . h */
class ClassTwo ; // khai bao truoc ClassTwo
class ClassOne { ClassTwo two ; };

/* Header2 . h */
class ClassOne ; // khai bao truoc ClassOne
class ClassTwo { ClassOne one ; };

• Định nghĩa chồng: khi 1 lớp hoặc cấu trúc được gọi 2 lần trong 1 file nguồn. Điều này sẽ gây
ra lỗi trong thời gian biên dịch (compile-time error ) và thường xuất hiện khi gọi nhiều file
header trong 1 file header khác, làm cho header được gọi 2 lần khi bạn biên dịch file nguồn.
Để khắc phục được lỗi này ta sử dụng các include guard để đảm bảo rằng một header file
chỉ được include một lần trong chương trình. Chẳng hạn như file MyClass.h lúc này sẽ có nội
dung
# ifndef MYCLASS_H // neu MyClass chua duoc dinh nghia
# define MYCLASS_H // thi dinh nghia MyClass

class MyClass {
public :
void func () ;
int data ;
};

# endif

10.4 Một số nâng cấp, bổ sung trong C++11


Bên cạnh một số tính năng mới bổ sung hoặc nâng cấp như đã được trình bày trong những chương
trước, chuẩn C++11 cũng có một số thay đổi quan trọng chẳng hạn như
10.4. Một số nâng cấp, bổ sung trong C++11 106

10.4.1 Tính toán song song

Trước đây các tính toán song song với ngôn ngữ C++ được thực hiện chủ yếu thông qua các tiện
ích của hệ điều hành (OS facilities) chẳng hạn như thư viện pthread (POSIX thread ) của các hệ
điều hành họ Unix, hay các thư viện MPI3 , OpenMP,...
Tuy nhiên kể từ chuẩn C++11 trở đi, tính năng hỗ trợ lập trình song song đa luồng (multithread )
đã được tích hợp vào trong ngôn ngữ C++. Để có thể sử dụng được tính năng này ta cần sử dụng
thư viện thread
Ví dụ 10.2: Tính toán song song với C++11
# include < iostream >
# include < thread >

static const int num_threads = 10;

// Ham nay se duoc goi boi cac tac vu


void call_from_thread ( int tid ) {
std :: cout << " Launched by thread " << tid << std :: endl ;
}

int main () {
std :: thread t [ num_threads ];

// Khai bao cac tac vu


for ( int i = 0; i < num_threads ; ++ i ) {
t [ i ] = std :: thread ( call_from_thread , i ) ;
}
std :: cout << " Launched from the main \ n " ;

// Dua cac tac vu vao trong tac vu chinh


for ( int i = 0; i < num_threads ; ++ i ) {
t [ i ]. join () ;
}

return 0;
}

Để biên dịch chương trình chạy song song ta gõ lệnh sau


g ++ - std = c ++11 - pthread file_name . cpp

với các phiên bản trình biên dịch cũ hơn gcc4.7, ta thay thế tùy chỉnh -std=c++11 bằng -std=c
++0x.

10.4.2 Các hàm begin() và end() tổng quát

Hầu như tất cả các lớp chứa của STL đều có các hàm thành viên là begin() và end() để trả về
iterator cho phép truy cập tới các phần tử của lớp. C++11 đưa ra hai hàm begin() và end() không
phải là thành viên của bất cứ lớp chứa nào và có thể dễ dàng được nạp chồng cho bất cứ lớp hay
một mảng nào.
for ( auto it = v . begin () ; it != v . end () ; ++ it ) // C ++03
for ( auto it = begin ( v ) ; it != end ( v ) ; ++ it ) // C ++11

3
Xem thêm tài liệu “Cơ bản lập trình song song MPI cho C/C++” (http://goo.gl/J8kIJA)
107 CHƯƠNG 10. MỘT SỐ VẤN ĐỀ NÂNG CAO

10.4.3 Alternative function


Trong C++03 thì template như ở dưới không hợp lệ vì Lhs, Rhs và kiểu trả về không xác định được
kiểu
template < class Lhs , class Rhs >
Ret adding_func ( const Lhs & lhs , const Rhs & rhs ) { return lhs + rhs ;}

Kiểu Ret là tổng của kiểu Lhs và Rhs Với type-inference. Trong C++11 mọi việc trở nên đơn giản
hơn với từ khóa decltype và cú pháp trailing-return-type
template < class Lhs , class Rhs >
auto adding_func ( const Lhs & lhs , const Rhs & rhs ) -> decltype ( lhs + rhs ) {
return lhs + rhs ;}

10.4.4 Khởi tạo theo kiểu danh sách


Trong các chuẩn C++ cũ, ta có khởi tạo giá trị ban đầu cho mảng hay chuỗi bằng cách sử dụng
cặp ngoặc nhọn, chẳng hạn như
char s []={ 'H ' , 'e ' , 'l ' , 'l ' , 'o ' , '\ n ' };
int arr [] = {1 , 2 , 3 , 4 , 5};

Tuy nhiên ta không thể làm điều này với các container mà phải thêm lần lượt từng phần tử vào
std :: vector < int > v ;
for ( int i =0; i <5; i ++) {
v . push_back ( arr [ i ]) ;
}

Với chuẩn C++11, việc khởi tạo giá trị ban đầu bằng cặp ngoặc nhọn được chấp nhận
std :: vector < int > v {1 , 2 , 3 , 4 , 5};

Ví dụ 10.3: Một số ví dụ cho việc khởi tạo giá trị ban đầu
class C {
public :
C ( int i , int j ) ;
};

C c {0 ,0}; // tuong duong voi C c (0 ,0) ;

int * a = new int [3] {1 , 2 , 0};

class X {
int a [4];
public :
X () : a {1 ,2 ,3 ,4} {} // khoi tao gia tri ban dau cho mang a
};

10.4.5 Các hàm default và delete


C++11 bổ sung các từ khóa default và delete trên các hàm thành viên của lớp, cho phép một
hàm khai báo với default được gọi tự động khi khởi tạo và ngăn chặn các hàm khai báo với delete
được sử dụng.
Ví dụ của việc sử dụng từ khóa default
10.4. Một số nâng cấp, bổ sung trong C++11 108

struct A {
int x ;
A ( int x = 1) : x ( x ) {} // ham khoi tao do nguoi dung dinh nghia
};

struct B : A {
B ( int y ) : A ( y ) {}
// ham khoi tao B :: B () khong duoc dinh nghia boi vi da co dinh nghia
roi
};

struct C : A {
C ( int y ) : A ( y ) {}
C () = default ; // ham khoi tao C :: C () duoc dinh nghia va ham nay se
goi A :: A ()
};

B b; // bao loi bien dich


C c; // chap nhan

Ví dụ của việc sử dụng từ khóa delete


struct NoInt {
void f ( double i ) ;
void f ( int ) = delete ; // khong su dung ham nay
};

f (0.5) ; // chap nhan


f (3) ; // bao loi vi 3 co kieu int

10.4.6 Ủy nhiệm hàm khởi tạo


Trong C++11, một hàm khởi tạo có thể gọi một hàm khởi tạo khác trong cùng một lớp.
Ví dụ:
class M {
int x , y ;
char * p ;
public :
M ( int v ) : x ( v ) , y (0) , p ( new char [ MAX ]) {} // ham khoi tao thu 1
M () : M (0) // ham khoi tao thu 2 , goi ham thu 1: M ( int v ) voi v = 0
};

10.4.7 Tham chiếu rvalue


(rvalue reference) Trong C++, các đối tượng ở bên trái và bên phải của toán tử gán (=) được gọi
là lvalue và rvalue. Khi thực hiện, trình biên dịch sẽ tính giá trị của rvalue sau đó gán giá trị tính
được vào địa chỉ của giá trị lvalue, như vậy về bản chất phép gán là thao tác lưu kết quả vào tham
chiếu (qua tên) tới một đối tượng. Nói cách khác, bất kì một biểu thức nào (như lệnh gán, lời gọi
hàm . . . ) cần lưu kết quả vào một tham chiếu, nó sẽ là một lvalue, ngược lại bất kì biểu thức nào
trả về giá trị dưới dạng một đối tượng thì đó chính là một rvalue.
Vấn đề của các rvalue là chúng có thời gian tồn tại rất ngắn trong chương trình, do chỉ dùng cho
các biến tạm, trung gian nên cần phải gán chúng cho các lvalue để có thể dùng lại sau này. Khái
109 CHƯƠNG 10. MỘT SỐ VẤN ĐỀ NÂNG CAO

niệm tham chiếu rvalue ra đời giúp kéo dài tuổi thọ của các đối tượng rvalue và đồng thời cũng
giúp cho việc lập trình hiệu quả hơn.
Mục đích chính của việc sử dụng tham chiếu rvalue là kĩ thuật move semantics, trong đó thay vì
tạo ra một bản sao của đối tượng và gán vào địa chỉ của lvalue, trình biên dịch sẽ chuyển toàn bộ
nội dung của rvalue cho lvalue, điều này giúp tránh được việc loại bỏ rvalue ra khỏi bộ nhớ, cũng
như việc thực hiện copy dữ liệu.
Ví dụ:
std :: vector < BigType > v1 ;
for ( int i = 0; i < n ; ++ i ) {
BigType obj ;
v1 . push_back ( obj ) ;
}

Trong ví dụ trên ta có đối tượng obj có kích thước lớn (BigType), mỗi khi phương thức push_back
() được thực hiện, một bản sao của obj sẽ được tạo ra, và sau đó được dùng để tạo ra đối tượng
v1[i] qua hàm khởi tạo copy (copy constructor ). Tiếp đến đối tượng bản sao cũng sẽ bị hủy. Có
thể thấy rằng việc truyền đối tượng obj như vậy sẽ là một thao tác tốn thời gian (cấp phát bộ
nhớ, copy dữ liệu) vì kích thước của nó có thể rất lớn. Thay vào đó ta thực hiện
std :: vector < BigType > v1 ;
for ( int i = 0; i < n ; ++ i ) {
BigType obj ;
v1 . push_back ( std :: move ( obj ) ) ;
}

Chương trình sẽ tiến hành chuyển toàn bộ nội dung của obj cho v1[i], với std::move(obj) là
một tham chiếu rvalue.
Trong trường hợp chúng ta muốn đưa kĩ thuật move vào trong lớp, ta cần khai báo một hàm khởi
tạo move (move constructor ) và toán tử move (move assignment operator ) như sau
class MyClass {
MyClass ( MyClass &&) ; // ham khoi tao cho ki thuat move
MyClass && operator =( MyClass &&) ; // toan tu move
};

10.4.8 Các hàm emplace


Đối với lớp vector, C++11 giới thiệu hai hàm mới là emplace_back() và emplace_front() (tương
tự như push_back() và push_front()) cho phép các tham số được dùng khi khởi tạo các đối tượng
của một lớp chứa có thể được dùng theo kiểu in-place, tức là một cách trực tiếp để tạo ra một đối
tượng mà lớp chứa quản lý thay vì tạo ra đối tượng rồi mới sao chép (copy) hoặc di chuyển (move)
giống như hàm push_back().
Ví dụ 10.4: So sánh sự khác biệt giữa emplace_back và push_back
# include < iostream >
# include < vector >
# include < string >

class Person {
public :
std :: string name ;
int age ;
public :
Person ( std :: string name , int age ) : name ( name ) , age ( age ) {}
10.4. Một số nâng cấp, bổ sung trong C++11 110

~ Person () {}
};

int main ()
{
std :: vector < Person > list = {};

list . push_back ( Person ( " Anh " , 10) ) ; // ok


list . push_back ( " Bao " , 11 ) ; // error

list . emplace_back ( Person ( " Anh " , 10) ) ; // ok


list . emplace_back ( " Bao " , 11 ) ; // ok

return 0;
}
Tài liệu tham khảo

[1] Trần Đình Quế, Nguyễn Mạnh Hùng, Ngôn ngữ lập trình C++ , Học viện Công nghệ Bưu
chính Viễn thông (2006).
[2] H.W. Bell, C++ Programming for Physicists (2009).
[3] H. Watson, Intro to Standard Template Library,
http://web.eng.fiu.edu/watsonh/eel3160/Lectures/STL-Lectures.pdf
[4] Ebook team (updatesofts.com), C++ cơ bản và nâng cao
[5] http://en.wikipedia.org/wiki/Standard_Template_Library
[6] http://en.wikipedia.org/wiki/Anonymous_function
[7] http://vi.wikipedia.org/wiki/C++
[8] http://vi.wikipedia.org/wiki/C++11
[9] http://vi.wikipedia.org/wiki/Tr%C3%ACnh_bi%C3%AAn_d%E1%BB%8Bch
[10] http://vi.wikipedia.org/wiki/L%E1%BA%ADp_tr%C3%ACnh_h%C6%B0%E1%BB%9Bng_%C4%
91%E1%BB%91i_t%C6%B0%E1%BB%A3ng
[11] http://vi.wikipedia.org/wiki/Th%C6%B0_vi%E1%BB%87n_chu%E1%BA%A9n_C%2B%2B
[12] http://vi.wikipedia.org/wiki/Ng%C3%B4n_ng%E1%BB%AF_l%E1%BA%ADp_tr%C3%ACnh
[13] http://moriator.wordpress.com/2007/11/06/cach-t%E1%BB%95-ch%E1%BB%A9c-file-c-
va-c/
[14] http://moriator.wordpress.com/2007/11/07/cach-t%E1%BB%95-ch%E1%BB%A9c-file-c-
va-c-ph%E1%BA%A7n-2/
[15] http://sinhvienit.net/@tut/lap-trinh-phan-mem/c-ascii-c/43-bai-5.2-
namespaces.html
[16] http://diendan.congdongcviet.com/threads/t46021::cach-su-dung-cac-stl-
container.cpp
[17] http://blog.smartbear.com/c-plus-plus/the-biggest-changes-in-c11-and-why-
you-should-care/
[18] http://www.pcworld.com.vn/articles/cong-nghe/cong-nghe/2014/03/1234591/chuan-
c-11-nhu-mot-ngon-ngu-lap-trinh-moi/
[19] http://root.cern.ch/drupal/content/cling
[20] http://isocpp.org/std/status

111
Tài liệu tham khảo 112

[21] http://www.functionx.com/cpp/index.htm
[22] http://www.cplusplus.com/info/history/
[23] http://www.cplusplus.com/doc/tutorial/preprocessor/
[24] http://www.gotw.ca/gotw/009.htm
[25] http://www.fotech.org/forum/index.php?showtopic=36547
[26] http://www.cplusplus.com/forum/articles/10627/
[27] http://cs.brown.edu/~jak/proglang/cpp/stltut/tut.html
[28] http://www.cim.mcgill.ca/~mpersson/cxx_diagrams.html
[29] http://www.cs.uic.edu/~jbell/CourseNotes/CPlus/FileIO.html
[30] http://www.cplusplus.com/doc/tutorial/polymorphism/
[31] https://www.scribd.com/doc/63794401/Stl
[32] https://solarianprogrammer.com/archives/

You might also like