ثنائية لماذا لا تدعم وظائف دعم C++ المصفوفات؟



امثلة على المصفوفات بلغة c++ pdf (6)

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

من مناقشة هنا:

http://forum.codecall.net/c-c/32457-function-return-array-c.html

https://ffff65535.com

تمكّنك بعض اللغات من الإعلان عن وظيفة تقوم بإرجاع صفيف مثل وظيفة عادية ، مثل Java:

public String[] funcarray() {
   String[] test = new String[]{"hi", "hello"};
   return test;
}

لماذا لا تدعم C ++ شيئًا مثل int[] funcarray(){} ؟ يمكنك إرجاع صفيف ، ولكن من المتاعب الحقيقية لجعل هذه الوظيفة. وأيضا ، سمعت في مكان ما أن الأوتار هي مجرد صفائف شار. حتى إذا كان بإمكانك إرجاع سلسلة في C ++ ، فلماذا لا تكون مصفوفة؟


"لماذا لا يدعم C ++ أمرًا مثل": لأنه لن يكون له أي معنى. في اللغات المرجعية مثل JAVA أو PHP ، تعتمد إدارة الذاكرة على تجميع البيانات المهملة. يتم تحرير أجزاء الذاكرة التي ليس لها مراجع (لا يوجد متغير في برنامجك) بعد الآن. في هذا السياق ، يمكنك تخصيص الذاكرة ، وتمرير المرجع حولها بحرص.

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

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

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


إرجاع std::vector<> بدلاً من صفيف. بشكل عام ، لا تعمل الصفائف بشكل جيد مع C ++ ، ويجب تجنبها بشكل عام.

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


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

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

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

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

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


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

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

int array[n];

ينشئ 4 * n بايت من الذاكرة (على نظام 32 بت) على بنية تخزين العناصر المرتبطة بنطاق أي كتلة تعليمات برمجية يجعل الإعلان. بالمقابل،

int* array = (int*) malloc(sizeof(int)*n);

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

int n = 4;
printf("%d", n);

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

int[] foo(args){
    result[n];
    // Some code
    return result;
}

تكمن المشكلة هنا في أن النتيجة يتم تقييمها لعنوان العنصر 0 من المصفوفة. ولكن عند محاولة الوصول إلى هذه الذاكرة من خارج هذه الدالة (عبر قيمة الإرجاع) ، يكون لديك مشكلة لأنك تحاول الوصول إلى ذاكرة ليست في النطاق الذي تعمل به (مكدس استدعاء الدالة). إذاً ، الطريقة التي نتناول بها هذا هي مع معيار "المرور بالرجوع" jiggery-pokery:

int* foo(args){
    int* result = (int*) malloc(sizeof(int)*n));
    // Some code
    return result;
}

مازلنا نحصل على عنوان ذاكرة يشير إلى العنصر 0 من المصفوفة ، ولكن الآن لدينا الوصول إلى تلك الذاكرة.

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

هذا يقودنا إلى C ++. السبب الرئيسي في اختراع C ++ هو أن Bjarne Stroustrup كان يقوم بتجربة Simula (أساسا OOPL الأصلي) أثناء عمله الدكتوراه ، ويعتقد أنها كانت رائعة من الناحية النظرية ، لكنه لاحظ أنها تؤدي بشكل رهيب إلى حد ما. وهكذا بدأ العمل على ما كان يُدعى "ج" بالفصول الدراسية ، والتي تمت إعادة تسميتها إلى لغة ++ C. في القيام بذلك ، كان هدفه جعل لغة البرمجة التي أخذت بعض من أفضل الميزات من سيمولا لكنها بقيت قوية وسريعة. اختار أن يمد C بسبب أدائه الأسطوري بالفعل ، وكانت إحدى المقايضات أنه اختار عدم تنفيذ إدارة الذاكرة التلقائية أو جمع القمامة على نطاق واسع مثل غيره من OOPL. تعمل إعادة الصفيف من إحدى فئات القوالب لأنك تستخدم فصلًا جيدًا. ولكن إذا كنت تريد إرجاع صفيف C ، فعليك القيام بذلك بالطريقة C. بمعنى آخر ، يدعم C ++ إرجاع صفيف بالضبط بنفس الطريقة التي يقوم بها Java؛ لا يفعل كل هذا العمل من أجلك. لأن المتأنق الدنماركي يعتقد أنه سيكون بطيئًا جدًا.


وقال آخرون أنه في C ++ ، استخدم واحد ناقلات <> بدلا من المصفوفات الموروثة من C.

إذن لماذا لا يسمح C ++ بإرجاع C صفائف؟ لأن C لا.

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





function