Toán tử, biểu thức và chuyển đổi
Ưu tiên của toán tử chính, chuyển đổi số học, sự nguy hiểm của việc trộn có dấu/không dấu và bốn phiên bản C++ được đặt tên.
Biểu thức: Các khối xây dựng của tính toán
biểu thức trong C++ là một chuỗi các toán tử và toán hạng tính toán một giá trị. Ngay cả một câu lệnh đơn giản nhưx = a + b;cũng chứa nhiều biểu thức phụ:a,b,a + bvàx = a + b(bản thân phép gán là một biểu thức trả về giá trị được gán).
Mọi biểu thức đều có hai thuộc tính: một loại và một danh mục giá trị (lvalue, rvalue, v.v.). Hiểu các thuộc tính này là điều cần thiết vì C++ áp dụng chuyển đổi ngầm giữa các loại và quy tắc cho những chuyển đổi này có thể tạo ra kết quả đáng ngạc nhiên nếu bạn không cẩn thận.
Toán tử độ ưu tiên xác định cách nhóm các toán tử (liên kết chặt chẽ hơn), trong khi tính kết hợp xác định hướng nhóm cho các toán tử ở cùng mức độ ưu tiên. Việc mắc phải những lỗi này sẽ dẫn đến các lỗi tinh vi được biên dịch mà không có cảnh báo.
Sự ngạc nhiên về độ ưu tiên của toán tử
Ưu tiên của người vận hành là một trong những nguồn lỗi phổ biến nhất. Khi nghi ngờ, hãy sử dụng dấu ngoặc đơn để thể hiện rõ ý định của bạn:
#include <iostream>
int main() {
// Precedence trap #1: bitwise vs comparison
int x = 5;
// This does NOT check if bit 1 is set:
if (x & 1 == 0) { // Parsed as: x & (1 == 0) → x & 0 → 0
std::cout << "Never reached!\n";
}
// Correct:
if ((x & 1) == 0) { // Parentheses fix the grouping
std::cout << "Even\n";
}
// Precedence trap #2: ternary vs assignment
int a = 1, b = 2;
int result = a > b ? a : b = 10; // Parsed as: (a > b ? a : b) = 10 — ERROR!
// Correct:
int correct = (a > b) ? a : b; // Parenthesize for clarity
// Precedence trap #3: pointer dereference vs member access
// (*ptr).member vs ptr->member — both are equivalent
// *ptr.member is parsed as *(ptr.member) — usually a compile error
// Short-circuit evaluation
int* ptr = nullptr;
// Safe: the second operand is NOT evaluated if the first is false
if (ptr != nullptr && *ptr > 0) {
std::cout << "Value: " << *ptr << '\n';
}
// The && and || operators guarantee left-to-right evaluation and short-circuit
// Overloaded && and || do NOT short-circuit — avoid overloading them
}Nhấn Chạy để biên dịch và chạy ví dụ này.
Chuyển đổi số học & thăng cấp số nguyên
Khi bạn kết hợp các loại trong một biểu thức, C++ sẽ áp dụng chuyển đổi ngầm theo một bộ quy tắc được gọi là chuyển đổi số học thông thường:
1. Khuyến mãi số nguyên: Các loại nhỏ hơnint(char,short,bool) trước tiên được thăng hạng lênint(hoặcunsigned intnếu giá trị không thể vừa vớiint). Điều này có nghĩa làchar + chartạo raintchứ không phảichar.
2. Cân bằng: Nếu các toán hạng vẫn khác nhau về loại sau khi thăng hạng, thì loại "nhỏ hơn" sẽ được chuyển đổi thành loại "lớn hơn" theo phân cấp:int→unsigned int→long→unsigned long→long long→unsigned long long→float→double→long double.
3. Tương tác có dấu/không dấu: Khi loại đã ký gặp loại không dấu có cùng thứ hạng, giá trị đã ký sẽ được chuyển thành không dấu. Đây là nơi xảy ra những chuyển đổi tiềm ẩn nguy hiểm nhất.
Trộn các số nguyên có dấu và không dấu là một trong những cạm bẫy phổ biến và nguy hiểm nhất trong C++. Khi một giá trị có dấu âm được chuyển đổi hoàn toàn thành không dấu, nó sẽ bao bọc thành một số dương rất lớn (vì các loại không dấu sử dụng số học mô-đun).
Điều này thường xảy ra khi so sánh các giá trịintvớisize_t(làunsigned) - ví dụ: lặp quastd::vectorvới bộ đếm vòng lặpintvà so sánh nó vớivector.size().
Biên dịch bằng-Wsign-compare(có trong-Wall) để phát hiện những vấn đề này.
- Một int âm được chuyển đổi thành unsigned sẽ trở thành một số rất lớn (ví dụ: -1 trở thành 4294967295 cho 32-bit)
- So sánh các giá trị đã ký và không dấu sẽ chuyển đổi giá trị đã ký thành giá trị không dấu trước tiên
- vector.size() trả về size_t (không dấu) — so sánh với int âm sẽ cho kết quả sai
- Sử dụng -Wsign-compare để nắm bắt các vấn đề so sánh đã ký/không dấu tại thời điểm biên dịch
C++ Diễn viên được đặt tên so với Diễn viên kiểu C
C++ cung cấp bốn toán tử truyền được đặt tên để thay thế(Type)exprtruyền kiểu C nguy hiểm. Mỗi loại có một mục đích cụ thể và mức độ an toàn khác nhau:
#include <iostream>
#include <cstdint>
class Base {
public:
virtual ~Base() = default;
virtual void speak() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void speak() override { std::cout << "Derived\n"; }
void only_in_derived() { std::cout << "Special!\n"; }
};
int main() {
// static_cast — compile-time checked, for "natural" conversions
double pi = 3.14159;
int truncated = static_cast<int>(pi); // Explicit: you acknowledge the truncation
std::cout << truncated << '\n'; // 3
// dynamic_cast — runtime-checked, for safe downcasting in class hierarchies
Base* base_ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr);
if (derived_ptr) { // Check for success!
derived_ptr->only_in_derived(); // Safe
}
// const_cast — adds or removes const (use sparingly!)
const int ci = 42;
// int& mutable_ref = const_cast<int&>(ci); // Legal but modifying ci is UB!
// Only valid use: calling a non-const API with a const object you know won't be modified
// reinterpret_cast — reinterprets the bit pattern (dangerous!)
std::int64_t address = reinterpret_cast<std::int64_t>(base_ptr);
std::cout << "Address: " << address << '\n';
// C-style cast: (int)pi — NEVER use this in C++
// It tries static_cast, then const_cast, then reinterpret_cast —
// you don't know which one was applied!
delete base_ptr;
}Nhấn Chạy để biên dịch và chạy ví dụ này.
Sử dụng các phôi được đặt tên để truyền đạt ý định và kiểm tra trình biên dịch thích hợp:
- static_cast — Dành cho các chuyển đổi được xác định rõ: chuyển đổi loại số, cập nhật trong hệ thống phân cấp lớp vàvoid*thành con trỏ đã nhập. Đây là dàn diễn viên bạn sẽ sử dụng thường xuyên nhất.
- dynamic_cast — Để truyền phát an toàn trong hệ thống phân cấp đa hình. Yêu cầu ít nhất một hàm ảo trong lớp cơ sở. Trả vềnullptr(đối với con trỏ) hoặc gửistd::bad_cast(đối với tham chiếu) khi không thành công. Có chi phí thời gian chạy.
- const_cast — Chỉ để giao tiếp với các API cũ không đúng hằng số. Không bao giờ sử dụng nó để sửa đổi một đối tượng const thực sự.
- reinterpret_cast — Để diễn giải lại bit ở mức độ thấp. Hầu như luôn chỉ ra mã dành riêng cho nền tảng. Hiếm khi cần thiết trong mã ứng dụng.
Không bao giờ sử dụng kiểu C(Type)exprtrong C++ — chúng ẩn chuyển đổi nào đang được thực hiện và bỏ qua các bước kiểm tra an toàn.
- Sử dụng static_cast để chuyển đổi số và phát sóng an toàn - nó được kiểm tra tại thời điểm biên dịch
- Sử dụng Dynamic_cast để phát sóng an toàn trong hệ thống phân cấp đa hình - nó được kiểm tra khi chạy
- Chỉ sử dụng const_cast để giao tiếp với các API kế thừa không chính xác
- Không bao giờ sử dụng kiểu C trong C++ — chúng ẩn chuyển đổi nào được áp dụng
- Mức độ ưu tiên của toán tử xác định việc nhóm - khi nghi ngờ, hãy sử dụng dấu ngoặc đơn để thể hiện ý định rõ ràng
- Đánh giá ngắn mạch trong && và || đảm bảo thứ tự từ trái sang phải và bỏ qua những đánh giá không cần thiết
- Khuyến mãi số nguyên chuyển đổi các loại nhỏ (char, short, bool) thành int trước số học
- Trộn các loại có dấu và không dấu sẽ chuyển đổi giá trị có dấu thành không dấu - một int âm trở thành một số dương rất lớn
- Sử dụng bốn dàn diễn viên được đặt tên (static_cast, Dynamic_cast, const_cast, reinterpret_cast) thay vì dàn diễn viên kiểu C
- Phép thuật kiểu C rất nguy hiểm vì chúng âm thầm thử nhiều kiểu bó bột mà không cho bạn biết loại nào được áp dụng
Câu đố — Kiểm tra kiến thức
(15 XP)1. Biểu thức`x & 1 == 0`thực sự đánh giá là gì?
2. Điều gì xảy ra khi bạn so sánh`-1 < 0u`(trong đó`0u`không được ký)?
3. Bạn nên sử dụng loại C++ nào để truyền xuống an toàn trong hệ thống phân cấp lớp đa hình?