سازندهها، مخربها و قانون 0/3/5
بر چرخه حیات کامل اشیاء C++ — از ساخت تا تخریب — مسلط شوید و یاد بگیرید چه زمانی توابع عضو ویژه بنویسید (یا ننویسید).
چرخه حیات شیء در C++
در C++، شما کنترل مستقیم بر زمان و نحوه ایجاد و تخریب اشیاء دارید. کامپایلر میتواند تا شش تابع عضو ویژه را بهصورت خودکار تولید کند: سازنده پیشفرض، سازنده کپی، عملگر تخصیص کپی، سازنده انتقال، عملگر تخصیص انتقال و مخرب. درک اینکه چه زمانی خودتان بنویسید — و چه زمانی به کامپایلر اجازه دهید — یکی از مهمترین مهارتها در C++ است.
انواع سازندهها
سازندهها (Constructors) یک شیء را مقداردهی اولیه میکنند. 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
سازندههای تکآرگومانی تبدیلهای ضمنی را فعال میکنند که میتوانند باگهای ظریفی ایجاد کنند. از 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;
}لیست مقداردهنده عضو و ترتیب مقداردهی
لیست مقداردهنده عضو (Member Initializer List) روش ترجیحی برای مقداردهی اعضا است. برای اعضای 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;
}مخربها و قانون صفر / سه / پنج
یک مخرب (Destructor) (~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
میتوانید صریحاً نسخه تولیدشده توسط کامپایلر یک عضو ویژه را با =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 استفاده کنید تا اعضای ویژه تولیدشده توسط کامپایلر صحیح باشند
- اگر هر یک از مخرب/سازندهکپی/تخصیصکپی را نوشتید، هر پنج تا را بنویسید (قانون پنج)
=defaultنیت را مستند میکند؛=deleteصریحاً یک عملیات را منع میکند
آزمون — دانش خود را بسنجید
(20 امتیاز)1. کلمه کلیدی `explicit` روی یک سازنده از چه چیزی جلوگیری میکند؟
2. اعضای کلاس به چه ترتیبی مقداردهی اولیه میشوند؟
3. قانون صفر (Rule of Zero) چیست؟