الربط الداخلي والخارجي في C ++

يوم جيد للجميع!

نقدم لك ترجمة لمقال مثير للاهتمام تم إعداده لك كجزء من الدورة التدريبية "C ++ Developer" . نأمل أن تكون مفيدة ومثيرة للاهتمام بالنسبة لك ، وكذلك للمستمعين لدينا.

دعنا نذهب.

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

باختصار

يتم تضمين وحدة الترجمة (.c / .cpp) وجميع ملفات الرأس الخاصة بها (.h / .hpp) في وحدة الترجمة. إذا كان كائن أو وظيفة مرتبطة داخليًا داخل وحدة ترجمة ، فسيكون هذا الرمز مرئيًا للرابط فقط داخل وحدة الترجمة هذه. إذا كان للكائن أو الوظيفة ارتباط خارجي ، فسيتمكن الرابط من رؤيته عند معالجة وحدات الترجمة الأخرى. باستخدام الكلمة الأساسية الثابتة في مساحة الاسم العالمية يعطي الحرف الربط الداخلي. الكلمة الخارجية extern يعطي ربط خارجي.
يوفر المحول البرمجي الافتراضي الأحرف الارتباطات التالية:

  • متغيرات عالمية غير const - الربط الخارجي؛
  • Const المتغيرات العالمية - الربط الداخلي ؛
  • وظائف - ربط خارجي.



الأساسيات

أولاً ، دعنا نتحدث عن مفهومين بسيطين ضروريين لمناقشة الربط.

  • الفرق بين الإعلان والتعريف ؛
  • وحدات البث.

انتبه أيضًا إلى الأسماء: سنستخدم مفهوم "الرمز" عندما يتعلق الأمر بأي "كيان رمز" يعمل به الرابط ، على سبيل المثال مع متغير أو وظيفة (أو مع فئات / هياكل ، لكننا لن نركز عليها).

إعلان مقابل التعريف

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

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

وظائف

الفرق بين تعريف وإعلان وظيفة واضح للغاية.

int f(); //  int f() { return 42; } //  

المتغيرات

مع المتغيرات ، الأمر مختلف قليلاً. الإعلان والتعريف عادة لا يتم مشاركتهما. الشيء الرئيسي هو:

 int x; 

ليس فقط يعلن x ، ولكن أيضا يعرف ذلك. هذا بسبب استدعاء المُنشئ الافتراضي int. (في C ++ ، بخلاف Java ، لا يقوم مُنشئ الأنواع البسيطة (مثل int) بتهيئة القيمة إلى 0 افتراضيًا. في المثال أعلاه ، سيكون x مساوياً لأي من البيانات المهملة الموجودة في عنوان الذاكرة المخصص بواسطة المترجم).

ولكن يمكنك فصل تعريف المتغير وتعريفه بوضوح باستخدام الكلمة الأساسية الخارجية.

 extern int x; //  int x = 42; //  

ومع ذلك ، عند تهيئة الإعلان extern وإضافته ، يتحول التعبير إلى تعريف وتصبح الكلمة الرئيسية الخارجية بلا فائدة.

 extern int x = 5; //   ,   int x = 5; 

معاينة الإعلان

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

مثال

افترض أن لدينا إعلان دالة (يُسمى النموذج الأولي) لـ f يأخذ كائنًا من النوع Class حسب القيمة:

 // file.hpp void f(Class object); 

تشمل على الفور تعريف Class - ساذجة. لكن بما أننا أعلنا للتو ، فهذا يكفي لإعطاء المترجم إعلانًا عن Class . وبالتالي ، سيكون المترجم قادرًا على التعرف على الوظيفة من خلال النموذج الأولي الخاص بها ، وسوف نكون قادرين على التخلص من اعتماد file.hpp على الملف الذي يحتوي على تعريف Class ، يقول class.hpp:

 // file.hpp class Class; void f(Class object); 

لنفترض أن file.hpp موجود في 100 ملف آخر. ودعونا نقول إننا نغير تعريف الفصل في class.hpp. إذا قمت بإضافة class.hpp إلى file.hpp ، ستحتاج إلى إعادة ترجمة جميع الملفات المائة التي تحتوي عليها. بفضل الإعلان الأولي للفئة ، ستكون الملفات الوحيدة التي تتطلب إعادة تجميع هي class.hpp و file.hpp (على افتراض أن f معرّف هناك).

تواتر الاستخدام

الفرق المهم بين الإعلان والتعريف هو أنه يمكن الإعلان عن الرمز عدة مرات ، ولكن يتم تعريفه مرة واحدة فقط. حتى تتمكن من التصريح المسبق لوظيفة أو فئة ما تشاء ، ولكن يمكن أن يكون هناك تعريف واحد فقط. وهذا ما يسمى قاعدة تعريف واحد . في C ++ ، يعمل التالي:

 int f(); int f(); int f(); int f(); int f(); int f(); int f() { return 5; } 

وهذا لا يعمل:

 int f() { return 6; } int f() { return 9; } 

وحدات البث

يعمل المبرمجون عادة مع ملفات الرأس وملفات التنفيذ. ولكن ليس المجمعين - فهم يعملون مع وحدات الترجمة (وحدات الترجمة ، للغة القصيرة - TU) ، والتي تسمى أحيانًا بوحدات الترجمة. تعريف هذه الوحدة بسيط للغاية - أي ملف يتم نقله إلى المترجم بعد معالجته الأولية. للتأكد من دقته ، هذا ملف ناتج عن عمل معالج ماكرو ملحق أولي يتضمن تعليمة برمجية المصدر ، والتي تعتمد على تعبيرات #ifndef و # #ifndef ، ولصق نسخة من جميع ملفات #include .

الملفات التالية متوفرة:

header.hpp:

 #ifndef HEADER_HPP #define HEADER_HPP #define VALUE 5 #ifndef VALUE struct Foo { private: int ryan; }; #endif int strlen(const char* string); #endif /* HEADER_HPP */ 

program.cpp:

 #include "header.hpp" int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + VALUE; } 

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

 int strlen(const char* string); int strlen(const char* string) { int length = 0; while(string[length]) ++length; return length + 5; } 

الاتصالات

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

التواصل الخارجي

عندما يكون للرمز (متغير أو وظيفة) اتصال خارجي ، يصبح مرئيًا للرابطين من الملفات الأخرى ، أي "عالميًا" المرئي ، ويمكن لجميع وحدات الترجمة الوصول إليه. هذا يعني أنه يجب عليك تعريف مثل هذا الرمز في مكان محدد لوحدة ترجمة واحدة ، عادةً في ملف التنفيذ (.c / .cpp) ، بحيث يكون له تعريف مرئي واحد فقط. إذا حاولت تعريف الرمز في وقت واحد في نفس الوقت الذي يتم فيه الإعلان عن الرمز ، أو إذا وضعت التعريف في ملف للإعلان ، فإنك تخاطر بغضب الرابط. تؤدي محاولة إضافة ملف إلى أكثر من ملف تنفيذ واحد إلى إضافة تعريف إلى أكثر من وحدة ترجمة - سيبكي رابطك.

تعلن الكلمة الأساسية الخارجية في C و C ++ (صراحة) أن حرفاً لديه اتصال خارجي.

 extern int x; extern void f(const std::string& argument); 

كل من الشخصيات لديها اتصال خارجي. تمت الإشارة أعلاه إلى أن المتغيرات العمومية const لها ارتباط داخلي افتراضيًا ، والمتغيرات العالمية غير const لها ارتباط خارجي. هذا يعني أن int x ؛ - مثل extern int x ، أليس كذلك؟ ليس حقا int x ؛ مماثل في الواقع إلى extern int {{} ؛ (باستخدام بناء جملة تهيئة التهيئة العامة / الأقواس لتجنب التحليل غير السار (التحليل الأكثر حرجًا)) ، بما في ذلك int x ؛ ليس فقط تعلن ، ولكن أيضا يحدد س. لذلك ، لا تضيف extern إلى int x؛ على المستوى العالمي سيء مثل تحديد المتغير عند إعلانه خارجيًا:

 int x; //   ,   extern int x{}; //      . extern int x; //      ,   

مثال سيء

دعنا نعلن عن وظيفة مع وصلة خارجية في file.hpp وتحديدها هناك:

 // file.hpp #ifndef FILE_HPP #define FILE_HPP extern int f(int x); /* ... */ int f(int) { return x + 1; } /* ... */ #endif /* FILE_HPP */ 

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

 // file.hpp #ifndef FILE_HPP #define FILE_HPP int f(int) { return x + 1; } #endif /* FILE_HPP */ 

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

دعونا نرى لماذا هذا لا يستحق كل هذا العناء. الآن لدينا ملفان للتنفيذ: a.cpp و b.cpp ، وكلاهما مدرج في file.hpp:

 // a.cpp #include "file.hpp" /* ... */ 


 // b.cpp #include "file.hpp" /* ... */ 

والآن ، دع المحول البرمجي يعمل وإنشاء وحدتي ترجمة لملفي التطبيق أعلاه (تذكر أن #include حرفيًا تعني copy / لصق):

 // TU A, from a.cpp int f(int) { return x + 1; } /* ... */ 

 // TU B, from b.cpp int f(int) { return x + 1; } /* ... */ 

عند هذه النقطة ، يتدخل رابط (يحدث الربط بعد التحويل البرمجي). يأخذ الرابط الحرف f ويبحث عن تعريف. اليوم هو محظوظ ، ويجد ما يصل إلى اثنين! واحد في وحدة الترجمة A ، والآخر في B. الرابط يتجمد بالسعادة ويخبرك بشيء من هذا القبيل:

 duplicate symbol __Z1fv in: /path/to/ao /path/to/bo 

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

استخدام

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

 #define CLK 1000000 

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

 // global.hpp namespace Global { extern unsigned int clock_rate; } // global.cpp namespace Global { unsigned int clock_rate = 1000000; } 

(سيحتاج مبرمج C ++ الحديث إلى استخدام القيم الحرفية للفصل: غير موقعة int clock_rate = 1'000'000؛)

الاتصال الداخلي

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

لإعلان رمز مترابط ، توجد الكلمة الأساسية الثابتة في C و C ++. يختلف هذا الاستخدام عن استخدام ثابت في الفئات والوظائف (أو بشكل عام ، في أي كتل).

مثال

هنا مثال:

header.hpp:

 static int variable = 42; 

file1.hpp:

 void function1(); 

file2.hpp:

 void function2(); 

file1.cpp:

 #include "header.hpp" void function1() { variable = 10; } 


file2.cpp:

 #include "header.hpp" void function2() { variable = 123; } 

main.cpp:

 #include "header.hpp" #include "file1.hpp" #include "file2.hpp" #include <iostream> auto main() -> int { function1(); function2(); std::cout << variable << std::endl; } 

تحصل كل وحدة ترجمة ، بما في ذلك header.hpp ، على نسخة فريدة من المتغير ، بسبب اتصالها الداخلي. هناك ثلاث وحدات ترجمة:

  1. file1.cpp
  2. file2.cpp
  3. main.cpp

عندما يتم استدعاء function1 ، تحصل نسخة من متغير file1.cpp على القيمة 10. عند استدعاء function2 ، تحصل نسخة من متغير file2.cpp على القيمة 123. ومع ذلك ، لا تتغير القيمة التي يتم إرجاعها في main.cpp وتبقى مساوية 42.

مساحات الأسماء مجهولة

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

في أي حال ، هذا هو:

 namespace { int variable = 0; } 

هل (تقريبا) نفس الشيء مثل:

 static int variable = 0; 

استخدام

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

ومع ذلك ، يمكن استخدام الاتصالات الداخلية لإخفاء وحدة الترجمة من المنطقة العالمية لوظائف المساعد المحلي. افترض أن هناك وظيفة مساعد foo في file1.hpp التي تستخدمها في file1.cpp. في نفس الوقت ، لديك وظيفة foo في file2.hpp المستخدمة في file2.cpp. تختلف foo الأولى والثانية عن بعضها البعض ، ولكن لا يمكنك التوصل إلى أسماء أخرى. لذلك ، يمكنك أن تعلن لهم ثابت. إذا لم تقم بإضافة كل من file1.hpp و file2.hpp إلى وحدة الترجمة نفسها ، فسيؤدي ذلك إلى إخفاء foo عن بعضها البعض. إذا لم يتم ذلك ، فسيكون لديهم اتصال خارجي ضمنيًا وسيصادف تعريف foo الأول تعريف التعريف ، مما يؤدي إلى حدوث خطأ في رابط حول انتهاك قاعدة التعريف الواحد.

النهاية

يمكنك دائمًا ترك تعليقاتك و / أو أسئلتك هنا أو زيارتنا في يوم مفتوح.

Source: https://habr.com/ru/post/ar432834/


All Articles