تتشابه المراجع في سلوكها وتختلف عن المؤشّرات الثابتة (const pointers)، وتُعرَّف عن طريق إتباع الرمز &
(ampersand) باسم نوع.
int i = 10; int &refi = i;
يمثّل refi
في المثال أعلاه مرجعًا مربوطًا (reference bound) إلى i
، كذلك فإن المراجع تُجرِّد مفهوم المؤشرات وتتصرف كاسم للكائن المُشار إليه:
refi = 20; // i = 20;
يمكنك أيضًا تعريف عدة مراجع في تعريف واحد:
int i = 10, j = 20; int &refi = i, &refj = j; // خطأ شائع // int& refi = i, k = j;
في المثال السابق ستكون refi
من نوع &int
رغم أن k
ستكون من نوع int
وليس &int
. أيضًا، يجب تهيئة المراجع بشكل صحيح في التعريف، ولن يمكنك تعديلها بعد ذلك. انظر المثال التالي حيث تؤدي الشيفرة إلى خطأ في التصريف لأن الإعلان عن متغير المرجع i
يتطلب مهيِّئًا.
int &i;
كذلك لا يمكن ربط مرجع إلى المؤشر الفارغ nullptr
مباشرة، على عكس المؤشرات، انظر المثال التالي حيث نحصل على خطأ لاستحالة ربط مرجع غير ثابت لقيمة يسارية من نوع int
بعنصر من نوع nullptr_t
:
int *const ptri = nullptr; int &refi = nullptr;
الدلالات القيميّة والمرجعية
يكون لنوع ما دلالة قيميّة (value semantics) إذا كانت حالة الكائن القابلة للمُلاحظة مختلفة وظيفيًا عن جميع الكائنات الأخرى من ذلك النوع. هذا يعني أنه إذا نسخت أحد الكائنات فستحصل على كائن جديد، ولن تؤثر التعديلات على الكائن الجديد بأي شكل من الأشكال على الكائن القديم. معظم أنواع C++ الأساسية لها دلالات قيميّة، انظر المثال التالي حيث تطبع std::cout << i
الرقم 5 لأن i
لم تتأثر بالتغييرات التي أجريت على j
.
int i = 5; int j = i; // منسوخ j += 20; std::cout << i; // j لم تتأثر بالتغييرات التي أجريناها على i تطبع 5، لأنّ
معظم الأنواع المُعرّفة في المكتبة القياسية لها دلالة قيميّة أيضًا:
std::vector<int> v1(5, 12); // مصفوفة خماسية كل قيمها تساوي 12 std::vector<int> v2 = v1; // نسخ المتجهة v2[3] = 6; v2[4] = 9; std::cout << v1[3] << " " << v1[4]; // "12 12"
يقال إنّ نوعًا ما له دلالة مرجعيّة (reference semantics) إذا تشاركت نُسخ ذلك النوع حالتها القابلة للملاحظة مع كائنات أخرى (خارجية)، بحيث يؤدي تعديل كائن واحد إلى تغيير حالة كائن آخر، وللمؤشّرات دلالة قيميّة في C++ فيما يتعلّق بهويّة الكائن الذي تشير إليه، ولها دلالة مرجعيّة فيما يتعلق بحالة الكائن الذي تشير إليه:
int *pi = new int(4); int *pi2 = pi; pi = new int(16); assert(pi2 != pi); // تتحقق دائما int *pj = pi; *pj += 5; std::cout << *pi; // يشيران إلى نفس العنصر pj و pi تطبع 9 لأن
كذلك فإن مراجع C++ لها دلالة مرجعية.
النسخ العميق ودعم النقل
إذا كنت تريد أن تجعل لنوع ما دلالة قيميّة وكان ذلك النوع يحتاج إلى تخزين الكائنات الديناميكية (dynamically allocated)، فسيحتاج ذلك النوع عند عمليات النسخ إلى تخصيص (allocate) نسخ جديدة من تلك الكائنات، كما يجب أن يفعل ذلك أيضًا عند تعيين النسخة (copy assignment). يُسمّى هذا النوع من النسخ "النسخ العميق" (deep copy) إذ أنّه يحوّل الدلالة المرجعية إلى دلالة قيمية:
struct Inner { int i; }; const int NUM_INNER = 5; class Value { private: Inner *array_; // في العادة يكون لها دلالة مرجعية public: Value(): array_(new Inner[NUM_INNER]) {}~Value() { delete[] array_; } Value(const Value &val): array_(new Inner[NUM_INNER]) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; } Value &operator = (const Value &val) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; return *this; } };
الإصدار ≥ C++ 11
تسمح الدلالة النقليّة (Move semantics) لنوع مثل الصنف Value
أن يتجنب نسخ بياناته المرجعية (referenced data)، وإذا استعمل المُستخدم القيمة بطريقة تؤدي إلى نقلٍ (move) فيمكن أن يؤدّي ذلك إلى تفريغ الكائن المنسوخ من البيانات التي أشار إليها:
struct Inner { int i; }; constexpr auto NUM_INNER = 5; class Value { private: Inner *array_; // في العادة يكون لها دلالة مرجعية public: Value(): array_(new Inner[NUM_INNER]) {} // nullptr يُسمح بالنقل حتى لو أعادت ~Value() { delete[] array_; } Value(const Value &val): array_(new Inner[NUM_INNER]) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; } Value &operator = (const Value &val) { for (int i = 0; i < NUM_INNER; ++i) array_[i] = val.array_[i]; return *this; } // النقل يعني لا تخصيص للذاكرة // لا يمكن إطلاق اعتراض Value(Value &&val) noexcept: array_(val.array_) { // لقد أخذنا القيمة القديمة val.array_ = nullptr; } // لا يمكن رفع اعتراضات Value &operator = (Value &&val) noexcept { // ستُدمّر قريبا val حيلة ذكية، لأن // لقد استبدلنا بياناتنا ببياناته، وسيدمّر مدمّره بياناتنا std::swap(array_, val.array_); } };
لا شك أننا نستطيع جعل مثل هذا النوع غير قابل للنسخ إن أردنا منع النسخ العميق مع السماح بنقل الكائن، انظر:
struct Inner { int i; }; constexpr auto NUM_INNER = 5; class Value { private: Inner *array_; // في العادة يكون لها دلالة مرجعية public: Value(): array_(new Inner[NUM_INNER]) {} // nullptr يُسمح بالنقل حتى لو أعادت ~Value() { delete[] array_; } Value(const Value &val) = delete; Value &operator = (const Value &val) = delete; // النقل يعني لا تخصيص للذاكرة // لا يمكن إطلاق اعتراض Value(Value &&val) noexcept: array_(val.array_) { // لقد أخذنا القيمة القديمة val.array_ = nullptr; } // لا يمكن رفع اعتراضات Value &operator = (Value &&val) noexcept { // ستُدمّر قريبا val حيلة ذكية، لأن // لقد استبدلنا بياناتنا ببياناته، مدمّره سيدمّر بياناتنا std::swap(array_, val.array_); } };
نستطيع أن نطبق قاعدة الصفر (Rule of Zero) من خلال استخدام مؤشّر فريد unique_ptr
:
struct Inner { int i; }; constexpr auto NUM_INNER = 5; class Value { private: unique_ptr<Inner []>array_; // نوع للنقل فقط public: Value(): array_(new Inner[NUM_INNER]) {} // لا داعي للحذف الصريح، أو حتى التصريح ~Value() = default; { delete[] array_; } // لا داعي للحذف الصريح، أو حتى التصريح Value(const Value &val) = default; Value &operator = (const Value &val) = default; // تنفيذ النقل عنصرا بعنصر Value(Value &&val) noexcept = default; // تنفيذ النقل عنصرا بعنصر Value &operator = (Value &&val) noexcept = default; };
هذا الدرس جزء من سلسلة دروس عن C++.
ترجمة -بتصرّف- للفصل Chapter 26: References والفصل Chapter 27: Value and Reference Semantics من كتاب C++ Notes for Professionals
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.