Trong các kỳ thi tin học, lời giải thường được viết bằng C++. Bạn viết lời giải thành một tệp văn bản; máy chấm biên dịch tệp ấy thành chương trình, chạy nó trên từng bộ dữ liệu rồi so kết quả in ra với đáp án. Vì vậy mọi lời giải, từ bài in một dòng chữ cho tới bài khó của IOI, đều chung một bộ khung và chung một việc cuối cùng: in ra đúng kết quả.
Bài này dạy phần nhỏ nhất mà chương trình nào cũng có: bộ khung, lệnh in chữ và số, phép tính và biến. Các chương trình dưới đây chưa đọc dữ liệu vào mà tự in kết quả; đọc dữ liệu bằng cin là việc của bài B1. Máy không đoán ý bạn, nên ta sẽ nói tới từng dấu chấm phẩy và từng dấu cách.
Bộ khung của một chương trình
Từ ý tưởng tới một chương trình chạy được có ba bước. Viết mã nguồn (source code) vào tệp .cpp; biên dịch (compile), tức nhờ trình biên dịch g++ dịch mã nguồn thành chương trình chạy được; rồi chạy chương trình đó. Trên LQDOJ bạn chọn ngôn ngữ C++ (bản 17 trở lên) và dán mã nguồn vào ô nộp bài, hai bước sau do máy chấm làm. Trên máy cá nhân, bạn dùng một môi trường lập trình như Code::Blocks, Dev-C++ hay VS Code có cài g++, hoặc gõ g++ -std=c++17 main.cpp -o main trong cửa sổ dòng lệnh rồi chạy tệp main vừa tạo.
Đây là khung ta dùng cho cả khóa. Nó in một dòng chữ rồi dừng:
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout << "Hello, World!\n";
}
Hello, World!
Đọc từng dòng, từ trên xuống:
#include <bits/stdc++.h>nạp (include) thư viện, tức kho công cụ viết sẵn, trong đó cócoutđể in. Tệp này đi kèm g++ và gom gần hết thư viện chuẩn vào một dòng nên dân thi đấu hay dùng; trình biên dịch khác như Visual Studio hay clang trên macOS không có nó, khi đó viết#include <iostream>. Dòng bắt đầu bằng#không có dấu;.using namespace std;Công cụ của thư viện chuẩn nằm trong một “ngăn” tên làstd. Có dòng này, ta viếtcoutthay vìstd::cout.int main() { ... }khai báo hàm (một khối lệnh có tên)main, nơi chương trình bắt đầu chạy. Các lệnh trong cặp ngoặc nhọn{ }được thực hiện lần lượt từ trên xuống. Mỗi chương trình có đúng mộtmain.ios::sync_with_stdio(false);vàcin.tie(0);giúp nhập xuất nhanh hơn. Chương trình này chưa đọc dữ liệu nên chưa thấy lợi; bài B1 sẽ giải thích. Cứ viết sẵn như một thói quen.cout << "Hello, World!\n";là lệnh thật sự làm việc: in dòng chữ ra màn hình.
Mọi lệnh kết thúc bằng dấu ;, như dấu chấm kết thúc câu; quên nó là lỗi người mới gặp nhiều nhất. C++ phân biệt chữ hoa và chữ thường: Cout, Main, INT đều sai. Nhiều sách còn viết return 0; trước dấu } cuối: main trả về một số nguyên cho hệ điều hành, và 0 nghĩa là chạy xong bình thường. Dòng ấy có thể bỏ vì C++ tự thêm cho main, và khóa này bỏ cho gọn.
Chương trình hỏng theo ba kiểu, và biết mình gặp kiểu nào là xong nửa việc sửa:
- Lỗi biên dịch (Compile Error): sai cú pháp, g++ từ chối dịch nên không có chương trình nào để chạy.
- Lỗi khi chạy (Runtime Error): dịch được nhưng chương trình dừng giữa chừng, như khi chia cho 0.
- Sai kết quả (Wrong Answer): chạy xong bình thường nhưng output khác đáp án.
Lỗi biên dịch có lời báo khá rõ; trên LQDOJ bạn đọc nó ở trang kết quả bài nộp khi nhận Compile Error. Giả sử lệnh cout << "Hello" nằm ở dòng 8 của khung trên, thiếu dấu ;, và dòng 9 là một lệnh cout khác. g++ báo:
main.cpp:8:20: error: expected ';' before 'cout'
8 | cout << "Hello"
| ^
| ;
main.cpp:8:20 là tệp, dòng và cột; sau error: là lời mô tả. Ba thói quen giúp đọc nhanh:
- Sửa lỗi đầu tiên trước rồi biên dịch lại. Các lỗi phía sau thường chỉ là hệ quả: một dấu
}thiếu có thể sinh hàng chục dòng lỗi. - Nhìn cả dòng được báo lẫn dòng ngay phía trên. Trình biên dịch chỉ nhận ra sai khi đọc tới chỗ vô lý, nên tùy phiên bản g++, thiếu
;có thể bị báo ở chính dòng ấy hoặc ở dòng kế tiếp. - Đọc lời mô tả.
expected ';' before ...là thiếu;ở cuối lệnh liền trước;'Cout' was not declared in this scopelà một cái tên chưa khai báo (gõ sai hoa thường, quên nháy kép quanh chữ, hay quênusing namespace std;);expected '}' at end of inputlà có{mở mà chưa đóng.
Cảnh báo (warning) không chặn việc dịch nhưng đừng bỏ qua: lệnh g++ -std=c++17 -Wall main.cpp -o main bật thêm cảnh báo, chẳng hạn khi biến được dùng lúc chưa gán. Với lỗi khi chạy và sai kết quả, g++ không nói gì; ta chạy tay, hoặc chèn lệnh cout in biến giữa chừng để xem chương trình đang làm gì, rồi xóa các lệnh in đó đi.
Ví dụXin chào C++
Đề. In đúng dòng chữ Hello, C++!. Không có dữ liệu vào.
Lời giải. Dùng nguyên bộ khung, chỉ đổi xâu trong lệnh cout. Máy chấm so từng ký tự, nên dấu phẩy, dấu cách, dấu chấm than và hai dấu + phải đúng như đề, cuối dòng có \n.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout << "Hello, C++!\n";
}
Hello, C++!
In chữ và số với cout
cout (console output) đại diện cho màn hình. Toán tử << đẩy từng mảnh vào cout, mảnh nào đẩy trước thì hiện trước. Chữ đặt trong nháy kép (xâu ký tự, hay string) được in nguyên văn; số viết thẳng, không cần nháy.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout << "Score: " << 95 << "\n";
cout << 1 << 2 << 3 << "\n";
cout << 1 << " " << 2 << " " << 3 << "\n";
cout << "Line 1\nLine 2\n";
}
Score: 95
123
1 2 3
Line 1
Line 2
Dòng đầu nối chữ và số trong một lệnh. Dòng hai in 123 dính liền: cout không tự chèn dấu cách, muốn có khoảng trắng phải đẩy " " vào như dòng ba. \n (gạch chéo ngược rồi chữ n) là ký tự xuống dòng; nó có thể nằm giữa một xâu như dòng bốn, hoặc đứng riêng trong nháy đơn '\n' (nháy đơn dành cho đúng một ký tự; bài B1 viết theo cách này, hai cách cho cùng kết quả). Nhiều ngôn ngữ khác tự xuống dòng sau mỗi lệnh in, còn cout thì không: thiếu \n, chữ in sau nối ngay vào chữ trước. Muốn in chính dấu nháy kép hay dấu \ thì viết \" và \\ trong xâu: cout << "\"Hi\"\n"; in ra "Hi". Ta cũng không dùng endl vì nó chậm khi in nhiều dòng; bài B1 nói rõ vì sao.
Bốn chỗ hay sai:
- Quên nháy kép:
cout << Hello;làm g++ đi tìm một biến tênHellorồi báo lỗi. - Gõ
/nthay cho\n: máy in hẳn hai ký tự/n. Dấu\nằm gần phím Enter. - Máy chấm so output với đáp án, nên một chữ thừa, một dấu cách thiếu hay một dòng nhắc như
Ket qua:đều có thể thành Wrong Answer. Đề viết “không in thêm lời nhắc” vì thế. - Chỉ in chữ không dấu: chữ có dấu hiển thị tùy bảng mã của hệ thống, còn đề thi thường chỉ đòi ký tự ASCII.
Hỏi nhanh
Chương trình chỉ có ba lệnh: cout << 1; rồi cout << 2; rồi cout << 3;, không lệnh nào in \n. Màn hình hiện gì?
Mỗi lệnh cout nối thêm vào cuối những gì đã in, không tự chèn dấu cách hay xuống dòng, nên ba số dính vào nhau thành 123. Đáp án ba dòng tưởng mỗi lệnh in tự xuống dòng như ở một số ngôn ngữ khác. Đáp án 1 2 3 tưởng máy tự chèn dấu cách. Còn 3 nhầm rằng lệnh sau xóa lệnh trước, trong khi màn hình giữ mọi thứ đã in.
Ví dụMột hàng số
Đề. In các số trên một dòng, cách nhau bởi một dấu cách.
Lời giải. cout không tự chèn dấu cách, nên đẩy " " vào giữa các số; thiếu nó thì máy in 123. Kết thúc dòng bằng \n.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout << 1 << " " << 2 << " " << 3 << "\n";
}
1 2 3
Phép tính với số nguyên
Số nguyên thông thường trong C++ có kiểu int. Năm phép tính là +, -, * (nhân), / (chia) và % (lấy phần dư). Chương trình sau thử từng phép, kèm vài chỗ dễ nhầm:
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout << 8 + 6 << "\n";
cout << "8 + 6" << "\n";
cout << 8 - 11 << "\n";
cout << 8 * 6 << "\n";
cout << 7 / 2 << "\n";
cout << 7 % 2 << "\n";
cout << -7 / 2 << " " << -7 % 2 << "\n";
cout << 7 / 2.0 << "\n";
cout << 1 + 2 * 5 << "\n";
cout << (1 + 2) * 5 << "\n";
cout << 100 / 5 * 2 << "\n";
cout << 100 / (5 * 2) << "\n";
}
14
8 + 6
-3
48
3
1
-3 -1
3.5
11
15
40
10
Dòng một không có nháy kép nên máy tính ra 14; dòng hai có nháy kép nên chỉ in lại chữ 8 + 6.
Chia hai số nguyên cho kết quả nguyên: 7 / 2 là 3, phần lẻ bị bỏ chứ không làm tròn. % cho phần dư của chính phép chia ấy, nên 7 % 2 là 1. Hai phép luôn khớp nhau: a == (a / b) * b + a % b với mọi số nguyên a và b khác 0 (ở đây / là phép chia nguyên của C++), chẳng hạn . Với số âm, thương bỏ phần lẻ về phía 0 và phần dư mang dấu của số bị chia: -7 / 2 là -3, -7 % 2 là -1. Muốn giữ phần lẻ thì ít nhất một số phải viết dạng số thực (kiểu double, bài B1 nói kỹ), có dấu chấm thập phân: 7 / 2.0 là 3.5. Chia số nguyên cho 0 thường làm chương trình dừng giữa chừng vì lỗi khi chạy.
Thứ tự tính giống toán học: *, /, % trước, +, - sau, nên 1 + 2 * 5 là 11. Dấu ngoặc tròn đổi thứ tự: (1 + 2) * 5 là 15. Các phép cùng mức tính từ trái sang phải, nên 100 / 5 * 2 là (100 / 5) * 2, bằng 40 chứ không phải 10. Phép tính C++ chỉ dùng ngoặc tròn; [ ] và { } mang nghĩa khác. Khi không chắc, cứ thêm ngoặc.
Hỏi nhanh
Giá trị của biểu thức 23 / 4 * 4 + 23 % 4 trong C++ là bao nhiêu?
Tính từ trái sang phải: 23 / 4 là 5 (bỏ phần lẻ), 5 * 4 là 20, 23 % 4 là 3, tổng là 23, đúng đẳng thức a == (a / b) * b + a % b với a = 23, b = 4.
Đáp án 26 “rút gọn” / 4 * 4 như trong đại số, nhưng phép chia nguyên đã bỏ phần lẻ trước khi nhân lại. Đáp án 20 quên cộng phần dư. Đáp án 4 hiểu 23 / 4 * 4 thành 23 / (4 * 4), trái với quy tắc các phép cùng mức tính từ trái sang phải.
Ví dụChia kẹo cố định
Đề. Chia đều 17 viên kẹo cho 5 bạn, mỗi bạn nhận một số nguyên viên như nhau; in số kẹo mỗi bạn nhận và số kẹo còn dư trên một dòng, cách nhau một dấu cách.
Lời giải. Số kẹo mỗi bạn nhận là thương nguyên của 17 chia 5, số kẹo dư là phần dư của phép chia ấy: đúng là / và % ở trên. Thương nhân số bạn cộng phần dư trả lại số kẹo ban đầu, , và phần dư nhỏ hơn số bạn nên không đủ chia thêm mỗi bạn một viên. Cất 17 và 5 vào hai biến có tên để sau này đổi số chỉ sửa một chỗ.
Chỗ dễ sai là quên " " giữa hai số (máy in 32), đảo thứ tự hai số, hay viết 17.0 / 5 làm thương thành 3.4.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int candies = 17, kids = 5;
cout << candies / kids << " " << candies % kids << "\n";
}
3 2
Biến: ô nhớ có tên
Muốn dùng lại một kết quả, ta cất nó vào biến (variable): một ô nhớ có tên, chứa một giá trị. Câu int a = 10; tạo ô kiểu số nguyên tên a và bỏ số 10 vào đó.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int a = 10;
int b = a + 5;
a = a * 2;
b += 3;
cout << a << " " << b << "\n";
}
20 18
Dòng một tạo ô a và bỏ 10 vào. Dòng hai lấy giá trị trong a, cộng 5, bỏ vào ô b. Dòng ba đọc a, nhân 2, ghi lại vào a. Dòng bốn là cách viết gọn của b = b + 3. Dấu = là phép gán (assignment), không phải so sánh (so sánh dùng ==, bài B2): máy tính vế phải trước rồi ghi kết quả vào biến bên trái, đè lên giá trị cũ. Vì thế a = a * 2; hợp lệ, đọc là “lấy a, nhân 2, ghi lại vào a”, dù trong toán học chỉ đúng khi . Các phép gán gộp +=, -=, *=, /=, %= viết gọn hơn, còn a++ nghĩa là a += 1. Gán chỉ chép giá trị tại lúc đó: sau int b = a + 5;, biến b sống riêng, a đổi về sau không kéo b theo, khác với ô công thức trong bảng tính.
Hỏi nhanh
Máy chạy lần lượt int a = 4; int b = a; a = 10; rồi in cout << a << " " << b;. Màn hình hiện gì?
int b = a; chép giá trị 4 của a vào b ngay lúc đó, rồi b sống riêng. Lệnh a = 10; chỉ đổi a, nên in ra 10 4. Đáp án 10 10 coi b như một công thức luôn theo a; 4 4 bỏ quên lệnh a = 10;; còn 4 10 đảo thứ tự in.
Tên biến gồm chữ cái, chữ số và dấu _, không bắt đầu bằng chữ số, không trùng từ khóa như int, return; total và Total là hai biến khác nhau. Hãy đặt tên theo nghĩa (candies, price) thay vì x1, x2.
Khai báo mà chưa gán giá trị là chỗ sai kinh điển:
int c;
cout << c << "\n";
Biến khai báo trong main mà chưa gán chứa giá trị tùy ý còn sót trong bộ nhớ: hôm nay in 0, mai in một số lạ, nên lỗi lúc có lúc không và rất khó tìm. Luôn gán giá trị đầu ngay khi khai báo, như int c = 0;.
Một int chỉ chứa được các số nguyên từ -2147483648 đến 2147483647, tức cỡ hai tỷ về mỗi phía. Phép tính vượt khỏi khoảng đó không báo lỗi mà trả số sai: đó là tràn số (overflow). Kiểu long long chứa được số lớn hơn nhiều, tới khoảng 9223372036854775807 (số có 19 chữ số):
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
long long a = 50000;
long long area = a * a;
cout << area << "\n";
}
2500000000
Nếu a là int thì tích a * a vẫn tính trong int và tràn ngay, dù kết quả sau đó được cất vào biến long long; máy thường in một số âm vô nghĩa như -1794967296. Vì vậy phải khai báo long long cho chính các biến tham gia phép tính. Quy tắc nhớ: số nào có thể vượt hai tỷ thì dùng long long.
Hỏi nhanh
Ta muốn in đúng tích 50000 * 50000, bằng 2500000000 (hai tỷ rưỡi, vượt int). Cách nào làm được?
Chỉ cách đầu cho phép nhân thực hiện trong long long. Ở cách hai, a * a là tích của hai int nên tràn trước, rồi kết quả sai mới được chép vào c; đổi kiểu của c không cứu được phần đã mất. Cách ba tràn int hiển nhiên. Cách bốn nhân hai hằng số kiểu int nên cũng tràn, và g++ còn cảnh báo integer overflow in expression.
Ví dụBiến đầu tiên
Đề. Khai báo một biến số nguyên có giá trị 10, tăng nó thêm 3, rồi in giá trị của biến.
Lời giải. Đề muốn in giá trị của biến, nên in a chứ không in thẳng số 13. a += 3 cộng 3 vào chính a.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int a = 10;
a += 3;
cout << a << "\n";
}
13
Bảng theo dõi biến
Chương trình thực hiện từng lệnh theo thứ tự từ trên xuống, và mỗi lệnh chỉ đổi vài biến. Muốn hiểu một đoạn code, hay tìm chỗ sai trong nó, ta chạy tay: kẻ một bảng, mỗi cột một biến, mỗi hàng sau một lệnh, rồi ghi giá trị mới.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int x = 6;
int y = x * 2;
x = x + y;
y = y - x;
cout << x << " " << y << "\n";
}
18 -6
Ở lệnh x = x + y;, vế phải dùng giá trị đang có là 6 và 12, rồi kết quả 18 ghi đè lên x; giá trị 6 cũ biến mất. Vì vậy ở lệnh sau, y - x là chứ không phải . Giá trị đã bị ghi đè là mất hẳn, và chính điều này khiến bài hoán đổi ở dưới cần thêm một biến tạm.
Hỏi nhanh
Máy chạy lần lượt int u = 2; int v = u + 3; u = v * 2; v = u - v; rồi in cout << u << " " << v;. Màn hình hiện gì?
Kẻ bảng: sau lệnh đầu u = 2; sau lệnh hai v = 2 + 3 = 5; lệnh ba tính v * 2 = 10 rồi ghi đè lên u, nên u = 10; lệnh bốn dùng giá trị u mới, v = 10 - 5 = 5. Màn hình hiện 10 5.
Đáp án 10 -3 vẫn dùng u = 2 ở lệnh cuối, quên rằng u đã bị ghi đè. Đáp án 10 -5 tính v - u thay vì u - v. Đáp án 5 10 đảo thứ tự hai số khi in.
Ví dụĐổi giá trị
Đề. Hai biến số nguyên có giá trị lần lượt là 4 và 9; hoán đổi giá trị của hai biến rồi in giá trị mới của chúng trên một dòng, theo thứ tự ban đầu của hai biến.
Lời giải. Viết a = b; b = a; không được: sau lệnh đầu cả hai biến đều là 9, giá trị 4 đã bị ghi đè và mất, đúng như ta thấy ở bảng theo dõi biến. Cần một chỗ cất tạm giá trị cũ của a trước khi ghi đè nó, là biến tạm t: muốn đổi nước giữa hai ly thì phải có ly thứ ba. Ba lệnh theo thứ tự: t = a; rồi a = b; rồi b = t;.
Gọi giá trị ban đầu của a và b là và . Sau t = a; thì t giữ ; sau a = b; thì a bằng , còn vẫn nằm trong t; sau b = t; thì b bằng . Đúng là hoán đổi, với mọi giá trị chứ không riêng 4 và 9.
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int a = 4, b = 9;
int t = a;
a = b;
b = t;
cout << a << " " << b << "\n";
}
9 4
Thư viện có sẵn hàm swap(a, b); làm đúng việc này trong một lệnh. Tự viết ba lệnh một lần để hiểu vì sao cần biến tạm; từ các bài sau ta dùng swap cho gọn.
Khi đi thi
- Dán khung mẫu trước rồi mới nghĩ lời giải; chạy thử trên ví dụ của đề và so output từng ký tự: dấu cách, xuống dòng, không lời nhắc.
- Gặp lỗi biên dịch thì sửa lỗi đầu tiên, nhìn cả dòng được báo lẫn dòng phía trên nó, rồi biên dịch lại trước khi sửa tiếp.
- Gán giá trị đầu cho mọi biến, dùng
long longkhi số có thể vượt hai tỷ, và nhớ/giữa hai số nguyên bỏ phần lẻ.
Tóm tắt
- Khung:
#include <bits/stdc++.h>,using namespace std;,int main() { ... }; mỗi lệnh kết thúc bằng;, chữ hoa và chữ thường khác nhau. cout << a << bin lần lượt từng mảnh, không tự chèn dấu cách hay xuống dòng; xuống dòng bằng\n./giữa hai số nguyên bỏ phần lẻ,%lấy phần dư, vàa == (a / b) * b + a % b.*,/,%tính trước+,-; cùng mức thì từ trái sang phải.=là phép gán: ghi đè giá trị cũ và chỉ chép giá trị. Luôn gán giá trị đầu;intchứa tới khoảng hai tỷ, vượt thì dùnglong long.- Hoán đổi hai biến cần một biến tạm; muốn kiểm một đoạn code thì chạy tay bằng bảng theo dõi biến.
Bài tập
+.<<; để ý dấu cách sau dấu hai chấm.***.*.7 + 5 mà không đặt trong nháy kép.a = a * 3;, theo đúng thứ tự đề bài.Đọc thêm
- Hello, World!OI WikiChọn môi trường lập trình, cài trình biên dịch, biên dịch bằng dòng lệnh
g++và chương trình đầu tiên, viết cho người thi đấu (tiếng Trung). - Tôi đã học tin học như thế nào? Phần 2VNOI WikiKinh nghiệm của một cựu tuyển thủ: vì sao chọn C++, dùng công cụ nào, biên dịch bằng
g++ra sao và nên làm những bài nào trước. - Structure of a programcplusplus.comGiải thích từng dòng của chương trình đầu tiên, vùng tên
std, chú thích và dấu chấm phẩy. - Arithmetic operatorsLearn C++Phép chia nguyên và chia cho 0, những chỗ dễ nhầm của toán tử số học.
- Signed integersLearn C++Khoảng giá trị của từng kích thước số nguyên và điều xảy ra khi vượt khỏi khoảng đó.
- Uninitialized variables and undefined behaviorLearn C++Vì sao biến chưa gán giá trị gây lỗi lúc có lúc không.