انتقل إلى المحتوى

البنائون والمدمرون وقاعدة 0/3/5

أتقن دورة الحياة الكاملة لكائنات C++ - بدءًا من البناء وحتى التدمير - وتعلم متى تكتب (أو لا تكتب) وظائف الأعضاء الخاصة.

دورة حياة الكائن في C++

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

أصناف المنشئ

يقوم المنشئون بتهيئة الكائن. يدعم C++ عدة أنواع: الافتراضي (بدون وسيطات)، المعلمات، التحويل (وسيطة واحدة، تحويل ضمني)، و التفويض (يستدعي أحد المنشئين آخر).

constructors.cpp
#include <iostream>
#include <string>

class Temperature {
    double celsius_;
    std::string label_;

public:
    // Default constructor
    Temperature() : celsius_{0.0}, label_{"unknown"} {}

    // Parameterized constructor
    Temperature(double c, const std::string& label)
        : celsius_{c}, label_{label} {}

    // Converting constructor — allows implicit conversion from double
    Temperature(double c) : celsius_{c}, label_{"sensor"} {}

    // Delegating constructor — reuses another constructor
    Temperature(const std::string& label)
        : Temperature{0.0, label} {}  // delegates to parameterized

    void print() const {
        std::cout << label_ << ": " << celsius_ << "°C\n";
    }
};

int main() {
    Temperature t1;                   // default
    Temperature t2{36.6, "body"};     // parameterized
    Temperature t3 = 100.0;           // converting (implicit)
    Temperature t4{"outdoor"};        // delegating

    t1.print();  // unknown: 0°C
    t2.print();  // body: 36.6°C
    t3.print();  // sensor: 100°C
    t4.print();  // outdoor: 0°C
    return 0;
}

الكلمة المفتاحية الصريحة

تعمل مُنشئات الوسيطة الواحدة على تمكين التحويلات الضمنية، والتي يمكن أن تسبب أخطاء دقيقة. استخدم explicit لمنع التحويلات غير المقصودة — وهذا هو ما تريده دائمًا تقريبًا.

explicit.cpp
#include <iostream>
#include <string>

class UserId {
    int id_;
public:
    explicit UserId(int id) : id_{id} {}
    int get() const { return id_; }
};

void process(UserId uid) {
    std::cout << "Processing user #" << uid.get() << '\n';
}

int main() {
    UserId u1{42};       // OK: direct initialization
    // UserId u2 = 42;   // ERROR: implicit conversion blocked by explicit
    // process(42);      // ERROR: would require implicit conversion
    process(UserId{42}); // OK: explicit construction
    return 0;
}

قوائم مُهيئ الأعضاء وترتيب التهيئة

قائمة مُهيئ الأعضاء هي الطريقة المفضلة لتهيئة الأعضاء. إنه مطلوب لأعضاءconstوالأعضاء المرجعيين والأعضاء الذين ليس لديهم مُنشئ افتراضي. تتم تهيئة الأعضاء حسب ترتيب التصريح، وليس الترتيب الذي يظهرون به في قائمة المُهيئات.

init_list.cpp
#include <iostream>
#include <string>

class Connection {
    const int max_retries_;      // const: MUST use init list
    std::string& log_target_;   // reference: MUST use init list
    std::string host_;
    int port_;

public:
    Connection(std::string& log, const std::string& host, int port)
        : max_retries_{3}          // const member
        , log_target_{log}         // reference member
        , host_{host}              // initialized in declaration order
        , port_{port}
    {
        // Constructor body runs AFTER all members are initialized
        log_target_ += "[Connection to " + host_ + " created]\n";
    }

    void print() const {
        std::cout << host_ << ':' << port_
                  << " (max retries: " << max_retries_ << ")\n";
    }
};

int main() {
    std::string log;
    Connection conn{log, "db.example.com", 5432};
    conn.print();
    std::cout << log;
    return 0;
}

المدمرون وقاعدة الصفر / ثلاثة / خمسة

يتم تشغيل destructor (~ClassName()) عند تدمير كائن ما — فهو يحرر الموارد ويجب ألا يطرح استثناءات. بالنسبة لمعظم الفئات، تكون أداة التدمير التي أنشأها المترجم كافية.

قاعدة الصفر: تفضل أغلفة RAII (std::string،std::vector،std::unique_ptr) حتى لا تحتاج أبدًا إلى كتابة أي وظائف خاصة للأعضاء. إذا كان يجب عليك إدارة مورد خام، فإن قاعدة الثلاثة تنص على أنك بحاجة إلى مُدمر، ومُنشئ النسخ، وعامل تعيين النسخ. تعمل القاعدة الخمسة على توسيع هذا ليشمل منشئ الحركة ومشغل مهمة النقل.

rule_of_five.cpp
#include <iostream>
#include <cstring>

// === Rule of Zero: the GOAL ===
// No special members needed — std::string handles everything
class Person {
    std::string name_;
    int age_;
public:
    Person(const std::string& name, int age)
        : name_{name}, age_{age} {}
    // Compiler generates correct copy/move/destructor automatically!
};

// === Rule of Five: when managing a raw resource ===
class CharBuffer {
    char* data_;
    std::size_t size_;

public:
    explicit CharBuffer(const char* str)
        : size_{std::strlen(str)}
        , data_{new char[std::strlen(str) + 1]}
    {
        std::strcpy(data_, str);
    }

    // Destructor
    ~CharBuffer() { delete[] data_; }

    // Copy constructor (deep copy)
    CharBuffer(const CharBuffer& other)
        : size_{other.size_}
        , data_{new char[other.size_ + 1]}
    {
        std::strcpy(data_, other.data_);
    }

    // Copy assignment (copy-and-swap idiom)
    CharBuffer& operator=(CharBuffer other) {  // note: by value
        swap(*this, other);
        return *this;
    }

    // Move constructor
    CharBuffer(CharBuffer&& other) noexcept
        : data_{other.data_}, size_{other.size_}
    {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // Move assignment
    CharBuffer& operator=(CharBuffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    friend void swap(CharBuffer& a, CharBuffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

    void print() const {
        std::cout << (data_ ? data_ : "(null)") << '\n';
    }
};

int main() {
    CharBuffer a{"Hello"};
    CharBuffer b = a;           // copy constructor
    CharBuffer c{"World"};
    c = a;                      // copy assignment
    CharBuffer d = std::move(a); // move constructor
    d.print();  // Hello
    a.print();  // (null) — moved-from state
    return 0;
}

=افتراضي و=حذف

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

default_delete.cpp
#include <memory>
#include <string>

// Non-copyable, movable resource handle
class FileHandle {
    std::string path_;
    int fd_;

public:
    FileHandle(const std::string& path, int fd)
        : path_{path}, fd_{fd} {}

    // Explicitly default the destructor
    ~FileHandle() = default;

    // Delete copy operations — file handles should not be duplicated
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // Default move operations
    FileHandle(FileHandle&&) = default;
    FileHandle& operator=(FileHandle&&) = default;
};

int main() {
    FileHandle fh{"data.txt", 3};
    // FileHandle copy = fh;         // ERROR: copy deleted
    FileHandle moved = std::move(fh); // OK: move is allowed
    return 0;
}
خطأ شائع

تتم تهيئة الأعضاء بالترتيب الذي تم الإعلان عنهم فيه** في الفصل، وليس بالترتيب الذي يظهرون به في قائمة المُهيئات. إذا كان أحد الأعضاء يعتمد على عضو آخر، فتأكد من إعلان التبعية أولاً. يحذر المجمعون من هذا باستخدام-Wreorder- لا تتجاهل هذا التحذير أبدًا. خطأ كلاسيكي: تهيئةsize_من معلمة ثم استخدامsize_لتخصيصdata_، مع إعلانdata_قبلsize_.

النقاط الرئيسية
  • استخدمexplicitعلى مُنشئات الوسيطة الفردية لمنع التحويلات الضمنية غير المقصودة
  • The member initializer list is required forconstand reference members, and more efficient for all members
  • Members initialize in declaration order, not initializer-list order
  • Aim for the Rule of Zero — use RAII types so the compiler-generated special members are correct
  • إذا كتبت أيًا من destructor/copy-ctor/copy-sign، فاكتب الخمسة جميعًا (قاعدة الخمسة)
  • =defaultنية المستندات؛ يحظر=deleteإجراء أي عملية بشكل صريح

اختبار — اختبر معرفتك

(20 نقطة خبرة)

1. What does the`explicit`keyword on a constructor prevent?

2. In what order are class members initialized?

3. What is the Rule of Zero?