هناك العديد من العقبات والأخطاء التي قد يتعرض لها مطورو لغة البرمجة ++C، يمكن لهذه العقبات أن تصعب عملية البرمجة وترفع تكاليفها، فتعلم قواعد البرمجة ووجود مهارات في لغات البرمجة الأخرى مثل جافا Java وسي شارب #C ليست كافية وحدها للاستفادة الكاملة من إمكانيات لغة ++C وتجنب الأخطاء.
حيث يتطلب إتقان ++C عدة سنوات من الخبرة العملية لتجنب الوقوع في الأخطاء الدقيقة بسبب عدة عوامل متعلقة بطبيعة اللغة، وسنلقي الضوء في هذه المقالة على أهم الأخطاء التي يقع بها مطورو ++C سواء المبتدئون منهم أو الخبراء إن لم يكونوا حذرين في التعامل مع هذه اللغة.
الخطأ 1: الاستخدام الخاطئ للمعاملين new
و delete
بداية سنشرح مشكلة شائعة في إدارة الذاكرة في لغة ++C، فمن الصعب جدًا -مهما حاولنا- تحرير كل الذاكرة المخصصة ديناميكيًا، حتى ولو نجحنا في ذلك ستبقى النتيجة غير آمنة وتصدر الاستثناءات أثناء التنفيذ.
سنلقي الضوء هنا على المثال البسيط التالي:
void SomeMethod() { ClassA *a = new ClassA; SomeOtherMethod(); // it can throw an exception delete a; }
إذا أصدر هذا الكود البسيط استثناء فلن يتم حذف الكائن a
أبدًا. ففي هذا الكود تنشئ الدالة SomeMethod
كائن من الصنف ClassA باستخدام التخصيص الديناميكي للذاكرة new ClassA
. بعد ذلك، تستدعي الدالة SomeOtherMethod
والتي قد تتسبب في حدوث استثناء أثناء تنفيذها. فإذا وقع هذا الاستثناء قبل تنفيذ delete a
، فلن يتم تحرير الكائن `` a من الذاكرة، مما يؤدي إلى ما يعرف بتسريب الذاكرة Memory Leak.
يقدم المثال التالي طريقة أقصر وأكثر أمانًا لتنفيذ هذه المهمة، حيث نستخدم هنا طريقة المؤشر الذكي auto_ptr
الذي لم يعد موجودًا في النسخة 11 من لغة البرمجة ++C ولكنه لا يزال يستخدم على نطاق واسع في النسخ القديمة.
ملاحظة: يمكن استبدال المؤشر auto_ptr
في النسخة 11 من ++C بالمؤشر unique_ptr
أو المؤشر scoped_ptr
من خلال المكتبة Boost التي توفر بعض الأدوات المفيدة لإدارة الذاكرة.
void SomeMethod() { std::auto_ptr<ClassA> a(new ClassA); // deprecated, please check the text SomeOtherMethod(); // it can throw an exception }
هنا سيحذف الكائن a
مباشرة بمجرد انتهاء البرنامج من تنفيذ الكود الموجود بين قوسين.
هذا المثال هو الأبسط للمشكلات التي يمكن تحدث في ++C، فهناك العديد من الأمثلة التي يجب فيها أن يتم الحذف في مكان آخر (مثال: دالة خارجية أو خيط thread آخر)، لذلك لا ينصح أبدًا باستخدام المعاملين new
وdelete
مع بعضهما البعض وإنما يجب استخدام المؤشرات الذكية بدلًا من ذلك.
الخطأ 2: عدم استخدام الهادم الوهمي Virtual Destructor
هذا من الأخطاء الأكثر شيوعًا التي تؤدي إلى استهلاك حجم الذاكرة داخل الأصناف المشتقة من أصناف أخرى، فعند إنشاء صنف أساسي base class وصنف مشتق derived class يحتوي على ذاكرة مخصصة، فمن الضروري أن تكون دالة الهدم في الصنف الأساسي وهمية virtual لأن ++C تعتمد على نوع المؤشر عند استدعاء الدوال، وبالتالي إذا حذفنا كائنًا من خلال مؤشر إلى صنف أساسي لا يحتوي على دالة هادم وهمية فلن تُستدعى دالة الهدم للصنف المشتق عند حذف الكائن. وهذا يؤدي إلى عدم تحرير الذاكرة بشكل صحيح وظهور مشكلة تسرب الذاكرة.
هناك بعض الحالات التي لا يرغب فيها بوجود دالة الهدم الوهمية virtual destructor، على سبيل المثال عندما يكون الصنف لا يقبل الوراثة وحجمه وأداؤه في غاية الأهمية، حيث تضيف دالة الهدم الوهمية -أو أي دالة وهمية أخرى- بيانات إضافية لبنية الصنف بمعنى أنها تضيف مؤشر إلى جدول وهمي مما يؤدي إلى زيادة حجم الصنف class.
ولما كان من الممكن توريث الأصناف في معظم الحالات -حتى وإن لم نكن نريد توريثها- لذا يفضل أن يضيف المبرمج دالة الهدم الوهمية عند التصريح عن الصنف class. من ناحية أخرى، في حال عدم رغبة المبرمج بإضافة أي دوال وهمية للكود لتعزيز أداء البرنامج فيتوجب عليه وضع ملاحظة عند التصريح عن الصنف تخبر باقي المطورين بأن هذا الصنف يجب أن لا يوسع عن طريق الوراثة منه. ومن أفضل الخيارات لتجنب الوقوع في هذه المشكلة هو استخدام بيئة التطوير المتكاملة IDE التي تدعم إنشاء دالة هدم افتراضية أثناء عملية إنشاء الصنف.
هناك نقطة إضافية أخرى في هذا الموضوع وهي الأصناف والقوالب التي تكون موجودة ضمن المكتبة بشكل ضمني built -in فلا يمكن توريث هذه الأصناف كما أنها لا تحتوي على هادم وهمي افتراضي، على سبيل المثال إذا أنشأنا صنف سلسلة نصية string class فإنه سيرث تلقائيًا من الصنف std::string
وهنا تكمن المشكلة ففي حال استخدم أحد المطورين هذا الصنف كمؤشر أو مرجع للصنف std::string
فإن هذا سيؤدي إلى تسريب الذاكرة.
class MyString : public std::string { ~MyString() { // ... } }; int main() { std::string *s = new MyString(); delete s; // May not invoke the destructor defined in MyString }
لتجنب الوقوع في مثل هذه المشكلات في لغة ++C، يجب على المطورين استخدام الوراثة الخاصة private inheritance والتي تعتبر الطريقة الأكثر أمانًا لإعادة استخدام الأصناف أو القوالب من المكتبة الأساسية القياسية.
الخطأ 3: حذف مصفوفة باستخدام delete
أو باستخدام المؤشر الذكي
ستحتاج في كثير من التطبيقات العملية إلى إنشاء مصفوفات مؤقتة بحجم متغير وهو ما يسمى المصفوفات المخصصة ديناميكيًا، ويتطلب التعامل مع هذه المصفوفات عناية خاصة لتحرير الذاكرة بعد الانتهاء من استخدامها.
فمن المهم جدًا تحرير الذاكرة المخصصة لهذه المصفوفات بعد الانتهاء من استخدامها، ولكن المشكلة الكبيرة في لغة ++C هي أنها تتطلب عامل الحذف ذي الأقواس المربعة delete[]
والذي ينساه المبرمج غالبًأ ، وعامل الحذف لن يقوم بحذف الذاكرة المخصصة للمصفوفة فحسب وإنما سيستدعي في البداية تابع الهدم destructor لجميع الكائنات أو العناصر ضمن المصفوفة.
بالمقابل فإن استخدام أمر الحذف بدون أقواس مربعة []
أمر غير صحيح خاصة بالنسبة للأنواع الأساسية الأولية على الرغم من عدم وجود تابع هدم لهذه الأنواع، لذا يجب الانتباه لاستخدام الأقواس لضمان تحرير الذاكرة بشكل صحيح.
وعند تخصيص مصفوفة ديناميكيًا، يتأكد المصرف compiler عادة من أن المؤشر يشير إلى أول عنصر في المصفوفة ويمكننا الوصول إلى باقي العناصر عن طريق التحرك من خلال هذا المؤشر.لكن لا يضمن كل مترجم وضع المؤشر على العنصر الأول من المصفوفة ولذلك فإن استخدام الكلمة المفتاحية delete
بدون أقواس يمكن أن يؤدي إلى سلوك غير متوقع عند تحرير الذاكرة المخصصة للمصفوفة.
إن استخدام المؤشرات الذكية مثل auto_ptr
و<unique_ptr<T
و shared_ptr
أمر غير صحيح أيضًا فعندما يخرج المؤشر الذكي خارج المجال فإنه سيستدعي عامل الحذف بدون أقواس مما يؤدي إلى حدوث المشكلة نفسها التي شرحناها في الفقرة السابقة.
عندما يكون استخدام المؤشر الذكي للمصفوفة أمرًا مطلوبًا، توجب على البرنامج استخدام الصنف scoped_array
أو الصنف shared_array
من المكتبة Boost أو الصنف <[]unique_ptr<T
بالتحديد.
إذا كنا غير محتاجين لاستخدام تقنية إدارة الذاكرة المسماة عد المراجع reference counting عند التعامل مع المصفوفات، فإن الحل الأفضل في هذه الحالة هو استخدام متجهات مكتبة القوالب المعيارية STL بدلًا عنها لأن تلك المتجهات لا تتعامل مع تحرير الذاكرة فحسب وإنما تحتوي على وظائف إضافية مهمة لا مجال لذكرها الآن.
الخطأ 4: إعادة مرجع إلى كائن محلي داخل دالة
هناك خطأ شائع يرتكبه المطورون المبتدئون عند التعامل مع المراجع في دوال ++C، وهو إرجاع مرجع إلى كائن محلي أنشأه داخل الدالة. يحدث هذا الخطأ غالبًا من قبل المطورين المبتدئين عند محاولة تحسين الأداء عن طريق تجنب النسخ غير الضروري للكائنات وسنلقي الضوء عليه نظرًا لكمية التعليمات البرمجية القديمة التي تعاني من هذه المشكلة.
لنأخذ المثال التالي والذي يريد المبرمج من خلاله إدخال بعض التحسينات على الكود وتجنب عمليات النسخ غير الضرورية:
Complex& SumComplex(const Complex& a, const Complex& b) { Complex result; ….. return result; } Complex& sum = SumComplex(a, b);
في هذه الحالة سيشير الكائن sum
إلى الكائن المحلي result
وستعيد الدالة الكائن sum
، ولكن السؤال المطروح: أين سيتواجد الكائن result
بعد تنفيذ الدالة SumComplex
؟.
الجواب هو أن الكائن result
كان مخزنًا في المكدس، ولكن بعد تنفيذ الدالة وإرجاع القيمة فإن ذاكرة المكدس ستستخدم لتخزين الكائنات والمتحولات المحلية ضمن الدالة ومن ثم ستدمر كل هذه الكائنات، مما سيؤدي في النهاية إلى نتيجة غير متوقعة، وهذا ينطبق حتى على أنواع البيانات الأساسية.
هناك حالات يمكنك فيها الاستفادة من تقنية تحسين القيمة المعادة Return Value Optimization أو اختصارًا RVO وهي تقنية يستخدمها المصرف compiler لمنع حدوث مشكلات تتعلق بأداء البرنامج:
Complex SumComplex(const Complex& a, const Complex& b) { return Complex(a.real + b.real, a.imaginar + b.imaginar); } Complex sum = SumComplex(a, b);
بالنسبة للنسخ الحديثة من المصرفات compilers، عندما تتضمن عبارة إرجاع قيمة باني الكائن constructor فإن المصرف سيعمل على تحسين الكود لقائيًا لتجنب عمليات النسخ الزائدة. فبدلاً من إنشاء كائن مؤقت أولاً داخل الدالة ثم نسخه لاحقًا إلى المكان النهائي مثل المتغير sum في الكود أعلاه، يعمل المترجم على إنشاء الكائن النهائي مباشرة في موقعه المطلوب. هذا يعني أن sum سيبنى مباشرة باستخدام القيم المرجعة من الدالة دون الحاجة إلى كائن وسيط أو عملية نسخ إضافية مما يحسن الأداء ويوفر الذاكرة.
الخطأ 5: استخدام مرجع لمورد أو مصدر محذوف
تحدث مثل هذه الأخطاء في لغة ++C بشكل كبير وخصوصًا في التطبيقات متعددة الخيوط multithread، وللتوضيح سنأخذ الكود التالي:
الخيط رقم 1:
Connection& connection= connections.GetConnection(connectionId); // ...
الخيط رقم 2:
connections.DeleteConnection(connectionId); // …
الخيط رقم 1:
connection.send(data);
في هذا المثال، ستكون نتيجة التنفيذ غير متوقعة في حال استخدم كلا الخيطين نفس المعرف connection ID، وغالبًا ما يجد المطور صعوبة في إيجاد سبب وقوع أخطاء انتهاك الوصول access violation هنا.
ففي مثل هذه الحالات وعند وصول عدة خيوط إلى نفس المصدر، سيكون من المخاطرة الاحتفاظ بمؤشرات أو مراجع لهذا المورد لأنه يمكن لمؤشر آخر أن يحذفه، ولتجنب هذا علينا استخدام المؤشرات الذكية مع تقنية العد المرجعي والتي تعتبر الطريقة الأكثر أمانًا مثل المؤشر shared_ptr
من المكتبة Boost، إذ يستخدم هذا النوع من المؤشرات العمليات الذرية atomic operations لزيادة أو إنقاص عداد المراجع reference counter مما يجعله آمنًا للاستخدام في بيئة متعددة الخيوط.
ملاحظة: يتعقب عداد المراجع في البرمجة عدد المراجع لكائن معين في الذاكرة فكلما تم إنشاء مرجع جديد إلى الكائن يزداد بقيمة 1 وكلما تم حذف مرجع ينقص بقيمة 1.
الخطأ 6: السماح برمي توابع الهدم لاستثناءات
ليس من الضروري أن يرمي تابع الهدم destructor استثناءً، وحتى لو كان ذلك ضروريًا فيمكن إيجاد عدة طرق أفضل للتعامل مع هذه الحالة. إذ لا تصدر توابع الهدم عادة الاستثناءات بشكل متعمد، وإنما يمكن تسجيل أو معرفة الحدث الذي تسبب في وقوع الاستثناء عند تدمير كائن معين في البرنامج للتعامل معه بشكل صحيح، وللتوضيح سنلقي نظرة على المثال التالي:
class A { public: A(){} ~A() { writeToLog(); // could cause an exception to be thrown } }; // … try { A a1; A a2; } catch (std::exception& e) { std::cout << "exception caught"; }
إذا حدث الاستثناء في المثال السابق مرتين في نفس الوقت كما في حالة تدمير كلا الكائنين فإن العبارة catch
لن تنفذ ابدًا، والسبب في ذلك هو أنه عند وجود استثنائين يعملان على التوازي (في نفس الوقت) سواء كانا من نفس النوع أو من نوعين مختلفين فإن بيئة تشغيل اللغة ++C لن تكون قادرة على تحديد طريقة التعامل معهما، ونتيجة لذلك تستدعي بيئة التشغيل دالة الإنهاء abort
مما يؤدي إلى تعطل البرنامج.
تنص القاعدة العامة في هذه الحالة على عدم السماح برمي الاستثناءات من توابع الهدم في البرنامج حتى لو لم تبدو التعليمات البرمجية جذابة من الناحية الجمالية، فمن المهم هنا التعامل مع الاستثناءات المحتملة داخل تابع الهدم لمنعها من الانتشار بشكل أكبر
try { writeToLog(); // could cause an exception to be thrown } catch (...) {}
الخطأ 7: استخدام المؤشر auto_ptr
بشكل خاطئ
توقف دعم المؤشر أو القالب auto_ptr
منذ صدور النسخة 11 من لغة البرمجة ++C وذلك لعدة أسباب (لا داعي لذكرها الآن)، ولكنه لا يزال يستخدم على نطاق واسع لأن معظم المشاريع المطورية سابقًا تعمل في النسخة 98++C.
يتمتع القالب auto_ptr
بميزة مهمة قد لا يعرفها الكثير من مطوري ++C، وهي أنه عند نسخ كائن من نوع auto_ptr
، تنتقل ملكيته من الكائن الأصلي إلى الكائن الجديد. هذا يعني أن الكائن الأصلي يصبح فارغًا ولا يمكن الوصول إلى بياناته أو استخدامه بعد ذلك. إذا لم تستخدم هذا القالب بحذر وبطريقة احترافية، قد يتسبب ذلك في مشكلات كبيرة. للتوضيح ألقِ نظرة على الكود التالي:
auto_ptr<ClassA> a(new ClassA); // deprecated, please check the text auto_ptr<ClassA> b = a; a->SomeMethod(); // will result in access violation error
سيؤدي الكود السابق إلى خطأ انتهاك الوصول، حيث يحتوي الكائن b
فقط على مؤشر للكائن Class A
بينما سيكون الكائن a
فارغًا، وستؤدي محاولة الوصول إلى الكائن a
إلى ظهور خطأ انتهاك الوصول.
توجد عدة طرق لاستخدام الكائن auto_ptr
بشكل خاطئ لذا يجب على المطور تذكر الأمور الأربعة التالية: 1.يجب عدم استخدام المؤشر auto_ptr
ضمن حاويات مكتبة القوالب المعيارية STL، لأن نسخ الحاويات سيؤدي إلى وضع بيانات غير صحيحة في الحاويات الأصلية. بالإضافة إلى ذلك، قد تتسبب بعض خوارزميات مكتبة القوالب المعيارية في حدوث أخطاء في المؤشرات الخاصة بـ auto_ptr
.
-
عدم استخدام المؤشر
auto_prt
كوسيط دالة لأن هذا سيؤدي إلى نسخ القيمة وستصبح القيمة الأصلية الممررة إلى الوسيط خاطئة بعد استدعاء الدالة. -
إذا استخدم المبرمج المؤشر
auto_ptr
ضمن بيانات الصنف، يجب عليه التأكد من أن عملية النسخ داخل تابع البناء constructor تنجز بشكل صحيح وذلك إما من خلال إنشاء نسخة صحيحة داخل منشئ النسخة copy constructor وعامل الإسناد assignment operator أو عدم السماح بتطبيق هذه العمليات عن طريق جعلهاprivate
مما يمنع النسخ غير المقصود للبيانات. -
استخدام المؤشرات الذكية الأحدث من المؤشر
auto_ptr
إذا كان الأمر ممكنًا.
الخطأ 8: استخدام المؤشرات والمراجع غير الصالحة
هذا الخطأ شائع جدًا حتى أن يمكننا تأليف كتاب كامل حوله، حيث تحتوي كل حاوية من مكتبة القوالب المعيارية STL على بعض الحالات الخاصة التي تبطل عمل المؤشرات والمراجع، ومن المهم لكل مبرمج أن يكون على علم بهذه الحالات عند استخدام المؤشرات والمراجع.
يمكن أن تحدث إحدى هذه المشكلات بشكل متكرر في بيئات التطوير التي تمتلك خاصية الخيوط المتعددة multiple threads، ولذلك من الضروري اعتماد آليات المزامنة synchronization mechanisms لمنع حدوث هذه المشكلة.
سنلقي نظرة على الكود التالي الذي يستخدم متجه vector لتخزين السلاسل النصية:
vector<string> v; v.push_back(“string1”); string& s1 = v[0]; // assign a reference to the 1st element vector<string>::iterator iter = v.begin(); // assign an iterator to the 1st element v.push_back(“string2”); cout << s1; // access to a reference of the 1st element cout << *iter; // access to an iterator of the 1st element
للوهلة الأولى ومن وجهة نظر منطقية يتبين لنا أن الكود صحيح ولا يوجد فيه مشكلات، ولكن عند إضافة عنصر ثاني إلى المتجه فسيحتاج لإعادة توضع الذاكرة وهذا يمكن أن يجعل كل من مؤشر ومرجع المتجه غير صالحين (يشيران إلى قيمة أخرى)، ونتيجة لذلك يمكن أن تتسبب محاولة الوصول لهم في السطرين الأخيرين من الكود بعد إضافة العنصر الجديد إلى حدوث خطأ انتهاك الوصول Access Violation لأن المؤشر والمرجع لم يعودا يشيران إلى الأماكن الصحيحة في الذاكرة.
الخطأ 9: تمرير كائن عن طريق القيمة
من المعروف لدى معظم المبرمجين أن عملية تمرير الكائن عن طريق القيمة by value (وهي عبارة عن أخذ نسخة من الكائن وتمريرها إلى الدالة) فكرة سيئة فقد يكون لها تأثير سلبي على أداء البرنامج، ومع ذلك نجدهم يؤجلون معالجة هذا الموضوع إلى وقت لاحق لتجنب كتابة كود إضافي مما يؤدي تقليل فاعلية الكود المكتوب وجعله يتسبب في لمشكلات غير متوقعة
class A { public: virtual std::string GetName() const {return "A";} … }; class B: public A { public: virtual std::string GetName() const {return "B";} ... }; void func1(A a) { std::string name = a.GetName(); ... } B b; func1(b);
يصرّف هذا الكود بشكل صحيح وبدون مشكلات، ومع ذلك فإن الدالة func1
ستنسخ جزء من الكائن b
الذي ينتمي إلى الصنف A
ضمن الكائن a
عند استدعائها، وهذا ما يعرف باسم مشكلة التقطيع slicing problem.
بالنتيجة سوف يستدعي البرنامج عند تنفيذه التابع من الصنف A
بدلًا من الصنف B
، ويعتبر هذا سلوكًا غير متوقع للشخص الذي يستدعي الدالة.
تحدث مشكلة أخرى مشابهة عند محاولة التقاط catch الاستثناء كما في المثال التالي:
class ExceptionA: public std::exception; class ExceptionB: public ExceptionA; try { func2(); // can throw an ExceptionB exception } catch (ExceptionA ex) { writeToLog(ex.GetDescription()); throw; }
في المثال السابق، عندما تطلق الدالة func2
استثناء من النوع ExceptionB
، سيتلقط الاستثناء ويعالج في كتلة try/catch
. ولكن بسبب مشكلة النسخ، ينسخ الاستثناء إلى كائن من النوع الأساسي ExceptionA
بدلاً من النوع الفرعي ExceptionB
. هذا يؤدي إلى فقدان المعلومات الخاصة بنوع الاستثناء الفرعي مما يتسبب في استدعاء تابع غير صحيح. وعند إعادة رمي الاستثناء باستخدام throw
، سيرمى استثناء من النوع ExceptionA
بدلاً من النوع الأصلي ExceptionB
.
النصيحة في نهاية هذه الفكرة هي يجب على المبرمج تمرير الكائنات حسب المرجع reference وليس حسب القيمة وأن يلتقط الاستثناءات بالمرجع &
وليس بالقيمة. هذا يحافظ على نوع الاستثناء الفرعي ويمنع المشاكل التي تحدث عند فقدان التفاصيل الخاصة بالاستثناء.
الخطأ 10: استخدام التحويلات المعرفة من المستخدم عن طريق توابع البناء وعوامل التحويل
تشير التحويلات المخصصة أو المعرفة من قبل المستخدم في ++C إلى إمكانية تعريف تحويلات مخصصة بين أنواع البيانات المختلفة، يمكن إنشاء هذه التحويلات باستخدام توابع البناء constructors وعوامل التحويل conversion operators في الأصناف.
وبشكل عام فإن هذا النوع من التحويلات يمنح المبرمج تحكمًا أكبر في طريقة تحويل الكائنات من أنواع مختلفة في كود ++C، وهو مفيد جدًا في بعض الأحيان لكنه قد يؤدي إلى تحويلات غير متوقعة يكون من الصعب تحديد موقعها. لنفترض أن أحدهم أنشأ مكتبة تحتوي على صنف string
فيه دالتا بناء لإنشاء السلاسل النصية الأولى وسيطها عدد والثانية وسيطها محارف كما يلي:
class String { public: String(int n); String(const char *s); …. }
نستطيع من خلال التابع الأول إنشاء سلسلة نصية طولها n، ونستطيع من خلال التابع الثاني إنشاء سلسلة نصية تحتوي على الحروف المدخلة من المستخدم، ولكن تبدأ المشكلة عندما يكون لدينا كود مثل الكود التالي:
String s1 = 123; String s2 = ‘abc’;
في المثال الأول أعلاه، ستستدعى دالة البناء الأولى التي تنشئ سلسلة نصية s1
حجمها هو 123 محرف وليس سلسلة تحتوي على الأحرف 123
، أما المثال الثاني فيحوي على علامات اقتباس مفردة بدلًا من علامات اقتباس مزدوجة والتي ستؤدي إلى استدعاء دالة البناء الأولى أيضًا وإنشاء سلسلة نصية ذات حجم كبير جدًا (لن يفهم 'abc' كسلسلة نصية).
توضيح عند استخدام علامات الاقتباس الفردية يعامل كل حرف على أنه قيمة محرفية character literal، وتمثل قيمة كل حرف بناءً على ترميز الحرف وبالتالي ستحول 'abc'
إلى قيمة عددية باستخدام قيمة ASCII المقابلة لكل حرف في السلسلة وتدمج معًا لتكوين قيمة عددية كبيرة الحجم.
تعتبر هذه الأمثلة بسيطة جدًا، وهناك العديد من الأمثلة الأكثر تعقيدًا التي تؤدي إلى الالتباس وحدوث نتائج غير متوقعة التي قد يكون من الصعب تحديدها أو التنبؤ بها.
توجد قاعدتان بشكل عام لتجنب حدوث هذه المشكلات:
-
تعريف التابع المنشأ باستخدام الكلمة المفتاحية
explicit
لمنع حدوث التحويلات الضمنية - استخدام توابع تحويل صريحة بدلًا من استخدام عوامل التحويل، وهذا الأمر يتطلب جهد إضافي في كتابة الكود ولكنه أبسط في القراءة ويمكن أن يساعد في تجنب النتائج غير المتوقعة.
الخلاصة
تعتبر لغة ++C لغة قوية جدًا، وفي الحقيقة فإن العديد من التطبيقات التي نستخدمها يوميًا مبنية على لغة ++C. وبالرغم من أن لغة البرمجة ++C توفر مرونة كبيرة للمطورين من خلال ما توفره من ميزات متطورة وباعتبارها من لغات البرمجة كائنية التوجه، إلا أن هذه الميزات المتقدمة يمكن أن تسبب بعض الالتباس والإحباط لبعض المطورين إذا لم تستخدم بشكل صحيح واحترافي. نتمنى لكم الاستفادة الجيدة من هذا المقال، وفي حال كان لديكم أي تساؤل يمكن تركه في قسم التعليقات أسفل المقال أو كتابته في قسم الأسئلة والأجوبة في أكاديمية حسوب.
ترجمة وبتصرف للمقال Top 10 Most Common C++ Mistakes That Developers Make لكاتبه Vatroslav Bodrozic
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.