Download as pdf or txt
Download as pdf or txt
You are on page 1of 125

Phần I.

GIỚI THIỆU C++

Phần này giới thiệu về cấu trúc của một chương trình C++, mục đích là để giúp bạn
nhanh chóng viết được những chương trình đầu tiên, qua đó hiểu cách thức soạn
thảo, dịch và chạy chương trình. Các khái niệm sẽ được giải thích chi tiết hơn trong
các chương sau.

1
Phần I
GIỚI THIỆU C++

Chương 1. Lập trình và ngôn ngữ lập trình


1.1. Tóm lược lịch sử
Ý tưởng về chiếc máy có thể làm việc theo chỉ thị ra đời từ rất sớm. Năm 1843,
Charles Barbages thiết kế chiếc máy phân tích (analytical engine) - một công cụ
tính toán cơ học chạy bằng hơi nước. Ada Lovelace, một cộng sự của Charles
Barbages đã viết nhưng ghi chú cho cỗ máy, trong đó phân biệt rõ ràng hai thành
phần:
Phần cứng: Một hệ thống vật lý làm việc theo chỉ thị (instructions)
Phần mềm: Chứa các chỉ thị giao cho phần cứng thực hiện
Khả năng làm việc theo chỉ thị là một đặc tính khác biệt hoàn toàn so với các công
cụ tính toán thô sơ, đó là sự khác nhau căn bản giữa hai khái niệm Computer và
Calculator*. Ada Lovelace thậm chí đã thử viết một chương trình để diễn giải cách
thức làm việc của máy. Mặc dù chiếc máy không thể hoàn thành do những khó khăn
về tài chính, sau này người ta ghi nhận Barbages là “cha đẻ của máy tính” và Ada
Lovelace là “lập trình viên đầu tiên của thế giới”.
Chiếc máy tính đầu tiên được ra đời vào những năm 1940s, đó là một chiếc máy
chạy bằng điện, sử dụng mã nhị phân để biểu diễn các chỉ thị (gọi là mã máy hay
ngôn ngữ máy). Các kỹ sư đưa các chỉ thị vào máy dưới dạng các băng đục lỗ. Công
việc lập trình kiểu thủ công này mất rất nhiều công sức.
Hợp ngữ (assembly language) là ngôn ngữ lập trình cấp thấp (low-level
programming language) sử dụng các lệnh hợp ngữ để thay thế các chỉ thị dạng mã
nhị phân. Trình hợp dịch (assembler) sẽ chuyển từng lệnh hợp ngữ thành mã máy
tương ứng. Nếu so với việc lập trình trực tiếp bằng mã nhị phân, hợp ngữ cho phép
viết, bảo trì và nâng cấp chương trình dễ dàng hơn. Tuy nhiên nhược điểm cơ bản
là tập lệnh hợp ngữ hoàn toàn phụ thuộc vào kiến trúc của máy tính. Lập trình trên
một hệ thống máy tính khác đòi hỏi phải học lại hoàn toàn tập lệnh hợp ngữ tương
ứng, tức là học lại hẳn một ngôn ngữ khác.
Các ngôn ngữ lập trình cấp cao (high-level programming languages) được tạo ra
nhằm giúp cho người lập trình viết ra những chương trình không phụ thuộc hoặc
ít phụ thuộc vào phần cứng máy tính, chúng gần với ngôn ngữ tự nhiên hơn so với
ngôn ngữ máy. Ưu điểm lớn nhất của các ngôn ngữ lập trình cấp cao là rất dễ đọc,

*Hai thuật ngữ này lẽ ra nên gọi là máy lập trình (computer) và máy tính (calculator), nhưng chúng
đều được gọi là “máy tính” trong tiếng Việt.

2
Lê Minh Hoàng
Chương 1
Lập trình và ngôn ngữ lập trình

dễ viết và dễ bảo trì cũng như nâng cấp. Những chương trình viết ra phải được
dịch thành mã máy thông qua một chương trình dịch (hay trình dịch). Trình dịch
phụ thuộc vào phần cứng, tức là trên mỗi kiến trúc máy tính, mỗi hệ điều hành, cần
có trình dịch riêng. Có hai kiểu trình dịch:
Thông dịch (interpreter): Dịch từng dòng lệnh và chạy luôn dòng lệnh đó. Quá
trình này giống như cách làm của một thông dịch viên. Điều đó có nghĩa là trình
thông dịch luôn phải hiện diện trong máy để chạy chương trình.
Biên dịch (compiler): Dịch toàn bộ chương trình ra mã máy trong một tiến trình
độc lập, mã máy này sau đó có thể thực hiện trực tiếp trên máy tính mà không
cần sự hiện diện của trình biên dịch nữa. Quá trình này giống như khi ta dịch
sách.
Vì nhiều lý do kỹ thuật, đặc biệt là việc thông dịch phải tích hợp cả tiến trình dịch
vào trong tiến trình thực thi, nên các chương trình chạy bằng thông dịch chậm hơn
rất nhiều so với sản phẩm tạo ra bằng cách biên dịch. Các phần mềm biên dịch còn
tích hợp những khả năng tự động tối ưu mã nên tốc độ thực thi của những mã biên
dịch hiện đại không bị thua kém quá nhiều so với chương trình viết trực tiếp bằng
ngôn ngữ máy hay hợp ngữ.
Chú ý rằng không có khái niệm ngôn ngữ biên dịch hay thông dịch. Thông dịch hay
biên dịch chỉ là cách thức chuyển ngôn ngữ lập trình cấp cao sang ngôn ngữ máy.
Một số ngôn ngữ lập trình có cả trình biên dịch và trình thông dịch, thậm chí có
ngôn ngữ lập trình không có trình dịch, chúng chỉ tồn tại trên thiết kế và chưa bao
giờ được sử dụng để viết phần mềm trong thực tế.
Ý tưởng về ngôn ngữ lập trình cấp cao bắt đầu từ cuối những năm 1940s và cho
tới ngày nay đã có hàng trăm ngôn ngữ lập trình. Ta điểm qua một vài sự kiện quan
trọng:
Năm 1954, ngôn ngữ FORTRAN được phát minh tại IBM, FORTRAN là ngôn ngữ
lập trình cấp cao đầu tiên được sử dụng rộng rãi, có trình biên dịch đi kèm.
Cuối những năm 1950, một hội đồng những nhà khoa học máy tính tại Mỹ và Châu
Âu thành lập đề án xây dựng ngôn ngữ ALGOL (ALGOrithmic Language), trong đó
đưa ra 3 đặc tả quan trọng của ngôn ngữ được dùng cho cả những thế hệ ngôn ngữ
sau này, đó là: Cấu trúc khối lồng nhau, phạm vi hiệu lực của một định danh, định
nghĩa dạng chuẩn Backus-Naur cho văn phạm phi ngữ cảnh.
Thập kỷ 1960s và 1970s chứng kiến sự ra đời của một loạt ngôn ngữ lập trình,
đáng chú ý nhất là Pascal (1970) và C (1972). Chúng đều là ngôn ngữ đa năng,
nhưng Pascal chủ yếu dùng để viết các chương trình ứng dụng, mô tả thuật toán
trên các ấn bản khoa học còn C thường được coi là ngôn ngữ lập trình hệ thống vì
đó chính là ngôn ngữ xây dựng nên hệ điều hành Unix. Giai đoạn này cũng có một

3
Ngôn ngữ lập trình C++
Phần I
GIỚI THIỆU C++

sự tranh luận sôi nổi về tính hiệu quả của phương pháp lập trình cấu trúc
(structured programming) - một kỹ thuật lập trình nhằm tăng tính rõ ràng, chất
lượng của chương trình và giảm thời gian phát triển chương trình bằng cách sử
dụng các lệnh chọn, lệnh lặp với cấu trúc khối và chương trình con. Tranh cãi xoay
quanh vấn đề sử dụng lệnh nhảy goto, một bộ phận đáng kể lập trình viên cho rằng
sử dụng lệnh nhảy goto là một phong cách lập trình tồi. Tuy vậy trong một vài
trường hợp, việc dùng các cấu trúc khác để thay thế lệnh nhảy goto lại làm phức
tạp hóa chương trình. Một số ngôn ngữ lập trình cấu trúc những vẫn cho phép sử
dụng lệnh nhảy goto nhưng kèm theo những khuyến cáo khi sử dụng.
Thập kỷ 1980s, 1990s chứng kiến sự hợp nhất tương đối của các ngôn ngữ lập
trình. Thay vì phát minh ra những ngôn ngữ lập trình mới, các ý tưởng tốt được
các ngôn ngữ lập trình học tập lẫn nhau và nâng cấp lên thành các phiên bản mới
của ngôn ngữ. Mô hình lập trình hướng đối tượng (Object-Oriented Programming
- OOP) cho bổ sung nhiều kỹ thuật mới cho phương pháp lập trình cấu trúc. Những
đặc tả OOP được tích hợp vào trong ngôn ngữ C, Pascal thành ngôn ngữ C++ và
Object Pascal (Delphi). Thời gian này cũng đánh dấu sự ra đời của ngôn ngữ Java:
Một kiến trúc ngôn ngữ lập trình và cả trình dịch hoàn toàn độc lập với phần cứng,
chạy được trên mọi kiến trúc có trang bị máy ảo Java.
Trong kỷ nguyên hiện tại, đã có nhiều ngôn ngữ lập trình trở nên lạc hậu và không
còn sử dụng nữa, bên cạnh đó có nhiều ngôn ngữ lập trình mới được thiết kế. Ta
có thể tạm phân loại các ngôn ngữ lập trình ra thành các nhóm như sau:
Nhóm ngôn ngữ dùng để học nhập môn lập trình như Scratch, Python, Pascal, …
đây là những ngôn ngữ dễ học ở mức độ cơ bản. Chúng thích hợp cho những người
chỉ cần biết khái niệm lập trình, hoặc chỉ có nhu cầu làm những chương trình đơn
giản. Với định hướng trở thành lập trình viên chuyên nghiệp, cũng có thể học
chúng đến mức độ nào đó rồi chuyển sang học ngôn ngữ khác.
Nhóm ngôn ngữ lập trình đa năng (general-purpose programming languages) như
Object Pascal, C, C++, C#, Java, … : Đây là nhóm ngôn ngữ nền tảng và quan trọng
nhất mà bất kỳ lập trình viên chuyên nghiệp nào cũng phải biết ít nhất một ngôn
ngữ. Nhóm ngôn ngữ này được chuẩn hóa ISO, trình dịch liên tục được tối ưu và
nâng cấp để chạy được trên nhiều kiến trúc phần cứng khác nhau. Học ngôn ngữ
lập trình đa năng cho phép lập trình viên có thể viết ra các chương trình hiệu suất
cao, không chịu bất kỳ sự giới hạn nào của ngôn ngữ, ngoài ra còn có thể hiểu được
những bí quyết tối ưu mã lệnh.
Nhóm ngôn ngữ lập trình chuyên biệt (special-purpose programming languages):
Đây là những ngôn ngữ thiết kế riêng cho một vài nhiệm vụ cụ thể, chẳng hạn ngôn

4
Lê Minh Hoàng
Chương 1
Lập trình và ngôn ngữ lập trình

ngữ R dùng trong các phần mềm thống kê, MATLAB dùng cho các công cụ xử lý
toán học, LISP dùng trong các ứng dụng trí tuệ nhân tạo. Những chương trình viết
trên những ngôn ngữ này xử lý một vài tác vụ chuyên biệt rất dễ dàng và hiệu quả
nhờ cấu trúc của ngôn ngữ và hệ thống thư viện đi kèm, nhưng với những tác vụ
khác có thể rất khó hoặc không thể thực hiện được.
Nhóm ngôn ngữ kịch bản (scripting languages): Đây là những ngôn ngữ dùng để
viết những đoạn mã không phải để dịch và chạy trực tiếp trên máy, chúng chứa
một “kịch bản” cho một phần mềm khác thực hiện. Chẳng hạn như Java Script để
các trình duyệt thực hiện ngay trên trang Web hay VB Script để viết các Macro
trong Microsoft Office.
1.2. Vài nét về ngôn ngữ lập trình C++
Vào cuối những năm 1960s, Dennis M. Ritchie và Ken Thompson từ phòng thí
nghiệm Bell của công ty AT&T phát triển hệ điều hành UNIX cho các máy tính lớn.
Ban đầu, hệ điều hành này được viết hoàn toàn bằng hợp ngữ. UNIX tích hợp sẵn
những trình dịch cho hợp ngữ, FORTRAN và đặc biệt là ngôn ngữ B, ngôn ngữ được
chính Ken Thompson phát triển. Ngôn ngữ B được sử dụng để thay thế cho hợp
ngữ trong quá trình phát triển tiếp theo của hệ điều hành UNIX, bởi nó cho phép
viết những đoạn mã ngắn gọn hơn và dễ dàng hơn nhiều so với hợp ngữ.
Nhược điểm của ngôn ngữ B là nó không có cơ chế định kiểu, không có khái niệm
dữ liệu có cấu trúc. Đó là lý do mà Ritchie tiếp tục phát triển ngôn ngữ B thành một
ngôn ngữ mới đặt tên là C, giữ lại hầu hết cú pháp của B nhưng đưa thêm vào các
kiểu dữ liệu mới và một vài sự thay đổi (1971-1973). Ngôn ngữ C có sức mạnh của
một ngôn ngữ bậc cao nhưng lại dễ dàng tích hợp những đoạn mã hợp ngữ, rất
thuận lợi cho việc viết hệ điều hành và cả các chương trình phần mềm. Rất nhiều
thành phần của UNIX sau đó được viết bằng C, bản thân phần cơ sở (kernel) của
UNIX cũng được viết lại hoàn toàn bằng C (1973).
Ngôn ngữ C được viết ra trong một cuốn sách kinh điển: “The C Programming
Language”, bởi Kernighan và Ritchie và từ đó phổ biến khắp thế giới. Viện tiêu
chuẩn quốc giá Mỹ (American National Standards Institute – ANSI) đã chuẩn hóa*
ngôn ngữ C với một số sửa đổi nhỏ, trở thành ANSI C (1983) và sau đó cả tổ chức
tiêu chuẩn quốc tế (International Standards Organization – ISO) cũng chuẩn hóa

*Chuẩn hóa là đưa ra những tiêu chuẩn, quy định bắt buộc cho ngôn ngữ nhằm giúp cho những
chương trình viết trên ngôn ngữ được chuẩn hóa có thể dịch được và chạy được trên tất cả môi
trường tuân theo tiêu chuẩn đã định. Mặc dù công cụ lập trình cụ thể có thể có những mở rộng
thêm cho ngôn ngữ, nhưng chúng ta không dùng các thành phần không chuẩn hóa trong lúc học để
có thể dễ dàng chuyển đổi sang môi trường lập trình, chương trình dịch khác.

5
Ngôn ngữ lập trình C++
Phần I
GIỚI THIỆU C++

ngôn ngữ C. Mặc dù tên đúng bây giờ là ISO C, nhưng hầu hết mọi người vẫn quen
thuộc với từ ANSI C hơn.
Ngôn ngữ lập trình C++ được phát minh bởi Bjarne Stroustrup. Khi làm luận án
tiến sĩ, ông có cơ hội làm việc với ngôn ngữ Simula67, ngôn ngữ đầu tiên hỗ trợ
cho các mô hình lập trình hướng đối tượng (Object-Oriented Programming – OOP).
Stroupstrup nhận thấy OOP rất hữu dụng cho việc phát triển phần mềm, tuy nhiên
Simula quá chậm để sử dụng trên thực tế. Không lâu sau, ông bắt đầu nghiên cứu
mở rộng ngôn ngữ C với các tính năng hỗ trợ lập trình hướng đối tượng và cho ra
đời một ngôn ngữ mới: C with Classes.
Năm 1983, C with Classes được đổi tên thành C++ (toán tử ++ trong ngôn ngữ C là
toán tử tăng lên), rất nhiều tính năng mới được thêm vào ngôn ngữ như: phương
thức ảo, nạp chồng hàm, tham chiếu với ký hiệu &, từ khóa const, … Năm 1985,
Stroustrup viết cuốn sách “The C++ Programming Language”, mặc dù ngôn ngữ
chưa được chuẩn hóa, C++ đã trở nên phổ biến từ khi xuất bản cuốn sách này. Ngôn
ngữ được cập nhật một lần nữa vào năm 1989 với việc đưa thêm vào kiến trúc đa
thừa kế của các lớp.
Năm 1998, tổ chức tiêu chuẩn quốc tế đã chuẩn hóa C++: ISO/IEC 14882:1998 gọi
tắt là C++ 98, đặc biệt là thư viện chuẩn các lớp mẫu (Standard Template Library –
STL) cũng được chuẩn hóa. Tiêu chuẩn này có nhiều vấn đề và được chỉnh sửa lại
năm 2003 trong bản C++ 03.
Vào giữa năm 2011, chuẩn C++ 11 được công bố, trong đó đưa vào một số thành
phần của thư viện Boost và rất nhiều sự nâng cấp của ngôn ngữ: hỗ trợ xử lý biểu
thức chính quy (regular expression), thư viện thuật toán tạo số ngẫu nhiên, thư
viện đồng hồ thời gian thực, cú pháp vòng lặp mới, hàm lambda… Chuẩn này được
hoàn thiện hơn với C++14.
Mặc dù các chuẩn có tính kế thừa, không hẳn một chương trình viết trong một
chuẩn C++ có thể dịch và chạy được với trình dịch cho một chuẩn khác. Hầu hết
các trình dịch hiện nay đều hỗ trợ C++14, những chuẩn C++ mới hơn như C++17,
C++20, … đôi khi chỉ được hỗ trợ một phần hoặc hiện tại chưa được hỗ trợ trong
các trình dịch. Chúng ta sẽ sử dụng trình dịch GNU GCC (cụ thể là G++) cho các ví
dụ trong cuốn sách này. Tiêu chuẩn C++ được sử dụng là ISO C++14.

6
Lê Minh Hoàng
Chương 2
Cấu trúc chương trình C++

Chương 2. Cấu trúc chương trình C++


2.1. Chương trình đầu tiên
Cách học lập trình hiệu quả nhất chính là viết chương trình, xét một chương trình
C++ đơn giản: In ra màn hình dòng chữ “Xin chao!”.

 Chương trình 1 
1 | //Chương trình C++ đầu tiên Xin chao!
2 | #include <iostream>
3 |
4 | int main()
5 | {
6 | std::cout << "Xin chao!";
7 | }
Tuy đơn giản, chương trình cũng phải bao gồm các thành phần bắt buộc của một
chương trình C++.
Bên phải là kết quả in ra màn hình, các con số và dấu “|” bên trái mỗi dòng là chỉ số
dòng để tiện chỉ định, chúng không phải là thành phần của chương trình. Ta phân
tích từng dòng một về vai trò của chúng:
Dòng 1:
//Chương trình C++ đầu tiên
Hai dấu gạch cho biết phần còn lại trên dòng là chú thích (comment), những chú
thích được viết ra bởi lập trình viên, dùng để giải thích một đoạn mã hoặc một số
lưu ý quan trọng về đoạn mã. Chú thích không ảnh hưởng đến cách thức chương
trình thực hiện, chỉ có tác dụng cho người đọc mà thôi.
Dòng 2:
#include <iostream>
Các dòng bắt đầu bằng dấu # sẽ được trình biên dịch xử lý trước khi dịch. Trong
chương trình cụ thể này, chỉ thị #include <iostream> sẽ yêu cầu trình biên dịch
thêm vào một đoạn mã chuẩn (nằm trong file đề mục iostream) cho phép thực hiện
các lệnh nhập/xuất dữ liệu.
Dòng 3:
Đây là một dòng trống, không có hiệu lực trong chương trình, chỉ đơn giản là tăng
tính sáng sủa, dễ đọc.
Dòng 4:
int main()
Dòng này bắt đầu khai báo của một hàm. Hàm là một đoạn mã được đặt tên, trong
trường hợp này tên hàm là “main”. Chương trình C++ có thể chứa nhiều hàm, mỗi

7
Ngôn ngữ lập trình C++
Phần I
GIỚI THIỆU C++

hàm có kiểu kết quả trả về, tên hàm và các tham số. Ta sẽ trình bày về hàm trong
các chương sau.
Hàm main là một hàm đặc biệt trong chương trình C++, nó là hàm được gọi chạy
đầu tiên khi thực thi chương trình C++, bất kể hàm đó nằm ở đâu trong mã. Hàm
main có kiểu kết quả trả về là một giá trị số nguyên (int), trong đó giá trị trả về là
0 nếu chương trình kết thúc bình thường và giá trị trả về khác 0 nếu đó là mã lỗi
sinh ra trong lúc thực hiện. Hàm main trong chương trình này không có tham số,
phần tham số (trong dấu ngoặc đơn) được để trống.
Dòng 5 và 7:
Dấu “{” cho biết nơi bắt đầu thân hàm và dấu “}” cho biết nơi kết thúc hàm. Phần
nằm giữa hai dấu {…} chứa cách lệnh sẽ cần thực hiện khi gọi hàm.
Dòng 6:
std::cout << "Xin chao!";
Dòng này là một lệnh của C++, các lệnh được thi hành theo thứ tự xuất hiện trong
hàm. Lệnh cụ thể này có 3 thành phần:
std::cout, đối tượng chỉ thiết bị xuất chuẩn (standard character output device),
thông thường đây chính là màn hình máy tính.
Toán tử <<: chỉ ra rằng những gì tiếp theo sau sẽ được ghi ra (đẩy ra) thiết bị
xuất chuẩn.
Một đoạn ký tự trong dấu nháy kép (“…”) là nội dung được ghi ra thiết bị xuất
chuẩn.
Tất cả các lệnh đơn giản cần kết thúc bởi dấu chấm phẩy “;”, một trong những lỗi
cú pháp phổ biến nhất khi lập trình C++ là quên kết thúc lệnh bằng dấu chấm phẩy.
C++ không có các quy tắc nghiêm ngặt về số lệnh trên một dòng cũng như quy tắc
thụt lề (indent), sự phân chia các dòng cũng như cách thức viết thụt lề là để chương
trình dễ hiểu với người đọc.
Ta thêm một lệnh vào chương trình:

 Chương trình 2 
1 | //Chương trình C++ thứ hai Xin chao! Day la C++
2 | #include <iostream>
3 |
4 | int main()
5 | {
6 | std::cout << "Xin chao! ";
7 | std::cout << "Day la C++";
8 | }

8
Lê Minh Hoàng
Chương 2
Cấu trúc chương trình C++

Chương trình Hello2.cpp thực hiện ghi ra thiết bị xuất chuẩn 2 lần bằng 2 lệnh khác
nhau. Các lệnh được thi hành theo đúng thứ tự xuất hiện trong thân hàm.
Nhắc lại rằng việc phân tách các lệnh ra trên các dòng khác nhau cũng như quy tắc
thụt lề chỉ là để làm cho chương trình dễ đọc đối với con người, không ảnh hưởng
đến cách thức thực hiện. Chẳng hạn ta có thể viết lại chương trình như sau:
1 | //Chương trình C++ thứ hai
2 | #include <iostream>
3 | int main(){std::cout << "Xin chao! "; std::cout << "Day la C++";}
Chú ý là ở chương trình trên, ta không thể ghép dòng 2 vào cuối dòng 1, bởi nếu
vậy toàn bộ nội dung dòng 2 sẽ trở thành chú thích. Ta cũng không thể ghép dòng
3 vào cuối dòng 2, các chỉ thị tiền xử lý (bắt đầu bằng dấu #) không phải là câu lệnh
(nên chúng không kết thúc bởi dấu chấm phẩy), chúng cần viết riêng biệt trên từng
dòng, được trình biên dịch đọc lần lượt và xử lý trước khi quá trình biên dịch bắt
đầu.
Thậm chí, mỗi lệnh của C++ cũng có thể tách ra trên nhiều dòng liên tiếp nếu việc
cắt dòng không vi phạm cú pháp của lệnh:
1 | //Chương trình C++ thứ hai
2 | #include <iostream>
3 | int main()
4 | {
5 | std::cout
6 | << "Xin chao! ";
7 | std::cout
8 | << "Day la C++";
9 | }
2.2. Chú thích
Chú thích là phần văn bản không có ý nghĩa với trình dịch, chỉ dùng để ghi chú
những lưu ý quan trọng hoặc mô tả vắn tắt ý nghĩa của đoạn lệnh. C++ cho phép
hai cách viết chú thích:
Cách 1: //Chú thích
Cách viết này coi toàn bộ phần văn bản bắt đầu từ hai dấu // đến cuối dòng là chú
thích. Phần văn bản này nằm trên một dòng.
Cách 2: /*Chú thích*/
Cách viết này coi toàn bộ phần văn bản bắt đầu từ cặp ký tự /* tới cặp ký tự */ là
chú thích. Phần văn bản này có thể nằm trên nhiều dòng liên tiếp.
1 | /* Chương trình C++
2 | với nhiều chú thích */
3 | #include <iostream>
4 | int main()
5 | {
6 | std::cout << "Xin chao! "; //In ra: Xin chào!
7 | std::cout << "Day la C++"; //In ra: Đây là chương trình C++
8 | }

9
Ngôn ngữ lập trình C++
Phần I
GIỚI THIỆU C++

2.3. Không gian tên std


Trong các chương trình đã trình bày, cout là một đối tượng trong thư viện chuẩn
(standard library). Tất cả những thành phần trong thư viện chuẩn được khai báo
trong một không gian tên (namespace) std.
Để tham chiếu tới một đối tượng trong một không gian tên, ta có thể viết đối tượng
đó dạng: không gian tên∷tên đối tượng (chẳng hạn std::cout). Có một cách khác là
khai báo sử dụng một không gian tên, khi đó ta không cần phần tiền tố không gian
tên (std::) đi trước tên đối tượng nữa. Ví dụ chương trình Hello2.cpp có thể viết
với phương pháp khai báo sử dụng không gian tên std.

Hello2.cpp ✓ 
1 | #include <iostream>
2 | using namespace std; //Dùng không gian tên std
3 |
4 | int main()
5 | {
6 | cout << "Xin chao! "; //Không cần viết std::cout
7 | cout << "Day la C++"; //Không cần viết std::cout
8 | }
Việc sử dụng không gian tên có thể hiểu qua ví dụ sau:
Lớp Mầm có một bạn tên là A; lớp Lá cũng có một bạn tên A. Khi tất cả các lớp đứng
dưới sân trường, muốn gọi bạn A ở lớp Lá, ta phải chỉ định đầy đủ “Bạn A ở lớp
Lá”. Tuy nhiên khi đang ở trong lớp Lá, ta chỉ cần gọi bạn A là đủ (vì coi như ta đã
sử dụng không gian tên của lớp Lá trong trường hợp này rồi).
Để chương trình được ngắn gọn và đơn giản, các ví dụ sau này tôi sẽ sử dụng không
gian tên std khá thường xuyên. Mặc dù vậy, khi viết các chương trình lớn, việc sử
dụng không gian tên std dễ gây ra xung đột về tên trong thư viện chuẩn và tên do
người lập trình tự định nghĩa. Một số chuẩn công nghiệp còn không cho phép dùng
lệnh “using namespace std;”. Chúng ta sẽ thảo luận chi tiết hơn về cách dùng cũng
như xử lý xung đột ở các chương sau.
2.4. Bộ chữ viết của C++
Bộ chữ viết của C++ gồm có:
Các chữ cái hoa (A…Z), các chữ cái thường (a…z), dấu gạch nối dưới (_) cũng
được coi là chữ cái.
Các chữ số (0…9)
Các ký tự đơn: + - * / = < > [ ] . , ( ) : ^ @ { } $ # % &
Các cặp ký tự: !=; <=; >=; ==; +=; -=; *=; /=; /*; */; //;…
Dấu cách (hay dấu trống) để ngăn cách các từ
Dấu chấm phẩy “;” để ngăn cách các lệnh

10
Lê Minh Hoàng
Chương 2
Cấu trúc chương trình C++

Trên đây là những ký tự liên quan tới cú pháp của C++, còn các ký tự trong chú
thích, hằng xâu ký tự thì không có ràng buộc gì cả.
Một điều quan trọng cần lưu ý: C++ có phân biệt chữ hoa và chữ thường. Hai tên
(định danh) khác nhau về ký tự hoa/thường được coi là khác nhau. Chẳng hạn cout
và CoUt là hai tên hoàn toàn phân biệt.

11
Ngôn ngữ lập trình C++
Phần I
GIỚI THIỆU C++

Chương 3. Cài đặt Code::Blocks


3.1. Môi trường phát triển tích hợp
Để viết một chương trình, ta cần một hệ soạn thảo hay trình soạn thảo (editor)
chuyên dụng cho một ngôn ngữ. Khi đã viết xong chương trình, ta lại cần một trình
dịch (compiler) để dịch mã nguồn sang mã máy. Chương trình chưa chắc đã chạy
đúng ngay, nếu là chương trình phức tạp, ta lại cần có những công cụ hỗ trợ gỡ rối
gọi là trình gỡ rối (debugger). Những chương trình này đôi khi do nhiều tác giả,
nhiều hãng phần mềm khác nhau viết, chúng chạy một cách độc lập và yêu cầu lập
trình viên khi dùng phần mềm nào phải kích hoạt phần mềm đó, công việc có phần
thủ công, dễ nhầm lẫn và chậm nếu chưa quen các thao tác.
Môi trường phát triển tích hợp (Integrated Development Environment – IDE) là một
hệ thống “tất cả trong một”, chứa đầy đủ cả trình soạn thảo, trình dịch, và trình gỡ
rối, lập trình viên chỉ việc sử dụng lựa chọn chức năng trên giao diện của IDE
(thông thường bằng các phím tắt – shortcut key).
Code∷Blocks là một IDE cho ngôn ngữ C++. Đây là một phần mềm nhỏ, miễn phí
và thích hợp cho việc học tập. Thực ra Code∷Blocks chỉ đơn thuần là một trình
soạn thảo, tuy nhiên nó tương tác tốt với trình dịch GNU G++ và trình gỡ rối GNU
GDB đính kèm cùng với bộ cài đặt Code∷Blocks.
Bài học này hướng dẫn các bạn cài đặt Code∷Blocks và đặt một số thiết lập phù
hợp trên hệ điều hành Windows. Hiện tại là bản Code∷Blocks phiên bản 17.12, ở
những phiên bản khác, giao diện có thể khác chút ít.
Nếu các bạn đã quen dùng một IDE khác, cũng có thể sử dụng chính IDE đó để viết
chương trình, tuy nhiên cần cấu hình trình dịch theo chuẩn ISO C++14 cho tương
thích với các nội dung sẽ trình bày về sau.
3.2. Cài đặt
Các bạn có thể tải về bộ cài đặt của Code∷Blocks từ trang web:
http://www.codeblocks.org/
Lưu ý tải về bộ cài đặt có đi kèm MinGW, MinGW chứa trình dịch GNU GCC và trình
gỡ rối GNU GDB. Sau đó cài đặt đầy đủ vào máy tính theo hướng dẫn trong phần
mềm.

12
Lê Minh Hoàng
Chương 3
Cài đặt Code::Blocks

3.2.1. Chọn trình dịch


Ở lần chạy đầu tiên, Code∷Blocks tự
động phát hiện các trình dịch C++ có
sẵn trong máy.
Chọn trình dịch GNU GCC làm trình
dịch mặc định (Set as default)

3.2.2. Thiết lập cho trình soạn thảo (Settings/Editor)


Chọn font chữ soạn thảo trong bảng
Editor settings
Nên chọn font chữ có độ rộng cố định
(fixed-width) như font consolas hay
courier new. Cỡ chữ tùy thuộc vào
kích thước màn hình sao cho rõ ràng.
Cỡ chữ có thể thay đổi dễ dàng khi
soạn thảo (giữ phím Ctrl và lăn bánh
xe của con chuột).

Trong bảng Encoding settings, chọn


kiểu mã hóa UTF-8 làm mặc định (as
default encoding).
Việc này để dễ dàng viết các chú thích
tiếng Việt có dấu.

13
Ngôn ngữ lập trình C++
Phần I
GIỚI THIỆU C++

Trong phần SpellChecker, bỏ tất cả


chức năng kiểm tra chính tả.
(Khi lập trình chỉ cần trình dịch báo
lỗi cú pháp của ngôn ngữ lập trình,
không cần kiểm tra chính tả theo
ngôn ngữ tự nhiên)

3.2.3. Thiết lập cho trình dịch (Settings/Compiler)


Trong bảng Compiler Flags, đánh dấu
chọn:
-std=c++14: Mã nguồn sẽ được viết
theo chuẩn C++ ISO Standard 2014
-static: Để mã biên dịch có thể chạy
độc lập không phụ thuộc vào các thư
viện của trình biên dịch nữa. Chương
trình do đó có thể thực hiện ngay cả
trên máy không có trình biên dịch

Trong bảng Linker settings, phần


Other linker options, đặt lại kích
thước bộ nhớ stack. Bộ nhớ stack là
nơi dùng để lưu các biến địa phương
và các tham số hàm. Mặc định, trình
biên dịch dành bộ nhớ stack khá nhỏ
cho mã khả thi (khoảng ~1MB)
Ta có thể nới rộng bộ nhớ stack để
tiện viết các hàm đệ quy hay những
hàm có biến địa phương lớn. Như ví
dụ này, bộ nhớ stack được đặt thành 60 triệu byte bằng tham số dịch:
-Wl,--stack,60000000

14
Lê Minh Hoàng
Chương 3
Cài đặt Code::Blocks

3.2.4. Thiết lập cho trình gỡ rối (Settings/Debugger)


Mở GCB/CDB debugger, mục Default.
Điền vào Executable path đường dẫn
tới file gdb32.exe. Thường file này
nằm ở thư mục con MinGW\bin trong
thư mục cài đặt Code∷Blocks
Bỏ chọn chỗ Disable startup scripts

3.3. Viết và chạy chương trình


3.3.1. Tạo dự án phần mềm
Dự án phần mềm có thể chứa nhiều file:
File mã nguồn (Source files)
File tiêu đề (Header files)
File tài nguyên (Resource files)

Khi dịch dự án, toàn bộ các file trong dự án sẽ được dịch và liên kết với nhau thành
một phần mềm. Trong các chương trình đơn giản, dự án chỉ chứa một file mã
nguồn, vì vậy để viết những chương trình nhỏ trong học tập, ta chỉ cần tạo ra một
dự án duy nhất, sau đó có thể thay đổi file mã nguồn của nó để làm một chương
trình khác. Khi làm phần mềm lớn, mỗi phần mềm cần tương ứng với một dự án
của riêng nó cho tiện quản lý các files.
Để tạo ra một dự án phần mềm
(project): Chọn menu File / New /
Project… và chọn tiếp Empty project.

15
Ngôn ngữ lập trình C++
Phần I
GIỚI THIỆU C++

Chọn tên dự án và nơi đặt dự án:


Tên project (Project title), thư mục
chứa project (Folder to create project
in), tên file dự án (Project filename) và
đường dẫn tới file dự án.
Trong tên project, tên file và đường
dẫn chỉ nên sử dụng các chữ cái
tiếng Anh và chữ số. Chương trình
dịch GCC rất hay gặp sự cố khi tên file,
tên đường dẫn có dấu cách hoặc các
ký tự không phải chữ cái/chữ số.
Project nên đặt trực tiếp trong một thư mục (tạm gọi là thư mục làm việc). Nơi
chúng ta chứa file mô tả dự án phần mềm (.cbp), các file dữ liệu vào, file kết quả
ra. Đường dẫn và tên thư mục làm việc nên đặt ngắn gọn cho tiện truy cập.
Chọn trình dịch là:
GNU GCC Compiler
Xóa trắng mục Output dir và Objects
output dir
Để đơn giản ta đặt mã biên dịch thẳng
vào trong thư mục chứa dự án, kể cả
khi ta dịch dự án ở chế độ gỡ rối
(debug) hay phát hành (release).

3.3.2. Tạo file mã nguồn đưa vào dự án


Trên giao diện của Code∷Blocks với dự án vừa tạo, ta cần tạo file mã nguồn để viết
chương trình vào đó. (Menu File/New/File… chọn C/C++ Source)

16
Lê Minh Hoàng
Chương 3
Cài đặt Code::Blocks

Chọn ngôn ngữ lập trình là C++.

Chọn đường dẫn và tên file mã nguồn.


Tên đường dẫn và file mã nguồn chỉ
nên dùng các chữ cái tiếng Anh và
chữ số. Chương trình dịch GCC rất
hay gặp sự cố khi tên file, tên đường
dẫn có dấu cách hoặc các ký tự không
phải chữ cái/chữ số.
File mã nguồn nền đặt vào một thư
mục để lưu trữ lại, đường dẫn và tên
thư mục nên đặt ngắn gọn cho tiện
truy cập.
Đánh dấu chọn cả hai ô Build Targets: Debug và Release. Mã nguồn này sẽ được
đưa vào dự án và được dịch kể cả khi ta dịch dự án trong chế độ gỡ rối (debug) hay
phát hành (release).
Trong ví dụ cụ thể này, ta chọn file mã nguồn là Hello.cpp đặt trong thư mục
D:\Work\Source.

17
Ngôn ngữ lập trình C++
Phần I
GIỚI THIỆU C++

3.3.3. Viết chương trình, dịch và chạy


Soạn thảo:
Bây giờ trong dự án ABC đã có file mã
nguồn Hello.cpp, ta bắt đầu viết
chương trình vào trong file mã nguồn.
Chẳng hạn ta viết nguyên mã của 
Chương trình 1 
Viết xong ta ghi lại mã nguồn (Bấm
Ctrl+S hoặc chọn menu File/Save
file). Việc ghi lại mã nguồn nên được
thực hiện thường xuyên như một thói
quen phòng trường hợp mất mã nguồn do các sự cố treo máy.

Biên dịch:
Để dịch chương trình, ta bấm Ctrl+F9
(hoặc vào menu Build/Build).
Thao tác này thực hiện kiểm tra lỗi cú
pháp:
Nếu có lỗi cú pháp, trong cửa sổ
Build messages sẽ hiện mô tả về
lỗi, ta cần sửa và dịch lại.
Nếu không có lỗi cú pháp, chương
trình dịch sẽ sinh mã máy tương
ứng, trong cửa số Build log sẽ hiện thông báo dịch thành công. Mã khả thi đã
được sinh ra (trong trường hợp này là file ABC.exe nằm trong thư mục chứa
dự án: D:\Work)
Chú ý là mỗi khi có sự sửa đổi mã nguồn, ta cần dịch lại để kiểm tra lỗi cú pháp
cũng như sinh ra mã máy cập nhật với mã nguồn mới.

18
Lê Minh Hoàng
Chương 3
Cài đặt Code::Blocks

Chạy:
Để chạy chương trình, ta bấm
Ctrl+F10 (hoặc vào menu Build/ Run)
Cũng có thể bấm F9 (hoặc vào menu
Build/ Build and run), chức năng này kiêm luôn 2 công đoạn: Dịch và chạy.
Sau khi chương trình thực hiện xong, nếu thấy thông báo “Process return 0” tức là
chương trình kết thúc bình thường, không có lỗi (mã lỗi trả về cho hệ điều hành là
0). Nếu mã lỗi trả về khác 0, tức là chương trình có lỗi lúc chạy và bị kết thúc bất
bình thường.
Chương trình chạy xong, ta bấm một phím bất kỳ để kết thúc chương trình và quay
về màn hình Code∷Blocks. Chương trình cần phải kết thúc thì chúng ta mới có thể
chạy lại hoặc viết chương trình mới.

3.3.4. Viết một chương trình mới


Để viết một chương trình mới, ta có
thể tạo một dự án mới. Tuy nhiên
cũng có thể dùng một dự án cũ và chỉ
thay đổi file mã nguồn cho nhanh.
Bấm nút chuột phải vào tên file trong
dự án (cửa sổ Management/
Projects), chọn Remove file from
project. Cách khác là vào menu
Project rồi chọn Remove files… Ta bắt
đầu lại với một dự án rỗng

Lặp lại quy trình tạo file mã nguồn


mới, soạn thảo chương trình, dịch và
chạy… Chẳng hạn ta viết chương trình
Hello2.cpp với mã nguồn như trong
 Chương trình 2 

19
Ngôn ngữ lập trình C++
Phần I
GIỚI THIỆU C++

3.3.5. Mở một file mã nguồn cũ


Đôi khi chúng ta cần mở một file mã
nguồn đã viết từ trước để nạp vào dự
án nhằm sửa đổi, nâng cấp. Việc đầu
tiên cũng là phải làm rỗng dự án: Bấm
nút chuột phải vào tên file trong dự
án, chọn Remove file from project.

Tiếp theo, bấm nút chuột phải vào tên


dự án và chọn Add files, sau đó chọn
file mã nguồn đã có sẵn để nạp vào dự
án. (Cách khác là vào menu
Project/Add files…)
Chú ý rằng file mã nguồn nạp vào dự
án cần có một sự thay đổi nào đó để
báo cho IDE biết rằng mã nguồn đã
thay đổi, cần dịch lại để sinh mã máy
mới tương ứng. (Phiên bản hiện tại
của Code∷Blocks không cho rằng cần phải dịch lại khi thay đổi file mã nguồn). Cách
đơn giản nhất để đưa vào một sự thay đổi trong file mã nguồn là viết một dấu cách
vào cuối một dòng nào đó hoặc viết ra một ký tự bất kỳ rồi xóa đi.
Các bạn có thể cho rằng việc tạo ra một dự án cần nhiều thao tác và tốn thời gian.
Hơn nữa Code∷Blocks có thể chạy trực tiếp file mã nguồn mà không cần qua dự
án, vậy tại sao phải làm dự án?
Với những chương trình nhỏ trong học tập, chúng ta chỉ cần bỏ công sức một lần
để làm một dự án, sau đó chỉ việc viết mã nguồn hoặc đưa file mã nguồn đã có sẵn
vào dự án mà thôi.
Việc chạy mã nguồn trong dự án cho phép chúng ta sử dụng các chức năng của
trình gỡ rối GDB. Sau này khi viết những chương trình dài, các bạn sẽ thấy rằng
công sức soạn thảo một chương trình ít hơn rất nhiều so với công sức bỏ ra để sửa

20
Lê Minh Hoàng
Chương 3
Cài đặt Code::Blocks

lỗi cho nó. Việc sử dụng thành thạo các chức năng của trình gỡ rối kết hợp với các
cách thức gỡ rối thủ công sẽ làm giảm chi phí về thời gian và công sức hoàn thiện
chương trình.

Bài tập 3-1


Bài tập trong phần này chỉ là yêu cầu các bạn cài đặt Code∷Blocks, viết và chạy một
chương trình, biết đọc và sửa các thông báo lỗi cú pháp và tìm hiểu về các phím
soạn thảo. Việc này sẽ giúp ích cho các bạn soạn thảo chính xác và nhanh hơn trong
các chương trình phức tạp về sau.

21
Ngôn ngữ lập trình C++
Phần II.
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Phần này trình bày những thành tố cơ bản của ngôn ngữ C++, bao gồm các khái
niệm tên, biến, hằng, các toán tử, lệnh nhập xuất dữ liệu …
Định kiểu mạnh là một trong những đặc trưng của các ngôn ngữ lập trình và trình
biên dịch tốc độ cao, bao gồm cả C++. Định kiểu mạnh bao gồm những quy tắc
nghiêm ngặt khi khai báo, gán giá trị và viết biểu thức, điều này giúp cho trình
biên dịch sinh mã tính toán tối ưu nhất có thể.
Nội dung của phần này cung cấp cho các bạn những hiểu biết về kiểu dữ liệu, các
phép toán trên dữ liệu và cách tổ chức dữ liệu trong bộ nhớ của C++, nắm rõ những
kiến thức đó sẽ giúp bạn viết chương trình hiệu quả hơn, ít lỗi hơn và chuẩn hơn.

23
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Chương 4. Biến và kiểu


Giả sử bạn được yêu cầu nhớ hai số nguyên, số thứ nhất bằng 6, số thứ hai bằng 7.
Sau đó bạn được yêu cầu tăng số thứ nhất lên 3 đơn vị rồi đưa ra hiệu: số thứ nhất
trừ số thứ hai.
Hãy hình dung chi tiết những gì được bạn xử lý trong đầu: Bạn cần nhớ hai số, gọi
số thứ nhất là 𝑎 và số thứ hai là 𝑏, ban đầu 𝑎 = 6 và 𝑏 = 7. Khi được yêu cầu tăng
số thứ nhất lên 3 đơn vị, bạn cần nhớ lại số thứ nhất 𝑎 = 9, sau đó đưa ra hiệu 𝑎 −
𝑏 = 9 − 7 = 2.
Vấn đề trên nếu giao cho máy tính sẽ được thực hiện tương tự, máy tính cần hai
vùng nhớ để nhớ hai số 𝑎, 𝑏. Sau đó một tiến trình tương tự sẽ được mô tả bằng
C++:
int a, b;
a = 6;
b = 7;
a = a + 3;
cout << a – b;
Đây chỉ là một ví dụ đơn giản, trong đó ta chỉ cần nhớ hai số nguyên, nhưng máy
tính có thể lưu trữ và xử lý hàng triệu các vùng nhớ như vậy một cách tinh vi.
Biến (variable) là một khái niệm quan trọng trong các ngôn ngữ lập trình bậc cao.
Nó là một vùng nhớ được gán tên và dùng để chứa một giá trị thuộc kiểu dữ liệu
xác định trước, giá trị này có thể thay đổi trong quá trình thực thi.
Mỗi biến cần có một tên để xác định nó và phân biệt nó với những biến khác. Ví dụ
trong đoạn mã trên, tên biến là a và b, chúng ta có thể đặt cho chúng bất kỳ tên nào,
miễn là tên đó hợp lệ theo tiêu chuẩn của C++.
4.1. Tên và quy tắc đặt tên
Tên (hay định danh – identifier) là một dãy các ký tự thỏa mãn các điều kiện sau:
Bắt đầu bằng một chữ cái: a…z, A…Z, _
Các ký tự tiếp theo có thể là chữ cái hoặc chữ số: a…z, A…Z, 0…9, _
Không được trùng với từ khóa
Các chữ cái ở đây là ở bảng mã ASCII, tức là chỉ gồm các chữ cái trong tiếng Anh,
dấu gạch nối dưới “_” cũng được coi là chữ cái. Lưu ý rằng C++ phân biệt chữ hoa
và chữ thường, chẳng hạn Sunday và SunDay được coi là 2 tên khác nhau. Các tên
có hiệu lực trong cùng một phạm vi của chương trình không được trùng nhau.
Không có ràng buộc cụ thể nào về độ dài tên trong chuẩn C++, nhiều trình biên dịch
cho phép tên có độ dài lên tới hàng trăm, hàng ngàn ký tự, riêng trình biên dịch
G++ mà ta sử dụng, độ dài tên không bị giới hạn. Mặc dù vậy, một số trình biên dịch

24
Lê Minh Hoàng
Chương 4
Biến và kiểu

C++ trên các thiết bị khác chỉ dựa vào 31 ký tự đầu tiên để so sánh tên, vì vậy không
nên đặt tên dài hơn 31 ký tự.
Từ khóa là một số từ đặc biệt, chúng được sử dụng vào mục đích riêng của ngôn
ngữ: trong các cấu trúc điều khiển, mô tả dữ liệu cấu trúc, … Tên do lập trình viên
đặt không được trùng với các từ khóa này. Chẳng hạn các từ khóa using,
namespace, if, else, switch, while, do, for, struct, class, void, return, … Danh mục
các từ khóa rất nhiều, ta có thể tham khảo ở mục 4.4.1.
4.2. Các kiểu dữ liệu cơ sở
Giá trị của các biến được lưu trữ ở đâu trong bộ nhớ là do chương trình dịch bố trí.
Trong ngôn ngữ lập trình cấp thấp, ta phải tự bố trí và biết địa chỉ vùng nhớ này
mới có thể đặt dữ liệu vào đó. Trong ngôn ngữ lập trình bậc cao, ta không cần biết
vùng nhớ này nằm ở đâu, gọi tên của biến tức là truy cập vùng nhớ tương ứng.
Tuy nhiên, dữ liệu lưu trong bộ nhớ chỉ là các bit (chữ số nhị phân – BInary digiT),
điều duy nhất chương trình cần phải nhận thức được là loại dữ liệu lưu trữ tại một
vùng nhớ để có thể diễn giải giá trị đó và đưa ra những phép xử lý phù hợp với dữ
liệu. Việc này đưa đến khái niệm về kiểu dữ liệu (data type) của biến.
Chẳng hạn với phép chia biến 𝑎 cho biến 𝑏 (𝑎/𝑏), nếu 𝑎 và 𝑏 là hai biến thuộc kiểu
số nguyên, phép chia này sẽ được diễn giải thành phép chia lấy phần nguyên
(17/2 = 8), còn nếu 𝑎 và 𝑏 là hai biến thuộc kiểu số thực, phép chia này được diễn
giải thành phép chia lấy kết quả số thực (17/2 = 8.5).
Các kiểu dữ liệu cơ sở được ngôn ngữ C++ hỗ trợ trực tiếp bao gồm 4 loại:
Kiểu số nguyên: Có nhiều kiểu dữ liệu dùng để lưu trữ các số nguyên, mỗi kiểu
có thể biểu diễn các số nguyên trong một phạm vi nào đó. Kiểu có kích thước
càng lớn thì phạm vi biểu diễn càng rộng, có kiểu biểu diễn được số âm, có kiểu
chỉ biểu diễn các số không âm.
Kiểu số thực: Cũng có nhiều kiểu dữ liệu biểu diễn số thực, chúng được đặc
trưng bởi kích thước, độ chính xác cũng như phạm vi biểu diễn.
Kiểu ký tự: Những kiểu dữ liệu này để biểu diễn ký tự tùy theo bảng mã ký tự
được sử dụng, chẳng hạn các chữ cái, chữ số, các ký tự như ‘$’, ‘&’, ‘#’,…, Kiểu
ký tự phổ biến nhất mà chúng ta dùng để học tập là kiểu char, biểu diễn các ký
tự trong bảng mã ASCII (American Standard Code for Information Interchange).
Kiểu logic boolean: Trong C++, kiểu này có tên là bool, dùng để biểu diễn hai
giá trị chân lý: true (Đúng) và false (Sai).
Để thuận lợi cho việc viết và ước lượng giá trị hằng số. Ta dùng dấu chấm “.” để
phân tách phần nguyên và phần thập phân, một vài trường hợp dùng dấu phẩy “,”

25
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

để phân tách các hàng nghìn. Điều này không đúng với ký pháp của Việt Nam*
nhưng lại khớp với ngôn ngữ lập trình, tránh sự nhập nhằng không cần thiết.
Ví dụ: 𝜋 ≈ 3.141593, 1 tỉ = 1,000,000,000
Trên các máy tính hiện nay và trong rất nhiều tài liệu, kể cả trong tài liệu này, ta
quy ước 1 byte tương ứng với một dãy 8 bit. Mặc dù khái niệm bit và byte có tính
độc lập tương đối:
bit: Là đơn vị nhỏ nhất của thông tin, biểu diễn 2 trạng thái, ký hiệu 0 và 1
byte: Là đơn vị truy cập thông tin số hóa, tức là dù ta chỉ có nhu cầu truy cập
một bit riêng lẻ, thiết bị lưu trữ (bộ nhớ, đĩa, …) vẫn phải truy cập cả 1 byte
chứa bit đó. Byte là một dãy bit liền nhau, độ dài dãy bit tùy theo máy tính. Tuy
vậy, hầu như tất cả các máy tính hiện nay đều dùng byte là dãy gồm 8 bit nên
quan niệm 1 byte = 8 bit trở thành tiêu chuẩn không chính thức. Trong các kiểu
dữ liệu ta trình bày tiếp theo, kiểu dữ liệu nào cũng có kích thước là một số chia
hết cho 8, chính là do tiêu chuẩn không chính thức nói trên.
4.2.1. Các kiểu số nguyên
C++ có rất nhiều kiểu số nguyên, kiểu số nguyên phổ biến nhất có tên là int, trong
chương trình dịch với chuẩn C++14 mà ta sử dụng, mỗi biến kiểu int chiếm 32 bit
và có thể chứa giá trị số nguyên thuộc phạm vi [−231 ; 231 − 1]. Những kiểu số
nguyên khác hay được sử dụng là unsigned, long long, unsigned long long.
Kiểu int và long long thuộc kiểu số nguyên có dấu (biểu diễn được cả số âm và số
dương – signed), kiểu unsigned và unsigned long long thuộc kiểu số nguyên không
dấu (chỉ biểu diễn những số nguyên không âm - unsigned)
Tên kiểu và phạm vi biểu diễn Kích thước
int
-231…231-1 32 bit
-2,147,483,648 … 2,147,483,647
unsigned
0 … 232-1 32 bit
0 … 4,294,967,295
long long
-263 … 263-1 64 bit
-9,223,372,036,854,775,808 … 9,223,372,036,854,775 807
unsigned long long
0 … 264-1 64 bit
0 … 18,446,744,073,709,551,615

*Ở Việt Nam dùng dấu phẩy để phân tách phần nguyên và phần thập phân, dùng dấu chấm hoặc
dấu cách để phân tách hàng nghìn

26
Lê Minh Hoàng
Chương 4
Biến và kiểu

Những mô tả kiểu trong bảng trên chỉ là tương ứng với chương trình dịch sử dụng
trong cuốn sách này, không phải chuẩn C++ ISO Standard. Chi tiết về các chuẩn số
nguyên theo C++ ISO Standard có thể tra cứu trong mục 4.4.2.
4.2.2. Các kiểu số thực
Số thực trong C++ biểu diễn dưới dạng dấu chấm động (floating-point). Cách biểu
diễn này tách ô nhớ số thực ra làm 2 phần: Phần thứ nhất biểu diễn một số nguyên
𝑎 gọi là phần chữ số có nghĩa, phần thứ hai biểu diễn một số nguyên 𝑏 gọi là phần
mũ. Khi đó giá trị của số thực là 𝑎 × 10𝑏 (ký hiệu 𝑎e𝑏).
Chẳng hạn nếu 𝑎 = 123456789 và 𝑏 = −5 thì giá trị số thực là: 1234.56789
Một đặc trưng quan trọng đối với kiểu số thực là số chữ số có nghĩa khi thực hiện
tính toán trên kiểu số thực đó, số chữ số có nghĩa này được đặc trưng bởi phần bit
biểu diễn số nguyên 𝑎. Chẳng hạn một kiểu số thực có 16 chữ số có nghĩa thì khi
tính toán với những số có giá trị tuyệt đối quá lớn hoặc quá nhỏ, vẫn chỉ có 16 chữ
số đầu tiên trong biểu diễn thập phân là “tin được” mà thôi. Ngoài đặc trưng về số
chữ số có nghĩa, phạm vi có thể biểu diễn của các kiểu số thực rất rộng nên có thể
không cần quan tâm.
C++ cung cấp ba kiểu số thực: float, double và long double. Số chữ số có nghĩa
của kiểu double không ít hơn so với float và số chữ số có nghĩa của kiểu long double
không ít hơn số chữ số có nghĩa của long double. Chi tiết cụ thể về ba kiểu dữ liệu
này trong G++ có thể tra cứu trong mục 4.4.3.
4.2.3. Kiểu ký tự
Kiểu ký tự có thể xử lý hiệu quả nhất là kiểu char. Mỗi biến kiểu char chiếm đúng
1 byte (trên máy tính hiện nay là 8 bit). Kiểu ký tự dùng để biểu diễn dữ liệu gồm
1 ký tự riêng lẻ trong một bảng mã: Chữ cái, chữ số, các dấu… Chi tiết về các kiểu
ký tự khác có thể tra cứu trong mục 4.4.4
4.2.4. Kiểu logic boolean
Kiểu logic boolean trong C++ có tên là bool, dùng để lưu trữ và tính toán các biểu
thức logic mệnh đề. Kiểu này chỉ có hai giá trị: false (sai) và true (đúng).
Không có quy định tiêu chuẩn về cách thức lưu trữ kiểu bool, tuy nhiên trong hầu
hết các chương trình dịch, kiểu boolean chiếm đúng 1 byte (8 bit).
4.2.5. Các kiểu dữ liệu đặc biệt
Kiểu void là kiểu có miền giá trị rỗng. Kiểu này được dùng trong vài trường hợp,
chẳng hạn hàm trả về kiểu void tức là hàm không cần trả về bất cứ giá trị nào.
Kiểu std∷nullptr_t hay decltype(nullptr) là kiểu dữ liệu đặc biệt dành riêng cho
con trỏ nullptr.

27
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

4.3. Khai báo biến


4.3.1. Cú pháp khai báo biến
C++ là ngôn ngữ định kiểu mạnh, mọi biến cần phải khai báo trước khi sử dụng.
Điều này để trình biên dịch biết được kích thước của biến cũng như cách diễn giải
giá trị của biến. Cú pháp khai báo biến của C++ rất đơn giản: Ta viết tên kiểu, theo
sau là dấu cách, rồi các tên biến, kết thúc bằng dấu chấm phẩy “;”. Ví dụ:
int i, j, k; //i, j, k là các biến số nguyên
double x, y, z; //x, y, z là các biến số thực
char ch; //ch là biến kiểu ký tự
bool b; //b là một biến kiểu logic boolean
4.3.2. Khởi tạo biến
Biến ngay sau khi khai báo có thể được nhận ngay một giá trị gọi là giá trị khởi tạo
cho nó. Có 3 cách khởi tạo giá trị biến:
Cách 1: Cách phổ biến nhất là viết dấu “=” và giá trị biến ngay sau tên biến. Ví dụ:
int i = 0, j = 1, k = 2;
Cách 2: Cách này thường dùng để khởi tạo đối tượng qua phương thức dựng
(constructor), tuy nhiên cũng có thể dùng để khởi tạo giá trị biến thuộc những kiểu
dữ liệu cơ sở. Viết giá trị khởi tạo trong cặp dấu ngoặc đơn “(…)” ngay sau tên biến.
Ví dụ:
int i(0), j(1), k(2);
Cách 3: Cách này thường được dùng để khởi tạo các trường của đối tượng, cũng có
thể dùng để khởi tạo giá trị biến thuộc những kiểu dữ liệu cơ sở. Viết giá trị khởi
tạo trong cặp dấu ngoặc nhọn “{…}” ngay sau tên biến. Ví dụ:
int i{0}, j{1}, k{2};
Vấn đề khởi tạo biến đối với các kiểu dữ liệu phức hợp khá phức tạp. Ta sẽ trình
bày rõ hơn trong phần nói về lập trình hướng đối tượng.
4.3.3. Tự động phát hiện kiểu
Khi một biến được khởi tạo, chương trình dịch có thể tự động phát hiện kiểu của
biến căn cứ vào giá trị khởi tạo. Nếu biến có giá trị khởi tạo, có thể khai báo biến
đó bằng kiểu auto. Ví dụ:
int x = 0;
auto y = x; //Tương đương với int y = x;
Ta cũng có thể yêu cầu trình dịch nội suy kiểu của biến qua một biến khác hoặc
một biểu thức, decltype(biểu thức) là kiểu mà trình dịch suy ra từ giá trị biểu thức
đó, ví dụ:
int x;
decltype(x) y; //tương đương với int y;

28
Lê Minh Hoàng
Chương 4
Biến và kiểu

Cách khai báo decltype có ích khi ta có nhiều biến bắt buộc phải cùng kiểu, khi đó
như trong ví dụ trên, chỉ cần sửa kiểu của biến 𝑥 thành double chẳng hạn thì kiểu
của biến 𝑦 cũng trở thành double.
4.3.4. Kiểu xâu ký tự
Trong chương trình Hello.cpp, trong lệnh:
cout << "Xin chao!";
"Xin chao!" thực ra là một xâu gồm 9 ký tự (tính cả dấu cách và dấu chấm than).
Xâu ký tự là một dãy các ký tự liên tiếp, thực ra đây là một kiểu dữ liệu phức hợp
mà ta sẽ trình bày kỹ hơn sau này trong một chương riêng nói về kiểu string.
4.4. Tra cứu 
4.4.1. Danh mục các từ khóa
Danh mục từ khóa (keywords hay reserved words) được định nghĩa khác nhau
trong mỗi chuẩn C++. Tất cả những từ sau đều phải coi là từ khóa dù trong chuẩn
C++14 ta dùng, có thể nó chưa được coi là từ khóa. Điều này để hạn chế những
xung đột khi nâng cấp chương trình lên những chuẩn C++ mới hơn:
alignas, alignof, and, and_eq, asm, atomic_cancel, atomic_commit,
atomic_noexcept, auto, bitand, bitor, bool, break, case, catch, char, char8_t,
char16_t, char32_t, class, compl, concept, const, consteval, constexpr, const_cast,
continue, co_await, co_return, co_yield, decltype, default, delete, do, double,
dynamic_cast, else, enum, explicit, export, extern, false, float, for, friend, goto, if,
inline, int, long, mutable, namespace, new, noexcept, not, not_eq, nullptr, operator,
or, or_eq, private, protected, public, reflexpr, register, reinterpret_cast, requires,
return, short, signed, sizeof, static, static_assert, static_cast, struct, switch,
synchronized, template, this, thread_local, throw, true, try, typedef, typeid,
typename, union, unsigned, using, virtual, void, volatile, wchar_t, while, xor,
xor_eq.
Nhớ hết danh mục từ khóa là một việc khó, các hệ soạn thảo chuyên dụng cho C++
(như Code∷Blocks) có cơ chế làm nổi bật từ khóa bằng các màu sắc hoặc chữ đậm
để lập trình viên có thể tránh khai báo tên trùng với từ khóa.
4.4.2. Các kiểu số nguyên trong C++
 Các kiểu số nguyên kích thước cố định
Đây là những kiểu số nguyên mà kích thước cũng như phạm vi biểu diễn của chúng
không thay đổi trên tất cả các cấu hình phần cứng, hệ điều hành và chương trình
dịch (nếu chúng được hỗ trợ trên nền tảng đó). Các kiểu số nguyên này đi theo
từng cặp, mỗi cặp gồm 2 kiểu cùng kích thước, một kiểu có dấu (có thể biểu diễn
số âm), một kiểu không dấu (chỉ biểu diễn số không âm).

29
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Tên kiểu Kích thước Số âm Phạm vi biểu diễn


int8_t 8 bit Có INT8_MIN … INT8_MAX
uint8_t 8 bit Không 0 … UINT8_MAX
int16_t 16 bit Có INT16_MIN … INT16_MAX
uint16_t 16 bit Không 0 … UINT16_MAX
int32_t 32 bit Có INT32_MIN … INT32_MAX
uint32_t 32 bit Không 0 … UINT32_MAX
int64_t 64 bit Có INT64_MIN … INT64_MAX
uint64_t 64 bit Không 0 … UINT64_MAX
Các kiểu số nguyên kích thước cố định có thể nhớ như sau: 8 kiểu số nguyên chia
làm 4 cặp, mỗi cặp gồm 2 kiểu: intλ_t và uintλ_t, trong đó λ là số bit cho một biến
kiểu đó, có thể là 8, 16, 32 hoặc 64. Phạm vi biểu diễn của hai kiểu này chứa đúng
2λ giá trị (có tất cả 2λ dãy bit khác nhau độ dài λ).
Kiểu intλ_t có thể biểu diễn số âm, phạm vi biểu diễn từ INTλ_MIN đến
INTλ_MAX. Đây là hai hằng số định nghĩa từ trước: INTλ_MIN = −2λ−1 ;
INTλ_MAX = 2λ−1 − 1
kiểu uintλ_t chỉ biểu diễn các số không âm, phạm vi biểu diễn từ 0 đến
UINTλ_MAX, trong đó UINTλ_MAX bằng 2λ − 1
Ví dụ kiểu int8_t có phạm vi biểu diễn từ −27 tới 27 − 1 (−128 … 127) còn kiểu
uint16_t có phạm vi biểu diễn từ 0 tới 216 − 1 (0 … 65,535).
Hiện tại C++ chỉ có 8 kiểu số nguyên này mà thôi*, tất cả những kiểu số nguyên
khác thực ra được ánh xạ vào một trong 8 kiểu này tùy vào cấu hình phần cứng,
hệ điều hành và chương trình dịch.
 Các kiểu số nguyên phụ thuộc nền tảng lập trình
Ngoài những kiểu số nguyên kích thước cố định, có những kiểu số nguyên được
đặt ra cho phù hợp với nền tảng lập trình (cấu hình phần cứng, hệ điều hành,
chương trình dịch).
Các kiểu số nguyên “nhỏ”: int_leastλ_t / uint_leastλ_t tương ứng là kiểu số
nguyên có dấu / không dấu nhỏ nhất với kích thước không nhỏ hơn λ bit.
Các kiểu số nguyên “nhanh”: int_fastλ_t / uint_fastλ_t tương ứng là kiểu số
nguyên có dấu / không dấu nhanh nhất với kích thước không nhỏ hơn λ bit
Ở đây, λ có thể là 8, 16, 32, hoặc 64.
Về ý nghĩa, các kiểu số nguyên “nhỏ” / “nhanh” tương ứng là kiểu số nguyên tiết
kiệm bộ nhớ nhất / nhanh nhất mà vẫn đủ số bit để biểu diễn phạm vi cần thiết.

*Hiện nay hai kiểu số nguyên kích thước cố định: int128_t và uint128_t đã được đề nghị nhưng
chưa được chuẩn hóa, chúng tồn tại trên chương trình dịch G++ 64 bit nhưng với một tên gọi khác

30
Lê Minh Hoàng
Chương 4
Biến và kiểu

Mặc dù quy ước như vậy, chương trình dịch vì các lý do kỹ thuật khác nhau chưa
chắc đã tuân thủ quy tắc này. Chẳng hạn với phiên bản G++ trên Windows, kiểu
int_leastλ_t cũng như kiểu int_fastλ_t cũng đều được ánh xạ thành kiểu intλ_t. Có
thể do G++ quan niệm rằng kiểu số nguyên càng nhỏ tính cành nhanh, mặc dù trên
thực tế không hẳn như vậy.
Ngoài ra có hai kiểu số nguyên “rộng” nhất:
Kiểu intmax_t là kiểu số nguyên có dấu, kích thước lớn nhất và phạm vi biểu
diễn rộng nhất trong tất cả các kiểu số nguyên có dấu mà nền tảng lập trình có
hỗ trợ. Phạm vi biểu diễn của kiểu này từ INTMAX_MIN đến INTMAX_MAX.
Kiểu uintmax_t là kiểu số nguyên không dấu, kích thước lớn nhất và phạm vi
biểu diễn rộng nhất trong tất cả các kiểu số nguyên không dấu mà nền tảng lập
trình có hỗ trợ. Phạm vi biểu diễn của kiểu này từ 0 đến UINTMAX_MAX.
Phiên bản G++ hiện tại trên Windows ánh xạ intmax_t thành int64_t, ánh xạ
uintmax_t thành uint64_t.
Các kiểu số nguyên kích thước cố định, các kiểu số nguyên phụ thuộc nền tảng lập
trình, cũng như các hằng số như INT16_MIN hay UINT64_MAX được khai báo trong
thư viện cstdint. Ta chỉ cần thêm lệnh tiền xử lý:
#include <cstdint>
vào chương trình là dùng được. Đôi khi thư viện này đã được nạp gián tiếp trong
quá trình nạp thư viện khác, ta vẫn có thể dùng dù quên không nạp thư viện cstdint
(điều này không được khuyến khích).
 Các kiểu số nguyên chuẩn của C++
Gọi là các kiểu số nguyên chuẩn có vẻ không được hợp lý, tất cả các kiểu số nguyên
ta trình bày trước đây đều đã được chuẩn hóa với ý nghĩa và mục đích sử dụng rõ
ràng của từng kiểu. Những kiểu số nguyên nói đến trong mục này thực ra là kế
thừa của một quá trình phát triển không được chuẩn hóa, việc cố gắng giữ lại tính
tương thích với những phiên bản đầu tiên dẫn đến các tên gọi dài dòng, khó nhớ
và thiếu nhất quán. Tuy nhiên chúng lại được sử dụng nhiều nhất để giữ tính tương
thích giữa những phiên bản C++, có thể sử dụng ngay mà không cần nạp bất cứ thư
viện nào.

31
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Tên kiểu Số âm Quy định G++

signed char Có Chính xác 1 byte int8_t

unsigned char Không Kích thước bằng signed char uint8_t

Không nhỏ hơn signed char,


signed short int Có int16_t
Ít nhất 16 bit

unsigned short int Không Kích thước bằng signed short int uint16_t

Không nhỏ hơn signed short int,


signed int Có int32_t
Ít nhất 16 bit

unsigned int Không Kích thước bằng signed int uint32_t

Không nhỏ hơn int,


signed long int Có int32_t
Ít nhất 32 bit

unsigned long int Không Kích thước bằng signed long int uint32_t

Không nhỏ hơn signed long int,


signed long long int Có int64_t
Ít nhất 64 bit

unsigned long long int Không Kích thước bằng signed long long int uint64_t

Cột 1 của bảng trên là tên kiểu đầy đủ, thực ra chỉ cần viết phần in đậm, chẳng
hạn kiểu “signed short int” có thể viết là “signed short”, “short int” hoặc đơn
giản là “short”.
Kiểu char được quy định là chính xác 1 byte, hầu hết các máy tính đều sử dụng
đơn vị 1 byte bằng 8 bit. Tuy có những máy tính sử dụng đơn vị 1 byte bằng 6,
7 hay 9 bit… nhưng những máy này không còn được sử dụng và cũng chưa bao
giờ có trình dịch C++ cho chúng nên ta có thể coi 1 byte luôn được tạo thành từ
8 bit.
Các kiểu cũng chia làm từng cặp, cặp kiểu sau có kích thước lớn hơn, phạm vi
biểu diễn rộng hơn cặp kiểu trước.
Cột cuối cùng là kiểu số nguyên kích thước cố định mà G++ ánh xạ vào.
Có một chú ý là kiểu int (signed int) và kiểu unsigned (unsigned int) luôn được ánh
xạ vào thành cặp kiểu số nguyên mà việc tính toán và lưu trữ thuận lợi nhất trên
máy tính. Chuẩn C++ quy định kích thước hai kiểu này ít nhất 16 bit, hầu hết các
chương trình dịch hiện nay đều dùng kiểu int và unsigned kích thước 32 bit.
int = int32_t: −231 … 231 − 1 (−2,147,483,648 … 2,147,483,647)
unsigned = uint32_t: 0 … 232 − 1 (0 … 4,294,967,295)

32
Lê Minh Hoàng
Chương 4
Biến và kiểu

Mặc dù không có lý do gì để ánh xạ kiểu int trở lại thành int16_t trên các nền tảng
từ nay về sau, để lưu trữ giá trị 1 tỉ, một số lập trình viên vẫn cẩn thận dùng kiểu
long thay vì int.
Tất cả những gì không được quy ước rõ ràng trong chuẩn thì rất có thể bị thay đổi
trong nền tảng lập trình khác. Hiện tại, các kiểu int, long, int32_t là một, nhưng điều
này không được đảm bảo trong mọi thế hệ máy tính, hệ điều hành cũng như
chương trình dịch. Muốn hạn chế rắc rối khi chuyển chương trình sang dịch ở nền
tảng lập trình khác, ta nên coi chúng là những kiểu khác nhau*. Tương tự như vậy,
kiểu int64_t hiện nay cũng là int_least64_t, int_fast64_t, intmax_t, long long, nhưng
chúng phải được coi như những kiểu khác nhau khi lập trình.
Đây là kinh nghiệm khi sử dụng các kiểu số nguyên:
Khảo sát mình viết phần mềm trên nền tảng nào, dùng trình dịch nào, cụ thể
trên môi trường đó các kiểu số nguyên chuẩn kích thước bao nhiêu, phạm vi
biểu diễn thế nào.
Dùng kiểu int và unsigned nếu đáp ứng được yêu cầu.
Dùng kiểu long long và unsigned long long nếu muốn biểu diễn số nguyên
lớn hơn kiểu int và unsigned. Một lựa chọn khác chắc chắn hơn là dùng
intmax_t và uintmax_t.
Khi cần tiết kiệm bộ nhớ hay yêu cầu số nguyên phải có số bit định sẵn (chẳng
hạn các vấn đề xử lý bit), dùng các kiểu số nguyên kích thước cố định mà vẫn
đáp ứng được phạm vi cần biểu diễn.
Những kiểu số nguyên còn lại, khi cần ta có thể tra cứu tài liệu về cách dùng.
 Nguyên lý biểu diễn bù 2 
Mọi thông tin lưu trên máy tính đều phải được chuyển hóa thành các bit (BInary
digiT), kiểu số nguyên int8_t và uint8_t đều chiếm 8 bit, nhưng miền giá trị biểu
diễn của hai kiểu này khác nhau, tức là sẽ có hai giá trị khác nhau trên hai kiểu
được biểu diễn bởi cùng một dãy bit.

*Khoảng những năm 1980 - 1990, với những máy tính cũ, hệ điều hành DOS, chương trình dịch
Turbo C, kiểu int được ánh xạ thành int16_t. Hầu hết các chương trình dịch hiện nay đều ánh xạ int
thành int32_t. Cũng có thể trong tương lai, kiểu int được ánh xạ thành int64_t. Vì vậy ta chỉ được
quan niệm int là kiểu số nguyên có dấu mà máy có thể tính toán và lưu trữ nhanh nhất (bằng kích
thước các thanh ghi tính toán của CPU). Kích thước mỗi biến int, phạm vi biểu diễn của kiểu int đều
không được định chuẩn.
Mặc dù vậy, theo xu hướng kích thước thanh ghi của CPU ngày càng lớn lên chứ không nhỏ đi qua
các thế hệ, ta có thể coi kiểu int bao giờ cũng là kiểu số nguyên có dấu kích thước ít nhất là 32 bit.

33
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Cách biểu diễn số nguyên của C++ tuân theo nguyên lý bù 2 (Two’s complement)*:
Một số nguyên 𝜆 bit được biểu diễn bằng dãy bit, đánh số từ bit 0 (bit đơn vị) tới
bit thứ 𝜆 − 1 (bit cao nhất). Trong tài liệu, các bit được viết từ phải qua trái theo
thứ tự đó. Ví dụ một giá trị trong kiểu số nguyên 8 bit (𝜆 = 8):

Số hiệu: 7 6 5 4 3 2 1 0
bit: 1 1 0 0 1 1 0 1

Hình 4-1. Biểu diễn số nguyên 8 bit

Nếu kiểu số nguyên này là kiểu số nguyên không dấu, dãy bit chính là biểu diễn nhị
phân của giá trị số: Lấy bit đơn vị nhân với 20 , bit số hiệu 1 nhân với 21 , …, bit số
hiệu 𝜆 − 1 nhân với 2𝜆 − 1 rồi cộng dồn kết quả lại. Ví dụ, dãy bit trong Hình 4-1
biểu diễn giá trị 205 với kiểu uint8_t:
1.20 + 0.21 + 1.22 + 1.23 + 0.24 + 0.25 + 1.26 + 1.27 = 205
Công thức tổng quát: dãy bit 𝑏𝜆−1 𝑏𝜆−2 … 𝑏1 𝑏0 biểu diễn một số nguyên không dấu
giá trị bằng:
𝜆−1

∑ 𝑏𝑖 . 2𝑖
𝑖=0

Đối với kiểu số nguyên có dấu, công thức tính hơi khác một chút: Giá trị bit cao
nhất không phải nhân với 2𝜆−1 mà nhân với −2𝜆−1. Tức là dãy bit 𝑏𝜆−1 𝑏𝜆−2 … 𝑏1 𝑏0
biểu diễn một số nguyên có dấu giá trị bằng:
𝜆−2
𝜆−1
−𝑏𝜆−1 . 2 + ∑ 𝑏𝑖 . 2𝑖
𝑖=0

Ví dụ dãy bit ở Hình 4-1 sẽ biểu diễn giá trị -51 trong kiểu int8_t†:
1.20 + 0.21 + 1.22 + 1.23 + 0.24 + 0.25 + 1.26 − 1.27 = −51
Cách tính giá trị trong kiểu không dấu và có dấu chỉ khác nhau tại bit cao nhất. Nếu
bit cao nhất (𝑏𝜆−1 ) bằng 1, giá trị không dấu được cộng vào 2𝜆−1 trong khi giá trị

* Chuẩn C++14 không bắt buộc mọi trình dịch cần biểu diễn số nguyên theo nguyên lý bù 2, tuy
nhiên mọi trình dịch hiện tại đều tuân theo nguyên lý này. Trong chuẩn C++20, nguyên lý bù 2 là
bắt buộc để biểu diễn số nguyên.
† Chú ý rằng giá trị 51 trong hệ nhị phân là 00110011, nhưng giá trị -51 trong máy tính không thể

biểu diễn kiểu -00110011, vì máy không còn thừa bất kỳ một bit nào để lưu thông tin cho dấu “-”.
Biểu diễn nhị phân trong toán học giống với biểu diễn bit trên máy tính chỉ trên các số không âm,
để biểu diễn số âm, máy tính phải dùng nguyên lý bù 2.

34
Lê Minh Hoàng
Chương 4
Biến và kiểu

có dấu bị trừ đi 2𝜆−1 ứng với bit đó. Điều này làm cho giá trị không dấu lớn hơn giá
trị có dấu tương ứng đúng 2𝜆 đơn vị (ví dụ 205 − (−51) = 256 = 28 ).
Nếu bit cao nhất bằng 0, khi đó dãy bit biểu diễn hai giá trị như nhau trong cả
kiểu có dấu và không dấu tương ứng. Giá trị nhỏ nhất mà dãy bit đó biểu diễn
được là: 00
⏟ … 0 = 0, giá trị lớn nhất mà dãy bit đó biểu diễn được là: 0 11
⏟ …1 =
𝜆 bit 0 𝜆−1 bit 1
𝜆−1
2 − 1.
Nếu bit cao nhất bằng 1, giá trị có dấu mà dãy bit đó biểu diễn là một số âm,
nhỏ hơn giá trị không dấu đúng 2𝜆 đơn vị. Đặc trưng của các số âm là có bit cao
nhất bằng 1.
Một số tính chất khác:
Dãy toàn bit 0 biểu diễn giá trị 0 trong cả kiểu số nguyên không dấu và có dấu
Dãy toàn bit 1 biểu diễn giá trị lớn nhất trong kiểu số nguyên không dấu (2𝜆 −
1), giá trị −1 trong kiểu số nguyên có dấu.
Dãy có bit cao nhất là bit 1, còn các bit khác bằng 0: 100 … 0 biểu diễn giá trị
nhỏ nhất trong kiểu số nguyên có dấu (−2𝜆 − 1).
Câu hỏi đặt ra là máy tính làm thế nào có thể tính đúng được phép toán số nguyên
nếu như nó lưu trữ hai số khác nhau bởi cùng một dãy bit. Cụ thể hơn, làm thế nào
máy tính biết được nên diễn giải dãy bit 11001101 thành 205 hay -51 để thực hiện
phép tính cho đúng?
Câu trả lời là máy không cần quan tâm, phép tính sẽ tự đúng. Đó là lợi thế của phép
biểu diễn bù 2.
Ta lấy thêm một giá trị số nữa để làm ví dụ, chẳng hạn giá trị 25, trong kiểu int8_t
hay uint8_t, giá trị này đều có biểu diễn bằng dãy bit: 00011001. Xét phép cộng hai
dãy bit*:
11001101
+
00011001
────────
11100110
Phép tính được thực hiện như các học sinh tiểu học vẫn làm, nhưng trên cơ số 2:
Cộng từ hàng đơn vị lên: Nếu kết quả là 0 hay 1 thì viết luôn xuống, nếu tổng là 2

*Theo chuẩn C++14, khi cộng hai số nguyên int8_t, máy sẽ đổi hai số nguyên đó về kiểu int rồi mới
cộng. Tuy nhiên với kiểu int, dãy bit quá dài (32 bit) nên khó nhẩm, ta giả sử máy cộng trực tiếp hai
số nguyên 8 bit cho dễ trình bày.

35
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

thì viết xuống số 0 rồi nhớ 1 sang cột bên trái, nếu tổng là 3 thì viết xuống số 1 rồi
nhớ 1 sang cột bên trái.
Dãy bit kết quả là 11100110, dãy bit này biểu diễn giá trị 230 trong kiểu uint8_t và
biểu diễn giá trị -26 trong kiểu int8_t. Nếu hai số hạng thuộc kiểu uint8_t, phép tính
này diễn giải thành 205 + 25 = 230; còn nếu hai số hạng thuộc kiểu int8_t, phép
tính này diễn giải thành −51 + 25 = −26.
Như vậy dãy bit kết quả là đúng theo quy ước biểu diễn số nguyên, máy có thể làm
hàng tỉ phép tính như vậy, chỉ xử lý trên dãy bit mà không cần biết số đang xử lý là
âm hay dương, cho đến khi thu được kết quả cuối cùng… Việc in kết quả lại là
chuyện khác, chương trình dịch khi xử lý biểu thức đã nhận diện được biểu thức
được tính theo kiểu dữ liệu nào, nó sẽ sinh mã máy để in ra số 230 hoặc số -26 tùy
theo kiểu dữ liệu tương ứng.
4.4.3. Các kiểu số thực
Các kiểu số thực trong C++ được biểu diễn trong máy theo cách tiện lợi cho bộ
đồng xử lý toán học 80x87 – một thành phần tích hợp với bộ vi xử lý chịu trách
nhiệm xử lý các phép toán số thực. Đây là thông tin cụ thể về các kiểu số thực được
G++ định nghĩa:
Tên kiểu Kích thước Số chữ số có nghĩa Phạm vi biểu diễn giá trị tuyệt đối
float 32 bit 6 1.18 × 10−38 … 3.40 × 1038
double 64 bit 15 2.23 × 10−308 … 1.80 × 1038
long double 96 bit 18 3.36 × 10−4932 … 1.19 × 104932
Kiểu số thực gồm số 0.0 và tất cả các giá trị thực thuộc phạm vi biểu diễn giá trị
tuyệt đối. Phạm vi biểu diễn giá trị tuyệt đối trong bảng trên chỉ là những con số
xấp xỉ. Các kiểu số thực có thể biểu diễn được cả số âm và số dương miễn là giá trị
tuyệt đối của nó bằng 0 hoặc nằm trong phạm vi này.
Các kiểu số thực còn hỗ trợ những giá trị đặc biệt:
Giá trị INFINITY (+∞) và -INFINITY (−∞)
Giá trị -0.0, giá trị này bằng với 0.0, nhưng nó có một vài tác dụng, chẳng
hạn:1.0/0.0 = INFINITY nhưng 1.0/−0.0 = −INFINITY.
Giá trị NAN (Not A Number): Giá trị này không bằng với bất cứ giá trị nào kể cả
chính nó, thông thường đây là kết quả của biểu thức 0.0/0.0
Những giá trị INFINITY và NAN nằm trong thư viện cmath.
4.4.4. Các kiểu ký tự chuẩn
C++ xử lý ký tự như số nguyên. Hai ký tự khác nhau tương ứng với hai số nguyên
khác nhau. Việc lưu trữ ký tự như số nguyên cho phép chúng ta so sánh ký tự, thực
hiện các phép toán +, - để tìm ký tự đứng sau hay đứng trước.

36
Lê Minh Hoàng
Chương 4
Biến và kiểu

Kiểu char là kiểu ký tự xử lý hiệu quả nhất trên máy tính, nó được ánh xạ thành
kiểu số nguyên signed char hay unsigned char tùy thuộc vào nền tảng lập trình
(trên kiến trúc vi xử lý ARM và PowerPC là unsigned char, trên kiến trúc x86, x64
là loại máy chúng ta thường dùng thì lại là signed char). Mặc dù vậy chúng ta phải
coi chúng là những kiểu khác nhau.
Nhắc lại là C++ coi các ký tự như số nguyên, ký tự đó có hình thù (glyph) như thế
nào lại do bảng mã quyết định. Bảng mã đơn giản nhất trong việc học lập trình là
bảng mã chuẩn quốc gia Mỹ để trao đổi thông tin (American Standard Codes for
Information Interchange – ASCII)
…0 …1 …2 …3 …4 …5 …6 …7 …8 …9 …A …B …C …D …E …F
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
0…
NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
1…
DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EN SUB ESC FS GS RS US
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
2… ! " # $ % & ' ( ) * + , - . /
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
3… 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
4… @ A B C D E F G H I J K L M N O
80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
5… P Q R S T U V W X Y Z [ \ ] ^ _
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
6… ` a b c d e f g h i j k l m n o
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
7… p q r s t u v w x y z { | } ~ DEL
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
8 Ç ü é â ä à å ç ê ë è ï î ì Ä Å
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
9… É æ Æ ô ö ò û ù ÿ Ö Ü ø £ Ø × ƒ
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
A… á í ó ú ñ Ñ ª º ¿ ® ¬ ½ ¼ ¡ « »
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
B… ░ ▒ ▓ │ ┤ ╡ ╢ ╖ ╕ ╣ ║ ╗ ╝ ╜ ╛ ┐
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
C… └ ┴ ┬ ├ ─ ┼ ╞ ╟ ╚ ╔ ╩ ╦ ╠ ═ ╬ ╧
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
D… ╨ ╤ ╥ ╙ ╘ ╒ ╓ ╫ ╪ ┘ ┌ █ ▄ ▌ ▐ ▀
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
E…
α ß Γ π Σ σ µ τ Φ Θ Ω δ ∞ φ ε ∩
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
F… ≡ ± ≥ ≤ ⌠ ⌡ ÷ ≈ ° ∙ · √ ⁿ ² ■

Trong bảng mã ASCII, 32 ký tự đầu tiên (mã số từ 0 tới 31) là các ký tự điều khiển,
mặc dù một vài ký tự cũng có hình thù, việc in ra chúng chủ yếu mang vai trò điều
khiển, chẳng hạn nếu in ra ký tự số 9 (Horizontal Tab – HT) ta sẽ được một dấu
tab, ký tự số 10 tương ứng với việc xuống dòng (Line Feed – LF), ký tự số 13 tương
ứng với việc đẩy con trỏ về đầu dòng trên cửa sổ văn bản…
Các ký tự mang mã số từ 32 tới 127 là tập ký tự hay được sử dụng nhất, gần như
bắt buộc phải có trên mọi bàn phím thông thường. Mã số 32 là dấu cách, các mã số

37
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

từ 48 tới 57 tương ứng với các chữ số (0…9), các mã số từ 65 đến 90 ứng với các
chữ cái hoa tiếng Anh (A…Z), các mã số từ 97 tới 122 ứng với các chữ cái thường
tiếng Anh (a…z). Riêng mã số 127 được gán cho phím Delete trên bàn phím, dùng
để nhận phím trên cửa sổ văn bản.
Tập các ký tự mang mã số từ 128 đến 255 là tập mã ASCII mở rộng, chúng bao gồm
các ký tự kẻ khung văn bản, chữ cái tiếng Pháp, tiếng Hy Lạp,…
Trên các máy tính hiện nay, kiểu char chiếm đúng 8 bit, nó có thể biểu diễn 28 =
256 ký tự khác nhau và vì vậy nó có thể biểu diễn trọn vẹn bảng mã ASCII. Tuy
nhiên vì kiểu char được ánh xạ thành signed char, kiểu số nguyên có dấu với phạm
vi biểu diễn từ −27 đến 27 − 1 (-128…127), vì vậy các ký tự mang mã số từ 0 tới
127 tương ứng với chính giá trị số đó, còn các ký tự mang mã số 128 tới 255 tương
ứng với các giá trị số từ −128 tới −1 trong kiểu char. Điều này cần chú ý khi so
sánh ký tự.
Ví dụ: ‘A’ < ‘B’; ‘1’ < ‘2’; nhưng ‘a’ > ‘à’ dù mã của ký tự ‘a’ là 97 còn mã của chữ ‘à’
là 133 (= −123 trong kiểu char).
Ngoài ra, C++ còn hỗ trợ một vài kiểu ký tự khác, những kiểu này dùng trong phần
mềm với giao diện cần bảng mã ký tự lớn hơn:
Kiểu char16_t: Biểu diễn ký tự trong mảng mã UTF-16, nó được lưu trữ và xử
lý như kiểu uint_least16_t.
Kiểu char32_t: Biểu diễn ký tự trong mảng mã UTF-32, nó được lưu trữ và xử
lý như kiểu uint_least32_t.
Kiểu wchar_t: Kiểu để biểu diễn mọi ký tự trong bảng mã mà nền tảng lập trình
chấp nhận, kiểu này cũng được được biểu diễn như một kiểu số nguyên không
dấu.
4.4.5. Kiểu liệt kê
Kiểu liệt kê (enumeration) không phải là một kiểu dữ liệu có sẵn, nó do lập trình
viên định nghĩa nhằm tăng tính trong sáng của chương trình.
Kiểu liệt kê có thể khai báo bằng cú pháp:
enum «Tên kiểu» {«Tên các giá trị cách nhau bởi dấu phẩy»};
Ví dụ ta định nghĩa một kiểu tên là TColor chứa các giá trị màu nhìn thấy trong
quang phổ: Đỏ, Cam, Vàng, Lục, Lam, Chàm, Tím.
1 | enum TColor {Red, Orange, Yellow, Green, Blue, Indigo, Violet};
2 | TColor c = Yellow;
3 | int x = c; //x = 2
Hoàn toàn có thể quy ước màu đỏ (Red) là số 0, màu cam (Orange) là số 1, …, màu
tím (Violet) là số 6, và viết các số nguyên 0…6 thay vì các tên màu như trên. Tuy

38
Lê Minh Hoàng
Chương 4
Biến và kiểu

nhiên việc này bắt lập trình viên phải nhớ thêm một quy ước không cần thiết, tăng
khả năng nhầm lẫn khi lập trình. Các giá trị kiểu liệt kê được gán tên gợi nhớ ý
nghĩa, tránh sai sót khi soạn thảo và ngăn chặn các phép toán không được phép
trên kiểu (chẳng hạn biểu thức 𝑐 + 3 sẽ bị báo lỗi do toán tử + không được định
nghĩa trên kiểu TColor)
Về bản chất, mỗi giá trị trong kiểu liệt kê sẽ được lưu trữ như một số nguyên (tạm
gọi là mã) và hoàn toàn có thể gán cho một biến số nguyên. Nếu không có chỉ định
cụ thể, giá trị đầu tiên của kiểu liệt kê sẽ được gán mã 0, giá trị sau được gán mã
hơn mã của giá trị trước đúng 1 đơn vị, đó là lý do lệnh khởi tạo int 𝑥 = Yellow;
tương đương với int 𝑥 = 2 (Red = 0, Orange = 1, Yellow = 2,…).
Ta cũng có thể quy ước một mã khác cho một giá trị kiểu liệt kê (mã vẫn được gán
tăng dần cho các giá trị liệt kê tiếp theo nếu không được chỉ định), điều này có thể
làm cho hai giá trị liệt kê khác nhau mang mã bằng nhau. Ví dụ:
enum TColor {Red, Orange = 5, Yellow, Blue, Indigo = 6, Violet};
//Red = 0, Orange = 5, Yellow = 6, Blue = 7, Indigo = 6, Violet = 7
Hai giá trị kiểu liệt kê có thể so sánh được qua các toán tử so sánh (mục 6.6), bản
chất là so sánh hai mã tương ứng của chúng. Như ví dụ trên ta có:
Blue == Violet
Red < Yellow
4.4.6. Hiện thông tin về kiểu
Chương trình nhỏ sau đây sẽ in ra các thông tin về kiểu dữ liệu T. Chương trình sử
dụng một số hàm trong thư viện limits
1 | #include <iostream>
2 | #include <limits>
3 | using namespace std;
4 | using T = long double;
5 |
6 | int main ()
7 | {
8 | cout << boolalpha; //In ra true/false thay vì 1/0
9 | //Kích thước 1 biến tính bằng bit
10 | cout << "Size: " << sizeof(T) * 8 << " bits" << '\n';
11 | //Giá trị nhỏ nhất
12 | cout << "Minimum: " << numeric_limits<T>::min() << '\n';
13 | //Giá trị lớn nhất
14 | cout << "Maximum: " << numeric_limits<T>::max() << '\n';
15 | //Có dấu hay không?
16 | cout << "Is signed: " << numeric_limits<T>::is_signed << '\n';
17 | //Số bit biểu diễn phần không dấu
18 | cout << "Non-sign bits: " << numeric_limits<T>::digits << '\n';
19 | //Số chữ số có nghĩa, với số thực
20 | cout << "Significant digits: " << numeric_limits<T>::digits10 << '\n';
21 | //Có giá trị infty không
22 | cout << "Has infinity: " << numeric_limits<T>::has_infinity << '\n';
23 | }
Kết quả in ra:

39
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Size: 96 bits
Minimum: 3.3621e-4932
Maximum: 1.18973e+4932
Is signed: true
Non-sign bits: 64
Significant digits: 18
Has infinity: true
Bằng cách thay đổi kiểu dữ liệu T ở dòng 4, chẳng hạn thay long double bởi
intmax_t, ta sẽ có thông tin về kiểu intmax_t (nhớ nạp thư viện cstdint khi dùng
kiểu này).

Bài tập 4-1


Trong các tên sau, tên nào là không hợp lệ?
Birthday, Too_hot?, __First_Initial, 1stProgram, down.to.earth, see you, OldName,
case, One+Two.
Bài tập 4-2
Để tính toán lãi suất tiết kiệm, kiểu dữ liệu gì bạn có thể sử dụng để tính toán tỉ lệ,
tiền gốc, tiền lãi. Giải thích tại sao.
Bài tập 4-3
Nếu kiểu dữ liệu số nguyên 128 bit trở thành chuẩn, bạn hãy dự đoán miền giá trị
của kiểu int128_t và uint128_t.
Bài tập 4-4
Xét giá trị 𝒞 thuộc một trong các kiểu số nguyên có dấu, giá trị này được biểu diễn
bởi một dãy bit. Chứng minh rằng nếu ta đảo hết các bit (từ 0 thành 1 và từ 1 thành
0) rồi cộng thêm 1 vào dãy bit, ta sẽ được biểu diễn của giá trị −𝒞 theo nguyên lý
bù 2.
Ví dụ:
51 = 00110011
−51 = 11001101
Bài tập 4-5
Trong phần trình bày về nguyên lý bù 2, ta có đưa một “chứng minh” là máy tính
đúng trên dãy bit không cần biết dãy bit đó biểu diễn số âm hay số dương. Chứng
minh này thực hiện qua một ví dụ và không đủ sức thuyết phục. Bạn hãy đưa ra
một chứng minh chặt chẽ hơn. Để trợ giúp cho việc tìm lời chứng minh, các bạn
được cung cấp một chương trình in ra dãy bit biểu diễn một giá trị số nguyên:

40
Lê Minh Hoàng
Chương 4
Biến và kiểu

1 | #include <iostream> 11001101


2 | #include <cstdint>
3 | #include <bitset>
4 |
5 | int main()
6 | {
7 | int8_t a = -51;
8 | std::cout << std::bitset<8>(a);
9 | }
Ta có thể đổi int8_t sang kiểu dữ liệu khác, chú ý thay số 8 ở dòng 8 bởi kích thước
(tính bằng bit) của kiểu dữ liệu mới.

41
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Chương 5. Hằng
Hằng là một biểu thức có giá trị không đổi trong chương trình. Đôi khi người ta
phân biệt hằng (constant) với giá trị thực thể (literal). Ta sử dụng khái niệm hằng
chung cho cả hai loại. Khi cần phân biệt, ta sẽ dùng từ tên hằng (constant) và giá
trị hằng (literal).
5.1. Hằng số nguyên
Trong chương trình một giá trị số nguyên có thể viết nhiều kiểu:
Viết bình thường như trong ký pháp toán học với hệ thập phân
Viết 0b, tiếp theo là dãy bit biểu diễn giá trị đó.
Viết 0, tiếp theo là các chữ số trong hệ cơ số 8 (octa) biểu diễn giá trị đó
Viết 0x, tiếp theo là các chữ số trong hệ thập lục phân (cơ số 16 – hexa) biểu
diễn giá trị đó.
Chú ý rằng ký tự “0” trong cụm tiền tố “0b”, “0”, “0x” là chữ số 0, không phải chữ cái
O.
Ví dụ giá trị 1234, ta có thể viết theo nhiều cách khác nhau*:
1234
0b10011010010
02322
0x4d2
Các giá trị số nguyên trong biểu thức được coi là kiểu int hay long long khi tính
toán, tùy thuộc vào độ lớn của nó. Nếu muốn giá trị này khi tính toán được coi là
kiểu khác, ta có thể thêm vào các chữ cái hậu tố:
Chữ u hoặc U nếu muốn coi hằng đó thuộc kiểu không dấu (unsigned)
Chữ l hoặc L nếu muốn coi hằng đó thuộc kiểu long
Chữ ll hoặc LL nếu muốn coi hằng đó thuộc kiểu long long
Ví dụ 8ull cũng là giá trị 8 nhưng trong tính toán được coi là kiểu unsigned long
long.

*Hệ cơ số 8 dùng 8 chữ số 0, 1, 2, 3, 4, 5, 6, 7 để ký pháp các giá trị số, mỗi đơn vị ở hàng bên trái
gấp 8 lần mỗi đơn vị ở hàng bên phải liền sau. Ví dụ:
2322(8) = 2.8⏟3 + 3.8 ⏟2 + ⏟ 2.81 + 2.8
⏟0 = 1234
1024 192 16 2
Hệ cơ số 16 dùng 16 chữ số 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f. Trong đó a là chữ số có giá trị 10,
b có giá trị 11, …, f có giá trị 15. Mỗi đơn vị ở hàng bên trái gấp 16 lần mỗi đơn vị ở hàng bên phải
liền sau. Ví dụ:
4𝑑2(16) = ⏟4.162 + 13.16
⏟ 1 +⏟ 2.160 = 1234
1024 208 2

42
Lê Minh Hoàng
Chương 5
Hằng

Ngoài việc muốn chắc chắn kiểu cho giá trị hằng, việc ép kiểu cho hằng còn có tác
dụng xử lý tràn số (tràn phạm vi), điều này sẽ được trình bày rõ hơn trong chương
nói về các toán tử. Ví dụ:
cout << 2000000000 + 2000000000; //in ra -294967296
cout << 2000000000ll + 2000000000ll; //in ra 4000000000
5.2. Hằng số thực
Trong chương trình, một giá trị số thực có thể viết hai kiểu:
Viết bình thường theo ký pháp thập phân 𝑎.𝑏 trong đó 𝑎 là phần nguyên và 𝑏
là phần thập phân. Chú ý dấu phân tách phần nguyên và phần thập phân là dấu
chấm “.”, không phải dấu phẩy, và dấu này bắt buộc phải có. Nếu 𝑎 = 0 có thể
viết “.𝑏” thay vì “0.𝑏”, nếu 𝑏 = 0 có thể viết “𝑎.” thay vì “𝑎.0”. Tuy nhiên nếu 𝑎 =
𝑏 = 0, có thể viết 0. hoặc .0 nhưng không được viết mỗi dấu “.”, ví dụ: 3.1416; .5;
7. là những cách viết đúng hằng số thực.
Với 𝑋 là một số thực viết trong hệ thập phân và 𝑌 là một số nguyên, ta có thể
viết 𝑋𝑒𝑌 tương ứng với giá trị 𝑋 × 10𝑌 . Ví dụ: 1e9 là giá trị số thực bằng 1 tỉ
(1000000000), 1𝑒-9 là giá trị số thực bằng 1 phần tỉ (0.000000001).
Lý do bắt buộc phải có dấu chấm thập phân là để phân biệt hằng số nguyên với
hằng số thực. Nếu giá trị số thực (ví dụ 7.0) lại là số nguyên (7), việc viết 7 sẽ được
trình dịch hiểu là hằng số nguyên và có thể thực hiện phép tính số nguyên thay vì
phép tính số thực. Để tránh những rắc rối có thể, ta viết thêm dấu chấm “.” hoặc cả
cụm “.0” vào sau giá trị số.
Ví dụ:
cout << 7 / 2; //in ra 3, phép chia lấy phần nguyên
cout << 7. / 2.; //in ra 3.5, phép chia số thực
Các giá trị số thực trong biểu thức nếu không có chỉ định cụ thể sẽ được coi là kiểu
double. Muốn giá trị này khi tính toán được coi là kiểu khác, ta có thể thêm vào các
chữ cái hậu tố:
Chữ f hoặc F nếu muốn coi giá trị số thực là kiểu float. Ví dụ:
1.23f
Chữ l hoặc L nếu muốn coi giá trị số thực là kiểu long double. Ví dụ:
3.14159265358979324L
5.3. Hằng ký tự và hằng xâu ký tự
Cách thứ nhất để viết hằng ký tự là viết trực tiếp ký tự đó bên trong cặp dấu nháy
đơn. Ví dụ 'a', 'b', '0', '1'. Chú ý cách này không dùng để viết được chính ký tự nháy
đơn, dấu backslash “\”, hay một vài ký tự điều khiển, chẳng hạn dấu xuống dòng.

43
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Cách thứ hai để viết hằng ký tự là dùng mã điều khiển: vẫn phải có cặp ký tự nháy
đơn nhưng ở giữa là mã điều khiển* quy định trong bảng sau:
Mã điều khiển Mô tả Mã ASCII
\' Dấu nháy đơn ' 39 (0x27)
\" Dấu nháy kép " 34 (0x22)
\? Dấu hỏi ? 63 (0x3f)
\\ Dấu backslash \ 92 (0x5c)
\a Phát tiếng beep 07 (0x07)
\b Backspace (lùi con nháy lại 1 vị trí) 08 (0x08)
\f Sang trang (Khi in ra máy in) 12 (0x0c)
\n Xuống dòng 10 (0x0a)
\r Về đầu dòng 13 (0x0d)
\t Dấu tab 09 (0x09)
\v Dấu tab dọc (Khi in form) 11 (0x0b)
\nnn nnn là mã số ASCII trong hệ cơ số 8
\xnn nn là mã số ASCII trong hệ cơ số 16
Một hằng ký tự có thể có nhiều cách viết, chẳng hạn dấu chấm hỏi có thể viết là '?',
'\?', '\077', hay '\x3f', nên chọn cách viết thuận tiện nhất.
Để viết một hằng xâu ký tự, ta viết dãy ký tự nằm giữa hai dấu nháy kép, các ký tự
có thể viết trực tiếp nếu không gây ra sự nhập nhằng, hoặc viết theo mã điều khiển
ở bảng trên. Ví dụ câu lệnh:
cout << "\"How do you do\" is not a question.\nIt's formal way of saying \"Hello\".";
Sẽ in ra màn hình
"How do you do" is not a question.
It's formal way of saying "Hello".
Có một số quy tắc viết hằng ký tự và hằng xâu ký tự cho bảng mã khác như bảng
mã Unicode, hoặc phép mã hóa UTF-8, UTF-16, UTF-32, … tuy nhiên việc tìm hiểu
về cách thức bố trí ký tự trong các bảng mã này khá phức tạp, các bạn có thể tìm
hiểu trong các tài liệu khác. Trong cuốn sách này chúng ta chỉ quan tâm tới bảng
mã ASCII với kiểu ký tự char mà thôi.
5.4. Hằng logic boolean
Vì kiểu bool chỉ có hai giá trị nên để viết hằng kiểu bool, ta dùng từ khóa false
(tương ứng với giá trị chân lý Sai) và từ khóa true (tương ứng với giá trị chân lý
Đúng). Kiểu bool cũng có quy ước thứ tự: false < true.

* “Escape sequence” khó tìm từ tương đương, tôi dịch tạm thành mã điều khiển

44
Lê Minh Hoàng
Chương 5
Hằng

5.5. Định nghĩa hằng


Khi một giá trị hằng xuất hiện nhiều lần trong chương trình với cùng một ý nghĩa,
khi cần sửa đổi giá trị đó sẽ mất nhiều thời gian và có thể bị bỏ sót. Việc gán cho
giá trị hằng một cái tên và dùng tên thay cho giá trị chính là để giải quyết vấn đề
đó.
Để khai báo một hằng, ta dùng cú pháp:
const «Tên hằng» = «Biểu thức»;
Ở đây const là từ khóa khai báo hằng, «Tên hằng» là một tên tự chọn phù hợp với
quy tắc đặt tên (mục 4.1) và «Biểu thức» là giá trị tương ứng với tên hằng, dấu “;”
để kết thúc khai báo hằng.
Ví dụ:
const long long Billion = 1000000000LL;
const int ThisYear = 2019;
const int NextYear = ThisYear + 1;
const char FirstLetter = 'A';
const long double Pi = 3.14159265358979324L;
Sau khi khai báo hằng, ta có thể viết Billion thay cho giá trị 1000000000LL, viết
ThisYear thay cho 2019, NextYear thay cho 2020,…
Việc sử dụng tên hằng thay cho giá trị có hai lợi ích chính:
Hạn chế sai sót khi viết giá trị. Chẳng hạn chương trình cần viết 100 lần giá trị
1 tỉ: 1000000000LL, rất dễ có lần ta viết thừa/thiếu số 0 nhưng trình dịch
không thể báo lỗi vì đó là cách viết hợp lệ. Nếu dùng tên Billion và bị viết sai,
trình dịch sẽ báo lỗi.
Tiết kiệm thời gian khi thay đổi giá trị. Chẳng hạn năm nay là 2019, giá trị này
được đưa vào chương trình rất nhiều lần. Sang năm khi dịch và chạy lại chương
trình và ta phải đổi 2019 thành 2020 ở rất nhiều chỗ. Nếu sử dụng hằng
ThisYear thay cho 2019, ta chỉ cần sửa đổi một chỗ duy nhất trong khai báo
hằng từ 2019 thành 2020 mà thôi (dĩ nhiên lúc đó hằng NextYear cũng tự động
đổi thành 2021).
Một vài chuẩn công nghiệp bắt buộc mọi giá trị hằng đều phải khai báo tên hằng
tương ứng. Tên hằng được chú thích và viết tài liệu đầy đủ ở một file riêng trong
dự án, sau đó tên này được dùng thay hoàn toàn cho giá trị hằng. Những giá trị
hằng xuất hiện vô tổ chức trong chương trình được gọi là những con số ma thuật
(magic numbers) và bị nghiêm cấm.

Bài tập 5-1


Cho biết kiểu của những hằng sau:

45
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

'a', "a",
10, 10u, 10l, 10UL, 012, 0xa,
3.14, 3.14f, 3.14L,
100u, 100u, 100., 1e2, 1e-2
Bài tập 5-2
Cách viết hằng số thực: 2f là đúng hay sai?
Bài tập 5-3
Hai giá trị hằng viết bởi 12, 012 có khác nhau không?
Các giá trị hằng, 12, 012, 12., .12e2 khác nhau gì về kiểu?
Bài tập 5-4
Những cách viết hằng sau đây có đúng không, nếu đúng, giá trị hằng bằng bao
nhiêu, thuộc kiểu gì: .314e1L, 1234f, 1.2e3.4, 123.e4

46
Lê Minh Hoàng
Chương 6
Toán tử và biểu thức

Chương 6. Toán tử và biểu thức


Biểu thức bao gồm các toán tử (operators) và các toán hạng (operands). Chẳng hạn
biểu thức 𝑎 + 𝑏 có toán tử là phép cộng “+” và hai toán hạng là 𝑎 và 𝑏. Trong C++,
toán hạng có thể là hằng, biến, hàm, hoặc một biểu thức khác. Những biểu thức này
có định kiểu và toán tử phải tương thích với kiểu của toán hạng.
6.1. Toán tử gán
Biểu thức gán (assignment) có cú pháp:
«Tên biến» = «Biểu thức»;
Ở đây «Tên biến» là tên một biến đã khai báo, dấu “=” là ký hiệu toán tử gán
(assignment operator), «Biểu thức» là giá trị gán cho biến có tên ở vế trái.
Khi gặp toán tử gán, máy sẽ tính giá trị của «Biểu thức» ở vế phải, lấy giá trị đó gán
cho biến với «Tên biến» chỉ ra trong vế trái. Giá trị cũ của biến bị hủy, thay bằng
giá trị mới. Giá trị của biến sau khi gán được coi là giá trị của cả biểu thức gán.
Biểu thức gán cũng được coi là một câu lệnh trong C++, gọi là lệnh gán. Ví dụ một
đoạn chương trình sau:
1 | int x, y;
2 | x = 4;
3 | x = x + 8; //x = 4 + 8 = 12
4 | x = x * 2; //x = 12 * 2 = 24
5 | y = (x = x * 5) + 3; //x = 24 * 5 = 120; y = 120 + 3 = 123
Giải thích kết quả in ra:
Dòng 1 khai báo hai biến 𝑥, 𝑦 kiểu int.
Dòng 2 gán giá trị 4 cho 𝑥.
Dòng 3 là một lệnh gán, máy tính giá trị vế phải: 4 + 8 = 12 rồi gán giá trị 12 cho
𝑥.
Dòng 4 lại có một lệnh gán, máy tính giá trị vế phải: 12 ∗ 2 = 24 (dấu * là toán tử
nhân) rồi gán giá trị 24 cho 𝑥
Dòng 5 cũng là một lệnh gán nhưng vế trái là biến 𝑦, biểu thức vế phải là một phép
cộng: cộng giá trị biểu thức (𝑥 = 𝑥 ∗ 5) với 3. Biểu thức 𝑥 = 𝑥 ∗ 5 là một biểu thức
gán, ngoài việc cho 𝑥 giá trị bằng 24 × 5 = 120, máy cũng coi 120 là giá trị của biểu
thức (𝑥 = 𝑥 ∗ 5), giá trị 120 này được cộng với 3 rồi lấy kết quả gán cho biến 𝑦, ta
có 𝑦 = 123.

47
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Thứ tự thực hiện của biểu thức gán là từ phải qua trái: tính biểu thức vế phải trước,
thực hiện phép gán sang biến ở vế trái, rồi coi như giá trị gán là kết quả biểu thức.
Lệnh gán sau đây hoàn toàn hợp lệ trong C++:
int a, b, c;
a = b = c = 6; //a = (b = (c = 6))
Trong lệnh này, cả 𝑎, 𝑏, 𝑐 đều được gán bằng 6, nhưng thứ tự gán cho 𝑐 trước, đến
𝑏, rồi cuối cùng mới đến 𝑎.
Trong lệnh gán, «Biểu thức» vế phải phép gán phải có kiểu tương thích với biến ở
vế trái. Tương thích không có nghĩa là kiểu phải giống nhau. Có một vài quy tắc
chuyển kiểu ngầm được C++ định nghĩa, ta có thể xem ở mục 6.11 cuối bài.
6.2. Các toán tử số học trên số nguyên
Có 6 toán tử số học trên số nguyên. Những toán tử này yêu cầu các toán hạng đều
là số nguyên mới có thể thực hiện được để cho kết quả là số nguyên.
Toán tử Mô tả
+ Phép cộng
- Phép trừ
* Phép nhân
/ Phép chia lấy phần nguyên
% Phép chia lấy phần dư
- Phép lấy số đối
Nói chung các toán tử số học được viết như trong ký pháp toán học, trong đó phép
nhân ký hiệu là * (chứ không phải ×) và phép chia lấy phần dư ký hiệu là %.
Cần phân biệt phép lấy số đối (toán tử 1 ngôi*) với phép trừ (toán tử 2 ngôi†), đây
là hai toán tử khác nhau nhưng dùng chung một ký hiệu:
Ví dụ: Biểu thức
−(5 − 8) − 3
Dấu trừ đầu tiên trong biểu thức là phép lấy số đối, hai dấu trừ còn lại là phép trừ.
Thực ra C++ còn có toán tử “+” một ngôi nữa, tuy nhiên toán tử này không có ý
nghĩa lắm, thường chỉ sử dụng để trình bày code cho cân đối mà thôi. Ví dụ hai
nghiệm của phương trình |𝑥 − 3| = 𝐶 (với ẩn 𝑥 và hằng số 𝐶 ≠ 0) có thể viết như
sau cho thẳng cột:
x1 = +C + 3; //Dấu + trước chữ C chỉ là để thẳng cột với
x2 = -C + 3; //dấu lấy số đối (-C) ở hàng này

* Toán tử với một toán hạng


† Toán tử với hai toán hạng

48
Lê Minh Hoàng
Chương 6
Toán tử và biểu thức

Đối với phép chia lấy phần nguyên và phép chia lấy phần dư: Giá trị của phép tính
𝑎 / 𝑏 và 𝑎 % 𝑏 quy định như sau: Đặt 𝑞 = 𝑎 / 𝑏 và 𝑟 = 𝑎 % 𝑏
Nếu 𝑏 = 0, giá trị của phép tính không xác định (đối với trình dịch G++, chương
trình sẽ chạy sinh lỗi)
Nếu 𝑏 > 0, 𝑞 là thương nguyên và 𝑟 là số dư trong phép chia 𝑎 cho 𝑏, giống như
trong toán học.
Nếu 𝑏 < 0, chuẩn C++14 chỉ đảm bảo rằng |𝑟| < |𝑏| và 𝑎 = 𝑏𝑞 + 𝑟. Tùy theo cách
cài đặt của trình dịch và cách xử lý trong tập lệnh CPU, số dư 𝑟 có thể âm hoặc
dương và theo đó giá trị 𝑞 cũng bị phụ thuộc vào trình dịch và CPU. Ví dụ vài
trường hợp cụ thể đối với G++:
a b q = a / b r = a % b
7 -2 -3 1
-7 2 -3 -1
Muốn lấy được số dư đúng theo định nghĩa toán học: 0 ≤ 𝑟 < |𝑏|, ta phải kiểm tra
nếu 𝑟 âm thì cộng thêm 𝑟 đúng 𝑏 đơn vị. Cách khác là dùng công thức:
𝑟 = (𝑎 % 𝑏 + 𝑏) % 𝑏
6.3. Các toán tử tăng/giảm
Với 𝑥 là một biến thuộc kiểu số nguyên hoặc kiểu ký tự. Biểu thức ++𝑥 hay 𝑥++
tăng giá trị của 𝑥 lên 1, biểu thức --𝑥 hay 𝑥-- giảm giá trị của 𝑥 đi 1. Tuy cùng một
công dụng đối với biến 𝑥, nhưng kết quả biểu thức có khác giữa cách viết ++𝑥 với
𝑥++, giữa cách viết --𝑥 với 𝑥--
Biểu thức Công dụng Giá trị biểu thức
++x Tăng x lên 1 Giá trị của x sau khi tăng
x++ Tăng x lên 1 Giá trị của x trước khi tăng
--x Giảm x đi 1 Giá trị của x sau khi giảm
x-- Giảm x đi 1 Giá trị của x trước khi giảm
Ví dụ:
int x = 1, y, z;
y = ++x; //x = 2, y = 2
z = x++; //x = 3, z = 2
Chú ý rằng các toán tử ++ và -- phải viết liền, không có dấu cách ở giữa. Lệnh:
y = - -x; //Có dấu cách giữa 2 dấu –
Được hiểu là lệnh gán 𝑦 = 𝑥, vì mỗi dấu “-” được diễn dịch thành phép lấy số đối.
6.4. Các toán tử trên số thực
Những toán tử số thực đặt trong biểu thức cho kết quả là số thực:

49
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Toán tử Mô tả
+ Phép cộng
- Phép trừ
* Phép nhân
/ Phép chia
- Phép lấy số đối
Các toán tử trên số thực được viết như trong ký pháp toán học. Trên số thực cũng
có toán tử một ngôi + như trên số nguyên. Ví dụ −3.0 ∗ + − 5.0 = 15.0, trong biểu
thức này tất cả các dấu + và – đều là phép toán một ngôi, chỉ phép * là phép toán
hai ngôi.
6.5. Các toán tử gán nhanh
Toán tử gán nhanh sửa đổi giá trị của một biến bằng cách thực hiện một thao tác
trên chính nó. Nếu 𝑣 là một biến và 𝑒 là một biểu thức tương thích kiểu với 𝑣:
Toán tử Cách viết Tương đương
+= v += e; v = v + e
-= v -= e; v = v - e
*= v *= e; v = v * e
/= v /= e; v = v / e
%= v %= e; v = v % e
Với ⋆ là một phép toán hai ngôi, chú ý rằng phép gán nhanh 𝑣 ⋆= 𝑒 không chắc hợp
lệ hoặc tương đương với 𝑣 = 𝑣 ⋆ 𝑒 nếu 𝑣 thuộc kiểu dữ liệu phức hợp, hoặc với
toán tử ⋆ do người lập trình định nghĩa.
6.6. Các toán tử so sánh
Hai biểu thức có thể so sánh với nhau bởi các toán tử so sánh:
Toán tử Cách viết C++ Ý nghĩa
== L == R 𝐿=𝑅
!= L != R 𝐿≠𝑅
< L < R 𝐿<𝑅
> L > R 𝐿>𝑅
<= L <= R 𝐿≤𝑅
>= L >= R 𝐿≥𝑅
Toán tử so sánh cho kết quả là true hoặc false. Kết quả là true nếu giá trị vế phải
và vế trái phù hợp với toán tử so sánh, ngược lại kết quả là false.
Ví dụ:

50
Lê Minh Hoàng
Chương 6
Toán tử và biểu thức

Biểu thức Giá trị


1 == 2 false
3 != 4 true
5 < 6 true
7 > 7 false
8 <= 8 true
9 >= 10 false
Hai vế của toán tử so sánh có thể là hai biểu thức bất kỳ miễn là chúng có kiểu
tương thích với phép so sánh.
6.7. Các toán tử logic
Có ba toán tử logic: phép phủ định, phép “và” logic, phép “hoặc” logic*.
Toán tử Ký hiệu Ký hiệu khác
Phủ định ! not
“Và” logic && and
“Hoặc” logic || or
Cột “ký hiệu khác” chỉ ra cách viết tương đương, chọn cách nào là tùy sở thích. Cách
viết not/and/or hay được dùng khi sử dụng bàn phím không thuận tiện cho việc
gõ các ký hiệu !, &, | (chẳng hạn bàn phím trên màn hình cảm ứng của điện thoại di
động).
Toán tử phủ định khi đặt trước một biểu thức logic sẽ cho giá trị phủ định của biểu
thức. Ví dụ:
Biểu thức Giá trị
!false true
!true false
!(1 == 2) true
!(3 < 4) false
Toán tử “và” logic (&&) khi đặt giữa hai toán hạng logic 𝐴, 𝐵 sẽ cho một biểu thức
𝐴 && 𝐵 với giá trị chân lý chỉ ra trong bảng sau:
A B A && B
false false false
false true false
true false false
true true True
Giá trị của biểu thức 𝐴 && 𝐵 bằng true nếu và chỉ nếu cả 𝐴 và 𝐵 đều bằng true.
Tính chất:

*Những phép toán này trong tiếng Anh là logical NOT, logical AND và logical OR. Trong thuật ngữ
toán tiếng Việt gọi là phép phủ định, phép hội logic và phép tuyển logic. Những thuật ngữ hội/tuyển
hơi xa lạ với những người không làm toán, vì vậy tôi tạm gọi là phép “và” logic và phép “hoặc” logic.

51
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

(𝐴 && false) ⇔ (false && 𝐴) ⇔ false


(𝐴 && true) ⇔ (true && 𝐴) ⇔ 𝐴
Toán tử “hoặc” logic (||) khi đặt giữa hai toán hạng logic 𝐴, 𝐵 sẽ cho một biểu thức
𝐴 || 𝐵 với giá trị chân lý chỉ ra trong bảng sau:
A B A || B
false false false
false true true
true false true
true true true
Giá trị của biểu thức 𝐴 || 𝐵 bằng false nếu và chỉ nếu cả 𝐴 và 𝐵 đều bằng false.
Tính chất:
(𝐴 || false) ⇔ (false || 𝐴) ⇔ 𝐴
(𝐴 || true) ⇔ (true || 𝐴) ⇔ true
Trong khi phép phủ định (!) có độ ưu tiên hơn các toán tử so sánh thì phép &&
cũng như phép || lại có độ ưu tiên thấp hơn các toán tử so sánh (xem mục 6.10). Vì
vậy để kiểm tra một ký tự 𝑐 (kiểu char) có phải là một chữ cái hoa tiếng Anh hay
không, ta có thể viết biểu thức logic:
c >= 'A' && c <= 'Z'
mà không cần viết đủ các cặp dấu ngoặc:
(c >= 'A') && (c <= 'Z')
Chú ý rằng cũng với yêu cầu trên nhưng biểu thức:
'A' <= c <= 'Z'
luôn cho giá trị bằng true: Phép so sánh 'A' <= c cho giá trị false hoặc true, giá trị
này sẽ được chuyển đổi thành 0 hoặc 1 khi so sánh với 'Z' (bằng 90). Cách viết kiểu
toán học này không sai về cú pháp biểu thức logic, nhưng là sai ngữ nghĩa đối với
chương trình dịch (xem thêm mục 6.11.2 về cơ chế chuyển đổi kiểu).
Khi đánh giá biểu thức logic, máy dùng cơ chế tính tắt đối với phép && cũng như
phép ||:
Đối với biểu thức 𝐴 && 𝐵: Nếu 𝐴 bằng false, máy sẽ không tính giá trị biểu thức
𝐵 nữa mà kết luận ngay 𝐴 && 𝐵 có giá trị false.
Đối với biểu thức 𝐴 || 𝐵: Nếu 𝐴 bằng true, máy sẽ không tính giá trị biểu thức
𝐵 nữa mà kết luận ngay 𝐴 || 𝐵 có giá trị true.
Điều này cần đặc biệt chú ý vì nếu biểu thức 𝐵 có nhiệm vụ nào đó (chẳng hạn có
chứa lời gọi hàm), nhiệm vụ này sẽ không được thực hiện trong cả hai trường hợp
trên. Hãy phân tích chương trình sau để hiểu rõ hơn cơ chế tính tắt:

52
Lê Minh Hoàng
Chương 6
Toán tử và biểu thức

1 | #include <iostream>
2 | using namespace std;
3 |
4 | int main()
5 | {
6 | cout << boolalpha; //Yêu cầu in giá trị logic kiểu true/false thay vì 1/0
7 | int i = 3;
8 | bool b = (i < 1) && (++i == 4); //Bỏ qua phép so sánh ++i == 4
9 | cout << b << '\n'; //in ra false
10 | cout << i; //in ra 3, vì lệnh ++i ở trên không được thực hiện
11 | }
6.8. Toán tử điều kiện
Toán tử điều kiện (?) đặt trong biểu thức có cú pháp sau:
«Điều kiện» ? «A» : «B»;
«Điều kiện» là một biểu thức logic, «A» và «B» là hai biểu thức.
Nếu điều kiện đúng, giá trị của cả biểu thức điều kiện được đặt bằng «A», ngược lại
nếu điều kiện sai, giá trị của cả biểu thức điều kiện được đặt bằng «B». Chú ý rằng
chỉ một trong hai biểu thức «A» hoặc «B» được tính.
Ví dụ với 𝑥, 𝑦, 𝑧 là ba biến kiểu int, muốn đặt 𝑧 bằng giá trị lớn hơn trong hai giá trị
𝑥, 𝑦 (𝑧 = max(𝑥, 𝑦)), ta có thể viết:
z = x > y ? x : y;
6.9. Toán tử dấu phẩy
Toán tử dấu phẩy (“,”) được dùng để phân tách hai hoặc nhiều biểu thức, các biểu
thức nối tiếp bằng toán tử dấu phẩy được thực hiện lần lượt từ trái qua phải và chỉ
giá trị biểu thức cuối cùng được lấy làm kết quả.
Ví dụ với hai biến 𝑎, 𝑏 kiểu int, lệnh:
a = (b = 1, b + 5);
sẽ gán 𝑏 = 1, sau đó gán giá trị 𝑏 + 5 cho 𝑎, ta có 𝑎 = 6.
Biểu thức dùng toán tử dấu phẩy được gọi là biểu thức dấu phẩy (comma
expression)
6.10. Độ ưu tiên của các toán tử
Còn một vài toán tử khác sẽ được giới thiệu trong những chương tiếp theo, chẳng
hạn các toán tử xử lý bit, toán tử lấy địa chỉ và giải tham chiếu, …
Biểu thức trong C++ có thể chứa rất nhiều toán tử và toán hạng, ta cần biết được
độ ưu tiên của các toán tử để hiểu cách thức trình dịch diễn giải biểu thức, qua đó
đặt các cặp dấu ngoặc đơn hợp lý để biểu thức được tính đúng.
Ví dụ: Biểu thức 4 ∗ 2 + 3 ∗ 5 tương đương với cách viết (4 ∗ 2) + (3 ∗ 5), việc đặt
dấu ngoặc đơn trong trường hợp này là thừa. Nhưng biểu thức 4 ∗ 2 + 3 ∗ 5 không
tương đương với cách viết 4 ∗ (2 + 3) ∗ 5.

53
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Dưới đây là bảng độ ưu tiên của các toán tử:


Cột đầu tiên chỉ ra thứ tự ưu tiên, thứ tự càng nhỏ, độ ưu tiên của toán tử càng cao.
Cột thứ hai là toán tử tương ứng với độ ưu tiên, vì C++ dùng rất nhiều ký hiệu và
các toán tử đôi khi dùng ký hiệu giống nhau, cột thứ ba giải thích về toán tử đó để
tránh nhầm lẫn.
Cột thứ tư là thứ tự trong một biểu thức chứa nhiều toán tử ngang hàng, ta có thể
hiểu như cách máy đặt thêm dấu ngoặc đơn để xác định chính xác thứ tự tính. Ký
hiệu (→)là thứ tự từ trái qua phải và ký hiệu (←) là thứ tự từ phải qua trái
Ví dụ với 𝑥 là một biến int có giá trị 0, biểu thức
𝑥+= 𝑥+= 𝑥+= 2
tương đương với:
𝑥+= (𝑥+= (𝑥+= 2))
(đều cho giá trị 𝑥 = 8)
Do thứ tự của phép gán nhanh là từ phải qua trái.
Thứ tự ưu tiên Toán tử Mô tả Thứ tự
1 :: Phạm vi →
++ -- Tăng/giảm: x++, x--
() Lời gọi hàm: f()
2 →
[] Truy cập theo chỉ số: a[]
. -> Truy cập thành viên
++ -- Tăng/giảm: ++x, --x
! Phủ định logic: !x
~ Phép đảo bit: ~x
() Toán tử ép kiểu: (T)x
3
- + Toán tử số học một ngôi ←
& * Lấy địa chỉ, hủy tham chiếu
new delete Cấp phát, giải phóng bộ nhớ
sizeof() Kích thước kiểu/biến
4 .* ->* Con trỏ tới thành viên →
5 * / % Phép toán nhân, chia, lấy dư →
6 + - Phép toán cộng, trừ →
7 << >> Dịch trái/phải dãy bit →
8 < <= > >= Quan hệ thứ tự →
9 == != Quan hệ bằng/khác nhau →
10 & Toán tử BitAnd (hội bit) →
11 ^ Toán tử BitXor (hiệu đối xứng bit) →
12 | Toán tử BitOr (tuyển bit) →
13 && Phép “và” logic →
14 || Phép “hoặc” logic →
= *= /= %= += -= Phép gán/gán nhanh
>>= <<= &= ^= |= ←
15
? Toán tử điều kiện: a ? b : c
16 , Toán tử dấu phẩy →

54
Lê Minh Hoàng
Chương 6
Toán tử và biểu thức

Đối với toán tử điều kiện 𝑎? 𝑏: 𝑐 biểu thức 𝑏 luôn được coi là được nằm trong dấu
ngoặc đơn bất kể các toán tử trong 𝑏 mức ưu tiên như thế nào so với phần còn lại
của biểu thức: 𝑎? (𝑏): 𝑐
sizeof thực ra là một hàm, nhưng nó được coi như trường hợp đặc biệt:
sizeof(𝑖𝑛𝑡) ∗ 𝑝 được diễn dịch thành (sizeof(𝑖𝑛𝑡)) ∗ 𝑝 chứ không phải
sizeof((𝑖𝑛𝑡) ∗ 𝑝)
Các toán tử dù có được định nghĩa lại (chẳng hạn đối tượng cout định nghĩa lại
toán tử <<) nhưng vẫn giữ nguyên độ ưu tiên liệt kê trong bảng trên.
Ta lấy một ví dụ để thấy rõ vai trò của độ ưu tiên các toán tử: Với 𝑥, 𝑦 là hai biến
kiểu int, ta muốn in ra giá trị lớn hơn trong hai giá trị 𝑥, 𝑦:
Biểu thức Diễn dịch thành Kết quả
std::cout << x > y ? x : y; ((std::cout << x) > y) ? x : y; Lỗi
std::cout << (x > y) ? x : y; (std::cout << (x > y)) ? x : y; Sai
std::cout << (x > y ? x : y); std::cout << ((x > y) ? x : y); Đúng
Phần còn lại của chương này đề cập tới những vấn đề hơi khó hiểu, nhưng lại rất
quan trọng trong lập trình C++ nói riêng và tất cả các ngôn ngữ lập trình định kiểu
mạnh nói chung. Trong những ngôn ngữ lập trình đó, một biểu thức đúng hoàn
toàn về mặt toán học lại có thể tính sai do không tương thích kiểu dữ liệu.
Định kiểu mạnh là đặc trưng của các ngôn ngữ lập trình hiệu suất cao. Muốn tận
dụng được hiệu suất của các ngôn ngữ lập trình đó, bắt buộc lập trình viên phải
hiểu rõ cơ chế xử lý biểu thức.
6.11. Cơ chế ngầm chuyển đổi kiểu
Ngầm chuyển đổi kiểu (implicit type conversion) là tình trạng một giá trị biểu thức
được tự chuyển đổi sang kiểu khác nhằm thích nghi với việc tính toán hay gán giá
trị cho biến. Việc này do chương trình dịch sinh mã tự thực hiện, lập trình viên
không cần can thiệp.
6.11.1. Ngầm chuyển đổi kiểu trong phép gán và khởi tạo biến
Xét phép gán:
𝑥 = 𝐸;
Trong đó 𝑥 là một biến và 𝐸 là một biểu thức.
 Ngầm chuyển đổi kiểu khi khởi tạo/gán biến bool
Khi khởi tạo hoặc gán giá trị một biến bool 𝑥 bằng biểu thức 𝐸. Biểu thức 𝐸 có thể
thuộc kiểu bool, số nguyên, số thực, giá trị trong kiểu liệt kê, con trỏ. Nếu 𝐸 bằng
false (kiểu bool), hoặc 0 (số nguyên, số thực hoặc mã thứ tự trong kiểu liệt kê),
hoặc nullptr (con trỏ), biến 𝑥 sẽ nhận giá trị false. Nếu không 𝑥 sẽ nhận giá trị true.

55
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

bool x;
x = 5; //x = true
x = 0; //x = false
x = 0.0; //x = false
x = 'a'; //x = true, C++ xử lý ký tự như số nguyên, 'a' = 97
 Ngầm chuyển đổi kiểu khi khởi tạo/gán biến số nguyên
Khi khởi tạo hoặc gán giá trị cho một biến nguyên 𝑥 bằng biểu thức 𝐸:
Nếu 𝐸 có kiểu bool, giá trị false được ngầm chuyển thành 0 và giá trị true được
ngầm chuyển thành 1.
Nếu 𝐸 có kiểu số thực, phần thập phân sẽ được cắt bỏ, phần nguyên được gán
cho 𝑥, nếu giá trị phần nguyên nằm ngoài phạm vi biểu diễn của kiểu biến 𝑥,
kết quả không xác định (undefined)
Nếu 𝐸 có kiểu số nguyên (hoặc kiểu được coi như số nguyên như ký tự, liệt kê):
Nếu giá trị của 𝐸 nằm trong phạm vi biểu diễn của kiểu biến 𝑥. Giá trị này
sẽ được gán cho 𝑥.
Nếu giá trị của 𝐸 nằm ngoài phạm vi biểu diễn của kiểu biến 𝑥, 𝑥 sẽ được
gán giá trị 𝐸 ′ trong kiểu sao cho 𝐸 ′ đồng dư với 𝐸 theo mô đun 2𝜆 . Ở đây
𝜆 là kích thước tính theo bit của kiểu biến 𝑥 (𝜆 ∈ {8,16,32,64}).
1 | int16_t x; //int16_t: -215 … 215-1 = -32768 … 32767
2 | x = false; //Gán int16_t = bool: x = 0
3 | x = true; // Gán int16_t = bool: x = 1
4 | x = -2.5; // Gán int16_t = double: x = -2
5 | x = 40000.9; //Gán int16_t = double: không xác định
6 | x = 3; // Gán int16_t = int: x = 3
7 | x = 1234567; //Gán int16_t = int: x = -10617 (216 = 65536)
Đôi điều phải nói thêm về phép gán 𝑥 = 𝐸 trong đó 𝐸 là một biểu thức nguyên có
giá trị nằm ngoài kiểu của 𝑥. Dĩ nhiên khi đó, kiểu của 𝐸 có số bit không ít hơn 𝜆 là
kích thước kiểu của 𝑥. Phép gán 𝑥 = 𝐸 sẽ copy chính xác 𝜆 bit của 𝐸 sang 𝑥 bắt đầu
từ bit thấp nhất (bit đơn vị), những bit cao hơn (nếu có) sẽ bị bỏ qua và không
được copy. Tức là nếu sau phép gán đó, giá trị của 𝑥 = 𝐸 ′ thì 𝐸 − 𝐸 ′ sẽ có dãy bit
tận cùng bởi 𝜆 bit 0, nói cách khác 𝐸 − 𝐸 ′ chia hết cho 2𝜆 : 𝐸 ≡ 𝐸′(mod 2𝜆 ) bất kể
𝑥 và 𝐸 là kiểu số nguyên không dấu hay có dấu.
(Lưu ý rằng miền giá trị của kiểu biến 𝑥 gồm 2𝜆 số nguyên liên tiếp, vì vậy chỉ có
duy nhất một giá trị đồng dư với 𝐸 theo mô đun 2𝜆 mà thôi. Điều đó đảm bảo kết
quả phép gán xác định duy nhất)
Ví dụ trong lệnh gán ở dòng 7:
1234567 = 00000000 00010010 11010110 10000111
-10617 = 11010110 10000111

56
Lê Minh Hoàng
Chương 6
Toán tử và biểu thức

Cơ chế này chỉ đúng khi các số nguyên trong máy được biểu diễn tuân theo nguyên
lý bù 2 (xem mục 4.4.2). Trong chuẩn C++14, nguyên lý bù 2 là không bắt buộc, vì
vậy lệnh khởi tạo/gán tràn phạm vi cho một biến 𝑥 kiểu số nguyên có dấu cũng bị
coi là không xác định.
Nguyên lý bù 2 hiện đang áp dụng cho mọi trình dịch C++, nó cũng sẽ sớm được
đưa vào chuẩn C++20. Vì vậy riêng trường hợp này ta có thể thoải mái sử dụng các
kiến thức trên mà không phải lo ngại về hành vi không xác định (undefined
behavior).
Việc khởi tạo hoặc gán giá trị tràn phạm vi kiểu số nguyên khá phức tạp về bản
chất. Những lập trình viên sử dụng kiểu gán này hoặc là rất kinh nghiệm, hoặc là
rất ngớ ngẩn. Lời khuyên cho các bạn mới học lập trình là: Hạn chế sử dụng phép
khởi tạo hoặc gán tràn phạm vi đối với biến kiểu số nguyên.
 Ngầm chuyển đổi kiểu khi khởi tạo/gán biến số thực
Khi khởi tạo hoặc gán giá trị cho một biến số thực 𝑥 bằng biểu thức 𝐸:
Nếu 𝐸 có kiểu số thực, giá trị được gán tự nhiên sang 𝑥
Nếu 𝐸 là kiểu số nguyên hoặc kiểu có thể ngầm chuyển sang kiểu số nguyên (ký
tự, liệt kê, con trỏ, bool), giá trị được gán sang 𝑥 với phần thập phân bằng .0
Khi khởi tạo hoặc gán giá trị biểu thức cho biến số thực, một vài chữ số có thể bị
mất, chữ số cuối cùng có thể được làm tròn tùy thuộc vào số chữ số có nghĩa của
kiểu biến số thực. Việc có làm tròn hay không là tùy thuộc vào máy tính và chương
trình dịch (implementation-defined). Tức là kết quả có thể có sai số, nhưng sai số
này do làm tròn lên hay làm tròn xuống tùy thuộc các máy khác nhau, trình dịch
khác nhau.
Phép gán và phép khởi tạo biến chỉ giống nhau trên các kiểu dữ liệu cơ sở, chúng
có hiệu ứng khác nhau trên các kiểu phức hợp, ta sẽ đề cập đến vấn đề này khi nói
về các kiểu dữ liệu đó.
6.11.2. Ngầm chuyển đổi kiểu trong biểu thức số học
Khi tính toán một biểu thức số học, trình dịch phân tích biểu thức lớn ra thành các
biểu thức nhỏ, mỗi biểu thức nhỏ chỉ gồm một toán tử số học. Ví dụ biểu thức:
(8 + 3) ∗ (9 − 4)
Sẽ được tính qua các biểu thức nhỏ sau:
8 + 3 = 11
9−4=5
11 ∗ 5 = 55
Do đặc thù của trình dịch, chỉ một vài kiểu dữ liệu số học có thể thực hiện việc tính
toán. Những kiểu dữ liệu khác phải ngầm chuyển thành một trong những kiểu này

57
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

mới có thể tính toán được bằng các phép tính số học. Danh sách các kiểu “tính được”
này xếp theo thứ tự từ “nhỏ” đến “lớn” theo danh sách sau:
int
unsigned
long
unsigned long
long long
unsigned long long
float
double
long double
Tính “nhỏ” và “lớn” của kiểu, ta đặt trong dấu nháy kép, là do quy ước của chuẩn.
Không có nghĩa là kiểu “lớn” hơn thì chiếm nhiều bit hơn hay có phạm vi biểu diễn
rộng hơn.
Các toán hạng trong biểu thức sẽ được ngầm chuyển đổi sang các kiểu “tính được”
ở trên bằng cách áp dụng lần lượt các hai quy tắc sau đây:
Quy tắc 1: Toán hạng sẽ được ngầm chuyển sang kiểu “tính được” nhỏ nhất có miền
giá trị chứa trọn vẹn miền giá trị của kiểu toán hạng. Trên các máy tính chúng ta
dùng lập trình, kiểu int là int32_t, tức là các toán hạng kiểu int8_t, uint8_t, int16_t,
uint16_t, kể cả bool và char (theo quy tắc ngầm chuyển kiểu khi khởi tạo biến, mục
6.11.1) sẽ được chuyển hết thành int trước khi thực hiện toán tử số học. Các kiểu
dữ liệu số khác không đổi do chúng đều là kiểu “tính được”.
Quy tắc 2: Nếu toán tử yêu cầu nhiều toán hạng, tất cả các toán hạng sẽ được
chuyển thành kiểu “lớn” nhất trong các kiểu toán hạng. Giá trị biểu thức được tính
trong chính kiểu này.
Ta xét một vài đoạn chương trình sau để tìm hiểu bản chất của phép ngầm chuyển
đổi kiểu khi tính toán biểu thức:
1 | unsigned short a = 0;
2 | unsigned int b = 0;
3 | long long x;
4 | x = a - 1; //x = -1
5 | x = b - 1; //x = 4294967295
Ta nhận ra một kết quả “kỳ quặc”: Rõ ràng kiểu của 𝑏 (unsigned int) bao trùm cả
kiểu của 𝑎 (unsigned short), nhưng phép tính 𝑎 − 1 đúng còn phép tính 𝑏 − 1 lại
không đúng với kết quả toán học.
Số 1 trong biểu thức được coi là kiểu int theo quy tắc viết hằng số (mục 5.1). Vậy
nên:
Biểu thức 𝑎 − 1 được diễn giải thành
unsigned short - int = int - int = int

58
Lê Minh Hoàng
Chương 6
Toán tử và biểu thức

Biểu thức 𝑏 − 1 được diễn giải thành


unsigned int - int = unsigned int - unsigned int = unsigned int
Rõ ràng giá trị -1 thuộc phạm kiểu int nhưng không thuộc phạm vi kiểu unsigned
int, phép tính tràn phạm vi cho ta 𝑥 = 4294967295 ( = 232 − 1) ở dòng 5.
Chú ý rằng dòng 5 là một lệnh gán và ta bị tràn số khi tính biểu thức trừ ở vế phải
dấu “=”, không liên quan tới kiểu biến ở vế trái. Ở đây kiểu biến 𝑥 (long long) thừa
đủ để chứa giá trị -1.
Xét một ví dụ khác:
1 | int a = 1234567890;
2 | long long b = a;
3 | long long x;
4 | x = a * 10; //x = -539222988
5 | x = b * 10; //x = 12345678900
6 | x = a * 10LL; //x = 12345678900
Lại một kết quả “kỳ quặc” nữa cần được giải thích ở dòng 4, phép tính 𝑎 ∗ 10 được
diễn giải thành:
int * int = int
Phạm vi biểu diễn của kiểu kết quả là int (−231 … 231 − 1), không đủ chứa giá trị
12345678900. Tràn số xảy ra ngay ở phép nhân.
Ở dòng 5, phép tính 𝑏 ∗ 10 được diễn giải thành:
long long * int = long long * long long = long long
Phạm vi biểu diễn của kiểu kết quả bây giờ là long long (−263 … 263 − 1), đủ chứa
giá trị 12345678900.
Ở dòng 6, phép tính 𝑎 ∗ 10𝐿𝐿 được diễn giải thành
int * long long = long long * long long = long long
Trường hợp này phép nhân cũng không bị tràn số và cho kết quả đúng
Phép tính tràn phạm vi (tràn số) xảy ra ngay cả khi ta cộng/trừ/nhân hai hằng số
nguyên mà không cẩn thận về kiểu toán hạng:
long long x;
x = 1234567890 * 10; //x = -539222988
x = 1234567890LL * 10; //x = 12345678900
x = 1234567890 * 10LL; //x = 12345678900
x = 1234567890LL * 10LL; //x = 12345678900
Ngay cả với các phép tính số thực có toán hạng là số nguyên, ta cũng cần để ý để
hạn chế sai số không đáng có:
1 | long long a = 999999999999999999LL;
2 | long double x;
3 | x = a + .0f; //x = 999999984306749440.0
4 | x = a + .0; //x = 1000000000000000000.0
5 | x = a + .0l; //x = 999999999999999999.0

59
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Ở dòng 3, 𝑎 bị đổi kiểu thành float và biểu thức được tính theo kiểu float (sai số
nhiều nhất). Ở dòng 4, 𝑎 bị đổi kiểu thành double và biểu thức được tính theo kiểu
double (sai số ít hơn). Ở dòng 5, 𝑎 bị đổi kiểu thành long double và biểu thức cũng
được tính theo kiểu long double (trong trường hợp này không có sai số).
6.12. Toán tử ép kiểu
C++ cho phép lập trình viên ép kiểu một số toán hạng để can thiệp vào quá trình
ngầm chuyển đổi kiểu khi sinh mã tính toán biểu thức. Việc ép kiểu cho toán hạng
𝑎 tương tự như việc lấy giá trị của 𝑎 gán cho biến 𝑎′ thuộc một kiểu khác rồi tính
toán biểu thức theo toán hạng 𝑎′ thay vì 𝑎. Có hai cú pháp đơn giản để ép kiểu:
(«Tên kiểu»)«Biểu thức» hoặc «Tên kiểu»(«Biểu thức»)
Cả hai cách viết có công dụng tương đương: Đầu tiên «biểu thức» được tính bình
thường áp dụng các luật tính toán: độ ưu tiên các toán tử (mục 6.10), cơ chế ngầm
chuyển đổi kiểu trong tính toán biểu thức (mục 6.11.2), … để được một giá trị. Giá
trị này được chuyển sang kiểu chỉ ra trong «Tên kiểu», quy tắc chuyển được nêu
ra trong cơ chế ngầm chuyển đổi kiểu trong phép gán/khởi tạo biến (mục 6.11.1).
Giá trị sau khi đã chuyển kiểu sẽ được dùng để tính toán tiếp…
Cách viết thứ nhất thường được dùng khi «Biểu thức» là một hằng hay một biến.
Cách viết thứ hai thường được dùng khi «Tên kiểu» không có dấu cách. Nếu biểu
thức phức tạp và tên kiểu cũng có dấu cách, ta phải dùng các cặp dấu ngoặc đơn
bọc lấy cả phần «Tên kiểu» và phần «Biểu thức»:
(«Tên kiểu»)(«Biểu thức»)
Ví dụ: Tính trung bình cộng và tích của hai số nguyên 𝑎, 𝑏:
1 | int a = 999999;
2 | int b = 1000000;
3 | double c = double(a + b) / 2; //c = 999999.5
4 | long long d = (long long)a * b; //d = 999999000000
Dòng 3, máy tính 𝑎 + 𝑏 = 1999999 kiểu int, ép kiểu số nguyên int này thành
double rồi chia 2, đây là phép chia số thực (hằng số 2 kiểu int được ngầm chuyển
thành double), cho kết quả là 999999.5 kiểu double, kết quả này được gán cho 𝑐.
Nếu viết:
double c = (a + b) / 2;
Phép chia trong biểu thức này là phép chia nguyên, cho kết quả 999999 kiểu int,
khi gán cho 𝑐, cơ chế ngầm chuyển đổi kiểu cho 𝑐 = 999999.0
Những cách viết sau cũng cho giá trị trung bình cộng đúng, các bạn có thể tự phân
tích theo luật:

60
Lê Minh Hoàng
Chương 6
Toán tử và biểu thức

double c = (double(a) + b) / 2;
double c = (a + (double)b) / 2;
double c = (a + b) / double(2);
double c = (a + b) / 2.0
Dòng 4, 𝑎 được ép kiểu thành long long trước. Theo quy tắc ngầm chuyển đổi kiểu
khi thực hiện phép nhân, 𝑏 sau đó cũng được ngầm chuyển thành kiểu long long
và tích 𝑎 ∗ 𝑏 được tính theo kiểu long long, ta có phép tính đúng. Thu được 𝑑 =
999999000000
Nếu viết:
d = a * b;
Phép tính 𝑎 ∗ 𝑏 có cả hai toán hạng kiểu int, phép nhân sẽ được tính theo kiểu int
và được kết quả tràn khỏi phạm vi biểu diễn kiểu int (−231 … 231 − 1). Giá trị tính
sai (−728379968) sau đó được gán cho 𝑑… Lỗi này ta đã phân tích trong mục
trước.
Chú ý rằng nếu viết:
d = (long long)(a * b)
Ta vẫn thu được một phép tính sai do lỗi tràn số với sai lầm chưa hề được sửa
chữa. Lý do là ta bị tràn số ngay ở phép nhân, kết quả đã sai thì ép kiểu thành long
long vẫn không có gì thay đổi.
Cách ép kiểu này kế thừa từ ngôn ngữ C, bị coi là nguy hiểm với các kiểu dữ liệu có
cấu trúc, hay để lại rác trong bộ nhớ và kéo theo nhiều hành vi không xác định. C++
đã bổ sung thêm nhiều toán tử ép kiểu khác an toàn và tinh vi hơn, ta sẽ trình bày
chúng ở những chương sau. Kinh nghiệm là chỉ nên sử dụng toán tử ép kiểu nói
trên đối với các kiểu dữ liệu cơ sở mà chúng ta đã điểm qua.
6.13. So sánh hai biểu thức khác kiểu
Không chỉ các toán tử số học, biểu thức với toán tử so sánh, toán tử xử lý bit, cũng
tuân theo luật ngầm chuyển đổi kiểu, cho phép ép kiểu.
Có hai điều nên tránh khi viết biểu thức số nguyên, trừ khi bạn biết chắc chắn mình
đang làm gì.
Không dùng lẫn kiểu số nguyên có dấu và không dấu trong cùng một biểu thức
Không dùng kiểu số nguyên không dấu với phép tính trừ
Hai tiêu chuẩn trên có thể đạt được bằng cách ép kiểu. Vi phạm hai tiêu chuẩn này
dễ dẫn tới những kết quả sai và rất khó kiểm soát. Ví dụ:
1 | int a = -2;
2 | unsigned b = 3;
3 | bool c = a > b; //c = true
4 | bool d = b - 10 < 0; //d = false

61
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Ở dòng 3, 𝑎 được ép kiểu thành unsigned trước khi so sánh với 𝑏, phép so sánh
−2 > 3 trở thành 4294967294 > 3 và cho giá trị đúng (true).
Ở dòng 4, số 10 (kiểu int) được ép kiểu thành unsigned, phép tính 𝑏 − 10 tính theo
kiểu unsigned không bao giờ cho số âm được, kết quả phép so sánh là sai (false).
Nếu bạn không muốn máy cho rằng −2 > 3 hay 3 − 10 ≮ 0, hãy ép kiểu cho toán
hạng trước khi so sánh:
1 | int a = -2;
2 | unsigned b = 3;
3 | bool c = a > int(b); //c = false
4 | bool d = int(b) - 10 < 0; //d = true
6.14. Hành vi không xác định và hành vi không chuẩn
Hành vi không xác định (undefined behaviors) nghĩa là chương trình thực hiện một
hành động trong một tình trạng ngẫu nhiên nào đó mà vô tình nó làm cho hành
động trở nên đúng. Khi chạy chương trình ở máy khác, hoặc ở thời điểm khác, hành
động đó có thể cho kết quả khác.
Một hành vi không xác định hay gặp nhất là lỗi đọc giá trị biến chưa khởi tạo cũng
như chưa gán giá trị. Ví dụ:
int i;
i = i + 1;
Rất nhiều người cho rằng khi khai báo biến 𝑖, nó sẽ nhận giá trị 0 và vì thế lệnh gán
tiếp theo sẽ cho giá trị 𝑖 bằng 1. Thực ra không phải như vậy, khi khai báo biến 𝑖,
biến đó được đặt vào một vùng nhớ, vùng nhớ này có thể chứa “rác” do chương
trình trước để lại hoặc rác do chính quá trình cấp phát và giải phóng bộ nhớ tạo ra.
Lệnh gán 𝑖 = 𝑖 + 1 sẽ lấy giá trị của biến 𝑖 (đang là rác) cộng thêm 1 rồi gán lại cho
biến 𝑖, kết quả là biến 𝑖 sau lệnh gán này mang kết quả không xác định.
Còn rất nhiều kiểu hành vi không xác định khác: Truy cập phần tử ngoài mảng, giải
tham chiếu con trỏ nullptr, thay đổi giá trị biến nhiều lần ngay trong cùng một biểu
thức…
Hành vi không chuẩn, hay hành vi phụ thuộc đặc thù cài đặt (implementation-
defined behaviors) lỗi nhẹ hơn, trên thực tế chương trình vẫn chạy ổn định với một
trình dịch, một nền tảng lập trình xác định trước, nhưng vì sử dụng các lệnh đặc
thù của môi trường lập trình, khi mang sang trình dịch khác hoặc một máy tính với
kiến trúc khác, chương trình có thể không dịch được hoặc chạy sai. Ví dụ:
Lệnh tiền xử lý:
#include <bits/stdc++.h>
Lệnh này dịch tốt trên các phiên bản G++, nhưng các chương trình dịch khác không
hiểu và báo lỗi.

62
Lê Minh Hoàng
Chương 6
Toán tử và biểu thức

Một ví dụ khác là hàm __gcd(. , . ) để tính ước số chung lớn nhất của hai số. Nói
chung các hàm bắt đầu bằng hai dấu gạch nối dưới là để chương trình dịch dùng
vào mục đích riêng, không phải cho người lập trình sử dụng vì nó không phải chuẩn
C++, trong các thế hệ sau của chương trình dịch, hàm này có thể không còn hoặc
đổi tên.
Chúng ta không được lập trình những hành vi không xác định và nên hạn chế hành
vi không chuẩn.

Bài tập 6-1


Giả sử ta cần kiểm tra một số nguyên 𝑥 có phải là số có 1 chữ số trong hệ thập phân
hay không. Chỉ ra cách viết đúng trong hai cách viết biểu thức sau:
a) 0 <= x <= 9
b) 0 <= x && x <= 9
Bài tập 6-2
Giả sử 𝑥 là một số nguyên trong phạm vi từ 0 tới 9, và 𝑐 là một biến kiểu char. Chỉ
ra rằng lệnh:
c = x + '0';
hoàn toàn hợp lệ và cho 𝑐 bằng ký tự chữ số tương ứng với 𝑥 trong bảng mã ASCII
(Ví dụ nếu 𝑥 = 8 thì 𝑐 = '8')
Bài tập 6-3
Giả sử 𝑥 là một biến kiểu int và 𝑐 là một biểu thức kiểu char có giá trị là một ký tự
chữ số (từ '0' tới '9'). Chỉ ra rằng lệnh:
x = c – '0';
hoàn toàn hợp lệ và cho 𝑥 bằng giá trị số tương ứng với ký tự 𝑐 (Ví dụ nếu 𝑐 = '5'
thì 𝑥 = 5)
Bài tập 6-4
Giả sử 𝑐 và 𝐶 là hai biến kiểu char.
Chứng minh rằng nếu 𝑐 là một chữ cái thường ('a' … 'z') thì biểu thức:
C = c - 'a' + 'A';
Gán 𝐶 bằng chữ cái hoa tương ứng với 𝑐 (ví dụ nếu 𝑐 = 'b' thì 𝐶 = 'B')
Chứng minh rằng nếu 𝐶 là một chữ cái hoa ('A' … 'A') thì biểu thức:
c = C - 'a' + 'A';
Gán 𝑐 bằng chữ cái thường tương ứng với 𝐶 (ví dụ nếu 𝐶 = 'Z' thì 𝑐 = 'z')
Bài tập 6-5
Với 𝑥 và 𝑦 là hai số nguyên, giải thích kết quả của lệnh:

63
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

cout << (x > y) ? x : y;


Bài tập 6-6
Bằng kiến thức về các toán tử logic, hãy giải thích tại sao C++ không có các toán tử
gán nhanh && =, || =, bởi nếu có thì các cặp lệnh sau cũng không tương đương:
𝑎 &&= 𝑏 với 𝑎 = 𝑎 && 𝑏
𝑎 ||= 𝑏 với 𝑎 = 𝑎 || 𝑏
Bài tập 6-7
Xét phép gán 𝑥 = 𝑎 trong đó 𝑥 là một biến int và 𝑎 là một biến kiểu signed char.
Dãy bit của 𝑥 và dãy bit của 𝑎 có gì khác nhau sau phép gán, xét trong hai trường
hợp giá trị 𝑎 âm hoặc không âm.
Bài tập 6-8
Xét phép gán 𝑥 = 𝐸 trong đó 𝑥 là kiểu số nguyên 𝜆 bit và 𝐸 thuộc kiểu số nguyên
nhiều bit hơn 𝜆. Chứng minh nếu giá trị của 𝐸 nằm trong phạm vi biểu diễn của
kiểu 𝑥, chỉ cần copy 𝜆 bit thấp nhất từ 𝐸 sang 𝑥 là bảo toàn được giá trị, ngay cả
trong trường hợp 𝐸 là số âm, bit dấu nằm ở vị trí cao nhất và không được copy.
Bài tập 6-9
Năm nhuận là năm chia hết cho 400, hoặc năm chia hết cho 4 nhưng không chia
hết cho 100. Ví dụ các năm 1600, 2000, 2012, 2040 là những năm nhuận
Với một năm biểu diễn bằng số nguyên 𝑥, viết biểu thức logic cho giá trị true nếu
𝑥 là năm nhuận và cho giá trị false nếu 𝑥 không phải năm nhuận
Bài tập 6-10
Chứng minh quy tắc De Morgan đối với biểu thức logic:
! (𝐴 && 𝐵) ⇔ ! 𝐴 || ! 𝐵
! (𝐴 || 𝐵) ⇔ ! 𝐴 && ! 𝐵
Bài tập 6-11
Trên mặt phẳng với hệ tọa độ Decartes vuông góc Ο𝑥𝑦. Viết biểu thức logic kiểm
tra một điểm (𝑥, 𝑦) có nằm trên đúng một trong hai trục tọa độ không?
Bài tập 6-12
Xét đoạn chương trình sau đây:
1 | uint16_t a;
2 | uint32_t b;
3 | b = a = 1000000;
4 | a = b = 1000000;
Cho biết giá trị của 𝑎 và 𝑏 sau lệnh ở dòng 3
Cho biết giá trị của 𝑎 và 𝑏 sau lệnh ở dòng 4

64
Lê Minh Hoàng
Chương 6
Toán tử và biểu thức

65
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Chương 7. Nhập/xuất dữ liệu cơ bản


7.1. Các luồng nhập/xuất
Các chương trình chúng ta lấy làm ví dụ chỉ đơn giản in ra các giá trị lên màn hình.
Thực ra những thư viện chuẩn của C++ cung cấp rất nhiều phương thức để tương
tác với người sử dụng thông qua các thiết bị vào/ra.
C++ có khái niệm luồng (stream) để đại diện cho các thiết bị nhập/xuất, thiết bị
này có thể là bàn phím, màn hình, máy in, và cả file. Luồng là một đối tượng cho
phép ghi ký tự vào hoặc đọc ký tự ra, người lập trình không cần biết đối tượng do
luồng đó đại diện là thiết bị gì, việc đọc/ghi ký tự từ luồng sẽ biến thành các hoạt
động tương tác với thiết bị tương ứng, dựa trên các đoạn mã điều khiển được viết
trong thư viện chuẩn. Người lập trình chỉ cần biết là luồng chứa các ký tự liên tiếp
và luồng đó cho phép đọc hay cho phép ghi hay cho phép cả đọc và ghi.
Đây là những luồng được định nghĩa trong thư viện iostream.
Tên Thiết bị tương ứng
std::cin Luồng nhập chuẩn
std::cout Luồng xuất chuẩn
std::cerr Luồng báo lỗi chuẩn
std::clog Luồng biên bản chuẩn
Ban đầu luồng nhập chuẩn được gán cho bàn phím, luồng xuất chuẩn, luồng báo
lỗi chuẩn và luồng biên bản chuẩn được gán cho màn hình. Những luồng này hoàn
toàn có thể định hướng lại sang thiết bị khác, nên chúng ta phải coi như chúng ứng
với các thiết bị khác nhau.
7.2. Luồng xuất chuẩn
Luồng xuất chuẩn có tên là std::cout, đây là luồng chỉ cho phép ghi và mặc định nó
đại diện cho thiết bị màn hình. Đối tượng cout được định nghĩa lại toán tử << để
đẩy biểu thức đứng sau toán tử ra thiết bị xuất chuẩn. Ví dụ:
1 | int x = 4; abc1234
2 | std::cout << "abc"; //In ra xâu abc
3 | std::cout << 123; //In ra số 123
4 | std::cout << x; //In ra số 4
Mặc dù 3 lệnh ghi vào luồng std::cout ở trên có khác nhau, lệnh thứ nhất in ra một
xâu ký tự, lệnh thứ hai in ra một hằng số nguyên, lệnh thứ ba in ra giá trị của một
biến 𝑥. Tuy nhiên ta có thể coi phần đứng sau toán tử << trong ba lệnh trên là các
biểu thức, std::cout chỉ có nhiệm vụ in giá trị biểu thức đó ra thiết bị xuất chuẩn.
Biểu thức ở lệnh thứ nhất là một hằng xâu ký tự, biểu thức ở lệnh thứ hai là một
hằng số nguyên, biểu thức ở lệnh thứ ba là một biến 𝑥.

66
Lê Minh Hoàng
Chương 7
Nhập/xuất dữ liệu cơ bản

Ta cũng có thể viết gộp các toán tử << trong các lệnh trên và được kết quả tương
tự:
1 | int x = 4;
2 | std::cout << "abc" << 123 << x;
Xét lệnh std::cout << 𝐸, trong đó 𝐸 là một biểu thức, toán tử << ngoài tác dụng ghi
giá trị biểu thức 𝐸 ra thiết bị xuất chuẩn, nó còn trả về giá trị của cả biểu thức
(std::cout << 𝐸), giá trị của cả biểu thức này chính là đối tượng std::cout sau khi
giá trị biểu thức 𝐸 đã được ghi ra. Điều này giải thích tính logic của lệnh ở dòng 2
nếu viết thêm các cặp dấu ngoặc đơn biểu thị quá trình xử lý biểu thức:
((std::cout << "abc") << 123) << x;
Khi ký tự '\n' được ghi ra luồng xuất chuẩn, thiết bị xuất chuẩn tương ứng sẽ xuống
dòng để những lệnh in ra tiếp theo được bắt đầu trên một dòng mới.
1 | int x = 4; abc
2 | std::cout << "abc\n"; 123
3 | std::cout << 123 << '\n'; 4
4 | std::cout << x << '\n';
Thay vì in ra ký tự '\n', ta có thể in ra đối tượng std::endl để xuống dòng, chú ý là
đối tượng này không phải là ký tự và cách thức hoạt động có khác so với ký tự '\n'.
Ta sẽ trình bày rõ hơn ở cuối chương.
7.3. Luồng nhập chuẩn
Luồng nhập chuẩn có tên là std::cin, đây là luồng chỉ cho phép đọc và mặc định nó
đại diện cho thiết bị bàn phím. Đối tượng std::cin được định nghĩa lại toán tử >>
để đọc dữ liệu từ thiết bị nhập chuẩn ra một biến đứng sau toán tử (biến thuộc
kiểu cơ sở hoặc kiểu xâu ký tự). Ví dụ một chương trình cho nhập vào hai số nguyên
và in ra tổng hai số đó:
1 | #include <iostream> Cho 2 so nguyen: 2 3
2 | Tong hai so = 5
3 | int main()
4 | {
5 | int x, y;
6 | std::cout << "Cho 2 so nguyen: ";
7 | std::cin >> x;
8 | std::cin >> y;
9 | std::cout << "Tong hai so = " << x + y;
10 | }
Khi chạy chương trình,
Lệnh ở dòng 6 in ra thông báo “Cho 2 so nguyen: ”.
Hai lệnh ở dòng 7 và dòng 8 sẽ đọc giá trị hai biến 𝑥, 𝑦 từ thiết bị nhập chuẩn (chờ
người dùng gõ vào giá trị 2 số nguyên từ bàn phím, hai giá trị này có thể gõ cách

67
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

nhau bởi một hoặc vài dấu cách, hoặc dấu xuống dòng, nhưng cần phải bấm Enter
khi kết thúc nhập).
Lệnh ở dòng 9 in ra “Tong hai so = ”, tiếp theo là giá trị biểu thức 𝑥 + 𝑦 là tổng hai
số được nhập vào.
Lệnh std::cin >> 𝑥 còn là một biểu thức có giá trị đúng bằng đối tượng std::cin
khi đã đọc xong giá trị cho biến 𝑥. Vì vậy hai lệnh ở dòng 7 và dòng 8 có thể viết
gộp thành
std::cin >> x >> y;
vì lệnh này được diễn giải:
(std::cin >> x) >> y;
7.4. Các luồng khác
Luồng std::cerr và std::clog cùng loại với std::cout, đều là để tương tác với các thiết
bị ra (output devices), chúng thuộc các kiểu luồng xuất (output stream) – chỉ cho
phép ghi ra luồng. Trong khi đó std::cin thuộc kiểu luồng nhập (input stream) – chỉ
cho phép đọc từ luồng.
Có một số luồng khác như std::wcout, std::wcerr, std::wclog, std::wcin dùng để xử
lý các ký tự thuộc bảng mã lớn hơn như Unicode, các bạn có thể tham khảo trong
các tài liệu khác.
Trong lập trình, luồng cerr thường được dùng để ghi ra các thông báo lỗi, còn luồng
clog thường để ghi biên bản trong quá trình chạy nhằm mục đích gỡ rối.
Khi nhiều chương trình chạy đồng thời, luồng xuất của chương trình này có thể trở
thành luồng nhập của chương trình khác thông qua cơ chế định hướng lại
(redirect) thiết bị nhập/xuất. Một trong những ví dụ là trình dịch G++ và
Code∷Blocks: Khi Code∷Blocks dịch chương trình, nó gọi G++ để dịch và lấy về các
thông báo lỗi, kết quả dịch thông qua các luồng xuất của G++, từ đó hiển thị thông
báo lỗi, dòng lỗi, hay các cảnh báo lên giao diện của chính Code∷Blocks.
7.5. Một số chi tiết kỹ thuật
7.5.1. Vùng đệm đọc/ghi
Các luồng, trong quá trình tương tác với thiết bị, có thể sử dụng một vùng bộ nhớ
gọi là đệm (buffer). Ví dụ mỗi lượt máy sẽ đọc một lượng lớn dữ liệu từ thiết bị
nhập vào vùng đệm, các lệnh nhập lấy dữ liệu từ vùng đệm, cho tới khi hết dữ liệu
trong vùng đệm thì máy lại đọc một lượng lớn dữ liệu tiếp theo… Cách làm ngược
lại với các thiết bị xuất. Các lệnh xuất ghi dữ liệu ra vùng đệm, cho tới khi vùng
đệm đầy thì thực hiện xả đệm: toàn bộ vùng đệm được đẩy ra thiết bị xuất và vùng
đệm lại được làm rỗng để sẵn sàng đón nhận dữ liệu kế tiếp…

68
Lê Minh Hoàng
Chương 7
Nhập/xuất dữ liệu cơ bản

Sử dụng vùng đệm cho phép giảm số lần khởi động/tắt mô tơ ổ đĩa, giảm số lần
thiết lập kết nối mạng, tăng độ bền thiết bị và giúp quá trình đọc/ghi nhanh hơn
đáng kể.
Bây giờ ta sẽ trình bày sự khác nhau giữa ký tự ‘\n’ và đối tượng std::endl khi ghi
ra một luồng xuất:
Lệnh std::cout << '\n' ghi ký tự xuống dòng ra vùng đệm bình thường như
mọi ký tự khác. Nếu vùng đệm đầy mới thực hiện phép xả đệm.
Lệnh std::cout << std::endl cũng ghi ký tự xuống dòng ra vùng đệm, nhưng
ngay sau đó nó thực hiện lệnh xả đệm (std::cout.flush()). Kết quả in ra trên
thiết bị xuất chuẩn (màn hình) ngay tức thì.
Vậy khi nào dùng '\n' và khi nào dùng std::endl?
Ta dùng std::endl khi cần thấy kết quả ngay (chẳng hạn khi ghi những thông tin lỗi
ra cerr hoặc thông tin để gỡ rối ra clog). Lưu ý rằng std::endl làm mất hiệu ứng của
bộ nhớ đệm, chậm hơn đáng kể so với '\n' nếu phải in ra nhiều dòng.
Lời khuyên ở đây là dùng std::endl nếu hiểu công dụng của nó, còn nếu không, ta
dùng '\n' và khi cần xả đệm linh hoạt có thể viết thêm lệnh std::cout.flush() (hoặc
std::cout << std::flush);
Việc xả đệm thường được dùng nếu chương trình tương tác với người. Các thông
báo phải được in ra ngay lập tức để người dùng quyết định hành động, chẳng hạn
nếu ta chạy chương trình ở chế độ gỡ rối, từng dòng một, nếu chạy qua một dòng
có lệnh xuất nhưng dữ liệu không in ra màn hình do vẫn nằm trong vùng đệm thì
rất khó gỡ rối.
Khi chương trình kết thúc, việc xả đệm được thực hiện tự động trước khi thiết bị
xuất đóng lại. Chính vì vậy, chương trình không bao giờ nên sử dụng std::endl mà
nên dùng '\n' nếu chạy trong các hệ thống kiểm định tự động bằng test. Ngoại trừ
một vài trường hợp hãn hữu, chẳng hạn hai chương trình thực hiện một công việc
hay một trò chơi tương tác: luồng xuất của mỗi chương trình được định hướng vào
luồng nhập của chương trình còn lại. Khi một chương trình gửi thông điệp cho
chương trình còn lại và chờ phản hồi, việc xả đệm khiến thông điệp chính thức
được gửi đi và do đó, chương trình còn lại mới có thể xử lý và phản hồi.
7.5.2. Cơ chế đồng bộ luồng C và C++
Ngôn ngữ C++ được phát triển từ ngôn ngữ C, các chuẩn C++ vẫn cố gắng giữ được
tính tương thích nhất định với các chương trình cũ trên ngôn ngữ C. Một trong
những sự thỏa hiệp là C++ vẫn giữ cơ chế nhập xuất của C (scanf/printf) nhưng cố
gắng đồng bộ hai cơ chế nhập xuất. Lệnh ghi ra thiết bị xuất chuẩn của C là
printf(… ), còn lệnh ghi ra thiết bị xuất chuẩn của C++ là std::cout << ⋯. Mặc dù

69
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

chúng đều tương tác với thiết bị xuất chuẩn nhưng chúng lại là hai luồng hoạt động
tương tranh. Có nghĩa là nếu chương trình sử dụng lẫn lộn cả hai kiểu lệnh trên,
hai luồng xuất chuẩn của C và C++ có thể tranh nhau sử dụng thiết bị, dẫn đến tình
trạng lệnh xuất trước nhưng kết quả có thể hiển thị sau.
Để tránh tình trạng tương tranh, C++ đặt mặc định cơ chế đồng bộ giữa luồng nhập
xuất của nó và luồng nhập xuất cũ của C. Chẳng hạn, cho luồng C++ xả trước mỗi
lệnh ghi ra luồng C và ngược lại, cho luồng C xả trước mỗi lệnh ghi ra luồng C++.
Việc này lại làm cơ chế nhập xuất chậm đi đáng kể.
Để tăng tốc độ nhập xuất, khi chắc chắn rằng chương trình không sử dụng lẫn lộn
các luồng nhập xuất của C và C++, ta có thể tắt cơ chế đồng bộ này bằng lệnh:
std::ios_base::sync_with_stdio(false);
Tham số của lệnh là false nếu muốn tắt cơ chế đồng bộ luồng nhập xuất của C và
C++, nếu muốn bật lại chế độ đồng bộ, thực hiện lệnh trên với tham số true.
7.5.3. Cơ chế giằng luồng nhập/xuất
Ta xét hai lệnh sau:
std::cout << "Cho so nguyen duong x: ";
std::cin >> x;
Trong mục 7.5.1, ta đã trình bày về hiệu ứng của vùng đệm nhập/xuất. Hãy hình
dung nếu dòng thông báo “Cho so nguyen duong x: ” vẫn còn trong vùng đệm và
chưa được in ra màn hình, khi mà lệnh ở dòng sau đã chờ nhập 𝑥: người dùng sẽ
thấy một màn hình trống, không biết phải làm gì.
C++ không để xảy ra tình trạng này bằng cơ chế giằng luồng std::cin với luồng
std::cout: Mỗi khi có lệnh nhập từ std::cin, luồng std::cout sẽ xả trước. Điều này lại
khiến cho quá trình nhập xuất chậm đi đáng kể.
Khi chương trình chạy trong các hệ thống kiểm định tự động bằng test, có thể
không cho std::cin được giằng với bất kỳ luồng xuất nào nữa bởi lệnh:
std::cin.tie(nullptr);
Còn nếu muốn cho std::cin tái thiết việc giằng với std::cout, ta có thể viết lệnh
std::cin.tie(&std::cout).
Để hiểu rõ hơn về hiệu ứng bộ nhớ đệm cũng như cơ chế giằng luồng. Ta xét
chương trình nhập vào một số nguyên 𝑥 và cho biết 𝑥 là số chẵn hay số lẻ.

70
Lê Minh Hoàng
Chương 7
Nhập/xuất dữ liệu cơ bản

1 | #include <iostream>
2 | using namespace std;
3 |
4 | int main()
5 | {
6 | ios_base::sync_with_stdio(false);
7 | cin.tie(nullptr);
8 | int x;
9 | cout << "Cho so nguyen x: ";
10 | cin >> x;
11 | cout << x << " la so " << (x % 2 == 0 ? "chan" : "le");
12 | }
Chạy chương trình, ta có thể thấy trên màn hình không hiện ra thông báo gì, nhập
vào số 8, chương trình chạy và kết thúc với giao diện như sau:
8
Cho so nguyen x: 8 la so chan
Điều này được lý giải là tất cả các dữ liệu in ra thiết bị xuất chuẩn vẫn còn nằm
trong vùng đệm, chưa đưa ra thiết bị kể cả khi người dùng đã nhập vào giá trị 𝑥.
Đến khi chương trình kết thúc, vùng đệm mới được xả và in ra hết.
Có hai cách khắc phục mà các bạn có thể thử nghiệm:
Bỏ dòng 7 hoặc biến nó thành chú thích: //cin.tie(nullptr);
Cách khác là thay vào dòng 9 thành: cout << "Cho so nguyen x: " << flush;
Lời khuyên là nên dùng cách thứ hai: xả đệm một cách chủ động khi cần thiết.
Trong bài thi của các kỳ thi lập trình, hai lệnh ở dòng 6 và dòng 7:
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
thường là những lệnh được thực hiện đầu tiên nhằm tăng tốc độ nhập xuất, điều
này quan trọng khi mà giới hạn thời gian cho mỗi test chấm được tính chi li đến
từng milli giây. Tuy nhiên trong các chương trình ví dụ, đưa hai dòng này vào trong
mọi chương trình sẽ làm dài dòng thêm và vướng mắt không cần thiết. Các chương
trình trong sách sẽ không có hai lệnh này, các bạn có thể thêm vào nếu thực tế cần
thiết.

Bài tập 7-1


Viết chương trình nhập vào hai số nguyên 𝑎, 𝑏 (|𝑎|, |𝑏| ≤ 109 ). Cho biết tổng 𝑎 + 𝑏,
hiệu 𝑎 − 𝑏, tích 𝑎 × 𝑏 và trung bình cộng của 𝑎 và 𝑏
Bài tập 7-2
Viết chương trình nhập vào hai số thực 𝑎, 𝑏 trong đó 𝑎 ≠ 0. In ra nghiệm của
phương trình bậc nhất: 𝑎𝑥 + 𝑏 = 0

71
Ngôn ngữ lập trình C++
Phần II
CÁC THÀNH TỐ CƠ BẢN CỦA C++

Bài tập 7-3


Viết chương trình nhập vào hai số nguyên 𝑎, 𝑏. Xét tập các số nguyên ∈ [𝑎; 𝑏], in ra
tổng các số trong tập, tổng các số chẵn trong tập và tổng các số lẻ trong tập.
Bài tập 7-4
Viết chương trình nhập vào hai số nguyên dương 𝑎, 𝑏. In ra số nguyên 𝑞 nhỏ nhất
thỏa mãn 𝑎 < 𝑏𝑞
Bài tập 7-5
Viết chương trình nhập vào một số nguyên dương 𝑛, in ra ba số nguyên liên tiếp
có tổng bằng 𝑛 hoặc thông báo rằng không tồn tại ba số nguyên như vậy
Bài tập 7-6
Viết chương trình nhập vào một chữ cái thường tiếng Anh, in ra chữ cái hoa tương
ứng
Bài tập 7-7
Viết chương trình nhập vào một chữ cái hoa tiếng Anh, in ra chữ cái thường tương
ứng
Bài tập 7-8
Viết chương trình nhập vào một chữ cái tiếng Anh, in ra chữ cái liền trước và liền
sau trong bảng mã ASCII
Bài tập 7-9
Cho một nền nhà hình chữ nhật kích thước 𝑚 × 𝑛. Một người muốn lát nền bằng
các viên gạch hình vuông kích thước 𝑘 × 𝑘. Ban đầu các viên gạch kích thước như
nhau nhưng mỗi viên gạch có thể mài cho nhỏ đi, hỏi để lát kín nền nhà cần mua ít
nhất bao nhiêu viên gạch?
Viết chương trình nhập vào ba số nguyên dương 𝑚, 𝑛, 𝑘 như mô tả ở trên và in ra
số viên gạch cần mua
Bài tập 7-10
Viết chương trình nhập vào 4 số thực 𝑎, 𝑏, 𝑐, 𝑑, kiểm tra xem hai đoạn [𝑎, 𝑏] và
[𝑐, 𝑑] trên trục số có điểm chung hay không.

72
Lê Minh Hoàng
Phần III.
Lập trình cấu trúc

Lập trình cấu trúc (structured programming) là một mô hình lập trình nhằm viết
ra các chương trình rõ ràng, chất lượng, giảm thời gian phát triển cũng như bảo
trì chương trình.
Trong khi các ngôn ngữ lập trình bậc thấp chỉ cho phép cấu trúc lệnh tuần tự và
lệnh nhảy, các ngôn ngữ lập trình bậc cao bổ sung thêm các cấu trúc lệnh chọn,
lệnh lặp, khái niệm khối, chương trình con và đệ quy. Những thành tố này được
sử dụng rộng rãi và thay thế dần cho lệnh nhảy (yếu tố dễ làm chương trình mất
kiểm soát).
Hầu hết các nhà khoa học máy tính đều thừa nhận rằng việc học và áp dụng các
khái niệm lập trình cấu trúc là rất hữu ích, tuy vậy vẫn còn nhiều tranh cãi xung
quanh việc chấm dứt hay duy trì sự tồn tại của lệnh nhảy. C++ hỗ trợ đầy đủ các
thành phần của lập trình cấu trúc và cũng duy trì lệnh nhảy, bởi trong vài trường
hợp, việc cố gắng thay thế lệnh nhảy một cách cứng nhắc sẽ làm chương trình
phức tạp thêm đáng kể.
Trong phần này, ngoài các khái niệm của lập trình cấu trúc, ta cũng sẽ làm quen
với khái niệm bài toán trong tin học, thuật toán và phương pháp tinh chế từng
bước trong lập trình chuyển giao thuật toán cho máy tính.
Phần III
Lập trình cấu trúc

Chương 8. Các cấu trúc điều khiển


8.1. Lệnh đơn
C++ quan niệm lệnh đơn giản (hay lệnh đơn) là:
Lệnh khai báo (và khởi tạo) biến.
Biểu thức kết thúc bởi dấu chấm phẩy “;”. Biểu thức có thể là biểu thức gán,
biểu thức số học, biểu thức logic, biểu thức điều kiện, biểu thức dấu phẩy,… nói
chung là tất cả các loại biểu thức khi cho thêm dấu chấm phẩy để kết thúc, nó
trở thành một lệnh đơn giản và khi lệnh này thi hành, biểu thức sẽ được đánh
giá kết quả. Ví dụ 12345; hay (4 + 2) * 3; là những lệnh đơn giản hoàn toàn
hợp lệ, có điều chúng vô dụng mà thôi.
Lệnh rỗng (chỉ gồm dấu chấm phẩy)
Lệnh đơn giản bao giờ cũng có dấu chấm phẩy cuối cùng.
Các ví dụ đã đề cập chỉ gồm các lệnh đơn giản, được thi hành theo thứ tự xuất hiện
trong chương trình. Tuy nhiên chương trình không chỉ giới hạn trong cấu trúc đó,
nó còn có thể quyết định thực hiện hay không thực hiện một loạt lệnh, hoặc thực
hiện lặp đi lặp lại một loạt lệnh nhiều lần…
Ta sẽ giới thiệu lần lượt các cấu trúc điều khiển của C++. Các cấu trúc điều khiển
này dễ học hơn nhiều so với ngữ pháp tự nhiên. Nhưng từ việc hiểu đúng tới lúc
dùng được thành thạo lại cần một quá trình luyện tập bền bỉ và rút kinh nghiệm
nghiêm túc. Có rất nhiều ví dụ trong chương này không chỉ để hiểu hoạt động của
cấu trúc điều khiển, mà để ôn lại các khái niệm đã trình bày trong chương trước
cũng như giới thiệu một số hàm chuẩn nhằm hỗ trợ việc lập trình hiệu quả hơn.
8.2. Cấu trúc tuần tự
Khi các câu lệnh được viết liên tiếp tạo thành một khối lệnh, chúng sẽ được thi
hành tuần tự theo đúng thứ tự ấy.
8.2.1. Giải phương trình bậc nhất
Ví dụ đầu tiên được chọn là chương trình giải phương trình bậc nhất:
𝑎𝑥 + 𝑏 = 0 (𝑎 ≠ 0)
𝑏
Ta biết rằng phương trình này có duy nhất một nghiệm 𝑥 = − 𝑎. Chương trình có
nhiệm vụ cho nhập vào hai số thực 𝑎, 𝑏 và in ra nghiệm 𝑥 của phương trình.

74
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Chương trình 3
1 | #include <iostream> Cho a, b = 3 2
2 | using namespace std; Nghiem x = -0.666667
3 |
4 | int main()
5 | {
6 | double a, b;
7 | cout << "Cho a, b = ";
8 | cin >> a >> b;
9 | cout << "Nghiem x = " << -b / a;
10 | }
Cấu trúc tuần tự thể hiện trong khối lệnh của hàm main:
Dòng 6 khai báo hai biến số thực double tên 𝑎 và 𝑏.
Dòng 7 là lệnh in ra màn hình dòng chữ “Cho a, b = ”.
Dòng 8 là lệnh đọc dữ liệu, chờ người dùng nhập vào hai giá trị số thực, sau đó giá
trị thứ nhất được gán cho biến 𝑎, giá trị thứ hai được gán cho biến 𝑏
Dòng 9 là lệnh in ra màn hình dòng chữ “Nghiem x = ”, tiếp theo là giá trị của biểu
thức −𝑏/𝑎 ứng với hai giá trị 𝑎, 𝑏 vừa nhập vào.
8.2.2. Tính diện tích hình tròn
Ví dụ tiếp theo là chương trình tính diện tích hình tròn biết đường kính của nó. Ta
𝑑2
biết rằng hình tròn đường kính 𝑑 có diện tích bằng 𝜋 × . Giá trị số 𝜋 có thể viết
4
xấp xỉ trong hệ thập phân, tuy nhiên làm như vậy dễ nhầm lẫn.
Thư viện cmath cung cấp rất nhiều hàm, trong đó có các hàm lượng giác và hàm
ngược của chúng:
Tên hàm Tham số Giá trị kết quả Tên hàm toán học
std::cos(α) 𝛼 ∈ (−∞; +∞) ∈ [−1; +1] Cosine of α
std::sin(α) 𝛼 ∈ (−∞; +∞) ∈ [−1; +1] Sine of α
std::tan(α) 𝛼 ∈ (−∞; +∞) ∈ [−∞; +∞] Tangent of α
std::acos(t) 𝑡 ∈ [−1; +1] ∈ [0; 𝜋] Arccosine of t
std::asin(t) 𝑡 ∈ [−1; +1] ∈ [−𝜋/2; +𝜋/2] Arcsine of t
std::atan(t) 𝑡 ∈ [−∞; +∞] ∈ [−𝜋/2; +𝜋/2] Arctangent of t
std::atan2(x, y) 𝑥, 𝑦 ∈ [−∞; +∞] ∈ [−𝜋/2; +𝜋/2]
Phần III
Lập trình cấu trúc

Trong bảng trên, 𝛼 là số thực tương ứng với số đo cot 𝛼 =


1
tan 𝛼
góc theo đơn vị radian (rad) * (180° = 𝜋 (rad) ).
C++ không cung cấp hàm cot(𝛼) để tính cotangent,
tuy nhiên ta có thể tính qua công thức: sin 𝛼 tan 𝛼
cot(𝛼) = 1/ tan(𝛼) 𝛼
1 cos 𝛼
Cách viết miền giá trị [−∞; +∞] có nghĩa là tham
số hoặc kết quả hàm có thể bằng INFINITY và -
INFINITY (hai giá trị đặc biệt trong các kiểu số
thực)
Các hàm acos, asin atan là hàm ngược tương ứng với các hàm cos, sin và tan:
acos(𝑡) = 𝛼 ⟹ cos(𝛼) = 𝑡
asin(𝑡) = 𝛼 ⟹ sin(𝛼) = 𝑡
atan(𝑡) = 𝛼 ⟹ tan(𝛼) = 𝑡
Hàm atan2(𝑥, 𝑦) trả về số đo góc (có hướng) tạo bởi tia Ο𝑥 với tia OM trong đó M
là điểm có tọa độ (𝑥, 𝑦). Ví dụ: atan2(0. ,1. ) = atan(𝐼𝑁𝐹𝐼𝑁𝐼𝑇𝑌) = 𝜋/2.
Các hàm lượng giác và hàm ngược của chúng có nhiều phiên bản, đều trả về kết
quả kiểu số thực. Kiểu này chính là kiểu của tham số nếu tham số thuộc kiểu số
thực, và là kiểu double nếu tham số thuộc kiểu số nguyên. Trình dịch sẽ lựa chọn
phiên bản phù hợp với tham số của hàm.
Bằng các hàm ngược của hàm lượng giác, ta có thể tính hằng số 𝜋 bằng nhiều cách:
𝜋 = atan(1) × 4 = acos(−1) = atan(INFINITY) × 2 = atan2(0,1) × 2
Ta chọn cách viết đầu tiên để khai báo hằng số 𝜋:
const long double Pi = std::atan(1.L) * 4;

*Trên đường tròn đơn vị (bán kính 1), lấy một cung tròn độ dài 1, góc ở tâm tương ứng với cung
đó có độ lớn đúng 1 radian.

76
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Chương trình 4
1 | #include <iostream> Cho duong kinh: 2
2 | #include <cmath> Dien tich: 3.14159
3 | using namespace std;
4 | const long double Pi = atan(1.L) * 4;
5 |
6 | int main ()
7 | {
8 | long double d;
9 | cout << "Cho duong kinh: ";
10 | cin >> d;
11 | cout << "Dien tich: " << d * d / 4 * Pi;
12 | }
Biến 𝑑 kiểu long double được khai báo trong khối lệnh của hàm main, trong khi
hằng Pi được khai báo bên ngoài. Một khai báo không nằm trong bất kỳ hàm nào
gọi là khai báo toàn cục (global). Những định danh (tên) khai báo toàn cục có thể
dùng ở bất cứ đâu trong chương trình, còn những định danh khai báo trong khối
lệnh chỉ được dùng nội bộ trong khối lệnh đó mà thôi. Trong chương trình đơn
giản này chỉ có một hàm, có thể khai báo hằng Pi trong khối lệnh hàm main hoặc
khai báo biến 𝑑 là biến toàn cục đều được.
Lý do cách tính hằng 𝜋 = atan(1) × 4 được chọn mà không dùng acos(−1) khá
đặc biệt. Thực ra trong các bộ đồng xử lý toán học chuyên tính toán số thực, cách
viết acos(−1) không những đúng mà còn ngắn gọn hơn. Tuy nhiên khi tính toán
và kể cả gán giá trị số thực, người ta luôn đề phòng một sai số nhất định do không
biết trình dịch và bộ xử lý làm gì bên trong. Nếu giá trị −1 vì một lý do nào đó bị
giảm đi dù chỉ một lượng rất nhỏ, nó sẽ nằm ngoài khoảng phạm vi chấp nhận cho
tham số ([−1; +1]), khiến cho phép tính trở thành hành vi không xác định (thông
thường hàm acos() sẽ trả về NAN nếu tham số nằm ngoài khoảng [−1; +1]).
Khi in một giá trị biểu thức số thực, mặc định luồng xuất chuẩn sẽ in ra tối đa 6 chữ
số. Muốn in ra khuôn dạng số thập phân với 𝑘 chữ số sau dấu chấm thập phân, ta
dùng lệnh:
std::cout << std::fixed << std::setprecision(k);
Lệnh này đẩy ra std::cout hai đối tượng: std::fixed để thiết lập in số thực theo
khuôn dạng thập phân thông thường, tiếp theo là đẩy ra std::cout đối tượng
std::setprecision(𝑘) để thiết lập in số thực với 𝑘 chữ số sau dấu chấm thập phân.
Sau khi đã thiết lập, tất cả giá trị số thực đẩy ra std::cout sẽ được in ra dưới khuôn
dạng đó. Chú ý nạp thư viện iomanip để dùng được std::setprecision(. ).
Phần III
Lập trình cấu trúc

Chương trình 5
1 | #include <iostream> Cho duong kinh: 2
2 | #include <cmath> Dien tich: 3.1415926535897932
3 | #include <iomanip>
4 | using namespace std;
5 | const long double Pi = atan(1.L) * 4;
6 |
7 | int main ()
8 | {
9 | long double d;
10 | cout << "Cho duong kinh: ";
11 | cin >> d;
12 | cout << fixed << setprecision(16);
13 | cout << "Dien tich: " << d * d / 4 * Pi;
14 | }
Ta mới làm quen với hàm chuẩn std::atan. C++ còn rất nhiều hàm chuẩn trong các
thư viện nữa. Nhưng cần chú ý là những hàm đó thường có 2 phiên bản: Phiên bản
dành cho C (không có std::) và phiên bản dành cho C++ (có std::). Chẳng hạn có
hai hàm khác nhau của thư viện cmath: Hàm atan và hàm std::atan, hàm atan luôn
trả về giá trị kiểu double còn hàm std::atan trả về giá trị cùng kiểu với tham số nếu
tham số thuộc kiểu số thực. Điều này cũng tương tự với các hàm khác như hàm lấy
căn (sqrt và std::sqrt), hàm lấy giá trị tuyệt đối (abs và std::abs). Ta nên dùng phiên
bản của C++ có nhiều tính năng hơn (khi đã khai báo using namespace std; thì chắc
chắn phiên bản được dùng là dành cho C++)
8.3. Cấu trúc ghép
Khi các câu lệnh được viết liên tiếp tạo thành một khối lệnh và được đặt vào giữa
cặp ngoặc nhọn ({… }), chúng được coi như một lệnh duy nhất gọi là lệnh ghép.
Những lệnh nằm trong cặp dấu ngoặc nhọn được gọi là lệnh thành phần của lệnh
ghép.
Công dụng của lệnh ghép là ở một số nơi trong chương trình chỉ được viết một
lệnh duy nhất, nếu chúng ta muốn thi hành nhiều lệnh, cần phải “gói” chúng vào
thành một lệnh ghép.
Lệnh ghép không cần kết thúc bởi dấu chấm phẩy “;” (dù có thừa cũng không sao).
Phần nằm giữa hai dấu ngoặc nhọn là khối lệnh của lệnh ghép. Những định danh
(hằng, biến, …) khai báo bên trong một khối lệnh chỉ được dùng bên trong khối
lệnh đó mà thôi.

78
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

1 | #include <iostream> #include <iostream>


2 |
3 | int main() int main()
4 | { {
5 | int a = 0; int a = 0;
6 | { //Bắt đầu lệnh ghép { //Bắt đầu lệnh ghép
7 | int b = a + 1; //OK int b = a + 1; //OK
8 | std::cout << b; //OK } //Kết thúc lệnh ghép
9 | } //Kết thúc lệnh ghép std::cout << b; //Lỗi
10 | } }
Hai chương trình trên đều có biến 𝑎 được khai báo và khởi tạo ở dòng 5, nằm trong
khối lệnh của hàm main, nên trong bất kỳ nơi nào của hàm main nó cũng được
nhìn thấy. Điều này làm cho lệnh ở dòng 7 của cả hai chương trình hoàn toàn hợp
lệ: Khai báo biến 𝑏 trong khối lệnh của lệnh ghép và khởi tạo nó bằng 𝑎 + 1.
Dòng 8 của chương trình bên trái nằm trong khối lệnh của lệnh ghép, nó nhìn thấy
biến 𝑏 và như thế lệnh: std::cout << b; hoàn toàn hợp lệ. Tuy nhiên dòng 9 của
chương trình bên phải nằm ngoài khối lệnh của lệnh ghép, nó sẽ không nhìn thấy
biến 𝑏 và báo lỗi biên dịch.
Lệnh ghép nói chung chỉ dùng kết hợp với các lệnh khác nên các ví dụ về lệnh ghép
cũng sẽ được trình bày sau.
Khái niệm khối lệnh có ý nghĩa quan trọng về phạm vi nhìn thấy của một định danh.
Những cấu trúc lựa chọn, lặp trình bày tiếp theo đây cũng đều có khối lệnh của
riêng nó xác định bởi cú pháp của lệnh. Ta sẽ trình bày rõ hơn về các khối lệnh và
phạm vi nhìn thấy của định danh trong mục 8.6, tạm thời ta hiểu rằng một định
dang (hằng, biến, kiểu) khai báo trong một khối lệnh thì không được nhìn thấy bên
ngoài khối lệnh ấy.
8.4. Cấu trúc lựa chọn
Cấu trúc lựa chọn cho phép chương trình quyết định thi hành hay bỏ qua một lệnh
tùy thuộc vào điều kiện đặt ra.
8.4.1. Lệnh if…
Cú pháp:
if («Điều kiện»)
«Lệnh»
if là từ khóa
«Điều kiện» là một biểu thức bool, cặp dấu ngoặc đơn là bắt buộc.
«Lệnh» là một lệnh thành phần của cấu trúc if…
Khi gặp cấu trúc if…, chương trình sẽ tính giá trị biểu thức «Điều kiện»:
Nếu giá trị tính được bằng true, chương trình sẽ thi hành «Lệnh»
Nếu giá trị tính được bằng false, «Lệnh» sẽ bị bỏ qua.
Toàn bộ cấu trúc if («Điều kiện») «Lệnh» cũng được coi là 1 lệnh. Khi cài đặt
chương trình cho một thuật toán diễn tả bằng ngôn ngữ tự nhiên, cấu trúc này
Phần III
Lập trình cấu trúc

được dùng để diễn tả câu: “Nếu «Điều kiện» đúng thì làm «Lệnh»”. Hình 4-1 là sơ
đồ thực hiện của lệnh if…

Bắt đầu

false
«Điều kiện»

true

«Lệnh»

Kết thúc

Hình 8-1. Sơ đồ khối của lệnh if…

 Ví dụ 1 (if…)
Viết chương trình cho nhập vào một số thực 𝑠 là điểm của một học sinh. In ra màn
hình câu “Chuc mung” nếu 𝑠 ≥ 9.

Chương trình 6
1 | #include <iostream> Diem = 9.6
2 | using namespace std; Chuc mung
3 |
4 | int main()
5 | {
6 | double s;
7 | cout << "Diem = ";
8 | cin >> s;
9 | if (s >= 9)
10 | cout << "Chuc mung";
11 | }
Trong Chương trình 6, có 4 lệnh thi hành tuần tự, bao gồm các lệnh đơn giản
ở các dòng 6, 7, 8 và một lệnh if viết trên hai dòng 9, 10.
Lệnh ở dòng 10 gọi là lệnh thành phần của lệnh if. Lệnh thành phần có thể viết trên
cùng dòng, nhưng nếu viết trên một dòng mới thì nên viết lùi vào thêm 1 khoảng
cố định (indent) để chương trình dễ đọc hơn, như ở Chương trình 6 là 4 dấu
cách. Ta sẽ nói kỹ về nguyên tắc trình bày chương trình trong mục NEEDREF.
 Ví dụ 2 (if…): Giải và biện luận phương trình bậc 2
Giải và biện luận phương trình bậc 2 với hệ số và nghiệm thực
𝑎𝑥 2 + 𝑏𝑥 + 𝑐 = 0 (𝑎 ≠ 0)
Chương trình cần cho nhập vào ba số thực 𝑎, 𝑏, 𝑐 trong đó 𝑎 ≠ 0. In ra nghiệm và
biện luận cho phương trình tương ứng.

80
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Thuật toán giải và biện luận phương trình bậc hai đã được cung cấp trong các kiến
thức đại số. Ta nhắc lại để chuyển thành chương trình:
Đầu tiên tính Δ = 𝑏 2 − 4𝑎𝑐
Nếu Δ < 0: Phương trình vô nghiệm
𝑏
Nếu Δ = 0: Phương trình có nghiệm kép 𝑥 = − 2𝑎
Nếu Δ > 0: Phương trình có hai nghiệm phân biệt:
−𝑏 + √Δ
𝑥1 =
2𝑎
−𝑏 − √Δ
𝑥2 =
2𝑎
Chương trình cần khai báo ba biến số thực 𝑎, 𝑏, 𝑐 và cho phép nhập vào giá trị cho
chúng, đây là các hệ số của phương trình. Ta khai báo thêm một biến số thực 𝐷𝑒𝑙𝑡𝑎
và gán giá trị cho nó bằng 𝑏 2 − 4𝑎𝑐. Chương trình sau đó chỉ gồm 3 lệnh if:
if (Delta < 0)…
if (Delta == 0)…
if (Delta > 0)…
Ngoài ra, thuật toán yêu cầu phải thực hiện phép khai căn bậc 2. Thư viện cmath
hỗ trợ hàm std::sqrt(𝑥) để tính căn bậc 2 của 𝑥, để sử dụng hàm std::sqrt ta chỉ cần
thêm dòng #include <cmath> vào đầu chương trình.
Kết quả hàm std::sqrt(𝑥) cùng kiểu với 𝑥 nếu 𝑥 thuộc một trong các kiểu số thực,
kết quả hàm thuộc kiểu double nếu 𝑥 thuộc kiểu số nguyên.

Chương trình 7
1 | #include <iostream> Cho a, b, c: 2 3 4
2 | #include <cmath> //sqrt Vo nghiem
3 | #include <iomanip> //setprecision
4 | using namespace std; Cho a, b, c: 4 4 1
5 | Nghiem kep x = -0.5000
6 | int main()
7 | { Cho a, b, c: 1 -5 6
8 | double a, b, c; x1 = 3.0000
9 | cout << "Cho a, b, c: "; x2 = 2.0000
10 | cin >> a >> b >> c;
11 | double Delta = b * b - 4 * a * c;
12 | cout << fixed << setprecision(4);
13 | if (Delta < 0)
14 | cout << "Vo nghiem";
15 | if (Delta == 0)
16 | cout << "Nghiem kep x = " << -b / (2 * a);
17 | if (Delta > 0)
18 | {
19 | double x1 = (-b + sqrt(Delta)) / (2 * a);
20 | double x2 = (-b - sqrt(Delta)) / (2 * a);
21 | cout << "x1 = " << x1 << '\n'
22 | << "x2 = " << x2;
23 | }
24 | }
Phần III
Lập trình cấu trúc

Trong mã lệnh hàm main của Chương trình 7 dòng 8 cho đến dòng 12 không
có gì đặc biệt: Nhập vào ba biến số thực 𝑎, 𝑏, 𝑐 và tính 𝐷𝑒𝑙𝑡𝑎 = 𝑏 2 − 4𝑎𝑐.
Lệnh ở dòng 14: cout << "Vo nghiem"; chỉ được thi hành khi điều kiện Delta < 0 ở
dòng 13 là đúng.
Lệnh ở dòng 16: cout << "Nghiem kep x = " << -b / (2 * a); chỉ được thi hành khi
Delta == 0 (dòng 15)
Từ dòng 18 đến dòng 23 là một lệnh ghép, nó chỉ được thi hành khi Delta > 0 (dòng
17). Lệnh ghép này thi hành tuần tự ba lệnh thành phần: Tính nghiệm 𝑥1, tính
nghiệm 𝑥2, sau đó in ra hai nghiệm cách nhau bởi dấu xuống dòng. Vì có tới 3 lệnh,
để trở thành lệnh thành phần của if (Delta > 0), chúng phải được gói lại thành
một lệnh ghép bằng cặp dấu ngoặc nhọn {…}
8.4.2. Lệnh if…else…
Cú pháp:
if («Điều kiện»)
«Lệnh 1»
else
«Lệnh 2»
if và else là từ khóa
«Điều kiện» là một biểu thức bool, cặp dấu ngoặc đơn là bắt buộc.
«Lệnh 1» là một lệnh thành phần của cấu trúc, sẽ được thi hành nếu «Điều kiện»
đúng.
«Lệnh 2» cũng là một lệnh thành phần của cấu trúc, sẽ được thi hành nếu «Điều
kiện» sai.
Khi gặp cấu trúc if…else…, chương trình sẽ tính giá trị biểu thức «Điều kiện»:
Nếu giá trị tính được bằng true, chương trình sẽ thi hành «Lệnh 1»
Nếu giá trị tính được là false, chương trình sẽ thi hành «Lệnh 2».
Toàn bộ cấu trúc if («Điều kiện») «Lệnh 1» else «Lệnh 2» cũng được coi là 1 lệnh.
Khi cài đặt chương trình cho một thuật toán diễn tả bằng ngôn ngữ tự nhiên, cấu
trúc này dùng để diễn tả câu: “Nếu «Điều kiện» đúng thì làm «Lệnh 1», nếu không
làm «Lệnh 2»”.
Hình 4-1 là sơ đồ thực hiện của lệnh if…else…

82
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Bắt đầu

false
«Điều kiện»

true

«Lệnh 1» «Lệnh 2»

Kết thúc

Hình 8-2. Sơ đồ khối của lệnh if…else…

 Ví dụ 1 (if…else…): Kiểm tra năm nhuận


Theo quy ước dương lịch, năm nhuận là năm chia hết cho 400 hoặc năm chia hết
cho 4 nhưng không chia hết cho 100. Viết chương trình nhập vào một số nguyên 𝑦
tương ứng với một năm dương lịch. Cho biết năm 𝑦 có phải là năm nhuận hay
không.

Chương trình 8
1 | #include <iostream> Nam: 2004
2 | using namespace std; 2004 la nam nhuan
3 |
4 | int main() Nam: 2019
5 | { 2019 khong phai nam nhuan
6 | int y;
7 | cout << "Nam: ";
8 | cin >> y;
9 | if (y % 400 == 0 || (y % 4 == 0 && y % 100 != 0))
10 | cout << y << " la nam nhuan";
11 | else
12 | cout << y << " khong phai nam nhuan";
13 | }
Các dòng từ 9 tới 12 có thể viết gộp bằng biểu thức điều kiện:
cout << y
<< (y % 400 == 0 || (y % 4 == 0 && y % 100 != 0) ? " la": " khong phai")
<< " nam nhuan";
tuy nhiên cách viết bằng lệnh if… tỏ ra sáng sủa hơn. Nói chung biểu thức điều kiện
chỉ nên dùng khi nó đơn giản, lệnh if…else… luôn là sự lựa chọn tốt và không thể
thay thế trong trường hợp hai lệnh thành phần là những lệnh phức tạp.
Để ý biểu thức điều kiện:
y % 400 == 0 || (y % 4 == 0 && y % 100 != 0)
Cặp dấu ngoặc đơn trong trường hợp này là thừa, do phép toán && ưu tiên hơn
phép toán ||. Cặp dấu ngoặc đơn này được thêm vào chỉ để đọc biểu thức dễ dàng
hơn.
Phần III
Lập trình cấu trúc

 Ví dụ 2 (if…else…): Tính độ dài phủ


Viết chương trình cho nhập vào 4 số thực 𝑎, 𝑏, 𝑐, 𝑑 trong đó 𝑎 ≤ 𝑏 và 𝑐 ≤ 𝑑. In ra
độ dài phần trục số thực bị phủ bởi hai đoạn [𝑎; 𝑏] và [𝑐; 𝑑] ([𝑎; 𝑏] ∪ [𝑐; 𝑑]).
Để thực hiện yêu cầu của bài toán, ta phải xét hai trường hợp:
Nếu hai đoạn [𝑎; 𝑏] và [𝑐; 𝑑] không có điểm chung, phần trục số bị phủ bằng
tổng độ dài hai đoạn: 𝑏 − 𝑎 + 𝑑 − 𝑐
Nếu hai đoạn [𝑎; 𝑏] và [𝑐; 𝑑] giao nhau, phần trục số bị phủ cũng là một đoạn,
đoạn đó có đầu mút trái là min(𝑎, 𝑐) và đầu mút phải là max(𝑏, 𝑑), độ dài bằng
max(𝑏, 𝑑) − min(𝑎, 𝑐)
Ta phải tìm cách viết những biểu thức điều kiện trong thuật toán trên một cách
ngắn gọn và chính xác.
Điều kiện cần và đủ để hai đoạn [𝑎; 𝑏] và [𝑐; 𝑑] không có điểm chung là có một đầu
mút phải của đoạn này nhỏ hơn đầu mút trái của đoạn còn lại, điều này có thể viết
bằng biểu thức bool:
b < c || d < a
Việc tính min và max của hai số có thể viết bằng lệnh if, tuy nhiên vì phép tính này
tương đối đơn giản, ta có thể dùng biểu thức điều kiện:
b > d ? b : d → max(b, d)
a < c ? a : c → min(a, c)

Chương trình 9
1 | #include <iostream> Cho a, b, c, d: 1 8 3 10
2 | using namespace std; Do dai phu = 9
3 |
4 | int main() Cho a, b, c, d: 1 2 3 4
5 | { Do dai phu = 2
6 | double a, b, c, d;
7 | cout << "Cho a, b, c, d: ";
8 | cin >> a >> b >> c >> d;
9 | cout << "Do dai phu = ";
10 | if (b < c || d < a) //2 đoạn rời nhau
11 | cout << b - a + d - c;
12 | else //2 đoạn giao nhau
13 | {
14 | double L = a < c ? a : c; //min(a, c)
15 | double R = b > d ? b : d; //max(b, d)
16 | cout << R - L;
17 | }
18 | }
Thư viện algorithm có sẵn hàm std::min và hàm std∷max để tính min và max của
hai biểu thức cùng kiểu. Ta cũng có thể sử dụng những hàm này để viết lại
Chương trình 9 cho gọn.

84
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Chương trình 10


1 | #include <iostream>
2 | #include <algorithm> //std::min, std::max
3 | using namespace std;
4 |
5 | int main()
6 | {
7 | double a, b, c, d;
8 | cout << "Cho a, b, c, d: ";
9 | cin >> a >> b >> c >> d;
10 | cout << "Do dai phu = ";
11 | if (b < c || d < a)
12 | cout << b - a + d - c;
13 | else
14 | cout << max(b, d) - min(a, c);
15 | }
8.4.3. Nhiều cấu trúc if…else… lồng nhau
Trong cả cấu trúc if… và if…else…, lệnh thành phần có thể là bất cứ loại lệnh gì của
C++. Trong trường hợp lệnh thành phần lại là một cấu trúc if… hoặc if…else…, ta
có một lệnh phức hợp gồm các cấu trúc if… và if…else… lồng nhau.
C++ có luật để gắn một phần else… tương ứng với một phần if… như vậy:
Xét tất cả các phần else… trong cấu trúc theo thứ tự xuất hiện, mỗi phần else… sẽ
được gắn tương ứng với phần if… chưa có else… đứng trước và gần nó nhất.
Ví dụ ta cần xử lý trên hai số nguyên 𝑎, 𝑏 theo quy trình:
Nếu 𝑏 ≠ 0:
𝑎
Nếu 𝑎 chia hết cho 𝑏 thông báo giá trị phân số 𝑏 là số nguyên
𝑎
Nếu 𝑏 = 0: Thông báo giá trị phân số 𝑏 không xác định

Xét đoạn chương trình sau:


1 | if (b != 0)
2 | if (a % b == 0) std::cout << "So nguyen";
3 | else std::cout << "Khong xac dinh";
Đoạn chương trình trên hoạt động không đúng với yêu cầu, chẳng hạn:
𝑎 = 1, 𝑏 = 2: In ra “Khong xac dinh”
𝑎 = 1, 𝑏 = 0: Không in ra gì cả
Lý do là phần else… ở dòng 3 sẽ được gắn với phần if… ở dòng 2. Cả cấu trúc
if…else… ở dòng 2 và dòng 3 là lệnh thành phần của lệnh if ở dòng 1. Nếu viết thụt
lề đúng sẽ dễ hiểu hơn:
1 | if (b != 0)
2 | if (a % b == 0) std::cout << "So nguyen";
3 | else std::cout << "Khong xac dinh";
Tuy nhiên nếu thêm một phần else tưởng như vô tác dụng, đoạn chương trình trở
nên đúng:
Phần III
Lập trình cấu trúc

1 | if (b != 0)
2 | if (a % b == 0) std::cout << "So nguyen";
3 | else;
4 | else cout << std::"Khong xac dinh";
Phần else ở dòng 3 có lệnh thành phần là lệnh rỗng. Dấu chấm phẩy chỉ có ý nghĩa
giữ cho phần else ở dòng 3 tồn tại mà không gặp lỗi biên dịch. Sự tồn tại của phần
else ở dòng 3 khiến cho nó được gắn với phần if ở dòng 2, kéo theo việc phần else
ở dòng 4 sẽ được gắn với phần if ở dòng 1.
Tuy nhiên, làm như vậy khiến cho chương trình thêm rối, cách đơn giản hơn là sử
dụng cấu trúc ghép để gói lệnh if… ở dòng 2 lại, không cho có cơ sở để dính dáng
tới phần else cuối cùng:
1 | if (b != 0)
2 | {
3 | if (a % b == 0) std::cout << "So nguyen";
4 | }
5 | else std::cout << "Khong xac dinh";
Bây giờ trình dịch đã hiểu đoạn lệnh này là một cấu trúc if…else…, phần if có lệnh
thành phần là một lệnh ghép {…}, và phần else có lệnh thành phần là lệnh
std::cout << ….
Khi cấu trúc if…else… lồng nhau quá phức tạp, dù cho đoạn code hoạt động đúng,
đôi khi các lập trình viên vẫn gói từng đoạn lệnh vào trong các lệnh ghép để phân
biệt rõ các khối lệnh.
Ta xét một ví dụ khác: Viết chương trình nhập vào hai số thực 𝑎, 𝑏. Giải và biện
luận phương trình:
𝑎𝑥 + 𝑏 = 0
Trong trường hợp 𝑎 ≠ 0 , đây là một phương trình bậc nhất và ta đã giải ở
Chương trình 3. Bài toán này không có ràng buộc 𝑎 ≠ 0 nên cần biện luận cho
trường hợp 𝑎 = 0 nữa:
Nếu 𝑎 ≠ 0: In ra nghiệm duy nhất 𝑥 = −𝑏/𝑎
Nếu không: (𝑎 = 0)
Nếu 𝑏 = 0: Thông báo phương trình có vô số nghiệm
Nếu không (𝑏 ≠ 0): Thông báo phương trình vô nghiệm
Cấu trúc trên được chuyển ra chương trình một cách tự nhiên:

86
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Chương trình 11


1 | #include <iostream> Cho a, b = 3 2
2 | using namespace std; Nghiem x = -0.666667
3 |
4 | int main() Cho a, b = 0 0
5 | { Vo so nghiem
6 | double a, b;
7 | cout << "Cho a, b = "; Cho a, b = 0 1
8 | cin >> a >> b; Vo nghiem
9 | if (a != 0)
10 | cout << "Nghiem x = " << -b / a;
11 | else
12 | if (b == 0)
13 | cout << "Vo so nghiem";
14 | else
15 | cout << "Vo nghiem";
16 | }
(Trong trường hợp cảm thấy khó khăn trong đọc hiểu và kiểm soát lệnh, có thể gói
các dòng từ 12 tới 15 thành một lệnh ghép)
8.4.4. Lệnh switch…
Cú pháp:
switch («Biểu thức»)
{
case «Hằng 1»: «Nhóm lệnh 1»
case «Hằng 2»: «Nhóm lệnh 2»
...
default: «Nhóm lệnh mặc định»
}
switch, case, default là các từ khóa
«Biểu thức» là một biểu thức mang kiểu cơ sở, ngoại trừ các kiểu số thực
«Hằng 1», «Hằng 2»,…, là các giá trị hằng không giống nhau, thuộc kiểu có thể
so sánh bằng (==) với «Biểu thức», ngoại trừ các kiểu số thực.
«Nhóm lệnh…»: Các lệnh tuần tự, nhóm lệnh có thể rỗng
Cặp dấu ngoặc đơn (…) và cặp dấu ngoặc nhọn {…} nhất thiết phải có.
Khi gặp cấu trúc switch, chương trình tính giá trị 𝐸 của «Biểu thức», sau đó lần
lượt so sánh 𝐸 với «Hằng 1», «Hằng 2», … Ngay khi gặp «Hằng 𝑖» bằng 𝐸, máy sẽ
thực hiện tuần tự từng nhóm lệnh bắt đầu từ «Nhóm lệnh 𝑖 » cho tới khi gặp lệnh
break; hoặc khi đã thực hiện xong hết các nhóm lệnh (kể cả «Nhóm lệnh mặc định»
nếu nó được viết sau «Nhóm lệnh 𝑖»).
Phần default: «Nhóm lệnh mặc định» không nhất thiết phải có. Nếu giá trị 𝐸 không
bằng với bất kỳ giá trị nào trong số «Hằng 1», «Hằng 2», …, chương trình sẽ bắt
đầu thi hành từ «Nhóm lệnh mặc định» cho tới khi gặp lệnh break; hoặc khi đã thi
hành xong hết các nhóm lệnh. Chú ý rằng phần default:… không nhất thiết phải đặt
cuối cùng (sau các phần case…).
Toàn bộ cấu trúc switch được coi là 1 lệnh. Khi cài đặt chương trình cho một thuật
toán diễn tả bằng ngôn ngữ tự nhiên, cấu trúc này được dùng để diễn tả câu: “Tùy
Phần III
Lập trình cấu trúc

theo «Biểu thức» bằng trường hợp nào trong số các «Hằng 1», «Hằng 2», …, thi hành
các nhóm lệnh tuần tự bắt đầu từ nhóm lệnh tương ứng với hằng đó (nếu không có
trường hợp nào thỏa mãn thì bắt đầu từ nhóm lệnh mặc định) cho tới khi gặp break;
hoặc đã thi hành hết các nhóm lệnh”.
Lý do «Biểu thức» và các «Hằng…» không được phép thuộc kiểu số thực vì kiểu số
thực luôn tiềm ẩn sai số khi tính toán cũng như khi chuyển đổi kiểu, điều này có
thể gây nhầm lẫn cho phép so sánh bằng nhau.
Hình 4-1 là sơ đồ khối của lệnh switch…, hình vẽ này không hoàn toàn đúng với
các quy chuẩn về sơ đồ khối (mỗi khối chữ nhật chứa nhóm lệnh lại có 2 đường
ra). Lý do là vì lệnh switch… không thể biểu diễn sơ đồ khối chuẩn nếu nó chứa
break; - một lệnh nhảy phá vỡ tính cấu trúc. Ta sẽ trình bày về các lệnh nhảy trong
phần NEEDREF.

Bắt đầu

𝐸 = «Biểu thức»;
false

true
𝐸 == «Hằng 1» «Nhóm lệnh 1»

false break;

true
𝐸 == «Hằng 2» «Nhóm lệnh 2»
break;
false

true
… … break;

false break;

«Nhóm lệnh mặc định» Kết thúc

Hình 8-3. Sơ đồ khối của lệnh switch…

 Ví dụ 1 (switch): Tính số ngày trong tháng


Viết chương trình nhập vào số nguyên dương 𝑚 là tháng và số nguyên dương 𝑦 là
năm. Cho biết tháng 𝑚 năm 𝑦 có bao nhiêu ngày.
Ta biết rằng bất kể năm nào, các tháng 1, 3, 5, 7, 8, 10, 12 đều có 31 ngày. Các tháng
4, 6, 9, 11 đều có 30 ngày. Riêng tháng 2 có thể có 29 hoặc 28 ngày tùy theo 𝑦 có
phải năm nhuận hay không.
Chương trình này có thể viết bằng cấu trúc switch như sau

88
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

1 | #include <iostream> Cho thang va nam: 2 1900


2 | using namespace std; So ngay: 28
3 |
4 | int main () Cho thang va nam: 2 2020
5 | { So ngay: 29
6 | int m, y;
7 | cout << "Cho thang va nam: "; Cho thang va nam: 4 2000
8 | cin >> m >> y; So ngay: 30
9 | cout << "So ngay: ";
10 | switch (m) Cho thang va nam: 12 2019
11 | { So ngay: 31
12 | case 2:
13 | if (y % 400 == 0 || (y % 4 == 0 && y % 100 != 0))
14 | cout << 29;
15 | else
16 | cout << 28;
17 | break;
18 | case 4: case 6: case 9: case 11:
19 | cout << 30;
20 | break;
21 | default:
22 | cout << 31;
23 | }
24 | }
Các phần case… trên dòng 18 có thể viết trên cùng dòng hoặc mỗi nhãn case… riêng
một dòng đều được, trên dòng này chỉ có nhãn case 11:, có nhóm lệnh tương ứng,
các nhãn case…: khác có nhóm lệnh rỗng.
Nếu 𝑚 = 2, nhóm lệnh từ dòng 13 tới 17 được thi hành: Lệnh thứ nhất là một lệnh
if…else…: Nếu 𝑦 là năm nhuận thì in ra số 29, nếu không in ra số 28, lệnh thứ hai
là break; thoát lệnh switch.
Nếu 𝑚 ∈ {4,6,9,11}, nhóm lệnh ở dòng 19, 20 được thi hành: in ra số 30 và thoát
lệnh switch bởi break;
Nếu 𝑚 bằng bất kỳ giá trị nào liệt kê ở trên, nhóm lệnh mặc định ở dòng 22 được
thi hành: in ra số 31 và đây cũng là nơi kết thúc lệnh switch.
So sánh với cấu trúc if… và if…else…, cấu trúc switch liệt kê các trường hợp ra một
cách rõ ràng, tránh việc viết thiếu trường hợp. Mặc dù không mạnh bằng các cấu
trúc if… và if…else…, lệnh switch bổ sung một cách viết mã lệnh dễ kiểm soát hơn.
 Ví dụ 2 (switch): Tính thứ trong tuần
Ở ví dụ trước ta lập trình chỉ ra số ngày trong một tháng xác định. Nếu với một
ngày, tháng, năm cho trước, ta có công thức tính ra được nó là ngày thứ mấy, thì
khi đó ta hoàn toàn có thể thiết kế một cuốn lịch vĩnh cửu.
Bài toán đặt ra là cho ba số nguyên 𝑑, 𝑚 và 𝑦 ứng với ngày, tháng và năm. Cho biết
ngày 𝑑 tháng 𝑚 năm 𝑦 là ngày thứ mấy?
Một trong những thuật toán nổi tiếng cho vấn đề này là công thức Zeller, được
trình bày như sau:
Phần III
Lập trình cấu trúc

Công thức quan niệm không có tháng 1 và tháng 2. Tháng 1 và tháng 2 được coi là
tháng 13 và tháng 14 của năm trước. Vì vậy nếu tháng (𝑚) nhỏ hơn 3, ta cần cộng
thêm nó với 12 và giảm năm (𝑦) đi 1:
if (m < 3)
{
m += 12;
--y;
}
Công thức sử dụng khái niệm “thế kỷ” nhưng không giống với khái niệm trong
dương lịch: Năm 𝑦 thuộc thế kỷ ⌊𝑦/100⌋, tức là thế kỷ 0 tính từ năm 0 tới năm 99,
thế kỷ 1 tính từ năm 100 đến năm 199, …, thế kỷ 20 tính từ năm 2000 tới năm
2099. Các năm trong thế kỷ được đánh số từ 0 đến 99, tức là năm 𝑦 là năm thứ
𝑦 % 100 trong thế kỷ ⌊𝑦/100⌋
Gọi 𝑐 là thế kỷ có chứa năm 𝑦: 𝑐 = ⌊𝑦/100⌋
Gọi 𝑟 là số hiệu của năm 𝑦 trong thế kỷ 𝑐: 𝑟 = 𝑦 % 100
Công thức Zeller:
13(𝑚 + 1) 𝑟 𝑐
𝐶𝑜𝑑𝑒 = 𝑑 + ⌊ ⌋ + 𝑟 + ⌊ ⌋ + ⌊ ⌋ + 5𝑐
5 4 4
Khi đó tùy theo số dư của 𝐶𝑜𝑑𝑒 khi chia cho 7 (𝐶𝑜𝑑𝑒 % 7), nếu là 0 tương ứng với
thứ bảy, 1 tương ứng với chủ nhật, 2 tương ứng với thứ hai, …

Chương trình 12


1 | #include <iostream> 21 | switch (code % 7)
2 | using namespace std; 22 | {
3 | 23 | case 0:
4 | int main () 24 | cout << "Thu Bay"; break;
5 | { 25 | case 1:
6 | int d, m, y, c, r, code; 26 | cout << "Chu Nhat"; break;
7 | cout << "Cho ngay thang nam: "; 27 | case 2:
8 | cin >> d >> m >> y; 28 | cout << "Thu Hai"; break;
9 | cout << d << '/' << m << '/' << y 29 | case 3:
10 | << " la ngay "; 30 | cout << "Thu Ba"; break;
11 | if (m < 3) 31 | case 4:
12 | { 32 | cout << "Thu Tu"; break;
13 | m += 12; 33 | case 5:
14 | --y; 34 | cout << "Thu Nam"; break;
15 | } 35 | case 6:
16 | c = y / 100; 36 | cout << "Thu Sau"; break;
17 | r = y % 100; 37 | }
18 | code = d + 13 * (m + 1) / 5 + 38 | }
19 | r + r / 4 + Cho ngay thang nam: 2 9 1945
20 | c / 4 + c * 5; 2/9/1945 la ngay Chu Nhat
Lệnh break; ở dòng 36 chương trình trên là thừa, ta thêm vào chỉ để trình bày cân
đối mà thôi.

90
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

8.5. Cấu trúc lặp


Cấu trúc lặp cho phép thi hành lặp đi lặp lại một lệnh tùy thuộc vào điều kiện được
kiểm tra tại mỗi lượt lặp.
8.5.1. Lệnh while…
Cú pháp:
while («Điều kiện»)
«Lệnh»
while là từ khóa
«Điều kiện» là một biểu thức bool, gọi là điều kiện lặp. Cặp dấu ngoặc đơn là
bắt buộc.
«Lệnh» là một lệnh thành phần của cấu trúc while…
Cấu trúc while…, cũng được coi là lệnh while…, chỉ định cho chương trình thực
hiện lặp đi lặp lại việc: tính biểu thức «Điều kiện», nếu giá trị tính được là true thì
thi hành «Lệnh», nếu không, kết thúc lệnh while…
Chú ý rằng «Lệnh» sẽ không được thi hành một lần nào nếu «Điều kiện» bằng false
ngay khi đi vào cấu trúc while…
Khi cài đặt chương trình cho một thuật toán diễn tả bằng ngôn ngữ tự nhiên, cấu
trúc while… được dùng để diễn tả câu: “Chừng nào «Điều kiện» đúng thì làm (lặp
lại) «Lệnh»”. Hình 8-4 là sơ đồ thực hiện của lệnh while…

Bắt đầu

false
«Điều kiện»

true

«Lệnh»

Kết thúc

Hình 8-4. Sơ đồ khối của lệnh while…

 Ví dụ 1 (while…)
Viết chương trình cho nhập vào số nguyên dương 𝑛 (1 ≤ 𝑛 ≤ 2.109 ), tìm lũy thừa
lớn nhất của 10 thỏa mãn 10𝑘 ≤ 𝑛.
Phần III
Lập trình cấu trúc

Thuật toán cho vấn đề này có thể mô tả như sau:


Khởi tạo một biến nguyên 𝑃 = 1, rõ ràng với tình trạng này: 𝑃 = 100 ≤ 𝑛.
Chừng nào nhân 𝑃 lên 10 lần vẫn được giá trị ≤ 𝑛, ta nhân 𝑃 lên 10 lần và lặp
lại…
Cuối cùng in ra 𝑃 là giá trị cần tìm.
Chương trình:
1 | #include <iostream>
2 | using namespace std;
3 |
4 | int main ()
5 | {
6 | int n;
7 | cout << "Cho n = ";
8 | cin >> n;
9 | int p = 1; //Khởi tạo p = 100 ≤ n
10 | while (p * 10 <= n) //Chừng nào nhân p * 10 vẫn chưa vượt quá n
11 | p *= 10; //Nhân p lên 10 lần
12 | cout << p; //p là lũy thừa lớn nhất của 10 không vượt quá n
13 | }
Thuật toán khá đơn giản, nhưng cách thức viết chương trình trên lại có vấn đề.
Chương trình cho kết quả đúng với các giá trị 𝑛 không quá lớn, thậm chí khi 𝑛 bằng
1 tỉ, chương trình vẫn “may mắn” chạy đúng. Tuy nhiên khi 𝑛 bằng 2 tỉ, chương
trình sẽ treo.
Lỗi nằm ở biểu thức điều kiện p * 10 <= n của vòng lặp while… Khi 𝑛 bằng 2 tỉ
(2.109 ), vòng lặp while sẽ thử lần lượt các giá trị 𝑝 = 100 , 101 , … , 109 , đến khi giá
trị 𝑝 đạt tới 109 , phép tính 𝑝 ∗ 10 được tính theo kiểu int và gặp lỗi tràn số. Nếu
phân tích kỹ dựa trên những hiểu biết về cách thức biểu diễn số nguyên, có thể
chứng minh được sau một số lượt lặp với phép tính tràn số p *= 10, giá trị của 𝑝 sẽ
trở thành 0, từ đó trở đi, điều kiện lặp luôn đúng và giá trị 𝑝 không thay đổi nữa.
Chương trình rơi vào một vòng lặp vô hạn.
Để khắc phục lỗi này, ta có thể dùng kỹ thuật ép kiểu sang kiểu số nguyên lớn hơn
để tránh tràn số trong phép nhân (thay dòng 10 bởi while (p * 10LL <= n)). Tuy
vậy cách này không thực hiện được nếu ràng buộc dữ liệu thay đổi và cho phép
nhập giá trị lớn nhất trong kiểu số nguyên cho biến 𝑛.
Cách đơn giản hơn mà lại hiệu quả với mọi kiểu số nguyên là sử dụng biểu thức
điều kiện tương đương:
𝑛 do 𝑝 nguyên 𝑛
𝑝 × 10 ≤ 𝑛 ⟺ 𝑝 ≤ ⇔ 𝑝≤⌊ ⌋
10 10

92
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Chương trình 13


1 | #include <iostream> Cho n = 987654321
2 | using namespace std; 100000000
3 |
4 | int main ()
5 | {
6 | int n;
7 | cout << "Cho n = ";
8 | cin >> n;
9 | int p = 1; //Khởi tạo p = 100 ≤ n
10 | while (p <= n / 10) //Chừng nào p * 10 ≤ n
11 | p *= 10; //Nhân p lên 10 lần
12 | cout << p; //p = 10k lớn nhất: 10k ≤ n
13 | }
Chương trình trên cũng có thể viết không cần vòng lặp dựa trên các hàm chuẩn
được cung cấp của thư viện cmath. Trong bài này ta chỉ cần dùng hàm pow và log10
là đủ nhưng ta sẽ giới thiệu luôn một số hàm liên quan để tiện sử dụng trong những
trường hợp khác
Hàm std::pow(𝑎, 𝑥): Tính 𝑎 𝑥 theo kiểu số thực.
Hàm std::exp(𝑥): Tính 𝑒 𝑥 , ở đây 𝑒 là hằng số toán học ≈ 2.71828
Hàm std::log(𝑥): Tính logarithm tự nhiên của 𝑥 (ln 𝑥)
Hàm std::log2(𝑥): Tính logarithm nhị phân 2 của 𝑥 (log 2 𝑥)
Hàm std::log10(𝑥): Tính logarithm thập phân của 𝑥 (log10 𝑥)
Miền xác định và miền giá trị của các hàm này giống như trong các định nghĩa toán
học. Nếu các tham số cùng kiểu và đều là kiểu số thực, các hàm trên trả về kết quả
cùng kiểu với tham số, nếu không trả về kết quả kiểu double.
Quay lại bài toán của chúng ta, để tìm lũy thừa lớn nhất của 10 không vượt quá 𝑛
ta có thể tính qua công thức;
1 | long double k = std::floor(std::log10(n));
2 | int p = std::pow(10L, k);
Giải thích:
Dòng 1 tính log10 𝑛, đây là số thực 𝑥 thỏa mãn 10𝑥 = 𝑛. Số thực này được làm
tròn xuống số nguyên gần nhất 𝑘, giá trị cần tìm là 10𝑘 .
Dòng 2 tính 𝑝 = 10𝑘 chính là đáp số, vì ta đã khai báo 𝑘 kiểu long double, hàm
std::pow(10𝐿, 𝑘) sẽ được tính kiểu long double, sau đó gán kết quả sang số
nguyên 𝑝 để giữ lại phần nguyên.
Cách này tiềm ẩn nhiều rủi ro vì có sai số tính toán với số thực, đặc biệt khi chuyển
sang trình dịch khác với một kiến trúc vi xử lý khác.
 Ví dụ 2 (while…): Kiểm tra số nguyên tố
Một số tự nhiên 𝑛 là một số nguyên tố nếu nó có đúng 2 ước số dương là 1 và chính
𝑛. Ví dụ 2,3,5,7,11 là các số nguyên tố, trong khi đó 0, 1, 4, 6, 10 không phải là số
nguyên tố.
Phần III
Lập trình cấu trúc

Viết chương trình cho nhập vào một số tự nhiên 𝑛 (𝑛 ≤ 109 ) và kiểm tra xem 𝑛 có
phải là số nguyên tố hay không.
Để đơn giản, ta xét trường hợp 𝑛 ≥ 2 trước. Có rất nhiều thuật toán để kiểm tra
tính nguyên tố, đơn giản nhất là dựa vào định nghĩa: Số tự nhiên 𝑛 ≥ 2 là số
nguyên tố nếu nó thỏa mãn điều kiện: 𝑛 không có ước số nào nằm trong phạm vi
[2; 𝑛 − 1]

Khởi tạo một biến số nguyên 𝑑 = 2.


Lặp: Chừng nào 𝑛 không chia hết cho 𝑑, tăng 𝑑 lên 1 (bước này được giải thích là
ta thử và tìm một ước 𝑑 của 𝑛 tính từ 𝑑 = 2 trở đi)
Kết thúc:
Nếu bước lặp dừng và thu được 𝑑 < 𝑛, tức là 𝑛 có ước số 𝑑 ∈ [2; 𝑛 − 1], ta kết
luận 𝑛 không phải số nguyên tố.
Ngược lại, nếu bước lặp chỉ dừng khi 𝑑 == 𝑛, ta kết luận 𝑛 là số nguyên tố
Thuật toán trên có thể mô tả như sau:
1 | d = 2;
2 | while (n % d != 0) ++d;
3 | if (d < n) «Kết luận n không nguyên tố»
4 | else «Kết luận n nguyên tố»
Thuật toán này không hiệu quả nếu 𝑛 là một số nguyên tố lớn (chẳng hạn 𝑛 =
999,999,937; vòng lặp ở dòng 2 phải thực hiện gần 1 tỉ lượt lặp).
Nhận xét rằng nếu 𝑛 không phải là số nguyên tố thì vòng lặp ở dòng 2 sẽ tìm ra một
ước số 𝑑 ∈ [2; 𝑛 − 1], hơn thế nữa, 𝑑 chắc chắn ≤ √𝑛.
Nhận xét này dựa vào quan sát: Nếu 𝑛 không nguyên tố, vòng lặp ở dòng 2 không
thể dừng khi gặp giá trị 𝑑 ∣ 𝑛 mà 𝑑 > √𝑛, bởi nếu vậy, vòng lặp đã phải dừng khi
duyệt qua giá trị 𝑛/𝑑 (nhỏ hơn 𝑑 mà cũng là ước của 𝑛).
Ta viết lại đoạn chương trình theo thuật toán mới: Chỉ cần thử các số nguyên 𝑑 ∈
[2; √𝑛] mà thôi, khi 𝑑 đã vượt quá √𝑛 chắc chắn 𝑛 là số nguyên tố:
1 | d = 2;
2 | while (d <= √n && n % d != 0) ++d;
3 | if (d <= √n) «Kết luận n không nguyên tố»
4 | else «Kết luận n nguyên tố»

94
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Điều kiện d <= √n có thể viết lại thành d * d <= n để tránh sử dụng phép căn số
thực (phép tính chậm, có sai số và sinh lỗi nếu như 𝑛 âm)*.
Ta cần thêm một xử lý nhỏ nữa cho trường hợp 𝑛 < 2. Khi 𝑛 < 2 thì điều kiện lặp
không thỏa mãn ngay từ đầu, vòng lặp while… ở dòng 2 sẽ thoát ngay với giá trị
khởi tạo 𝑑 = 2 (≰ √𝑛) dẫn tới kết luận sai: 𝑛 là số nguyên tố. Để xử lý nốt trường
hợp 𝑛 < 2, ta chỉ cần thay đổi điều kiện của lệnh if ở dòng 3 thành:
if (n < 2 || d * d <= n) «Kết luận n không nguyên tố»

Chương trình 14


1 | #include <iostream> Cho n = 35
2 | using namespace std; 35 khong nguyen to
3 |
4 | int main () Cho n = 43
5 | { 43 nguyen to
6 | int n;
7 | cout << "Cho n = "; Cho n = 123456789
8 | cin >> n; 123456789 khong nguyen to
9 | int d = 2;
10 | while (d * d <= n && n % d != 0) Cho n = 999999937
11 | ++d; 999999937 nguyen to
12 | if (n < 2 || d * d <= n)
13 | cout << n << " khong nguyen to";
14 | else
15 | cout << n << " nguyen to";
16 | }
 Ví dụ 3 (while…): Tìm ước số chung lớn nhất
Viết chương trình cho nhập vào hai số nguyên 𝑎, 𝑏 (0 ≤ 𝑎, 𝑏 ≤ 109 ; 𝑎, 𝑏 không
đồng thời bằng 0), tìm số nguyên dương 𝑑 lớn nhất là ước của cả 𝑎 và 𝑏.
Một trong những thuật toán hiệu quả nhất để tính ước số chung lớn nhất (Greatest
Common Divisor - GCD) của hai số là thuật toán Euclid, có từ khoảng năm 300 trước
công nguyên. Trong quá trình phát triển môn số học, cũng đã có nhiều thuật toán
tính ước số chung lớn nhất được phát hiện. Tuy vậy những thuật toán hiệu quả
hơn so với thuật toán Euclid thường phức tạp và chỉ áp dụng trong những trường
hợp đặc biệt mới phát huy hiệu quả, chẳng hạn khi mà phép chia lấy dư khó thực
hiện hoặc dữ liệu được cho dưới dạng biểu diễn nhị phân.
Tư tưởng của thuật toán Euclid dựa vào định lý sau:
Nếu 𝑏 = 0, ta có GCD(𝑎, 𝑏) = 𝑎
Nếu 𝑏 ≠ 0, gọi 𝑟 là số dư của phép chia 𝑎 cho 𝑏, khi đó GCD(𝑎, 𝑏) = GCD(𝑏, 𝑟)

*Điều kiện d <= √n ngoài cách viết tương đương d * d <= n còn có thể viết thành d <= n / d để
tránh luôn cả nguy cơ tràn số của phép nhân nếu ràng buộc dữ liệu thay đổi và cho phép nhập vào
giá trị lớn nhất trong kiểu số nguyên cho 𝑛. Tuy nhiên cách viết d * d <= n vẫn hay được dùng hơn
so với d <= n / d nếu như chắc chắn phép nhân không bị tràn số, lý do là mã máy của phép chia
chậm hơn mã máy của phép nhân
Phần III
Lập trình cấu trúc

Chứng minh
Nếu 𝑏 = 0, hiển nhiên GCD(𝑎, 𝑏) = 𝑎
Nếu 𝑏 ≠ 0, gọi 𝑞 và 𝑟 lần lượt là thương và số dư của phép chia 𝑎 cho 𝑏 (𝑞, 𝑟 ∈ ℤ)
𝑎 = 𝑞𝑏 + 𝑟
Với một số nguyên 𝑑 ≠ 0 bất kỳ, chia cả 2 vế cho 𝑑:
𝑎 𝑏 𝑟
=𝑞× +
𝑑 𝑑 𝑑
𝑎 𝑏 𝑟
Nếu 𝑑 ước chung của cả 𝑎 và 𝑏: ta có và đều là số nguyên, vậy cũng phải
𝑑 𝑑 𝑑
là số nguyên, suy ra 𝑑 là ước chung của 𝑏 và 𝑟.
𝑏 𝑟 𝑎
Nếu 𝑑 là ước chung của cả 𝑏 và 𝑟 thì và 𝑑 đều là số nguyên, vậy 𝑑 cũng phải là
𝑑
số nguyên, suy ra 𝑑 là ước chung của 𝑎 và 𝑏.
Vậy tập các ước chung của 𝑎 và 𝑏 trùng với tập các ước chung của 𝑏 và 𝑟. Hai tập
trùng nhau thì giá trị lớn nhất phải bằng nhau. Ta có GCD(𝑎, 𝑏) = GCD(𝑏, 𝑟).
Thuật toán Euclid:
Bắt đầu: Nhận vào 2 số 𝑎, 𝑏
Lặp: Chừng nào 𝑏 ≠ 0, tính 𝑟 là số dư của phép chia 𝑎 cho 𝑏 và thay cặp (𝑎, 𝑏)
bởi cặp (𝑏, 𝑟).
Kết thúc: Khi 𝑏 = 0, giá trị 𝑎 là ước số chung lớn nhất của hai số ban đầu
Ta thực nghiệm thuật toán này bằng tay với một dữ liệu cụ thể: 𝑎 = 76; 𝑏 = 172
Bước lặp (a, b) r = a % b
1 (76, 172) 76
2 (172, 76) 20
3 (76, 20) 16
4 (20, 16) 4
5 (16, 4) 0
6 (4, 0)
Tính đúng đắn của thuật toán đã được chứng minh, ta sẽ chỉ ra tính dừng của thuật
toán, tức là vòng lặp của thuật toán không bao giờ bị rơi vào một quá trình vô hạn
mà sẽ kết thúc sau hữu hạn lượt lặp.
Với giả thiết 𝑎, 𝑏 không âm. Khi 𝑏 ≠ 0, số dư 𝑟 của phép chia 𝑎 cho 𝑏 chắc chắn
thỏa mãn 𝑟 < 𝑏. Vì vậy nếu thay cặp (𝑎, 𝑏) bởi cặp (𝑏, 𝑟), giá trị số thứ hai trong
cặp sẽ giảm đi nhưng không bao giờ âm được. Vì vậy quá trình này phải kết thúc
sau hữu hạn bước.
Việc đánh giá hiệu suất của thuật toán liên quan tới cả một lý thuyết lớn về độ phức
tạp tính toán, ta sẽ không trình bày ở đây.

96
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Chương trình 15


1 | #include <iostream> Cho a, b = 76 172
2 | using namespace std; GCD(76, 172) = 4
3 |
4 | int main ()
5 | {
6 | int a, b;
7 | cout << "Cho a, b = "; cin >> a >> b;
8 | cout << "GCD(" << a << ", " << b << ") = ";
9 | while (b != 0)
10 | {
11 | int r = a % b;
12 | a = b;
13 | b = r;
14 | }
15 | cout << a;
16 | }
Từ dòng 10 tới dòng 14 là một lệnh ghép, đó là lệnh thành phần của lệnh while…;
Lệnh while… chỉ cho phép viết duy nhất một lệnh thành phần, ở đây ta có 3 lệnh
(1 lệnh khởi tạo biến 𝑟 và 2 lệnh gán) nên chúng cần được “gói” lại trong một lệnh
ghép.
Chương trình trên có thể sửa đổi một chút để tính ước số chung lớn nhất ngay cả
khi 𝑎, 𝑏 có thể âm. Cách thứ nhất là ngay từ đầu thay 𝑎, 𝑏 bởi giá trị tuyệt đối của
chúng. Cách thứ hai là đến lúc in kết quả thay vì in ra 𝑎 ta sẽ in ra |𝑎|.
Giá trị tuyệt đối của 𝑎 có thể tính bằng biểu thức điều kiện:
a >= 0 ? a : -a;
Cách khác là sử dụng hàm std::abs(𝑥) để tính giá trị tuyệt đối của 𝑥.
Nếu 𝑥 thuộc kiểu số nguyên có dấu, thư viện cstdlib có hàm std::abs(𝑥) để tính giá
trị tuyệt đối của 𝑥, kết quả hàm cùng kiểu với 𝑥. Chú ý là nếu 𝑥 bằng giá trị nhỏ
nhất trong một kiểu số nguyên có dấu, std::abs(𝑥) không xác định do kết quả hàm
tràn phạm vi biểu diễn kiểu.
Nếu 𝑥 thuộc kiểu số thực, thư viện cmath cũng có hàm std::abs(𝑥) để tính giá trị
tuyệt đối của 𝑥, kết quả hàm cùng kiểu với 𝑥.
Nếu cả hai thư viện cstdlib và cmath đều được nạp, tùy theo kiểu của tham số 𝑥,
trình dịch sẽ chọn phiên bản hàm std::abs(𝑥) phù hợp.
8.5.2. Lệnh do…while…
Cú pháp:
do
«Lệnh»
while («Điều kiện»);
do và while là từ khóa
«Lệnh» là một lệnh thành phần của cấu trúc do…while…
«Điều kiện» là một biểu thức bool, gọi là điều kiện lặp. Cặp dấu ngoặc đơn và
dấu chấm phẩy là bắt buộc.
Phần III
Lập trình cấu trúc

Cấu trúc do…while…, cũng được coi là lệnh do…while…, chỉ định cho chương trình
thực hiện việc: Thi hành «Lệnh», tính giá trị biểu thức «Điều kiện» rồi lặp lại nếu
giá trị tính được là true, nếu không, kết thúc lệnh do…while…
Chú ý rằng «Lệnh» sẽ được thi hành ít nhất một lần trong cấu trúc này.
Khi cài đặt chương trình cho một thuật toán diễn tả bằng ngôn ngữ tự nhiên, cấu
trúc do…while… được dùng để diễn tả câu: “Làm «Lệnh» và lặp lại chừng nào thấy
«Điều kiện» đúng” hoặc “Làm «Lệnh» và lặp lại cho tới khi «Điều kiện» sai”. Hình 8-5
là sơ đồ thực hiện của lệnh do…while…

Bắt đầu

«Lệnh»

true
«Điều kiện»

false

Kết thúc

Hình 8-5. Sơ đồ khối của lệnh do…while…

 Ví dụ 1 (do…while…)
Những chương trình đã viết đều là cài đặt thuật toán chạy một lần, muốn thực hiện
lại thuật toán, người dùng phải chạy lại chương trình với nhiều thao tác.
Ta có thể lồng chương trình cài đặt thuật toán chạy một lần vào một cấu trúc
do…while…: Khi kết thúc, đưa ra câu hỏi cho người sử dụng: “Có muốn làm tiếp
không?” và nếu người sử dụng nhập vào đúng ký tự ‘c’ (hoặc ‘C’), chương trình sẽ
được cho chạy lặp lại từ đầu. Vòng lặp này kết thúc nếu như người sử dụng nhập
vào một ký tự khác. Sơ đồ như sau:
char answer; //Biến nhận ký tự trả lời
do
{
«Đoạn chương trình cài đặt thuật toán chạy một lần»
cout << "Co muon tiep khong? ";
cin >> answer;
}
while (answer == 'c' || answer == 'C');
Ví dụ ta lồng đoạn chương trình tính ước số chung lớn nhất của hai số (Chương
trình 15) vào trong sơ đồ này.

98
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Chương trình 16


1 | #include <iostream> Cho a, b = 315 420
2 | using namespace std; GCD(4, 6) = 105
3 | Co muon tiep khong? c
4 | int main ()
5 | { Cho a, b = 1234 5678
6 | char answer; GCD(1234, 5678) = 2
7 | do Co muon tiep khong? C
8 | {
9 | int a, b; Cho a, b = 13579 2468
10 | cout << "Cho a, b = "; cin >> a >> b; GCD(13579, 2468) = 1
11 | cout << "GCD(" << a << ", " << b << ") = "; Co muon tiep khong? k
12 | while (b != 0)
13 | {
14 | int r = a % b;
15 | a = b;
16 | b = r;
17 | }
18 | cout << a << '\n';
19 | cout << "Co muon tiep khong? ";
20 | cin >> answer;
21 | cout << '\n';
22 | }
23 | while (answer == 'c' || answer == 'C');
24 | }
Các dòng từ 9 tới dòng 18 chính là đoạn chương trình tính ước số chung lớn nhất
của hai số mà ta đã viết trước đây.
 Ví dụ 2 (do…while…): Tách chữ số
Viết chương trình cho nhập vào một số tự nhiên 𝑛 (0 ≤ 𝑛 ≤ 109 ), cho biết số chữ
số và tổng các chữ số trong biểu diễn thập phân của 𝑛.
Ý tưởng giải quyết có thể phát biểu như sau:
Khởi tạo một biến 𝑐𝑛𝑡 = 0 để đếm số chữ số và biến 𝑠𝑢𝑚 = 0 để tính tổng các
chữ số.
Xét một chữ số 𝑑 trong biểu diễn thập phân của 𝑛: tăng 𝑐𝑛𝑡 lên 1 và tăng 𝑠𝑢𝑚
lên 𝑑 đơn vị, sau đó xóa chữ số vừa xét ra khỏi biểu diễn thập phân của 𝑛.
Khi đã xóa hết chữ số trong biểu diễn thập phân của 𝑛, 𝑐𝑛𝑡 cho biết số chữ số
và 𝑠𝑢𝑚 cho biết tổng các chữ số cần tìm
Để hoàn thiện thuật toán, ta phải trả lời những câu hỏi: Làm thế nào lấy một chữ
số thập phân của 𝑛 để xét, làm thế nào xóa chữ số đó đi, và làm thế nào để biết đã
xét và xóa hết các chữ số?
Giả sử 𝑛 có biểu diễn thập phân là 𝑑𝑘 𝑑𝑘−1 … 𝑑1 𝑑0 , ta sẽ xét lần lượt các chữ số từ
hàng đơn vị lên:
Chữ số hàng đơn vị 𝑑0 có thể tính bằng công thức 𝑑 = 𝑛 % 10
Nếu xóa chữ số hàng đơn vị 𝑑0 , ta thu được giá trị 𝑛 mới có biểu diễn thập phân
là 𝑑𝑘 𝑑𝑘−1 … 𝑑1 . Giá trị mới này có thể tính bằng công thức 𝑛 /= 10 (phép chia
nguyên) và bây giờ chữ số 𝑑1 lại là chữ số hàng đơn vị, ta lặp lại quy trình tương
tự…
Phần III
Lập trình cấu trúc

Khi còn lại một chữ số cuối cùng 𝑑𝑘 (𝑛 = 𝑑𝑘 ), chữ số này được xét và xử lý, sau
đó công thức 𝑛 /= 10 cho ta giá trị 𝑛 = 0, đây là điều kiện để kết thúc vòng lặp
(𝑛 == 0).

Chương trình 17


1 | #include <iostream> Cho n = 1234
2 | using namespace std; So chu so: 4
3 | Tong chu so: 10
4 | int main ()
5 | {
6 | int n;
7 | cout << "Cho n = ";
8 | cin >> n;
9 | int cnt = 0, sum = 0;
10 | do
11 | {
12 | int d = n % 10; //d là chữ số hàng đơn vị
13 | ++cnt; //Đếm thêm 1 chữ số
14 | sum += d; //Cộng dồn d vào sum
15 | n /= 10; //Cắt bỏ chữ số hàng đơn vị của n
16 | }
17 | while (n != 0); //n ≠ 0 thì tách tiếp
18 | cout << "So chu so: " << cnt << '\n';
19 | cout << "Tong chu so: " << sum;
20 | }
Ta mô phỏng hoạt động của chương trình bằng tay với một giá trị cụ thể, 𝑛 = 1234
Bước lặp n d cnt sum n mới
1 1234 4 1 4 123
2 123 3 2 7 12
3 12 2 3 9 1
4 1 1 4 10 0
 Ví dụ 3 (do…while…): Tách chữ số (cách khác)
Ta giữ nguyên bài toán ở mục trước: Đếm số chữ số và tính tổng các chữ số của
một số tự nhiên 𝑛. Có điều ta sẽ trình bày một thuật toán khác để tách các chữ số
đúng theo thứ tự từ hàng cao nhất đến hàng đơn vị (từ trái qua phải).
Giả sử 𝑛 có biểu diễn thập phân là 𝑑𝑘 𝑑𝑘−1 … 𝑑1 𝑑0 . Gọi 𝑝 là lũy thừa lớn nhất của 10
không vượt quá 𝑛 (𝑝 = 10𝑘 ). Dễ thấy rằng 𝑝 có cùng số chữ số với 𝑛 (bằng 𝑘 + 1),
Xem sơ đồ chữ số sau:
𝑛= 𝑑𝑘 𝑑𝑘−1 𝑑𝑘−2 … 𝑑1 𝑑0
𝑝= 1 0 0 … 0 0
Thuật toán trong Chương trình 13 đã được cung cấp để tìm 𝑝. Dựa vào sơ đồ
chữ số, để lấy chữ số 𝑑𝑘 , ta có thể dùng công thức 𝑛/𝑝 (phép chia nguyên).
Bây giờ, thay vì cắt bỏ chữ số 𝑑𝑘 từ biểu diễn thập phân của 𝑛, ta có thể thay thế
nó bởi số 0, đây là biểu diễn thập phân của giá trị 𝑛 %= 𝑝, nếu sau đó giảm 𝑝 đi 10
lần (𝑝 /= 10), ta có sơ đồ chữ số sau:

100
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

𝑛= 0 𝑑𝑘−1 𝑑𝑘−2 … 𝑑1 𝑑0
𝑝= 1 0 … 0 0
Lặp lại với giá trị 𝑛 mới, để tách chữ số 𝑑𝑘−1 ta lại dùng công thức 𝑛/𝑝, sau đó lại
thay chữ số 𝑑𝑘−1 bởi 0 (𝑛 %= 𝑝) và giảm 𝑝 đi 10 lần (𝑝 /= 10):
𝑛= 0 0 𝑑𝑘−2 … 𝑑1 𝑑0
𝑝= 1 … 0 0
… Ở lượt lặp cuối cùng, ta có 𝑝 = 1, sau khi tách nốt chữ số hàng đơn vị, lệnh
𝑝 /= 10 cho ta 𝑝 = 0, đây là điều kiện để dừng vòng lặp.
Chương trình sau đây cài đặt đúng ý tưởng đó

Chương trình 18


1 | #include <iostream> Cho n = 102400
2 | using namespace std; So chu so: 6
3 | Tong chu so: 7
4 | int main ()
5 | {
6 | int n;
7 | cout << "Cho n = ";
8 | cin >> n;
9 | //Tìm p = lũy thừa của 10 cùng số chữ số với n
10 | int p = 1;
11 | while (p <= n / 10) p *= 10;
12 | //Tách chữ số
13 | int cnt = 0, sum = 0;
14 | do
15 | {
16 | int d = n / p; //Tách 1 chữ số
17 | ++cnt; //Đếm thêm 1 chữ số
18 | sum += d; //Cộng dồn d vào sum
19 | n %= p; //Thay chữ số vừa tách bởi 0
20 | p /= 10; //Giảm p đi 10 lần
21 | }
22 | while (p != 0); //p ≠ 0 thì tách tiếp
23 | cout << "So chu so: " << cnt << '\n';
24 | cout << "Tong chu so: " << sum;
25 | }
 Ví dụ 4 (do…while): Tìm biểu diễn nhị phân
Chương trình 18 có vẻ dài dòng hơn so với Chương trình 17. Lợi ích thu
được chính là ta tách được các chữ số theo đúng thứ tự viết trong ký pháp số học:
nếu in ra từng chữ số theo thứ tự tách ra, ta sẽ được đúng số 𝑛 ban đầu:
Phần III
Lập trình cấu trúc

1 | #include <iostream> Cho n = 43210


2 | using namespace std; 43210
3 |
4 | int main ()
5 | {
6 | int n;
7 | cout << "Cho n = ";
8 | cin >> n;
9 | //Tìm p = lũy thừa lớn nhất của 10 ≤ n
10 | int p = 1;
11 | while (p <= n / 10) p *= 10;
12 | do //Vòng lặp tách chữ số
13 | {
14 | cout << n / p; //In ra 1 chữ số
15 | n %= p; //Thay chữ số vừa tách bởi 0
16 | p /= 10;
17 | }
18 | while (p != 0); //p ≠ 0 thì tách tiếp
19 | }
Có gì đó ngớ ngẩn! có vẻ ta vừa mất quá nhiều công sức chỉ để thay thế một lệnh
có sẵn: std::cout << n;.
Bây giờ ta thử thay các số 10 trong chương trình trên bởi số 2, hoặc bài bản hơn,
khai báo một hằng const int Radix = 2; rồi thay các số 10 trong chương trình trên
bởi Radix. Chương trình trên sẽ in ra biểu diễn của 𝑛 trong hệ cơ số 𝑅𝑎𝑑𝑖𝑥, trong
trường hợp này 𝑅𝑎𝑑𝑖𝑥 = 2, đó là biểu diễn nhị phân của 𝑛.

Chương trình 19


#include <iostream> Cho n = 43210
using namespace std; 1010100011001010
const int Radix = 2; //Cơ số

int main ()
{
int n;
cout << "Cho n = ";
cin >> n;
//Tìm p = lũy thừa lớn nhất của Radix ≤ n
int p = 1;
while (p <= n / Radix) p *= Radix;
do //Vòng lặp tách chữ số
{
cout << n / p; //In ra 1 chữ số
n %= p; //Thay chữ số vừa tách bởi 0
p /= Radix;
}
while (p != 0); //p ≠ 0 thì tách tiếp
}
Muốn tìm biểu diễn của số tự nhiên 𝑛 trong hệ cơ số khác, đơn giản chỉ cần đổi
hằng số 𝑅𝑎𝑑𝑖𝑥.

102
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

8.5.3. Lệnh for…


Cú pháp:
for («Khởi tạo» «Điều kiện»; «Cập nhật»)
«Lệnh»
for là từ khóa
«Khởi tạo» là một lệnh đơn
«Điều kiện» là một biểu thức bool, gọi là điều kiện lặp. Dấu chấm phẩy phía sau
«Điều kiện» là bắt buộc
«Cập nhật» là một biểu thức
«Lệnh» là một lệnh thành phần của cấu trúc for…
Chú ý:
«Khởi tạo» là một lệnh đơn: lệnh khai báo (và khởi tạo) biến, biểu thức kết thúc
bởi dấu “;”, hoặc lệnh rỗng (chỉ có dấu “;”). Chính vì lệnh đơn luôn có dấu “;” kết
thúc nên một số tài liệu mô tả cú pháp lệnh for… có dấu chấm phẩy ngăn cách giữa
phần «Khởi tạo» và «Điều kiện», mặc dù viết thêm dấu chấm phẩy đó là không chặt
chẽ.
Cấu trúc for…, cũng được coi là lệnh for… chỉ định cho chương trình thực hiện việc:
Thi hành lệnh đơn: «Khởi tạo» nếu phần «Khởi tạo» có khai báo biến thì biến
đó sẽ có phạm vi nhìn thấy trong toàn khối lệnh của cấu trúc for…
Thực hiện lặp đi lặp lại việc: tính biểu thức «Điều kiện», nếu giá trị tính được là
true thì thi hành «Lệnh» và sau đó thi hành lệnh đơn «Cập nhật»;, nếu không, kết
thúc lệnh for…
«Điều kiện» là một biểu thức bool, cũng cho phép là biểu thức rỗng, trong trường
hợp này biểu thức rỗng được coi luôn có giá trị true.
«Cập nhật» là một biểu thức và cũng cho phép là biểu thức rỗng, thông thường đây
là biểu thức có chức năng cập nhật lại điều kiện lặp.
«Lệnh» cũng có thể là một lệnh rỗng (chỉ gồm dấu chấm phẩy)
Nếu «Khởi tạo» là lệnh rỗng và «Cập nhật» là biểu thức rỗng thì cấu trúc for…
tương đương với cấu trúc while…
Khi cài đặt chương trình cho một thuật toán diễn tả bằng ngôn ngữ tự nhiên, cấu
trúc for… được dùng để diễn tả câu: “«Khởi tạo», chừng nào «Điều kiện» đúng thì
làm (lặp lại) «Lệnh» sau đó «Cập nhật»;…”.
Hình 8-6 là sơ đồ thực hiện của lệnh for…
Phần III
Lập trình cấu trúc

Bắt đầu

«Khởi tạo»;

false
«Điều kiện»

true

«Lệnh»

«Cập nhật»;

Kết thúc

Hình 8-6. Sơ đồ khối của lệnh for…

 Ví dụ 1 (for…) Tính giai thừa


Giai thừa của một số tự nhiên 𝑛, ký hiệu 𝑛! là tích các số nguyên từ 1 tới 𝑛:
𝑛! = 1 × 2 × … × 𝑛
Với quy ước 0! = 1, ta có thể định nghĩa:
1, nếu 𝑛 = 0
𝑛! = {
(𝑛 − 1)! × 𝑛, nếu 𝑛 > 0
Bài toán đầu tiên yêu cầu viết chương trình cho nhập vào số nguyên 𝑛 (1 ≤ 𝑛 ≤
20) và in ra giá trị 𝑛!.
Thuật toán:
Khai báo một biến số nguyên 𝑓 khởi tạo bằng 1
Với ∀𝑖 = 1,2, … , 𝑛 ta nhân 𝑓 lên 𝑖 lần. Điều này có thể mô tả chi tiết hơn: Khởi
tạo 𝑖 = 1. Tại mỗi bước lặp ta nhân 𝑓 lên 𝑖 lần sau đó tăng 𝑖 lên 1 rồi lặp lại
chừng nào 𝑖 ≤ 𝑛.
Kết thúc vòng lặp, 𝑓 là giá trị 𝑛! cần tìm

104
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Chương trình 20


1 | #include <iostream> Cho n = 6
2 | using namespace std; 6! = 720
3 |
4 | int main() Cho n = 20
5 | { 20! = 2432902008176640000
6 | int n;
7 | cout << "Cho n = "; cin >> n;
8 | long long f = 1;
9 | for (int i = 1; i <= n; ++i)
10 | f *= i;
11 | cout << n << "! = " << f;
12 | }
Vì giá trị 𝑛! có thể khá lớn, dòng 8 khai báo biến 𝑓 kiểu long long để tránh tràn số.
Dòng 9 và dòng 10 là một lệnh for…
Phần khởi tạo int i = 1; khai báo một biến i và khởi tạo nó bởi giá trị 1. Biến i
này có thể dùng trong khối lệnh của vòng lặp for…, nó không được nhìn thấy từ
ngoài khối lệnh.
Điều kiện lặp i <= n và biểu thức cập nhật ++i mô tả hoạt động lặp: Chừng nào
i <= n, thực hiện lệnh f *= i; ở dòng 10, sau đó tăng i lên 1 (++i;)
Vòng lặp này có thể diễn giải bằng một câu đơn giản: Với biến nguyên 𝑖 chạy từ 1
tới 𝑛, nhân 𝑓 lên 𝑖 lần.
Dòng 11 không có gì đặc biệt: In kết quả tính 𝑛!.
 Ví dụ 2 (for…): Liệt kê ước số
Ta sẽ viết chương trình cho nhập vào số nguyên dương 𝑛 ≤ 109 và liệt kê các ước
số của 𝑛 theo thứ tự tăng dần.
Bắt đầu với một thuật toán đơn giản: Thử mọi giá trị số nguyên 𝑑 chạy từ 1 tới 𝑛,
nếu 𝑛 chia hết cho 𝑑 thì in ra 𝑑. Ý tưởng này thể hiện đơn giản bằng một vòng lặp
for…:
for (int d = 1; d <= n; ++d)
if (n % d == 0)
std::cout << d << ' ';
Ví dụ kiểm tra tính nguyên tố đã nêu nhận xét: Nếu 𝑛 chia hết cho 𝑑 thì 𝑛 cũng chia
hết cho 𝑑′ = 𝑛/𝑑. Nếu 𝑑 ≠ 𝑑′ , một trong hai ước số 𝑑 hoặc 𝑑′ sẽ phải nhỏ hơn √𝑛.
Dựa vào nhận xét này, ta thiết kế một thuật toán khác hiệu quả hơn:
Đầu tiên liệt kê các ước số < √𝑛 theo thứ tự tăng dần.
Tiếp theo, nếu 𝑛 là số chính phương, ta in ra √𝑛
Cuối cùng, liệt kê các ước số > √𝑛 theo thứ tự tăng dần
Phần III
Lập trình cấu trúc

Chương trình 21


1 | #include <iostream> Cho n = 144
2 | using namespace std; 1
3 | 2
4 | int main() 3
5 | { 4
6 | int n, d; 6
7 | cout << "Cho n = "; cin >> n; 8
8 | for (d = 1; d * d < n; ++d) //In ra các ước < sqrt(n) 9
9 | if (n % d == 0) 12
10 | cout << d << '\n'; 16
11 | if (d * d == n) //Nếu n là số chính phương 18
12 | cout << d << '\n'; //in ra d = sqrt(n) 24
13 | for (--d; d >= 1; --d) //In ra các ước > sqrt(n) 36
14 | if (n % d == 0) 48
15 | cout << n / d << '\n'; 72
16 | } 144
Từ dòng 8 tới dòng 10 liệt kê các ước < √𝑛 theo thứ tự tăng dần: Bắt đầu với 𝑑 =
1 và tăng dần 𝑑 lên, mỗi khi gặp 𝑑 ∣ 𝑛 thì in ngay ra 𝑑. Vòng lặp dừng khi 𝑑 đạt tới
giá trị thỏa mãn 𝑑 2 ≥ 𝑛 tức là 𝑑 ≥ √𝑛.
Dòng 11: Kết thúc vòng lặp for thứ nhất nếu 𝑑 = √𝑛 thì 𝑛 là số chính phương và
𝑑 ∣ 𝑛, ta in ra 𝑑 là một ước số nữa. Chú ý rằng sau bước này, 𝑑 vẫn là số nguyên
nhỏ nhất ≥ √𝑛.
Từ dòng 13 tới dòng 15, ta liệt kê các ước > √𝑛 theo thứ tự tăng dần: Trước tiên
𝑑 được giảm đi 1 để trở thành số nguyên lớn nhất < √𝑛, sau đó cho 𝑑 giảm dần về
1, mỗi khi gặp 𝑑 ∣ 𝑛 thì in ra ước số 𝑛/𝑑. Vì 𝑑 được duyệt theo thứ tự giảm dần nên
các ước số được liệt kê theo thứ tự tăng dần.
Có một chú ý về những vòng lặp vô hạn, chẳng hạn:
while (true);
Thông thường, chương trình sẽ treo khi chạy lệnh này. Tuy nhiên chuẩn C++14
quy định, nếu chương trình dịch phát hiện một vòng lặp với điều kiện lặp luôn
đúng, đồng thời mỗi lượt lặp không đưa ra một hành động gì, trình dịch được phép
không dịch lệnh lặp đó vào mã máy, tức là lệnh lặp đó có thể bị bỏ qua trên một vài
trình dịch.
8.6. Cấu trúc khối lệnh
Chương trình C++ có cấu trúc gồm các khối lồng nhau. Mỗi khối có phạm vi (scope)
riêng của nó và có thể chứa những khối khác. Ngoài cùng là khối toàn cục (global
scope), nó chứa bên trong khối lệnh của các hàm (kể cả hàm main), các lớp,… , khối
lệnh của hàm lại có thể chứa bên trong khối lệnh của cấu trúc if…, while…, for… …

106
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Khối toàn cục


Hàm main

while…
if…

switch…

for…
do…while…

Hình 8-7. Cấu trúc khối và phạm vi

8.6.1. Phạm vi nhìn thấy của tên


Khi trong một khối B, ta khai báo một tên (định danh) ID, đây có thể là tên biến,
hằng, kiểu, … Khi đó:
ID sẽ được nhìn thấy bên trong khối B tính từ vị trí khai báo đến hết khối B, kể
cả trong các khối con của B viết sau vị trí khai báo.
ID không được nhìn thấy bên ngoài khối B, cũng không được nhìn thấy từ đầu
khối B cho đến trước vị trí khai báo.
Xét một ví dụ, để đơn giản ta lấy cấu trúc khối của lệnh ghép:
1 | int Global; //Biến toàn cục
2 |
3 | int main ()
4 | {
5 | ID = 0; //Sai
6 | {
7 | ID = 1; //Sai
8 | }
9 | { //Đầu Khối B
10 | ID = 2; //Sai
11 | int ID; //Vị trí khai báo
12 | ID = 3; //Đúng
13 | {
14 | Global = ID; //Đúng
15 | }
16 | } //Cuối khối B
17 | ID = 4; //Sai
18 | }
Dòng 1 khai báo một biến Global ở khối toàn cục, biến này sẽ được nhìn thấy trong
toàn bộ chương trình (tính từ dòng 1 trở đi).
Dòng 11 khai báo biến ID nằm trong một lệnh ghép với khối lệnh gọi là khối B
Những lệnh gán ở dòng 5 ID = 0;, dòng 7 ID = 1; và dòng 17 ID = 4; nằm ngoài
khối B, lệnh gán ở dòng 10 ID = 2; nằm trong khối B nhưng đứng trước vị trí khai
báo. Chúng đều sai vì không nhìn thấy biến ID.
Phần III
Lập trình cấu trúc

Lệnh gán ở dòng 13 ID = 3; nằm trong khối B và sau vị trí khai báo. Lệnh gán ở
dòng 14 Global = ID; nằm trong khối con của khối B nằm sau vị trí khai báo. Chúng
là những lệnh gán đúng vì nhìn thấy biến ID (lệnh gán ở dòng 14 đúng vì nhìn thấy
cả biến Global).
Một điều cần chú ý nữa là khi chương trình chạy ra khỏi một khối, tất cả tên trong
khối đó cùng với các đối tượng tương ứng sẽ bị hủy. Có nghĩa là nếu một khối được
chạy nhiều lần (trong vòng lặp chẳng hạn), lần chạy sau không được tận dụng giá
trị biến ở lần chạy trước nếu biến đó được khai báo trong khối. Ví dụ:
1 | #include <iostream>
2 | using namespace std;
3 |
4 | int main ()
5 | {
6 | for (int i = 1; i <= 10; ++i)
7 | {
8 | int x;
9 | if (i == 1) x = 0; //OK
10 | cout << x++; //Hành vi không xác định
11 | }
12 | }
Lưu ý là trong chương trình trên, biến 𝑖 nằm trong khối lệnh của lệnh for… (từ
dòng 6 tới dòng 11) còn biến 𝑥 nằm trong khối lệnh của lệnh ghép (từ dòng 7 tới
dòng 11). Kể từ lượt lặp với 𝑖 = 2 trở đi, biến 𝑥 được khai báo nhưng chưa khởi
tạo giá trị, biểu thức x++ trở thành hành vi không xác định.
8.6.2. Phạm vi xác định của tên
Nếu ta khai báo nhiều tên (định danh) trong một khối, các tên này phải hoàn toàn
phân biệt. Nhưng hai tên nằm trong hai khối khác nhau có thể trùng nhau. Câu hỏi
đặt ra là nếu tên ID được khai báo trong nhiều khối, khi một lệnh S dùng đến tên
ID nó sẽ xác định đó là ID trong khối nào?
Quy tắc là trình dịch sẽ tìm ID trong khối chứa lệnh S trước (gọi là khối B), nếu
nhìn thấy tên ID trong khối, nó sẽ dùng luôn cho lệnh S. Nếu không nhìn thấy nó sẽ
tìm ra ngoài khối chứa khối B, và cứ tiếp tục như vậy… Nếu tìm đến khối toàn cục
mà vẫn không nhìn thấy ID, trình dịch sẽ báo lỗi.
Xét hai chương trình dưới đây

108
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

1 | #include <iostream> 1 | #include <iostream>


2 | using namespace std; 2 | using namespace std;
3 | 3 |
4 | int main () 4 | int main ()
5 | { 5 | {
6 | int ID = 0; 6 | int ID = 0;
7 | { 7 | {
8 | ID = 1; 8 | int ID = 1;
9 | { 9 | {
10 | ID = 2; 10 | int ID = 2;
11 | cout << ID; //2 11 | cout << ID; //2
12 | } 12 | }
13 | cout << ID; //2 13 | cout << ID; //1
14 | } 14 | }
15 | cout << ID; //2 15 | cout << ID; //0
16 | } 16 | }
Kết quả in ra: 222 Kết quả in ra: 210
Ở chương trình bên trái, chỉ có một biến ID khai báo trong hàm main, tất cả các
lệnh gán, hay in ID ra đều sử dụng biến này. Dòng 6 khởi tạo ID = 0;, dòng 8 gán
ID = 1; và dòng 10 gán ID = 2;. Ở dòng 11, 13 và 15 lệnh cout << ID; sẽ in ra giá
trị biến ID chung của cả hàm main, ta thu được 222.
Ở chương trình bên phải, mỗi khối lệnh khai báo và khởi tạo riêng một biến ID.
Lệnh cout << ID; ở dòng 11 in ra giá trị ID khai báo ở dòng 10, ta được số 2. Lệnh
cout << ID; ở dòng 13 in ra giá trị ID khai báo ở dòng 8, ta được số 1. Còn lệnh
cout << ID; ở dòng 15 in ra giá trị ID khai báo ở dòng 6, ta được số 0. Kết quả in
ra là 210.

Hiệu ứng lề (side effect):


Nếu ta có khối A chứa khối B, trong đó khối A khai báo một tên ID ở vị trí trước
khối B:
Nếu khối B sử dụng một tên ID bên trong khối B nhưng quên chưa khai báo,
tên ID của khối A sẽ bị sử dụng không mong muốn.
Nếu khối B muốn truy cập tên ID của khối A, nhưng bên trong khối B lỡ có một
biến trùng tên ID, khi đó tên ID của khối A không được dùng còn tên ID của
khối B bị sử dụng không mong muốn.
Trong chương trình lớn không thể tránh khỏi có những tên trùng nhau trong các
khối lồng nhau, việc sử dụng nhầm tên ở chỗ khác sẽ không được trình dịch báo
lỗi nếu nó vẫn đúng luật. Cần hết sức cẩn thận với hiệu ứng lề.
8.7. Lệnh nhảy
Lệnh nhảy cho phép chương trình đang thực hiện bình thường được nhảy ra khỏi
khối lệnh tới một vị trí khác để thực hiện tiếp, phá vỡ quy tắc của cú pháp ngôn
ngữ lập trình bậc cao.
Các lệnh nhảy thực ra không phải thành phần của lập trình cấu trúc, thậm chí mô
hình lập trình cấu trúc được đề xuất chính là để chống lại việc dùng lệnh nhảy. Tuy
Phần III
Lập trình cấu trúc

vậy, việc cấm tuyệt đối sử dụng các lệnh nhảy khiến cho việc lập trình đôi lúc gặp
khó khăn, C++ vẫn cho phép dùng lệnh nhảy nhưng với một số ràng buộc hạn chế*.
8.7.1. Lệnh nhảy goto
Để sử dụng lệnh nhảy goto, ta cần chọn một «Tên nhãn» (label). Dùng «Tên nhãn»
và dấu hai chấm “:” đánh dấu vị trí trước một lệnh. Lệnh bắt buộc phải có sau «Tên
nhãn» và dấu “:”, nếu vị trí đánh dấu đang không có lệnh nào đứng sau, ta có thể
dùng dấu “;” để coi như phía sau nhãn có một lệnh rỗng.
Khi chương trình gặp lệnh
goto «Tên nhãn»;
sẽ nhảy ngay đến vị trí đánh dấu và thi hành các lệnh tiếp từ đó…
Ràng buộc: Không được nhảy qua các khai báo. Điều này dễ hiểu vì nếu nhảy qua
các khai báo, tên trong các khai báo đó sẽ không tồn tại ở vị trí nhảy đến, các lệnh
thi hành sau khi nhảy nếu có truy cập đến những tên này sẽ là trái logic xử lý của
chương trình.
Ví dụ ta viết lại Chương trình 15 tính ước số chung lớn nhất bằng lệnh goto.
Chương trình viết bằng vòng lặp while được đặt ở bên phải cho tiện so sánh.
1 | #include <iostream> #include <iostream>
2 | using namespace std; using namespace std;
3 |
4 | int main () int main ()
5 | { {
6 | int a, b, r; int a, b, r;
7 | cin >> a >> b; cin >> a >> b;
8 | Repeat: while (b != 0)
9 | if (b == 0) goto Done; {
10 | r = a % b; r = a % b;
11 | a = b; a = b;
12 | b = r; b = r;
13 | goto Repeat; }
14 | Done: cout << a;
15 | cout << a; }
16 | }
Chương trình cứ chạy đến dòng 13 thì nhảy ngược về dòng 8 để thực hiện lặp,
trong quá trình lặp, dòng 9 có kiểm tra điều kiện dừng (b == 0) và nếu điều kiện
này thỏa mãn sẽ nhảy thẳng tới lệnh in kết quả rồi thoát chương trình. Chỉ là một

*Lý do mà người ta chống lại lệnh nhảy một phần chính là các thuật toán cổ điển ban đầu được biểu
diễn bằng sơ đồ khối, lệnh nhảy khiến cho không thể vẽ được sơ đồ khối chuẩn mực khi một khối
lệnh có thể có nhiều lối ra do lệnh nhảy. Tuy nhiên với các thuật toán hiện đại, phương pháp sơ đồ
khối cũng trở nên lạc hậu và chủ yếu người ta dùng mã giả (pseudo code) để phát biểu thuật toán,
lệnh nhảy vẫn được cho phép miễn là không ảnh hưởng tới khả năng kiểm soát chương trình.

110
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

ví dụ đơn giản nhưng có thể thấy lệnh goto khiến cho chương trình tối tăm và khó
kiểm soát hơn hẳn.
Theo định lý Böhm – Jacopini (1966), mọi quá trình tính toán trên máy tính đều có
thể diễn tả bằng ba cấu trúc cơ bản: Tuần tự, lựa chọn và lặp. Chúng ta có thể lập
trình mà không cần lệnh goto.
Tuy goto có thể thay thế được nhưng loại bỏ nó đôi khi sẽ khiến cấu trúc chương
trình cồng kềnh thêm đang kể. Vậy nên:
Tránh làm dụng goto, chỉ dùng trong những trường hợp thực sự khó thay thế
và không làm mã lệnh mất kiểm soát.
Ngôn ngữ lập trình cũng cung cấp vài dạng lệnh nhảy có điều kiện khắt khe hơn
để thay thế cho goto, chúng ta có thể sử dụng chúng cùng với những tiêu chuẩn
đặc biệt*.
8.7.2. Lệnh break; và continue;
 Lệnh break;
Khi chương trình gặp break; nó sẽ tìm khối lệnh nhỏ nhất chứa break; mà khối đó
là của lệnh switch hay lệnh lặp, lệnh tương ứng với khối đó sẽ thoát.
Có thể hiểu đơn giản: break; là goto ra sau khối lệnh switch hay lệnh lặp chứa nó.
Nếu nhìn trên sơ đồ khối, lệnh break; có nghĩa là nhảy thẳng đến ô:
Kết thúc

Chú ý là nếu có nhiều khối ứng với các lệnh lặp hay switch lồng nhau, break; chỉ
thoát đúng khối nhỏ nhất chứa nó mà thôi.
 Lệnh continue;
Khi chương trình gặp continue;, nó sẽ xác định khối lệnh lặp nhỏ nhất chứa
continue;, lệnh thành phần của vòng lặp sẽ bị thoát, chương trình nhảy đến kiểm
tra điều kiện lặp và thi hành lượt lặp mới nếu điều kiện đúng. Xét trên sơ đồ khối,
lệnh continue; có nghĩa là nhảy thẳng tới ô:

«Điều kiện»

8.7.3. Ví dụ 1
Viết chương trình lặp: Cho nhập vào các giá trị số thực. Khi người dùng nhập vào
số thực 𝑆:

*Nhược điểm chung của các kiểu lệnh nhảy là dễ nhảy qua đoạn mã dọn dẹp, khiến cho chương
trình sinh ra rác trong bộ nhớ, C++ đã khắc phục điều này bằng nhiều kỹ thuật cao cấp mà ta sẽ
trình bày sau.
Phần III
Lập trình cấu trúc

Nếu 𝑆 > 0, in ra thước cạnh hình vuông có diện tích bằng 𝑆 (√𝑆)
Nếu 𝑆 < 0, thông báo nhập dữ liệu sai và bắt nhập lại
Nếu 𝑆 = 0, thoát chương trình

Chương trình 22


1 | #include <iostream> Cho S = 6
2 | #include <cmath> 2.44949
3 | using namespace std; Cho S = -3
4 | Du lieu sai
5 | int main () Cho S = 4
6 | { 2
7 | long double S; Cho S = 0
8 | while (true)
9 | {
10 | cout << "Cho S = ";
11 | cin >> S;
12 | if (S == 0) break;
13 | if (S < 0)
14 | {
15 | cout << "Du lieu sai\n";
16 | continue;
17 | }
18 | cout << sqrt(S) << '\n';
19 | }
20 | }
Từ dòng 8 đến dòng 19 là một lệnh while… với điều kiện lặp luôn đúng. Vòng lặp
này chỉ có một cách thoát khi người dùng nhập vào giá trị 0, lệnh break; ở dòng 12
sẽ thoát khỏi vòng lặp và sau đó dừng chương trình.
Trong trường hợp người dùng nhập vào giá trị âm, lệnh continue; ở dòng 16 sẽ cho
bắt đầu lần lặp mới ngay, bỏ qua dòng 18.
Lệnh ở dòng 18: cout << sqrt(S) << '\n'; chỉ được thực hiện khi người dùng nhập
vào giá trị dương.
Ví dụ này cho thấy các lệnh nhảy break; và continue; làm cho chương trình ngắn
hơn, xử lý gọn từng trường hợp sau đó không cần quan tâm nữa. Nếu thay thế
chúng sẽ cần một cấu trúc if…else…if…else… khá dài dòng mà cũng khó kiểm soát.
8.7.4. Ví dụ 2
Viết chương trình cho nhập vào số nguyên dương 𝑛 ≤ 10, in ra các số nguyên từ 1
tới 𝑛2 theo dạng bảng kích thước 𝑛 × 𝑛: Điền từ trên xuống dưới theo từng hàng
và mỗi hàng điền đúng 𝑛 số từ trái qua phải. Ví dụ 𝑛 = 5:
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
Kỹ thuật ở chương trình này không có gì đặc biệt: Khởi tạo một biến nguyên 𝑐 = 0,
Làm một vòng lặp 𝑛 lần, mỗi lần có nhiệm vụ in ra 1 hàng trong lệnh thành phần

112
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

của vòng lặp. Để in ra một hàng, ta in ra một dấu xuống dòng, sau đó lại thực hiện
một vòng lặp 𝑛 lần: mỗi lần in ra ++𝑐;

Chương trình 23


1 | #include <iostream> n = 5
2 | #include <iomanip>
3 | using namespace std; 1 2 3 4 5
4 | 6 7 8 9 10
5 | int main () 11 12 13 14 15
6 | { 16 17 18 19 20
7 | int n, c = 0; 21 22 23 24 25
8 | cout << "n = "; cin >> n;
9 | for (int i = 1; i <= n; ++i)
10 | { //in ra dòng thứ i
11 | cout << '\n';
12 | for (int j = 1; j <= n; ++j)
13 | cout << setw(4) << ++c; //in ra số thứ j trên dòng i
14 | }
15 | }
Trong Chương trình 23, dòng 13 có đẩy ra std::cout đối tượng std::setw(4)
của thư viện iomanip. Số 4 này có thể thay bởi số dương 𝑘 bất kỳ. Khi đối tượng
std::setw(k) được đẩy ra std::cout, nó dành chiều ngang mặc định là 𝑘 ô để in giá
trị được đẩy ra thiết bị xuất chuẩn ngay tiếp theo:
Nếu giá trị in ra thực sự chỉ cần ít hơn 𝑘 ô, máy sẽ thêm ký tự trống vào phía
trước cho đủ 𝑘 ô.
Nếu giá trị in ra thực sự cần nhiều hơn 𝑘 ô, máy tự động dành đủ chỗ để in,
không bị mất ký tự
Ví dụ:
Lệnh std::cout << std::setw(5) << 12; sẽ in ra màn hình: 12
Lệnh std::cout << std::setw(5) << 123456; sẽ in ra màn hình: 123456
Chú ý là std::setw(k) chỉ có tác dụng với đúng một giá trị in ra ngay sau đó thôi.
Những kiến thức trên là phụ, hỗ trợ cho việc trình bày màn hình mà thôi. Ta quay
lại bài toán, bây giờ nếu yêu cầu cho nhập thêm một số nguyên dương 𝑞 ≤ 𝑛2 nữa
và chỉ in bảng cho tới khi in ra được số 𝑞 là phải dừng. Ví dụ 𝑛 = 5; 𝑞 = 18, ta phải
in ra:
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18
Sửa Chương trình 23 như thế nào cho khéo?
Có thể thay dòng 13 bởi: if (c < q) cout << setw(4) << ++c; tuy kết quả in ra màn
hình đã đúng, ta thấy dù từ thời điểm 𝑐 == 𝑞, chương trình không in ra gì nữa
nhưng hai vòng lặp for… lồng nhau vẫn chạy cho đến hết, tốn thời gian vô ích.
Một cách khác là cài lệnh break;. Ở cả hai vòng lặp, mỗi khi kết thúc một lượt lặp
ta đều kiểm tra nếu c == q thì break; ngay. Sở dĩ ta cần cài lệnh kiểm tra và break;
tới hai lần vì lệnh break; chỉ “bẻ gãy” vòng lặp chứa nó mà thôi. Vấn đề trở nên
Phần III
Lập trình cấu trúc

phức tạp khi số vòng lặp lồng nhau không chỉ có 2 mà có nhiều hơn nữa, vậy làm
sao để bẻ gãy tất cả các vòng lặp đó từ bên trong. Cách đẹp nhất có thể làm là lệnh
goto.

Chương trình 24


1 | #include <iostream> n, q = 5 18
2 | #include <iomanip>
3 | using namespace std; 1 2 3 4 5
4 | 6 7 8 9 10
5 | int main () 11 12 13 14 15
6 | { 16 17 18
7 | int n, q, c = 0;
8 | cout << "n, q = "; cin >> n >> q;
9 | for (int i = 1; i <= n; ++i)
10 | {
11 | cout << '\n';
12 | for (int j = 1; j <= n; ++j)
13 | {
14 | cout << setw(4) << ++c;
15 | if (c == q) goto Done;
16 | }
17 | }
18 | Done:;
19 | }
Thực ra cách làm chính quy trong trường hợp này là viết riêng cấu trúc nhiều vòng
lặp lồng nhau ra một hàm và dùng lệnh return để thoát hàm. Như ví dụ này ta có
thể không cần khai báo nhãn Done: mà thay dòng 15 bởi: if (c == q) return 0; để
thoát khỏi hàm main. Lệnh return chính là một lệnh nhảy, ta sẽ nói kỹ hơn về nó
khi học về hàm.
Nhân đây, ta cũng có một kinh nghiệm khi phát biểu thuật toán:
Nên phát biểu thuật toán bằng mã giả (dựa trên một ngôn ngữ lập trình nhưng
không cần trình bày quá chi tiết, có thể dùng các ký hiệu toán học, có thể chen lẫn
ngôn ngữ tự nhiên nếu như thao tác ở đó đơn giản hoặc đã/sẽ được chi tiết hóa ở
một chỗ khác)
Trong trường hợp phát biểu bằng ngôn ngữ tự nhiên cho dễ trao đổi giữa người
với người, nên dùng các câu từ tương đương với ngôn ngữ lập trình. Ví dụ với
chương trình tính ước số chung lớn nhất: “Chừng nào 𝑏 ≠ 0, tính 𝑟 = 𝑎 % 𝑏, thay
cặp số (𝑎, 𝑏) bởi cặp số (𝑏, 𝑟) và lặp lại”.
Không nên phát biểu thuật toán kiểu: Bước 1…; Bước 2…; Bước 3…; Nếu … quay
về bước 1; Nếu … chuyển sang bước 5. Phát biểu như vậy thì cách dễ nhất để viết
chương trình chính là dùng goto với luồng xử lý nhảy loạn xạ rất khó kiểm soát.
Rất tiếc là kiểu trình bày này có trong rất nhiều tài liệu thuật toán, kể cả trong
những ấn bản nghiên cứu gần đây.

114
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

8.8. Bài toán, thuật toán và chương trình


8.8.1. Bài toán, mô hình, thể hiện
Xét một ví dụ về ba cách phát biểu vấn đề:
Bài toán A: Một người đi siêu thị mua một vài sản phẩm giống nhau. Khi thanh toán,
mỗi túi của siêu thị chỉ gói được tối đa một số sản phẩm nhất định. Hỏi nhân viên
bán hàng cần dùng ít nhất bao nhiêu túi để gói hàng cho khách.
Bài toán B: Cho 𝑛 sản phẩm, mỗi túi có thể chứa tối đa 𝑘 sản phẩm. Cho 𝑛 và 𝑘 (0 ≤
𝑛 ≤ 109 ; 1 ≤ 𝑘 ≤ 109 ), hãy cho biết số túi ít nhất cần dùng.
Bài toán C: Một khách hàng đi siêu thị mua 100 sản phẩm giống nhau. Mỗi túi của
siêu thị có thể đựng được tối đa 6 sản phẩm. Hỏi để gói hết các sản phẩm cho khách
thì nhân viên bán hàng cần ít nhất bao nhiêu túi.
Mặc dù cả ba vấn đề trên có gì đó rất chung, sự khác biệt nằm ở chỗ:
(A) là bài toán tổng quát (abstract problem), vấn đề mà các nhân viên bán hàng,
các ông chủ siêu thị phải giải quyết khá thường xuyên; Bài toán tổng quát thường
cho bởi những thông tin khá mơ hồ và hình thức, yêu cầu về lời giải không cụ thể
và rõ ràng.
(B) là mô hình (model) toán học của bài toán tổng quát, mô hình này xác định rõ
ràng phải giải quyết vấn đề gì?, với giả thiết nào đã cho và lời giải cần phải đạt
những yêu cầu gì? Mô hình toán học cho phép tìm ra giải pháp cho bài toán tổng
quát. Việc tìm kiếm giải pháp có thể bắt đầu từ việc hiểu cách thức con người giải
quyết vấn đề tổng quát, cũng có thể do sự khác biệt (cả về ưu điểm và nhược điểm)
của máy tính mà cách thức giải quyết vấn đề trên mô hình toán học hoàn toàn khác
biệt so với cách thông thường của con người. Giải pháp này sẽ được chuyển giao
ra thành chương trình cho máy tính thực hiện.
(C) là một trường hợp cụ thể (instance) của bài toán tổng quát trong thực tế mà
máy tính sẽ tuân theo chính xác giải pháp (đã được lập trình) để đưa ra kết quả
với trường hợp cụ thể đó.
Cần hiểu rõ: máy tính không giải được bài toán tổng quát, máy tính chỉ giải một
trường hợp cụ thể của bài toán tổng quát xác định qua dữ liệu cụ thể. Lời giải
của trường hợp cụ thể này đạt yêu cầu hay không đạt yêu cầu, tốt hay không tốt…
là phụ thuộc vào giải pháp được lập trình sẵn, giải pháp đó (gọi là thuật toán, thứ
mà ta đã nhắc tới vài lần từ đầu chương) được xây dựng trên mô hình của bài toán
và việc này phải do con người thực hiện.
Những bài toán tin học được phát biểu theo dạng “hỗn hợp” giữa vấn đề thực tế và
mô hình toán học. Không có liên hệ thực tế, bài toán trở nên khô khan và vô bổ.
Không có mô hình toán học, các ràng buộc dữ liệu và yêu cầu chất lượng lời giải
trở nên mập mờ dẫn tới khó tìm thuật toán cũng như viết chương trình. Lưu ý là
Phần III
Lập trình cấu trúc

các ràng buộc dữ liệu cũng như các yêu cầu chất lượng lời giải đôi khi phải được
ngầm hiểu qua bài toán thực tế: Chẳng hạn tuổi của một người luôn phải là số
không âm, hay số quả trứng trong một giỏ chắc chắn phải là số nguyên và cũng
không bao giờ là số âm được.
Đây không phải là cuốn sách về thuật toán nên ta chỉ trình bày sơ lược các khái
niệm như vậy. Một số ví dụ trong tài liệu này đôi khi sử dụng thẳng mô hình toán
học cho ngắn gọn, như bài toán giải phương trình bậc nhất hay tính diện tích hình
tròn, bởi ứng dụng thực tế của nó đã được môn toán trình bày từ bậc phổ thông.
Một số ví dụ khác dựa trên bài toán thực tế, tuy nhiên mô hình có thể không hoàn
toàn khớp với bài toán đó. Một số ràng buộc có thể bị lược bỏ nếu chúng không
quá quan trọng, cũng có thể sửa đổi cho rõ ràng hơn, cũng có thể tổng quát lên để
tìm giải pháp phổ dụng hơn. Như ví dụ ở mô hình (B), không có khách hàng nào
vào siêu thị mua tới 1 tỉ món hàng (109 ), và cũng không có túi nào của siêu thị dùng
để chứa tới 1 tỉ sản phẩm cả. Nhưng mô hình ấy lại có thể chuyển đổi để xử lý một
bài toán khác: Chẳng hạn người ta muốn phân 𝑛 vi khuẩn vào một số ít nhất các
nhóm để làm thí nghiệm, mỗi nhóm chứa không quá 𝑘 vi khuẩn…, khi ấy số lượng
vi khuẩn lên tới 109 lại là sự kiện rất bình thường.
Ngay cả những ràng buộc hoàn toàn phi thực tế cũng có thể được đưa vào mô hình,
mục đích là để bắt buộc người lập trình phải chọn cách thức khai báo phù hợp và
phải tìm một giải pháp tốt. Làm quen với các bài toán tin học, bạn sẽ được giả định
có những ngôi nhà 1 tỉ tỉ tầng, những thực thể nhanh hơn tốc độ ánh sáng, hay hai
người chơi một trò chơi với các đống sỏi lên tới 1 tỉ viên… Thay vì thắc mắc, hãy
tìm giải pháp cho nó.
8.8.2. Thiết kế thuật toán
Tìm giải pháp cho một bài toán là quá trình thiết kế thuật toán. Thuật toán là một
hệ thống chặt chẽ và rõ ràng các chỉ thị nhằm xác định một dãy thao tác trên dữ
liệu vào sao cho: chỉ cần dữ liệu tuân thủ ràng buộc của bài toán, sau một số hữu
hạn bước thực hiện các thao tác đã chỉ ra, ta đạt được mục tiêu đã định.
 Ví dụ 1
Xét bài toán trong mục trước: Một người đi siêu thị mua 𝑛 sản phẩm giống nhau
(0 ≤ 𝑛 ≤ 109 ), mỗi túi của siêu thị có thể gói được tối đa 𝑘 sản phẩm (1 ≤ 𝑘 ≤
109 ). Hỏi khi thanh toán, nhân viên bán hàng của siêu thị cần dùng ít nhất bao
nhiêu túi để gói hàng cho khách.
Yêu cầu: Viết chương trình nhập vào hai số nguyên 𝑛 (số sản phẩm được mua) và
𝑘 (số sản phẩm tối đa có thể đưa vào một túi), cho biết số túi ít nhất cần dùng để
gói hàng.

116
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Thuật toán để giải quyết dựa trên chính hành động thực tế của nhân viên siêu thị:
Lấy túi cho đầy sản phẩm vào, nếu túi đã đầy và còn sản phẩm chưa được gói thì
lấy một túi mới... Như vậy số túi chứa đầy đủ 𝑘 sản phẩm là ⌊𝑛/𝑘⌋, sau khi dùng hết
số lượng túi này thì số sản phẩm còn lại là 𝑛 % 𝑘, nếu con số này khác 0 ta cần thêm
đúng 1 túi nữa. Công thức để tính số túi có thể viết trong C++ là:
n / k + (n % k != 0 ? 1 : 0)
Công thức khác dựa vào bản chất của phép ngầm chuyển đổi kiểu từ bool sang int
(false: 0, true: 1) là:
n / k + (n % k != 0)
hoặc (int→bool→{0,1}):
n / k + bool(n % k)
Tuy nhiên cách viết gọn nhất có thể là:
(n + k - 1) / k
Công thức trên có thể lý giải như sau: Trước khi cho sản phẩm cuối cùng vào túi,
𝑛−1
ta có đúng ⌊ ⌋ túi đầy, dù sản phẩm cuối cùng phải cho vào túi mới hay đưa vào
𝑘
𝑛−1 𝑛+𝑘−1
túi chưa đầy thì ta vẫn cần thêm 1 túi nữa. Kết quả là ⌊ + 1⌋ = ⌊ ⌋. Công
𝑘 𝑘
thức này cũng đúng kể cả khi 𝑛 = 0.
Với ràng buộc dữ liệu 𝑛, 𝑘 ≤ 109 , kiểu số nguyên int là đủ để khai báo hai biến này
cũng như để thực hiện các phép tính cần thiết.

 WRAP.cpp ✓ 
1 | #include <iostream> Cho n, k = 100 6
2 | using namespace std; So tui = 17
3 |
4 | int main()
5 | {
6 | int n, k;
7 | cout << "Cho n, k = ";
8 | cin >> n >> k;
9 | cout << "So tui = " << (n + k - 1) / k;
10 | }
Có một cách tính khác: Số túi cần tìm sẽ là số nguyên 𝑞 nhỏ nhất thỏa mãn 𝑞 × 𝑘 ≥
𝑛, hay nói cách khác 𝑞 là số nguyên nhỏ nhất không nhỏ hơn 𝑛/𝑘. Trong toán học
ký hiệu giá trị này là ⌈𝑛/𝑘⌉, còn trong C++ ta có thể viết là ceil(double(𝑛)/𝑘) để
tính phép chia số thực 𝑛/𝑘 rồi làm tròn lên. Tuy nhiên ta không nên sử dụng các
phép tính số thực chừng nào vẫn còn có thể dùng các phép tính số nguyên để thay
thế, nhằm tránh sai số không đáng có.
Hàm ceil(𝑥): Trả về số nguyên nhỏ nhất ≥ 𝑥 (trần của 𝑥, ký hiệu ⌈𝑥⌉).
Hàm floor(𝑥): Trả về số nguyên lớn nhất ≤ 𝑥 (sàn của 𝑥, ký hiệu ⌊𝑥⌋).
Hai hàm này nằm trong thư viện cmath. Kết quả có kiểu trùng với kiểu của 𝑥 nếu
𝑥 thuộc kiểu số thực, kết quả có kiểu double nếu 𝑥 thuộc kiểu số nguyên. (Chú ý là
mặc dù kết quả nguyên nhưng hai hàm này trả về kiểu số thực). Ví dụ:
Phần III
Lập trình cấu trúc

ceil(3.2) = 4.0
ceil(−3.2) = −3.0
floor(3.6) = 3.0
floor(−3.6) = −4.0
 Ví dụ 2
Xét một bài toán khác: Một người nông dân cần quây một khu đất hình chữ nhật
để trồng rau. Diện tích khu đất đó không được nhỏ hơn 𝑛 mét vuông (𝑛 là số
nguyên dương không quá 1018 ) và khi quây xong, người nông dân phải dựng rào
bao quanh khu đất. Vì chiều ngang của mỗi đoạn tường rào mua sẵn dài đúng 1
mét nên để đỡ mất công, anh ta muốn độ dài cạnh của khu đất cũng phải là số
nguyên và để giảm chi phí, anh ta muốn chu vi khu đất nhỏ nhất có thể.
Yêu cầu: Viết chương trình cho nhập vào số 𝑛 và đưa ra độ dài hai cạnh của khu
đất theo phương án tối ưu tìm được.
Bỏ qua những điều phi thực tế như diện tích khu đất trồng rau có thể lên tới 1 tỉ tỉ
mét vuông hay để rào khu đất đó lại cần tới hàng tỉ đoạn tường rào. Ta có thể mô
hình hóa bài toán này như sau:
Dữ liệu được cho (đầu vào – input): Số nguyên dương 𝑛 ≤ 1016
Yêu cầu: Tìm hai số nguyên dương 𝑎, 𝑏 thỏa mãn:
𝑎×𝑏 ≥ 𝑛
{
𝑎 + 𝑏 → min; (𝑎 + 𝑏 nhỏ nhất có thể)
Kết quả cần đưa ra (đầu ra – output): Hai số 𝑎, 𝑏 tìm được chính là số đo hai cạnh
của hình chữ nhật tính bằng mét.
Việc xây dựng thuật toán lại là vấn đề hoàn toàn con người, dựa vào các kỹ năng
toán học và logic. Không giảm tính tổng quát, giả sử 𝑎 ≤ 𝑏. Với bất kỳ phương án
nào mà 𝑎 và 𝑏 chênh lệch nhau nhiều hơn 1 đơn vị, ta có thể tăng 𝑎 lên 1 và giảm
𝑏 đi 1, điều này làm cho chu vi không đổi nhưng diện tích được tăng lên:
(𝑎 + 1) × (𝑏 − 1) = 𝑎𝑏 + ⏟
𝑏 − 𝑎 − 1 > 𝑎𝑏
>0

Suy ra tồn tại phương án thỏa mãn yêu cầu mà 𝑎 và 𝑏 chênh lệch nhau không quá
1, ta sẽ tìm một phương án như vậy.
Nhận xét thứ hai là 𝑏 ≥ ⌈√𝑛⌉, thật vậy, nếu 𝑏 < ⌈√𝑛⌉ thì do 𝑏 là số nguyên, ta có
𝑏 < √𝑛 và vì thế 𝑎 × 𝑏 ≤ 𝑏 2 < 𝑛, trái với ràng buộc.
Ta cũng suy ra luôn được 𝑎 ≤ ⌈√𝑛⌉, bởi khi 𝑏 ≥ ⌈√𝑛⌉, chọn 𝑎 = ⌈√𝑛⌉ cũng đủ để
𝑎 × 𝑏 ≥ 𝑛 rồi, tăng 𝑎 lên nữa sẽ không tối ưu về chu vi. Vậy thì:
𝑎 ≤ ⌈√𝑛⌉ ≤ 𝑏

118
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Bây giờ ta chỉ ra rằng chắc chắn 𝑏 = ⌈√𝑛⌉:


Nếu 𝑎 = ⌈√𝑛⌉, khi đó chỉ cần chọn 𝑏 = ⌈√𝑛⌉ cũng đủ để 𝑎 × 𝑏 ≥ 𝑛, tăng 𝑏 lên
nữa sẽ không tối ưu về chu vi.
Nếu 𝑎 < ⌈√𝑛⌉, do tính nguyên của cả 𝑎 và 𝑏, ta suy ra 𝑏 = ⌈√𝑛⌉ bởi nếu không
𝑎 kém 𝑏 nhiều hơn 1 đơn vị.
Vậy thì thuật toán có thể tóm tắt lại: Tính 𝑏 = ⌈√𝑛⌉, sau đó tính 𝑎 là số nguyên
𝑛
dương nhỏ nhất thỏa mãn 𝑎 × 𝑏 ≥ 𝑛 theo công thức 𝑎 = ⌈𝑏 ⌉, công thức này ta đã
giới thiệu ở ví dụ trước về cách viết trong C++: 𝑎 = (𝑛 + 𝑏 − 1)/𝑏;

Chương trình 25


1 | #include <iostream> Cho dien tich toi thieu: 123
2 | #include <cmath> Kich thuoc: 11 * 12
3 | using namespace std;
4 |
5 | int main()
6 | {
7 | long long n;
8 | cout << "Cho dien tich toi thieu: ";
9 | cin >> n;
10 | int a, b;
11 | b = ceil(sqrt((long double)n));
12 | a = (n + b - 1) / b;
13 | cout << "Kich thuoc: " << a << " * " << b;
14 | }
Chương trình 25 cũng cần tới hàm std::sqrt để tính căn bậc 2. Nhắc lại là hàm
std::sqrt nằm trong thư viện cmath, kết quả hàm std::sqrt(𝑥) cùng kiểu với 𝑥 nếu
𝑥 thuộc kiểu số thực, kết quả hàm thuộc kiểu double nếu 𝑥 thuộc kiểu số nguyên.
Vì ta cần tính √𝑛 với 𝑛 có thể cần tới 17 chữ số có nghĩa, biến 𝑛 được khai báo kiểu
long long để đọc dữ liệu, và được ép kiểu thành long double khi tính căn để hạn
chế sai số tính toán (một cách khác là dùng hàm std::sqrtl(𝑛)).
8.8.3. Triển khai chương trình
Sau khi đã có thuật toán, ta phải tiến hành lập trình cài đặt thuật toán đó. Muốn lập
trình đạt hiệu quả cao, cần phải có kỹ thuật lập trình tốt. Kỹ thuật lập trình tốt thể
hiện ở kỹ năng viết chương trình, khả năng gỡ rối và thao tác nhanh. Lập trình tốt
không phải chỉ cần nắm vững ngôn ngữ lập trình là đủ, phải biết cách viết chương
trình uyển chuyển, khôn khéo và phát triển dần dần để chuyển các ý tưởng ra
thành chương trình hoàn chỉnh. Kinh nghiệm cho thấy một thuật toán hay nhưng
do cài đặt vụng về nên khi chạy lại cho kết quả sai hoặc tốc độ chậm.
 Tinh chế từng bước
Thông thường, ta không nên cụ thể hóa ngay toàn bộ chương trình mà nên tiến
hành theo phương pháp tinh chế từng bước (Stepwise refinement):
Phần III
Lập trình cấu trúc

Ban đầu, chương trình được viết để thể hiện thuật toán với các bước tổng thể,
mỗi bước nêu lên một công việc phải thực hiện.
Một công việc đơn giản hoặc là một đoạn chương trình đã quen thuộc thì ta tiến
hành viết mã lệnh ngay bằng ngôn ngữ lập trình.
Một công việc phức tạp thì ta lại chia ra thành những công việc nhỏ hơn để lại
tiếp tục với những công việc nhỏ hơn đó.
Phương pháp tinh chế từng bước là một thể hiện của tư duy giải quyết vấn đề từ
trên xuống, giúp cho người lập trình có được một định hướng thể hiện trong phong
cách viết chương trình. Tránh việc mò mẫm, xóa đi viết lại nhiều lần, biến chương
trình thành tờ giấy nháp.
Ví dụ: Viết chương trình cho nhập vào một số nguyên dương 𝑘 và liệt kê các số
nguyên tố trong phạm vi từ 1 tới 𝑘.
Thuật toán ta xây dựng là với mỗi giá trị 𝑛 chạy từ 2 tới 𝑘, ta kiểm tra nếu 𝑛 là số
nguyên tố thì in ra 𝑛. Chương trình đầu tiên được viết thô như sau:
1 | #include <iostream>
2 | using namespace std;
3 |
4 | int main ()
5 | {
6 | int k;
7 | cout << "Cho k = "; cin >> k;
8 | for (int n = 2; n <= k; ++n)
9 | {
10 | «Kiểm tra tính nguyên tố của n»;
11 | if («n là số nguyên tố»)
12 | cout << n << ' ';
13 | }
14 | }
Tiếp theo, việc kiểm tra tính nguyên tố là một đoạn chương trình đã quen thuộc,
ta cụ thể hóa luôn thành mã lệnh. Trong quá trình cụ thể hóa và thử nghiệm, ta có
thể thêm những thao tác nhỏ miễn là không ảnh hưởng tới thiết kế ban đầu. Chẳng
hạn ta có thể in ra 6 số nguyên tố trên một dòng màn hình và mỗi số chiếm 10
khoảng trống cho đẹp. Muốn vậy ta khởi tạo một biến đếm int cnt = 0; mỗi lần in
ra một số nguyên tố ta ++cnt; và nếu cnt chia hết cho 6 một dấu xuống dòng sẽ
được in ra.

120
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Chương trình 26


1 | #include <iostream>
2 | #include <iomanip>
3 | using namespace std;
4 |
5 | int main ()
6 | {
7 | int k, cnt = 0;
8 | cout << "Cho k = "; cin >> k;
9 | for (int n = 2; n <= k; ++n)
10 | { //Kiểm tra tính nguyên tố của n
11 | int d = 2;
12 | while (d * d <= n && n % d != 0)
13 | ++d;
14 | if (d * d > n) //n là số nguyên tố
15 | {
16 | cout << setw(10) << n; //In ra n
17 | if (++cnt % 6 == 0) cout << '\n'; //Tăng đếm và xuống dòng mỗi 6 số
18 | }
19 | }
20 | }
 Nguyên tắc trình bày chương trình
Ta đã làm quen với những cấu trúc điều khiển và khá nhiều chương trình. Phần
cuối của chương này nói về cách thức trình bày sao cho chương trình dễ đọc, dễ
hiểu.
Có nhiều nguyên tắc trình bày chương trình (coding style), những nguyên tắc này
thường do những lập trình viên kinh nghiệm hoặc các công ty phần mềm đặt ra
nhằm giữ sự thống nhất giữa các đoạn mã do nhiều người viết. Các nguyên tắc
trình bày không giống nhau ở tất cả mọi điểm, nhưng điểm chung của chúng là tính
thống nhất. Có nghĩa là nếu chương trình được viết theo một nguyên tắc trình bày
nào đó, nó sẽ phải tuân thủ nguyên tắc ấy từ đầu đến cuối không có ngoại lệ.
Có những điểm chung trong các quy tắc trình bày chương trình mà những lập trình
viên mới nên học và làm quen ngay từ khi bắt đầu:
Nguyên tắc thụt lề (indent): Nếu lệnh A là lệnh thành phần của lệnh B, khối lệnh
của A cần phải thụt lề vào một khoảng cố định so với khối lệnh của B. Một
trường hợp ngoại lệ có thể linh động nếu A là lệnh ghép, vì nó đã có cặp dấu
ngoặc nhọn {…} đánh dấu cấu trúc khối và các lệnh bên trong dấu ngoặc nhọn
đã phải thụt lề. Trường hợp này lệnh ghép A có thể viết thẳng bên dưới lệnh B.
Nguyên tắc đặt dấu cách:
Các toán tử một ngôi (phép phủ định !, phép lấy số đối -) được viết liền với
toán hạng đằng sau nó. Ví dụ !(a < b) hay -3 * 5.
Các toán tử hai ngôi cần có dấu cách giữa toán tử và hai toán hạng hai bên.
Ví dụ: a == b hay 4 + 2 * 3
Phía sau dấu phẩy hoặc dấu chấm phẩy luôn có dấu cách nếu vẫn còn ký tự
không phải ký tự xuống dòng đứng sau.
Những quy tắc khác ta có thể tự tham khảo trong các chương trình mẫu.
Phần III
Lập trình cấu trúc

Bài tập 8-1


Viết chương trình cho nhập vào hai số thực 𝑥, 𝑦 và in ra khoảng cách giữa chúng
trên trục số.
Bài tập 8-2
Cho biết giá trị của biến 𝑏 sau khi thực hiện đoạn lệnh:
int a = 0, b;
if (a == 0)
{
a == 1; b = 5;
}
else
if (a == 1)
{
a = 2; b = 6;
}
else b = 7;
Bài tập 8-3
Cho một ví dụ mà hai cấu trúc lệnh này được thực hiện khác nhau:
if («Điều kiện») if («Điều kiện»)
«Lệnh 1» «Lệnh 1»
else if (!«Điều kiện»)
«Lệnh 2» «Lệnh 2»
Bài tập 8-4
Viết chương trình cho nhập vào ba số thực 𝑎, 𝑏, 𝑐 là độ dài 3 cạnh của một tam giác,
tính toán và in ra:
Diện tích tam giác đó với 4 chữ số sau dấu chấm thập phân.
Bán kính đường tròn ngoại tiếp và nội tiếp của tam giác
Gợi ý:
Để tính diện tích tam giác biết độ dài 3 cạnh, có thể dùng công thức Heron:
Gọi 𝑝 là nửa chu vi tam giác:
𝑎+𝑏+𝑐
𝑝=
2
Khi đó diện tích tam giác (𝑆) có hệ thức:
𝑆 = √𝑝(𝑝 − 𝑎)(𝑝 − 𝑏)(𝑝 − 𝑐)
Nếu 𝑅 và 𝑟 lần lượt là bán kính đường tròn ngoại tiếp và nội tiếp của tam giác thì:
𝑎𝑏𝑐
𝑆= = 𝑝𝑟
4𝑅

122
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Bài tập 8-5


Cho một nền nhà hình chữ nhật kích thước 𝑚 × 𝑛. Một người muốn lát nền bằng
các viên gạch hình vuông kích thước 𝑘 × 𝑘. Ban đầu các viên gạch kích thước như
nhau nhưng mỗi viên gạch có thể mài cho nhỏ đi (phần mài đi phải bỏ, không thể
tái sử dụng), hỏi để lát kín nền nhà cần mua ít nhất bao nhiêu viên gạch?
Lập chương trình cho nhập vào ba số nguyên dương 𝑚, 𝑛, 𝑘 ≤ 109 và in ra số viên
gạch ít nhất cần mua.
Bài tập 8-6
Viết chương trình cho nhập vào 6 số thực 𝑎1 , 𝑏1 , 𝑐1 và 𝑎2 , 𝑏2 , 𝑐2 . Giải và biện luận
hệ phương trình:
𝑎 𝑥 + 𝑏1 𝑦 = 𝑐1
{ 1
𝑎2 𝑥 + 𝑏2 𝑦 = 𝑐2
Bài tập 8-7
Viết chương trình cho nhập vào hai số 𝑚 và 𝑦 lần lượt là tháng và năm, in ra lịch
của tháng đó. Ví dụ lịch tháng 9 năm 1945:
CN T2 T3 T4 T5 T6 T7
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
Bài tập 8-8
Viết chương trình tính bội số chung nhỏ nhất của hai số nguyên dương
Bài tập 8-9
Viết chương trình cho nhập vào hai số nguyên 𝑎, 𝑏 (𝑏 ≠ 0), in ra phân số tối giản
có giá trị bằng 𝑎/𝑏.
Bài tập 8-10
Trong Chương trình 18, điều kiện lặp có thể thay p != 0 bởi n != 0 được
không?, Tại sao?
Bài tập 8-11
Có một thuật toán khác để tính ước số chung lớn nhất của hai số nguyên dương 𝑎,
𝑏, tạm gọi là thuật toán trừ liên tiếp:
Chừng nào 𝑎 ≠ 𝑏, thay số lớn hơn trong hai số 𝑎, 𝑏 bởi hiệu của nó trừ đi số còn
lại. Khi 𝑎 = 𝑏 thì 𝑎 chính là ước số chung lớn nhất của hai số ban đầu.
Viết chương trình mô phỏng thuật toán trên và chỉ ra rằng thuật toán này hoạt
động chậm trong trường hợp cặp số (𝑎, 𝑏) có một số lớn hơn nhiều lần số còn lại.
Phần III
Lập trình cấu trúc

Bài tập 8-12


Xét thuật toán Euclid tìm ước số chung lớn nhất của hai số nguyên dương 𝑎, 𝑏. Tìm
cặp giá trị (𝑎, 𝑏) (1 ≤ 𝑎, 𝑏 ≤ 1018 ) khiến vòng lặp của thuật toán phải thực hiện
nhiều lần nhất, cho biết số lần lặp.
Bài tập 8-13
Viết chương trình phân tích một số nguyên dương 𝑛 ≤ 109 thành tích của các thừa
số nguyên tố.
Bài tập 8-14
𝑎
Viết chương trình tối giản một phân số 𝑏

Bài tập 8-15


Dãy số Fibonacci là dãy số nguyên vô hạn 𝑓0 , 𝑓1 , 𝑓2 , … định nghĩa như sau:
0, nếu 𝑛 = 0
𝑓𝑛 = {1, nếu 𝑛 = 1
𝑓𝑛−1 + 𝑓𝑛−2 , nếu 𝑛 ≥ 2
Ví dụ đoạn đầu của dãy Fibonacci: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, …
Viết chương trình nhập vào số nguyên dương 𝐹 ≤ 1018 và in ra đoạn đầu của dãy
số Fibonacci gồm các số ≤ 𝐹.
Bài tập 8-16
Hệ cơ số 16 (hệ thập lục phân) sử dụng 16 chữ số: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C,
D, E, F trong đó A là chữ số có giá trị 10, B có giá trị 11, …, F có giá trị 15.
Viết chương trình cho nhập vào một số tự nhiên 𝑛 và in ra biểu diễn của 𝑛 trong
hệ thập lục phân. Ví dụ 𝑛 = 1234 in ra 4D2
Bài tập 8-17
Lập chương trình nhập vào số nguyên dương 𝑛 ≤ 106 và số nguyên dương 𝑚 ≤
109 . Tính số dư của 𝑛! khi chia cho 𝑚
Bài tập 8-18
Một số nguyên dương 𝑛 được gọi là số hoàn hảo nếu tổng các ước dương của 𝑛
đúng bằng 2𝑛. Hãy lập chương trình liệt kê các số hoàn hảo trong phạm vi [1; 106 ]
Bài tập 8-19
Hai số nguyên dương 𝑎, 𝑏 được gọi là “bạn bè” nếu tổng các ước dương thực sự
của 𝑎 bằng 𝑏 và tổng các ước dương thực sự của 𝑏 bằng 𝑎. Hãy liệt kê tất cả các cặp
số bạn bè trong phạm vi từ [1; 106 ].

124
Lê Minh Hoàng
Chương 8
Các cấu trúc điều khiển

Bài tập 8-20


Lập chương trình nhập vào số thực dương 𝑥 và một số nguyên 𝑛. Hãy tìm phân số
𝑎 𝑎
có mẫu số 𝑏 thỏa mãn 1 ≤ 𝑏 ≤ 𝑛 và chênh lệch giữa 𝑏 và 𝑥 là nhỏ nhất có thể.
𝑏
2
Ví dụ 𝑥 = 0.67, 𝑛 = 5 thì phân số cần tìm là 3

You might also like