اذهب إلى المحتوى

المؤشرات (Pointers) في Cpp


محمد بغات

المؤشّر هو عنوان يشير إلى موقع في الذاكرة، وتُستخدم المؤشرات عادة للسماح للدوالّ أو هياكل البيانات بالحصول على معلومات عن الذاكرة وتعديلها دون الحاجة إلى نسخ الذاكرة المشار إليها، والمؤشّرات قابلة للاستخدام سواءً مع الأنواع الأوليّة (المٌضمّنة) أو الأنواع التي يعرّفها المستخدم.

وتستفيد المؤشّرات من عامل التحصيل ‎*‎ و العنونة ‎&‎ و السهم ‎->‎، إذ يُستخدم عاملا * و ‎‎->‎ للوصول إلى الذاكرة المشار إليها، فيما يُستخدم المعامل ‎&‎ للحصول على عنوانٍ في الذاكرة.

عمليات المؤشرات

يوجد مُعاملان يختصان بالمؤشّرات، وهما: معامل العنونة (Address-of operator) وهو & الذي يعيد عنوان عاملِهِ في الذاكرة، ومعامل التحصيل (Dereference) وهو * الذي يعيد قيمة المتغير الموجود في العنوان المحدّد بواسطة عامله.

int var = 20;
int *ptr;
ptr = &var;

cout << var << endl;
// 20 (قيمة المتغير)

cout << ptr << endl;
// 0x234f119 (موقع المتغير في الذاكرة)

cout << *ptr << endl;
// 20(ptr قيمة المتغير المخزن في مؤشر)

يُستخدم رمز النجمة * للتصريح عن مؤشّر لمجرد التوضيح بأنه مؤشر، ولا ينبغي أن تخلط بينه وبين عامل التحصيل (dereference operator) الذي يُستخدم للحصول على القيمة الموجودة في عنوان محدّد، إذ هما شيئان مختلفان مُمثّلان بنفس الرمز.

أساسيات المؤشرات

الإصدار ++‎>

ملاحظة: في كل ما يلي، سنفترض وجود الثابت C++‎ 11) ‎nullptr). بالنسبة للإصدارات السابقة، بدّل ‎nullptr‎ مكان ‎NULL‎، وهو الثابت الذي كان يؤدي وظيفة مشابهة.

إنشاء متغير المؤشر

يمكن إنشاء متغيّرات المؤشّرات باستخدام صيغة ‎*‎، على سبيل المثال: ‎int *pointer_to_int;‎. تحتوي المتغيرات من نوع المؤشّرات (‎int *‎ مثلا) على عنوان ذاكرة يكون موقعًا تُخزَّن فيه بيانات النوع الأساسي (كـ ‎int *‎ مرة أخرى). ويتضح الفرق عند مقارنة حجم المتغير مع حجم المؤشّر الذي يشير إلى نفس النوع. انظر المثال التالي الذي نصرح فيه عن بُنية من نوع big_struct تحتوي على ثلاثة أعداد long long int:

typedef struct {
    long long int foo1;
    long long int foo2;
    long long int foo3;
}
big_struct;

والآن ننشئ متغير bar من نوع big_struct، ثم ننشئ المتغير p_bar من نوع pointer to big_struce، ونهيئه إلى المؤشر الفارغ nullptr:

big_struct bar;
big_struct * p_bar0 = nullptr;
// `bar` يطبع حجم
std::cout << "sizeof(bar) = " << sizeof(bar) << std::endl;
// `p_bar` يطبع حجم
std::cout << "sizeof(p_bar0) = " << sizeof(p_bar0) << std::endl;
/* الناتج
sizeof(bar) = 24
sizeof(p_bar0) = 8
*/

أخذ عنوان متغير آخر

يمكن إسناد قيمة مؤشّر إلى مؤشر آخر مثل المتغيرات العادية بالضبط، إلا أنّه في حال المؤشّرات فإنّ عنوان الذاكرة هو الذي يُنسخ من مؤشّر إلى آخر وليس البيانات الحقيقية التي يشير إليها المؤشّر. كذلك قد تأخذ المؤشّرات القيمة ‎nullptr‎ التي تمثّل موقعًا فارغًا في الذاكرة، وتمثل المؤشّرات التي تساوي ‎nullptr‎ موقعًا غير صالح في الذاكرة، وعليه فهي لا تشير إلى أيّ بيانات فعليّة.

نحصل على عنوان الذاكرة الخاص بمتغيّر من نوع ما عن طريق إِسباق المتغيّر بعامل العَنْوَنة ‎&‎، وتكون القيمة المعادة من ‎&‎ هي مؤشّر إلى النوع الأساسي الذي يحتوي على عنوان ذاكرة المتغير، وهي بيانات صالحة طالما لم يخرج المتغير من النطاق. انظر المثال التالي حيث ننسخ p_bar0 إلى p_bar1، ثم نأخذ عنوان bar إلى p_bar_2، وعليه فإن p_bar1 يصير فارغًا nullptr، وp_bar2 صار يساوي bar&:

big_struct *p_bar1 = p_bar0;
big_struct *p_bar2 = &bar;

لنجعل الآن p_bar0 يساوي &bar:

p_bar0 = p_bar2;

p_bar2 = nullptr;

// p_bar0 == &bar
// p_bar1 == nullptr
// p_bar2 == nullptr

ذلك يخالف سلوك المراجع (references) كما يلي:

  • لا يؤدي إسناد مؤشّر إلى آخر إلى استبدال الذاكرة التي يشير إليها المؤشِّر المُسنَد إليه؛
  • يمكن أن تكون المؤشّرات فارغة.
  • عنوان العامل ينبغي أن يكون صريحًا.

الوصول إلى محتوى المؤشّر

يتطلب الوصول إلى العنوان استخدام العامل ‎&‎، أمّا الوصول إلى المحتوى فيتطلب استخدام عامل التحصيل ‎*‎ كسابقة (prefix)، عندما يُحصَّل مؤشّر فإنّه يُصبح متغيرًا من النوع الأساسي (underlying)، بل يكون مرجعًا إليه على الحقيقة، ويمكن بعدها قراءته وتعديله إن لم يكن ثابتًا (‎const‎). انظر المثال التالي حيث يشير p_bar0 إلى bar ويطبع 5 في الخطوة 1، ثم نسند القيمة التي يشير إليها المؤشر p_bar0 إلى baz في الخطوة 2، لتحتوي الأخيرة نسخة من البيانات المشار إليها من قبل p_bar0 في الخطوة 3:

(*p_bar0).foo1 = 5;
// 1 الخطوة
std::cout << "bar.foo1 = " << bar.foo1 << std::endl;
// 2 الخطوة
big_struct baz;
baz = *p_bar0;
// 3 الخطوة
std::cout << "baz.foo1 = " << baz.foo1 << std::endl;

يُختصر العامليْن ‎*‎ و ‎.‎ بالرمز ‎->‎:

std::cout << "bar.foo1 = " << (*p_bar0).foo1 << std::endl; // 5
std::cout << "bar.foo1 = " <<  p_bar0->foo1  << std::endl; // 5

تحصيل مؤشّرات غير صالحة

يجب أن تتأكد عند تحصيل مؤشر إلى أنّه يشير إلى بيانات صالحة، إذ قد يؤدي تحصيل مؤشّر غير صالح (أو مؤشّر فارغ) إلى حدوث خرق للذاكرة (memory access violation)، أو إلى قراءة البيانات المُهمَلة (garbage data) أو كتابتها.

big_struct *never_do_this() {
    // never_do_this هذا متغير محلي، ولا يوجد خارج
    big_struct retval;
    retval.foo1 = 11;
    // retval إعادة عنوان 
    return &retval;
    // تم تدميرها، وأي شيفرة تستخدم القيمة المعادة من قبل retval
    // لها مؤشّر إلى مساحة في الذاكرة `never_do_this` 
    // تحتوي البيانات المهملة
}

في مثل هذه الحالة، يطلق المصرّفان ‎g++‎ و ‎clang++‎ التحذيرات التالية:

(Clang) warning: address of stack memory associated with local variable 'retval' returned [-
Wreturn-stack-address]
(Gcc)   warning: address of local variable retval returned [-Wreturn-local-addr]

وعليه يجب توخي الحذر عند تمرير المؤشّرات كوسائط إلى دوال، إذ أنّها قد تكون فارغة:

void naive_code(big_struct *ptr_big_struct) {
    // ... `ptr_big_struct` شيفرة لا تتحقق من صلاحية
    ptr_big_struct -> foo1 = 12;
}
// (ٍSegmentation) خطأ في التجزئة
naive_code(nullptr);

حسابيات المؤشرات

الزيادة والإنقَاص

المؤشرات قابلة للزيادة أو الإنقاص منها، وتؤدي زيادة مؤشّر إلى نقل قيمة المؤشّر إلى العنصر الذي يلي العنصر المُشار إليه حاليًا في [المصفوفة](رابط الفصل 8)، أمّا إنقاصه فينقله إلى العنصر السابق في المصفوفة. كذلك لا يجوز إجراء العمليات الحسابيّة على المؤشّرات التي تشير إلى نوع ناقص مثل ‎void‎.

char* str = new char[10]; // str = 0x010
++str; // str = 0x011  in this case sizeof(char) = 1 byte
int* arr = new int[10]; // arr = 0x00100
++arr; // arr = 0x00104 if sizeof(int) = 4 bytes
void* ptr = (void* ) new char[10];
++ptr; // نوع ناقص void 

في حال زيادة مؤشّر يشير إلى العنصر الأخير، فإنّ المؤشّر سيشير إلى العنصر الذي يلي العنصر الأخير في المصفوفة، ولا يمكن تحصيل مؤشّر كهذا لكن يمكن إنقاصه، وتؤدي زيادة مؤشّر إلى العنصر الذي يلي العنصر الأخير في المصفوفة أو إنقاص مؤشّر إلى العنصر الأول إلى سلوك غير محدّد. أيضًا، يمكن التعامل مع المؤشّرات التي تشير إلى أنواع أخرى غير المصفوفات كما لو كانت تشير إلى مصفوفات أحادية.

الجمع والطرح

يمكن إضافة قيم عددية صحيحة إلى المؤشّرات، ويكافئ ذلك زيادة قيمة المؤشّر بعدد ما خلاف 1، كما يمكن أيضًا طرح قيم عددية صحيحة من المؤشّرات أيضًا مما يؤدي إلى إنقاص قيمة المؤشّر، ويجب أن يشير المؤشر إلى نوع كامل كما هو الحال مع الزيادة والإنقَاص اللذيْن شرحناهما أعلاه.

char* str = new char[10]; // str = 0x010
str += 2; // str = 0x010 + 2 * sizeof(char) = 0x012
int* arr = new int[10]; // arr = 0x100
arr += 2; // arr = 0x100 + 2 * sizeof(int) = 0x108, assuming sizeof(int) == 4.

الفرق بين المؤشرات

يمكن حساب الفرق بين مؤشِّرين يشيران إلى نفس النوع، لكن يجب أن يكون المؤشّران داخل نفس كائن المصفوفة، وإلا قد تحدث نتائج غير متوقعة. فإذا كان لدينا مؤشّران ‎P‎ و ‎Q‎ في نفس المصفوفة، وكان ‎P‎ يشير إلى العنصر رقم ‎i‎ في المصفوفة و ‎Q‎ يشير إلى العنصر رقم ‎j‎، فإنّ ‎P -‎ ‎Q‎ سيشير إلى العنصر رقم ‎i - ‎j‎ في المصفوفة، ويكون نوع النتيجة std::ptrdiff_t من <cstddef>.

char* start = new char[10]; // str = 0x010
char* test = &start[5];
std::ptrdiff_t diff = test - start; // 5 يساوي
std::ptrdiff_t diff = start - test; // -5 يساوي

المؤشرات إلى الأعضاء

مؤشّرات إلى الدوال التابعة الساكنة

تشبه الدوال التابعة الساكنة (‎static‎) دوالَّ C/C++‎ العادية باستثناء نطاقها، كما يلي:

  • تكون موجودة داخل الصنف (‎class‎)، لذا يجب أن يُرفق اسمها باسم الصنف.
  • لديها حق الوصول للأعضاء العامّة (‎public‎) والمحميّة (‎protected‎) والخاصّة (‎private‎)، فإذا كان لديك حق الوصول إلى دالة تابع ساكن ‎static‎ وسميته بشكل صحيح (أي أرفقته باسم الكائن الذي ينتمي إليه)، فيمكنك الإشارة إلى تلك الدالّة كما لو كانت دالّة عادية:
typedef int Fn(int); 
// هي نوع دالة تأخذ عددًا صحيحًا وتعيد عددًا صحيحا Fn 
// 'Fn' من النّوع MyFn() ّلاحظ أن
int MyFn(int i) {
    return 2 * i;
}

class Class {
    public:
        // 'Fn' من النوع Static() لاحظ أن
        static int Static(int i) {
            return 3 * i;
        }
}; // الصنف

int main() {
    Fn *fn;            // Fn هو مؤشّر إلى النوع  fn 
    fn = &MyFn;        // أشِر إلى دالة ما
    fn(3);            // استدعها
    fn = &Class::Static;    // أشِر إلى دالة أخرى
    fn(4);            // استدعها
} // main()

مؤشّرات إلى الدوال التوابع

للوصول إلى دالة تابعة من صنف ما، يجب أن يكون لديك "مقبض" (handle) للنسخة المحدّدة، إمّا للنسخة نفسها أو مؤشّر أو مرجع إليها. وإذا كان لديك نسخة من صنف فيمكنك الإشارة إلى أعضائها باستخدام مؤشّر-إلى-عضو (pointer-to-member)، لاحظ أنّه يجب أن يكون المؤشّر من نفس النوع الذي تشير إليه تلك الأعضاء.

typedef int Fn(int); // هي نوع دالة تأخذ عددًا صحيحًا وتعيد عددًا صحيحًا Fn 
class Class {
    public:
        // 'Fn' من النوع A() لاحظ أن
        int A(int a) {
            return 2 * a;
        }
    // 'Fn' من النوع B() لاحظ أن
    int B(int b) {
        return 3 * b;
    }
}; // صنف
int main() {
    Class c;            // تحتاج نسخة صنف لتجرب معها
    Class * p = & c;        // تحتاج مؤشر صنف لتجرب معه

    Fn Class::*fn;        // داخل الصنف Fn هو مؤشّر إلى النوع fn 

    fn = &Class::A;        // داخل أي صنف A يشير إلى fn 
    (c.*fn)(5);            // Fn عبر c الخاصة بـ A مرر 5 إلى دالة
    fn = & Class::B;        // داخل أي صنف B يشير الآن إلى fn 
        (p ->*fn)(6);        // Fn عبر c الخاصة بـ B مرر 6 إلى دالة
} // main()

على خلاف المؤشّرات التي تشير إلى متغيرات الأعضاء كما في المثال السابق، ويجب أن يكون الجمع بين نسخة الصنف والمؤشّر-إلى-العضو باستخدام الأقواس، الأمر الذي قد يبدو غريبًا كأن *. و *<- لم تكونا كافيتين!

المؤشّرات إلى متغيرات الأعضاء

للوصول إلى عضو من ‎class‎، يجب أن يكون لديك "مقبض" للنسخة المحددة، إما النسخة نفسها أو مؤشّر أو مرجع إليها. وإذا كانت لديك نسخة من الصنف ‎class‎ فتستطيع الإشارة إلى أعضائها باستخدام مؤشّر-إلى-عضو، لاحظ أنّ المؤشّر يجب أن يكون من نفس النوع الذي يشير إليه تلك الأعضاء.

class Class {
    public:
        int x, y, z;
    char m, n, o;
}; // صنف
int x;        // (Global) متغير عام
int main() {
    Class c;        // تحتاج نسخة صنف لتجرب معها
    Class *p = &c;    // تحتاج مؤشر صنف لتجرب معه

   int *p_i;        // int مؤشّر إلى

    p_i = & x        // x المؤشّر يشير الآن إلى 
    p_i = &c.x;        // c الخاص بـ  x الآن يشير إلى العنصر

    int Class::*p_C_i;        // مؤشّر إلى عدد صحيح داخل الصنف

    p_C_i = &Class::x;    // داخل الصنف x الإشارة إلى
    int i = c.*p_C_i;        // c داخل النسخة x لإيجاد  p_c_i استخدام
    p_C_i = &Class::y;    // داخل أي صنف y يشير إلى 
    i = c.*p_C_i         // c داخل النسخة y لإيجاد  p_c_i استخدام

  p_C_i = &Class::m; // خطأ

    char Class::*p_C_c = &Class::m; // هذا أفضل
} // main()

تتطلّب صياغة المؤشّر العضوي بعض العناصر الإضافية:

  • لتعريف نوع المؤشّر، تحتاج إلى ذِكر النّوع الأساسي إضافة إلى حقيقة أنه داخل صنف: ‎int Class::*ptr;‎.
  • إذا كان لديك صنف أو مرجع وتريد استخدامه مع مؤشّر-إلى-عضو فستحتاج إلى استخدام المعامل ‎.*‎ (يشبه المعامل ‎.‎).
  • إذا كان لديك مؤشّر يشير إلى صنف وتريد استخدامه مع مؤشّر-إلى-عضو، فستحتاج إلى استخدام المعامل ‎->*‎ (يشبه المعامل ‎->‎).

مؤشّرات إلى متغيرات الأعضاء الساكنة

متغيرات الأعضاء الساكنة تشبه متغيرات C / C++‎ العادية، باستثناء نطاقها:

  • إذ تكون موجودة داخل الصنف (‎class‎)، لذلك يجب أن يُرفق اسمها مع اسم الصنف؛
  • تتمتع بحق الوصول إلى العناصر العامّة (‎public‎) والمحميّة (‎protected‎) والخاصّة (‎private‎). لذلك إن كان لديك حق الوصول إلى متغير عضو ساكن وأرفقته باسم الكائن الذي ينتمي إليه بالشكل الصحيح، فيمكنك الإشارة إلى ذلك المتغير مثل أيّ متغير عادي:
class Class {
    public:
        static int i;
}; // صنف

int Class::i = 1;    // i تعريف قيمة

int j = 2;        // متغير عام
int main() {
    int k = 3; // متغير محلي

    int *p;

    p = &k;        // k الإشارة إلى
    *p = 2;        // تعديل المؤشر
    p = &j;        // j الإشارة إلى
    *p = 3;        // تعديل المؤشر
    p = &Class::i;    // Class::i الإشارة إلى
    *p = 4;        // تعديل المؤشر
} // main()

مؤشر This

جميع الدوال التوابع غير الساكنة لها معامِل خفيّ، وهو مؤشّر يشير إلى نسخة من الصنف يُسمّى ‎this‎، ويُدرج هذا المعامل خُفية في بداية قائمة المعاملات ويُعالَج من قِبل المُصرِّف. ويمكن الوصول إلى عضو من الصنف داخل دالة تابعة عبر ‎this‎، مما يسمح للمُصرِّف باستخدام دالة تابعة غير ساكنة لكل النسخ، ويسمح لدالة تابعة ما باستدعاء الدوال التوابع الأخرى بأشكال متعددة.

struct ThisPointer {
    int i;

    ThisPointer(int ii);

    virtual void func();

    int get_i() const;
    void set_i(int ii);
};
ThisPointer::ThisPointer(int ii): i(ii) {}

يعيد المصرّف كتابتها على النحو التالي:

ThisPointer::ThisPointer(int ii): this -> i(ii) {}

يكون المنشئ مسؤولًا عن تحويل الذاكرة المخصصة إلى this، وبما أنه المسؤول عن إنشاء الكائن أيضًا فإن this لن تكون صالحة تمامًا إلى أن يتم إنشاء النسخة. نتابع المثال:

/* virtual */
void ThisPointer::func() {
    if (some_external_condition) {
        set_i(182);
    } else {
        i = 218;
    }
}
// المصرّف يعيد كتابتها على النحو التالي
/* virtual */
void ThisPointer::func(ThisPointer* this) {
    if (some_external_condition) {
        this -> set_i(182);
    } else {
        this -> i = 218;
    }
}

int ThisPointer::get_i() const {
    return i;
}
// المصرّف يعيد كتابتها كـ
int ThisPointer::get_i(const ThisPointer* this) {
    return this -> i;
}

void ThisPointer::set_i(int ii) {
    i = ii;
}
// المصرّف يعيد كتابتها كـ
void ThisPointer::set_i(ThisPointer* this, int ii) {
    this -> i = ii;
}

يمكن استخدام ‎this‎ بأمان -ضمنيًا أو بشكل صريح- داخل المنشئ للوصول إلى أيّ حقل سبقت تهيئته أو حقل في صنف أب (parent class)، لكن بالمقابل فإن الوصول -ضمنيًا أو بشكل صريح- إلى الحقول التي لم تُهيّأ بعد أو إلى أيّ حقل في الصنف المشتق، يُعد من الممارسات غير الآمنة نظرًا لأنّ الصنف المشتق لم يُهيّأ بعد، وعليه تكون حقُوله ليست لا مهيّأة ولا موجودة. كذلك ليس من الآمن استدعاء الدوال التابعة الوهميّة عبر ‎this‎ في المُنشئ، ذلك أنّ دوالّ الصنف المشتق لن تُأخذ بالحسبان نظرًا لأنّ الصنف المشتق لم يُنشأ بعد، وعليه فإنّ مُنشئه لم يُحدِّث vtable بعد.

لاحظ أيضًا أن نوع الكائن في المُنشئ هو النوع الذي يُنشئه المُنشئ نفسه، ويبقى هذا صحيحًا حتى لو عُرِّف الكائن على أنه نوع مشتق. انظر المثال التالي حيث يكون كل من ‎ctd_good‎ و ‎ctd_bad‎ من نوع ‎CtorThisBase‎ داخل ‎CtorThisBase()‎، ومن نوع ‎CtorThis‎ داخل ‎CtorThis()‎، رغم أنّ نوعهما المعياري هو ‎CtorThisDerived‎.

ومع الاستمرار في الاشتقاق من الصنف الأساسي فإنّ النسخة تمرّ تدريجيًا عبر التسلسل الهرمي للصنف حتى تصبح نسخةً مكتملة من النوع المراد لها.

class CtorThisBase {
    short s;
    public:
        CtorThisBase(): s(516) {}
};

class CtorThis: public CtorThisBase {
    int i, j, k;
    public:
        // منشئ جيد
        CtorThis(): i(s + 42), j(this->i), k(j) {}

    // منشئ سيء
    CtorThis(int ii): i(ii), j(this->k), k(b ? 51 : -51) {
        virt_func();
    }

    virtual void virt_func() {
        i += 2;
    }
};

class CtorThisDerived: public CtorThis {
    bool b;
    public:
        CtorThisDerived(): b(true) {}
    CtorThisDerived(int ii): CtorThis(ii), b(false) {}
    void virt_func() override {
        k += (2 * i);
    }
};
// ...

CtorThisDerived ctd_good;
CtorThisDerived ctd_bad(3);

باعتبار هذه الأصناف والتوابع:

  • في المُنشئ الجيّد (انظر الشيفرة)، فإنّه بالنسبة إلى ‎ctd_good‎:

  • يُنشأ ‎CtorThisBase‎ بالكامل بحلول وقت إدخال المُنشئ ‎CtorThis‎، لذا تكون ‎s‎ في حالة صالحة أثناء تهيئة ‎i‎، ومن ثم يمكن الوصول إليها.

  • تُهيّأ ‎i‎ قبل الوصول إلى ‎j(this->i)‎، لذلك تكون ‎i‎ في حالة صالحة أثناء تهيئة ‎j‎، ومن ثم يمكن الوصول إليها.

  • تُهيّأ ‎j‎ قبل الوصول إلى k(j)‎‎‎، لذلك تكون ‎j‎ في حالة صالحة أثناء تهيئة ‎k‎، ومن ثم يمكن الوصول إليها.

  • في المُنشئ السيئ، فإنّه بالنسبة إلى ‎ctd_bad‎:

  • تُهيّأ ‎k‎ بعد الوصول إلى ‎j(this->k)‎، لذلك تكون ‎k‎ في حالة غير صالحة أثناء تهيئة ‎j‎ ، ويحدث سلوك غير محدد عند محاولة الوصول إليها.

  • لا يُنشأ ‎CtorThisDerived‎ إلّا بعد إنشاء ‎CtorThis‎، لذلك تكون ‎b‎ في حالة غير صالحة أثناء تهيئة ‎k‎ ، وقد تؤدّي محاولة الوصول إليها إلى سلوك غير محدّد.

  • يبقى الكائن‎ctd_bad‎ من النوع ‎CtorThis‎ إلى أن يغادر التابع ‎CtorThis()‎، ولن يتم تحديثه لاستخدام الجدول الوهمي (vtable) الخاص بالصنف المشتق ‎CtorThisDerived‎ حتّى التابع ‎CtorThisDerived()‎. وعليه تستدعي ‎virt_func ()‎ التابع ‎CtorThis::virt_func()‎ بغض النظر إن كان الهدف هو استدعاؤه هو أو استدعاء ‎CtorThisDerived::virt_func()‎.

استخدام المؤشر this للوصول إلى بيانات الأعضاء

لا يعدّ استخدام مؤشّر ‎this‎ في هذا السياق أمرًا ضروريًا، ولكنه سيجعل الشفرة أوضح للقارئ من خلال الإشارة إلى أنّ دالّة ما أو متغيرًا هو عضو من الصنف. انظر هذا المثال:

// مثال على هذا المؤشّر
#include <iostream>
#include <string>

using std::cout;
using std::endl;

class Class {
    public:
        Class();
    ~Class();
    int getPrivateNumber() const;
    private:
        int private_number = 42;
};

Class::Class() {}
Class::~Class() {}

int Class::getPrivateNumber() const {
    return this -> private_number;
}

int main() {
    Class class_example;
    cout << class_example.getPrivateNumber() << endl;
}

يمكنك مشاهدة مثال حيّ من هنا.

استخدام المؤشر this للتفريق بين المعامِلات وبيانات الأعضاء

هذه استراتيجية مفيدة لتمييز البيانات العضويّة عن المعاملات، لنأخذ مثالًا:

#include <iostream>
#include <string>

using std::cout;
using std::endl;

/*
 * @class Dog
 *   @member name
 *       Dog's name
 *   @function bark
 *       Dog Barks!
 *   @function getName
 *       To Get Private
 *       Name Variable
 */

class Dog {
    public:
        Dog(std::string name);
    ~Dog();
    void bark() const;
    std::string getName() const;
    private:
        std::string name;
};

Dog::Dog(std::string name) {

this->name هو متغير الاسم من صنف dog، ويكون name من معامِل الدالة. نتابع:

    this -> name = name;
}

Dog::~Dog() {}

void Dog::bark() const {
    cout << "BARK" << endl;
}

std::string Dog::getName() const {
    return this -> name;
}

int main() {
    Dog dog("Max");
    cout << dog.getName() << endl;
    dog.bark();
}

كما ترى هنا في المنشئ فقد نفذنا ما يلي:

this->name = name;

لاحظ أننا أسنَدنا المُعاملَ name إلى اسم المتغير الخاصّ من الصنف ‏‏Dog‏‏‏‏‏‏ (this->name)‏‏‏‏. انظر هنا لرؤية تطبيق حي للشيفرة أعلاه.

المؤهلات الخاصة بالمؤشر this ‏‏(this Pointer CV-Qualifiers)

يمكن للمؤشّر ‎this‎ أن يؤهَّل (cv-qualified) - أي تُحدَّد طبيعته، أهو ثابت أم متغير - مثل أيّ مؤشّر آخر. لكن بما أن المعامل ‎this‎ لا يُدرَج في قائمة المعاملات، فيجب استخدام صيغة خاصة بالمؤشّر this؛ لذا تُدرَج المؤهّلات (cv-qualifiers) بعد قائمة المعاملات، وقبل متن الدالة.

struct ThisCVQ {
    void no_qualifier() {}            // "this" is: ThisCVQ*
    void c_qualifier() const {}        // "this" is: const ThisCVQ*
    void v_qualifier() volatile {}        // "this" is: volatile ThisCVQ*
    void cv_qualifier() const volatile {}    // "this" is: const volatile ThisCVQ*
};

بما أن ‎this‎ معامِل فيمكن زيادة تحميل (overload) دالّة على أساس مؤهِّلات ‎this‎.

struct CVOverload {
    int func() {
        return 3;
    }
    int func() const {
        return 33;
    }
    int func() volatile {
        return 333;
    }
    int func() const volatile {
        return 3333;
    }
};

لن تكون الدالّة قادرة على الكتابة في متغيرات الأعضاء من خلال this عندما يكون ثابتًا (‎const‎) (بما في ذلك ‎const volatile‎)، سواء ضمنيًا أو بشكل صريح، والاستثناء الوحيد لهذا هو متغيرات الأعضاء القابلة للتغيير (‎mutable‎)، والتي يمكن أن تُكتب بغض النظر عن ثبوتيّتها. لهذا تُستخدم ‎const‎ للإشارة إلى أنّ التابع لا يغيِّر الحالة المنطقية للكائن -الطريقة التي يظهر بها الكائن للعالم الخارجي- حتى لو عدّل الحالة المادية، وهي الطريقة التي يظهر بها الكائن داخليًا.

اقتباس

الحالة المنطقية هي الطريقة التي يظهر بها الكائن للمُراقبين الخارجيين. ولا ترتبط بشكل مباشر بالحالة المادية، في الواقع، قد لا تُخزّن كحالة مادية. وطالما أنّ المراقبين الخارجيين لا يروْن أيّ تغييرات، فإنّ الحالة المنطقية ستُعدُّ ثابتة، حتى لو قُلِبتَ كل بتّة في الكائن.

الحالة المادّية، والمعروفة أيضًا باسم الحالة البتّيّة (bitwise state)، هي الكيفية التي يُخزّن بها الكائن في الذاكرة. إنّها تمثل جسد الكائن (الوحدات والأصفار الخام التي تشكل بياناته). يكون الكائن ثابتًا ماديًّا (physically constant) ما دام تمثيله في الذاكرة ثابتًا لا يتغيّر.

لاحظ أنّ لغة C++‎ تبني الثباتيّة (constness) على الحالة المنطقية وليس الحالة المادية.

class DoSomethingComplexAndOrExpensive {
    mutable ResultType cached_result;
    mutable bool state_changed;
    ResultType calculate_result();
    void modify_somehow(const Param & p);
    // ...
    public:
        DoSomethingComplexAndOrExpensive(Param p): state_changed(true) {
            modify_somehow(p);
        }
    void change_state(Param p) {
        modify_somehow(p);
        state_changed = true;
    }
    // إعادة نتيجة تحتاج إلى حسابات معقدة
	// يُحدَّد كثابت بما أنه لا يوجد سبب لتعديل الحالة المنطقية.
    ResultType get_result() const;
};
ResultType DoSomethingComplexAndOrExpensive::get_result() const {

يمكن تعديل cached_result و state_changed حتى مع مؤشر const ثابت، ورغم ان الدالة لن تغير الحالة المنطقية إلا أنها تعدّل الحالة المادية بتخزين النتيجة مؤقتًا كي لا تحتاج إلى إعادة حسابها في كل مرة تُستدعى الدالة فيها. ترى هذا واضحًا من قابلية كل من cached_result و state_changed للتغيير. انظر:

    if (state_changed) {
        cached_result = calculate_result();
        state_changed = false;
    }
    return cached_result;
}

ورغم أنك تستطيع استخدام ‎const_cast‎ مع ‎this‎ لجعله غير مؤهّل (non-cv-qualified)، إلا أنّه يوصى بتجنّب ذلك، ويُنصح باستخدام ‎mutable‎ بدلًا منه. كذلك قد تُحدث الكائنات غير المؤهّلة (‎const_cast‎) سلوكًا غير مُحدّدٍ عند استخدامها على كائن ثابت (‎const‎)، على عكس ‎mutable‎ التي صُمِّمت لتكون آمنة للاستخدام.

هناك استثناء لهذه القاعدة، وهو تعريف توابع وصول غير مؤهّلة (non-cv-qualified accessors) عبر توابع وصول ثابتة (‎const‎)، نظرًا لأنّ ذلك يضمن أنّ الكائن لن يكون ثابتًا إذا استُدعِيت النسخة غير المؤهّلة، لذلك لا توجد مخاطرة هنا.

class CVAccessor {
    int arr[5];
    public:
        const int & get_arr_element(size_t i) const {
            return arr[i];
        }
    int & get_arr_element(size_t i) {
        return const_cast < int & > (const_cast <
            const CVAccessor * > (this) -> get_arr_element(i));
    }
};

هذا يمنع التكرار غير الضروري للشيفرة.

وكما في المؤشّرات العادية، فإن كان ‎this‎ متغيّرًا ‎volatile‎ -بما في ذلك ‎const volatile‎- فإنه سيُحمَّل من الذاكرة في كل مرة يتم الوصول إليه بدلاً من تخزينه مؤقتًا، ومن ثم فإنّ تأثيره على سرعة البرنامج يشبه تأُثير التصريح عن مؤشّر متغيّر ‎volatile‎، لذا يجب توخّي الحذر.

لاحظ أنّه إذا كانت نسخة ما مؤهّلةً ثباتيًّا، فإنّ الدوال التابعة الوحيدة التي يُسمح لها بالوصول إليها هي التي يحمل المؤشّر ‎this‎ الخاص بها نفس التأهيل للنسخة ذاتها على الأقل:

  • يمكن للنُّسخ غير المؤهّلة أن تصل إلى كل الدوال التابعة.
  • يمكن للنسخ الثابتة (‎const‎) الوصول إلى الدوال ذات تأهيل ‎const‎ و ‎const ‎volatile‎.
  • يمكن للنسخ المتغيّرة (‎volatile‎) الوصول إلى الدوالّ ذات تأهيل ‎volatile‎ و ‎const‎ ‎volatile‎.
  • يمكن للنسخ ذات التأهيل ‎const ‎volatile‎ الوصول إلى الدوالّ ذات تأهيل ‎const ‎volatile‎.

هذا أحد المبادئ الأساسية للثباتيّة:

struct CVAccess {
    void func() {}
    void func_c() const {}
    void func_v() volatile {}
    void func_cv() const volatile {}
};
CVAccess cva;
cva.func(); // جيد
cva.func_c(); // جيد
cva.func_v(); // جيد
cva.func_cv(); // جيد
const CVAccess c_cva;
c_cva.func(); // خطأ
c_cva.func_c(); // جيد
c_cva.func_v(); // خطأ
c_cva.func_cv(); // جيد
volatile CVAccess v_cva;
v_cva.func(); // خطأ
v_cva.func_c(); // خطأ
v_cva.func_v(); // جيد
v_cva.func_cv(); // جيد
const volatile CVAccess cv_cva;
cv_cva.func(); // خطأ
cv_cva.func_c(); // خطأ
cv_cva.func_v(); // خطأ
cv_cva.func_cv(); // جيد

مؤهلات مؤشر this المرجعية

الإصدار C++‎ 11

يمكننا تطبيق المؤهّلات المرجعِيّة (ref-qualifiers) على ‎*this‎ على نحو مماثل لمؤهّلات ‎this‎ الثباتيّة، وتُستخدم المؤهّلات المرجعيّة للاختيار بين دلالات المرجع العادية والقيميّة اليمينيّة، ممّا يسمح للمُصرِّف أن يستخدم دلالات النسخ (copy) أو النقل (move) وفقًا لأيّهما أنسب، وتُطبَّق على ‎*this‎ بدلاً من ‎this‎.

وتجدر الإشارة أنه رغم استخدام المؤهِّلات المرجعية لصيغة المراجع (reference syntax) فلا يزال ‎this‎ مؤشّرًا، كذلك لاحظ أنّ المؤهّلات المرجِعيّة لا تغيّر في الواقع نوع ‎*this‎، بيْد أنه من الأسهل وصفها وفهمها من هذا المنظور.

struct RefQualifiers {
    std::string s;
    RefQualifiers(const std::string & ss = "The nameless one."): s(ss) {}
    // نسخة عادية
    void func() & {
        std::cout << "Accessed on normal instance " << s << std::endl;
    }
    // قيمة يمينية
    void func() && {
        std::cout << "Accessed on temporary instance " << s << std::endl;
    }
    const std::string & still_a_pointer() & {
        return this -> s;
    }
    const std::string & still_a_pointer() && {
        this -> s = "Bob";
        return this -> s;
    }
};
// ...
RefQualifiers rf("Fred");
rf.func(); // الخرج:  Accessed on normal instance Fred
RefQualifiers {}.func(); // الخرج:  Accessed on temporary instance The nameless one

لا يمكن زيادة تحميل دالة تابعة مع مؤهّل مرجعي مرة وبدونه مرّة أخرى، بل يختار المبرمج أحد الأمرَين. الجميل في الأمر أنه يمكن استخدام المؤهّلات الثباتيّة مع المؤهلات المرجعيّة، مما يسمح باتباع قواعد الثباتيّة. انظر المثال التالي:

struct RefCV {
    void func() & {}
    void func() && {}
    void func() const & {}
    void func() const && {}
    void func() volatile & {}
    void func() volatile && {}
    void func() const volatile & {}
    void func() const volatile && {}
};

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -بتصرّف- للفصول 30 وحتى 32 من كتاب C++ Notes for Professionals


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...