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ý).
#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.
#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ů.
#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.
#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.
#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;
}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_.
- Použijte
explicitna konstruktorech s jedním argumentem, abyste zabránili nechtěným implicitním převodům - The member initializer list is required for
constand 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?