رفتن به محتوا

عملگرها، عبارات و تبدیلات

بر تقدم عملگرها، تبدیلات حسابی، خطرات ترکیب علامت‌دار/بدون‌علامت، و چهار تبدیل نام‌دار ‎C++‎ مسلط شوید.

عبارات: بلوک‌های سازنده محاسبات

یک عبارت (Expression) در ‎C++‎ دنباله‌ای از عملگرها و عملوندها است که یک مقدار محاسبه می‌کند. حتی یک دستور ساده مانند x = a + b; شامل چندین زیرعبارت است: a، b، a + b، و x = a + b (خود انتساب عبارتی است که مقدار انتساب‌یافته را برمی‌گرداند).

هر عبارت دو ویژگی دارد: یک نوع و یک دسته‌بندی مقدار (lvalue، rvalue و غیره). درک این ویژگی‌ها ضروری است زیرا ‎C++‎ تبدیلات ضمنی بین انواع اعمال می‌کند، و قواعد این تبدیلات در صورت بی‌دقتی می‌توانند نتایج شگفت‌آوری تولید کنند.

تقدم عملگر تعیین می‌کند عملگرها چگونه گروه‌بندی شوند (کدام محکم‌تر می‌بندد)، در حالی که شرکت‌پذیری جهت گروه‌بندی را برای عملگرهای هم‌سطح تعیین می‌کند. اشتباه گرفتن این‌ها منجر به باگ‌های ظریفی می‌شود که بدون هشدار کامپایل می‌شوند.

شگفتی‌های تقدم عملگرها

تقدم عملگرها یکی از رایج‌ترین منابع باگ است. وقتی شک دارید، از پرانتز برای صریح کردن نیت‌تان استفاده کنید:

precedence.cppc++23
#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
}
خروجی

برای کامپایل و اجرای این نمونه، روی اجرا بزنید.

تبدیلات حسابی و ارتقاء عدد صحیح

وقتی انواع مختلف را در یک عبارت ترکیب می‌کنید، ‎C++‎ تبدیلات ضمنی را طبق مجموعه‌ای از قواعد به نام تبدیلات حسابی معمول اعمال می‌کند:

1. ارتقاء عدد صحیح (Integer Promotion): انواع کوچک‌تر از int مانند char، short و bool ابتدا به int ارتقا می‌یابند (یا unsigned int اگر مقدار در int نگنجد). این بدان معناست که char + char یک int تولید می‌کند، نه یک char.

2. متوازن‌سازی: اگر عملوندها پس از ارتقا همچنان نوع متفاوتی داشته باشند، نوع «کوچک‌تر» به نوع «بزرگ‌تر» تبدیل می‌شود طبق سلسله‌مراتب: int → unsigned int → long → unsigned long → long long → unsigned long long → float → double → long double.

3. تعامل علامت‌دار/بدون‌علامت: وقتی یک نوع علامت‌دار با نوع بدون‌علامت هم‌رتبه مواجه می‌شود، مقدار علامت‌دار به بدون‌علامت تبدیل می‌شود. اینجاست که خطرناک‌ترین تبدیلات ضمنی رخ می‌دهند.

دام

ترکیب اعداد صحیح علامت‌دار و بدون‌علامت یکی از رایج‌ترین و خطرناک‌ترین دام‌ها در ‎C++‎ است. وقتی یک مقدار علامت‌دار منفی به صورت ضمنی به بدون‌علامت تبدیل می‌شود، به عدد مثبت بسیار بزرگی تبدیل می‌شود (چون انواع بدون‌علامت از حساب پیمانه‌ای استفاده می‌کنند).

این اغلب هنگام مقایسه مقادیر int با size_t (که unsigned است) رخ می‌دهد — مثلاً پیمایش یک std::vector با شمارنده حلقه int و مقایسه آن با vector.size().

با -Wsign-compare (که در -Wall گنجانده شده) کامپایل کنید تا این مشکلات را شناسایی کنید.

  • یک int منفی تبدیل‌شده به بدون‌علامت به عدد بسیار بزرگی تبدیل می‌شود (مثلاً -1 برای 32 بیتی به 4294967295 تبدیل می‌شود)
  • مقایسه مقادیر علامت‌دار و بدون‌علامت ابتدا مقدار علامت‌دار را به بدون‌علامت تبدیل می‌کند
  • vector.size() مقدار size_t (بدون‌علامت) برمی‌گرداند — مقایسه با int منفی نتایج نادرست می‌دهد
  • از -Wsign-compare برای شناسایی مشکلات مقایسه علامت‌دار/بدون‌علامت در زمان کامپایل استفاده کنید

تبدیل‌های نام‌دار ‎C++‎ در برابر تبدیل‌های سبک C

‎C++‎ چهار عملگر تبدیل نام‌دار ارائه می‌دهد که جایگزین تبدیل خطرناک سبک C یعنی (Type)expr می‌شوند. هر کدام هدف خاصی دارد و سطوح ایمنی متفاوتی ارائه می‌دهد:

casts.cppc++23
#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;
}
خروجی

برای کامپایل و اجرای این نمونه، روی اجرا بزنید.

بهترین شیوه

از تبدیل‌های نام‌دار برای بیان نیت و دریافت بررسی مناسب کامپایلر استفاده کنید:

- static_cast — برای تبدیل‌های کاملاً تعریف‌شده: تبدیل انواع عددی، تبدیل بالا در سلسله‌مراتب کلاس، و تبدیل void* به اشاره‌گر با نوع. این تبدیلی است که بیشتر استفاده می‌کنید.
- dynamic_cast — برای تبدیل پایین ایمن در سلسله‌مراتب‌های چندریختی. نیاز به حداقل یک تابع مجازی در کلاس پایه دارد. در صورت شکست nullptr (برای اشاره‌گرها) یا std::bad_cast (برای ارجاعات) برمی‌گرداند. هزینه زمان اجرا دارد.
- const_cast — فقط برای رابط با APIهای قدیمی که const-correct نیستند. هرگز از آن برای تغییر شیء واقعاً const استفاده نکنید.
- reinterpret_cast — برای تفسیر مجدد بیتی سطح پایین. تقریباً همیشه نشان‌دهنده کد وابسته به پلتفرم است. به ندرت در کد برنامه نیاز است.

هرگز از تبدیل‌های سبک C یعنی (Type)expr در ‎C++‎ استفاده نکنید — آن‌ها پنهان می‌کنند کدام تبدیل انجام می‌شود و بررسی‌های ایمنی را دور می‌زنند.

  • از static_cast برای تبدیلات عددی و تبدیل‌های بالای ایمن استفاده کنید — در زمان کامپایل بررسی می‌شود
  • از dynamic_cast برای تبدیل‌های پایین ایمن در سلسله‌مراتب‌های چندریختی استفاده کنید — در زمان اجرا بررسی می‌شود
  • از const_cast فقط برای رابط با APIهای قدیمی غیر const-correct استفاده کنید
  • هرگز از تبدیل‌های سبک C در ‎C++‎ استفاده نکنید — آن‌ها پنهان می‌کنند کدام تبدیل اعمال می‌شود
نکات کلیدی
  • تقدم عملگر گروه‌بندی را تعیین می‌کند — وقتی شک دارید، از پرانتز برای صریح کردن نیت استفاده کنید
  • ارزیابی اتصال کوتاه در && و || ترتیب چپ به راست را تضمین و ارزیابی غیرضروری را رد می‌کند
  • ارتقاء عدد صحیح انواع کوچک (char، short، bool) را قبل از عملیات حسابی به int تبدیل می‌کند
  • ترکیب انواع علامت‌دار و بدون‌علامت مقدار علامت‌دار را به بدون‌علامت تبدیل می‌کند — int منفی به عدد مثبت بزرگی تبدیل می‌شود
  • از چهار تبدیل نام‌دار (static_cast، dynamic_cast، const_cast، reinterpret_cast) به جای تبدیل‌های سبک C استفاده کنید
  • تبدیل‌های سبک C خطرناک هستند زیرا بی‌صدا چندین نوع تبدیل را امتحان می‌کنند بدون اینکه بگویند کدام اعمال شده

آزمون — دانش خود را بسنجید

(15 امتیاز)

1. عبارت `x & 1 == 0` واقعاً چگونه ارزیابی می‌شود؟

2. وقتی `-1 < 0u` مقایسه می‌کنید (که `0u` بدون‌علامت است) چه اتفاقی می‌افتد؟

3. برای تبدیل پایین ایمن در سلسله‌مراتب کلاس چندریختی از کدام تبدیل ‎C++‎ استفاده کنید؟