انتقل إلى المحتوى

Operators, Expressions & Conversions

أسبقية المشغل الرئيسية، والتحويلات الحسابية، ومخاطر الخلط الموقع/غير الموقع، والقوالب الأربعة المسماة C++.

التعبيرات: اللبنات الأساسية للحساب

التعبير في 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. ترويج الأعداد الصحيحة: تتم ترقية الأنواع الأصغر من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 السالب الذي تم تحويله إلى unsigned عددًا كبيرًا جدًا (على سبيل المثال، -1 يصبح 4294967295 لـ 32 بت)
  • تؤدي مقارنة القيم الموقعة وغير الموقعة إلى تحويل القيمة الموقعة إلى غير موقعة أولاً
  • تُرجع الدالة Vector.size()‎ size_t (غير موقعة) — وتؤدي المقارنة مع قيمة int السالبة إلى نتائج خاطئة
  • استخدم -Wsign-compare للتعرف على مشكلات المقارنة الموقعة/غير الموقعة في وقت الترجمة

C++ الممثلين المُسمىين مقابل C-Style Casts

يوفر 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 — فقط للتفاعل مع واجهات برمجة التطبيقات القديمة غير الصحيحة. لا تستخدمه أبدًا لتعديل كائن ثابت حقًا.
- reinterpret_cast — لإعادة تفسير البتات ذات المستوى المنخفض. يشير دائمًا تقريبًا إلى التعليمات البرمجية الخاصة بالنظام الأساسي. نادرا ما تكون هناك حاجة إليها في رمز التطبيق.

لا تستخدم أبدًا قوالب النمط C(Type)exprفي C++ - فهي تخفي التحويل الذي يتم تنفيذه وتتجاوز فحوصات السلامة.

  • استخدم static_cast للتحويلات الرقمية والبث الآمن - يتم التحقق منه في وقت الترجمة
  • استخدم Dynamic_cast لعمليات البث الهابطة الآمنة في التسلسلات الهرمية متعددة الأشكال - يتم التحقق منها في وقت التشغيل
  • استخدم const_cast فقط للتفاعل مع واجهات برمجة التطبيقات القديمة غير الصحيحة
  • لا تستخدم قوالب النمط C أبدًا في C++ - فهي تخفي التحويل الذي تم تطبيقه
النقاط الرئيسية
  • أسبقية عامل التشغيل تحدد التجميع - عندما تكون في شك، استخدم الأقواس لتوضيح النية
  • تقييم ماس كهربائى في && و || يضمن الترتيب من اليسار إلى اليمين ويتخطى التقييم غير الضروري
  • يقوم الترويج الصحيح بتحويل الأنواع الصغيرة (char، short، bool) إلى int قبل الحساب
  • يؤدي خلط الأنواع الموقعة وغير الموقعة إلى تحويل القيمة الموقعة إلى غير موقعة - حيث يصبح العدد السالب رقمًا موجبًا كبيرًا
  • استخدم القوالب الأربعة المسماة (static_cast، وdynamic_cast، وconst_cast، وreinterpret_cast) بدلاً من القوالب ذات النمط C
  • تعد القوالب ذات النمط C خطيرة لأنها تحاول بصمت تجربة أنواع متعددة من القوالب دون إخبارك بالنوع الذي تم تطبيقه

اختبار — اختبر معرفتك

(15 نقطة خبرة)

1. ما الذي يتم تقييمه بالفعل للتعبير`x & 1 == 0`؟

2. ماذا يحدث عند مقارنة`-1 < 0u`(حيث يكون`0u`غير موقع)؟

3. ما هي مجموعة C++ التي يجب أن تستخدمها للبث الآمن في تسلسل هرمي متعدد الأشكال؟