Μετάβαση στο περιεχόμενο

Κατασκευαστές, Καταστροφείς & Κανόνας 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;
}

Καταστροφείς & Κανόνας Μηδέν / Τρία / Πέντε

Ένας καταστροφέας (~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σε κατασκευαστές με ένα όρισμα για να αποτρέψετε ακούσιες σιωπηρές μετατροπές
  • Η λίστα αρχικοποιητή μελών απαιτείται για τοconstκαι τα μέλη αναφοράς και πιο αποτελεσματική για όλα τα μέλη
  • Τα μέλη αρχικοποιούνται με σειρά δήλωσης, όχι με σειρά αρχικοποιητή-λίστας
  • Στοχεύστε στον Κανόνα του Μηδέν — χρησιμοποιήστε τύπους RAII, ώστε τα ειδικά μέλη που δημιουργούνται από τον μεταγλωττιστή να είναι σωστά
  • Εάν γράψετε οποιοδήποτε από το destructor/copy-ctor/copy-assign, γράψτε και τα πέντε (Κανόνας των πέντε)
  • Πρόθεση εγγράφων=default. Το=deleteαπαγορεύει ρητά μια λειτουργία

Κουίζ — Δοκιμάστε τις γνώσεις σας

(20 XP)

1. Τι εμποδίζει η λέξη-κλειδί`explicit`σε έναν κατασκευαστή;

2. In what order are class members initialized?

3. What is the Rule of Zero?