
c++中实现默认函数和删除函数
c++中实现默认函数和删除函数
文章目录
在 C++ 中,你可以使用
default
和 delete
关键字来分别指定一个成员函数为默认实现或者完全删除它。这通常用于类的特殊成员函数,比如构造函数、析构函数、拷贝构造函数、移动构造函数、拷贝赋值运算符以及移动赋值运算符。
默认函数
如果你想要提供一个默认的实现,可以使用 default
关键字。这通常用于当你希望编译器为你生成一个默认的行为时。
class MyClass {
public:
// 默认构造函数
MyClass() = default; // 编译器将提供一个默认构造函数
// 默认析构函数
~MyClass() = default; // 如果没有其他定义,编译器会提供一个默认析构函数
// 默认拷贝构造函数
MyClass(const MyClass&) = default;
// 默认拷贝赋值运算符
MyClass& operator=(const MyClass&) = default;
// 默认移动构造函数
MyClass(MyClass&&) = default;
// 默认移动赋值运算符
MyClass& operator=(MyClass&&) = default;
};
删除函数
如果你想要禁止某些行为,可以使用 delete
关键字来显式地删除这些函数。这对于那些你不希望用户能够调用的函数特别有用。
class MyClass {
public:
// 删除默认构造函数
MyClass() = delete; // 无法通过默认构造函数创建对象
// 删除拷贝构造函数
MyClass(const MyClass&) = delete; // 禁止拷贝构造
// 删除拷贝赋值运算符
MyClass& operator=(const MyClass&) = delete; // 禁止拷贝赋值
// 删除移动构造函数
MyClass(MyClass&&) = delete; // 禁止移动构造
// 删除移动赋值运算符
MyClass& operator=(MyClass&&) = delete; // 禁止移动赋值
};
注意
- 当你删除了一个函数,那么试图调用该函数将会导致编译错误。
- 如果你声明了某个特殊成员函数(如拷贝构造函数),则编译器将不再为你生成默认版本。如果需要默认的行为,必须明确指定
= default
。 - 在某些情况下,例如当类拥有指针成员变量时,删除默认的拷贝构造函数和拷贝赋值运算符是一个好主意,因为默认的行为可能会导致浅拷贝的问题。
- 同样地,如果类管理资源(如动态分配的内存),则应该谨慎考虑是否删除移动构造函数和移动赋值运算符,因为它们允许更高效的资源管理(例如通过移动语义)。
案例展示
让我们通过一个具体的例子来看看如何在 C++ 中使用默认函数和删除函数。
假设我们有一个 NonCopyable
类,这个类的对象不应该被拷贝或移动,可能是因为拷贝或移动会导致资源管理上的问题,或者是出于设计上的考虑。
不可拷贝也不可移动的类
#include <iostream>
class NonCopyable {
private:
int* data;
public:
NonCopyable() : data(new int(42)) {
std::cout << "NonCopyable object created with data: " << *data << std::endl;
}
~NonCopyable() {
delete data;
std::cout << "NonCopyable object destroyed." << std::endl;
}
// 删除拷贝构造函数
NonCopyable(const NonCopyable&) = delete;
// 删除拷贝赋值运算符
NonCopyable& operator=(const NonCopyable&) = delete;
// 删除移动构造函数
NonCopyable(NonCopyable&&) = delete;
// 删除移动赋值运算符
NonCopyable& operator=(NonCopyable&&) = delete;
};
int main() {
// 创建一个对象
NonCopyable obj;
// 下面的代码将不会编译,因为拷贝构造函数和赋值运算符都被删除了
// NonCopyable obj2 = obj;
// NonCopyable obj3(std::move(obj));
return 0;
}
在这个例子中,我们有一个 NonCopyable
类,它包含一个指向整数的指针 data
。我们删除了拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符,这意味着这个类的对象既不能被拷贝也不能被移动。如果我们尝试拷贝或移动一个 NonCopyable
对象,编译器将会报错。
只有默认构造函数和析构函数的类
接下来,我们看一个简单的例子,其中类只有默认的构造函数和析构函数,并且允许默认拷贝和移动语义。
#include <iostream>
class SimpleClass {
public:
SimpleClass() = default; // 使用默认构造函数
SimpleClass(const SimpleClass&) = default; // 使用默认拷贝构造函数
SimpleClass& operator=(const SimpleClass&) = default; // 使用默认拷贝赋值运算符
SimpleClass(SimpleClass&&) = default; // 使用默认移动构造函数
SimpleClass& operator=(SimpleClass&&) = default; // 使用默认移动赋值运算符
~SimpleClass() = default; // 使用默认析构函数
};
int main() {
SimpleClass obj1;
SimpleClass obj2 = obj1; // 使用默认拷贝构造函数
SimpleClass obj3;
obj3 = obj1; // 使用默认拷贝赋值运算符
SimpleClass obj4 = std::move(obj3); // 使用默认移动构造函数
SimpleClass obj5;
obj5 = std::move(obj4); // 使用默认移动赋值运算符
return 0;
}
在这个例子中,SimpleClass
只有默认的构造函数和析构函数,并且允许默认的拷贝和移动操作。因此,我们可以正常使用拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符来进行对象的创建和赋值。
其他的应用场景
我们可以继续探讨一些其他的应用场景。下面是一些额外的例子,展示了如何根据实际需求来使用默认函数和删除函数。
示例1:管理资源的类
假设我们有一个类 UniquePtr
,它类似于标准库中的 std::unique_ptr
,用于管理一个单一对象的生命周期。此类应该阻止对象的拷贝,但允许移动语义,以便在对象转移所有权时能够高效地进行资源管理。
#include <iostream>
class UniquePtr {
private:
int* ptr;
public:
// 构造函数
explicit UniquePtr(int value) : ptr(new int(value)) {
std::cout << "UniquePtr created with value: " << *ptr << std::endl;
}
// 析构函数
~UniquePtr() {
delete ptr;
std::cout << "UniquePtr destroyed." << std::endl;
}
// 删除拷贝构造函数
UniquePtr(const UniquePtr&) = delete;
// 删除拷贝赋值运算符
UniquePtr& operator=(const UniquePtr&) = delete;
// 移动构造函数
UniquePtr(UniquePtr&& other) noexcept : ptr(other.ptr) {
other.ptr = nullptr;
std::cout << "UniquePtr moved from another UniquePtr." << std::endl;
}
// 移动赋值运算符
UniquePtr& operator=(UniquePtr&& other) noexcept {
if (this != &other) {
delete ptr;
ptr = other.ptr;
other.ptr = nullptr;
std::cout << "UniquePtr moved from another UniquePtr via assignment." << std::endl;
}
return *this;
}
// 获取指针
int* get() const { return ptr; }
};
int main() {
// 创建一个对象
UniquePtr uptr(10);
// 移动构造
UniquePtr uptrMoved(std::move(uptr));
// 移动赋值
UniquePtr uptrNew;
uptrNew = std::move(uptrMoved);
// 输出指针值
std::cout << "Pointer value: " << *uptrNew.get() << std::endl;
return 0;
}
在这个例子中,UniquePtr
类不允许拷贝构造或拷贝赋值,但是支持移动构造和移动赋值。这样做是为了确保只有一个 UniquePtr
对象拥有对特定资源的所有权,并且当所有权转移时,可以高效地释放旧资源并获取新资源的所有权。
示例2:使用默认行为
另一个示例是当类不需要特殊的构造或赋值逻辑时,可以选择使用默认的行为。这通常适用于那些不需要额外初始化或清理工作的简单数据结构。
#include <iostream>
class DataWrapper {
public:
int data;
// 使用默认构造函数
DataWrapper() = default;
// 使用默认拷贝构造函数
DataWrapper(const DataWrapper&) = default;
// 使用默认拷贝赋值运算符
DataWrapper& operator=(const DataWrapper&) = default;
// 使用默认移动构造函数
DataWrapper(DataWrapper&&) = default;
// 使用默认移动赋值运算符
DataWrapper& operator=(DataWrapper&&) = default;
// 使用默认析构函数
~DataWrapper() = default;
};
int main() {
DataWrapper dw1;
dw1.data = 5;
DataWrapper dw2 = dw1; // 使用默认拷贝构造函数
DataWrapper dw3;
dw3 = dw1; // 使用默认拷贝赋值运算符
DataWrapper dw4 = std::move(dw3); // 使用默认移动构造函数
DataWrapper dw5;
dw5 = std::move(dw4); // 使用默认移动赋值运算符
return 0;
}
在这个例子中,DataWrapper
类允许所有默认的行为,因为它不需要任何特殊的构造或销毁逻辑。这种设计简化了类的使用,并允许编译器生成最优化的代码。
示例3:使用默认函数与自定义行为相结合
有时候,我们需要在类中同时使用默认函数和自定义函数。例如,我们可能希望类具有默认的拷贝构造函数和拷贝赋值运算符,但同时又需要一个自定义的移动构造函数和移动赋值运算符来更好地管理资源。
下面是一个例子,展示了一个类既有默认行为又有自定义行为的情况:
#include <iostream>
class ManagedResource {
private:
int* data;
public:
// 构造函数
ManagedResource(int value) : data(new int(value)) {
std::cout << "ManagedResource created with value: " << *data << std::endl;
}
// 默认的拷贝构造函数
ManagedResource(const ManagedResource& other) = default;
// 默认的拷贝赋值运算符
ManagedResource& operator=(const ManagedResource& other) = default;
// 自定义移动构造函数
ManagedResource(ManagedResource&& other) noexcept {
data = other.data;
other.data = nullptr;
std::cout << "ManagedResource moved from another ManagedResource." << std::endl;
}
// 自定义移动赋值运算符
ManagedResource& operator=(ManagedResource&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
std::cout << "ManagedResource moved from another ManagedResource via assignment." << std::endl;
}
return *this;
}
// 析构函数
~ManagedResource() {
delete data;
std::cout << "ManagedResource destroyed." << std::endl;
}
// 获取数据的值
int getValue() const { return *data; }
};
int main() {
// 创建一个对象
ManagedResource mr(10);
// 拷贝构造
ManagedResource copyMr(mr); // 使用默认拷贝构造函数
// 拷贝赋值
ManagedResource copyMr2;
copyMr2 = mr; // 使用默认拷贝赋值运算符
// 移动构造
ManagedResource moveMr(std::move(copyMr)); // 使用自定义移动构造函数
// 移动赋值
ManagedResource moveMr2;
moveMr2 = std::move(moveMr); // 使用自定义移动赋值运算符
// 输出数据的值
std::cout << "Original value: " << mr.getValue() << std::endl;
std::cout << "Copied value: " << copyMr2.getValue() << std::endl;
std::cout << "Moved value: " << moveMr2.getValue() << std::endl;
return 0;
}
在这个例子中,ManagedResource
类使用默认的拷贝构造函数和拷贝赋值运算符,因为它们对于浅拷贝来说是合适的。然而,为了有效地管理资源,在移动构造函数和移动赋值运算符中实现了自定义逻辑,这样可以避免不必要的复制,并确保资源的正确转移。
示例4:使用默认析构函数
有时,即使一个类有多个成员,它们可能都不需要特别的清理工作。在这种情况下,可以使用默认析构函数。
#include <iostream>
#include <vector>
class SimpleContainer {
public:
std::vector<int> elements;
// 默认构造函数
SimpleContainer() = default;
// 默认拷贝构造函数
SimpleContainer(const SimpleContainer&) = default;
// 默认拷贝赋值运算符
SimpleContainer& operator=(const SimpleContainer&) = default;
// 默认移动构造函数
SimpleContainer(SimpleContainer&&) = default;
// 默认移动赋值运算符
SimpleContainer& operator=(SimpleContainer&&) = default;
// 使用默认析构函数
~SimpleContainer() = default;
};
int main() {
SimpleContainer sc;
sc.elements.push_back(1);
sc.elements.push_back(2);
sc.elements.push_back(3);
// 创建一个拷贝
SimpleContainer scCopy(sc);
// 创建一个移动对象
SimpleContainer scMove(std::move(sc));
// 输出元素
for (int elem : scCopy.elements) {
std::cout << elem << " ";
}
std::cout << std::endl;
for (int elem : scMove.elements) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
在这个例子中,SimpleContainer
类包含一个 std::vector<int>
成员。由于 std::vector
已经提供了良好的析构逻辑,所以 SimpleContainer
的析构函数可以使用默认的实现。此外,所有的构造函数和赋值运算符也都使用默认实现,这意味着它们将执行浅拷贝或移动操作。
这些例子展示了如何灵活地结合使用默认函数和自定义函数来满足不同场景下的需求。
————————————————
最后我们放松一下眼睛
更多推荐
所有评论(0)