c++ ما هي الاختلافات بين متغير مؤشر ومتغير مرجع في C ++؟




15 Answers

ما هو مرجع C ++ ( للمبرمجين C )

يمكن اعتبار الإشارة كمؤشر ثابت (لا ينبغي الخلط بينه وبين مؤشر لقيمة ثابتة!) مع indirection تلقائي ، أي أن المترجم سيطبق المشغل * أجلك.

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

قد لا يروق مبرمجو C C لمراجع C ++ لأنه لن يكون واضحًا عند حدوث تفاعل غير مباشر أو إذا تم تمرير وسيطة حسب القيمة أو المؤشر بدون النظر إلى توقيعات الدوال.

قد لا يحب المبرمجون C ++ استخدام المؤشرات لأنها تعتبر غير آمنة - على الرغم من أن المراجع ليست في الواقع أكثر أمنا من المؤشرات الثابتة إلا في الحالات الأكثر تافها - يفتقر إلى الراحة التلقائية غير المباشرة ويحمل دلالة دلالية مختلفة.

خذ بعين الاعتبار العبارة التالية من الأسئلة المتداولة C ++ :

على الرغم من أن المرجع غالباً ما يتم تنفيذه باستخدام عنوان في لغة التجميع الأساسية ، من فضلك لا تفكر في إشارة كمؤشر مضحك المظهر إلى كائن. المرجع هو الكائن. ليس مؤشر إلى الكائن ولا نسخة من الكائن. هذا هو الشيء.

ولكن إذا كان المرجع حقاً كائن ، كيف يمكن أن يكون هناك مراجع متدلية؟ في اللغات غير المُدارة ، من المستحيل أن تكون المراجع أي "أكثر أمانًا" من المؤشرات - فهناك عمومًا طريقة غير موثوقة للقيم المتمايزة عبر حدود النطاق!

لماذا أعتبر مراجع C ++ مفيدة

قد تبدو المراجع C ++ من خلفية C ، وكأنها مفهوم سخيفة إلى حد ما ، ولكن يجب على المرء أن يستخدمها بدلاً من المؤشرات حيثما أمكن: إن التلقائية التلقائية مريحة ، وتصبح الإشارات مفيدة بشكل خاص عند التعامل مع RAII - ولكن ليس بسبب أي أمان مدرك ميزة ، ولكن لأنها تجعل من كتابة التعليمات البرمجية الاصطلاحية أقل محرجا.

RAII هي واحدة من المفاهيم المركزية لـ C ++ ، ولكنها تتفاعل بشكل غير تافه مع نسخ الدلالات. يتجنب تمرير الكائنات بالإحالة هذه المشكلات نظرًا لعدم مشاركة النسخ. إذا لم تكن المراجع موجودة في اللغة ، فسيتعين عليك استخدام المؤشرات بدلاً من ذلك ، والتي تكون أكثر تعقيداً للاستخدام ، وبالتالي تنتهك مبدأ تصميم اللغة أن الحل الأفضل في الممارسة يجب أن يكون أسهل من البدائل.

c++ pointers reference c++-faq

أعرف أن المراجع عبارة عن سكر نحوي ، لذلك من السهل قراءة الكود والكتابة.

لكن ما هي الاختلافات؟

ملخص من الإجابات والروابط أدناه:

  1. يمكن إعادة تعيين مؤشر أي عدد من المرات بينما لا يمكن إعادة تعيين مرجع بعد الربط.
  2. يمكن أن تشير المؤشرات إلى أي مكان ( NULL ) ، بينما يشير المرجع دائمًا إلى كائن.
  3. لا يمكنك أخذ عنوان مرجع كما يمكنك باستخدام المؤشرات.
  4. لا يوجد "حساب مرجعي" (ولكن يمكنك أن تأخذ عنوان كائن مدبب من قبل مرجع والقيام بحساب مؤشر عليه كما في &obj + 5 ).

لتوضيح الاعتقاد الخاطئ:

يكون معيار C ++ حذراً للغاية لتجنب إملاء كيف يمكن للمترجم تنفيذ المراجع ، ولكن يقوم كل مترجم C ++ بتنفيذ المراجع كمؤشرات. هذا هو ، إعلان مثل:

int &ri = i;

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

لذلك ، يستخدم كل من المؤشر والمرجع نفس مقدار الذاكرة.

كقاعدة عامة،

  • استخدم المراجع في معلمات الدالة وأنواع الإرجاع لتوفير واجهات مفيدة وتوثيق ذاتي.
  • استخدم مؤشرات لتنفيذ الخوارزميات وهياكل البيانات.

قراءة مثيرة للاهتمام:




خلافا للرأي الشعبي ، من الممكن أن يكون هناك إشارة NULL.

int * p = NULL;
int & r = *p;
r = 1;  // crash! (if you're lucky)

صحيح أنه من الأصعب القيام بمرجع - ولكن إذا قمت بإدارته ، فسوف تمزق شعرك في محاولة العثور عليه. المراجع ليست آمنة بطبيعتها في C ++!

من الناحية الفنية ، يعد هذا مرجعًا غير صالح ، وليس مرجعًا فارغًا. لا يدعم C ++ المراجع الفارغة كمفهوم كما قد تجده في اللغات الأخرى. هناك أنواع أخرى من المراجع غير الصالحة أيضًا. أي مرجع غير صالح يرفع شبح السلوك غير المعروف ، مثلما يفعل مؤشر غير صحيح.

الخطأ الفعلي في dereferencing مؤشر NULL ، قبل التعيين إلى مرجع. لكنني لست على علم بأي من المجمعين الذين سيولدون أي أخطاء على هذا الشرط - ينتشر الخطأ إلى نقطة أخرى في الشفرة. هذا ما يجعل هذه المشكلة خبيثة جدا. في معظم الأحيان ، إذا قمت بالتعريف عن مؤشر NULL ، فستتحطم على الفور في هذا المكان ولا يتطلب الأمر تصحيحًا كبيرًا حتى يتم اكتشافه.

المثال أعلاه هو قصير ومفتوح. فيما يلي مثال أكثر واقعية.

class MyClass
{
    ...
    virtual void DoSomething(int,int,int,int,int);
};

void Foo(const MyClass & bar)
{
    ...
    bar.DoSomething(i1,i2,i3,i4,i5);  // crash occurs here due to memory access violation - obvious why?
}

MyClass * GetInstance()
{
    if (somecondition)
        return NULL;
    ...
}

MyClass * p = GetInstance();
Foo(*p);

أريد أن أؤكد مجددًا أن الطريقة الوحيدة للحصول على مرجع فارغ هي من خلال التعليمة البرمجية غير الصحيحة ، وبمجرد حصولك على سلوك غير محدد. من غير المنطقي أبدًا التحقق من وجود مرجع فارغ ؛ على سبيل المثال يمكنك محاولة if(&bar==NULL)... ولكن قد المحول البرمجي تحسين العبارة خارج الوجود! لا يمكن أبداً أن يكون المرجع الصالح خاليًا من وجهة نظر المترجم ، فالمقارنة دائمًا ما تكون خاطئة ، وهي حرة في التخلص من الجملة if رمزًا ميتًا - وهذا هو جوهر السلوك غير المعروف.

الطريقة الصحيحة للبقاء بعيداً عن المشاكل هي تجنب dereferencing مؤشر NULL لإنشاء مرجع. إليك طريقة آلية لتحقيق ذلك.

template<typename T>
T& deref(T* p)
{
    if (p == NULL)
        throw std::invalid_argument(std::string("NULL reference"));
    return *p;
}

MyClass * p = GetInstance();
Foo(deref(p));

لإلقاء نظرة قديمة على هذه المشكلة من شخص لديه مهارات كتابية أفضل ، راجع Null References من Jim Hyslop و Herb Sutter.

للحصول على مثال آخر من مخاطر dereferencing مؤشر فارغ انظر Exposing سلوك غير محدد عند محاولة رمز المنفذ إلى نظام أساسي آخر بواسطة Raymond Chen.




لقد نسيت الجزء الأكثر أهمية:

وصول الأعضاء مع استخدامات المؤشرات ->
العضو الوصول مع استخدامات المراجع .

foo.bar بشكل واضح متفوق على foo->bar بنفس الطريقة التي vi متفوقة بشكل واضح على Emacs :-)




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

  • تم تصميم المراجع بحيث يسهل على المترجم أن يقيس أي الأسماء المستعارة المرجعية التي هي المتغيرات. هناك ميزتان رئيسيتان مهمتان للغاية: لا "حساب مرجعي" ولا إعادة تعيين للمراجع. هذه تسمح للالمترجم لمعرفة أي المراجع الاسم المستعار المتغيرات في وقت التحويل البرمجي.
  • يُسمح للمراجع بالرجوع إلى المتغيرات التي لا تحتوي على عناوين ذاكرة ، مثل تلك التي يختار المحول البرمجي وضعها في السجلات. إذا كنت تأخذ عنوان متغير محلي ، فمن الصعب جدًا على المترجم وضعه في سجل.

كمثال:

void maybeModify(int& x); // may modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // This function is designed to do something particularly troublesome
    // for optimizers. It will constantly call maybeModify on array[0] while
    // adding array[1] to array[2]..array[size-1]. There's no real reason to
    // do this, other than to demonstrate the power of references.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(array[0]);
        array[i] += array[1];
    }
}

قد يدرك المترجم المحسن أننا نصل إلى [0] و [1] مجموعة كبيرة. إنه يحب تحسين الخوارزمية من أجل:

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Do the same thing as above, but instead of accessing array[1]
    // all the time, access it once and store the result in a register,
    // which is much faster to do arithmetic with.
    register int a0 = a[0];
    register int a1 = a[1]; // access a[1] once
    for (int i = 2; i < (int)size; i++) {
        maybeModify(a0); // Give maybeModify a reference to a register
        array[i] += a1;  // Use the saved register value over and over
    }
    a[0] = a0; // Store the modified a[0] back into the array
}

لجعل هذا التحسين ، يحتاج إلى إثبات أن لا شيء يمكن أن يغير الصفيف [1] أثناء المكالمة. هذا من السهل القيام به. أنا لا أقل أبدًا من 2 ، لذا لا يمكن للمصفوفة [i] الإشارة أبداً إلى الصفيف [1]. maybeModify () يتم إعطاء a0 كمرجع (صفيف مستعار [0]). نظرًا لعدم وجود حساب "مرجع" ، يجب على المترجم أن يثبت فقط أن mayModify لن يحصل على عنوان x ، وقد أثبت أنه لا شيء يتغير الصفيف [1].

كما يجب أن يثبت أنه لا توجد طرق يمكن للمكالمة المستقبلية قراءة / كتابة [0] بينما يكون لدينا نسخة تسجيل مؤقتة في a0. غالبًا ما يكون هذا أمرًا ضئيلًا لإثباته ، لأنه في كثير من الحالات يكون من الواضح أنه لا يتم تخزين المرجع في بنية دائمة مثل مثيل الصف.

الآن افعل نفس الشيء مع المؤشرات

void maybeModify(int* x); // May modify x in some way

void hurtTheCompilersOptimizer(short size, int array[])
{
    // Same operation, only now with pointers, making the
    // optimization trickier.
    for (int i = 2; i < (int)size; i++) {
        maybeModify(&(array[0]));
        array[i] += array[1];
    }
}

السلوك هو نفسه. الآن فقط من الأصعب بكثير إثبات أن mayModify لا يقوم بتعديل المصفوفة [1] ، لأننا بالفعل أعطيناها مؤشرًا ؛ القطة خارج الحقيبة. الآن عليها أن تفعل البرهان الأكثر صعوبة: تحليل ثابت ل mayModify لإثبات أنه لم يكتب إلى & x + 1. كما يجب أن يثبت أنه لا ينقذ مؤشر يمكن أن يشير إلى صفيف [0] ، وهو فقط كما صعبة.

إن المترجمين الحديثين يصبحون أفضل وأفضل في التحليل الساكن ، لكن من الجيد دائماً مساعدتهم واستخدام المراجع.

وبطبيعة الحال ، في حالة عدم وجود مثل هذه التحسينات الذكية ، سيحول المجمعون بالفعل المراجع إلى مؤشرات عند الحاجة.

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

F createF(int argument);

void extending()
{
    const F& ref = createF(5);
    std::cout << ref.getArgument() << std::endl;
};

يتم إتلاف الكائنات المؤقتة عادةً مثل تلك التي تم إنشاؤها بواسطة استدعاء createF(5) في نهاية التعبير. ومع ذلك ، من خلال ربط هذا الكائن بمرجع ، ref ، سيعمل C ++ على إطالة عمر هذا الكائن المؤقت حتى يخرج ref من النطاق.




في حين يتم استخدام كل من المراجع والمؤشرات للوصول بشكل غير مباشر إلى قيمة أخرى ، هناك اختلافان مهمان بين المراجع والمؤشرات. الأول هو أن المرجع يشير دائمًا إلى كائن: إنه خطأ في تحديد مرجع بدون تهيئة له. سلوك التخصيص هو الاختلاف المهم الثاني: يؤدي التخصيص إلى مرجع إلى تغيير الكائن المرتبط به المرجع ؛ فإنه لا rebind المرجع إلى كائن آخر. وبمجرد التهيئة ، يشير المرجع دائمًا إلى نفس الكائن الأساسي.

خذ بعين الاعتبار هاتين الشظايا البرنامج. في الأول ، نخصص مؤشرًا لآخر:

int ival = 1024, ival2 = 2048;
int *pi = &ival, *pi2 = &ival2;
pi = pi2;    // pi now points to ival2

بعد المهمة ، ival ، يبقى الكائن الذي تم تناوله بواسطة pi بدون تغيير. يغير الواجب قيمة pi ، مما يجعله يشير إلى كائن مختلف. الآن ضع في اعتبارك برنامج مشابه يقوم بتعيين مرجعين:

int &ri = ival, &ri2 = ival2;
ri = ri2;    // assigns ival2 to ival

يتغير هذا الواجب ival ، القيمة المشار إليها بواسطة ri ، وليس المرجع نفسه. بعد التعيين ، لا يزال المرجعان يشيران إلى كائناتهما الأصلية ، وقيمة هذه الكائنات هي نفسها الآن.




المرجع هو اسم مستعار لمتغير آخر بينما يحتفظ المؤشر بعنوان ذاكرة للمتغير. يتم استخدام المراجع عمومًا كمعلمات دالة بحيث لا يكون الكائن الذي تم تمريره هو النسخة ولكن الكائن نفسه.

    void fun(int &a, int &b); // A common usage of references.
    int a = 0;
    int &b = a; // b is an alias for a. Not so common to use. 



هذا يعتمد على tutorial . ما هو مكتوب يجعلها أكثر وضوحا:

>>> The address that locates a variable within memory is
    what we call a reference to that variable. (5th paragraph at page 63)

>>> The variable that stores the reference to another
    variable is what we call a pointer. (3rd paragraph at page 64)

ببساطة لتذكر ذلك ،

>>> reference stands for memory location
>>> pointer is a reference container (Maybe because we will use it for
several times, it is better to remember that reference.)

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

انظر إلى العبارة التالية ،

int Tom(0);
int & alias_Tom = Tom;

alias_Tomيمكن أن يفهم على أنه alias of a variable(مختلف مع typedef، وهو alias of a type) Tom. كما يمكنك أيضًا نسيان مصطلحات مثل هذا البيان هو إنشاء مرجع Tom.




يمكن الإشارة إلى مؤشر في C ++ ، ولكن العكس غير ممكن يعني أن المؤشر إلى مرجع غير ممكن. يوفر مرجع إلى مؤشر بناء جملة أنظف لتعديل المؤشر. انظر إلى هذا المثال:

#include<iostream>
using namespace std;

void swap(char * &str1, char * &str2)
{
  char *temp = str1;
  str1 = str2;
  str2 = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap(str1, str2);
  cout<<"str1 is "<<str1<<endl;
  cout<<"str2 is "<<str2<<endl;
  return 0;
}

والنظر في الإصدار C من البرنامج أعلاه. في C عليك استخدام المؤشر إلى المؤشر (indirection متعددة) ، ويؤدي إلى الارتباك وقد يبدو البرنامج معقدًا.

#include<stdio.h>
/* Swaps strings by swapping pointers */
void swap1(char **str1_ptr, char **str2_ptr)
{
  char *temp = *str1_ptr;
  *str1_ptr = *str2_ptr;
  *str2_ptr = temp;
}

int main()
{
  char *str1 = "Hi";
  char *str2 = "Hello";
  swap1(&str1, &str2);
  printf("str1 is %s, str2 is %s", str1, str2);
  return 0;
}

قم بزيارة التالي للحصول على مزيد من المعلومات حول مرجع إلى المؤشر:

كما قلت ، لا يمكن مؤشر إلى مرجع. جرب البرنامج التالي:

#include <iostream>
using namespace std;

int main()
{
   int x = 10;
   int *ptr = &x;
   int &*ptr1 = ptr;
}



أستخدم المراجع ما لم أحتاج إلى أي من هذين الأمرين:

  • يمكن استخدام مؤشرات فارغة كقيمة الحارس ، غالباً طريقة رخيصة لتجنب التحميل الزائد للوظيفة أو استخدام bool.

  • يمكنك أن تفعل الحساب على مؤشر. فمثلا،p += offset;




فرق آخر هو أنه يمكن أن يكون لديك مؤشرات لنوع الفراغ (وهذا يعني مؤشر لأي شيء) ولكن يتم حظر المراجع إلى باطلة.

int a;
void * p = &a; // ok
void & p = a;  //  forbidden

لا أستطيع أن أقول إنني سعيد بهذا الفارق بالتحديد. كنت أفضل أن يتم ذلك مع الإشارة إلى معنى أي شيء مع عنوان ونفس السلوك للمراجع. سيسمح بتعريف بعض معادلات وظائف مكتبة C مثل memcpy باستخدام المراجع.




أيضاً ، قد يتم معالجة مرجع يمثل معلمة لدالة مضمن بشكل مختلف عن مؤشر.

void increment(int *ptrint) { (*ptrint)++; }
void increment(int &refint) { refint++; }
void incptrtest()
{
    int testptr=0;
    increment(&testptr);
}
void increftest()
{
    int testref=0;
    increment(testref);
}

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

وبطبيعة الحال ، بالنسبة للوظائف التي لم يتم تضمينها ، يقوم المؤشر والمرجع بتوليد نفس التعليمة البرمجية ومن الأفضل دائمًا تمرير intrinsics حسب القيمة عن طريق الإشارة في حالة عدم تعديلها وإعادتها بواسطة الدالة.




استخدام آخر مثير للاهتمام من المراجع هو توفير وسيطة افتراضية من نوع المعرفة من قبل المستخدم:

class UDT
{
public:
   UDT() : val_d(33) {};
   UDT(int val) : val_d(val) {};
   virtual ~UDT() {};
private:
   int val_d;
};

class UDT_Derived : public UDT
{
public:
   UDT_Derived() : UDT() {};
   virtual ~UDT_Derived() {};
};

class Behavior
{
public:
   Behavior(
      const UDT &udt = UDT()
   )  {};
};

int main()
{
   Behavior b; // take default

   UDT u(88);
   Behavior c(u);

   UDT_Derived ud;
   Behavior d(ud);

   return 1;
}

يستخدم النكهة الافتراضية "مرجع const المرجع لجانب" مؤقت من المراجع.




ربما تساعد بعض الاستعارات ؛ في سياق مساحة شاشة سطح المكتب -

  • يتطلب المرجع تحديد نافذة فعلية.
  • يتطلب مؤشر موقع قطعة من المساحة على الشاشة التي تؤكد أنها سوف تحتوي على صفر أو أكثر من مثيلاتها من نوع الإطار.



يوجد اختلاف غير تقني مهم جدًا بين المؤشرات والمراجع: تكون الوسيطة التي يتم تمريرها إلى دالة بواسطة مؤشر أكثر وضوحًا من وسيطة تم تمريرها إلى دالة بواسطة مرجع غير ثابت. فمثلا:

void fn1(std::string s);
void fn2(const std::string& s);
void fn3(std::string& s);
void fn4(std::string* s);

void bar() {
    std::string x;
    fn1(x);  // Cannot modify x
    fn2(x);  // Cannot modify x (without const_cast)
    fn3(x);  // CAN modify x!
    fn4(&x); // Can modify x (but is obvious about it)
}

مرة أخرى في C ، fn(x)يمكن تمرير مكالمة تشبه القيمة فقط ، لذلك لا يمكن تعديلها بالتأكيد x؛ لتعديل وسيطة ستحتاج إلى تمرير مؤشر fn(&x). لذلك إذا لم تسبقك حجة من قبل &كنت تعرف أنه لن يتم تعديلها. (العكس ، &يعني معدلاً ، لم يكن صحيحاً لأنك تحتاج أحيانًا إلى تمرير بنية للقراءة فقط كبيرة بواسطة constالمؤشر.)

يزعم البعض أن هذه ميزة مفيدة عند قراءة الكود ، بحيث يجب استخدام معلمات المؤشر دائمًا لمعلمات قابلة للتعديل بدلاً من constالمراجع ، حتى لو لم تكن الدالة تتوقع أبدًا nullptr. أي أن هؤلاء الناس يجادلون بأن التوقيعات الوظيفية مثل ما fn3()سبق يجب عدم السماح بها. تعد إرشادات نمط لغة C ++ من Google مثالاً على ذلك.




أقرر دائمًا this القاعدة من إرشادات C ++ الأساسية:

يفضل T * على T & عندما يكون "no argument" خيارًا صالحًا




Related