ما هي مخاطر C++ التي يجب أن أتجنبها؟[مغلق]

StackOverflow https://stackoverflow.com/questions/30373

  •  09-06-2019
  •  | 
  •  

سؤال

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

هل هناك أي مخاطر شائعة أخرى يجب تجنبها في لغة C++؟

هل كانت مفيدة؟

المحلول

القائمة المختصرة قد تكون:

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

بالطبع، لا تعد RAII والمؤشرات المشتركة والتشفير البسيط خاصة بـ C++، ولكنها تساعد في تجنب المشكلات التي تظهر بشكل متكرر عند التطوير في اللغة.

ومن الكتب الممتازة في هذا الموضوع:

  • C++ الفعالة - سكوت مايرز
  • C++ أكثر فعالية - سكوت مايرز
  • معايير الترميز C++ - سوتر وألكساندرسكو
  • الأسئلة الشائعة حول C++ - كلاين

لقد ساعدتني قراءة هذه الكتب أكثر من أي شيء آخر على تجنب هذا النوع من المزالق التي تسأل عنها.

نصائح أخرى

المزالق بالترتيب التنازلي لأهميتها

بادئ ذي بدء، يجب عليك زيارة الحائز على جائزة الأسئلة الشائعة حول لغة C++.لديها العديد من الإجابات الجيدة للمزالق.إذا كان لديك المزيد من الأسئلة، قم بزيارة ##c++ على irc.freenode.org في آي آر سي.نحن سعداء بمساعدتك، إذا استطعنا.لاحظ أن جميع المزالق التالية مكتوبة في الأصل.لا يتم نسخها فقط من مصادر عشوائية.


delete[] على new, delete على new[]

حل:يؤدي القيام بما سبق إلى سلوك غير محدد:كل شيء يمكن أن يحدث.افهم الكود الخاص بك وما يفعله، ودائمًا delete[] ماذا new[], ، و delete ماذا new, ، فلن يحدث ذلك.

استثناء:

typedef T type[N]; T * pT = new type; delete[] pT;

أنت بحاجه إلى delete[] على الرغم من أنك new, ، منذ أن قمت بتحديث مصفوفة.لذلك إذا كنت تعمل مع typedef, ، خذ عناية خاصة.


استدعاء وظيفة افتراضية في المنشئ أو المدمر

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


الاتصال delete أو delete[] على مؤشر محذوف بالفعل

حل:قم بتعيين 0 لكل مؤشر تقوم بحذفه.الاتصال delete أو delete[] على مؤشر فارغ لا يفعل شيئا.


أخذ حجم المؤشر، عند حساب عدد عناصر "المصفوفة".

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


استخدام مصفوفة كما لو كانت مؤشرًا.وهكذا باستخدام T ** لمجموعة ثنائية الأبعاد.

حل:يرى هنا لماذا هم مختلفون وكيف تتعامل معهم.


الكتابة إلى سلسلة حرفية: char * c = "hello"; *c = 'B';

حل:قم بتخصيص مصفوفة تمت تهيئتها من بيانات السلسلة الحرفية، ثم يمكنك الكتابة إليها:

char c[] = "hello"; *c = 'B';

الكتابة إلى سلسلة حرفية هي سلوك غير محدد.على أي حال، التحويل أعلاه من سلسلة حرفية إلى char * تم إهماله.لذلك من المحتمل أن يحذر المترجمون إذا قمت بزيادة مستوى التحذير.


إنشاء الموارد، ثم نسيان تحريرها عندما يحدث شيء ما.

حل:استخدم المؤشرات الذكية مثل std::unique_ptr أو std::shared_ptr كما أشارت الإجابات الأخرى.


تعديل كائن مرتين كما في هذا المثال: i = ++i;

حل:ما ورد أعلاه كان من المفترض أن يعين ل i قيمة ال i+1.لكن ما يفعله غير محدد.بدلا من الزيادة i وتعيين النتيجة، فإنه يتغير i على الجانب الأيمن أيضًا.يعد تغيير كائن بين نقطتي تسلسل سلوكًا غير محدد.تشمل نقاط التسلسل ||, &&, comma-operator, semicolon و entering a function (قائمة غير حصرية!).قم بتغيير الكود إلى ما يلي لجعله يعمل بشكل صحيح: i = i + 1;


قضايا متنوعة

نسيان تدفق التدفقات قبل استدعاء وظيفة الحظر مثل sleep.

حل:قم بتدفق الدفق عن طريق التدفق أيضًا std::endl بدلاً من \n أو عن طريق الاتصال stream.flush();.


الإعلان عن دالة بدلاً من المتغير.

حل:تنشأ المشكلة لأن المترجم يفسر على سبيل المثال

Type t(other_type(value));

كإعلان وظيفة من وظيفة t عودة Type ولها معلمة من النوع other_type من اتصل value.يمكنك حلها عن طريق وضع قوسين حول الوسيطة الأولى.الآن تحصل على متغير t من النوع Type:

Type t((other_type(value)));

استدعاء وظيفة كائن حر تم الإعلان عنه فقط في وحدة الترجمة الحالية (.cpp ملف).

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

House & getTheHouse() { static House h; return h; }

سيؤدي ذلك إلى إنشاء الكائن عند الطلب ويترك لك كائنًا تم إنشاؤه بالكامل في الوقت الذي تقوم فيه باستدعاء الوظائف عليه.


تحديد القالب في .cpp الملف، أثناء استخدامه في ملف .cpp ملف.

حل:دائمًا تقريبًا سوف تحصل على أخطاء مثل undefined reference to ....ضع جميع تعريفات القالب في رأس الصفحة، بحيث يتمكن المترجم بالفعل من إنتاج الكود المطلوب عندما يستخدمها.


static_cast<Derived*>(base); إذا كانت القاعدة مؤشرًا لفئة أساسية افتراضية لـ Derived.

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


dynamic_cast<Derived*>(ptr_to_base); إذا كانت القاعدة غير متعددة الأشكال

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


جعل وظيفتك تقبل T const **

حل:قد تعتقد أن هذا أكثر أمانًا من الاستخدام T **, ولكن في الواقع سوف يسبب الصداع للأشخاص الذين يريدون المرور T**:المعيار لا يسمح بذلك.ويعطي مثالا واضحا على سبب عدم السماح به:

int main() {
    char const c = ’c’;
    char* pc;
    char const** pcc = &pc; //1: not allowed
    *pcc = &c;
    *pc = ’C’; //2: modifies a const object
}

تقبل دائما T const* const*; بدلاً من.

هناك موضوع آخر (مغلق) حول المزالق حول لغة C++، لذا سيجدها الأشخاص الذين يبحثون عنها، وهو سؤال Stack Overflow مخاطر C++.

يجب أن يكون لدى البعض كتب C++ التي ستساعدك على تجنب مخاطر C++ الشائعة:

فعالة C ++
أكثر فعالية C++
المحكمة الخاصة بلبنان فعالة

يشرح كتاب STL الفعال ناقل مشكلة منطقية :)

لدى برايان قائمة رائعة:أود أن أضيف "ضع علامة واضحة دائمًا على مُنشئات الوسيطة الفردية (باستثناء تلك الحالات النادرة التي تريد فيها الإرسال التلقائي)."

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

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

استخدم static_cast، وdynamic_cast، وconst_cast، وreinterpret_cast بدلاً من القوالب ذات النمط C.على عكس طاقم الممثلين على النمط C، سيُعلمونك إذا كنت تطلب حقًا نوعًا مختلفًا من طاقم الممثلين عما تعتقد أنك تطلبه.وهي تبرز بصريًا، لتنبه القارئ إلى حدوث عملية التمثيل.

صفحة الويب مخاطر C++ يغطي سكوت ويلر بعض المخاطر الرئيسية لـ C++.

مسألتان أتمنى لو أنني لم أتعلمهما بالطريقة الصعبة:

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

(2) كن حذرًا عند عمليات التهيئة - (أ) تجنب مثيلات الفئة مثل العالمية/الإحصائيات؛و (ب) حاول تهيئة جميع متغيرات الأعضاء لديك إلى قيمة آمنة في ctor، حتى لو كانت قيمة تافهة مثل NULL للمؤشرات.

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

لقد ذكرت ذلك بالفعل عدة مرات، ولكن كتب سكوت مايرز فعالة C ++ و المحكمة الخاصة بلبنان فعالة يستحقون حقًا وزنهم ذهبًا للمساعدة في لغة C++.

تعال للتفكير في الأمر، ستيفن ديوهورست C++ مسكتك يعد أيضًا مصدرًا ممتازًا "من الخنادق".لقد ساعدني البند الخاص به بشأن طرح الاستثناءات الخاصة بك وكيفية بنائها في مشروع واحد.

باستخدام C++ مثل C.وجود دورة إنشاء وإصدار في الكود.

في C++، هذا ليس استثناءً آمنًا وبالتالي قد لا يتم تنفيذ الإصدار.في لغة C++ نستخدم رايي لحل هذه المشكلة.

يجب أن يتم تغليف جميع الموارد التي تم إنشاؤها وإصدارها يدويًا في كائن بحيث يتم تنفيذ هذه الإجراءات في المُنشئ/المدمر.

// C Code
void myFunc()
{
    Plop*   plop = createMyPlopResource();

    // Use the plop

    releaseMyPlopResource(plop);
}

في لغة C++، يجب أن يتم تغليف هذا في كائن:

// C++
class PlopResource
{
    public:
        PlopResource()
        {
            mPlop=createMyPlopResource();
            // handle exceptions and errors.
        }
        ~PlopResource()
        {
             releaseMyPlopResource(mPlop);
        }
    private:
        Plop*  mPlop;
 };

void myFunc()
{
    PlopResource  plop;

    // Use the plop
    // Exception safe release on exit.
}

الكتاب C++ مسكتك قد يكون مفيدا.

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

  • virtual وظائف في البنائين ليست كذلك.

  • لا تنتهك ODR (قاعدة التعريف الواحدة), ، هذا هو الغرض من مساحات الأسماء المجهولة (من بين أشياء أخرى).

  • يعتمد ترتيب تهيئة الأعضاء على الترتيب الذي تم الإعلان عنه.

    class bar {
        vector<int> vec_;
        unsigned size_; // Note size_ declared *after* vec_
    public:
        bar(unsigned size)
            : size_(size)
            , vec_(size_) // size_ is uninitialized
            {}
    };
    
  • القيم الافتراضية و virtual لها دلالات مختلفة.

    class base {
    public:
        virtual foo(int i = 42) { cout << "base " << i; }
    };
    
    class derived : public base {
    public:
        virtual foo(int i = 12) { cout << "derived "<< i; }
    };
    
    derived d;
    base& b = d;
    b.foo(); // Outputs `derived 42`
    

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

الدفع Boost.org.فهو يوفر الكثير من الوظائف الإضافية، وخاصة تطبيقات المؤشر الذكية الخاصة بهم.

PRQA لديها معيار ترميز C++ ممتاز ومجاني استنادًا إلى كتب سكوت مايرز وبيارن ستروستروب وهيرب سوتر.فهو يجمع كل هذه المعلومات معًا في مستند واحد.

  1. عدم قراءة الأسئلة الشائعة حول C++ Lite.وهو يفسر العديد من الممارسات السيئة (والجيدة!).
  2. عدم استخدام يعزز.ستوفر على نفسك الكثير من الإحباط من خلال الاستفادة من Boost حيثما أمكن ذلك.

كن حذرًا عند استخدام المؤشرات الذكية وفئات الحاويات.

يتجنب الطبقات الزائفة وشبه الطبقات...الإفراط في التصميم في الأساس.

نسيان تحديد مدمر الطبقة الأساسية الظاهري.وهذا يعني أن الدعوة delete على قاعدة* لن ينتهي الأمر بتدمير الجزء المشتق.

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

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

  • بليزباستا.وهذا أمر عظيم أراه كثيرًا..

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

  • تشغيل الأخطاء الفردية for الحلقات / الوصول إلى المصفوفة.

  • عدم تنظيف رمز الكائن بشكل صحيح عند بدء الفودو.

  • static_cast مكتئب على فئة أساسية افتراضية

ليس حقيقيًا...والآن عن مفهومي الخاطئ:وأعتقد أن A في ما يلي كانت هناك فئة أساسية افتراضية في حين أنها ليست كذلك في الواقع؛إنه، وفقًا لـ 10.3.1، أ فئة متعددة الأشكال.استخدام static_cast هنا يبدو أن تكون على ما يرام.

struct B { virtual ~B() {} };

struct D : B { };

باختصار، نعم، هذا مأزق خطير.

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

class SomeClass
{
    ...
    void DoSomething()
    {
        ++counter;    // crash here!
    }
    int counter;
};

void Foo(SomeClass & ref)
{
    ...
    ref.DoSomething();    // if DoSomething is virtual, you might crash here
    ...
}

void Bar(SomeClass * ptr)
{
    Foo(*ptr);    // if ptr is NULL, you have created an invalid reference
                  // which probably WILL NOT crash here
}

نسيان أ & وبالتالي إنشاء نسخة بدلاً من المرجع.

حدث هذا معي مرتين وبطرق مختلفة:

  • كانت إحدى الحالات موجودة في قائمة الوسائط، مما أدى إلى وضع كائن كبير على المكدس مما أدى إلى تجاوز سعة المكدس وتعطل النظام المضمن.

  • لقد نسيت & على متغير مثيل، مع التأثير الذي تم نسخ الكائن.بعد التسجيل كمستمع للنسخة، تساءلت لماذا لم أتلق أي رد اتصال من الكائن الأصلي.

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

القصد هو (x == 10):

if (x = 10) {
    //Do something
}

اعتقدت أنني لن أرتكب هذا الخطأ بنفسي، لكنني فعلت ذلك مؤخرًا.

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

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

#include <boost/shared_ptr.hpp>
class A {
public:
  void nuke() {
     boost::shared_ptr<A> (this);
  }
};

int main(int argc, char** argv) {
  A a;
  a.nuke();
  return(0);
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top