Přeskočit na obsah

Constructors, Destructors & Rule of 0/3/5

Master the full lifecycle of C++ objects — from construction through destruction — and learn when to write (or not write) special member functions.

Životní cyklus objektu v C++

V C++ máte přímou kontrolu nad tím, kdy a jak jsou objekty vytvářeny a ničeny. Kompilátor může automaticky vygenerovat až šest speciálních členských funkcí: výchozí konstruktor, konstruktor kopírování, operátor přiřazení kopírování, konstruktor přesunu, operátor přiřazení přesunu a destruktor. Pochopení, kdy napsat vlastní – a kdy to nechat kompilátoru – je jednou z nejdůležitějších dovedností v C++.

Odrůdy konstruktérů

Konstruktory inicializují objekt. C++ podporuje několik druhů: výchozí (žádné argumenty), parametrizované, konvertující (jeden argument, implicitní konverze) a delegování (jeden konstruktor volá jiný).

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

Explicitní klíčové slovo

Konstruktory s jedním argumentem umožňují implicitní konverze, které mohou způsobit drobné chyby. Použijte explicit, abyste zabránili nechtěným konverzím – to je téměř vždy to, co chcete.

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

Seznamy inicializátorů členů a pořadí inicializace

Seznam inicializátorů členů je preferovaný způsob inicializace členů. Je požadováno pro členyconst, referenční členy a členy bez výchozího konstruktoru. Členové jsou inicializováni v pořadí deklarace, nikoli v pořadí, v jakém se objevují v seznamu inicializátorů.

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

Destruktory a pravidlo nuly / tří / pěti

Když je objekt zničen, spustí se destruktor (~ClassName()) – uvolní zdroje a nesmí vyvolávat výjimky. Pro většinu tříd postačuje destruktor generovaný kompilátorem.

Pravidlo nuly: preferujte RAII obaly (std::string,std::vector,std::unique_ptr), takže nikdy nebudete muset psát žádné speciální členské funkce. Pokud musíte spravovat nezpracovaný zdroj, Pravidlo tří říká, že potřebujete destruktor, konstruktor kopírování a operátor přiřazení kopírování. Pravidlo pěti toto rozšiřuje o konstruktor přesunu a operátor přiřazení přesunu.

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

=výchozí a =smazat

You can explicitly request the compiler-generated version of a special member with =default, or explicitly suppress it with =delete. This makes your intent clear and documents the class's copying/moving semantics.

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;
}
Úskalí

Members are initialized in the order they are declared in the class, not the order they appear in the initializer list. If one member depends on another, make sure the dependency is declared first. Compilers warn about this with-Wreorder— never ignore that warning. A classic bug: initializingsize_from a parameter but then usingsize_to allocatedata_, withdata_declared beforesize_.

Klíčové poznatky
  • Použijteexplicitna konstruktorech s jedním argumentem, abyste zabránili nechtěným implicitním převodům
  • The member initializer list is required forconstand reference members, and more efficient for all members
  • Členové se inicializují v pořadí deklarace, nikoli v pořadí seznamu inicializátorů
  • Zaměřte se na Pravidlo nuly – použijte typy RAII, aby speciální členy vygenerované kompilátorem byly správné
  • Pokud napíšete něco z destructor/copy-ctor/copy-assign, napište všech pět (pravidlo pěti)
  • Záměr dokumentů=default;=deletevýslovně zakazuje operaci

Kvíz — Otestujte své znalosti

(20 XP)

1. Čemu brání klíčové slovo`explicit`na konstruktoru?

2. V jakém pořadí jsou inicializováni členové třídy?

3. What is the Rule of Zero?