Sari la conținut

Constructori, destructori și regula 0/3/5

Stăpânește întregul ciclu de viață al obiectelor C++ – de la construcție până la distrugere – și învață când să scrii (sau să nu scrii) funcții membre speciale.

Ciclul de viață al obiectului în C++

În C++, aveți control direct asupra momentului și modului în care obiectele sunt create și distruse. Compilatorul poate genera automat până la șase funcții membre speciale: constructor implicit, constructor de copiere, operator de atribuire de copiere, constructor de mutare, operator de atribuire de mutare și destructor. Înțelegerea când să-ți scrii propriul – și când să lași compilatorul să o facă – este una dintre cele mai importante abilități din C++.

Soiuri de constructori

Constructorii inițializează un obiect. C++ acceptă mai multe tipuri: implicit (fără argumente), parametrat, conversie (un singur argument, conversie implicită) și delegare (un constructor îl apelează pe altul).

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;
}

Cuvântul cheie explicit

Constructorii cu un singur argument permit conversiile implicite, care pot provoca erori subtile. Utilizați explicit pentru a preveni conversiile neintenționate - aceasta este aproape întotdeauna ceea ce doriți.

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;
}

Listele de inițializare a membrilor și ordinea de inițializare

Lista de inițializare a membrilor este modalitatea preferată de a inițializa membrii. Este obligatoriu pentru membriiconst, membrii de referință și membrii fără un constructor implicit. Membrii sunt inițializați în ordinea declarației, nu în ordinea în care apar în lista de inițializare.

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;
}

Destructori și regula lui Zero / Trei / Cinci

Un destructor (~ClassName()) rulează atunci când un obiect este distrus - eliberează resurse și nu trebuie să arunce excepții. Pentru majoritatea claselor, destructorul generat de compilator este suficient.

Regula lui Zero: preferați pachetele RAII (std::string,std::vector,std::unique_ptr), astfel încât să nu aveți nevoie să scrieți nicio funcție specială pentru membri. Dacă trebuie să gestionați o resursă brută, Regula de trei spune că aveți nevoie de un destructor, un constructor de copiere și un operator de atribuire de copiere. Regula celor cinci extinde acest lucru pentru a include constructorul de mutare și operatorul de atribuire a mutarii.

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;
}

=implicit și =șterge

Puteți solicita în mod explicit versiunea generată de compilator a unui membru special cu =default sau o puteți suprima în mod explicit cu =delete. Acest lucru vă face intenția clară și documentează semantica de copiere/mutare a clasei.

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;
}
Capcană

Membrii sunt inițializați în ordinea în care sunt declarați în clasă, nu în ordinea în care apar în lista de inițializare. Dacă un membru depinde de altul, asigurați-vă că dependența este declarată mai întâi. Compilatorii avertizează despre acest lucru cu-Wreorder- nu ignorați niciodată acest avertisment. O eroare clasică: inițializareasize_dintr-un parametru, dar apoi utilizareasize_pentru a alocadata_, cudata_declarat înainte desize_.

Idei cheie
  • Utilizațiexplicitpe constructorii cu un singur argument pentru a preveni conversiile implicite neintenționate
  • Lista de inițializare a membrilor este necesară pentruconstși membrii de referință și mai eficientă pentru toți membrii
  • Membrii se inițializează în ordinea declarației, nu în ordinea listei de inițializare
  • Vizualizați Regula lui Zero - utilizați tipurile RAII, astfel încât membrii speciali generați de compilator să fie corecti
  • Dacă scrieți oricare dintre destructor/copy-ctor/copy-assign, scrieți toate cele cinci (regula celor cinci)
  • =defaultdocuments intent;=deleteexplicitly forbids an operation

Test — Verifică-ți cunoștințele

(20 XP)

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

2. In what order are class members initialized?

3. Ce este Regula lui Zero?