Trong bài này chúng ta sẽ tìm hiểu toán tử trong C++: toán tử số học, tăng giảm, gán, so sánh, logic, toán tử bit, toán tử ba ngôi và thứ tự ưu tiên giữa chúng. Toán tử thì nhìn qua ai cũng hiểu, nhưng bài này có mấy cái bẫy mà g++ biên dịch vẫn qua, chạy ra kết quả sai, nên các bạn chạy thử từng ví dụ nhé.
Bài 5 trong Serie Lập trình C++ từ A tới Z
Toán tử trong C++ là gì?
Toán tử (operator) là ký hiệu yêu cầu chương trình thực hiện một phép tính trên các giá trị, gọi là toán hạng (operand). Trong biểu thức a + b, dấu + là toán tử, còn a và b là toán hạng.
Toán tử trong C++ chia thành các nhóm:
| Nhóm | Toán tử |
|---|---|
| Số học | + - * / % |
| Tăng, giảm | ++ -- |
| Gán | = += -= *= /= %= |
| So sánh | == != < > <= >= |
| Logic | && || ! |
| Bit | & | ^ ~ << >> |
| Ba ngôi | điều_kiện ? a : b |
Toán tử số học
#include <iostream>
int main()
{
int a = 17, b = 5;
std::cout << "a + b = " << a + b << '\n';
std::cout << "a - b = " << a - b << '\n';
std::cout << "a * b = " << a * b << '\n';
std::cout << "a / b = " << a / b << '\n';
std::cout << "a % b = " << a % b << '\n';
std::cout << "-17 % 5 = " << -17 % 5 << '\n';
std::cout << "17.0 / 5 = " << 17.0 / 5 << '\n';
return 0;
}
Cộng, trừ, nhân thì như toán học bình thường. Hai toán tử cần để ý là / và %:
- Chia
/: 2 số nguyên chia nhau thì ra số nguyên, phần thập phân bị bỏ, nên17 / 5 = 3. Chỉ cần một toán hạng là số thực như17.0 / 5thì mới ra 3.4. Lỗi này mình đã nói kỹ ở bài các kiểu dữ liệu trong C++. - Chia lấy dư
%: chỉ dùng với số nguyên,17 % 5 = 2. Hay dùng để kiểm tra chẵn lẻ (n % 2 == 0) hay quay vòng chỉ số (i % 7ra thứ trong tuần).
Lưu ý: Với số âm, -17 % 5 ra -2 chứ không phải 3 như phép chia lấy dư trong toán học. Dấu của kết quả theo dấu của số bị chia. Vì vậy kiểm tra số lẻ phải viết n % 2 != 0, viết n % 2 == 1 sẽ sai với mọi số lẻ âm.
Toán tử tăng, giảm và toán tử gán
++ tăng biến lên 1, -- giảm đi 1. Mỗi toán tử có 2 dạng: đặt trước biến (++i) và đặt sau biến (i++). Đứng một mình thì như nhau, nhưng khi nằm trong một biểu thức thì khác:
#include <iostream>
int main()
{
int i = 5;
int x = i++; // gan truoc, tang sau
std::cout << "x = " << x << ", i = " << i << '\n';
int j = 5;
int y = ++j; // tang truoc, gan sau
std::cout << "y = " << y << ", j = " << j << '\n';
int soSach = 10;
soSach += 5; // soSach = soSach + 5
soSach *= 2; // soSach = soSach * 2
std::cout << "soSach = " << soSach << '\n';
return 0;
}
x = i++: lấy giá trị cũ củai(5) gán chox, sau đó mới tăngilên 6.y = ++j: tăngjlên 6 trước, rồi mới gán choy.
Ví dụ đời thường: i++ giống như “đưa cho bạn số thứ tự hiện tại rồi mới bấm số mới”, còn ++i là “bấm số mới rồi mới đưa”.
Các toán tử gán kết hợp như +=, *= chỉ là cách viết tắt: soSach += 5 nghĩa là soSach = soSach + 5.
Mẹo nhỏ: cố gắng không đặt ++ vào giữa biểu thức phức tạp. Viết i++; trên một dòng riêng thì không bao giờ phải nghĩ xem nó tăng trước hay sau.
Toán tử so sánh
Toán tử so sánh trả về bool: true hoặc false. Có 6 toán tử: == (bằng), != (khác), <, >, <=, >=.
Lỗi kinh điển nhất của người mới là viết = (gán) thay cho == (so sánh):
#include <iostream>
int main()
{
int soSachConLai = 3;
if (soSachConLai = 0)
std::cout << "Het sach\n";
else
std::cout << "Con " << soSachConLai << " cuon\n";
return 0;
}
Kho còn 3 cuốn mà chương trình in ra “Con 0 cuon”. Dòng if (soSachConLai = 0) không so sánh gì cả: nó gán 0 cho biến, rồi lấy giá trị 0 đó làm điều kiện, mà 0 nghĩa là false. Vừa sai điều kiện, vừa mất luôn dữ liệu.
Lưu ý: Đây là lỗi đúng cú pháp nên g++ chỉ cảnh báo suggest parentheses around assignment used as truth value, và chỉ khi bật -Wall. Thấy cảnh báo này thì gần như chắc chắn các bạn đã gõ thiếu một dấu =.
Toán tử logic
&& (và), || (hoặc), ! (phủ định) dùng để ghép nhiều điều kiện. a && b chỉ đúng khi cả 2 đúng, a || b đúng khi ít nhất một vế đúng.
Điều ít người để ý là C++ đánh giá ngắn mạch (short-circuit): với a && b, nếu a đã sai thì kết quả chắc chắn sai, nên b không được tính nữa. Thử với một hàm có in ra màn hình để thấy rõ:
#include <iostream>
bool kiemTraThe()
{
std::cout << " (dang kiem tra the thu vien...)\n";
return true;
}
int main()
{
bool conSach = false;
std::cout << "Lan 1, conSach = false:\n";
if (conSach && kiemTraThe())
std::cout << " Cho muon\n";
else
std::cout << " Khong cho muon\n";
conSach = true;
std::cout << "Lan 2, conSach = true:\n";
if (conSach && kiemTraThe())
std::cout << " Cho muon\n";
return 0;
}
Lần 1 hết sách nên hàm kiemTraThe() không hề được gọi. Tương tự, với a || b nếu a đúng thì b bị bỏ qua.
Tính chất này rất hay được dùng để viết điều kiện an toàn, ví dụ if (soNguoi != 0 && tong / soNguoi > 10). Nếu soNguoi bằng 0 thì vế chia không bao giờ chạy, nên không bị lỗi chia cho 0. Cách viết hàm như kiemTraThe() ở đây chúng ta sẽ học ở bài về hàm.
Toán tử bit
Toán tử bit làm việc trên từng bit của số nguyên. Mình dùng std::bitset để in ra dạng nhị phân cho dễ nhìn:
#include <iostream>
#include <bitset>
int main()
{
unsigned int a = 12; // 1100
unsigned int b = 10; // 1010
std::cout << "a = " << std::bitset<4>(a) << '\n';
std::cout << "b = " << std::bitset<4>(b) << '\n';
std::cout << "a & b = " << std::bitset<4>(a & b) << '\n';
std::cout << "a | b = " << std::bitset<4>(a | b) << '\n';
std::cout << "a ^ b = " << std::bitset<4>(a ^ b) << '\n';
std::cout << "a << 1 = " << std::bitset<5>(a << 1) << " (= " << (a << 1) << ")\n";
std::cout << "a >> 2 = " << std::bitset<4>(a >> 2) << " (= " << (a >> 2) << ")\n";
std::cout << "~5 (int) = " << ~5 << '\n';
return 0;
}
&(AND): bit kết quả là 1 khi cả 2 bit đều là 1.|(OR): là 1 khi ít nhất một bit là 1.^(XOR): là 1 khi 2 bit khác nhau.<<,>>: dịch các bit sang trái, sang phải. Dịch trái 1 bit tương đương nhân 2, dịch phải 1 bit tương đương chia 2.~(NOT): đảo tất cả các bit.~5ra-6vìintcó 32 bit: đảo hết thì bit dấu cũng thành 1, nên ra số âm chứ không phải1010như nhiều người nghĩ.
Phân biệt & với &&: & tính trên từng bit, còn && tính đúng sai của cả biểu thức. Viết nhầm vẫn biên dịch được nhưng ý nghĩa khác hẳn.
Ứng dụng: lưu nhiều cờ trong một biến
Ứng dụng phổ biến nhất của toán tử bit là dùng mỗi bit làm một cờ (flag) bật/tắt. Ví dụ quyền của một tài khoản thư viện:
#include <iostream>
int main()
{
constexpr unsigned int QUYEN_DOC = 1 << 0; // 001
constexpr unsigned int QUYEN_MUON = 1 << 1; // 010
constexpr unsigned int QUYEN_SUA = 1 << 2; // 100
unsigned int quyen = QUYEN_DOC | QUYEN_MUON; // ban doc thuong: doc + muon
std::cout << "Duoc muon sach? " << ((quyen & QUYEN_MUON) != 0) << '\n';
std::cout << "Duoc sua sach? " << ((quyen & QUYEN_SUA) != 0) << '\n';
quyen |= QUYEN_SUA; // cap them quyen sua
quyen &= ~QUYEN_MUON; // thu hoi quyen muon
std::cout << "Sau khi doi: muon = " << ((quyen & QUYEN_MUON) != 0)
<< ", sua = " << ((quyen & QUYEN_SUA) != 0) << '\n';
return 0;
}
3 thao tác cần nhớ: |= để bật bit, &= ~ để tắt bit, & để kiểm tra bit. Các bạn học lập trình vi điều khiển sẽ dùng đúng 3 thao tác này hằng ngày để cấu hình thanh ghi, ví dụ bật tắt từng chân GPIO.
Toán tử ba ngôi
Toán tử ba ngôi điều_kiện ? a : b trả về a nếu điều kiện đúng, ngược lại trả về b. Nó là cách viết gọn của if/else khi chỉ cần chọn 1 trong 2 giá trị:
int giamGia = (soCuon >= 3) ? tamTinh * 10 / 100 : 0;
Chỉ nên dùng cho lựa chọn đơn giản như thế này. Lồng nhiều toán tử ba ngôi vào nhau thì dùng if/else cho dễ đọc.
Độ ưu tiên của toán tử
Khi biểu thức có nhiều toán tử, C++ tính theo độ ưu tiên (precedence), giống “nhân chia trước, cộng trừ sau” trong toán. Thứ tự từ cao xuống thấp của các toán tử hay dùng:
| Thứ tự | Toán tử |
|---|---|
| 1 | ++ -- (dạng sau), () gọi hàm |
| 2 | ++ -- (dạng trước), ! ~, dấu âm - |
| 3 | * / % |
| 4 | + - |
| 5 | << >> |
| 6 | < <= > >= |
| 7 | == != |
| 8 | & |
| 9 | ^ |
| 10 | | |
| 11 | && |
| 12 | || |
| 13 | ? :, các toán tử gán = += ... |
Bảng đầy đủ các bạn xem ở trang C++ operator precedence trên cppreference. Không cần thuộc, nhưng phải nhớ một chỗ hay gây lỗi: toán tử bit & có độ ưu tiên thấp hơn ==.
#include <iostream>
int main()
{
int maSach = 6;
if (maSach & 1 == 0)
std::cout << "Ma sach chan\n";
else
std::cout << "Ma sach le\n";
return 0;
}
Mã sách 6 là số chẵn mà chương trình in ra “le”. Vì == được tính trước, biểu thức thực chất là maSach & (1 == 0), tức là 6 & 0 bằng 0. Muốn đúng phải viết (maSach & 1) == 0.
Lưu ý: Khi trộn toán tử bit với toán tử so sánh, luôn thêm ngoặc tròn. Ngoặc thừa không làm chương trình chậm đi chút nào, nhưng thiếu ngoặc thì sai âm thầm như ví dụ trên. g++ có cảnh báo -Wparentheses cho trường hợp này, thêm một lý do để luôn bật -Wall.
Áp dụng vào chương trình thư viện
Thư viện bán thêm sách: mỗi cuốn 125.000 đồng, mua từ 3 cuốn trở lên được giảm 10%. Chương trình dùng toán tử số học, so sánh và toán tử ba ngôi:
#include <iostream>
int main()
{
constexpr int GIA_MOT_CUON = 125000;
int soCuon;
std::cout << "So cuon muon mua: ";
std::cin >> soCuon;
int tamTinh = soCuon * GIA_MOT_CUON;
int giamGia = (soCuon >= 3) ? tamTinh * 10 / 100 : 0; // mua tu 3 cuon giam 10%
std::cout << "Tam tinh: " << tamTinh << " dong\n";
std::cout << "Giam gia: " << giamGia << " dong\n";
std::cout << "Phai tra: " << tamTinh - giamGia << " dong\n";
return 0;
}
Để ý mình viết tamTinh * 10 / 100 chứ không phải tamTinh * 0.1. Tiền lưu bằng số nguyên, nên nhân trước rồi chia sau để kết quả vẫn là số nguyên và không bị sai số số thực.
Bài tập
- Nhập một số giây, in ra dạng giờ:phút:giây. Ví dụ 3725 giây là 1:2:5. Gợi ý: dùng
/và%. - Nhập một số nguyên, in ra nó chẵn hay lẻ bằng 2 cách: dùng
%và dùng toán tử bit&. Thử với cả số âm. - Nhập năm, kiểm tra có phải năm nhuận không: chia hết cho 4 nhưng không chia hết cho 100, hoặc chia hết cho 400. Viết điều kiện trên một dòng bằng
&&và||. - Trong ví dụ quyền thư viện, thêm quyền
QUYEN_XOAvà viết lệnh cấp quyền đó cho tài khoản.
Lời giải sẽ có trong bài tổng hợp bài tập C++ cơ bản nhé.
Kết
Toán tử trong C++ phần lớn giống C và giống toán học, nên chỗ đáng nhớ là các bẫy: chia số nguyên, chia lấy dư số âm, = nhầm với ==, và & có độ ưu tiên thấp hơn ==. Cả 4 lỗi đều biên dịch được, nên thói quen bật -Wall -Wextra và thêm ngoặc khi không chắc sẽ cứu các bạn rất nhiều lần.
Các bạn đã học toán tử trong lập trình C sẽ thấy gần như y hệt. Bài trước là hằng và biến C++, bài tiếp theo chúng ta sẽ dùng toán tử so sánh và logic để viết lệnh rẽ nhánh if, else, switch case. Thấy bài viết hữu ích thì chia sẻ cho bạn bè cùng học nhé.









