ميكانيكا اللغة من المداخن والمؤشرات

مقدمة


هذه هي المقالة الأولى من أربع مقالات في السلسلة التي ستوفر نظرة ثاقبة على ميكانيكا وتصميم المؤشرات ، والأكوام ، والأكوام ، وتحليل الهروب ، ودلالات Go / pointer. هذا المنشور عن المكدس والمؤشرات.

جدول المحتويات:

  1. ميكانيكا اللغة على المداخن والمؤشرات
  2. ميكانيكا اللغة في تحليل الهروب ( ترجمة )
  3. ميكانيكا اللغة في تحديد ملامح الذاكرة
  4. فلسفة التصميم في البيانات والمعاني

المقدمة


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

حدود الإطار


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

عندما يتم استدعاء دالة ، يحدث انتقال بين إطارين. ينتقل الرمز من إطار وظيفة الاستدعاء إلى إطار الوظيفة المطلوبة. إذا كانت البيانات مطلوبة لاستدعاء الوظيفة ، فيجب نقل هذه البيانات من إطار إلى آخر. يتم نقل البيانات بين إطارين في Go "حسب القيمة".

ميزة نقل البيانات "بالقيمة" هي سهولة القراءة. القيمة التي تراها في استدعاء الوظيفة هي ما يتم نسخه وقبوله على الجانب الآخر. لهذا السبب أقوم بربط "المرور بالقيمة" مع WYSIWYG ، لأن ما تراه هو ما تحصل عليه. كل هذا يسمح لك بكتابة كود لا يخفي تكلفة التبديل بين وظيفتين. يساعد هذا في الحفاظ على نموذج عقلي جيد لكيفية تأثير كل استدعاء دالة على البرنامج أثناء الفترة الانتقالية.

انظر إلى هذا البرنامج الصغير الذي يستدعي دالة بتمرير بيانات عدد صحيح "حسب القيمة":

القائمة 1:

01 package main
02
03 func main() {
04
05    // Declare variable of type int with a value of 10.
06    count := 10
07
08    // Display the "value of" and "address of" count.
09    println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
10
11    // Pass the "value of" the count.
12    increment(count)
13
14    println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")
15 }
16
17 //go:noinline
18 func increment(inc int) {
19
20    // Increment the "value of" inc.
21    inc++
22    println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")
23 }

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

المكدس مهم لأنه يوفر مساحة ذاكرة فعلية لحدود الإطار المعطاة لكل وظيفة على حدة. في الوقت الذي يؤدي فيه الغوروتين الرئيسي الوظيفة الرئيسية في القائمة 1 ، ستظهر مجموعة البرامج (على مستوى عالٍ جدًا) على النحو التالي:

الشكل 1:



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

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

عناوين


تُستخدم المتغيرات لتعيين اسم لخلية ذاكرة معينة لتحسين إمكانية قراءة التعليمات البرمجية ومساعدتك على فهم البيانات التي تعمل معها. إذا كان لديك متغير ، فلديك قيمة في الذاكرة ، وإذا كانت لديك قيمة في الذاكرة ، فيجب أن يكون لها عنوان. في السطر 09 ، تستدعي الوظيفة الرئيسية وظيفة println المدمجة لعرض "القيمة" و "العنوان" لمتغير العدد.

الإدراج 2:

09    println("count:\tValue Of[", count, "]\tAddr Of[", &count, "]")

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

سرد 3:

count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]

استدعاء دالة


بعد ذلك ، في السطر 12 ، تستدعي الوظيفة الرئيسية وظيفة الزيادة.

الإدراج 4:

12    increment(count)

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

القائمة 5:

18 func increment(inc int) {

إذا نظرت مرة أخرى إلى استدعاء دالة الزيادة في السطر 12 ، فسترى أن الرمز يمر "قيمة" عدد المتغيرات. سيتم نسخ هذه القيمة ونقلها ووضعها في إطار جديد لدالة الزيادة. تذكر أن وظيفة الزيادة يمكنها فقط القراءة والكتابة في الذاكرة في إطارها الخاص ، لذا فهي تحتاج إلى المتغير inc للحصول على نسختها الخاصة من قيمة العداد المرسلة وتخزينها والوصول إليها.

قبل أن يبدأ تنفيذ التعليمات البرمجية داخل دالة الزيادة ، ستظهر حزمة البرنامج (على مستوى عالٍ جدًا) على النحو التالي:

الشكل 2:



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

باقي الكود داخل زيادات الزيادة ويعرض "قيمة" و "عنوان" المتغير inc.

الإدراج 6:

21    inc++
22    println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]")

يجب أن يبدو إخراج السطر 22 في الملعب شيئًا كالتالي:

سرد 7:

inc:    Value Of[ 11 ]  Addr Of[ 0x10429f98 ]

إليك ما يبدو عليه المكدس بعد تنفيذ نفس أسطر التعليمات البرمجية:

الشكل 3:



بعد تنفيذ السطور 21 و 22 ، تنتهي وظيفة الزيادة وتعيد التحكم إلى الوظيفة الرئيسية. ثم تعرض الوظيفة الرئيسية مرة أخرى "القيمة" و "العنوان" لعدد المتغيرات المحلية في السطر 14.

القائمة 8:

14    println("count:\tValue Of[",count, "]\tAddr Of[", &count, "]")

يجب أن يبدو الإخراج الكامل للبرنامج في الملعب شيئًا مثل هذا:

القائمة 9:

count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]
inc:    Value Of[ 11 ]  Addr Of[ 0x10429f98 ]
count:  Value Of[ 10 ]  Addr Of[ 0x10429fa4 ]

قيمة العد في الإطار الرئيسي هي نفسها قبل استدعاء الزيادة وبعده.

العودة من الوظائف


ما الذي يحدث بالفعل للذاكرة على المكدس عندما تخرج الوظيفة ويعود التحكم إلى وظيفة الاستدعاء؟ الجواب القصير لا شيء. فيما يلي الشكل الذي يبدو عليه المكدس بعد إرجاع دالة الزيادة:

الشكل 4:



يبدو المكدس تمامًا كما في الشكل 3 ، باستثناء أن الإطار المرتبط بوظيفة الزيادة يعتبر الآن ذاكرة غير صالحة. هذا لأن إطار main نشط الآن. ظلت الذاكرة التي تم إنشاؤها لوظيفة الزيادة كما هي.

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

مشاركة القيمة


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

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

أنواع المؤشرات


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

جميع أنواع المؤشرات لها صفتان متطابقتان. أولاً ، يبدأون بالحرف *. ثانيًا ، لديهم جميعًا نفس الحجم في الذاكرة وتمثيل يشغل 4 أو 8 بايت يمثل العنوان. على بنيات 32 بت (على سبيل المثال ، في الملعب) تتطلب المؤشرات 4 بايت من الذاكرة ، وبنى 64 بت (على سبيل المثال ، جهاز الكمبيوتر الخاص بك) تتطلب 8 بايت من الذاكرة.

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

الوصول إلى الذاكرة غير المباشرة


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

القائمة 10:

01 package main
02
03 func main() {
04
05    // Declare variable of type int with a value of 10.
06    count := 10
07
08    // Display the "value of" and "address of" count.
09    println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]")
10
11    // Pass the "address of" count.
12    increment(&count)
13
14    println("count:\tValue Of[", count, "]\t\tAddr Of[", &count, "]")
15 }
16
17 //go:noinline
18 func increment(inc *int) {
19
20    // Increment the "value of" count that the "pointer points to". (dereferencing)
21    *inc++
22    println("inc:\tValue Of[", inc, "]\tAddr Of[", &inc, "]\tValue Points To[", *inc, "]")
23 }

تم إجراء ثلاثة تغييرات مثيرة للاهتمام للبرنامج الأصلي. التغيير الأول في السطر 12:

الإدراج 11:

12    increment(&count)

هذه المرة ، في السطر 12 ، لا ينسخ الرمز ويمرر "القيمة" إلى متغير العد ، ولكنه يمر "العنوان" بدلاً من متغير العد. الآن يمكنك أن تقول: "أنا أشارك" عدد المتغيرات مع زيادة الوظيفة. هذا ما يقوله العامل & - "حصة".

افهم أن هذا لا يزال "يمر بالقيمة" ، والفرق الوحيد هو أن القيمة التي تمر بها هي العنوان وليس العدد الصحيح. العناوين هي أيضا قيم. هذا هو ما يتم نسخه وتمريره عبر حدود الإطار لاستدعاء الوظيفة.

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

سرد 12:

18 func increment(inc *int) {

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

فيما يلي شكل المكدس بعد استدعاء دالة الزيادة:

الشكل 5:



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

الآن ، باستخدام متغير المؤشر ، يمكن أن تؤدي الوظيفة عملية قراءة وتغيير غير مباشرة لمتغير العد الموجود داخل الإطار الرئيسي.

الإدراج 13:

21    *inc++

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

يوضح الشكل 6 شكل المكدس بعد السطر 21.

الشكل 6: هذا



هو الناتج النهائي من هذا البرنامج:

القائمة 14:

count:  Value Of[ 10 ]              Addr Of[ 0x10429fa4 ]
inc:    Value Of[ 0x10429fa4 ]      Addr Of[ 0x10429f98 ]   Value Points To[ 11 ]
count:  Value Of[ 11 ]              Addr Of[ 0x10429fa4 ]

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

متغيرات المؤشر ليست خاصة


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

استنتاج


يصف هذا المنشور الغرض من المؤشرات وتشغيل المكدس وآليات المؤشرات في Go. هذه هي الخطوة الأولى في فهم الميكانيكا ، ومبادئ التصميم ، وتقنيات الاستخدام اللازمة لكتابة كود متماسك وقابل للقراءة.

في النهاية ، إليك ما تعلمته:

  • يتم تنفيذ الوظائف داخل حدود الإطار ، والتي توفر مساحة ذاكرة منفصلة لكل وظيفة مقابلة.
  • عندما يتم استدعاء دالة ، يحدث انتقال بين إطارين.
  • ميزة نقل البيانات "بالقيمة" هي سهولة القراءة.
  • المكدس مهم لأنه يوفر مساحة ذاكرة فعلية لحدود الإطار المعطاة لكل وظيفة على حدة.
  • كل ذاكرة المكدس الموجودة أسفل الإطار النشط غير صالحة ، ولكن الذاكرة من الإطار النشط وما فوق صالحة.
  • , .
  • , , .
  • — , , .
  • , , , , .
  • - , .
  • - - , , . , .

All Articles