البنائون والمدمرون وقاعدة 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;
}المدمرون وقاعدة الصفر / ثلاثة / خمسة
يتم تشغيل 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. وهذا يجعل نيتك واضحة ويوثق دلالات النسخ/النقل الخاصة بالفصل.
#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على مُنشئات الوسيطة الفردية لمنع التحويلات الضمنية غير المقصودة - The member initializer list is required for
constand reference members, and more efficient for all members - Members initialize in declaration order, not initializer-list order
- Aim for the Rule of Zero — use RAII types so the compiler-generated special members are correct
- إذا كتبت أيًا من destructor/copy-ctor/copy-sign، فاكتب الخمسة جميعًا (قاعدة الخمسة)
=defaultنية المستندات؛ يحظر=deleteإجراء أي عملية بشكل صريح
اختبار — اختبر معرفتك
(20 نقطة خبرة)1. What does the`explicit`keyword on a constructor prevent?
2. In what order are class members initialized?
3. What is the Rule of Zero?