Κατασκευαστές, Καταστροφείς & Κανόνας 0/3/5
Κατακτήστε τον πλήρη κύκλο ζωής των αντικειμένων C++ — από την κατασκευή έως την καταστροφή — και μάθετε πότε να γράφετε (ή να μην γράφετε) ειδικές συναρτήσεις μελών.
Κύκλος ζωής αντικειμένου στο C++
Στο C++, έχετε άμεσο έλεγχο για το πότε και πώς δημιουργούνται και καταστρέφονται αντικείμενα. Ο μεταγλωττιστής μπορεί να δημιουργήσει έως και έξι ειδικές συναρτήσεις μέλους αυτόματα: προεπιλεγμένος κατασκευαστής, κατασκευαστής αντιγραφής, τελεστής εκχώρησης αντιγραφής, κατασκευαστής μετακίνησης, τελεστής εκχώρησης μετακίνησης και καταστροφέας. Η κατανόηση του πότε να γράψετε το δικό σας — και πότε να αφήσετε τον μεταγλωττιστή να το κάνει — είναι μία από τις πιο σημαντικές δεξιότητες στο C++.
Κατασκευαστικές ποικιλίες
Οι κατασκευαστές αρχικοποιούν ένα αντικείμενο. Το C++ υποστηρίζει διάφορα είδη: προεπιλογή (χωρίς ορίσματα), παραμετροποιημένη, μετατροπή (μονό όρισμα, σιωπηρή μετατροπή) και εκχώρηση (ένας κατασκευαστής καλεί έναν άλλο).
#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 για να αποτρέψετε ακούσιες μετατροπές — αυτό είναι σχεδόν πάντα αυτό που θέλετε.
#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, μέλη αναφοράς και μέλη χωρίς προεπιλεγμένο κατασκευαστή. Τα μέλη αρχικοποιούνται με σειρά δήλωσης, όχι με τη σειρά που εμφανίζονται στη λίστα αρχικοποιητών.
#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), ώστε να μην χρειαστεί ποτέ να γράψετε ειδικές συναρτήσεις μέλους. Εάν πρέπει να διαχειριστείτε έναν ακατέργαστο πόρο, ο Κανόνας των Τριών λέει ότι χρειάζεστε έναν καταστροφέα, έναν κατασκευαστή αντιγραφής και έναν τελεστή εκχώρησης αντιγράφων. Ο Κανόνας του Πέντε επεκτείνει αυτό για να συμπεριλάβει τον κατασκευαστή μετακίνησης και τον τελεστή εκχώρησης κίνησης.
#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. Αυτό καθιστά σαφή την πρόθεσή σας και τεκμηριώνει τη σημασιολογία αντιγραφής/μετακίνησης της τάξης.
#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?