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




reference c++-faq (25)

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

المراجع أقل قوة من المؤشرات

1) بمجرد إنشاء مرجع ، لا يمكن القيام به لاحقًا للإشارة إلى كائن آخر ؛ لا يمكن أن تكون متقلبة. غالبا ما يتم ذلك مع مؤشرات.

2) لا يمكن أن تكون المراجع فارغة. غالباً ما يتم وضع المؤشرات NULL للإشارة إلى أنها لا تشير إلى أي شيء صالح.

3) يجب أن تتم تهيئة المرجع عند الإعلان عنه. لا يوجد مثل هذا القيد مع مؤشرات

نظرًا للقيود أعلاه ، لا يمكن استخدام المراجع في C ++ لتنفيذ هياكل البيانات مثل Linked List و Tree وغير ذلك. في Java ، لا تحتوي المراجع على قيود أعلاه ، ويمكن استخدامها لتنفيذ جميع هياكل البيانات. المراجع كونها أكثر قوة في Java ، هو السبب الرئيسي لعدم حاجة جافا إلى المؤشرات.

تعد المراجع أكثر أمانًا وأسهل في الاستخدام:

1) أكثر أمانًا: بما أنه يجب تهيئة المراجع ، فمن غير المحتمل وجود إشارات برية مثل المؤشرات البرية. لا يزال من الممكن وجود مراجع لا تشير إلى موقع صالح

2) سهولة الاستخدام: لا تحتاج المراجع إلى مشغل إلغاء الإشارة لإمكانية الوصول إلى القيمة. يمكن استخدامها مثل المتغيرات العادية. مطلوب "&" المشغل فقط في وقت الإعلان. كما يمكن الوصول إلى أعضاء مرجع الكائن باستخدام عامل نقطة (".") ، بخلاف المؤشرات التي يحتاج فيها مشغل السهم (->) إلى الوصول إلى الأعضاء.

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

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

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

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

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

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

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

int &ri = i;

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

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

كقاعدة عامة،

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

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


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

التحيات ، و rzej


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

int& j = i;

يصبح داخليا

int* const j = &i;

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

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

فمثلا:

class scope_test
{
public:
    ~scope_test() { printf("scope_test done!\n"); }
};

...

{
    const scope_test &test= scope_test();
    printf("in scope\n");
}

سوف تطبع:

in scope
scope_test done!

هذه هي آلية اللغة التي تسمح ScopeGuard للعمل.


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


يمكن الإشارة إلى مؤشر في 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;
}

الفرق بين المؤشر والمرجع

يمكن تهيئة المؤشر إلى 0 والمرجع لا. في الواقع ، يجب أن يشير المرجع أيضًا إلى كائن ، ولكن يمكن أن يكون المؤشر المؤشر الفارغ:

int* p = 0;

ولكننا لا يمكن أن يكون لها int& p = 0;وأيضاint& p=5 ; .

في الحقيقة للقيام بذلك بشكل صحيح ، يجب أن نكون قد صرحنا وعرفنا شيئًا في البداية ، فيمكننا أن نرجع إلى ذلك الكائن ، لذا فإن التنفيذ الصحيح للرمز السابق سيكون:

Int x = 0;
Int y = 5;
Int& p = x;
Int& p1 = y;

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

هناك اختلاف آخر هو أن المؤشر يمكن أن يشير إلى كائن آخر ، إلا أن الإشارة تشير دائمًا إلى نفس الكائن ، دعنا نأخذ هذا المثال:

Int a = 6, b = 5;
Int& rf = a;

Cout << rf << endl; // The result we will get is 6, because rf is referencing to the value of a.

rf = b;
cout << a << endl; // The result will be 5 because the value of b now will be stored into the address of a so the former value of a will be erased

نقطة أخرى: عندما يكون لدينا قالب مثل قالب STL ، فإن هذا النوع من قالب الفئة سيعرض دائمًا مرجعًا ، وليس مؤشرًا ، لتسهيل القراءة أو تعيين قيمة جديدة باستخدام عامل التشغيل []:

Std ::vector<int>v(10); // Initialize a vector with 10 elements
V[5] = 5; // Writing the value 5 into the 6 element of our vector, so if the returned type of operator [] was a pointer and not a reference we should write this *v[5]=5, by making a reference we overwrite the element by using the assignment "="

خلافا للرأي الشعبي ، من الممكن أن يكون هناك إشارة 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.


لا يمكن أبداً أن يكون المرجع NULL .


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

#include <iostream>
int main(int argc, char** argv) {
    // Create a string on the heap
    std::string *str_ptr = new std::string("THIS IS A STRING");
    // Dereference the string on the heap, and assign it to the reference
    std::string &str_ref = *str_ptr;
    // Not even a compiler warning! At least with gcc
    // Now lets try to print it's value!
    std::cout << str_ref << std::endl;
    // It works! Now lets print and compare actual memory addresses
    std::cout << str_ptr << " : " << &str_ref << std::endl;
    // Exactly the same, now remember to free the memory on the heap
    delete str_ptr;
}

الذي يخرج هذا:

THIS IS A STRING
0xbb2070 : 0xbb2070

إذا لاحظت حتى عناوين الذاكرة هي نفسها بالضبط ، وهذا يعني الإشارة بنجاح إلى متغير على كومة الذاكرة المؤقتة! الآن إذا كنت تريد حقا أن تحصل على فظيع ، وهذا يعمل أيضا:

int main(int argc, char** argv) {
    // In the actual new declaration let immediately de-reference and assign it to the reference
    std::string &str_ref = *(new std::string("THIS IS A STRING"));
    // Once again, it works! (at least in gcc)
    std::cout << str_ref;
    // Once again it prints fine, however we have no pointer to the heap allocation, right? So how do we free the space we just ignorantly created?
    delete &str_ref;
    /*And, it works, because we are taking the memory address that the reference is
    storing, and deleting it, which is all a pointer is doing, just we have to specify
    the address with '&' whereas a pointer does that implicitly, this is sort of like
    calling delete &(*str_ptr); (which also compiles and runs fine).*/
}

الذي يخرج هذا:

THIS IS A STRING

لذلك فالمرجع عبارة عن مؤشر تحت غطاء المحرك ، كلاهما يخزن فقط عنوان ذاكرة ، حيث يشير العنوان إلى أنه غير ذي صلة ، ما رأيك سيحدث إذا اتصلت بـ std :: cout << str_ref؛ بعد الدعوة حذف و str_ref؟ حسنًا ، من الواضح أنه يجمع جيدًا ، لكنه يتسبب في خطأ تجزئة في وقت التشغيل لأنه لم يعد يشير إلى متغير صالح ، فنحن في الأساس لدينا مرجع معطل لا يزال موجودًا (حتى يقع خارج النطاق) ، ولكنه عديم الفائدة.

وبعبارة أخرى ، فإن الإشارة ليست سوى مؤشر يحتوي على ميكانيكي المؤشر المستخلص بعيدًا ، مما يجعله أكثر أمانًا وأسهل في الاستخدام (لا توجد رياضيات عرضية للمفارقات ، ولا يختلط "." و "->" ، وما إلى ذلك) ، على افتراض أنك لا تحاول أي هراء مثل الأمثلة الخاصة بي أعلاه ؛)

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

القاعدة الرئيسية الوحيدة التي يجب تذكرها مع المراجع هي أنه يجب تعريفها في وقت الإعلان (باستثناء إشارة في الرأس ، وفي هذه الحالة يجب أن يتم تعريفها في المُنشئ ، بعد أن يكون الكائن الذي يحتوي عليه بنيت فوات الاوان لتحديده).

تذكر أن أمثلتي أعلاه هي مجرد أمثلة توضح ما هو المرجع ، فأنت لن ترغب أبدًا في استخدام مرجع بهذه الطرق! للاستخدام الصحيح للإشارة هناك الكثير من الإجابات هنا بالفعل التي تضرب الظفر على الرأس


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

    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. 

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

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

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

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

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

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

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

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

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

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

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


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

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

كمثال:

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 من النطاق.


إذا كنت تريد أن تكون متحذلقًا حقًا ، فهناك أمر واحد يمكنك القيام به مع مرجع لا يمكنك فعله بمؤشر: تمديد عمر كائن مؤقت. في C ++ إذا قمت بربط مرجع const إلى كائن مؤقت ، يصبح عمر هذا الكائن عمر المرجع.

std::string s1 = "123";
std::string s2 = "456";

std::string s3_copy = s1 + s2;
const std::string& s3_reference = s1 + s2;

في هذا المثال ، ينسخ s3_copy الكائن المؤقت الذي هو نتيجة لسَلسَلة. في حين يصبح s3_reference في جوهر الكائن المؤقت. إنها حقًا مرجع إلى كائن مؤقت له الآن نفس العمر كمرجع.

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


الفرق هو أنه يمكن تغيير متغير المؤشر غير الثابت (لا ينبغي الخلط بينه وبين مؤشر إلى ثابت) في وقت ما أثناء تنفيذ البرنامج ، يتطلب استخدام دلالات المؤشر (& ، *) ، بينما يمكن تعيين المراجع عند التهيئة فقط (هذا هو السبب في أنه يمكنك تعيينها في قائمة مُنشئ المُبدِع فقط ، ولكن ليس بطريقة أخرى) واستخدام دلالات الوصول إلى القيمة العادية. في الأساس تم تقديم المراجع للسماح بدعم مشغلي الحمولة الزائدة كما قرأت في بعض الكتاب القديم جدا. كما ذكر شخص ما في هذا الموضوع - يمكن تعيين المؤشر إلى 0 أو القيمة التي تريدها. 0 (NULL ، nullptr) يعني أنه يتم تهيئة المؤشر بدون أي شيء. إنه خطأ dereference مؤشر فارغة. ولكن في الواقع قد يحتوي المؤشر على قيمة لا تشير إلى بعض موقع الذاكرة الصحيح.لا تحاول المراجع في دورها السماح للمستخدم بتهيئة مرجع لشيء لا يمكن الإشارة إليه بسبب حقيقة أنك تقدم دائمًا نوعًا من النوع الصحيح إليه. على الرغم من وجود العديد من الطرق لجعل المتغير المرجعي قد تمت تهيئته على موقع ذاكرة غير صحيح - فمن الأفضل لك ألا تقوم بحفر هذا بالتفصيل. على مستوى الآلة كل من المؤشر والعمل المرجعي بشكل موحد - عبر المؤشرات. دعونا نقول في المراجع الأساسية هي السكر النحوي. تختلف مراجع rvalue عن هذا - فهي كائنات مكدس / كومة بشكل طبيعي.على الرغم من وجود العديد من الطرق لجعل المتغير المرجعي قد تمت تهيئته على موقع ذاكرة غير صحيح - فمن الأفضل لك ألا تقوم بحفر هذا بالتفصيل. على مستوى الآلة كل من المؤشر والعمل المرجعي بشكل موحد - عبر المؤشرات. دعونا نقول في المراجع الأساسية هي السكر النحوي. تختلف مراجع rvalue عن هذا - فهي كائنات مكدس / كومة بشكل طبيعي.على الرغم من وجود العديد من الطرق لجعل المتغير المرجعي قد تمت تهيئته على موقع ذاكرة غير صحيح - فمن الأفضل لك ألا تقوم بحفر هذا بالتفصيل. على مستوى الآلة كل من المؤشر والعمل المرجعي بشكل موحد - عبر المؤشرات. دعونا نقول في المراجع الأساسية هي السكر النحوي. تختلف مراجع rvalue عن هذا - فهي كائنات مكدس / كومة بشكل طبيعي.


بصرف النظر عن السكر النحوي ، مرجع هو مؤشر const ( لا مؤشر إلى const ). يجب عليك تحديد ما تشير إليه عندما تعلن متغير المرجع ، ولا يمكنك تغييره لاحقًا.

تحديث: الآن بعد أن فكرت في الأمر أكثر من ذلك ، هناك فرق مهم.

يمكن استبدال هدف مؤشر const بأخذ عنوانه واستخدام قالب Const.

لا يمكن استبدال هدف المرجع بأي شكل من الأشكال بأقل من UB.

يجب أن يسمح هذا المحول للقيام المزيد من التحسين على مرجع.


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

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 مثالاً على ذلك.


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

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 حسب القيمة عن طريق الإشارة في حالة عدم تعديلها وإعادتها بواسطة الدالة.


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

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

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


أشعر أن هناك نقطة أخرى لم يتم تغطيتها هنا.

بخلاف المؤشرات ، تكون المراجع متكافئة معًا للجسم الذي تشير إليه ، أي أي عملية يمكن تطبيقها على كائن يعمل كمرجع ، وبنفس الصيغة الصحيحة (الاستثناء هو بالطبع التهيئة).

في حين أن هذا قد يبدو سطحيًا ، أعتقد أن هذه الخاصية ضرورية لعدد من ميزات C ++ ، على سبيل المثال:

  • قوالب . بما أن معلمات القالب مكتوبة بالبط ، فإن الخصائص النحوية من النوع هي كل ما يهم ، في كثير من الأحيان يمكن استخدام نفس القالب مع كل من Tو T&.
    (أو std::reference_wrapper<T>التي لا تزال تعتمد على الزهر ضمني T&)
    قوالب التي تغطي كلا T&و T&&، بل هي أكثر شيوعا.

  • لافاليس . النظر في البيان str[0] = 'X';بدون مراجع أنها ستعمل فقط على سلاسل c ( char* str). يسمح إرجاع الحرف حسب المرجع للفئات المعرفة من قبل المستخدم بالحصول على نفس الترميز.

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

  • المشغل الزائد . مع المراجع فمن الممكن إدخال indirection إلى مكالمة المشغل - على سبيل المثال ، operator+(const T& a, const T& b)مع الاحتفاظ بنفس التدوين. هذا يعمل أيضا للوظائف المحملة بشكل منتظم.

هذه النقاط تمكن جزء كبير من C ++ والمكتبة القياسية لذلك هذا هو تماما خاصية رئيسية من المراجع.


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

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

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

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

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


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

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


لفهم الفرق بين التعريف والتعريف ، نحتاج إلى رؤية رمز التجميع:

uint8_t   ui8 = 5;  |   movb    $0x5,-0x45(%rbp)
int         i = 5;  |   movl    $0x5,-0x3c(%rbp)
uint32_t ui32 = 5;  |   movl    $0x5,-0x38(%rbp)
uint64_t ui64 = 5;  |   movq    $0x5,-0x10(%rbp)
double   doub = 5;  |   movsd   0x328(%rip),%xmm0        # 0x400a20
                        movsd   %xmm0,-0x8(%rbp)

وهذا هو التعريف الوحيد:

ui8 = 5;   |   movb    $0x5,-0x45(%rbp)
i = 5;     |   movl    $0x5,-0x3c(%rbp)
ui32 = 5;  |   movl    $0x5,-0x38(%rbp)
ui64 = 5;  |   movq    $0x5,-0x10(%rbp)
doub = 5;  |   movsd   0x328(%rip),%xmm0        # 0x400a20
               movsd   %xmm0,-0x8(%rbp)

كما ترون لا شيء يتغير.

يختلف التعريف عن التعريف لأنه يعطي معلومات يستخدمها فقط المترجم. على سبيل المثال uint8_t أخبر المترجم أن يستخدم asm دالة movb.

أنظر لهذا:

uint def;                  |  no instructions
printf("some stuff...");   |  [...] callq   0x400450 <[email protected]>
def=5;                     |  movb    $0x5,-0x45(%rbp)

الإعلان ليس له تعادل التعليمات لأنه لا يوجد شيء يجب تنفيذه.

علاوة على ذلك ، يخبر الإعلان المترجم بنطاق المتغير.

يمكننا أن نقول أن الإعلان هو عبارة عن معلومات يستخدمها المترجم لإنشاء الاستخدام الصحيح للمتغير ولأي مدة تنتمي بعض الذاكرة إلى متغير معين.





c++ pointers reference c++-faq