المسارات
/
C++
C++
/
المنهج
/
المؤشرات الذكية
ال

المؤشرات الذكية في C++

1 تمرين

نبذة عن المؤشرات الذكية

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

الصيغة العامة

تُنفّذ المؤشرات الذكية عادةً كقوالب أصناف في مكتبة C++ القياسية. المؤشران الذكيان الأكثر استخدامًا هما std::unique_ptr و std::shared_ptr.

المؤشرات الفريدة

std::unique_ptr مؤشر ذكي يملك الكائن ملكية حصرية. يضمن أنه في أي لحظة، لا يملك المورد سوى كائن std::unique_ptr واحد. عندما يُدمَّر std::unique_ptr المالك أو يُعاد تعيينه، فإنه يدمّر الكائنات ويحرّر ذاكرتها تلقائيًا.

#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");

// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr

مزايا std::make_unique()

عند إنشاء std::unique_ptr، يُفضّل استخدام std::make_unique() بدلًا من استخدام new مباشرةً لتخصيص الذاكرة. يوفر std::make_unique() عدة مزايا:

  1. أمان الاستثناءات: يضمن std::make_unique() أمان الاستثناءات. إذا طُرح استثناء أثناء بناء الكائن، فستُحرَّر الذاكرة تلقائيًا، مما يمنع تسريب الذاكرة.
  2. الوضوح: يجعل استخدام std::make_unique() الكود أوضح وأكثر إيجازًا. فهو يلغي الحاجة إلى تحديد النوع المخصص صراحةً، لأن وسائط القالب تُستنتج تلقائيًا.
  3. فرص التحسين: تتاح للمصرّفات فرصة تحسين std::make_unique() بشكل أكثر فعالية من تخصيص الذاكرة يدويًا باستخدام new، مما قد يؤدي إلى تحسين الأداء.
  4. تجنّب سوء الاستخدام: يمكن حذف المورد الأساسي عندما يُنشأ std::unique_ptr يدويًا. سيؤدي ذلك إلى سلوك غير معرّف عندما يحاول std::unique_ptr حذفه عند نهاية نطاقه.

المؤشرات المشتركة

std::shared_ptr مؤشر ذكي يسمح لعدة كائنات std::shared_ptr بمشاركة ملكية المورد نفسه. يتتبع عدد المؤشرات المشتركة التي تشير إلى المورد، ولا يحرّر الذاكرة إلا عندما يخرج آخر مؤشر مشترك يملك المورد من النطاق أو يُعاد تعيينه.

// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");

// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
Caution

في C++17 وما قبلها، لا يكون استخدام std::shared_ptr مع المصفوفات عبر std::make_shared<T[]> مدعومًا مباشرةً. ورغم أنه يمكن تخصيص المصفوفات باستخدام std::make_shared<T[]>، فإن إنشاء مؤشرات مشتركة منها مباشرةً قد يؤدي إلى سلوك غير معرّف بسبب اختلافات إدارة الذاكرة بين الكائنات المفردة والمصفوفات. بدلًا من ذلك، فكّر في استخدام std::vector أو دوال حذف مخصصة لإدارة المصفوفات مع المؤشرات المشتركة بفعالية. تأكد دائمًا من التوافق مع المصرّف وتنفيذ المكتبة القياسية لديك عند التعامل مع تخصيصات المصفوفات والمؤشرات المشتركة في C++17.

مزايا std::make_shared()

على غرار std::make_unique()، يقدم std::make_shared() فوائد مثل تحسين كفاءة الذاكرة، وأمان الاستثناءات، وقابلية القراءة. فهو يجمع تخصيص الذاكرة لكتلة التحكم والكائن المُدار في عملية واحدة، مما يعزز الكفاءة ويقلل من خطر تسريب الذاكرة. بالإضافة إلى ذلك، يبسّط الاستنتاج التلقائي لوسائط القالب الكود ويعزز قابلية القراءة. يعزز استخدام std::make_shared() كودًا أنظف وأكثر أمانًا وكفاءة عند التعامل مع كائنات std::shared_ptr في C++.

المؤشرات الضعيفة

std::weak_ptr صنف مرافق لـ std::shared_ptr يوفر مرجعًا "ضعيفًا" غير مالك لكائن يديره مؤشر مشترك. يسمح بالوصول إلى المورد دون التأثير على عمره. تكون المؤشرات الضعيفة مفيدة في السيناريوهات التي يلزم فيها كسر المراجع الدائرية لمنع تسريب الذاكرة.

// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;

// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.

std::weak_ptr والملكية الدائرية

صُمم std::weak_ptr لمعالجة مشكلة الملكية الدائرية، المعروفة أيضًا بالمراجع الدائرية، التي قد تحدث عند استخدام std::shared_ptr.

في سيناريو الملكية الدائرية، يشير كائنان أو أكثر من std::shared_ptr إلى بعضهما البعض، مما ينشئ دورة لا يمكن فيها حذف أي من الكائنات، لأنها تحمل مراجع قوية أحدهما للآخر، مما يؤدي إلى تسريب الذاكرة.

يوفر std::weak_ptr حلًا لهذه المشكلة بالسماح بمراجع ضعيفة إلى الكائنات المشتركة دون المساهمة في عدد مراجعها. هذا يعني أنه يستطيع مراقبة الكائن المشترك والوصول إليه، لكنه لا يمنعه من الحذف. إذا حُررت جميع المراجع القوية إلى الكائن المشترك، فسيُدمَّر الكائن، وتُعاد تعيين المؤشرات الضعيفة التي تراقبه تلقائيًا إلى nullptr.

إليك مثالًا قصيرًا يوضح استخدام std::weak_ptr لكسر الملكية الدائرية:

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::weak_ptr<Node> next;
};

// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;

// Release strong references to nodes
node1.reset();
node2.reset();

// Nodes are automatically destructed due to weak references

تُطبع "Node constructed" مرتين، تليها سطران من "Node destructed".

عندما لا يكون next مؤشرًا ضعيفًا بل std::shared_ptr، فإنه ينشئ حالة ملكية دائرية، مما يؤدي إلى تسريب الذاكرة لأن كلا العقدتين ستحملان مراجع قوية أحدهما للآخر، مما يمنع تدميرهما.

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::shared_ptr<Node> next; // Change to shared_ptr
};

في هذا السيناريو، يحمل كل من node1 و node2 مراجع قوية أحدهما للآخر عبر std::shared_ptr، مما يشكّل دورة. عندما يخرج البرنامج ويحاول تحرير المراجع القوية باستخدام reset()، يبقى عدد مراجع كلتا العقدتين غير صفري لأن كل عقدة تحمل مرجعًا قويًا إلى الأخرى. لذلك، لا تُدمَّر العقدتان، مما يؤدي إلى تسريب الذاكرة. تُطبع "Node constructed" مرتين، لكنها لا يتبعها أي سطر من "Node destructed".

المؤشرات المعلقة و std::weak_ptr

تحدث المؤشرات المعلقة عندما يشير مؤشر إلى كائن حُذف، مما يؤدي إلى سلوك غير معرّف عند إلغاء الإشارة إلى المؤشر. يساعد std::weak_ptr على تجنب المؤشرات المعلقة بتوفير مرجع ضعيف غير مالك إلى كائن يديره std::shared_ptr. إليك كيفية تجنب المؤشرات المعلقة باستخدام std::weak_ptr:

  1. أنشئ std::weak_ptr: بدلًا من الاحتفاظ مباشرةً بـ std::shared_ptr، أنشئ std::weak_ptr إلى الكائن المشترك.

  2. تحقق من الصلاحية: قبل استخدام std::weak_ptr، تحقق من صلاحيته باستخدام دالة expired(). تُرجع هذه الدالة true إذا كان الكائن المشترك المرتبط قد حُذف.

  3. اقفل std::weak_ptr: للوصول إلى الكائن المشترك بأمان، استخدم دالة lock()، التي تُرجع std::shared_ptr يشير إلى الكائن نفسه إذا كان لا يزال صالحًا. إذا حُذف الكائن، تُرجع lock() مؤشر std::shared_ptr فارغًا.

إليك مثالًا برمجيًا يوضح استخدام std::weak_ptr لتجنب المؤشرات المعلقة:

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Create weak pointers
node1->next = node2;
node2->next = node1;

// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
    // Use lockedNode2 safely
} else {
    // Handle case where node2 has been deleted
}

في هذا المثال، node1->next و node2->next كائنا std::weak_ptr. قبل الوصول إلى الكائن المشترك، تُفحص الصلاحية باستخدام دالة expired(). ثم يُستخدم lock() للوصول بأمان إلى الكائن المشترك. يضمن هذا الأسلوب تجنب المؤشرات المعلقة عند التعامل مع std::weak_ptr.

نصائح الاستخدام

استخدم المؤشرات الذكية افتراضيًا: std::unique_ptr للملكية الحصرية و std::shared_ptr للملكية المشتركة. احتفظ بالمؤشرات الخام للمراجع غير المالكة أو عند التعامل مع كود قديم. في معظم الحالات، يكون std::unique_ptr كافيًا للملكية الحصرية، لأنه يوفر إدارة ذاكرة خفيفة دون عبء عدّ المراجع. ينبغي استخدام std::shared_ptr باعتدال، لأنه يقدم عبئًا وتعقيدًا ما لم تكن هناك حاجة حقيقية إلى ملكية مشتركة. يتخصص std::weak_ptr في كسر التبعيات الدائرية أو مراقبة الكائنات المشتركة، لكنه ليس شائع الاستخدام.

تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة

تعلّم المؤشرات الذكية