معناشناسی کپی: عمیق در مقابل سطحی
تفاوت بین کپی سطحی و عمیق، نحوه پیادهسازی عملیات کپی صحیح برای کلاسهای مالک منابع و اصطلاح کپی-و-تعویض را بیاموزید.
چرا معناشناسی کپی اهمیت دارد
وقتی یک شیء را در C++ کپی میکنید، دقیقاً چه چیزی کپی میشود؟ برای انواع ساده، پاسخ واضح است — تمام اعضا بیت به بیت کپی میشوند. اما وقتی یک شیء از طریق یک اشارهگر مالک یک منبع است، سازنده کپی پیشفرض (تولیدشده توسط کامپایلر) یک کپی سطحی انجام میدهد: مقدار اشارهگر را کپی میکند نه دادهای که به آن اشاره میکند. این یعنی دو شیء اکنون یک منبع مشترک دارند که منجر به باگهای آزادسازی مضاعف (double-free) و خرابی داده میشود.
درک معناشناسی کپی ضروری است زیرا کپیها بهصورت ضمنی در بسیاری از مکانها رخ میدهند: ارسال با مقدار، بازگشت از توابع، درج در ظروف و تخصیص.
مشکل کپی سطحی
سازنده کپی تولیدشده توسط کامپایلر هر عضو را کپی میکند. برای اشارهگرها، این آدرس را کپی میکند — نه دادهای که به آن اشاره شده است. این یک کپی سطحی است و برای کلاسهای مالک منابع خطرناک است.
#include <iostream>
#include <cstring>
class BrokenString {
char* data_;
std::size_t len_;
public:
BrokenString(const char* s)
: len_{std::strlen(s)}
, data_{new char[std::strlen(s) + 1]}
{
std::strcpy(data_, s);
}
~BrokenString() {
delete[] data_; // frees the buffer
}
// No copy constructor defined!
// Compiler generates shallow copy: copies the pointer
void print() const {
std::cout << data_ << '\n';
}
};
int main() {
BrokenString a{"Hello"};
{
BrokenString b = a; // shallow copy: b.data_ == a.data_
b.print(); // "Hello" — works for now
} // b destroyed: delete[] b.data_ (same pointer as a.data_!)
// a.data_ is now a dangling pointer — UNDEFINED BEHAVIOR
a.print(); // may crash, print garbage, or appear to work
return 0;
}پیادهسازی کپی عمیق
یک کپی عمیق حافظه جدید تخصیص میدهد و دادههای واقعی را کپی میکند. این تضمین میکند که هر شیء کپی مستقل خود از منبع را دارد.
#include <iostream>
#include <cstring>
#include <algorithm>
class SafeString {
char* data_;
std::size_t len_;
public:
explicit SafeString(const char* s)
: len_{std::strlen(s)}
, data_{new char[std::strlen(s) + 1]}
{
std::strcpy(data_, s);
}
// Deep copy constructor
SafeString(const SafeString& other)
: len_{other.len_}
, data_{new char[other.len_ + 1]}
{
std::strcpy(data_, other.data_);
}
// Deep copy assignment with self-assignment check
SafeString& operator=(const SafeString& other) {
if (this == &other) return *this; // self-assignment guard
// Allocate new resource BEFORE releasing old one
char* new_data = new char[other.len_ + 1];
std::strcpy(new_data, other.data_);
// Release old resource
delete[] data_;
// Adopt new resource
data_ = new_data;
len_ = other.len_;
return *this;
}
~SafeString() { delete[] data_; }
void print() const {
std::cout << data_ << " (len=" << len_ << ")\n";
}
};
int main() {
SafeString a{"Hello"};
SafeString b = a; // deep copy: b gets its own buffer
SafeString c{"World"};
c = a; // deep copy assignment
a.print(); // Hello (len=5)
b.print(); // Hello (len=5)
c.print(); // Hello (len=5)
return 0;
}اصطلاح کپی-و-تعویض (Copy-and-Swap)
اصطلاح کپی-و-تعویض (Copy-and-Swap Idiom) یک پیادهسازی زیبا و ایمن در برابر استثنا از عملگر تخصیص کپی ارائه میدهد. این اصطلاح از سازنده کپی و یک تابع swap مجدداً استفاده میکند و با حداقل تکرار کد، ایمنی استثنای قوی فراهم میکند.
#include <iostream>
#include <cstring>
#include <utility>
class Buffer {
char* data_;
std::size_t size_;
public:
explicit Buffer(std::size_t size)
: size_{size}, data_{new char[size]{}} {}
Buffer(const Buffer& other)
: size_{other.size_}, data_{new char[other.size_]}
{
std::memcpy(data_, other.data_, size_);
}
~Buffer() { delete[] data_; }
// Copy-and-swap: take parameter by VALUE (invokes copy ctor)
Buffer& operator=(Buffer other) { // copy happens here
swap(*this, other); // swap resources
return *this;
} // 'other' destroyed here, freeing our old resource
// Non-throwing swap — essential for strong exception safety
friend void swap(Buffer& a, Buffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
char& operator[](std::size_t i) { return data_[i]; }
std::size_t size() const { return size_; }
};
int main() {
Buffer a{5};
a[0] = 'H'; a[1] = 'i';
Buffer b{10};
b = a; // copy-and-swap: exception-safe, self-assignment-safe
std::cout << b[0] << b[1] << " (size " << b.size() << ")\n";
return 0;
}غیرفعالسازی کپی
برخی انواع نباید قابل کپی باشند — برای مثال، انواعی که منابع منحصر به فرد مانند دستههای فایل، اتصالات شبکه یا نخها را نمایندگی میکنند. از =delete برای ارتباط و اعمال واضح این موضوع استفاده کنید:
class UniqueConnection {
public:
UniqueConnection(const UniqueConnection&) = delete;
UniqueConnection& operator=(const UniqueConnection&) = delete;
// Move operations can still be enabled
};قبل از C++11، این کار با اعلان عملیات کپی بهصورت private بدون تعریف انجام میشد — یک ترفند گیجکننده که =delete بهصورت زیبایی جایگزین آن شده است.
یک تخصیص کپی سادهلوحانه که منبع قدیمی را قبل از تخصیص جدید حذف میکند، از دو جهت معیوب است: (۱) تخصیص به خود (a = a) داده را حذف میکند و سپس سعی میکند از آن کپی کند، و (۲) اگر new پس از delete استثنا پرتاب کند، شیء در وضعیت نامعتبر باقی میماند. اصطلاح کپی-و-تعویض هر دو مشکل را حل میکند: کپی قبل از هرگونه تغییر انجام میشود و swap عملیات noexcept است.
بهترین راه برای جلوگیری از باگهای کپی این است که هرگز منابع خام را مستقیماً مدیریت نکنید. به جای char* از std::string، به جای آرایههای خام از std::vector و به جای اشارهگرهای خام مالک از std::unique_ptr استفاده کنید. وقتی همه اعضای شما معناشناسی کپی صحیحی دارند، عملیات کپی تولیدشده توسط کامپایلر بهصورت خودکار صحیح هستند. این قانون صفر در عمل است.
- کپی تولیدشده توسط کامپایلر سطحی است — مقادیر اشارهگر را کپی میکند، نه دادهای که به آن اشاره شده
- کلاسهای مالک منابع به سازنده و عملگر تخصیص کپی عمیق نیاز دارند
- اصطلاح کپی-و-تعویض ایمنی استثنا و ایمنی تخصیص به خود را بهصورت رایگان فراهم میکند
- از
=deleteبرای منع صریح کپی انواع منبع منحصر به فرد استفاده کنید - بهترین استراتژی قانون صفر است: از پوششدهندههای RAII استفاده کنید تا هرگز نیازی به منطق کپی سفارشی نداشته باشید
آزمون — دانش خود را بسنجید
(15 امتیاز)1. وقتی یک شیء که از طریق اشارهگر خام مالک حافظه هیپ است را بهصورت سطحی کپی کنید، چه اتفاقی میافتد؟
2. چرا اصطلاح کپی-و-تعویض پارامتر را با مقدار دریافت میکند؟
3. کدام رویکرد برای جلوگیری از نیاز به عملیات کپی سفارشی پیشنهاد میشود؟