Biến, kiểu dữ liệu và nhập/xuất
Mọi chương trình đều xoay quanh một việc: nhận dữ liệu vào, xử lý, rồi đưa kết quả ra. Hãy tưởng tượng bạn viết một chương trình tính điểm trung bình. Máy cần một chỗ để giữ từng con điểm, một chỗ để giữ kết quả trung bình, và một cách để hỏi người dùng nhập số rồi in đáp án lên màn hình. Những chỗ giữ dữ liệu đó chính là biến, còn việc hỏi và in là nhập/xuất. Nắm chắc hai thứ này là bạn đã có bộ khung cho gần như mọi bài.
Trong C++, một biến là một ô nhớ có tên để ta gọi nó, và có kiểu quy định nó
chứa loại dữ liệu gì cùng chiếm bao nhiêu byte. Khác với Python (chỗ mà một biến có thể
lúc giữ số, lúc giữ chữ), C++ bắt bạn khai báo kiểu ngay từ đầu và giữ nguyên kiểu đó.
Điều này thoạt nghe phiền, nhưng đổi lại chương trình chạy nhanh và ít nhập nhằng. Ta đưa
kết quả ra màn hình bằng cout, và đọc dữ liệu từ bàn phím vào biến bằng cin. Cả bài
này ta đi qua các kiểu hay dùng, cách khai báo và khởi tạo, ép kiểu, hằng số, rồi tới
cin/cout/getline và một cái bẫy kinh điển tên là tràn số.
Vì sao C++ lại khắt khe về kiểu như vậy? Câu trả lời nằm ở chỗ máy tính không hiểu khái
niệm "một con số" chung chung. Bộ nhớ chỉ là một dãy ô byte, mỗi byte chứa tám bit 0
hoặc 1. Cùng một chuỗi bit, nếu đọc theo cách này thì ra số nguyên, đọc theo cách khác
thì ra số thực, đọc kiểu khác nữa thì ra một ký tự. Kiểu chính là bản hướng dẫn nói cho
máy biết phải đọc và diễn giải các bit đó ra sao, và phải dành bao nhiêu byte cho biến.
Khi bạn viết int tuoi = 20;, trình biên dịch làm ba việc: dành ra bốn byte liên tiếp
trong bộ nhớ, ghi nhớ rằng khối bốn byte đó tên là tuoi và phải được đọc theo cách của
số nguyên có dấu, rồi ghi vào đó dạng nhị phân của số 20.
Hệ quả là mọi phép tính trên biến đều được trình biên dịch quyết định sẵn từ lúc dịch, không phải dò tìm lúc chạy. Đó là lý do một chương trình C++ chạy nhanh hơn nhiều so với một ngôn ngữ động, nơi mỗi phép cộng phải hỏi lại "hai thứ này thực ra là gì" trước khi cộng. Cái giá phải trả là bạn phải suy nghĩ trước về kiểu, và phải chịu trách nhiệm khi dữ liệu vượt quá sức chứa của kiểu mình chọn. Bài này dạy bạn cách chọn cho đúng.
Quy ước: mọi đoạn mã dưới đây ngầm hiểu đã có #include <iostream> (hoặc
#include <bits/stdc++.h>) và using namespace std; ở đầu file, viết một lần rồi bỏ qua
cho gọn. Khi cần string thì thêm #include <string>, khi cần vector thì thêm
#include <vector>.
Ý tưởng cốt lõi
- Khai báo biến:
int tuoi = 20;, kiểuint(số nguyên), têntuoi, giá trị20. - Vài kiểu hay dùng:
int(số nguyên),double(số thực),string(chuỗi),bool(đúng/sai). cout << x;inxra màn hình;cin >> x;đọc từ bàn phím vàox.
Thử ngay: xem biến và nhập/xuất chạy từng bước
1int main() {2 int tuoi = 20;3 double diem = 8.5;4 string ten = "Lan";5 cout << ten << " - " << tuoi6 << " tuoi - " << diem;7}
Khai báo và khởi tạo biến
Muốn dùng một biến, trước hết phải khai báo nó: cho biết kiểu và tên. Cú pháp cơ bản
là kiểu tên; hoặc kiểu tên = giá_trị_ban_đầu;. Dạng thứ hai vừa tạo biến vừa đặt luôn
giá trị, gọi là khởi tạo.
int tuoi; // khai bao, chua co gia tri xac dinh
tuoi = 20; // gan gia tri sau
int diem = 8; // khai bao va khoi tao cung luc
int a = 1, b = 2, c = 3; // nhieu bien cung kieu, ngan cach bang dau phay
Có một điểm khác Python rất dễ sập bẫy: biến cục bộ trong C++ không tự động bằng 0.
Nếu bạn viết int dem; rồi cộng dồn vào dem mà quên gán dem = 0; trước, thì dem
bắt đầu từ một số rác bất kỳ và kết quả sẽ sai một cách khó hiểu. Thói quen tốt là luôn
khởi tạo ngay khi khai báo.
Tên biến nên gợi nghĩa (tong, dem_chan, diemTB), chỉ gồm chữ, số và dấu gạch dưới,
không bắt đầu bằng số, và phân biệt hoa thường (Tong khác tong).
int tong; for (...) tong += a[i]; cho ra số rác vì tong chưa được đặt về 0. Luôn viết
int tong = 0;. Với biến đếm khởi tạo 0, với biến tích khởi tạo 1.
Các kiểu dữ liệu cơ bản
Bảng dưới là những kiểu bạn dùng đi dùng lại suốt khóa học:
int: số nguyên 32 bit, chứa khoảng từ âm hai tỉ tới dương hai tỉ (chính xác là $\pm 2.1 \times 10^9$). Dùng cho đa số đếm, chỉ số, số lượng.long long: số nguyên 64 bit, chứa tới khoảng $\pm 9.2 \times 10^18$. Dùng khi tổng hoặc tích có thể lớn.double: số thực (có phần thập phân), như8.5,3.14,-0.001.char: một ký tự đơn, viết trong dấu nháy đơn:'A','z','7'.bool: giá trị đúng/sai, chỉ nhậntruehoặcfalse.string: chuỗi ký tự, viết trong dấu nháy kép:"Xin chao".
int soLuong = 42; // so nguyen
long long danSo = 8000000000LL; // so lon, hau to LL
double giaTien = 19.99; // so thuc
char hangChu = 'A'; // mot ky tu
bool daDung = true; // dung/sai
string hoTen = "Nguyen Van A"; // chuoi
Chú ý dấu nháy: 'A' là một char, còn "A" là một string dài một ký tự, hai thứ
khác nhau. Số thực dùng double; đừng nhét số thực vào int vì phần thập phân sẽ bị cắt
mất. Bên trong máy, char thực chất là một số nguyên nhỏ (mã ASCII), nên 'A' có giá
trị 65 và bạn có thể làm phép tính trên nó, ví dụ 'a' + 1 cho ra 'b'.
Kích thước và giới hạn của từng kiểu
Mỗi kiểu chiếm một số byte cố định, và chính số byte đó quyết định nó chứa được tới đâu.
Một kiểu n bit có 2^n mẫu bit khác nhau, nên chứa được đúng 2^n giá trị, không hơn.
Với số nguyên có dấu, một nửa số mẫu dành cho giá trị âm, nên khoảng chứa là từ
-2^(n-1) tới 2^(n-1) - 1. Bảng dưới ghi các con số bạn nên thuộc lòng trên máy 64 bit
thông dụng:
| Kiểu | Số byte | Khoảng chứa xấp xỉ |
|---|---|---|
int | 4 | -2.1e9 tới 2.1e9 |
long long | 8 | -9.2e18 tới 9.2e18 |
unsigned int | 4 | 0 tới 4.3e9 |
float | 4 | khoảng 7 chữ số có nghĩa |
double | 8 | khoảng 15 chữ số có nghĩa |
char | 1 | -128 tới 127 |
bool | 1 | chỉ true hoặc false |
string | n/a | dài tùy nội dung, tự cấp phát thêm |
Bạn có thể tự kiểm chứng bằng toán tử sizeof, nó trả về số byte của một kiểu hay một
biến:
#include <iostream>
using namespace std;
int main() {
cout << sizeof(int) << " " << sizeof(long long) << "\n"; // 4 8
cout << sizeof(double) << " " << sizeof(char) << "\n"; // 8 1
return 0;
}
Đừng học thuộc bảng như học vẹt. Điều cần rút ra là: int đủ cho gần như mọi biến đếm và
chỉ số, nhưng không đủ khi bạn cộng dồn hàng triệu số lớn hoặc nhân hai số cỡ một
triệu. Ranh giới hai tỉ nghe thì to, thực tế lại rất dễ chạm. Một tích 100000 * 100000
đã là mười tỉ, vượt int từ lâu. Khi nghi ngờ, dùng long long: nó chỉ tốn thêm bốn
byte, một cái giá không đáng kể so với một đáp án sai.
Kiểu không dấu (unsigned) bỏ hẳn phần âm để dồn toàn bộ mẫu bit cho số dương, nên
chứa gấp đôi về phía dương. Nghe có vẻ hời, nhưng nó là nguồn của một lớp lỗi rất khó
thấy: một biểu thức không dấu không bao giờ âm được, nên khi kết quả lẽ ra phải âm thì nó
quấn vòng thành một số dương khổng lồ. Hàm size() của vector và string trả về kiểu
không dấu, nên đoạn sau chạy sai một cách bất ngờ:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v; // vector rong, v.size() = 0
// SAI: v.size() - 1 la khong dau, 0 - 1 quan vong thanh so rat lon
for (int i = 0; i + 1 < (int)v.size(); i++) {
cout << v[i] << " "; // cach viet an toan: ep ve int truoc
}
cout << "ok\n";
return 0;
}
Nếu viết i <= v.size() - 1 với vector rỗng, v.size() - 1 không cho -1 mà cho một số
xấp xỉ mười tám tỉ tỉ, điều kiện luôn đúng và vòng lặp lao vào vùng nhớ không thuộc về
nó. Quy tắc an toàn: khi phải trừ hay so sánh với size(), hãy ép nó về int bằng
(int)v.size(), hoặc viết điều kiện dưới dạng cộng như trong ví dụ trên.
Số thực và bẫy so sánh bằng
double biểu diễn số thực theo chuẩn dấu phẩy động: một phần lưu chữ số có nghĩa, một
phần lưu số mũ. Cơ chế này cho phép chứa cả số rất lớn lẫn rất nhỏ trong tám byte, nhưng
đổi lại nó chỉ xấp xỉ. Cũng như hệ thập phân không viết nổi 1/3 bằng hữu hạn chữ
số, hệ nhị phân không viết nổi 0.1 bằng hữu hạn bit. Máy lưu một giá trị gần 0.1 nhất
có thể, sai lệch cỡ 10^-17, rồi mọi phép tính tiếp theo mang theo sai lệch đó.
#include <iostream>
#include <cstdio>
using namespace std;
int main() {
double x = 0.1 + 0.2;
if (x == 0.3) cout << "bang nhau\n";
else cout << "KHONG bang nhau\n"; // dong nay moi chay
printf("%.17f\n", x); // 0.30000000000000004
return 0;
}
Kết quả in ra cho thấy 0.1 + 0.2 không cho đúng 0.3 mà cho một số lệch ở chữ số thứ
mười bảy. Vì vậy so sánh hai số thực bằng == gần như luôn là một lỗi. Cách đúng là hỏi
"hai số này có đủ gần nhau không", tức so sánh trị tuyệt đối của hiệu với một ngưỡng nhỏ,
thường gọi là epsilon:
#include <iostream>
#include <cmath>
using namespace std;
int main() {
double x = 0.1 + 0.2;
const double EPS = 1e-9; // nguong sai so chap nhan duoc
if (fabs(x - 0.3) < EPS) cout << "coi nhu bang nhau\n";
return 0;
}
if (a == b) với a, b kiểu double có thể sai ngay cả khi về mặt toán học hai vế
bằng nhau, vì sai số làm tròn nhị phân tích lũy qua từng phép tính. Luôn so sánh qua
ngưỡng: if (fabs(a - b) < 1e-9). Cùng lý do đó, đừng dùng double làm biến đếm vòng
lặp và đừng dùng double để lưu tiền tệ khi cần chính xác tuyệt đối; hãy lưu số xu bằng
long long.
Còn một câu hỏi hay gặp: khi nào dùng float thay double? Câu trả lời cho người mới
là gần như không bao giờ. float chỉ có bốn byte và khoảng bảy chữ số có nghĩa, sai số
tích lũy nhanh hơn hẳn, trong khi lợi ích tiết kiệm bộ nhớ chỉ đáng kể khi bạn giữ hàng
triệu số cùng lúc. Mặc định hãy dùng double.
Suy kiểu tự động với auto
Từ C++11, bạn có thể để trình biên dịch tự suy ra kiểu của biến từ giá trị khởi tạo, bằng
từ khóa auto. Đây không phải kiểu động như Python: kiểu vẫn được chốt cứng lúc dịch
và không đổi được nữa, chỉ là bạn khỏi phải gõ tên nó ra.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main() {
auto n = 10; // n la int
auto x = 2.5; // x la double
auto s = string("xin chao"); // s la string
vector<int> v = {3, 1, 4};
for (auto giaTri : v) cout << giaTri << " "; // giaTri la int
cout << "\n" << n << " " << x << " " << s << "\n";
return 0;
}
auto phát huy tác dụng nhất khi tên kiểu dài dòng, ví dụ khi duyệt các cấu trúc dữ liệu
phức tạp mà bạn sẽ gặp ở những bài sau. Đánh đổi là mã trở nên khó đọc nếu lạm dụng: nhìn
auto kq = tinhToan(a, b); bạn không biết kq là số nguyên hay số thực nếu không lần
ngược về định nghĩa hàm. Lời khuyên thực dụng: với biến cơ bản trong bài tập, cứ ghi kiểu
rõ ràng cho dễ đọc; dành auto cho những chỗ tên kiểu thật sự cồng kềnh. Chú ý auto x = 5;
cho int chứ không phải double, nên auto tong = 0; rồi cộng số thực vào sẽ mất phần lẻ.
Hằng số với const
Có những giá trị không bao giờ đổi trong suốt chương trình: số Pi, số học sinh tối đa,
thuế suất. Đặt chúng thành hằng bằng từ khóa const. Sau khi gán, mọi cố gắng thay
đổi hằng sẽ bị trình biên dịch báo lỗi, giúp bạn tránh sửa nhầm.
const double PI = 3.14159;
const int MAX_HS = 1000;
// PI = 3.14; // loi bien dich: khong duoc gan lai hang
double chuVi = 2 * PI * 5; // dung binh thuong trong bieu thuc
Dùng hằng còn giúp mã dễ đọc: nhìn MAX_HS hiểu ngay ý nghĩa, hơn hẳn một con số 1000
rải rác khắp nơi.
Ép kiểu
Đôi khi bạn cần đổi một giá trị từ kiểu này sang kiểu khác, gọi là ép kiểu. Cách viết
rõ ràng nhất là (kiểu)giá_trị. Ví dụ kinh điển là phép chia: chia hai số int cho nhau
thì C++ làm chia nguyên, bỏ phần lẻ.
int a = 7, b = 2;
cout << a / b << "\n"; // in ra 3, chia nguyen
cout << (double)a / b << "\n"; // in ra 3.5, ep a sang double truoc
double tb = (double)(a + b) / 2; // trung binh dung, co phan le
Ngược lại, ép một double về int sẽ cắt cụt phần thập phân (không làm tròn): (int)3.9
cho 3, (int)-3.9 cho -3. Nắm quy tắc này để không bị mất dữ liệu ngoài ý muốn.
double tb = (a + b) / 2; với a, b kiểu int vẫn ra sai vì phép (a + b) / 2 được
tính trong int (chia nguyên) rồi mới gán vào double. Phải ép trong biểu thức:
(double)(a + b) / 2.
Nhập với cin, xuất với cout
Toán tử >> đọc dữ liệu vào biến, tự bỏ qua khoảng trắng và ký tự xuống dòng giữa các
giá trị. Toán tử << đẩy giá trị ra màn hình. Vì đã khai báo kiểu, cin tự biết cần đọc
số hay chữ, bạn không phải ép kiểu thủ công như khi đọc chuỗi rồi convert trong Python.
int n;
cin >> n; // doc mot so nguyen
int a, b;
cin >> a >> b; // doc hai so, cach nhau bang khoang trang hoac xuong dong
cout << a + b << "\n"; // in ket qua roi xuong dong
cout << "Tong = " << a + b << "\n"; // ghep chuoi va so trong mot lenh
Nối nhiều << trong một lệnh để in liền mạch nhiều thứ. Dùng "\n" để xuống dòng; nó
nhanh và gọn hơn endl (vốn còn ép xả bộ đệm mỗi lần nên chậm khi in nhiều).
Để đọc một dãy n số, đọc n trước rồi lặp:
int n;
cin >> n;
long long tong = 0; // long long de tranh tran khi cong nhieu so
for (int i = 0; i < n; i++) {
int x;
cin >> x;
tong += x; // cong don tung so vao tong
}
cout << tong << "\n";
getline và cái bẫy cin.ignore
cin >> s chỉ đọc một từ, dừng lại ngay khi gặp khoảng trắng. Muốn đọc cả một dòng
gồm nhiều từ có dấu cách, dùng getline(cin, s).
string ten;
getline(cin, ten); // doc ca dong, ke ca co khoang trang: "Nguyen Van A"
cout << "Xin chao " << ten << "\n";
Cái bẫy hay gặp: khi trộn cin >> x rồi getline. Lệnh cin >> đọc giá trị nhưng
để lại ký tự xuống dòng trong bộ đệm; getline ngay sau đó đọc trúng ký tự thừa này
và trả về một dòng rỗng. Khắc phục bằng cin.ignore() để bỏ ký tự xuống dòng còn sót.
int tuoi;
cin >> tuoi;
cin.ignore(); // bo ky tu xuong dong con sot trong bo dem
string ten;
getline(cin, ten); // gio moi doc dung dong ten
Tràn số
Đây là cái bẫy phổ biến nhất với người mới. Khi một phép cộng hoặc nhân cho ra kết quả
vượt quá giới hạn của int (khoảng hai tỉ), ta gặp tràn số. Với số nguyên có dấu,
tràn số là hành vi không xác định: chương trình không báo lỗi gì cả, cứ chạy và
cho ra đáp án bậy. Nhiều máy cho ra một giá trị trông như bị quấn vòng, nhưng chuẩn ngôn
ngữ không hề bảo đảm điều đó, nên tuyệt đối đừng dựa vào. Chỉ kiểu không dấu
(unsigned) mới được bảo đảm quấn vòng theo phép chia lấy dư.
int a = 1000000, b = 1000000;
int sai = a * b; // 10^12 khong vua trong int, bi tran
long long dung = (long long)a * b; // 1000000000000, dung
cout << sai << " " << dung << "\n";
Điểm tinh tế: phép nhân a * b với a, b kiểu int được tính trong int rồi mới
gán. Nghĩa là nó đã tràn trước khi kịp chuyển sang long long. Muốn đúng, phải ép một
toán hạng sang long long ngay trong biểu thức: (long long)a * b.
long long dung = a * b; vẫn tràn vì a * b tính trong int trước rồi mới gán. Phải ép
sớm: (long long)a * b. Quy tắc chung: khi tổng hoặc tích có thể vượt hai tỉ, khai báo
biến và ép kiểu bằng long long.
Chọn kiểu phù hợp
Chọn kiểu là quyết định đầu tiên và cũng là quyết định dễ hối tiếc nhất, vì chọn sai thì chương trình vẫn dịch, vẫn chạy, chỉ ra đáp án sai. Một quy trình suy nghĩ gọn gàng gồm ba câu hỏi.
Câu hỏi thứ nhất: dữ liệu này về bản chất là gì? Số lượng người, số lần lặp, chỉ số phần
tử là số nguyên, chọn họ int. Điểm số, chiều dài, tỉ lệ phần trăm là số thực, chọn
double. Tên, địa chỉ, mã sinh viên có chữ cái là chuỗi, chọn string. Câu trả lời
có hay không là bool.
Câu hỏi thứ hai: giá trị lớn nhất có thể lên tới đâu, kể cả trong các kết quả trung gian?
Đây là chỗ nhiều người bỏ sót. Biến đầu vào có thể bé xíu nhưng tổng hoặc tích của chúng
thì không. Nếu đề bài nói mỗi số tối đa một triệu và có tối đa một trăm nghìn số, thì
tổng có thể tới 10^11, vượt int, phải dùng long long.
Câu hỏi thứ ba: có cần chính xác tuyệt đối không? Nếu có, hãy tránh số thực. Tiền tệ là ví
dụ kinh điển: lưu bằng double thì sau nhiều phép cộng, tổng có thể lệch vài xu và không
ai đối soát nổi. Lưu số xu bằng long long rồi chia cho 100 khi in ra là cách làm chắc
chắn.
Cuối cùng, hãy ưu tiên kiểu đơn giản nhất mà vẫn đủ dùng. Đừng khai báo long long cho
biến đếm vòng lặp chạy mười lần, cũng đừng dùng double cho số lượng học sinh. Kiểu đúng
làm mã tự giải thích ý đồ của bạn cho người đọc.
Mã nguồn
#include <iostream>
#include <string>
using namespace std;
int main() {
int tuoi = 20;
double diem = 8.5;
string ten = "Lan";
cout << ten << " - " << tuoi << " tuoi - " << diem;
}
Đây là chương trình mà trình mô phỏng ở đầu bài đã chạy. Lúc đó bạn mới thấy hiện tượng,
còn bây giờ đã đủ khái niệm để đọc chậm bốn dòng thân hàm này, vì chúng gói đủ mọi ý của
bài. Dòng int tuoi = 20;
dành bốn byte, đặt tên tuoi cho khối byte đó, và ghi vào giá trị 20 dưới dạng nhị phân.
Dòng double diem = 8.5; dành tám byte và ghi vào dạng dấu phẩy động của 8.5; ở đây may
mắn là 8.5 biểu diễn được chính xác trong nhị phân vì phần lẻ đúng bằng một nửa, nhưng
đừng vì thế mà tin rằng mọi số thực đều thế. Dòng string ten = "Lan"; khác hẳn hai dòng
trên: string không phải kiểu có kích thước cố định, nó tự quản lý một vùng nhớ chứa các
ký tự và tự nới rộng khi chuỗi dài ra, nên bạn không cần đếm trước độ dài như thời còn
phải khai báo mảng ký tự.
Dòng cuối cout << ten << " - " << tuoi << ... chạy từ trái sang phải, đẩy lần lượt từng
giá trị ra luồng xuất. Điểm đáng chú ý là cout biết in tuoi như một số nguyên và in
ten như một chuỗi, dù bạn chỉ viết cùng một toán tử <<. Nó làm được vì kiểu của mỗi
biến đã được biết từ lúc dịch, nên trình biên dịch chọn sẵn cách in đúng cho từng cái.
Đây chính là lợi ích cụ thể của việc khai báo kiểu mà phần dẫn nhập đã nói tới.
Tự kiểm tra
Vì sao diem nên là double chứ không phải int?
int chỉ chứa số nguyên; gán 8.5 vào int sẽ bị cắt phần thập phân còn 8.
double giữ được phần lẻ, nên dùng cho điểm số, số đo, tiền tệ có phần thập phân...
Bài 1: đọc hai số nguyên rồi in tổng, hiệu, tích của chúng.
#include <iostream>
using namespace std;
int main() {
int a, b;
cin >> a >> b;
cout << a + b << " " << a - b << " " << a * b << "\n";
return 0;
}
Ba giá trị in trên một dòng, cách nhau bằng dấu cách. Nếu lo tích lớn có thể tràn, đổi
sang long long và ép (long long)a * b.
Bài 2: đọc điểm ba môn (số thực) rồi in điểm trung bình với 2 chữ số thập phân.
#include <iostream>
#include <cstdio>
using namespace std;
int main() {
double a, b, c;
cin >> a >> b >> c;
double tb = (a + b + c) / 3;
// fixed + setprecision can #include <iomanip> hoac bits/stdc++.h
printf("%.2f\n", tb);
return 0;
}
Vì ba biến đã là double, phép chia là chia thực nên tb giữ phần lẻ đúng. Ở đây dùng
printf("%.2f") cho gọn; cách khác là cout << fixed << setprecision(2) << tb.
Bài 3: đọc họ tên đầy đủ (có dấu cách) trên một dòng rồi in lại.
#include <iostream>
#include <string>
using namespace std;
int main() {
string ten;
getline(cin, ten); // doc ca dong, giu duoc khoang trang
cout << "Xin chao " << ten << "\n";
return 0;
}
Nếu trước getline có một lệnh cin >> x thì phải chèn cin.ignore(); vào giữa, nếu
không getline sẽ đọc trúng dòng rỗng.
Bài 4: đọc số lượng n rồi đọc n số nguyên, in tổng của chúng. Mỗi số tối đa một triệu, n tối đa hai trăm nghìn.
#include <iostream>
using namespace std;
int main() {
int n;
cin >> n;
long long tong = 0; // tong toi da 2e5 * 1e6 = 2e11, vuot int
for (int i = 0; i < n; i++) {
int x;
cin >> x; // moi so vua trong int
tong += x; // x duoc mo rong sang long long truoc khi cong
}
cout << tong << "\n";
return 0;
}
Điểm mấu chốt nằm ở việc trả lời câu hỏi thứ hai của phần chọn kiểu: giá trị lớn nhất có
thể là bao nhiêu. Từng số đầu vào chỉ tới một triệu nên int thừa sức chứa, nhưng tổng
của hai trăm nghìn số như vậy lên tới 2 * 10^11, vượt xa giới hạn hai tỉ của int. Vì
vậy tong phải là long long. Ở đây không cần ép kiểu thủ công trong tong += x vì vế
trái đã là long long, nên x được tự động mở rộng trước khi cộng, khác hẳn trường hợp
a * b với cả hai đều là int.
Bài 5: đọc mã sinh viên (một từ) và họ tên đầy đủ (cả dòng, có dấu cách) rồi in ra.
#include <iostream>
#include <string>
using namespace std;
int main() {
string maSV;
cin >> maSV; // doc mot tu, de lai ky tu xuong dong trong bo dem
cin.ignore(); // bo ky tu xuong dong con sot
string hoTen;
getline(cin, hoTen); // gio moi doc dung ca dong ho ten
cout << maSV << ": " << hoTen << "\n";
return 0;
}
Bài này cố tình đặt bạn vào đúng tình huống trộn cin >> với getline. Sau khi
cin >> maSV lấy xong từ đầu tiên, ký tự xuống dòng do người dùng gõ vẫn nằm nguyên
trong bộ đệm. Nếu gọi getline ngay, nó thấy ký tự xuống dòng đầu tiên và kết luận dòng
đã hết, trả về một chuỗi rỗng. Dòng cin.ignore() bỏ đúng một ký tự còn sót đó, trả bộ
đệm về trạng thái sạch. Lưu ý phép so sánh tinh tế: lỗi này không làm chương trình sập, nó
chỉ lặng lẽ cho ra họ tên rỗng, nên rất dễ bị bỏ qua khi chấm bài.
Câu hỏi tự kiểm
- 1Với a, b kiểu int đều bằng 1000000, câu lệnh nào cho tích đúng 10^12 mà không bị tràn?
- 2Vì sao if (0.1 + 0.2 == 0.3) cho ra nhánh không bằng?
- 3Với vector<int> v rỗng, biểu thức v.size() - 1 cho giá trị gì?
- 4Sau cin >> tuoi; rồi getline(cin, ten); mà quên cin.ignore(), biến ten nhận gì?
Tóm tắt
- Biến là một khối byte trong bộ nhớ có tên và có kiểu; kiểu quyết định số byte và cách diễn giải các bit đó.
- Luôn khởi tạo biến ngay khi khai báo, vì biến cục bộ trong C++ không tự bằng
0. intbốn byte chứa tới khoảng hai tỉ,long longtám byte chứa tới khoảng chín tỉ tỉ; khi tổng hoặc tích có thể vượt hai tỉ thì dùnglong longvà ép kiểu ngay trong biểu thức, vì tràn số nguyên có dấu là hành vi không xác định và không hề báo lỗi.doublechỉ xấp xỉ số thực, nên không so sánh bằng==mà so qua ngưỡng epsilon.size()củavectorvàstringlà kiểu không dấu, đừng trừ trực tiếp mà hãy ép vềinttrước.constkhóa những giá trị không đổi;autođể trình biên dịch tự suy kiểu lúc dịch.cin >>đọc một từ và bỏ qua khoảng trắng,getlineđọc cả dòng; trộn hai thứ thì phải chèncin.ignore()vào giữa.
Biến = ô nhớ có tên và kiểu. Chọn đúng kiểu (int/double/string/bool)
để dữ liệu không bị mất mát. cout << để xuất, cin >> để nhập.