You are on page 1of 131

COMMENT

// Bỏ qua thứ đứng bên phải => comment dòng

Có thể bất hoạt 1 nhóm các dòng lệnh bằng cách biến nó thành ghi chú hết: bôi đen các dòng lệnh đó =>
Ctrl K Ctrl C

/* */ Bỏ qua thứ đứng bên trong => comment đoạn, hoặc chen giữa câu lệnh

#if 0

#end if Chương trình không chạy những thứ bên trong => na ná comment đoạn

 Thay if 0 thành if 1, chương trình chạy bình thường


 Hiểu nôm na là hàm if với condition true (1) thì những thứ trong nó được thực hiện, còn false (0)
thì những thứ bên trong không được thực hiện

BIẾN

int n Định nghĩa biến n

int n=6 hoặc int n(6) hoặc int n{ 6 } Định nghĩa biến n và gán giá trị cụ thể

 Có thể định nghĩa nhiều biến trên 1 dòng lệnh: int n, m;

int n=6, m=7;

int kiểu số nguyên, double kiểu số thực


char n; signed char m; unsigned int k; …

setprecision(10) Lấy độ chính xác 10 chữ số (bao gồm trước và sau dấu phẩy)

Lệnh này thuộc thư viện <iomanip>, muốn dùng phải định nghĩa #include <iomanip>

 Cùng định nghĩa 1 số nhưng cho 2 kết quả khác nhau vì độ chính xác khác nhau

Kiểu float có độ chính xác rất thấp, không chỉ ảnh hưởng đến hàng thập phân mà còn hàng trước dấu
thập phân => hạn chế dùng

 Số chấm động còn gặp phải lỗi làm tròn:

 Có nhiều cách định nghĩa biến cho kiểu dữ liệu char


Ghi chú 1: Bảng mã ASCII

 static_cast<a>(b) Ép kiểu dữ liệu a của kí tự b

Bảng mã ASCII chỉ đến mã số 127, nếu ép kiểu với số lớn hơn sẽ dẫn đến tràn số, xuất ra kí tự lỗi
 Có một số ký tự có thể đóng vai trò phím tắt của một số câu lệnh đặc biệt, dùng trong chuỗi
hoặc mảng kí tự, gọi là Escape Sequence

Bởi vì \n cũng đẩy con trỏ xuống dòng, nó có vai trò na ná lệnh endl.

Tuy nhiên, endl khiến vùng nhớ đệm bị “xả” mỗi khi chương trình biên dịch tới nó còn \n thì không, khi
làm việc với lượng lớn câu lệnh xuất ra, nên dùng \n để chương trình biên dịch nhanh hơn.

 Tương tự char, có nhiều cách định nghĩa cho biến luận lý boolean, bao gồm 1 cách khác mà char
không có là định nghĩa phủ định:

Nếu cout các biến b1, b2… trên sẽ xuất hiện giá trị 1 hoặc 0.

cout <<true; cout <<!true; //in 1 và 0 ra màn hình

Để hiện ra “true” hoặc “false”, ta dùng lệnh boolalpha và dùng noboolalpha để trả về trạng thái cũ:
Khi định nghĩa biến số nguyên với kiểu boolean, mọi giá trị khác 0 mặc định là true

NHẬP – XUẤT DỮ LIẸU

 cout << … xuất dữ liệu


o Đối với kiểu số nguyên, nếu nhập vào số thực sẽ ép kiểu ngầm định số nguyên dưới theo
hàm sàn, nếu nhập chữ cái sẽ cho ra giá trị rác ban đầu được gán vào khi định nghĩa
biến
o Khi dòng xuất gồm nhiều kiểu dữ liệu khác nhau (chữ cái, biến…), ngăn cách chúng bới
dấu <<

cin >> … nhập dữ liệu

 Có thể thực hiện nhập nhiều giá trị cùng 1 lúc. Khi này sau mỗi dữ liệu nhập có thể enter hoặc
dùng phím cách, sau dữ liệu cuối cùng thì enter:

HẰNG SỐ

 Tương tự như biến số, có nhiều cách định nghĩa hằng số với lệnh const, đứng trước hoặc sau
kiểu dữ liệu của hằng số:

 Ngoài ra, có thể khởi tạo hằng số bằng chỉ thị tiền xử lí #define

o Ưu điểm: không như định nghĩa bằng const, #define không khiến máy lưu giá trị hằng
vào bộ nhớ RAM => ít tốn dung lượng hơn. #define như 1 phép thay thế hoàn toàn 1
cụm ký tự bằng 1 giá trị cụ thể
o Nhược điểm: có thể #define 1 giá trị nhiều lần => làm việc quy mô lớn có thể xảy ra lỗi
giá trị không mong muốn
 Một hằng số khi định nghĩa phải được gán kèm một giá trị ngay, và không được thay đổi giá trị
hoặc định nghĩa hằng lại (trong hình trên giá hằng số trị pi được định nghĩa 4 lần nên phải đặt 4
tên khác nhau)

 Có thể khởi tạo hằng số bằng giá trị của 1 biến:

 Thường trong 1 project lớn, các hàm số được định nghĩa đầu chương trình, ngoài hàm
main(phạm vi toàn cục) hoặc trong 1 namespace khác của 1 header khác mà trong main.cpp sẽ
#include <header> ấy

TOÁN TỬ

 1 biểu thức, hay còn gọi thực thể toán học bao gồm 2 thành phần:
o Toán hạng: các biến số, hằng số hoặc lời gọi hàm
o Toán tử: cách thức làm việc giữa các toán hạng
 Có 3 loại toán tử:
o Toán tử 1 ngôi (Unary)

o Toán tử 2 ngôi (Binary)


Để thực hiện được phép chia hoàn chỉnh, ví dụ 9/6 cho ra 1.5, ta phải biến ít nhất 1 số thành số chấm
động và sử dụng dấu /. Có thể biến 1 số nguyên thành số chấm động bằng cách nhân với 1.0, dù là số
hay biến.

o Toán tử 1 ngôi tăng/giảm (increment/decrement operators), có 2 loại:

 Tiền tố (prefix): tăng/giảm giá trị biến x, rồi dùng x để tính toán

 Hậu tố (postfix): dùng x để tính toán, rồi tăng/giảm giá trị biến x

 Toán tử gán

 Toán tử quan hệ
o Dùng để so sánh 2 toán hạng với nhau. Trả về hoặc 1 (true) hoặc 0 (false)
o Toán tử so sánh bằng ==, khác với lệnh gán bằng =
o Không dùng toán tử quan hệ để so sánh 2 số chấm động, vì kết quả hầu hết các trường
hợp đều không chính xác do lỗi làm tròn
 Toán tử logic (logical operators)
o Kết hợp với toán tử quan hệ để thực hiện kiểm tra nhiều điều kiện cùng lúc

o Các điều kiện nên để trong dấu ngoặc () để tránh nhầm lẫn và không cần quan tâm đến
độ ưu tiên toán tử

 Toán tử trên bit (Bitwise operators)

 Toán tử hỗn hợp (Misc operators)


o Toán tử sizeof
o Toán tử phẩy (comma operator)
 Giúp thực hiện lần lượt các phép tính khi viết trên cùng 1 dòng, lần lượt từ trái
sang phải
 2 cách viết dưới tương đương nhau:

o Toán tử điều kiện (conditional operator)


 (c) ? x : y nếu c khác 0 (true) thì thực hiện x, không thì thực hiện y
 Có thể dùng như một cách khác của hàm điều kiện IF. 2 cách viết sau tương đương nhau:

 Bản thân x và y có thể là những toán tử nhỏ hơn. 2 cách viết sau tương đương nhau:

 Không như câu điều kiện IF, toán tử điều kiện có thể lồng vào để định nghĩa hằng
số (trường hợp này chỉ có thể dùng toán tử điều kiện) :


o Một số toán tử khác:

 Độ ưu tiên (operator precedence) và quy tắc kết hợp toán tử (associativity rules):
o Trong 1 phép toán, compiler sẽ thực hiện các phép tính đối với toán tử có độ ưu tiên cao
hơn
o Khi các toán tử có cùng độ ưu tiên, các quy tắc kết hợp sẽ cho compiler biết phải thực hiện
chúng từ trái sang phải (L->R) hay phải sang trái (R->L)
CHUỖI KÝ TỰ

 Chuỗi ký tự trong C++ được định nghĩa trong thư viện string và trong namespace std, nên để
dùng chúng phải #include <string>
 Có nhiều cách khởi tạo giá trị cho chuỗi ký tự:

 Để nhập dữ liệu chuỗi bao gồm cả khoảng trắng cho biến a, dùng hàm getline(cin, a,) thay vì cin >> a,
nếu không biến sẽ chỉ nhận các giá trị cho đến khoảng cách đầu tiên
 Vấn đề nhập xuất dữ liệu chuỗi:
o Cho chương trình sau:

 Đối với chương trình này, khi nhập tuổi rồi Enter chương trình sẽ kết thúc mà không
cho nhập tên, đồng thời việc hiển thị cũng có lỗi.
 Đó là vì khi nhập 10 tuổi, bộ nhớ đệm lấy từ bàn phím 3 kí tự “1”, “0” và “enter”.
Trong đó, kí tự “1” và “0” được đưa cho biến “age”, để lại trong bộ nhớ đệm chỉ còn kí
tự “enter”. Hàm getline(cin, name) sẽ lấy tất cả các dữ liệu trong bộ nhớ đệm trước
“enter” ấy (lúc này đang không có gì) và đưa cho biến “name” => biến “name” đang
rỗng và màn hình xuất ra như trên.
o Có thể sửa chương trình theo 2 cách:
 Dùng hàm cin.ignore(n, ‘m’) để hoặc dừng lại sau khi bỏ đi n kí tự trong bộ nhớ đệm,
hoặc sau khi bỏ đi kí tự mang lệnh ‘m’ (m gọi là kí tự phân tách) trong ấy. Đối với
“enter” làm xuống dòng, lệnh tương ứng sẽ là ‘\n’
Khi này sau khi nhập 10 tuổi và bộ nhớ đệm chỉ còn kí tự “enter”, chương trình sẽ bỏ
đi 1 kí tự “enter” ấy và hàm getline(cin, name) sẽ không kích hoạt gán giá trị cho biến
“name” phía sau

Ghi chú 2: bộ nhớ đệm lưu được tối đa 32767 kí tự, nên có thể lấy n=32767

 Dùng hàm getline(cin, name) thay vì cin >> age để nhập giá trị cho biến “age”. Lúc này
kí tự “enter” trong bộ nhớ đệm sẽ được sử dụng trong việc gán kí tự “1” và “0” vào
biến tuổi, rồi bộ nhớ đệm sẽ trống, không còn “enter” để hàm getline(cin, name) phía
sau kích hoạt gán giá trị cho biến “name”
o Nếu giá trị nhập cho chuỗi là 1 đoạn văn, ta cần xuống dòng trong quá trình nhập mà dùng nút
“enter” không khiến kết thúc nhập dữ liệu, dùng hàm đầy đủ getline(cin, a, ‘m’). Khi này, việc
nhập dữ liệu chỉ kết thúc khi nhấn “enter” sau phím “m”. Kí tự phân tách mặc định của hàm
getline(cin, a) là ‘\n’
 Để nối các chuỗi lại với nhau, dùng các toán tử trực tiếp lên các biến chuỗi:

Ghi chú 3: vì lệnh c=a+b đi trước nên c hiển thị tính theo a trước khi biến đổi thành aA

 Dùng hàm a.length() hoặc a.size() để xuất ra số kí tự chuỗi của biến a:

 Chuyển số thành chuỗi với hàm _itoa_s():

o “num” là số nguyên cần chuyển thành chuỗi ký tự


o “target” là chuỗi đích để chứa
o “base” là cơ số chuyển đổi, nhằm xác định một giá trị số nguyên

 Chuyển chuỗi thành số:


o Chuyển thành số nguyên với hàm atoi() / atol():

 “str” là chuỗi cần chuyển thành số.


 Trường hợp chuỗi chỉ định chứa cả thành phần có thể và không thể chuyển sang
kiểu int thì hàm atoi() sẽ chỉ chuyển phần string có thể chuyển thành int tính từ
đầu chuỗi.
 Nếu không thể chuyển sang kiểu int thì giá trị 0 sẽ được trả về.
o Tương tự, chuyển sang số chấm động với hàm atof()

BIẾN CỤC BỘ, BIẾN TOÀN CỤC và BIẾN TĨNH

 Biến cục bộ (Local variables)


o Một khối lệnh bắt đầu bằng { và kết thúc bằng }

o Một câu lệnh trong khối lệnh lớn không thể dùng biến ở khối lệnh con nhỏ hơn, nhưng
câu lệnh trong khối lệnh con có thể dùng biến định nghĩa từ đầu ở khối lệnh lớn bao nó:
o Một biến nằm trong khối lệnh vì sẽ bị phá hủy khi khối lệnh đo kết thúc, nên dù có gọi lại
cả khối lệnh đó ở nơi khác, biến đó vẫn không xài được:

Ghi chú 4: Khi gọi lại khối lệnh "someFunction", mọi thứ sẽ diễn ra lại từ đâu: biến "value" được sinh ra, gán giá trị 5 vào biến
"value" và biến "value" bị phá hủy => sau đó không dùng lại được biến này.

Ghi chú 5: Nhưng nếu trong hàm “someFunction” ta thực hiện cout << value, khi gọi hàm đó trong khối “main” và xuất chương
trình ta vẫn nhận được giá trị 5, vì điều đó được thực hiện trước khi biến bị xóa

o Các biến ở các khối lệnh khác nhau có thể trùng tên, vì chúng hoạt động độc lập và bị
phá hủy ngay khi khối lệnh chứa nó kết thúc.
o Thậm chí biến trong một khối lệnh có thể cùng tên với một biến khác của khối lệnh con
đang lồng trong nó. Khi biên dịch đến khối lệnh con, chương trình sẽ tự ẩn biến của khối
lệnh ngoài để làm việc với biến trong. Sau khi khối lệnh con kết thúc, chương trình quay
lại làm việc với biến ngoài:

o Nếu chỉ dùng một biến trong phạm vi một khối lệnh thì nên định nghĩa biến ngay bên
trong khối lệnh đó để dễ quản lý bộ nhớ và hiệu quả. Ví dụ nếu chỉ cần dùng biến “a”
trong vòng lặp For, ta có thể đóng hàm For ấy vào một khối lệnh và định nghĩa biến “a”
trong phạm vị khối lệnh ấy.

 Biến toàn cục (Gloabal variables)

o Quy ước định nghĩa biến toàn cục ở đầu tập tin, bên dưới lệnh gọi thư viện và
namespace

o Nếu biến toàn cục trùng tên một biến cục bộ bên trong một khối lệnh, khi biên dịch đến
khối lệnh đó, chương trình sẽ tự ẩn biến toàn cục bên ngoài và làm việc với biến với giá
trị trong khối lệnh. Muốn làm việc với biến toàn cục bên ngoài ấy, ta dùng toán tử phân
giải phạm vi ::

o Đối với các chương trình lớn với nhiều hàm từ nhiều nguồn được gọi ra, nên hạn chế sử dụng
biến toàn cục (non-constant). Bất kì hàm nào từ nguồn khác đều tiềm ẩn nguy cơ tác động lên
biến toàn cục của mình => nếu xảy ra sai sót với hàng trăm hàm khác nhau sẽ rất khó khăn để
sửa chữa.
o Sử dụng biến cục bộ bên trong một hàm sẽ giúp ta làm việc độc lập chỉ trong hàm ấy. Biến
toàn cục của ta không bị ảnh hưởng bởi các biến cục bộ bên ngoài, vì nếu lỡ có trùng tên, trình
biên dịch vẫn sẽ ưu tiên biến cục bộ khi biên dịch đến khối lệnh của mình.
 Biến tĩnh (Static variables)
o Khi sử dụng từ khóa static trước các biến cục bộ, nó sẽ trở thành biến tĩnh. Biến tĩnh chỉ
được thực hiện gọi duy nhất 1 lần xuyên suốt chương trình.

o Kết quả cho ra như thế bởi vì biến sau mỗi lời gọi hàm “doSomeThing”, biến cục bộ
“value” được khởi tạo lại với giá trị 0, cộng 1 vào rồi xuất ra. Trong khi dù có 3 lời gọi
hàm “doSomeThing_static”, biến tĩnh “s_value” chỉ được khởi tạo với giá trị 0 một lần
duy nhất, những lần còn lại chương trình chỉ cộng 1 vào nó mà không khởi tạo.
o Biến tĩnh có 2 dạng: liên kết ngoài (external linkage) và liên kết nội bộ (internal linkage):
 Biến toàn cục cũng có thể được xem là biến tĩnh liên kết ngoài, với từ khóa extern
đi kèm khi định nghĩa => có thể sử dụng ở mọi nơi trong và ngoài file
 Biến toàn cục lại là biến tĩnh liên kết nội bộ, với từ khóa static đi kèm khi định nghĩa
=> có thể sử dụng ở mọi nơi trong file, nhưng vô hiệu khi ra ngoài file
 Biến cục bộ là biến tĩnh liên kết nội bộ, với từ khóa static đi kèm khi định nghĩa
o Vì biến tĩnh không bao giờ định nghĩa trùng lắp lại nên có thể dùng để làm ID hoặc đánh số thứ
tự:
Ghi chú 6: thay ++s_id thành s_id+=1 đều như nhau

Ghi chú 7: nếu thay static int s_id thành int s_id thì số nhả ra sẽ là 1 1 1, vì giá trị các biến sẽ được khởi tạo lại từ đầu

ÉP KIỂU DỮ LIỆU
Có 2 loại ép kiểu:
 Ép kiểu ngầm định (Implicit type conversion)
o Là quá trình chuyển đổi giữa các kiểu dữ liệu cơ sở (số nguyên, số chấm động, luận lý…)
một cách ngầm định, tức trình biên dịch tự động chuyển đổi mà không cần lập trình viên
can thiệp
o Chuyển đổi giá trị từ một kiểu dữ liệu sang kiểu dữ liệu tương tự khác lớn hơn thường an toàn
và không mất dữ liệu:

Ghi chú 8: 1 là số nguyên integer, tương tự và nhỏ hơn long. Khi gán giá trị nguyên vào biến "a" có kiểu long, compiler ngầm
định ép kiểu integer sang long mà không đánh mất độ chính xác

o Chuyển đổi giá trị từ một kiểu dữ liệu sang kiểu dữ liệu tương tự khác nhỏ hơn hay giữa các
kiểu khác nhau thường không an toàn và mất dữ liệu:

Ghi chú 9: 1.5 là số chấm động double, tương tự và lớn hơn integer. Khi gán giá trị double vào biến "a" có kiểu integer,
compiler ngầm định ép kiểu double sang integer và đánh mất độ chính xác. Cụ thể, chỉ lấy phần nguyên, bỏ đi phần thập phân.

Ghi chú 10: 0.12345678f là số chấm động double, tương tự và lớn hơn float. Khi gán giá trị double vào biến "a" có kiểu float,
compiler ngầm định ép kiểu double sang float và đánh mất độ chính xác (double chính xác đến 17 kí tự, float chính xác 7 kí tự).

Ghi chú 11: 130 là số nguyên integer, khác kiểu kí tự char. Khi ép kiểu integer sang char với giá trị lớn hơn phạm vi cho phép
(bảng mã ASCII chỉ đến mã 127), giá trị xuất ra sẽ bị lỗi (tràn số).

o Quy tắc ép kiểu ngầm định:

o Để xác định kiểu dữ liệu của một toán tử “m”, dùng hàm typeid(m).name()
Ghi chú 12: Trong biểu thức a+b+c, cả 3 biến "a", "b" và "c" đều có kiểu dữ liệu nhỏ hơn integer, trình biên dịch sẽ ngầm định
ép sang kiểu integer.

Ghi chú 13: Trong biểu thức a+b+c, cả 3 biến "a", "b" và "c" có 3 kiểu dữ liệu khác nhau và có kiểu lớn hơn integer, trình biên
dịch sẽ ngầm định ép sang kiểu có độ ưu tiên cao nhất, trường hợp này là long.

 Ép kiểu tường minh (Explicit type conversion)


o Là quá trình chuyển đổi giữa các kiểu dữ liệu một cách tường minh, tức lập trình viên chủ
động can thiệp
o Có nhiều loại ép kiểu tường minh:
 C-style casts
Ép kiểu được thực hiện thông qua toán tử (), với tên kiểu dữ liệu cần chuyển đổi
nằm bên trong, hoặc dùng một lời gọi hàm ngay trong biểu thức:

Ghi chú 14: Ở đây nếu không ép kiểu, cả 2 toán hạng đều có kiểu dữ liệu integer sẽ cho ra kết quả cuối cùng có kiểu integer, tức
1.5 -> 1

 Static casts
static_cast<a>(b) Ép kiểu dữ liệu a của kí tự b

Ghi chú 15: Bảng mã ASCII chỉ đến mã số 127, nếu ép kiểu với số lớn hơn sẽ dẫn đến tràn số, xuất ra kí tự lỗi

Thay vì double d = (double)3 / 2 có thể dùng double d = static_cast<double>(3) / 2

HÀM

 Trong chương trình luôn nhất thiết có 1 hàm main, là hàm được viết sau tất cả các hàm chức
năng khác.
 1 hàm sau khi thực hiện các câu lệnh trong nó có thể có hoặc không trả về 1 giá trị nào:
o Nếu có trả về giá trị, kiểu trả về của hàm sẽ tương ứng với kiểu dữ liệu của giá trị trả về,
biểu thị trong câu lệnh return.
o Nếu không trả về giá trị, kiểu trả về của hàm sẽ là void. Và vì hàm void không mang
trong mình giá trị trả về, nó chỉ có thể được gọi độc lập, không thể gọi lồng trong các câu
lệnh hoặc biểu thức khác
Ghi chú 16: Nếu có câu lệnh return thì nhất thiết kiểu trả về không là void

Ghi chú 17: Không thể viết lệnh cout << sayHello(); vì đó là hàm void, chỉ có thể gọi độc lập sayHello();

Ghi chú 18: Các hàm có thể gọi chồng chéo lên nhau. Ở đây, hàm “sayHi” được gọi chồng bên trong hàm “sayHello”

 Đối với hàm có trả về, trình biên dịch chỉ thực thi lệnh trả về đầu tiên, và từ đấy bỏ qua tất cả
lệnh trả về hoặc các lệnh khác phía sau. Nếu muốn trả về nhiều giá trị trong hàm return, sử dụng
phép truyền tham chiếu hoặc con trỏ.

 Tham số và đối số:

Ghi chú 19: x và y là tham số, 2 và 3 là đối số được truyền cho biến x và y thông qua lời gọi hàm

 Có 3 cách truyền giá trị đối số: truyền giá trị, truyền tham chiếu
o Truyền giá trị (Pass by value)
 Thực hiện thông qua lời gọi hàm tên_hàm(đối số 1, đối số 2,…);
 Giá trị truyền vào có thể là một số, một biến mang giá trị, hoặc một biểu thức

Ghi chú 20: Nên dùng kiểu truyền giá trị khi đối số là các kiểu dữ liệu cơ bản, hoặc khi không cần thay đổi giá trị của đối số sau
khi thực hiện hàm

o Truyền tham chiếu (Pass by reference)


 Biến tham chiếu

Ghi chú 21: x là biến tham chiếu, y là biến được tham chiếu. Tuy ban đầu y=1, trong hàm "reference" đã thực hiện câu lệnh gán
x=2 và nó cũng tác động lên y, làm sau cùng y=2 => giá trị của đối số y thay đổi sau lời gọi hàm

 Trong lời gọi hàm truyền tham chiếu, chỉ có thể truyền đối số là một biến, không
thể là một hằng số hoặc một biểu thức
o Truyền tham chiếu hằng (Pass by constant reference)
 Cũng tương tự như truyền tham chiếu, nhưng biến tham chiếu hằng không cho
phép thực hiện thay đổi giá trị của biến được tham chiếu, và vì vậy nên không
cho phép thực hiện thay đổi của biến tham chiếu, là chính nó.
 Gọi biến tham chiếu hằng x kiểu integer với lệnh const int &x

Ghi chú 22: : x là biến tham chiếu hằng, y là biến được tham chiếu. Vì biến tham chiếu hằng không cho phép thay đổi giá trị của
biến được tham chiếu là y, vốn được thực hiện thông qua lệnh thay đổi giá trị của biến tham chiếu là x, nên lệnh x=2 báo lỗi

Trong lời gọi hàm truyền tham chiếu hằng, đối số có thể là một biến, một hằng
số hoặc một biểu thức
 Nên dùng nếu không có nhu cầu thay đổi giá trị của tham số sau lời gọi hàm. Vì
khi lồng vào trong chương trình lớn hơn, người khác sẽ không thể lỡ tay thay
đổi giá trị của biến tham chiếu của mình.
 Tiền khai báo và nguyên mẫu hàm
o Khi biên dịch chương trình, compiler đọc từ trên xuống dưới. Một hàm chỉ được gọi
thành công tại nơi phía sau nơi nó được định nghĩa. Nếu lời gọi hàm xuất hiện trước khi
nó được định nghĩa sẽ báo lỗi Identifier not found

Ghi chú 23: Ở đây, dù hàm "sayHello" hay hàm "sayHi" đứng trước đều mắc phải lỗi Identifier not found, vì trong hàm này có
lời gọi hàm kia

o Để khắc phục, phải làm cho trình biên dịch biết đến sự tồn tại của hàm trước cả khi nó
được định nghĩa, thông qua nguyên mẫu hàm
o Để định nghĩa nguyên mẫu hàm “function” có chứa biến số nguyên x và không cần lệnh
return:
 void function();
 void function(int x);
 void function(int);
 Khai báo (declarations) và định nghĩa (definitions)
o Khai báo là giới thiệu sự tồn tại của một định danh và mô tả nó: tên, kiểu dữ liệu, danh
sách tham số (nếu có), giúp trình biên dịch biết được sự tồn tại của nó trước khi định
nghĩa nó. Compiler không cần cấp vùng nhớ cho định danh được khai báo:

o Định nghĩa là trình bày rõ kiểu dữ liệu hoặc giá trị khởi tạo của một định danh. Compiler
cần cấp vùng nhớ cho định danh khi định nghĩa:
o Có thể thực hiện khai báo nhiều lần, nhưng chỉ định nghĩa một lần.

CẤU TRÚC RẼ NHÁNH CÓ ĐIỀU KIỆN

 Câu điều kiện IF


Ghi chú 24: Condition có thể là một hoặc nhiều điều kiện nối với nhau bởi các toán tử && (and), || (or) hoặc ! (not)

Ghi chú 25: Các statements có thể là một câu lệnh, hoặc một nhóm các câu lệnh đặt trong một block { }

o Nếu có nhiều hơn 2 trường hợp khả dĩ để cho ra 2 hướng kết quả, sử dụng lệnh else if
để tăng số lượng trường hợp:

o Toán tử điều kiện (conditional operator)


 (c) ? x : y nếu c khác 0 (true) thì thực hiện x, không thì thực hiện y
 x và y có thể là những giá trị (khi lồng trong câu lệnh) hoặc những toán tử nhỏ
hơn (thì sẽ không có ; ở cuối):

 Câu điều kiện SWITCH


o switch (x)
{ - x là giá trị nhập vào, có thể là một số, một biến hoặc một biểu thức
case 1: - Khi x = 1, trình biên dịch cho thực hiện lệnh statement 1, tương tự
statement 1; các trường hợp khác
break; - Khi x khác tất cả các case label, trình biên dịch cho thực hiện lệnh
case 2: statement 0 ở label default:
statement 2; - Một case có thể gồm nhiều lệnh khác nhau, không cần đặt trong
statement 2’; một block {}
break; - Lệnh break; dùng để ngăn các case với nhau. Trình biên dịch thực
… hiện tất cả các lệnh từ trên xuống dưới của một case tương ứng cho
default: đến khi gặp lệnh break; ,tức sẽ thực hiện các lệnh của case bên dưới
statement 0; cho đến khi gặp lệnh break; gần nhất
break;
}
o Trong tất cả các case (trừ case default), chỉ có thể khởi tạo biến mà không gán giá trị. Chỉ
có thể định nghĩa đầy đủ một biến trong case khi để tất cả trong một block {}

Hình 1: Không báo lỗi

Hình 2: Báo lỗi initialization of ‘m’ is skipped by …

 Câu lệnh GOTO


o label:
- Nhãn “label” gồm một hoặc nhiều câu lệnh statement
statement;
- Dùng lệnh goto label; để thực hiện các câu lệnh của nhãn ngay

tại vị trí ấy
goto label;
o Có 2 loại:
goto tới trước (khi lệnh goto đứng trước nhãn) và goto về sau (khi lệnh goto ở sau
nhãn):
 Dùng goto tới trước sẽ khiến trình biên dịch bỏ qua các lệnh phía sau và nhảy
đến nhãn để thực hiện các lệnh trong nhãn (nên nếu có lệnh khai báo biến bị bỏ
qua thì cũng không thể dùng biến đó trong nhãn):

 Dùng goto về sau kết hợp với câu điều kiện if có thể tạo thành vòng lặp có điều
kiện:

o Câu lệnh goto có phạm vi trong một hàm: nhãn và lệnh nằm chung một hàm mới có thể
gọi được

CẤU TRÚC LẶP

 Vòng lặp WHILE


Ghi chú 26: Expression có thể là một giá trị, một biến đếm hoặc một hay nhiều biểu thức

o Một vòng lặp có thể không được thực hiện vì biểu thức điều kiện từ đầu đã không thỏa
mãn, hoặc lặp vô hạn vì biểu thức điều kiện luôn đúng (hoặc dùng lệnh while(true) hoặc
while(n) với n ≠ 0).
o Để ngắt một vòng lặp vô hạn, có thể dùng lệnh return;, break; hoặc exit(0); đặt ở trong
và dưới cùng block lệnh.
o Biến đếm trong vòng lặp nên là biến có dấu. Hạn chế dùng kiểu dữ liệu unsigned cho
biến đếm, vì có thể sinh ra nhiều lỗi:

Ghi chú 27: Đây là vòng lặp vô hạn thay vì xuất ra các giá trị từ 10 đến 1. Biến đếm "count" có kiểu unsigned, khi từ 0 bị trừ đi 1
sẽ không thành -1 mà quay lại giá trị lớn nhất của nó, khiến biểu thức điều kiện luôn đúng

o Vòng lặp WHILE có thể lồng lẫn nhau: trong vòng lặp lớn có thể thiết lập các vòng lặp
nhỏ hơn.
o Có thể dùng lệnh system(“cls”) để xóa màn hình sau mỗi lần thực hiện nhập xuất

 Vòng lặp DO WHILE

Ghi chú 28: Expression có thể là một giá trị, một biến đếm hoặc một hay nhiều biểu thức

o Không như vòng lặp WHILE, vòng lặp DO WHILE thực hiện câu lệnh ít nhất một lần,
trước khi tiếp tục thực hiện hoặc dừng hẳn. (Bởi vì compiler biên dịch từ trên xuống
dưới, đến DO trước rồi mới đến WHILE)

Ghi chú 29: Nếu nhập vào giá trị không phải số nguyên sẽ xuất hiện lỗi stream input: không thể thực thi nhập liệu lại cho biến,
biến "selection" sẽ mang giá trị rác và lúc nào cũng thỏa biểu thức điều kiện, vòng lặp sẽ vô hạn

o Giải pháp: Lồng trong block DO các lệnh sau:


 cin.fail() khi nhập cho biến “selection” không đúng kiểu dữ liệu
 cin.clear() xóa tất cả các fail bit trong bộ nhớ đệm
 cin.ignore(32767, ‘\n’) xóa dữ liệu trong bộ nhớ đệm cho đến khi xóa đủ 32767
kí tự, hoặc đến khi vừa xóa xong kí tự phân tách của nút Enter
 Vòng lặp FOR

 init-statement khởi tạo một hoặc nhiều biến vòng lặp, chỉ có hiệu lực trong
phạm vi vòng lặp (nếu nhiều biến thì phải cùng kiểu dữ liệu)
 condition-expression biểu thức điều kiện của một hoặc nhiều biến đó
 end-expression biểu thức bước nhảy cho một hoặc nhiều biến vòng lặp
o Khi viết dưới dạng vòng lặp WHILE, các câu lệnh tương tự sẽ là:

Ghi chú 30: Vì biến đếm của vòng lặp WHILE có giá trị sử dụng trong phạm vi cả hàm, để nó chỉ có hiệu lực trong phạm vị vòng
lặp như biến vòng lặp thì phải để cả vòng lặp WHILE trong một block {}

o Có thể dời một số thành phần trong for(;;) ra ngoài. Các cách viết sau là tương đương:

o Có thể thiết lập vòng lặp vô hạn với lệnh for(;;), nó tương đương lệnh while(true)
o Cũng như vòng lặp WHILE, vòng lặp FOR có thể lồng lẫn nhau: trong vòng lặp lớn có thể
thiết lập các vòng lặp nhỏ hơn.

 Vòng lặp FOR EACH

o Để tăng hiệu suất khi làm việc với số lượng dữ liệu lớn, biến “n” trong vòng lặp FOR
EACH có thể để tham chiếu hoặc tham chiếu hằng, tùy trường hợp sử dụng
o Vòng lặp FOR EACH không thể làm việc với con trỏ, vì con trỏ đến một mảng không cho
biết kích thước của mảng đó:
 Break và Continue
o Lệnh break; kết hợp với câu điều kiện IF có thể dùng để ngắt một vòng lặp vô hạn:

o Lệnh continue; dùng để nhảy đến vị trí cuối cùng của một block {}, bỏ qua các lệnh phía
sau nó:

Ghi chú 31: khi biến i mang giá trị chẵn, lệnh continue; kích hoạt làm bỏ qua lệnh cout << i và thực hiện lại vòng
lắp với biến i tiếp theo

o Lệnh continue; thường được dùng trong vòng lặp FOR hơn vòng lặp WHILE, bởi vì biểu
thức bước nhảy (end-expression) của vòng lặp FOR nằm ngay trong for(;;) nên khi
continue sẽ không ảnh hưởng. Ngược lại, kích hoạt lệnh continue; có thể làm bỏ qua
biểu thức bước nhảy của vòng lặp WHILE, khiến biến đếm bất định, dẫn đến vòng lặp vô
hạn do biểu thức điều kiện không bao giờ thỏa mãn nữa.

PHÁT SINH SỐ NGẪU NHIÊN

 Để khởi tạo số ngẫu nhiên, sử dụng srand(unsigned int seed); (thuộc thư viện <cstdlib>)
o Đối số nhập vào là kiểu số nguyên không dấu, gọi là hạt giống (seed).
o Mỗi giá trị của seed cho ra một giá trị ngẫu nhiên khác nhau. Giá trị ngẫu nhiên này
được lấy ra bởi rand() (thuộc thư viện <cstdlib>).

Ghi chú 32: 5 là giá trị nhập vào, 54 là giá trị ngẫu nhiên xuất ra ứng với 5

o Để giá trị ngẫu nhiên nằm trong một khoảng giá trị cho trước, dùng rand() % m + n, với
m và n lần lượt là số phần tử và giá trị nhỏ nhất trong khoảng giá trị (nói cách khác, m là
giá trị lớn nhất, n là giá trị nhỏ nhất)
o Hai khởi tạo số ngẫu nhiên khác nhau của cùng một seed sẽ cho ra cùng một kết quả.
o Để khởi tạo một bộ số ngẫu nhiên khác nhau, các giá trị truyền vào seed mỗi lần khởi
tạo cũng phải ngẫu nhiên. Giá trị này lấy từ lệnh time(0) (thuộc thư viện <ctime>, cho ra
số giây từ 0 giờ ngày 1/1/1970, luôn là số nguyên không dấu.
 C++11 cung cấp một số thuật toán phát sinh số ngẫu nhiên, nằm trong thư viện <random>:
o random_device in; tạo một seed (đặt tên “in”) và khởi tạo cho nó một giá trị ngẫu nhiên
o mt19937 out(in()); khởi tạo giá trị ngẫu nhiên trong phạm vi 32bit dựa trên seed nhập
vào và gán vào “out”
o mt19937_64 out(in()); khởi tạo giá trị ngẫu nhiên trong phạm vi 64bit dựa trên seed
nhập vào và gán vào biến “out”

Ghi chú 33: Hiện tượng tràn số xảy ra do giá trị ngẫu nhiên của out() vượt quá khoảng cho phép của kiểu dữ liệu int của biến “n”

Ghi chú 34: Dùng kiểu dữ liệu auto để hệ thống tự điều chỉnh kiểu dữ liệu của biến "n" phù hợp với giá trị nó nhận vào để tránh
hiện tượng tràn số

o uniform_int_distribution<int> a(n,m); thuật toán khởi tạo biến số nguyên a có giá trị
nằm trong khoảng từ n đến m. Khi này, để gán giá trị ngẫu nhiên vào biến n có kiểu dữ
liệu auto, điều chỉnh câu lệnh thành auto n = a(out);

MẢNG

 Mảng một chiều


o Khai báo mảng một chiều:

Ghi chú 35: Nếu sử dụng tiền chỉ thị xử lý thì dùng lệnh #define n 5 và int array[n];
o Khởi tạo giá trị cho các phần tử trong mảng:
 Nhập thủ công giá trị m cho phần tử thứ n+1 trong mảng tên “array”: array[n] = m
 Để nhập thủ công các giá trị cho các phần tử trong mảng tên “array”, kết hợp
vòng lặp:

 Để nhập tự động các giá trị cho các phần tử trong mảng tên “arr”, kết hợp sử
dụng vòng lặp và phát sinh số ngẫu nhiên:

Ghi chú 36: Số lượng phần tử không cần được khai báo mà trình biên dịch sẽ tự tính toán thông qua số lượng giá trị nhập vào
=> không có phần tử nhận giá trị 0 thừa

Ghi chú 37: Bỏ dấu = trong các dòng khai báo


o Xuất giá trị phần tử trong mảng:
 Để xuất ra giá trị của một phần tử chỉ định bất kỳ, dùng lệnh cout:

 Nếu chỉ số index trong lệnh xuất ra nằm ngoài chỉ số có trong mảng, hệ thống sẽ
xuất ra giá trị rác:

 Để xuất ra tất cả/một phần các giá trị của mảng, kết hợp sử dụng vòng lặp:

o Truyền mảng vào hàm

 Có nhiều cách truyền mảng vào hàm:

Ghi chú 38: sử dụng con trỏ

 Có thể truyền hơn một mảng hoặc một biến vào hàm:

 Khi gọi một hàm đã được xây dựng sẵn và có làm việc với mảng, mảng được
tham chiếu chỉ cần gọi với tên, không cần []
o Các thao tác với mảng “array” gồm MAX phần tử với #define MAX 5
 Nhập mảng với số phần tử tự chọn (mang các giá trị ngẫu nhiên):

 Xuất mảng với số phần tử tự chọn (mang các giá trị ngẫu nhiên):
 Sao chép một mảng “array” với mảng “array2”:

 Tìm kiếm một phần tử trong mảng mang theo giá trị cho trước:

Ghi chú 39: Cách này sai, vì dù lệnh IF có được thực hiện hay không, ở lần chạy FOR tiếp theo sẽ tăng lên giá trị của t 1 đơn vị

 Sắp xếp các mảng theo thứ tự giá trị tăng dần:
 Thêm một phần tử mang giá trị tùy chọn tại một vị trí tùy chọn:

o Nếu xây dựng một mảng rồi gọi lại dùng sau thì phải để ý việc các biến trong hàm có nên
là biến tham chiếu hay không.

Ghi chú 40: "n" là số phần tử của mảng. Ở hàm "nhapMang", "n" là biến tham chiếu, tùy vào giá trị nhập vào nó khi ở trong
hàm, biến số lượng phần tử “nSize” thiết lập ngoài hàm cũng sẽ thay đổi theo. Để từ đó số phần tử trong hàm "xuatMang" cũng
thay đổi tương ứng. Nếu chỉ gọi mỗi hàm “nhapMang” mà không có hàm “xuatMang” ở sau thì biến n không để tham chiếu
cũng được, vì khi này biến “nSize” có bị thay đổi hay không cũng không ảnh hưởng.

 Mảng 2 chiều
o Khai báo mảng 2 chiều
Ghi chú 41: Nếu sử dụng tiền chỉ thị xử lý thì dùng lệnh #define row 5, #define column 5 và int array[row][column]; thay vì
array[5][5];

o Về cơ bản, bộ nhớ RAM xử lý mảng 2 chiều như các mảng 1 chiều chồng lên nhau. Kí tự
đầu ở hàng sau mang địa chỉ vùng nhớ sát sau kí tự cuối ở hàng ngay trước nó. Vì vậy,
khai báo mảng 2 chiều không thể để trống số kí tự của mỗi dòng (số cột), vì khi này sẽ
không thể xác định vị trí vùng nhớ cho hàng sau.

o Khai báo mảng 2 chiều: có nhiều cách khai báo, nhưng luôn phải xác định được số cột.
Trường hợp khai báo mảng rỗng thì phải có cả số hàng lẫn số cột:

Ghi chú 42: Có thể bỏ dấu "=" ở tất cả các cách trên

o Để nhập/xuất mảng 2 chiều, sử dụng 2 vòng lặp FOR lồng nhau (số dòng, số cột tùy
chọn):
Ghi chú 43: "ROW" và "COLUMN" là số dòng và cột tối đa. "row" và "column" là số dòng và cột tùy chọn, miễn >0 và nhỏ hơn
giới hạn

o Truyền mảng vào hàm:


 Có nhiều cách truyền mảng 2 chiều vào một hàm:

Ghi chú 44: Sử dụng con trỏ

 Có thể truyền hơn 1 mảng hoặc 1 biến vào hàm:

o Các thao tác với mảng “array” gồm ROW dòng và COLUMN cột với #define ROW 10,
#define COLUMN 10
 Tính tổng giá trị các phần tử của một dòng tự chọn:

 Tính tổng giá trị các phần tử của một cột tự chọn:
 Lớp dựng sẵn Array
o Biến kiểu std::array được định nghĩa trong thư viện <array>. Khi khai báo biến thuộc
kiểu dữ liệu std::array cần xác định số phần tử và kiểu dữ liệu của mảng:

Ghi chú 45: Số phần tử phỉa mang giá trị cụ thể. Khi chưa khởi tạo, các phần tử sẽ mang giá trị rác

o Có nhiều cách để khởi tạo giá trị cho các phần tử trong mảng:

o Toán tử [] không kiểm tra phạm vi của mảng, nên nếu dữ liệu nhập vào quá giới hạn của
mảng có thể gây chết chương trình. Hàm .at() được dùng để kiểm tra phạm vi trước khi
cho trình biên dịch chạy chương trình => lâu nhưng an toàn hơn:

o Các thao tác với lớp dựng sẵn array:


 Xem kích thước của mảng:

 Truyền mảng vào hàm:


hoặc

hoặc
Ghi chú 46: truyền tham chiếu

Ghi chú 47: truyền tham chiếu hằng

 Xuất mảng (bằng hàm xây dựng sẵn)

Sắp xếp tăng/ giảm các phần tử trong hàm: Dùng hàm sort thuộc thư viện
<array>
- sort(<tên mảng>.begin(), <tên mảng>.end()); sắp xếp tăng
- sort(<tên mảng>.rbegin(), <tên mảng>.rend()); sắp xếp giảm

MẢNG KÝ TỰ

 Trong C++ có 2 loại chuỗi ký tự: chuỗi ký tự std::string và chuỗi ký tự C-style


o Kiểu chuỗi std::string được xây dựng từ kiểu C-style nhưng đơn giản hơn.
o C-style bản chất là mảng một chiều các ký tự, kết thúc bằng ký tự ‘\0’ (null)

 Các cách khởi tạo mảng ký tự:


o Khởi tạo với độ dài cụ thể:

o Khởi tạo tự động xác định độ dài:


o Để xuất mảng ký tự, chỉ cần sử dụng lệnh cout bình thường, không cần vòng lặp FOR:

o Để xuất mảng ký tự, chỉ cần sử dụng lệnh cin bình thường, không cần vòng lặp FOR:

Ghi chú 48: Khi chỉ sử dụng hàm cin, mảng ký tự sẽ không thể bao gồm khoảng cách, vì trình biên dịch đã tự động ngưng khi
gặp dấu cách. Ngoài ra, nếu ký tự nhập vào quá giới hạn khai báo, chương trình sẽ báo lỗi

Ghi chú 49: Dùng hàm cin.c(<tên hàm>, <số ký tự tối đa>) sẽ cho phép khoảng cách trong hàm. Đồng thời nếu số ký tự nhập vào
quá giới hạn khai báo, trình biên dịch tự động cắt bỏ phần phía sau (khai báo 10 thì nhận 9 kí tự tối đa, kí tự cuối là ‘\n’)

 Một số thao tác trên mảng ký tự (cần kèm thư viện <cstring>)
o Xem độ dài mảng với hàm strlen():

o Viết thường/ viết hoa cả mảng với hàm _strlwr()_s _strupr()_s:

Ghi chú 50: Có thể bỏ phần _s nhưng phải #define _CRT_SECURE_NO_WARNINGS ở đầu chương trình

o Copy ký tự của một mảng sang mảng khác với hàm strcpy_s():

Ghi chú 51: Có thể bỏ phần _s nhưng phải #define _CRT_SECURE_NO_WARNINGS ở đầu chương trình
o Nối 2 chuỗi với nhau với hàm strcat_s():

 “target” là chuỗi đích


 “source” là chuỗi dùng để nối vào chuỗi “target”

 Hàm sẽ trả về kết quả là con trỏ của chuỗi kết quả.
 Cần chỉ định kích thước chuỗi đích phải đủ lớn để có thể chứa được chuỗi
nguồn được thêm vào nó, để tránh hiện tượng tràn bộ nhớ xảy ra.
o Nối 2 chuỗi với nhau với số ký tự chỉ định bằng hàm strncat_s():

 “n” là số ký tự tối đa được lấy ra từ đầu chuỗi “source” để nối vào chuỗi “target”

Ghi chú 52: Có thể bỏ phần _s nhưng phải #define _CRT_SECURE_NO_WARNINGS ở đầu chương trình

 Hàm sẽ trả về kết quả là con trỏ của chuỗi kết quả.
 Cần chỉ định kích thước chuỗi đích phải đủ lớn để có thể chứa được chuỗi
nguồn được thêm vào nó, để tránh hiện tượng tràn bộ nhớ xảy ra.
o Nối nhiều hơn 2 chuỗi với nhau với hàm sprintf():

 “target” là chuỗi đích


 “srt” là các chuỗi dùng để nối vào chuỗi target
 “*format” là chuỗi định dạng, là tập hợp các định dạng “%s” của các chuỗi str.
Có bao nhiêu chuỗi str được chỉ định thì có bấy nhiêu định dạng tương ứng.

 Cần chỉ định kích thước chuỗi đích phải đủ lớn để có thể chứa được chuỗi
nguồn được thêm vào nó, để tránh hiện tượng tràn bộ nhớ xảy ra.
o So sánh các chuỗi với nhau với hàm strcmp(). Về bản chất là so sánh giá trị mã ASCII của
cặp kí tự khác nhau đầu tiên. Hàm strcmp(<chuỗi 1>, <chuỗi 2>) chỉ trả về giá trị -1, 1
hoặc 0, nên phải tạo biến lưu giá trị đó rồi dùng lệnh IF để cho ra kết quả. Nếu chỉ muốn
so sánh n ký tự đầu tiên, dùng strcmp(<chuỗi 1>, <chuỗi 2>,n)
 Tìm kiếm chuỗi con trong một chuỗi:

CON TRỎ

 Khi khởi tạo giá trị cho một biến, một vùng trong bộ nhớ RAM được cấp cho biến đó
o Có thể truy vấn địa chỉ của biến bằng toán tử & đặt ngay trước biến đó. Địa chỉ này thay
đổi sau mỗi lần chạy chương trình.
o Toán tử * đặt trước tên biến có ý nghĩa như value của biến đó. Vì vậy, có thể lấy giá trị
đang nằm trong vùng địa chỉ của biến bằng toán tử *& đặt ngay trước biến đó. Khi trỏ
vào và thay đổi giá trị của vùng nhớ, biến được cấp địa chỉ tại vùng nhớ đó cũng bị thay
đổi giá trị theo

 Con trỏ là một biến chứa một địa chỉ bộ nhớ làm giá trị của nó. Để khai báo biến con trỏ, thêm *
giữa kiểu dữ liệu và tên biến:

o Cũng như một biến bình thường, biến con trỏ không được khởi tạo ngay khi khai báo
không mà sẽ mang giá trị rác. Tuy nhiên, giá trị biến con trỏ mang chỉ có thể là giá trị địa
chỉ:
Ghi chú 53: Biến con trỏ mang kiểu dữ liệu auto có thể trỏ đến vùng nhớ của nhiều kiểu dữ liệu khác nhau

o Nhìn chung, biến chứa giá trị địa chỉ vùng nhớ mang kiểu dữ liệu int thì có kiểu dữ liệu
int*, của double thì có giá trị double* và tương tự với các kiểu dữ liệu khác:

o Không như biến thông thường, kích thước bộ nhớ của biến con trỏ phụ thuộc vào kiến
trúc của tập tin biên dịch, không phụ thuộc vào kiểu dữ liệu.
o Truy cập con trỏ không hợp lệ: Khi truy cập con trỏ, ứng dụng sẽ đến vị trí vùng nhớ
được lưu trữ trong biến con trỏ và truy xuất nội dung trong vùng nhớ đó. Nếu cố gắng
truy xuất vào một vùng nhớ không được hệ điều hành phân bổ, ứng dụng có thể bị tắt:

Ghi chú 54: Sau khi biên dịch, trình biên dịch sẽ báo lỗi

 Con trỏ null:


o Ngoài giá trị địa chỉ ra, còn một giá trị khác mà con biến trỏ có thể lưu giữ, đó là 0. Khi
này, con trỏ trở thành con trỏ null:

o Nên khởi tạo biến con trỏ null nếu nó chưa trỏ vào địa chỉ nào. Không nên truyền giá trị
0 để khởi tạo con trỏ null, thay vào đó có thể sử dụng chỉ thị tiền xử lí #define hoặc từ
khóa nullptr:
 #define NULL 0;
int *cursor = NULL;
 Int *cursor = nullptr;
 Con trỏ và mảng
o Khi một mảng tên “array” được khởi tạo, biến mang tên “array” ấy thực chất lưu trữ địa
chỉ vùng nhớ của phần tử đầu tiên trong mảng:

o Vì vậy khi thao tác truy xuất giá trị của biến “array” ấy, giá trị ta lấy ra được là của phần
tử đầu tiên trong mảng:
o Con trỏ trỏ vào mảng (kiểu dữ liệu int) là trỏ vào phần tử đầu tiên của mảng, vì vậy kích
thước bộ nhớ nó chỉ tương đương với một phần tử đầu tiên của mảng:

o Khi truyền mảng vào hàm bằng cách truyền giá trị (pass by value), sẽ có sự sao chép tạo
ra thêm dữ liệu giá trị để hoạt động trong hàm => nếu mảng mang nhiều phần tử sẽ gây
tốn vùng nhớ và giảm hiệu suất. Vì vậy khi thực hiện truyền mảng vào hàm bằng cách
truyền giá trị, chương trình tự động chuyển mảng đó thành một con trỏ mảng với kích
thước bộ nhớ nhỏ cố định, bất kể quy mô của hàm:

o Vì là làm việc với con trỏ và địa chỉ nên khi truyền mảng vào hàm thực chất không có
phương thức truyền giá trị mà chỉ có truyền tham chiếu (pass by reference) => thay đổi
mảng tham chiếu trong hàm sẽ làm thay đổi mảng được tham chiếu truyền vào hàm.
 Các phép toán trên con trỏ
o Có thể thực hiện các phép toán cộng hoặc trừ trên biến con trỏ. Nếu biến con trỏ
“cursor” giữ địa chỉ vùng nhớ của một số nguyên biến thì “cursor+1” sẽ giữ địa chỉ vùng
nhớ của một số nguyên tiếp theo trong bộ nhớ. (“cursor+1” không trả địa chỉ vùng nhớ
sau cursor mà là địa chỉ của vùng nhớ của đối tượng tiếp theo thuộc kiểu dữ liệu mà
“cursor” trỏ tới):

Ghi chú 55: Mỗi địa chỉ cách nhau 4 đơn vị, tương đương kích thước của kiểu số nguyên là 4 bit
o Khi áp dụng vào mảng, vì cứ +1 vào biến con trỏ thì nó lại trả về địa chỉ của vùng nhớ của
đối tượng tiếp theo thuộc kiểu dữ liệu mà nó đang trỏ tới, nên cũng đồng nghĩa việc
hướng tới phần tử kế tiếp phần tử đang trỏ trong mảng:

o Vì thế cũng có thể sử dụng con trỏ để thực hiện các thao tác trên mảng, ví dụ xuất giá trị
các phần tử:

 Con trỏ và hằng

Ghi chú 56: “value” là một hằng, giá trị sẽ không thể thay đổi trong suốt vòng đời của chương trình. Việc gán một biến con trỏ
cho một hằng sẽ gây ra lỗi biên dịch.

o Đối với vùng dữ liệu của một hằng, ta sử dụng con trỏ hằng (Pointer to const value).
Không thể thay đổi giá trị mà nó đang trỏ đến, nhưng có thể cho nó trỏ đến một địa chỉ
vùng nhớ khác.

Ghi chú 57: Con trỏ hằng có thể trỏ vào địa chỉ của một vùng nhớ hằng lẫn biến, cũng như có thể trỏ vào các địa chỉ khác nhau

o Con trỏ hằng xử lý giá trị tại địa chỉ mà nó trỏ tới là hằng khi được truy cập thông qua
con trỏ, bất kể biến ban đầu được định nghĩa là hằng hay không:

Ghi chú 58: Tuy "value" không phải hằng nhưng con trỏ hằng "ptr" vẫn không cho phép thay đổi giá trị vùng dữ liệu mà nó đang
trỏ vào bằng phương pháp con trỏ

o Ngược lại với con trỏ hằng, hằng con trỏ (const pointers) là con trỏ không thể thay đổi
được địa chỉ vùng nhớ mà nó lưu trữ, nhưng có thể thay đổi được giá trị mà nó trỏ đến.

o Hằng con trỏ hằng (Const pointer to a const value) là một con trỏ vừa không thay đổi
được địa chỉ vùng nhớ mà nó lưu trữ, vừa không thay đổi được giá trị của vùng nhớ đó.

 Con trỏ void


o Một con trỏ truyền tải hai thông tin:
 Kiểu dữ liệu trỏ đến (int, double, ...) quy định cách truy xuất dữ liệu của vùng
nhớ.
 Địa chỉ của vùng nhớ mà nó trỏ tới quy định cụ thể nơi bạn có thể truy xuất dữ
liệu.
o Kiểu dữ liệu đang được trỏ đến là kiểu của con trỏ (int *, double *, ...), trong khi địa chỉ
của dữ liệu là giá trị thực tế chứa trong biến con trỏ.
o Con trỏ void còn được gọi là con trỏ tổng quát, là một kiểu con trỏ đặc biệt có thể trỏ
đến các đối tượng của bất kỳ kiểu dữ liệu nào. Khai báo giống như một con trỏ bình
thường, sử dụng từ khóa void làm kiểu con trỏ:

o Tuy nhiên, con trỏ void không xác định được kiểu dữ liệu của vùng nhớ mà nó trỏ tới,
chúng ta không thể truy xuất trực tiếp nội dung thông qua toán tử dereference (*)
được. Vì vậy, con trỏ kiểu void cần phải được ép kiểu tường minh sang con trỏ có kiểu
dữ liệu khác trước khi khai báo giá trị.

Ghi chú 59: “voidPtr” và “intPtr” đều trỏ vào địa chỉ của biến “value”, nhưng chúng ta chỉ có thể sử dụng toán tử dereference
(*) lên con trỏ “intPtr”, vì trình biên dịch không thể biết con trỏ “voidPtr” trỏ đến kiểu dữ liệu gì.

o Thông thường, để sử dụng được con trỏ void, phải lấy thông tin về kiểu con trỏ theo
cách khác (sử dụng thêm tham số hoặc biến cho biết kiểu con trỏ), sau đó ép kiểu con
trỏ đó đến một kiểu cụ thể (int, double, …) rồi sử dụng như bình thường.
 Con trỏ trỏ đến con trỏ
o Biến con trỏ thông thường sử dụng một dấu sao (*) khi khai báo:

o Biến con trỏ trỏ đến con trỏ sử dụng hai dấu sao (**) khi khai báo:

o Con trỏ trỏ đến con trỏ hoạt động như một con trỏ thông thường. Mọi thao tác trên con
trỏ cũng có thể thực hiện trên con trỏ trỏ đến con trỏ:

Ghi chú 60: Không thể gán trực tiếp một con trỏ trỏ đến con trỏ bằng giá trị

o Con trỏ trỏ đến con trỏ có thể được dùng để quản lý mảng một chiều các con trỏ (Arrays
of pointers):

Ghi chú 61: Mảng một chiều các con trỏ hoạt động như một mảng thông thường, ngoại trừ việc mỗi phần tử
mảng là một con trỏ.

o Con trỏ trỏ đến con trỏ trỏ đến con trỏ…

Ghi chú 62: Trong thực tế, những con trỏ như thế này không được sử dụng nhiều vì phức tạp.

 Con trỏ chuỗi:


o Khai báo con trỏ chuỗi:

o Khởi tạo giá trị cho con trỏ chuỗi: không dùng toán tử & khi gán địa chỉ của chuỗi cho
con trỏ
o Không như con trỏ thông thường, địa chỉ của con trỏ chuỗi được biểu diễn bởi các ký tự
từ vị trí con trỏ chỉ đến tới cuối chuỗi:

o Tuy nhiên cũng như con trỏ thường, giá trị lưu trữ trong con trỏ chuỗi chỉ là ký tự đầu
tiên trong chuỗi, và có thể sử dụng các phép toán để trích xuất các ký tự tiếp theo:

CẤP PHÁT BỘ NHỚ

 Ngôn ngữ C++ cung cấp 3 loại cấp phát bộ nhớ:


o Cấp phát bộ nhớ tĩnh (Static memory allocation): đối với biến tĩnh và biến toàn cục.
Vùng nhớ được cấp khi khai báo và tồn tại suốt thời gian chạy chương trình. Kích thước
phải biết tại thời điểm biên dịch chương trình.
o Cấp phát bộ nhớ tự động (Automatic memory allocation): đối với tham số hàm và biến
cục bộ. Vùng nhớ được cấp khi đi vào khối lệnh và giải phóng khi thoát khỏi khối lệnh.
Kích thước phải biết tại thời điểm biên dịch chương trình.
o Cấp phát bộ nhớ động (Dynamic memory allocation)
 Cấp phát động

o Ví dụ nếu có hàm nhập tên nhưng không biết số ký tự cụ thể, ta thường đặt cho nó một
giới hạn lớn theo mức phân tích trên để khi biên dịch chương trình không bị lỗi. Tuy
nhiên phần ký tự thừa sẽ gây tốn vùng nhớ và giảm hiệu suất.
o Cấp phát bộ nhớ động là yêu cầu hệ điều hành cấp phát vùng nhớ ngay tại thời điểm
chương trình đang chạy. Để cấp phát động cho một biến, ta sử dụng toán tử new:

Ghi chú 63: Cấp phát động một số nguyên (kiểu dữ liệu có thể thay đổi)

o Toán tử new tạo đối tượng sử dụng vùng nhớ đó và sau đó trả về một con trỏ chứa địa
chỉ của vùng nhớ đã được cấp phát. Để truy cập vào vùng nhớ được cấp phát, chúng ta
dùng con trỏ để lưu giữ địa chỉ được trả về bởi toán tử new:
Ghi chú 64: Cấp phát động một số nguyên và gán địa chỉ cho con trỏ ptr nắm giữ

o Thao tác trên vùng nhớ vừa được cấp phát thông qua con trỏ:

o Khi cấp phát động cho một biến, có thể cùng lúc khởi tạo giá trị cho nó:

o Khi không còn sử dụng một biến được cấp phát động, cần trao quyền quản lý vùng nhớ
đó lại cho hệ điều hành. Đối với các biến đơn (không phải mảng), điều này được thực
hiện thông qua toán tử delete:

Ghi chú 65: Toán tử delete không thực sự xóa bất cứ điều gì, chỉ là trao lại quyền sử dụng vùng nhớ được cấp phát cho hệ điều
hành. Sau lệnh delete ptr; biến con trỏ ptr vẫn có thể sử dụng như trước và có thể được gán một giá trị mới

o Khi delete một con trỏ, nó sẽ trỏ sang một vùng nhớ chưa được cấp phát. Khi này, nó là
một con trỏ lơ lửng (dangling pointer). Truy cập vào vùng nhớ hoặc xóa một con trỏ lơ
lửng sẽ dẫn đến lỗi undefined behavior. Vì vậy, khi delete một con trỏ, nên gán giá trị
null cho nó. Trong cấp phát bộ nhớ động, một con trỏ null có ý nghĩa “không có vùng
nhớ nào được cấp phát cho con trỏ này”.

o Rò rỉ bộ nhớ (Memory leaks)

Ghi chú 66: Trong hàm “doSomething” cấp phát động một số nguyên, nhưng không sử dụng toán tử delete để giải phóng vùng
nhớ đó. Vì con trỏ tuân theo tất cả các quy tắc giống như các biến thông thường, khi hàm kết thúc, biến “ptr” sẽ bị hủy khi là
biến duy nhất giữ địa chỉ của số nguyên được cấp phát động => chương trình đã "mất" địa chỉ của bộ nhớ được cấp phát động
trong hàm => không thể giải phóng vùng nhớ được cấp phát động.

Ghi chú 67: Con trỏ giữ địa chỉ của bộ nhớ được cấp phát động được gán một giá trị khác

Ghi chú 68: Cấp phát vùng nhớ liên tục nhiều lần

 Rò rỉ bộ nhớ xảy ra khi chương mất địa chỉ của một số vùng nhớ được cấp phát
động trước khi giải phóng nó cho hệ điều hành.
 Khi ấy, chương trình của bạn không thể xóa bộ nhớ được cấp phát động, bởi vì
chương trình không còn nắm giữ địa chỉ vùng nhớ đó. Hệ điều hành cũng không
thể sử dụng vùng nhớ này, vì vùng nhớ đó vẫn nằm trong quyền sử dụng của
chương trình. Chỉ sau khi chương trình tắt, hệ điều hành mới có thể dọn dẹp và
"đòi lại" tất cả vùng nhớ bị rò rỉ.
 Cấp phát mảng động (Dynamically allocating arrays)
o Khi khai báo mảng tĩnh, kích thước mảng phải được xác định tại thời điểm biên dịch và
không bao giờ thay đổi. Cấp phát động một mảng cho phép chúng ta xác định kích
thước mảng trong thời gian chạy chương trình.
o Để cấp phát và thu hồi mảng động, sử dụng toán tử new[] và delete[]:

o Khởi tạo giá trị cho mảng động cũng tương tự như mảng tĩnh:

Ghi chú 69: Mảng động phải được khai báo độ dài rõ ràng khi khởi tạo

o Để thay đổi kích thước mảng, chúng ta cần thực hiện:


 Cấp phát động một mảng mới.
 Sao chép các phần tử từ mảng cũ sang vùng nhớ mới.
 Xóa mảng cũ.

BIẾN (tiếp theo)

 Đến đây, có 3 loại biến trong C++:


o Biến giá trị dùng để chứa dữ liệu trực tiếp (số nguyên, số thực, ký tự, ...).
o Biến con trỏ dùng để chứa địa chỉ (hoặc null).
o Biến tham chiếu: so với 2 loại trên, nó:
 Không được cấp phát bộ nhớ, không có địa chỉ riêng.
 Dùng làm bí danh cho một biến (kiểu giá trị) nào đó và nó sử dụng vùng nhớ của
biến này.
 Một biến tham chiếu được khai báo bằng cách sử dụng toán tử & giữa kiểu dữ liệu và tên biến:
Ghi chú 70: Ở đây, & không có nghĩa là "địa chỉ", nó có nghĩa là "tham chiếu đến".

 Đối với hằng, chỉ có thể sử dụng tham chiếu hằng. Tuy nhiên tham chiếu hằng có thể tham chiếu
đến cả hằng lẫn biến:

 Biến tham chiếu và biến được tham chiếu có giá trị lưu ở cùng địa chỉ vùng nhớ, nên mọi thay
đổi lên biến tham chiếu sẽ làm thay đổi tương tự lên biến được tham chiếu, và ngược lại.

Ghi chú 71: "r" là biến tham chiếu, "n" là biến được tham chiếu

 Khi xây dựng một hàm và thực hiện lời gọi hàm, tham số tham chiếu hoạt động như một bí danh
cho đối số và không có bản sao của đối số được đưa vào tham số. Điều này làm cho hiệu suất tốt
hơn nếu đối số là kiểu dữ liệu có kích thước lớn.
 Có thể thao tác thay đổi giá trị trên một biến bằng tham chiếu hoặc bằng con trỏ, tuy nhiên các
tham chiếu nói chung an toàn hơn nhiều so với con trỏ khi thao tác (con trỏ có thể null hoặc ở
trạng thái “lơ lửng”).
 Một số lưu ý:
o Không giống như con trỏ, tham chiếu không không thể giữ giá trị null. Vì vậy, tham chiếu
phải được khởi tạo khi khai báo.
o Sau khi được khởi tạo, tham chiếu (biến lẫn hằng) không thể thay đổi để tham chiếu đến
biến khác.

VECTOR

 Vector có bản chất là mảng một chiều động, có các tính chất của một mảng một chiều.
 Với lớp std::vector, có thể tạo các mảng động mà không cần phải cấp phát và thu hồi vùng nhớ
bằng cách sử dụng toán tử new[] và delete[].
 Để sử dụng lớp std::vector, cần khai báo thư viện <vector> và namespace std:
Ghi chú 72: Mảng tạo bởi lớp vector không cần cung cấp độ dài mảng tại thời điểm biên dịch

 Khai báo vector với số phần tử chỉ định thì các phần tử mặc định bằng 0 (nếu kiểu số) hoặc null
(nếu kiểu chữ)

 Có thể khởi tạo vector với nhiều phần tử mang giá trị giống nhau:

 Khai báo và khởi tạo vector 2 chiều:

Ghi chú 73: Cần phải viết thêm dấu cách giữa cặp dấu > > khi khai báo vector 2 chiều để phân biệt với toán tử >> được sử dụng
để dịch chuyển bit trong C++.

Ghi chú 74: Khởi tạo các vector 1 chiều trước rồi dùng chúng để khai báo vector 2 chiều

 Việc truy cập các phần tử mảng có thể được thực hiện thông qua toán tử [] (không kiểm tra
phạm vi mảng) hoặc hàm at() (có kiểm tra phạm vi mảng):

 Khi một biến vector ra khỏi phạm vi được định nghĩa, nó sẽ tự động giải phóng vùng nhớ mà nó
nắm giữ. Điều này không chỉ tiện dụng (vì không phải tự làm điều đó), nó còn giúp ngăn ngừa rò
rỉ bộ nhớ.
 Không giống như mảng thông thường hoặc mảng dựng sẵn std::array (chỉ chứa kích thước
mảng), mảng kiểu std::vector chứa hai thuộc tính riêng biệt: kích thước (size) và dung lượng
(capacity).
o Kích thước (size) trả về số lượng phần tử đang được sử dụng trong mảng.
o Dung lượng (capacity) trả về số lượng phần tử được cấp phát cho vector trong bộ nhớ.

 Một số thao tác với mảng kiểu std::vector:


o Duyệt các phần tử trong vector:
 Dùng vòng lặp FOR EACH:
 Dùng vòng lặp FOR kết hợp chỉ số index:

 Dùng vòng lặp FOR kết hợp con trỏ iterator:

o Để xem kích thước mảng gồm bao nhiêu phần tử, sử dụng hàm size():

o Để xem mảng có trống hay không, sử dụng hàm empty(). Nó sẽ trả về true nếu hàm có
trống, false nếu không.

o Thay đổi kích thước một mảng được cấp phát động rất phức tạp (Cấp phát động một
mảng mới => Sao chép các phần tử từ mảng cũ sang vùng nhớ mới => Xóa mảng cũ). Tuy
nhiên, điều này rất đơn giản đối với std::vector thông quay hàm resize():

Ghi chú 75: Khi thay đổi đến kích thước lớn hơn, các giá trị phần tử hiện có được giữ nguyên. Các phần tử mới được khởi tạo
bằng giá trị mặc định của kiểu dữ liệu mảng.

Ghi chú 76: Khi thay đổi đến kích thước nhỏ hơn, các phần tử mang chỉ số vượt quá giới hạn kích thước mới sẽ bị cắt bỏ đi, còn
lại giữ nguyên.

o Cấp phát một dung lượng ban đầu cho std::vector bằng hàm reserve():

o Có thể làm trống một std::vector bằng hàm clear(). Nhưng nó chỉ sẽ làm trống bằng việc
xóa đi tất cả phần tử của nó, không giải phóng bộ nhớ sử dụng cho việc lưu trữ dữ liệu
đã dùng. Khi ấy, phần dung lượng thừa không dùng đến có thể làm hao phí tài nguyên
và giảm hiệu suất, khi ấy có thể dùng hàm shrink_to_fit()

o Lấy giá trị phần tử đầu tiên hoặc cuối cùng của mảng bằng hàm front() hoặc back()
o Xem giới hạn kích thước tối đa (không phải dung lượng hiện được cấp phát cho mảng,
mà giới hạn do hệ thống) của mảng bằng hàm max_size()

o Truy cập giá trị các phần tử trong mảng bằng hàm assign()

Ghi chú 77: truy cập 5 phần tử mang giá trị 1

Ghi chú 78: dựng trước một mảng "arr" rồi gán giá trị các phần tử trong mảng "arr" sang mảng "array". arr trỏ tới array[0],
nên arr+1 và arr+3 lần lượt là array[1] và array[3]. Hàm assign() sẽ include giá trí đầu và exclude giá trị cuối.

o Thêm một hoặc nhiều phần tử vào vị chí chỉ đỉnh bằng hàm insert()

 v.insert(it,000); Thêm 0 vào đầu mảng


 v.insert(it,3,333); Thêm 333 ba lần vào đầu mảng
 v.insert(it+4, v1.begin(), v1.end()); Thêm cả mảng “v1” (v1.begin(), v1.end()) vào
vị trí thứ 4 của mảng
 Có thể thay v1.begin(), v1.end() bằng v1.begin(), v1.begin()+2 đều cho ra cùng
một kết quả, vì trình biên dịch include phần tử đầu và exclude phần tử cuối
o Xóa một hoặc nhiều phần tử vào vị chí chỉ đỉnh bằng hàm erase()

 v1.erase(v1.begin()+4); xóa phần tử thứ 5


 v2.erase(v2.begin()+1, v2.begin()+3); xóa các phần tử từ phần tử thứ 2 đến thứ
3 (vì trình biên dịch include phần tử đầu và exclude phần tử cuối)
o Hoán đổi giá trị của các mảng bằng hàm swap()
 Hàm sẽ hoán đổi toàn bộ nội dung của 2 set đã cho cho nhau và làm thay đổi nội
dung cũng như độ dài của chúng.

 Hoặc có thể dùng function template là std::swap để tiến hành hoán đổi 2 set với
nhau. Template này được định nghĩa trong thư viện <utility>

o Đảo ngược các phần tử trong vector bằng function template std::reverse. Template này
được định nghĩa trong thư viện <algorithm>:

Ghi chú 79: Cần dòng lệnh #include<algorithm> ở đầu chương trình

o Sao chép vector:


 Vector thuộc kiểu dữ liệu đối tượng, do vậy khác với các kiểu dữ liệu nguyên
thủy, chúng ta không thể sử dụng toán tử bằng = để gán và sao chép một vector
vào một vector mới.
 Thay vào đó, chúng ta sẽ sử dụng cách copy constructor:

o Mặc dù std::vector có thể được sử dụng như một mảng động, nhưng nó cũng có thể
được sử dụng như một ngăn xếp (stack). Mảng kiểu std::vector cung cấp các phương
thức:
 front() trả về tham chiếu đến phần tử đầu của vector. Thay đổi nó cũng làm thay
đổi giá trị đầu của vector:

 back() trả về tham chiếu đến phần tử cuối của vector. Thay đổi nó cũng làm thay
đổi giá trị cuối của vector:

 push _back() hoặc emplace _back() thêm một phần tử vào cuối vector:
 pop_back() xóa một phần tử cuối vector.

o Sắp xếp các phần tử theo thứ tự chỉ định bằng hàm sort()
 Hàm sort() được định nghĩa trong thư viện <algorithm> và namespace std;
 Sắp xếp tăng:

 Sắp xếp giảm: thêm đối số greater<type>() với type là kiểu dữ liệu

o Tính tổng các phần tử trong một phạm vi chỉ định:


 Có thể sử dụng vòng lặp FOR EACH để truy xuất tới từng phần tử và tính tổng
của chúng:

 Hoặc có thể dùng hàm accumulate() thuộc thư viện <numeric> và namespace
std để tính tổng trong phạm vi chỉ định:

Ghi chú 80: Cần dòng lệnh #include<numeric> ở đầu chương trình

DEQUE

 Deque là viết tắt của double-ended-queue, là một mảng động tương tự như vector nhưng lại có
khả năng thêm xóa phần tử ở cả đầu lẫn cuối mảng đó với tốc độ cao
 Ngoại trừ ưu thế có thể chèn và xóa ở gần đầu mảng của deque thì hiệu suất của deque kém
hơn vector=> khi không chèn và xóa thường xuyên gần đầu mảng thì nên sử dụng vector.
 Khác với vector thì các phần tử của deque không phải lúc nào cũng được lưu trữ tại các địa chỉ
liên tiếp trong bộ nhớ, vì vậy việc lấy địa chỉ của một phần tử và chuyển nó dưới dạng một con
trỏ đến một hàm khác có thể gây ra sự cố trong xử lý.
 Lớp std::deque được định nghĩa trong thư viện <deque>
 Về cách sử dụng cũng như các hàm thành viên trong deque thì hầu như là giống với vector:
o Khai báo deque:
o Truy cập phần tử trong deque:

o Khởi tạo deque:

Ghi chú 81: Khởi tạo với các phần tử có giá trị giống nhau

o Khai báo deque 2 chiều:

Ghi chú 82: Cần phải viết thêm dấu cách giữa cặp dấu > > khi khai báo vector 2 chiều để phân biệt với toán tử >> được sử dụng
để dịch chuyển bit trong C++.

Ghi chú 83: Khởi tạo các deque 1 chiều trước rồi dùng chúng để khai báo deque 2 chiều

o Duyệt các phần tử trong deque:

Ghi chú 84: Vòng lặp FOR kết hợp con trỏ iterator

Ghi chú 85: Vòng lặp FOR EACH

Ghi chú 86: Vòng lặp FOR kết hợp chỉ số index

o Kiểm tra deque trống bằng hàm empty():


o Làm trống deque bằng hàm clear():

o Đảo ngược các phần tử trong deque bằng hàm reverse(), định nghĩa trong thư viện
<algorithm>:

Ghi chú 87: Cần dòng lệnh #include<algorithm> ở đầu chương trình

o Lấy kích thước của deque bằng hàm size():

o Thay đổi kích thước của deque bằng hàm resize():

o Giải phóng bộ nhớ cho deque bằng hàm shrink_to_fit(). Tuy nhiên, lớp std::deque không
có hàm capacity() để kiểm tra bộ nhớ.

o Sao chép giá trị của deque:

o Hoán đổi deque bằng hàm swap():

Ghi chú 88: sử dụng khuôn mẫu hàm được định nghĩa trong thư viện <utility>

o Sắp xếp các phần tử trong deque:


 Sắp xếp tăng:
 Sắp xếp giảm:

o Tính tổng các phần tử trong phạm vi chỉ định trong deque bằng hàm accumulate():

Ghi chú 89: Cần dòng lệnh #include<numeric> ở đầu chương trình

o Sử dụng deque như một ngăn xếp (stack):


 front() trả về tham chiếu đến phần tử đầu của deque. Thay đổi nó cũng làm thay
đổi giá trị đầu của deque:

 back() trả về tham chiếu đến phần tử cuối của deque. Thay đổi nó cũng làm thay
đổi giá trị cuối của deque:

 emplace _back() thêm một phần tử vào cuối deque (không thể dùng push_back())

 pop_back() xóa một phần tử cuối deque.

LIST

 List có bản chất là mảng một chiều động, có các tính chất của một mảng một chiều. Mảng kiểu
std::list là một danh sách liên kết đôi, nghĩa là từng phần tử trong danh sách sẽ chứa thông tin vị
trí của phần tử đứng trước và sau nó.
 Để sử dụng lớp std::list, cần khai báo thư viện <list> và namespace std:
 Hầu hết các thao tác và hàm thành viên trong list cũng như deque và vector, chỉ duy nhất việc
không thể truy cập vào một phần tử bất kì trong list bằng chỉ số index (cũng như set)
 Để duyệt các phần tử trong list hoặc truy cập đến một phần tử chỉ định, cần sử dụng vòng lặp
FOR EACH hoặc con trỏ iterator:
o Truy cập tất cả các phần tử trong list:

o Truy cập đến một phần tử chỉ định trong list:

SET

 Set có bản chất là mảng một chiều động, có các tính chất của một mảng một chiều. Mảng kiểu
std::set là mảng với các phần tử không trùng lặp và luôn được sắp xếp.
 Để sử dụng lớp std::set, cần khai báo thư viện <set> và namespace std:
 Dù có thể dùng bất cứ kiểu dữ liệu nào có trong C++ để khai báo type, tuy nhiên do trong set các
phần tử cần phải được sắp xếp, nên kiểu của chúng cũng phải là kiểu dữ liệu có thể được so
sánh. Nếu sử dụng các kiểu dữ liệu không cơ bản (struct hoặc class) thì phải tự định nghĩa toán
tử so sánh nội bộ operator<() để làm rõ quan hệ lớn nhỏ giữa chúng.

 Cũng có thể khai báo đồng thời nhiều set:

 Có thể khởi tạo giá trị cho set ngay tại thời điểm khai báo:

 Khai báo set 2 chiều:

Ghi chú 90: Cần phải viết thêm dấu cách giữa cặp dấu > > khi khai báo set 2 chiều, để phân biệt với toán tử >> được sử dụng để
dịch chuyển bit trong C++.

o “st” là tên set 2 chiều


o “l1”, “l2”, “l3” là tên các set 1 chiều được sử dụng như phần tử của set 2 chiều

Ghi chú 91: Khai báo và khởi tạo set 2 chiều

Ghi chú 92: Khởi tạo các set 1 chiều trước rồi dùng chúng để khai báo set 2 chiều

 Khác với vector hay mảng, do cấu trúc của set theo dạng cây chứ không phải dạng mảng nên
chúng ta không thể truy cập ngẫu nhiên vào phần tử bất kỳ trong một set:
o Không thể sử dụng index của các phần tử để truy cập.
o Nếu một truy cập vào một phần tử chỉ định, cần truy cập tuần tự từ phần tử đầu và
dừng đến khi gặp phần tử cần tìm => kết hợp câu điều kiện IF và vòng lặp FOR EACH:

Ghi chú 93: Truy cập vào phần tử thứ 3 trong set và kết thúc khi tìm thấy

o Nếu muốn truy cập tất cả các phần tử, sử dụng đơn thuần vòng lặp FOR EACH:

Ghi chú 94: Sử dụng con trỏ iterator

o Sử dụng 2 vòng lặp FOR lồng nhau để truy xuất tất cả phần tử của set 2 chiều:

o Khi khởi tạo set với các phần tử có trùng lặp thì khi truy xuất, các phần tử trùng lặp bị bỏ
đi và phần còn lại được sắp xếp theo thứ tự tăng dần:

Ghi chú 95: Nếu là set 2 chiều thì sự sắp xếp diễn ra trên từng set 1 chiều

 Các thao tác với mảng kiểu std::set:


o Lấy kích thước set bằng hàm size():
Ghi chú 96: Kích thước lấy ra là sau khi bỏ đi các phần tử trùng lặp

o Kiểm tra một set có trống hay không bằng hàm empty(). Hàm sẽ trả về true nếu set đã
trống, false nếu ngược lại:

o Làm trống một set bằng hàm clear(). Khác với vector, ngoài việc làm trống set chỉ định
thì còn giải phóng bộ nhớ sử dụng cho việc lưu trữ dữ liệu đã dùng.

o Chèn một phần tử vào trong set bằng hàm insert():


 Hàm sẽ trả về một cặp kết quả pair<iterator, bool> với iterator là trình lặp của
set kết quả, và bool là việc có thực hiện việc chèn hay không, dưới dạng 0 hoặc 1
(Bởi vì các phần tử trong một set là duy nhất, nên nếu chưa tồn tại thì phần tử
đó sẽ được chèn và ngược lại thì không được chèn).

 Có thể kiểm tra việc chèn phần tử đã thực hiện hay chưa bằng phương thức first
hoặc second từ kết quả trả về của hàm:

 Có thể chèn nhiều phần tử vào set bằng cách tạo một mảng rồi chèn mảng ấy
vào set:

Ghi chú 97: Nếu nhập lệnh auto a = st.insert(array, array+3); sẽ bị báo lỗi

o Xóa phần tử trong set bằng hàm erase():


 Xóa phần tử bằng giá trị chỉ định

Ghi chú 98: "value" là giá trị của phần tử trong set cần xóa
 Xóa phần tử bằng con trỏ iterator. Vị trí sẽ được tính sau khi các phần tử trùng
lặp được lọc bỏ và phần còn lại được sắp xếp:

Ghi chú 99: "itr" là con trỏ sau khi thiết lập qua trình lặp

Ghi chú 100: Khác với các container khác thì để chuyển con trỏ iterator chỉ đến vị trí index thứ n trong set, chúng ta không thể
đơn giản cộng vào n vào trình lặp, mà cần phải di chuyển lần lượt qua từng vị trí, bằng toán tử ++ với đủ số vòng lặp.

 Hàm erase() trả về giá trị 0 khi không thực hiện xóa (phần tử cần xóa không tồn
tại trong set) và 1 khi ngược lại

Ghi chú 101: Chỉ thực hiện được với trường hợp xóa phần tử theo giá trị chỉ định

 Có thể xóa nhiều phần tử trong một phạm vi chỉ định bằng cặp con trỏ iterator.
Các con trỏ đều phải được thông qua trình lặp vị trí

Ghi chú 102: Phạm vi xóa được tính từ iterator_first đến trước iterator_last, nghĩa là phần tử ở vị trí iterator_first sẽ được xóa
nhưng phần tử ở vị trí iterator_last thì không.

o Tìm phần tử trong set:

Ghi chú 103: "val" là giá trị của phần tử cần tìm

 Hàm find() sẽ trả về con trỏ iterator trỏ đến vị trí phần tử có giá trị bằng giá trị
chỉ định nếu nó có tồn tại trong set, và trỏ về vị trí cuối cùng trong set nếu
không tìm thấy => có thể dùng kết hợp với hàm erase() để xóa đi một phần tử
chỉ định mà không cần dùng vòng lặp FOR EACH để đẩy con trỏ.

Ghi chú 104: "val" là giá trị của phần tử cần tìm

 Hàm lower_bound() sẽ trả về con trỏ iterator trỏ đến vị trí phần tử đầu tiên có
giá trị lớn hơn hoặc bằng với giá trị chỉ định, và trỏ về vị trí cuối cùng trong set
nếu không tìm thấy.

Ghi chú 105: "key" là giá trị của phần tử cần tìm

 Hàm upper_bound() sẽ trả về con trỏ iterator trỏ đến vị trí phần tử đầu tiên có
giá trị lớn hơn với giá trị chỉ định, và trỏ về vị trí cuối cùng trong set nếu không
tìm thấy.

=> Kết hợp sử dụng hàm lower_bound() và upper_bound() có thể giúp truy cập
các phần tử nằm trong giá trị chỉ định [a,b)

Ghi chú 106: "val" là giá trị của phần tử cần tìm

 Hàm equal_range() sẽ trả về một cặp giá trị, với giá trị đầu tiên trỏ đến đầu
phạm vi (tương đương kết quả hàm lower_bound()), và giá trị thứ hai trỏ đến
cuối phạm vi (tương đương kết quả hàm upper_bound()).
 Để lấy giá trị từ hàm, cần một cặp giá trị chứa trong toán tử pair<,>, và để truy
xuất 2 giá trị ấy, sử dụng phương thức first và second:

Ghi chú 107: Đầu chương trình cần thêm dòng lệnh typedef set<int>::iterator It;

o Đếm số lần xuất hiện phần tử bằng hàm count():

Ghi chú 108: "val" là giá trị của phần tử cần đếm

 Do trong set các phần tử là duy nhất, nên một phần tử nếu tồn tại cũng chỉ có
xuất hiện 1 lần => kết quả trả về của hàm là 0 tương ứng với phần tử không tồn
tại, hoặc 1 tương ứng với phần tử có tồn tại.
o Tính tổng các phần tử trong set:
 Có thể sử dụng vòng lặp FOR EACH để truy xuất tới từng phần tử và tính tổng
của chúng:

 Hoặc có thể dùng hàm accumulate() thuộc thư viện <numeric> và namespace
std . Không như vector, không thể chỉ định các phạm vi nhỏ hơn bằng cách
st.begin()+n vì cấu trúc mảng kiểu std::set không cho phép truy cập phần tử chỉ
định một cách trực tiếp.

Ghi chú 109: Đối số cuối cùng (0) là giá trị khởi tạo của tổng.

Ghi chú 110: Cần dòng lệnh #include<numeric> ở đầu chương trình

o Sao chép set:


 Mảng kiểu std::set thuộc kiểu dữ liệu đối tượng, do vậy khác với các kiểu dữ liệu
nguyên thủy, không thể sử dụng toán tử bằng = để gán và sao chép một set vào
một set mới.
 Thay vào đó, sử dụng cách copy constructor trong set với cú pháp như sau:

Ghi chú 111: “st_src” là set nguồn để copy và “st_dest” là set đích dùng để dán kết quả sao chép.

o Hoán đổi 2 set bằng hàm swap():


 Hàm sẽ hoán đổi toàn bộ nội dung của 2 set đã cho cho nhau và làm thay đổi nội
dung cũng như độ dài của chúng.

Ghi chú 112: “st1” và “st2” là 2 set cần hoán đổi nội dung cho nhau.
 Hoặc có thể dùng function template là std::swap để tiến hành hoán đổi 2 set với
nhau. Template này được định nghĩa trong thư viện <utility>

Ghi chú 113: Cần thêm dòng lệnh #include<utility> đầu chương trình

 Multiset cũng như set, chỉ khác ở chỗ nó cho phép các phần tử mang giá trị giống nhau cùng tồn
tại. Mọi thao tác như khai báo, khởi tạo, định nghĩa,… đều tương tự như set. Nhưng vì cho phép
tồn tại các phần tử giống nhau nên vẫn có một số thao tác cho ra kết quả khác:
o Khi đếm số phần tử có trong multiset thì hàm size() sẽ trả về toàn bộ các phần tử mà
chúng ta đã dùng để khai báo.
o Luôn có thể chèn thêm một/ nhiều phần tử vào multiset với hàm insert() không phân
biệt việc nó đã tồn tại hay chưa, và hàm insert() khi này cũng không trả về giá trị bool 1
hoặc 0 như đối với set.
o Khi đếm số lần xuất hiện của một phần tử trong multiset bằng hàm count(), giá trị cho ra
có thể là 0, 1 hoặc nhiều hơn
o Tìm phần tử bằng hàm equal_range(): kết quả cho ra có nghĩa và hữu ích hơn, vì có khả
năng tồn tại nhiều hơn một phần tử mang giá trị mà ta đang tìm kiếm => các thao tác
trên giá trị lấy được từ hàm sẽ hữu ích hơn:
MAP

 Map có bản chất là mảng một chiều động, có các tính chất của một mảng một chiều. Mảng kiểu
std::map là mảng với các phần tử luôn là duy nhất (về chỉ số key) và được sắp xếp (tương tự
set).
 Để sử dụng lớp std::map, cần khai báo thư viện <map> và namespace std:
 Nhưng điểm khác biệt lớn của map so với các lớp khác là chỉ số index của các phần tử được thay
bằng chỉ số key, và chỉ số key này do lập trình viên tự quy định và khởi tạo. Tức thay vì mỗi phần
tử được khởi tạo bởi giá trị thì trong map, các phần tử sẽ được tạo thành từ một cặp khoá và giá
trị (key & value).
 Các phần tử trong map sẽ luôn được tự động sắp xếp dựa trên chỉ số key: nếu là int thì theo thứ
tự 123, nếu là char thì theo thứ tự abc, nếu là string thì theo thứ tự abc và tính từ kí tự đầu tiên
rồi xét dần.

 Khai báo map với các kiểu dữ liệu cơ bản cần một cặp giá trị. Còn với kiểu dữ liệu không nguyên
thủy, ví dụ như struct chẳng hạn thì chúng ta phải tự tạo ra toán tử so sánh nội bộ operator<()
để làm rõ quan hệ lớn nhỏ giữa các phần tử vì các phần tử trong map cần được sắp xếp.

Ghi chú 114: "k_type" là kiểu dữ liệu của khóa, "v_type" là kiểu dữ liệu của giá trị phần tử

Ghi chú 115: Khai báo đồng thời nhiều map

 Có thể khởi tạo chỉ số key và giá trị cho một phần tử bất kỳ thuộc map với toán tử []. Nếu thêm
một phần tử với chỉ số key có sẵn thì giá trị của phần tử sẵn có ấy sẽ bị ghi đè:

Ghi chú 116: Một phần tử có thể được khởi tạo nhiều lần, giá trị trong lượt thay đổi giá trị cuối cùng sẽ được sử dụng
làm giá trị phần tử.

 Khởi tạo map:

Ghi chú 117: "k_type" là kiểu dữ liệu của khóa, "v_type" là kiểu dữ liệu của giá trị phần tử

Ghi chú 118: Do mỗi khóa trong map là duy nhất, nên nếu chúng ta chỉ định các phần tử có cùng khóa thì dù giá trị của chúng có
giống hay khác nhau thì chỉ có duy nhất phần tử viết đầu tiên sẽ được lưu vào trong map
 Có thể truy cập phần tử trong map bằng toán tử [] hoặc hàm thành viên at():
o Truy cập trực tiếp bằng [] không kiểm tra phạm vi. Nếu như key tồn tại trong map, giá trị
tương ứng của key sẽ được trả về. Tuy nhiên nếu không tồn tại, giá trị 0 sẽ được trả về:

o Truy cập gián tiếp bằng hàm at() có kiểm tra phạm vi. Nếu như key tồn tại trong map, giá
trị tương ứng của key sẽ được trả về. Tuy nhiên nếu không tồn tại, lỗi out_of_range sẽ
được trả về
 Có thể duyệt tất cả các phần tử trong map bằng vòng lặp FOR EACH hoặc vòng lặp FOR lồng con
trỏ iterator:
o Sử dụng vòng lặp FOR EACH:

Ghi chú 119: vì mỗi biến vòng lặp x mang một cặp giá trị nên dùng phương thức first và second, kèm toán tử . phía trước

o Sử dụng vòng lặp FOR lồng con trỏ iterator:

Ghi chú 120: vì mỗi con trỏ trỏ vào một cặp giá trị nên dùng phương thức first và second, kèm toán tử -> phía trước

 Các thao tác với mảng kiểu std::map:


o Kiểm tra map trống với hàm empty():

o Làm trống map bằng hàm clear(). Không như vector và deque thì hàm còn giải phóng bộ
nhớ đã được sử dụng:

o Lấy kích thước map với hàm size(). Hàm sẽ lấy kích thước sau khi các phần tử với key
trùng lặp được loại bỏ:

o Đếm số lần xuất hiện của phần tử bằng hàm count():

Ghi chú 121: “mp” là tên map, "key" là khóa của phần tử cần tìm

 Do trong map các khóa là duy nhất, nên một phần tử nếu tồn tại cũng chỉ có
xuất hiện 1 lần duy nhất => kết quả trả về của hàm cũng chỉ là 0 tương ứng với
phần tử không tồn tại, hoặc 1 tương ứng với phần tử tồn tại trong map đó.
 Hàm có ý nghĩa hơn khi dùng trong multimap
o Tìm một phần tử trong map:
 Hàm find() sẽ trả về con trỏ iterator trỏ đến vị trí phần tử mang giá trị chỉ định
nếu nó tồn tại và đến vị trí cuối cùng trong map nếu ngược lại.

Ghi chú 122: “mp” là tên map, "key" là khóa của phần tử cần tìm

 Hàm lower_bound() sẽ trả về con trỏ iterator trỏ đến vị trí phần tử mang giá trị
lớn hơn hoặc bằng giá trị chỉ định nếu nó tồn tại và đến vị trí cuối cùng trong
map nếu ngược lại.

Ghi chú 123: “mp” là tên map, "key" là khóa của phần tử cần tìm

 Hàm upper_bound() sẽ trả về con trỏ iterator trỏ đến vị trí phần tử mang giá trị
lớn hơn giá trị chỉ định nếu nó tồn tại và đến vị trí cuối cùng trong map nếu
ngược lại.

Ghi chú 124: “mp” là tên map, "key" là khóa của phần tử cần tìm

=> Có thể kết hợp 2 hàm lower_bound() và upper_bound() để xóa hoặc in ra các
phần tử trong phạm vi chỉ định:

 Hàm equal_range() sẽ trả về một cặp giá trị, với giá trị đầu tiên trỏ đến đầu
phạm vi (tương đương kết quả hàm lower_bound()), và giá trị thứ hai trỏ đến
cuối phạm vi (tương đương kết quả hàm upper_bound()). Hàm có ý nghĩa hơn
khi sử dụng trong multiset vì các giá trị của khóa có thể lập lại.

Ghi chú 125: “ret” là biến con trỏ nhận cặp giá trị tương ứng lower_bound và upper_bound, mà mỗi giá trị lại mang 2 giá trị nhỏ
hơn là key và value, nên cần dùng 2 tầng phương thức . và => để truy xuất đến từng giá trị cụ thể nhất.

o Chèn phần tử trong map bằng hàm emplace(). Nếu phần tử mang key chỉ định đã tồn tại
thì việc chèn không được thực hiện. Không như hàm insert(), không thể dùng hàm
emplace() để chèn các phần tử từ một map vào trong map khác.

Ghi chú 126: "k " là giá trị của khóa, "v " là giá trị phần tử
o Chèn phần tử trong map bằng hàm insert(). Vì mỗi phần tử được cấu thành bởi key và
value, nên cần truyền vào map một cặp giá trị bằng phương thức pair<>:

Ghi chú 127: Trường hợp không rõ kiểu, hoặc muốn rút bỏ chỉ định kiểu của key và value

 “mp” là tên map


 pair<k_type,x_type>(k,v) sử dụng để chỉ định key và value của phần tử cần
thêm, trong đó “k_type”, “x_type” là kiểu và “k”, “v” là key và value.
 Hàm insert() sẽ trả về một cặp kết quả pair<iterator, bool> với iterator là trình
lặp trỏ đến map kết quả, và bool là việc có thực hiện việc chèn hay không, dưới
dạng 0 hoặc 1. Và để lưu cặp giá trị ấy, cần định nghĩa trước một biến bằng hàm
typedef

Ghi chú 128: "It" là tên biến lưu trữ, int và char lần lượt là kiểu dữ liệu của key và value

 Bởi vì các phần tử trong một map là duy nhất, nên thao tác chèn sẽ kiểm tra
xem mỗi phần tử được chèn đã tồn tại trong map hay chưa. Nếu chưa tồn tại thì
phần tử đó sẽ được chèn và ngược lại nếu đã tồn tại thì không được chèn, chứ
không ghi đè giá trị như khi dùng toán tử []

Ghi chú 129: cần dòng lệnh typedef map<int,char>::iterator It; trước hàm main()

 Có thể chèn một/nhiều phần tử từ một map vào trong map khác:

Ghi chú 130: “iterator_first” và “iterator_last” là các con trỏ iterator xác định phạm vi chứa các phần tử cần chèn ở trong một
map khác vào map ban đầu. Phạm vị [first, last)

o Xóa phần tử trong map bằng hàm erase().


 Xóa bằng cách sử dụng giá trị khóa. Khi này hàm erase() sẽ trả về số lượng phần
tử đã được xóa đi từ map ban đầu.

Ghi chú 131: “mp” là tên map, "key" là khóa của phần tử cần xóa
 Xóa bằng cách sử dụng con trỏ iterator. Khác với array, vector hay deque thì để
chuyển trình lặp chỉ đến vị trí index thứ n trong map, chúng ta không thể đơn
giản cộng vào n vào trình lặp, mà cần phải di chuyển lần lượt qua từng vị trí,
bằng toán tử ++ với đủ số vòng lặp.

Ghi chú 132: “mp” là tên map, "itr" là con trỏ trỏ tới vị trí của phần tử cần xóa

 Xóa các phần tử trong phạm vi chỉ định, chỉ dùng cách chon trỏ iterator:

Ghi chú 133: “mp” là tên map, “iterator_first” và “iterator_last” là các con trỏ iterator trỏ đến phạm vi bắt đầu và kết thúc xóa.

o Sao chép map:

Ghi chú 134: “type” là kiểu dữ liệu, “mp_src” là map nguồn để copy và “mp_dest” là map đích dùng để dán kết quả sao chép.

o Hoán đổi map:


 Sử dụng hàm swap():

 Sử dụng khuôn mẫu hàm swap(), được định nghĩa trong thư viện <utility>:
Ghi chú 135: cần dòng lệnh #include<utility> ở đầu chương trình

 Multimap cũng như map, chỉ khác ở chỗ nó cho phép các phần tử mang giá trị key giống nhau
cùng tồn tại. Mọi thao tác như khai báo, khởi tạo, định nghĩa,… đều tương tự như map. Nhưng
vì cho phép tồn tại các phần tử giống nhau nên vẫn có một số thao tác cho ra kết quả khác:
o Khi đếm số phần tử có trong multimap thì hàm size() sẽ trả về toàn bộ các phần tử mà
chúng ta đã dùng để khai báo.
o Luôn có thể chèn thêm một/ nhiều phần tử vào multimap với hàm insert() không phân
biệt việc nó đã tồn tại hay chưa, và hàm insert() khi này cũng không trả về giá trị bool 1
hoặc 0 như đối với map.
o Khi đếm số lần xuất hiện của một phần tử trong multimap bằng hàm count(), giá trị cho
ra có thể là 0, 1 hoặc nhiều hơn:

o Tìm phần tử bằng hàm equal_range(): kết quả cho ra có nghĩa và hữu ích hơn, vì có khả
năng tồn tại nhiều hơn một phần tử mang giá trị mà ta đang tìm kiếm => các thao tác
trên giá trị lấy được từ hàm sẽ hữu ích hơn:
HÀM (tiếp theo)

 Truyền địa chỉ cho hàm (Passing arguments by address)


o Truyền địa chỉ cho hàm là truyền địa chỉ của biến đối số chứ không phải giá trị biến đối
số. Vì đối số là một địa chỉ, tham số hàm phải là một con trỏ. Sau đó, hàm có thể truy
cập hoặc thay đổi giá trị được trỏ đến.

Ghi chú 136: Hàm “foo” đã thay đổi giá trị của đối số (biến “value”) thông qua tham số hàm là con trỏ “ptr” (địa chỉ biến
“value”).

o Truyền địa chỉ cho hàm thường được sử dụng với mảng:

Ghi chú 137: Có thể sử dụng tham số hằng con trỏ trong trường hợp hàm không có mục đích thay đổi giá trị đối số.

o Truyền địa chỉ cho hàm bằng giá trị


 Khi truyền một con trỏ tới một hàm theo địa chỉ, giá trị của con trỏ (địa chỉ mà
nó trỏ tới) sẽ được sao chép từ đối số sang tham số hàm => tham số hàm và đối
số đều trỏ đến cùng một giá trị.
 Nếu thay đổi giá trị được tham số hàm trỏ đến, điều đó sẽ thay đổi giá trị mà
đối số đang trỏ đến, vì cả tham số hàm và đối số đều trỏ đến cùng một giá trị.
 Nếu tham số hàm được gán một địa chỉ khác, điều đó sẽ không ảnh hưởng đến
đối số, vì tham số hàm chỉ là bản sao của đối số.
o Truyền địa chỉ cho hàm bằng tham chiếu:
 Chúng ta có thể truyền địa chỉ cho hàm bằng tham chiếu để thay đổi địa chỉ mà
đối số trỏ đến từ bên trong hàm.
 Về cơ bản, mọi thay đổi trên con trỏ tham chiếu cũng sẽ làm thay đổi con trỏ
được tham chiếu

 Giá trị trả về


o Hàm trả về giá trị (return by value)
 Hàm trả về giá trị là phương pháp an toàn và dễ sử dụng nhất:
 Giá trị trả về của hàm có thể là biến, hằng, biểu thức.
 Khi một giá trị được trả về, một bản sao của giá trị đó được tạo ra và trả về cho
lời gọi hàm.

 Hàm trả về giá trị thích hợp nhất khi trả về các biến được khai báo bên trong
hàm hoặc trả về các đối số hàm được truyền theo giá trị. Tuy nhiên, hàm trả về
giá trị sẽ gây giảm hiệu suất trong trường hợp giá trị trả về là kiểu cấu trúc
(structs) hoặc các lớp (classes) phức tạp.
o Hàm trả về địa chỉ (return by address)
 Hàm trả về địa chỉ là hàm trả về địa chỉ của một biến cho lời gọi hàm.
 Giá trị trả về của hàm chỉ có thể là địa chỉ của biến, không phải hằng hoặc biểu
thức (vì không có địa chỉ).
 Hàm trả về địa chỉ nhanh hơn trả về giá trị, vì chỉ cần sao chép địa chỉ và trả về
cho lời gọi hàm. Và không thể trả về địa chỉ của biến cục bộ bên trong hàm.

Ghi chú 138: Biến “value” bị hủy ngay sau khi ra khỏi hàm. Lúc này địa chỉ của biến “value” thuộc vùng nhớ không được cấp
phát (con trỏ lơ lửng), điều này có thể gây ra lỗi undefined behavior khi sử dụng nếu vùng nhớ này được cấp phát cho một
chương trình khác.

 Hàm trả về địa chỉ thường được sử dụng để trả về địa chỉ vùng nhớ được cấp
phát động:

o Hàm trả về tham chiếu (return by reference)


 Hàm trả về tham chiếu hoạt động gần như tương tự với phương pháp truyền
tham chiếu cho hàm. Nó trả về tham chiếu của một biến cho lời gọi hàm. Giá trị
trả về của hàm là biến, không phải hằng hoặc biểu thức.
 Giống hàm trả về địa chỉ, không nên trả về các tham chiếu của biến cục bộ:

Ghi chú 139: Biến “value” bị hủy ngay sau khi ra khỏi hàm. Lúc này, hàm trả về một tham chiếu đến vùng nhớ rác. Điều này có
thể gây ra lỗi undefined behavior khi sử dụng nếu vùng nhớ này được cấp phát cho một chương trình khác.

 Hàm trả về tham chiếu thường được sử dụng để trả về các đối số được truyền
theo tham chiếu:
Ghi chú 140: Hàm getElement(arr, 2) trả về một tham chiếu đến phần tử mảng arr[2], sau đó tham chiếu này (arr[2]) được gán
bằng 20 => phần tử thứ 2 của mảng “arr” được gán giá trị 20

 Hàm nội tuyến (Inline function)


o Khi một hàm được gọi, CPU sẽ
 Lưu địa chỉ bộ nhớ của dòng lệnh hiện tại mà nó đang thực thi (để biết nơi sẽ
quay lại sau lời gọi hàm)
 Lao chép các đối số của hàm trên ngăn xếp (stack)
 Chuyển hướng điều khiển sang hàm đã chỉ định.
 CPU sau đó thực thi mã bên trong hàm, lưu trữ giá trị trả về của hàm trong một
vùng nhớ/thanh ghi và trả lại quyền điều khiển cho vị trí lời gọi hàm.
o Điều này sẽ tạo ra một lượng chi phí hoạt động nhất định so với việc thực thi mã
trực tiếp (không sử dụng hàm).
o Đối với các hàm lớn hoặc các tác vụ phức tạp, tổng chi phí của lệnh gọi hàm thường
không đáng kể so với lượng thời gian mà hàm mất để chạy. Tuy nhiên, đối với các hàm
nhỏ, thường được sử dụng, thời gian cần thiết để thực hiện lệnh gọi hàm thường nhiều
hơn rất nhiều so với thời gian cần thiết để thực thi mã của hàm.
o Từ khoá inline được sử dụng để đề nghị (không phải là bắt buộc) trình biên dịch thực
hiện khai triển nội tuyến (inline expansion) với hàm đó, tức là chèn code của hàm đó
tại địa chỉ mà nó được gọi.

Ghi chú 141: Khi chương trình trên được biên dịch,
mã máy được tạo ra tương tự như hàm này
o Như trường hợp này, sử dụng inline functions sẽ thực thi nhanh hơn một chút so với
hàm thông thường.
o Trình biên dịch có thể không thực hiện nội tuyến trong các trường hợp như:
 Hàm chứa vòng lặp (for, while, do-while).
 Hàm chứa các biến tĩnh.
 Hàm đệ quy.
 Hàm chứa câu lệnh switch hoặc goto.
o Hầu hết các trình biên dịch hiện đại sẽ tự động đặt các hàm nội tuyến nếu cần thiết. Do
đó, trong hầu hết các trường hợp, nếu không có nhu cầu cụ thể để sử dụng từ khóa nội
tuyến, hãy để trình biên dịch tự động xử lý các hàm nội tuyến.
o Có thể đặt định nghĩa hàm nội tuyến trong file tiêu đề (*.h) (nghĩa là nó có thể được
include trong nhiều đơn vị biên dịch, hàm thông thường sẽ gây ra lỗi), nhưng nó làm cho
file tiêu đề lớn hơn.
 Nạp chồng hàm (function overloading)

o Hàm “addInteger” cho phép cộng các dữ liệu số nguyên, hàm “addDouble” cho phép
cộng các dữ liệu số thực. Khi có nhu cầu tính tổng 2 số, ta phải xác định kiểu dữ liệu
trước khi thực hiện lời gọi hàm => gây bất lợi trong nhiều trường hợp
o Nạp chồng hàm cho phép sử dụng cùng một tên gọi cho nhiều hàm (có cùng mục đích
nhưng khác nhau về kiểu dữ liệu tham số hoặc số lượng tham số.

Ghi chú 142: số lượng tham số có


thể khác nhau

o Một số hàm không thể nạp chồng:


 Hàm chỉ khác nhau kiểu trả về:

Ghi chú 143: Hai hàm trên giống nhau về tham số (không có), vì vậy trình biên dịch sẽ báo lỗi.

 Tham số hàm kiểu typedef:


Ghi chú 144: Khai báo typedef chỉ là một bí danh (không phải kiểu dữ liệu mới), vì vậy chương trình gặp lỗi

 Tham số hàm kiểu con trỏ * và mảng []

Ghi chú 145: Hai hàm trên giống nhau về tham số (int*), vì vậy trình biên dịch sẽ báo lỗi.

 Nạp chồng hàm với tham số là const chỉ khi tham số const không là tham chiếu
hoặc con trỏ:

Ghi chú 146: Biên dịch lỗi. Tham số 'i' được truyền theo giá trị, vì vậy 'i' trong fun() là bản sao của 'i' trong main(). Do đó, fun()
không thể sửa đổi 'i' của hàm main() => không quan trọng việc 'i' được nhận dưới dạng tham số const hay tham số bình thường.

Ghi chú 147: Biên dịch thành công

 Đối số mặc định (Default arguments)


o Đối số mặc định là một giá trị mặc định được cung cấp cho tham số hàm.
 Nếu người dùng không cung cấp một đối số rõ ràng cho một tham số có đối số
mặc định, giá trị mặc định sẽ được sử dụng.
 Nếu người dùng cung cấp một đối số cho tham số, thì đối số do người dùng
cung cấp sẽ được sử dụng.
 Tham số có giá trị mặc định thường được gọi là tham số tùy chọn.
o Một hàm có thể có nhiều đối số mặc định:

Ghi chú 148: Tất cả các tham số có đối số mặc định phải được khai báo liên tục, và đặt cuối cùng trong danh sách tham số.

o Đối với một hàm có tiền khai báo (nguyên mẫu hàm) và định nghĩa hàm, đối số mặc định
có thể được khai báo ở một trong hai, nhưng không phải cả hai.

o Hàm có đối số mặc định có thể được nạp chồng:

Ghi chú 149: Các tham số có đối số mặc định không được sử dụng để xác định tính duy nhất trong nạp chồng hàm. Khi gọi hàm
print(5);, trình biên dịch sẽ không thể xác định được người dùng muốn print(5) hay print(5, 0).

 Con trỏ hàm

o Giống như các biến, hàm cũng được lưu trữ tại một địa chỉ trong bộ nhớ. Khi hàm được
gọi, chương trình sẽ đi đến địa chỉ của hàm trong bộ nhớ, sau đó thực thi mã lệnh tại
vùng nhớ đó.
o Vì hàm cũng có địa chỉ trong bộ nhớ, nên ta cũng có thể khai báo một con trỏ cho một
hàm.
o Cú pháp khai báo con trỏ hàm:
<kiểu trả về> (*<tên con trỏ>)(<danh sách tham số>);

Ghi chú 150: Trình biên dịch ngầm chuyển đổi một hàm thành một con trỏ hàm nếu cần => không cần sử dụng toán tử & để lấy
địa chỉ của hàm

o Cấu trúc (tham số và kiểu trả về) của con trỏ hàm phải khớp với cấu trúc của hàm:
 Giá trị nhận vào của con trỏ phải cùng kiểu dữ liệu với biến trong hàm
 Giá trị trả về của con trỏ phải cùng kiểu dữ liệu với kiểu dữ liệu hàm

o Có 2 cách để thực hiện lời gọi hàm thông qua con trỏ hàm: ngầm định và tường minh

Ghi chú 151: Các tham số mặc định của hàm không sử dụng được thông qua con trỏ hàm. Tham số mặc định được compiler xác
định tại thời điểm biên dịch, còn con trỏ hàm được sử dụng tại thời điểm chương trình đang chạy.

o Truyền hàm vào hàm dưới dạng đối số


 Xét ví dụ: Xây dựng hàm thực hiện việc sắp xếp tăng, giảm mảng 1 chiều các số
nguyên.
 Có thể xây dựng riêng 2 hàm: một hàm sắp xếp tăng, một hàm sắp xếp giảm
Ghi chú 152: Hai hàm chỉ khác nhau một dòng so sánh if (arr[idx] < arr[j]) và if (arr[idx] > arr[j]). Thực hiện lời gọi
hàm với mảng tên “array” có 5 phần tử sapxeptang(array,5); và sapxepgiam(array,5); ở hàm main()

 Có thể xây dựng một hàm sắp xếp tổng quát bằng cách dựng thêm các hàm chỉ
thực hiện thao tác so sánh riêng, rồi truyền hàm đó (dưới dạng con trỏ) vào hàm
sắp xếp tổng quát ấy như một đối số:

Ghi chú 153: bool(*comparison)(int, int) là một con trỏ hàm, nó trỏ tới hàm bool tang() hoặc bool giam(), để từ đó cho ra a>b
hay a<b với các đối số là arr[find_idx] và arr[j]. Thực hiện lời gọi hàm với mảng tên “array” có 5 phần tử
sapxeptongquat(array,5, tang); và sapxeptongquat(array,5, giam); ở hàm main()

o Tương tự như những kiểu dữ liệu cơ bản khác, chúng ta có thể cung cập một đối số mặc
định cho tham số hàm kiểu con trỏ hàm.

o Có thể thay thế cho việc sử dụng con trỏ hàm bằng cách sử dụng kiểu dữ liệu
std::function thuộc thư viện <functional>
Ghi chú 154: Sử dụng kiểu dữ liệu std::function cũng tương tự như sử dụng con trỏ hàm, chỉ khác nhau về cách khai báo.

o Từ khóa auto được dùng để tự động nhận dạng kiểu dữ liệu thông qua kiểu dữ liệu của
giá trị khởi tạo ra nó, nên cũng có thể nhận dạng ra loại con trỏ hàm.

Ghi chú 155: Từ khóa auto giúp cú pháp đơn giản hơn. Tuy nhiên, nhược điểm là tất cả các chi tiết về các tham số và kiểu trả về
của hàm đều bị ẩn, do đó dễ mắc lỗi hơn khi sử dụng con trỏ hàm.

 Hàm đệ quy (Recursion Function)


o Trong lập trình, một hàm được gọi là đệ quy nếu bên trong thân hàm có một lời gọi đến
chính nó.
o Hàm đệ quy luôn có điều kiện dừng được gọi là điểm neo. Khi đạt tới điểm neo, hàm sẽ
không gọi chính nó nữa. Khi được gọi, hàm đệ quy thường được truyền cho một tham
số, thường là kích thước của bài toán lớn ban đầu. Sau mỗi lời gọi đệ quy, tham số sẽ
nhỏ dần, nhằm phản ánh bài toán đã nhỏ hơn và đơn giản hơn. Khi tham số đạt tới một
giá trị cực tiểu (tại điểm neo), hàm sẽ chấm dứt.

Ghi chú 156: Khi countDown(3) được gọi, thì "n 3" được in ra và countDown(2) được gọi. Sau đó countDown(2) in "n 2" và gọi
countDown(1). Sau đó countDown(1) in "n 1" và gọi countDown(0). Theo trình tự, trong hàm countDown(n) sẽ gọi
countDown(n-1), việc này được lặp lại vô hạn, đây là một hàm đệ quy lặp vô hạn.

o Hàm đệ quy phải có một điều kiện kết thúc đệ quy, nếu không chương trình sẽ lặp vô
hạn (đến khi tràn bộ nhớ ngăn xếp). Điều kiện dừng của hàm đệ quy gọi là điều kiện cơ
sở.
Ghi chú 157: Do có điều kiện kết thúc (count > 1), nên trong hàm countDown(1) không gọi countDown(0), vì vậy "pop 1" được in
ra và kết thúc hàm countDown(1). Lúc này, hàm countDown(1) bật ra khỏi ngăn xếp, hàm countDown(2) tiếp tục thực thi tại vị
trí sau lời gọi hàm countDown(1), do đó "pop 2" được in ra. Tuần tự đến khi thoát ra toàn bộ các lời gọi hàm đệ quy.

oSử dụng vòng lặp thường có hiệu suất cao hơn so với phiên bản hàm đệ quy. Vì khi một
hàm được gọi, chương trình sẽ tốn một lượng chi phí cho việc đưa hàm vào ngăn xếp =>
Ưu tiên sử dụng vòng lặp thay vì đệ quy.
 Khuôn mẫu hàm (Function templates)
o Khuôn mẫu hàm là một cái khuôn dùng để tạo ta nhiều hàm có định nghĩa giống nhau.
Và nó có thể được tái sử dụng nhiều lần nếu chúng ta muốn.
o Khi hàm khuôn mẫu được gọi, trình biên dịch sẽ tạo ra một bản sao của hàm đó, và thay
thế kiểu dữ liệu tương ứng của các tham số khuôn mẫu.

 Khi gặp lời gọi hàm Swap(x, y);, trình biên dịch lúc này sẽ tạo ra một hàm void
Swap(int& x, int& y) từ khuôn mẫu hàm, và biên dịch hàm thành mã máy.
 Khi gặp lời gọi hàm Swap(a, b);, trình biên dịch cũng tạo một phiên bản khác
void Swap(double& x, double& y) tương ứng.
 Khi gặp Swap(c, d);, vì trình biên dịch đã tạo ra một hàm void Swap(int& x, int&
y) trước đó, nên lúc này trình biên dịch không cần tạo ra phiên bản mới.
HÀM (tiếp theo)

Kiểu cấu trúc (Struct) là một tập hợp các thuộc tính liên quan tới cùng một đối tượng. (Ví dụ: tập hợp
các thuộc tính liên quan tới một người như tên, tuổi và giới tính). Các thuộc tính tạo nên một cấu trúc
được gọi là các thành viên (member).

Khác với mảng, Struct có khả năng nhóm các dữ liệu với nhiều kiểu dữ liệu khác nhau.

 Khai báo và khởi tạo kiểu cấu trúc sử dụng lệnh struct:
o Khai báo:

Ghi chú 158: cần phải thêm dấu ; vào vị trí cuối cùng khi khai báo cấu trúc

 “name” là tên của cấu trúc cần khai báo


 các cặp “type” và “member” là kiểu và tên của các thành viên
o Khởi tạo giá trị:

 “struct_name” là tên cấu trúc đã được khai báo


 “instance_name” là tên của thực thể tạo ra từ cấu trúc
o Khi muốn gán luôn các giá trị ban đầu vào các thành viên ngay khi tạo thực thể: Viết lần
lượt theo thứ tự giá trị của các thành viên, cách nhau bởi , và nằm giữa cặp { }

 Khai báo và khởi tạo kiểu cấu trúc sử dụng lệnh typedef struct:
o Khai báo:

 “struct_name” là tên của cấu trúc cần khai báo


 các cặp “type” và “member” là kiểu và tên của các thành viên
o Khởi tạo giá trị:

 “struct_name” là tên cấu trúc đã được khai báo


 “instance_name” là tên của thực thể tạo ra từ cấu trúc
o Khi muốn gán luôn các giá trị ban đầu vào các thành viên ngay khi tạo thực thể: Viết lần
lượt theo thứ tự giá trị của các thành viên, cách nhau bởi , và nằm giữa cặp { }
 Truy cập vào các thành viên trong kiểu cấu trúc
o Cú pháp:

 “member” là tên thành viên cần truy cập


 “instance” là tên của thực thể
o Gán giá trị mới cho thành viên đó thông qua toán tử =

 Con trỏ cấu trúc


o Thực thể tạo ra từ kiểu cấu trúc cũng là một loại dữ liệu trong C++, và do đó, có thể sử
dụng con trỏ để lưu trữ địa chỉ và qua đó thao tác với chúng.
o Cú pháp:
 Sau khi khai báo một kiểu cấu trúc:

 Khởi tạo một thực thể từ kiểu cấu trúc mới tạo, cũng như một con trỏ trỏ tới
thực thể này:

o Để truy cập vào các thành viên của thực thể, sử dụng tới dấu mũi tên ->

 “instance_pointer” là con trỏ cấu trúc dùng để lưu địa chỉ của thực thể “instance”
 “member” là tên thành viên cần truy cập

 Các hàm thành viên:


o Lấy kích thước bằng toán tử sizeof():

Ghi chú 159: Tổng kích thước của tất cả các thành viên trong struct là 41, tuy nhiên kích thước thực của
struct lại là 48. Lý do là vì một khoảng bộ nhớ trống (padding memory) đã được thêm vào giữa các thành
viên, nhằm tăng tốc độ truy cập của máy tính tới các thành viên.

o So sánh 2 kiểu cấu trúc:


 So sánh các thành viên với nhau:

 So sánh các kích thước với nhau với hàm memcmp


o Sao chép 2 kiểu cấu trúc:
 Sử dụng toán tử =

Ghi chú 160: Cả 2 thực thể này cần được tạo từ chung một kiểu cấu trúc thì mới tiến hành sao chép được.
- “src_instance” là thực thể nguồn cần copy
- “des_instance” là thực thể đích cần dán vào

 Sử dụng con trỏ cấu trúc:

 Cấu trúc và hàm:


Sử dụng phương pháp thông thường:
o Truyền cấu trúc cho hàm:

o Trả về cấu trúc từ hàm: Đây là ưu điểm của cấu trúc khi sánh với mảng, vì vốn chúng ta
không thể trả một mảng từ hàm.
 Đầu tiên, khai báo một kiểu cấu trúc:

 Tạo một hàm với kiểu dữ liệu là kiểu cấu trúc vừa khai báo:

 Truyền cấu trúc vào hàm và lấy dữ liệu kiểu cấu trúc từ hàm:

Ghi chú 161: Sử dụng hàm "printComplex" xây dựng sẵn

o Sử dụng phương pháp con trỏ:


 Đầu tiên, khai báo một kiểu cấu trúc:

 Tạo hàm tính tổng các cấu trúc thông qua con trỏ cấu trúc:
 Truyền cấu trúc vào hàm và lấy dữ liệu kiểu cấu trúc từ hàm:

Ghi chú 162: Sử dụng hàm "printComplex" xây dựng sẵn

 Mảng cấu trúc:


o Khai báo:

 “struct_name” là tên kiểu cấu trúc (cần được khai báo trước đó)
 “array_name” là tên mảng
 “length” là số phần tử của mảng
o Khởi tạo giá trị:

 “value” là giá trị của các thực thể tạo ra

o Truy cập vào phần tử của mảng: có thể truy cập vào các phần tử là các thực thể trong
mảng cấu trúc thông qua index, cũng như là truy cập vào các thành viên của từng thực
thể thông qua dấu chấm theo cách thông thường:

FILE

 Tạo con trỏ file


o Mỗi file được xử lý dưới dạng một thực thể của kiểu cấu trúc FILE - một kiểu cấu trúc
được quy định sẵn.
o Để thao tác với file, trước tiên chúng ta cần phải tạo một con trỏ chỉ đến thực thể của
kiểu cấu trúc FILE chứa thông tin của file đó trên bộ nhớ với cú pháp:

Ghi chú 163: Nên gán cho con trỏ giá trị NULL khi khởi tạo.

 “fp” là tên con trỏ file.


 Mở file
o Mở file với hàm fopen:

 “fp” là con trỏ file dùng để gán kiểu cấu trúc FILE được trả về từ hàm nếu mở
file thành công
 “filepath” là đường dẫn tới file cần mở
 “mode” là chế độ mở file, bao gồm:
 Trước khi mở file, cần tạo một con trỏ kiểu cấu trúc FILE. Việc mở file và gán địa
chỉ file vào con trỏ thường được tiến hành đồng thời:

o Mở file với hàm fopen_s:

 “errno_t” là kiểu lỗi


 “err” là tên biến để gán giá trị lỗi nếu mở file thất bại
 “fp” là con trỏ file dùng để gán kiểu cấu trúc FILE được trả về từ hàm nếu mở
file thành công, đi kèm toán tử &
 “filepath” là đường dẫn tới file cần mở
 “mode” là chế độ mở file
 Hàm fopen_s sẽ trả về một số tự nhiên biểu thị số hiệu của lỗi khi mở file. Nếu
số này bằng 0 thì việc mở file thành công, và nếu số này khác 0 thì chúng ta có
thể tìm ra nội dung lỗi bằng cách đối chiếu số hiệu của lỗi với bảng lỗi.
Trong trường hợp không cần xác định tới kiểu lỗi nếu có khi mở file:

 Hàm fopen_s an toàn và bảo mật hơn của hàm fopen vì cấm mở một file cùng
lúc khi đang mở file đó ở chế độ ghi.
 Xử lý lỗi khi mở file:
Nếu hàm fopen() không thể mở file chính xác, nó sẽ trả về giá trị NULL. Có thể
sử dụng điều này để phán đoán khi nào mở file thất bại và xử lý lỗi khi cần:

Nếu mở file bên trong hàm main(), có thể bắt chương trình dừng lại bằng cách
dùng lệnh return:

o Mở file bằng <fstream>:

 “filepath” là đường dẫn của file cần mở


 “ofs” là stream được tạo ra.
 Xử lý lỗi khi mở file thất bại bằng fstream: Nếu mở file thành công thì “ofs” sẽ
được trả về. Tuy nhiên nếu mở file thất bại thì ofs sẽ không tồn tại:

 Hoặc có thể khai báo một stream trước rồi mở file bằng stream đó sau:

Cũng giống như hàm fopen(), fstream cũng chuẩn bị sẵn các mode để mở file bằng hàm thành viên
open() như sau:

Ghi chú 164: Các mode này không tồn tại trong namespace std::ios nên chúng ta cần viết tên
namespace này trước tên các mode.

 Ví dụ ghi chèn thêm vào file:

 Tạo file mới


o Bằng cách sử dụng một trong 2 hàm mở file trên, chúng ta có thể tạo một file mới bằng
hàm fopen() với mode w hoặc a+:

 Đọc file sử dụng các hàm kế thừa từ ngôn ngữ C:


o Đọc từng ký tự trong file bằng hàm fgetc:

 Hàm sẽ trả về mã ASCII của 1 ký tự được đọc ra từ file.


 Trong trường hợp vị trí đọc ký tự đã là cuối file, hoặc là việc đọc file thất bại thì
giá trị EOF (-1) sẽ được trả về.
 Để chuyển kết quả sang kiểu ký tự, sử dụng thêm hàm char()

Ghi chú 165: Mã ASCII của "D" là 68

 Để có thể đọc tất cả các ký tự từ trong file, tạo ra một vòng lặp để đọc từng ký
tự từ đầu file cho tới khi kết quả trả về là giá trị EOF:

 Kí tự xuống dòng \n vẫn được tính là một ký tự, nếu file có nhiều dòng thì vẫn
đọc như thường.
o Đọc từng dòng trong file bằng hàm fgets:

 “fp” là con trỏ của file cần đọc


 “buf” là con trỏ tới nơi lưu trữ chuỗi đã đọc từ dòng trong file. Thông thường
chỉ định buf bằng một mảng
 “size” là kích thước (số ký tự) lớn nhất có thể đọc từ dòng trong file.
 Hàm sẽ trả về con trỏ lưu địa chỉ trên bộ nhớ của chuỗi được đọc từ dòng trong
file. Trong trường hợp vị trí đọc ký tự đã là cuối file, hoặc là việc đọc file thất bại
thì con trỏ NULL sẽ được trả về. Một dòng được tính từ đầu dòng cho tới khi
gặp ký tự xuống dòng \n.
 Cũng như fgetc, fgets chỉ đọc được một dòng trong 1 lần gọi. Để đọc cả file, tạo
ra một vòng lặp để đọc từng ký tự từ đầu file cho tới khi kết quả trả về là giá trị
NULL:

Ghi chú 166: Mảng con trỏ cần mang số phần tử ≥ số kí tự trong một dòng, nếu không sẽ lỗi.

 Khi giá trị của size nhỏ hơn số ký tự thực có trong dòng, chỉ có size -1 ký tự thực
được đọc (chỗ trống của còn lại sẽ được tự động lấp chỗ bằng một ký tự kết
thúc chuỗi \0). Lần đọc fgets tiếp theo sẽ vẫn tiếp tục ở dòng đang dang dở:

o Đọc từng dòng file theo định dạng chỉ định bằng hàm fscanf:

 Đọc file sử dụng <fstream>


o Đọc file bằng toán tử >>

 Đầu tiên, tạo một stream chứa thông tin file:

hoặc
 Khai báo một biến kiểu char hoặc string để lưu dữ liệu đọc từ file ra:

 Sử dụng toán tử >> trong để gán từng nội dung đọc từ file vào biến: từng kí tự
nếu là biến char, từng từ (kết thúc ở khoảng cách) nếu là biến string:

 Sử dụng toán tử << để xuất ký tự/ từ trong file:

Ghi chú 167: Ở đây, vì đã gán file vào biến “var1” (chiếm ký tự đầu là "D") nên khi gán tiếp file vào
biến “var2”, biến này bắt đầu chiếm kể từ vị trí sau đó là chữ "o" đến khi gặp khoảng cách (kết thúc từ)

Ghi chú 168: Tương tự, nếu gán file vào biến "var2" trước, biến này sẽ chiếm hết từ "Do", và biến
"var1" được gán sau sẽ bắt đầu từ ký tự tiếp theo là "K"
 Vì toán tử >> chỉ đọc từng từ hoặc từng ký tự, có thể kết hợp với một vòng lặp
để có thể đọc toàn bộ dữ liệu trong file:

Ghi chú 169: Biến char và string không bao gồm các ký tự khoảng cách, xuống dòng

o Đọc file bằng hàm getline():

 “input” là stream chứa thông tin file


 “str” là chuỗi để lưu kết quả đọc file
 “delimiter” là ký tự phân cách, và chúng ta có thể lược bỏ đối số này khi muốn
sử dụng ký tự phân cách mặc định là ký tự xuống dòng \n.
 Hàm getline sẽ trả về chuỗi đọc được, và trong trường hợp đọc file thất bại, giá
trị 0 sẽ được trả về.
 Hàm getline chỉ có thể đọc từng dòng file, do đó có thể kết hợp với một vòng lặp
để có thể đọc toàn bộ các dòng trong file.

 Đọc từng dòng trong file bằng hàm getline(): Kí tự phân cách là \n
 Đọc từng từ trong file bằng hàm getline(): Kí tự giới hạn là ‘ ‘:

 Ghi file sử dụng <fstream>


o Ghi file bằng toán tử <<

 Đầu tiên, tạo một stream chứa thông tin file:

hoặc nếu muốn ghi


lại toàn bộ n ký tự đầu file và giữ nguyên các ký tự sau

nếu muốn ghi tiếp vào trong file, bắt đầu


từ vị trí cuối cùng
Sử dụng toán tử << trong để ghi từng nội dung từ biến vào file: từng kí tự nếu là biến char, từng từ
nếu là biến string:

Ghi chú 170: Ở đây sử dụng mode ios::app để ghi tiếp


o Ghi file bằng hàm write
 Hàm nhận địa chỉ của một chuỗi tại bộ nhớ, và ghi n ký tự được chỉ định từ
chuỗi này vào trong file.

“s” là con trỏ tới một chuỗi có ít nhất n ký tự.


 Hàm write sẽ trả về luồng, và trong trường hợp ghi file thất bại thì hàm sẽ trả về
các flags nhằm thông báo lỗi:

Ghi chú 171: text1.data() tạo một con trỏ trỏ đến vị trí của “text1”, text1.size() cho ra kích thước
của “text1”

 Ghi mảng vào file:

 Ghi cấu trúc vào file:


 Đóng file
o Khi đang ở giữa chương trình mở 1 file để đọc hoặc ghi file thì file đó sẽ rơi vào trạng
thái bị khóa nhằm ngăn chặn chương trình hoặc phần mềm khác trên hệ điều hành truy
cập cùng lúc vào file, gây nên các chồng chéo trong xử lý. Để có các chương trình và
phần mềm khác có thể truy cập vào file thì sau khi kết thúc một phiên làm việc với file
trong C++, cần đóng nó lại.
o Sử dụng hàm fclose() để đóng: sẽ trả về 0 trong trường hợp kết thúc file bình thường.
Và một giá trị kiểu EOF được trả lại trong trường hợp kết thúc file bất thường.
 “fp” là con trỏ của file cần đóng.

o Sử dụng <fstream>

 “ofs” là stream của file

CLASS

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


 Hai phương pháp lập trình
o Lập trình thủ tục (Procedure Oriented Programming – POP) là phương pháp lập trình
xoay quanh chủ yếu việc sử dụng các hàm tạo thành một chuỗi các thủ tục để xử lý dữ
liệu. Trong lập trình thủ tục, chương trình sẽ được chia ra thành các Module nhỏ, chúng
liên kết và vận hành nối tiếp nhau. Mỗi hàm thực hiện một công việc cụ thể, và dữ liệu
được truyền qua các hàm dưới dạng thông số.

Ghi chú 172: Để thực hiện cộng 2 số nguyên theo cách lập trình thủ tục, ta tạo hàm add() lấy tham số là số nguyên
và trả về tổng của chúng. Ở hàm main(), ta gọi hàm add() đã tạo, truyền tham số cho nó và thu lại kết quả

o Lập trình hướng đối tượng (OOP – Object Oriented Programming) là một phương
pháp lập trình bằng cách đóng gói dữ liệu vào trong đối tượng và dùng chính đối tượng
đó để thao tác dữ liệu.
Ghi chú 173: Để thực hiện cộng 2 số nguyên theo cách lập trình hướng đối tượng, ta tạo ra một lớp 'Calculator' bao
gồm một phương thức add() giúp cộng hai só nguyên. Ở hàm main() tạo một thực thể của lớp 'Calculator' mang tên
'calc' và gọi phương thức add() của nó.

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


o Đối tượng (object hoặc instance) là một thực thể được tạo ra từ một lớp. Một đối
tượng được đóng gói bởi hai thành phần: dữ liệu (data, properties hoặc attributes) và
phương thức (methods hoặc functions).
o Lớp (class) có thể hiểu như một cái khuôn để tạo ra các đối tượng. Lớp là một bản thiết
kế, nơi định ra một tập hợp các thuộc tính và phương thức mà đối tượng sẽ mang theo.
Thuộc tính là các dữ liệu thành phần, còn phương thức là các hàm sẽ làm việc trên các
dữ liệu thành phần ấy.
o Xét một ví dụ:
Tạo một lớp mang tên ‘Person’. Lớp này có:
 Hai thuộc tính: ‘name’ là một chuỗi ký tự biểu diễn tên và ‘age’ là một số
nguyên biểu diễn tuổi của một người.
 Một phương thức: ‘introduce()’ là một hàm in ra một dòng giới thiệu về người
đó, bao gồm thông tin về tên và tuổi.

Sau khi đã định nghĩa hoàn chỉnh lớp ‘Person’, ở hàm main() chúng ta có thể tạo
các đối tượng dựa trên lớp mới tạo. Ở đây, ta tạo ra hai đối tượng ‘person1’ và
‘person2’, gán các thuộc tính ‘name’ và ‘age’ cho chúng rồi sử dụng phương
thức ‘introduce()’ để in ra dòng giới thiệu bản thân.
 Bốn đặc tính của lập trình hướng đối tượng:
o Tính kế thừa (Inheritance): cho phép một lớp mới được xây dựng từ một lớp cũ mang
trong nó mọi thuộc tính và phương thức của lớp cũ. Ngoài ra, lớp mới có thể có thêm
cho nó những đặc tính khác hoặc viết chồng lên những đặc tính cũ mà nó được kế thừa.
Lớp cũ gọi là lớp cha (superclass, parent class hoặc base class)
Lớp mới gọi là lớp con (subclass)
Ví dụ:
 Tồn tại một lớp ‘Vehicle’ mang trong nó các phương thức start(), stop(),
turnleft() và turnright(). Có thể tạo một lớp mới tên ‘Car’ từ lớp ‘Vehicle’ này và
bổ sung thêm một phương thức lighton(), tức lớp ‘Car’ mang trong nó tổng
cộng 5 phương thức, 4 được kế thừa từ lớp cha và 1 của riêng nó.
o Tính đa hình (Polymorphism): khả năng mà một phương thức trong class có thể đưa ra
các kết quả hoàn toàn khác nhau, tùy thuộc vào dữ liệu được xử lý.
Ví dụ:
 Cùng một lớp quản lý dữ liệu là các con vật, thì hành động kêu của chúng được
định nghĩa cho ra kết quả khác nhau (con mèo thì kêu meo meo, con chó thì sủa
gâu gâu) tùy vào loại động vật được đưa vào.
 Cùng một lớp ‘Area’ tính diện tích hình phẳng thực hiện các công thức tính khác
nhau tùy thuộc vào các hình đầu vào khác nhau.
o Tính trừu tượng (Abstraction): Khả năng mà chương trình có thể bỏ qua sự phức tạp
bằng cách tập trung vào cốt lõi của thông tin cần xử lý, tức có thể xử lý một đối tượng
bằng cách gọi tên một phương thức và thu về kết quả xử lý, mà không cần biết làm cách
nào đối tượng đó được các thao tác trong lớp.
Ví dụ:
 Có thể nấu cơm bằng nồi cơm điện bằng cách rất đơn giản là ấn công tắc nấu,
mà không cần biết là bên trong cái nồi cơm điện đó đã làm thế nào mà gạo có
thể nấu thành cơm
 Một cái điều khiển TV cung cấp cho người dùng một bộ phương thức đơn giản
để điều khiển TV mà không cần cho người dùng biết về cấu tạo phức tạp của
các mạch điện bên trong.
o Tính đóng gói (Encapsulation): Dữ liệu và thông tin được đóng gói lại, giúp các tác động
bên ngoài lên một đối tượng không thể làm thay đổi trạng thái nội tại của đối tượng đó,
mà chỉ có phương thức nội tại của đối tượng có thể thay đổi chính nó. Điều này đảm
bảo tính toàn vẹn của đối tượng, cũng như giúp giấu đi các dữ liệu thông tin cần được
che giấu.
Ví dụ:
 Một tài khoản ngân hàng ẩn đi số dư tài khoản và cung cấp các phương thức
để nạp tiền, rút tiền cũng như kiểm tra số dư còn lại.
 Trong một điện thoại iPhone, người dùng không thể các cấu trúc vận hành của
hệ điều hành IOS mà chỉ có Apple mới có thể.
2. Lớp (Class)
 Cấu trúc của một lớp:
o Một lớp bao gồm hai thành phần:
 Biến thành viên (member variable hoặc field): Các biến sử dụng trong nội bộ
lớp có tác dụng lưu trữ thông tin chính là thuộc tính của đối tượng tạo ra từ
lớp.
 Hàm thành viên (member function): các hàm có tác dụng định nghĩa các xử lý
có thể được sử dụng cho các đối tượng được tạo ra từ lớp.
o Bản thân std::vector cũng là một lớp trong C++, cung cấp bởi thư viện chuẩn, dùng để
lưu trữ và xử lý mảng động. Để sử dụng, cần phải include header file à trong đó lớp
std::vector được định nghĩa: <vector>
 Các thuộc tính của lớp std::vector bao gồm: ‘size()’, ‘capacity()’, ‘empty()’…
 Các phương thức bao gồm: ‘push_back()’, ‘pop_back(), ‘clear()’, ‘reverse()’,
‘begin()’ ,’end()’,…
 Khai báo và định nghĩa lớp:
Để khai báo lớp, chúng ta sẽ cần làm 2 việc, đó là định nghĩa lớp và tạo hàm thành viên
trong lớp.
o Định nghĩa lớp trên hai file cpp khác nhau:
 Khai báo và định nghĩa hàm thành viên của lớp ngay trên file header:

Ghi chú 174: Sử dụng chỉ thị tiền xử lý #ifndef và #define ở đầu header file, và #endif ở cuối, để tránh việc
header file bị include nhiều lần.

Ở đây, ta định nghĩa một hàm ‘myFunction()’ ngay bên trong khai báo lớp (định
nghĩa nội tuyến - inline function definition)
 Khai báo hàm thành viên của lớp trên file header, và định nghĩa chúng ở source
file, có sử dụng toán tử phân giải phạm vị (::)

o Định nghĩa lớp gộp trên cùng một file cpp:

 Ở đây, ta tạo một lớp mang tên ‘MyClass’ bao gồm một hàm thành viên là
‘myFunction()’ và một biến thành viên là ‘myVariable’.
 Đối với các hàm thành viên nhỏ và đơn giản, nên định nghĩa nội tuyến nó để
tăng hiệu suất chạy chương trình. Đối với các trường hợp phức tạp trong các dự
án lớn, nên chỉ khai báo hàm thành viên khi định nghĩa lớp và định nghĩa hàm ở
bên ngoài để dễ quản lý.
 Sử dụng lớp đã tạo
Cho trước một lớp ‘MyClass’ như sau:

o Sử dụng lớp thông qua tạo một thực thể:


 Tạo một thực thể trên stack. Truy cập đến các thuộc tính và phương thức bằng
toán tử chấm (.)
 Tạo một thực thể bằng toán tử new: tạo ra một con trỏ chỉ đến địa chỉ mà thực
thể được lưu trong bộ nhớ. Truy cập đến các thuộc tính và phương thức bằng
toán tử mũi tên (->).

Ghi chú 175: Sau khi kết thúc phải sử dụng lệnh delete để giải phóng bộ nhớ sử dụng để lưu
trữ thực thể này.
o Sử dụng lớp không thông qua thực thể:
 Khi tạo một thực thể, nó sẽ được thừa hưởng tất cả các thuộc tính lẫn phương
thức của lớp nguồn tạo ra nó. Nhưng có khi chúng ta không cần sử dụng tất cả
các chức năng có trong một lớp, mà chỉ dùng một phần nhỏ của lớp đó. Khi này,
ta có thế truy cập trực tiếp vào trong lớp và sử dụng chức năng, các hàm thành
viên mong muốn.
 Chỉ có các biến hay hàm thành viên thuộc kiểu public mới có thể được truy cập
từ bên ngoài class bằng phương pháp này.
 Để tránh xung đột tên của hàm hay tên biến ở trong và ngoài một lớp, chúng ta
cần thêm từ khóa static vào đầu tên hàm thành viên hoặc biến thành viên, với ý
nghĩa rằng các hàm và biến này thuộc về lớp hơn là về thực thể được tạo ra từ
lớp. Với đặc điểm này, ta có khả năng truy cập đến các biến hoặc hàm này từ
ngoài lớp mà không cần thông qua thực thể tạo từ lớp, sử dụng toán tử phân
giải phạm vi (::).
 Đối với phương pháp này, do không tạo ra thực thể nên chúng ta cần phải khởi
tạo giá trị của biến thành viên để có thể sử dụng như sau:

hoặc

 Không thể khởi tạo giá trị cho một biến tĩnh trong khi định nghĩa một lớp. Nếu
muốn khởi tạo giá trị của biến thành viên static ngay trong khi khai báo lớp,
phải dùng từ khóa const, vì giá trị của một hằng biến thành viên là biết trước
trong quá trình biên dịch và sẽ không thể thay đổi về sau.
Ghi chú 176: Chương trình báo lỗi khi khai báo biến static không hằng bên trong lớp
3. Trình truy cập
 Trình truy cập (Access modifier) được sử dụng để quản lý việc truy cập đến các thành phần của
lớp, giúp bảo đảm tính đóng gói và tăng tính an toàn trong lập trình hướng đối tượng.
 Có 3 trình truy cập:
o Công khai (Public): Có tác dụng chỉ định thành viên của lớp có thể được truy cập từ bất
cứ đâu trong chương trình.

 Trong lớp ‘TestClass’ có một biến thành viên ‘num’ và một hàm thành viên
‘print()’
 Vì cả hai đều nằm trong khối public, nó có thể được truy cập từ bất kỳ đâu trong
chương trình (‘num’ có thể được truy cập từ một hàm khác xây dựng bên ngoài,
‘fun()’ được truy cập từ hàm main())
o Riêng tư (Private): có tác dụng chỉ định thành viên của lớp chỉ có thể được truy cập từ
bên chính trong lớp.

Nếu lược bỏ hoặc không chỉ rõ trình truy cập khi khai báo thành viên của lớp, thì mặc
định trình truy cập riêng tư sẽ được chỉ định.

 Trong lớp ‘TestClass’ có một biến thành viên ‘num’ và một hàm thành viên
‘print()’
 Vì ‘num’ nằm trong khối private, việc truy cập đến nó từ hàm ‘func’ bên ngoài
lớp dẫn đến lỗi truy cập.
 Tuy nhiên, ‘print()’ nằm trong khối public, nó có thể được truy cập từ hàm
main(), và được gọi để truy cập đến biến ‘num’, vốn là thành viên trong cùng
một lớp với nó.
o Bảo vệ (Protected): có tác dụng chỉ định thành viên của lớp chỉ có thể được truy cập từ
bên trong lớp và từ các lớp con (lớp kế thừa).
 Trong lớp ‘TestClass’ có một biến thành viên ‘num’ và một hàm thành viên
‘print()’. Hàm ‘print()’ có thể được dùng để truy cập đến biến ‘num’, vốn là
thành viên trong cùng một lớp với nó.
 Tạo thêm một lớp ‘DerivedClass’ kế thừa từ lớp ‘TestClass’, có một hàm thành
viên ‘printNew()’.
 Với đối tượng ‘obj’ được tạo từ lớp nguồn, nó có thể được dùng để gọi hàm
‘print()’, từ đó truy cập đến biến ‘num’
 Với đối tượng ‘obj1’ được tạo từ lớp kế thừa, nó cũng có thể được dùng để gọi
hàm ‘printNew()’, từ đó truy cập đến biến ‘num’
 Việc truy cập đến nó từ hàm ‘func’ bên ngoài lớp dẫn đến lỗi truy cập.

 Tuy nhiên, nếu sửa biến ‘num’ nằm trong khối private, hàm ‘printNew()’ từ lớp
kế thừa cũng không thể truy cập đến nó nữa.
 Biến thành viên và hàm thành viên
o Biến thành viên:
 Khi khai báo một biến thành viên với từ khóa const, ta sẽ làm mất đi khả năng
thay đổi giá trị của nó, từ bên trong lẫn bên ngoài lớp, dù cho biến này có nằm
trong khối public

Ghi chú 177: Từ hàm main(), có thể đọc giá trị của biến 'x' nhưng không thể thay đổi nó
 Để truy cập được đến một biến thành viên, ta cần khởi tạo giá trị cho nó. Trước
khi khởi tạo, biến sẽ mang trong mình giá trị rác. Riêng với biến kiểu string ngay
khi được khai báo đã mang trong nó giá trị chuỗi rỗng “” thay vì rác:

 Biến thành viên có thể


o Hàm thành viên:
 Một hàm thành viên của một lớp có thể được dùng để truy cập đến các biến
thành viên của cùng lớp đó:

 Nhưng khi một hàm thành viên được khai báo với từ khóa const, nó sẽ mất đi
khả năng thay đổi giá trị của biến thành viên mà chỉ có thể đọc được giá trị của
biến thành viên đó.

4. Hàm khởi tạo – Hàm hủy


 Hàm khởi tạo (Constructor):
o Hàm khởi tạo là một loại hàm thành viên đặc biệt được thực thi tự động khi xuất hiện
một biến thành viên trong lớp, có chức năng khởi tạo giá trị cho các biến thành viên và
cấp phát một số tài nguyên (nếu cần thiết). Có 2 tính chất:
 Tên hàm khởi tạo trùng với tên lớp
 Hàm không mang kiểu dữ liệu trong nó, cũng như không trả về bất kỳ dữ liệu gì
khi kết thúc hàm. Khi khai báo hàm khởi tạo, không sử dụng từ khóa void.
o Khai báo hàm khởi tạo: Vì hàm khởi tạo, về bản chất, cũng là một hàm thành viên trong
lớp, nên cũng có 2 cách khai báo:
 Khai báo riêng trên hai file:

 Khai báo gộp trên cùng một file:

o Hàm khởi tạo mặc định (Default constructor) không chứa tham số:

 Do hàm khởi tạo mặc định không chứa tham số, khi tạo một thực thể từ một
lớp, chúng ta không truyền đối số vào hàm mà chỉ viết trực tiếp tên của thực thể
đó:

 Khi khai báo và khởi tạo giá trị cho các biến không tĩnh (non-static), chương
trình tự động thực thi ngầm một hàm khởi tạo mặc định với chức năng gán các
giá trị được khởi tạo vào các biến thành viên ấy:
là bản chất của
o Hàm khởi tạo không mặc định thì có chứa tham số. Do đó, khi tạo một thực thể từ một
lớp, chúng ta cần truyền đối số tương ứng vào hàm:

o Đối với hàm khởi tạo không mặc định, có thể sử dụng danh sách khởi tạo (constructor
initialization list) để khởi tạo giá trị cho các biến thành viên, thay vì khởi tạo giá trị cho
chúng bên trong thân hàm. Hai cách viết dưới đây là như nhau:

 Hàm hủy (Destructor)


o Hàm hủy là một hàm thành viên đặc biệt được thực thi tự động khi chương trình hủy
một thực thể được tạo ra từ lớp, được sử dụng với mục đích xóa và giải phóng thực thể
đó khỏi bộ nhớ. Có 2 tính chất:
 Tên hàm hủy trùng với tên lớp, nhưng kèm ký tự ‘~’ ở đầu.
 Hàm hủy không mang kiểu dữ liệu trong nó, cũng như không trả về bất kỳ dữ
liệu gì khi kết thúc hàm. Khi khai báo không sử dụng từ khóa void.
o Các kiểu dữ liệu thuộc loại POD như int, double hay vector có cơ chế tự động giải phóng
bộ nhớ nên chúng ta không cần dùng hàm hủy đối với các loại dữ liệu này. Các loại dữ
liệu như mảng vốn không có cơ chế trên, nên sau khi hủy đối tượng thì bắt buộc chúng
ta cần phải dùng hàm hủy để giải phóng bộ nhớ đã dùng để lưu giữ chúng.
o Cách gọi hàm hủy:
 Nếu tạo thực thể từ lớp trên stack (không dùng toán tử new), thì hàm hủy sẽ tự
động được gọi sau khi kết thúc phiên làm việc với thực thể đó. Khi hàm hủy
được gọi, chương trình sẽ thực thi mọi câu lệnh bên trong hàm.

Sau khi kết phiên làm việc với đối tượng ‘obj’, hàm hủy ‘~MyClass’ tự động được
gọi ra, chương trình thực hiện các câu lệnh bên trong bao gồm: giải phòng vùng
nhớ được cấp phát cho biến ‘data’ và in ra dòng thông báo.

Ghi chú 178: Trường hợp này chương trình vẫn xuất dòng thông báo nhưng vùng nhớ cho biến 'data' vẫn
chưa được giải phóng, vì không có dòng lệnh tương ứng cho việc đó trong hàm hủy khi nó được gọi ra.
 Nếu tạo thực thể từ lớp có dùng toán tử new, thì hàm hủy sẽ không tự động
được gọi sau khi kết thúc phiên làm việc với thực thể đó, mà chỉ được thực thi
khi toán tử delete xuất hiện. Khi hàm hủy được gọi, chương trình sẽ thực thi
mọi câu lệnh bên trong hàm.

Ghi chú 179: Khi không có toán tử delete, hàm hủy '~MyClass' không được thực thi, lệnh thông báo không được xuất ra.

Ghi chú 180: Khi có toán tử delete, hàm hủy '~MyClass' được thực thi, lệnh thông báo được xuất ra màn hình.
Cũng như trước, trong trường hợp này chương trình vẫn xuất dòng thông báo
nhưng vùng nhớ cho biến 'data' vẫn chưa được giải phóng, vì không có dòng
lệnh tương ứng cho việc đó trong hàm hủy khi nó được gọi ra. Toán tử delete
chỉ là chìa khóa kích hoạt hàm hủy chứ không thực hiện việc giải phóng bộ nhớ.
 Thứ tự khởi tạo và hủy:
o Khi có nhiều đối tượng được tạo từ nhiều lớp khác nhau, thứ tự khởi tạo và hủy của
chúng được sắp xếp theo một trật tự cụ thể:
 Các hàm khởi tạo được gọi theo thứ tự mà các thực thể được khai báo
 Các hàm hủy được gọi theo thứ tự ngược lại.
Điều này có nghĩa là thực thể nào được khai báo đầu tiên sẽ được hủy sau cùng, thực
thể nào được khai báo sau cùng lại được hủy đầu tiên.

o Ở đây, đối tượng ‘A’ được khai báo đầu tiên, hàm khởi tạo ‘A()’ được thực thi ngay sau
đó. Tiếp đến đối tượng ‘B’, hàm khởi tạo ‘B()’ rồi đến đối tượng ‘C’ và hàm khởi tạo ‘C()’.
Nhưng khi kết thúc chương trình, các hàm hủy được gọi ra theo thứ tự ngược lại: ‘~C()’
đến ‘~B()’ rồi đến ‘~A()’.

5. Kế thừa
 Kế thừa là một đặc tính quan trọng trong lập trình hướng đối tượng, cho phép tạo ra một lớp
mới từ lớp đã có. Lớp mới này thừa hưởng tất cả các đặc tính và phương thức có trong lớp
trước đó, và có khả năng mang theo các chức năng mới mà lớp cũ không có.
Lớp cũ gọi là lớp cha (superclass, parent class hoặc base class)
Lớp mới gọi là lớp con (subclass)
 Luôn cần phải khai báo lớp cha trước rồi mới khai báo lớp con sau, nếu làm ngược lại thì lỗi sẽ bị
xảy ra:
Ghi chú 181: ‘BaseClass’ là lớp nguồn, ‘DerivedClass’ là lớp con. ‘dc’ là đối tượng được tạo ra từ lớp con, mang trong nó phương
thức ‘print()’ của lớp nguồn, cũng như ‘printNew()’ của riêng nó.

 Tạo thực thể với toán tử new:


o Để sử dụng lớp kế thừa thông qua thực thể, có thể tạo thực thể đó bằng hai cách (dùng
toán tử new):
 Tạo một con trỏ thuộc lớp nguồn, rồi trỏ nó tới lớp kế thừa
 Tạo một con trỏ thuộc lớp kế thừa, rồi trỏ nó tới lớp kế thừa
o Xét trường hợp:

 Ở đây, con trỏ ‘shape’ thuộc lớp nguồn ‘Shape’ và trỏ tới lớp thừa kế
‘Rectangle’. Bởi vì bản chất nó là con trỏ của lớp nguồn, nó chỉ có thể được dùng
để gọi hàm ‘draw()’ trong lớp nguồn, không thể gọi hàm ‘resize()’ không có
trong lớp nguồn.
 Con trỏ ‘rect’ thuộc lớp thừa kế ‘Rectangle’ và trỏ tới lớp thừa kế ‘Rectangle’.
Bởi vì bản chất nó là con trỏ của lớp thừa kế, nó có thể được dùng để gọi hàm
‘draw()’ trong lớp nguồn lẫn hàm ‘resize()’ của chính nó.
 Phạm vi kế thừa: Lớp con không có khả năng kế thừa các thuộc tính và phương thức nằm trong
diện truy cập riêng tư (private) từ lớp nguồn.

 Mức độ kế thừa:
o Khi một lớp kế thừa công khai (publicly inherited) từ lớp nguồn:
 Những thuộc tính và phương thức diện công khai ở lớp nguồn được kế thừa và
giữ nguyên ở diện công khai trong lớp thừa kế.
 Những thuộc tính và phương thức diện bảo vệ ở lớp nguồn được kế thừa và giữ
nguyên ở diện bảo vệ trong lớp thừa kế.
 Những thuộc tính và phương thức diện riêng tư ở lớp nguồn không thể được kế
thừa.

o Khi một lớp kế thừa bảo vệ (protectedly inherited) từ lớp nguồn:


 Tất cả thuộc tính và phương thức diện công khai lẫn bảo vệ ở lớp nguồn được
kế thừa và chuyển sang diện bảo vệ trong lớp thừa kế.
 Những thuộc tính và phương thức diện riêng tư ở lớp nguồn không thể được kế
thừa.
o Khi một lớp kế thừa riêng tư (privately inherited) từ lớp nguồn:
 Tất cả thuộc tính và phương thức diện công khai lẫn bảo vệ ở lớp nguồn được
kế thừa và chuyển sang diện riêng tư trong lớp thừa kế.
 Những thuộc tính và phương thức diện riêng tư ở lớp nguồn không thể được kế
thừa.

 Thứ tự thực thi hàm khởi tạo và hàm hủy:


o Khi một đối tượng của lớp kế thừa được gọi hay hủy, nếu hàm khởi tạo và hàm hủy ở
lớp nguồn là mặc định (không chứa tham số), nó sẽ được tự động gọi trước và sau khi
gọi hàm khởi tạo và hàm hủy của lớp kế thừa:
 Khi một đối tượng của lớp kế thừa được gọi: Gọi hàm khởi tạo của lớp nguồn =>
Gọi hàm khởi tạo của lớp kế thừa => Tạo đối tượng
 Khi kết thúc phiên làm việc với đối tượng ấy: Gọi hàm hủy của lớp kế thừa =>
Gọi hàm hủy của lớp nguồn => Hủy đối tượng
o Khi một đối tượng của lớp kế thừa được gọi hay hủy, nếu hàm khởi tạo hoặc hàm hủy ở
lớp nguồn không phải là mặc định (có chứa tham số), cần tường minh gọi chúng ra ở
hàm khởi tạo hoặc hàm hủy ở lớp thừa kế:

 Ở đây, khi đối tượng ‘obj’ của lớp thừa kế được tạo, hàm khởi tạo
‘DerivedClass()’ của lớp thừa kế được gọi, yêu cầu hàm khởi tạo ‘BaseClass()’
của lớp nguồn được gọi trước. Tuy nhiên, ‘BaseClass()’ yêu cầu tham số khi gọi
và ‘DerivedClass()’ không cung cấp đối số nào cho nó, lỗi biên dịch xảy ra.
 Có thể sửa bằng cách cho ‘DerivedClass()’ cung cấp đối số để gọi ‘BaseClass()’:

 Còn đối với hàm hủy, do ‘~BaseClass()’ là mặc định, không yêu cầu tham số nên
được gọi ra bình thường.
 Kế thừa hàm khởi tạo:
o Với hàm khởi tạo của lớp nguồn không phải mặc định (có chứa tham số), nó sẽ không
thể được tự động thực thi. Khi muốn khai báo một thực thể của lớp kế thừa từ lớp
nguồn ấy, cần phải tường minh truyền tham số cho hàm khởi tạo của lớp nguồn thông
qua hàm khởi tạo của lớp kế thừa.
o Việc truyền đối số để gọi hàm khởi tạo của lớp nguồn ở hàm khởi tạo của lớp kế thừa có
thể thực hiện thông qua kế thừa hàm khởi tạo:

 Khi đối tượng ‘obj’ của lớp thừa kế được tạo, hàm khởi tạo ‘DerivedClass()’ của
lớp thừa kế được gọi, yêu cầu hàm khởi tạo ‘BaseClass()’ của lớp nguồn được
gọi trước.
 ‘BaseClass()’ yêu cầu hai tham số khi gọi và ở đây ‘DerivedClass()’ nhận hai tham
số và truyền chúng như là đối số để hàm ‘BaseClass()’ có thể được thực thi
 Đa kế thừa:
Đa kế thừa cho phép một lớp có thể kế thừa từ nhiều hơn một lớp nguồn, và mang
trong mình tất cả các thuộc tính và phương thức (không nằm trong diện riêng tư) của tất
cả các lớp nguồn đó.

Ghi chú 182: Đối tượng 'obj' được tạo ra mang trong nó ba phương thức: hai từ hai lớp nguồn và một từ lớp tạo ra chính nó.

 Con trỏ this


o Con trỏ this là một con trỏ đặc biệt ẩn dấu bên trong hàm thành viên của lớp, có tác
dụng trỏ đến đối tượng đã gọi đến hàm thành viên đó:

o Con trỏ this đặc biệt hữu dụng khi làm việc với các hàm thành viên có tham số trùng tên
với biến thành viên mà nó làm việc tới:

 Ở đây, hàm thành viên ‘setNum()’ nhận một tham số ‘num’, và lấy giá trị của
tham số này truyền cho biến thành viên cũng tên ‘num’. Sử dụng con trỏ this để
phân biệt giữa tham số hàm và biến thành viên.

 Nếu không có con trỏ this, chương trình sẽ hiểu là gán giá trị của tham số ‘num’
lại bằng chính nó.

6. Bạn (Friend)
 Lớp bạn (Friend class)
o Lớp bạn của một lớp nguồn là lớp mà ở đó ta có thể truy cập đến các thuộc tính và
phương thức thuộc diện riêng tư và bảo vệ của lớp nguồn.
 Ở đây, tạo một lớp nguồn ‘MyClass’ có chứa biến riêng tư ‘x’ và biến bảo vệ ‘y’,
hai biến này được khởi tạo với tham số ‘a’, ‘b’ truyền vào khi gọi hàm khởi tạo.
 Từ lớp nguồn, tạo một lớp bạn ‘MyFriendClass’ mang một hàm thành viên
‘printValues()’. Hàm này nhận tham số là một thực thể được tạo từ lớp nguồn,
và thông qua thực thể này tiếp cận đến các biến thành viên của lớp nguồn ở hai
dòng lệnh.
 Ở hàm main(), tạo một thực thể ‘obj’ từ lớp nguồn, truyền đối số 5 và 10 để
khởi tạo giá trị cho hai biến ‘x’ và ‘y’.
 Tạo thêm một thực thể ‘friendObj’ từ lớp bạn, thông qua đó thực thi hàm
‘printValues()’ với đối số truyền vào là ‘obj’, thực thể từ lớp nguồn.
o Bản thân lớp bạn cũng là một lớp, có riêng cho mình hàm khởi tạo và hàm hủy. Khi một
thực thể của lớp bạn được tạo, chỉ có hàm khởi tạo và hàm hủy của lớp bạn được thực
thi, còn của lớp nguồn thì không.
 Hàm bạn (Friend function)
o Tương tự lớp bạn, hàm bạn là một hàm có quyền truy cập vào một thuộc tính hoặc
phương thức diện bảo vệ hay riêng tư của một lớp có sẵn. Hàm bạn của một lớp có thể
tồn tại độc lập ngoài lớp đó, hoặc nằm trong một lớp khác.
o Hàm bạn tồn tại độc lập:
 Ở đây, tạo một lớp nguồn ‘MyClass’ có chứa biến riêng tư ‘x’ và biến bảo vệ ‘y’,
hai biến này được khởi tạo với tham số ‘a’, ‘b’ truyền vào khi gọi hàm khởi tạo.
 Từ lớp nguồn, tạo một hàm bạn ‘func()’, nhận tham số là một thực thể được tạo
từ lớp nguồn, và thông qua thực thể này tiếp cận đến các biến thành viên của
lớp nguồn và trả về tổng của chúng.
 Ở hàm main(), tạo một thực thể ‘obj’ từ lớp nguồn, truyền đối số 5 và 10 để
khởi tạo giá trị cho hai biến ‘x’ và ‘y’.
 Gọi hàm ‘func()’ với đối số truyền vào là ‘obj’, thực thể từ lớp nguồn.
o Hàm bạn tồn tại trong lớp bạn:

 Ở đây, tạo một lớp nguồn ‘MyClass’ có chứa biến riêng tư ‘x’ và biến bảo vệ ‘y’,
hai biến này được khởi tạo với tham số ‘a’, ‘b’ truyền vào khi gọi hàm khởi tạo.
 Từ lớp nguồn, tạo một hàm bạn ‘func()’, nhận tham số là một thực thể được tạo
từ lớp nguồn, và thông qua thực thể này tiếp cận đến các biến thành viên của
lớp nguồn và trả về tổng của chúng. Đồng thời, tạo một lớp bạn ‘MyFriendClass’
sẽ mang một hàm thành viên, chính là hàm bạn ‘func()’.
 Ở hàm main(), tạo một thực thể ‘obj’ từ lớp nguồn, truyền đối số 5 và 10 để
khởi tạo giá trị cho hai biến ‘x’ và ‘y’.
 Gọi hàm ‘func()’ với đối số truyền vào là ‘obj’, thực thể từ lớp nguồn.
 Tạo thêm một thực thể ‘friendObj’ từ lớp bạn, thông qua đó thực thi hàm bạn
‘func()’ với đối số truyền vào là ‘obj’, thực thể từ lớp nguồn.

7. Từ khóa virtual
 Từ khóa virtual dùng để khai báo ở lớp nguồn một hàm thành viên mà có thể được viết đè lại ở
lớp kế thừa. Điều này có nghĩa là lớp kế thừa có thể tái định nghĩa là một hàm thành viên của
riêng nó với tên và tham số giống với lớp nguồn.
 virtual được sử dụng chủ yếu trong những trường hợp liên quan về tính đa hình của lớp.
 Ở lớp nguồn, sử dụng từ khóa virtual khi khai báo hàm cần ghi đè. Ở lớp kế thừa, sử dụng từ
khóa override khi khai báo hàm ghi đè:
o Tạo một lớp nguồn ‘Shape’, mang theo một hàm có thể ghi đè ‘draw()’.
o Tạo lớp kế thừa ‘Rectangle, mang theo một hàm ghi đè tái định nghĩa là hàm ‘draw()’ ở
lớp nguồn
o Tạo lớp kế thừa ‘Circle’ và không định nghĩa gì thêm cho nó (‘Circle’ thừa kế hàm
‘draw()’ gốc từ lớp nguồn)
o Ở hàm main(), tạo ba đối tượng bằng toán tử new bao gồm:
 ‘shape’ thuộc lớp nguồn. Khi gọi hàm ‘draw()’ thông qua nó, hàm ‘draw()’ gốc
được thực thi.
 ‘rec’ thuộc lớp ‘Rectangle’. Khi gọi hàm ‘draw()’ thông qua nó, hàm ‘draw()’ tái
định nghĩa được thực thi.
 ‘cir’ thuộc lớp nguồn. Khi gọi hàm ‘draw()’ thông qua nó, hàm ‘draw()’ gốc được
thực thi, vì lớp ‘Circle’ không ghi đè lại hàm.
DANH SÁCH LIÊN KẾT

1. Tổng quát về danh sách liên kết


 Danh sách liên kết (Linked list) là một cấu trúc dữ liệu động (Các loại mảng là cấu trúc dữ liệu
tĩnh). So với mảng, nó có các lợi thế và nhược điểm riêng:

Danh sách liên kết Mảng


Số lượng Không cần biết trước Yêu cầu biết (Sử dụng new thì cũng
phần tử phải cung cấp lúc biên dịch chương
trình)
Thêm, bớt Tốn khoảng thời gian như nhau với Càng nhiều phần tử, thời gian cần
phần tử mọi số lượng phần tử. càng nhiều.
Truy xuất Không thể truy xuất trực tiếp. Có thể truy xuất trực tiếp.
phần tử Vị trí càng xa điểm bắt đầu, thời Tốn khoảng thời gian như nhau với
gian cần càng nhiều. mọi số lượng phần tử.
Bộ nhớ Tốn nhiều (Mỗi phần tử ngoài giá Ít tốn
trị còn phải chứa thêm con trỏ)
Phân mảnh Không, vì các phần tử không cần Có, vì các phần tử cần nằm liên tiếp
bộ nhớ nằm liên tiếp nhau trong bộ nhớ. nhau trong bộ nhớ.

 Có nhiều loại danh sách liên kết, một số loại chủ yếu bao gồm:
o Danh sách liên kết đơn (Singly Linked List): Mỗi phần tử liên kết với phần tử đứng sau
nó.

o Danh sách liên kết đôi (Doubly Linked List): Mỗi phần tử liên kết với phần tử đứng
trước và sau nó.

o Danh sách liên kết vòng (Circular Linked List)


o Danh sách liên kết có sắp xếp (Sorted Linked List)
o Danh sách liên kết lược (Skip List)
 Mỗi phần tử trong danh sách liên kết được gọi là một nút (node), gồm 2 thành phần:
o Phần dữ liệu: Chứa giá trị của phần tử
o Phần liên kết: Chứa địa chỉ con trỏ trỏ tới phần tử kế tiếp.

2. Danh sách liên kết đơn (Singly Linked List)


 Các bước để tạo một danh sách liên kết đơn:
(Xét ví dụ tạo một danh sách liên kết đơn 5 số nguyên ngẫu nhiên, tìm giá trị số nguyên lớn
nhất)
o Định nghĩa cấu trúc của một nút, bao gồm:
 Phần dữ liệu ‘data’ có kiểu dữ liệu int vì đề bài yêu cầu làm việc với số nguyên.
 Phần liên kết ‘pNext’ là một con trỏ kiểu node, dùng để lưu địa chỉ của phần tử
kế tiếp nó.
o Định nghĩa cấu trúc của một danh sách liên kết, bao gồm:

 Một con trỏ kiểu node ‘pHead’, dùng để trỏ tới phần tử đầu tiên của danh sách
liên kết.
 Một con trỏ kiểu node ‘pTail’, dùng để trỏ tới phần tử cuối cùng của danh sách
liên kết.
o Định nghĩa hàm ‘initialize()’ cho danh sách liên kết, bao gồm:

 Đưa con trỏ ‘pHead’ và ‘pTail’ của cấu trúc ‘list’ vừa định nghĩa về nullptr, vì 2
con trỏ này chưa được chỉ định trỏ vào bất cứ chỗ nào.
 Hàm này chỉ có chức năng chỉ định 2 con trỏ đầu, cuối để chúng không lơ lửng.
Hàm không trả về một giá trị nào nên kiểu void.
o Định nghĩa hàm ‘make_node()’ khai báo và khởi tạo cho một nút:

Ghi chú 183: Vì hàm làm việc với con trỏ kiểu node, chứ không phải kiểu node nên sử dụng toán tử -> thay vì . để truy cập đến
các thành phần của cấu trúc node.

 Hàm ‘make_node()’ nhận vào tham số là một số nguyên, rồi tạo ra một nút kiểu
node bao gồm: một giá trị ‘data’ là số nguyên được truyền vào và một con trỏ
‘pNext’ sẽ trỏ đến các nút khác.
 Hàm sẽ trả về một nút kiểu node, nên kiểu giá trị của hàm là node.
 Con trỏ ‘pNext’ chưa được chỉ định sẽ trỏ vào vùng nhớ nào, nên khởi tạo cho
nó giá trị nullptr.
 Hàm if có vai trò đảm bảo giá trị trả về của hàm là phù hợp. Trong trường hợp
đã có quá nhiều bộ nhớ được sử dụng mà tạo thêm một nút thì toán tử new
không thể cấp phát thêm vùng nhớ cho nút đó. Hàm sẽ trả về giá trị nullptr.
o Định nghĩa hàm ‘add_first_node()’ thêm một nút vào đầu danh sách liên kết:

 Hàm ‘add_first_node()’ nhận tham số đầu vào là danh sách liên kết ‘lt’ và một
con trỏ trỏ tới một nút ‘p’. Hàm sẽ thực hiện gán các giá trị cho các tham số,
không tạo ra thêm thực thể nào, nên cũng không trả về thực thể nào. Vì vậy,
kiểu hàm là void.
 Ở đây, ‘p’ là một con trỏ trỏ tới một nút kiểu node, tức mang giá trị là địa chỉ
của nút đó. Bởi vì cấu trúc node có thành phần liên kết là ‘pNext’, bản thân ‘p’
mang trong mình một thành viên là ‘pNext’ dùng dể trỏ tới địa chỉ của một nút
khác (tức ‘p’’ của một nút nào đó khác).
 Nếu danh sách đang rỗng, nút được truyền vào hàm sẽ trở nên duy nhất trong
danh sách, nên con trỏ ‘pHead’ và ‘pTail’ đều sẽ trở thành chính nút đó.
 Nếu danh sách đang không rỗng, ta cho ‘pNext’ của nút ‘p’ trỏ vào ‘pHead’, tức
mang giá trị địa chỉ của ‘pHead’ (‘pHead’ hiện đang là nút đầu tiên cũ của danh
sách), rồi cho con trỏ ‘pHead’ trở thành nút ‘p’ mới thêm vào.
o Định nghĩa hàm ‘add_last_node()’ thêm một nút vào cuối danh sách liên kết:

 Hàm ‘add_last_node()’ nhận tham số đầu vào là danh sách liên kết ‘lt’ và một
con trỏ trỏ tới một nút ‘p’. Hàm sẽ thực hiện gán các giá trị cho các tham số,
không tạo ra thêm thực thể nào, nên cũng không trả về thực thể nào. Vì vậy,
kiểu hàm là void.
 Ở đây, ‘p’ là một con trỏ trỏ tới một nút kiểu node, tức mang giá trị là địa chỉ
của nút đó. Bởi vì cấu trúc node có thành phần liên kết là ‘pNext’, bản thân ‘p’
mang trong mình một thành viên là ‘pNext’ dùng dể trỏ tới địa chỉ của một nút
khác (tức ‘p’’ của một nút nào đó khác).
 Nếu danh sách đang rỗng, nút được truyền vào hàm sẽ trở nên duy nhất trong
danh sách, nên con trỏ ‘pHead’ và ‘pTail’ đều sẽ trở thành chính nút đó.
 Nếu danh sách đang không rỗng, ta cho ‘pNext’ của nút cuối cùng (hiện đang là
‘pTail’) trỏ vào ‘p’, tức mang giá trị địa chỉ của ‘p’, rồi cho con trỏ ‘pTail’ trở
thành nút ‘p’ mới thêm vào.
o Định nghĩa hàm ‘display()’ hiển thị giá trị của các nút trong danh sách:

Ghi chú 184: Không cần truyền 'lt' dưới dạng tham chiếu, vì hàm này không thực hiện bất cứ sự thay đổi gì
lên danh sách liên kết.

 Hàm ‘display()’ nhận tham số đầu vào là danh sách liên kết ‘lt’. Hàm sẽ thực hiện
xuất giá trị của từng nút có trong danh sách, ngăn cách nhau bởi khoảng trống,
không tạo ra thêm thực thể nào, nên cũng không trả về thực thể nào. Vì vậy,
kiểu hàm là void.
 Thực thi vòng lặp for với biến chạy là con trỏ ‘k’ kiểu node. Sau mỗi vòng lặp, ‘k’
sẽ trỏ tới nút tiếp theo, và vòng lặp dừng khi ‘k’ trỏ quá nút cuối cùng.
o Định nghĩa hàm ‘findmax()’ tìm giá trị lớn nhất của một nút trong danh sách liên kết

Hàm ‘display()’ nhận tham số đầu vào là danh sách liên kết ‘lt’. Hàm sẽ trả về giá
trị lớn nhất của một nút trong danh sách, là một số nguyên. Vì vậy, kiểu hàm là
int.
 Tạo một biến số nguyên ‘max’. Khởi tạo giá trị cho nó bằng giá trị mà nút đầu
tiên của danh sách mang theo.
 Tạo một vòng lặp tượng tự như trong hàm ‘display()’. Bên trong vòng lặp, nếu
biến ‘max’ gặp phải một giá trị mang theo bởi một nút mà lớn hơn chính nó,
‘max’ sẽ mang giá trị mới là nó.
o Ở hàm main():
 Khai báo một biến ‘lt’ kiểu list
 Tạo một vòng lặp for chạy 5 lần. Trong mỗi lần thực hiện các thao tác:
 Khai báo một biến số nguyên ‘x’ và khởi tạo cho nó một giá trị ngẫu
nhiên nằm trong khoảng 1 đến 100 (Phát sinh số ngẫu nhiên sử dụng
thư viên <random>).
 Khai báo một con trỏ ‘p’ kiểu node, khởi tạo nó giá trị trả về bởi hàm
‘make_node()’, với tham số là biến ‘x’.
 Thêm nút ‘p’ mới tạo này vào danh sách ‘lt’. Ở đây, thêm nút mới vào
đầu danh sách hoặc cuối danh sách đều không ảnh hưởng đến bài toán,
vì các giá trị truyền vào đều ngẫu nhiên.
 Hiển thị tất cả giá trị mang trong 5 nút với hàm ‘display()’
 Hiển thị giá trị lớn nhất trong 5 giá trị với hàm ‘findmax()’
 Thêm một nút về sau:
o Định nghĩa hàm ‘add_next_node()’ thực hiện thêm một nút ‘p’ vào sau một nút ‘q’:

 Hàm ‘add_next_node()’ nhận tham số đầu vào là danh sách liên kết ‘lt’, nút kiểu
node ‘p’ và giá trị phần dữ liệu số nguyên của nút ‘q’ mà ta cần chèn ‘p’ về phía
sau gọi là ‘qdata’. Hàm sẽ thực hiện thêm một nút ‘p’ vào sau mỗi nút mà phần
dữ liệu trong nó bằng ‘qdata’, không tạo ra thêm thực thể nào, nên cũng không
trả về thực thể nào. Vì vậy, kiểu hàm là void.
 Đầu tiên, tạo một nút kiểu node với phần dữ liệu mang giá trị là ‘qdata’ với hàm
‘make_node()’
 Nếu trong danh sách liên kết chỉ đang có một nút duy nhất (giá trị mang trong
nút đó bằng giá trị mang trong nút ‘q’, đồng thời phần liên kết của nút ‘pHead’
trỏ về null), thì việc thêm vào nút ‘p’ phía sau đồng nghĩa với việc thêm nút ‘p’
vào cuối danh sách liên kết. Vì vậy, trường hợp này chỉ cần thực hiện lại hàm
‘add_last_node()’
 Nếu không, tạo một vòng lặp for duyệt từ đầu đến cuối danh sách. Với mỗi lần
gặp đến nút mang trong nó giá trị bằng với ‘qdata’ (tại thời điểm này, nút ‘k’
đang chính là nút ‘q’, thực hiện:
 Tạo một nút mới ‘x’ mang theo giá trị như nút ‘p’ truyền vào hàm.
 Trỏ phần liên kết của nút ‘x’ đến nơi mà phần liên kết của nút ‘q’ đang
trỏ tới, tức địa chỉ của nút tiếp theo, gọi là nút ‘q1’.
 Trỏ phần liên kết của nút ‘q’ về lại địa chỉ của nút ‘x’.
 Sau 3 bước, từ hệ q → q 1 trở thành q → x → q 1, với nút ‘x’ là bản sao của nút
‘p’.
 Bản thân nút ‘p’ chỉ có một, không thể chèn nhiều lần nếu như trong danh sách
có nhiều nút mang giá trị ‘qdata’. Vì vậy, ta tạo bản sao ‘x’ của nút ‘p’ mỗi lần
gặp nút ‘q’ để chèn vào danh sách.

o Ở hàm main():

Ghi chú 185: Trong danh sách có hai nút mang giá trị 1. Sau khi thực hiện truyền thêm nút, hai nút mang giá trị 10 xuất hiện.

 Khai báo một biến ‘lt’ kiểu list


 Tạo một mảng số nguyên ‘arr[]’ chứa 5 phần tử, truyền các giá trị cho chúng.
 Tạo một vòng lặp for chạy 5 lần. Trong mỗi lần thực hiện các thao tác:
 Khai báo một con trỏ ‘p’ kiểu node, khởi tạo nó giá trị trả về bởi hàm
‘make_node()’, với tham số là phần tử thứ i của mảng ‘arr[]’.
 Thêm nút ‘p’ mới tạo này vào cuối danh sách ‘lt’, để các nút mang theo
giá trị như giá trị truyền vào các phần tử trong mảng.
 Hiển thị tất cả giá trị mang trong 5 nút với hàm ‘display()’.
 Khai báo một con trỏ ‘p’ kiểu node, khởi tạo nó giá trị trả về bởi hàm
‘make_node()’, với tham số là 10.
 Thực hiện truyền nút ‘p’ này vào danh sách ‘lt’, phía sau mỗi nút có giá trị 1.
 Hiển lại thị tất cả giá trị mang trong 5 nút với hàm ‘display()’ để so sánh.
 Ở hàm ‘add_next_node()’, nếu không thực hiện tạo bản sao ‘x’ để chèn vào mà
chèn trực tiếp nút ‘p’, khi có sự lặp lại của nút mang giá trị 1 sẽ bị lỗi như sau:

3. Đọc – Ghi file


LỆNH

1. sizeof( ) kích thước bộ nhớ của kiểu dữ liệu/ biến nằm bên trong

2. Khai báo thư viện dữ liệu


#include <iostream>;
Using namespace std;
Vì phần lớn các câu lệnh được dùng đã được định nghĩa trong đó nên muốn xài thì phải khai báo nguồn.
Có thể khai báo tại chỗ dùng toán tử phân giải phạm vi thay vì khai báo đầu chương trình, tức dùng ở
đâu thì khai báo ở đấy
Std::int n=5 dùng câu lệnh gán biến n trong namespace std nếu đầu chương trình chưa khai báo std

3. static_cast<a>(b) Ép kiểu dữ liệu a của kí tự b


Bảng mã ASCII chỉ đến mã số 127, nếu ép kiểu với số lớn hơn sẽ dẫn đến tràn số, xuất ra kí tự lỗi

4. Định dạng dữ liệu


 setw(n) xác định độ dài của dữ liệu xuất. Khi sử dụng, các khoảng trống sẽ được thêm vào để dữ liệu
xuất có n kí tự (đếm luôn cả ký tự được xuất ra)
o setw(n) << left << “a”; Các khoảng trống được thêm vào bên phải kí tự “a” (tức “a” nằm bên trái)
sao cho nó có n kí tự, gồm cả kí tự “a”
o setw(n) << right << “a”; Các khoảng trống được thêm vào bên trái kí tự “a” (tức “a” nằm bên phải)
sao cho nó có n kí tự, gồm cả kí tự “a”
o Kí tự a còn có thể là khoảng trống
 setfill(‘?’) đi chung với setw(n) để thêm vào kí tự ? thay vì khoảng trống như mặc định
 Phân tích ví dụ:
o Giữa “ID” và “Name” có 5-2=3 kí tự, vì “ID” được khai báo gồm 5 kí tự tính cả nó => bên phải nó có
3 khoảng trống
o Tương tự, giữa “Name” và “Address” có (30-4)+(20-7)=39 kí tự, vì có 26 khoảng trống bên phải
“Name” và 13 khoảng trống bên trái “Address”
o Có 30+20+5=55 khoảng trống đã được thay thế bằng dấu “-“ ở dòng thứ 2 của bảng
o Khoảng cách giữa cột 2 và 3 được tạo bởi cả khoảng trống của setw(n) << left và setw(n) << right
o Để canh lề trái như cột 1 và 2, phải dùng setw(n) << left và để canh lề phải ở cột 3, dùng thêm
setw(n) << right. Bởi vì số kí tự của mỗi tên ở cột 2 là khác nhau, không dùng thì cột 3 sẽ không canh
ngay lề theo bên phải được

5. Số ở các hệ khác nhau


std::dec hệ thập phân
std::oct hệ bát phân
std::hex hệ thập lục phân

6. system(“cls”) xóa màn hình


Có thể lồng trong các vòng lặp WHILE, DO WHILE để xóa màn hình sau mỗi lần nhập xuất

7. cin.get() chỉ lấy kí tự đầu tiên trong nhóm các kí tự nhập vào

8. typedef <nguyên bản> <giá trị quy ước>; thay thế các kiểu dữ liệu có sẵn bằng một cái tên mới

9. #define <giá trị quy ước> <nguyên bản> thay thế các kiểu dữ liệu có sẵn bằng một cái tên mới, gắn một
giá trị cụ thể cho một cụm từ quy ước hoặc quy ước thay đổi cấu trúc một hàm…

10. using <giá trị quy ước> = <nguyên bản>; thay thế các kiểu dữ liệu có sẵn bằng một cái tên mới:

11.
12. F
13. F
14. F
15. F
16. F
17. F
18. F
19. F
20. F
21. F
22. F
23. F
24. F
25. f

You might also like