在C++类设计中,有时候我们不希望类的对象支持自动赋值操作,比如单例类、资源独占类或者包含不可复制成员的类,这时候就需要禁用类的自动赋值运算符。C++11引入的delete关键字为这种需求提供了简洁的实现方式。

C++自动赋值运算符的生成规则
当我们定义一个类时,如果没有显式声明拷贝赋值运算符和移动赋值运算符,编译器会在需要时自动生成这两个运算符:
- 如果用户没有声明任何拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数,编译器会生成默认的拷贝赋值运算符和移动赋值运算符
- 如果用户声明了拷贝操作或者析构函数,编译器不会生成移动赋值运算符
- 如果用户声明了移动操作,编译器不会生成拷贝赋值运算符
默认的赋值运算符会逐成员进行赋值,对于包含动态内存、文件句柄等资源的类,这种默认行为往往会导致资源重复释放或者悬空指针问题。
使用delete禁用赋值运算符
C++11之后,我们可以通过delete关键字显式禁用不需要的赋值运算符,这种方式比传统的私有声明方式更直观,也更符合现代C++的规范。
禁用拷贝赋值运算符
拷贝赋值运算符的作用是把一个已存在对象的内容赋值给另一个同类型的已存在对象,我们可以通过以下方式禁用它:
#include <iostream>
#include <string>
class ResourceClass {
private:
std::string* data; // 独占资源指针
public:
// 构造函数
ResourceClass(const std::string& str) : data(new std::string(str)) {}
// 析构函数释放资源
~ResourceClass() {
delete data;
}
// 禁用拷贝赋值运算符
ResourceClass& operator=(const ResourceClass& other) = delete;
};
int main() {
ResourceClass obj1("test");
ResourceClass obj2("demo");
// obj2 = obj1; // 编译错误,赋值运算符已被禁用
return 0;
}
上面的代码中,我们将拷贝赋值运算符声明为delete,当用户尝试执行obj2 = obj1这样的操作时,编译器会直接报错,阻止这种不安全的赋值行为。
禁用移动赋值运算符
移动赋值运算符是C++11新增的特性,用于将临时对象的资源转移给已存在的对象,同样可以用delete禁用:
#include <iostream>
#include <string>
class NoMoveAssign {
private:
std::string content;
public:
NoMoveAssign(const std::string& str) : content(str) {}
// 禁用移动赋值运算符
NoMoveAssign& operator=(NoMoveAssign&& other) = delete;
};
int main() {
NoMoveAssign obj1("hello");
NoMoveAssign obj2("world");
// obj2 = std::move(obj1); // 编译错误,移动赋值运算符已被禁用
return 0;
}
传统禁用方式对比
在C++11之前,开发者通常通过把赋值运算符声明为私有成员且不实现来禁用它,这种方式存在明显缺陷:
| 对比项 | 传统私有声明方式 | delete关键字方式 |
|---|---|---|
| 可读性 | 不直观,需要额外注释说明意图 | 语义明确,直接表达禁用意图 |
| 错误提示 | 友元类或者成员函数内调用时才会报错,提示不友好 | 编译阶段直接报错,错误提示清晰 |
| 适用场景 | 仅C++11之前版本可用 | C++11及之后版本推荐 |
注意事项
- delete关键字可以用于禁用任何函数,不仅仅是赋值运算符,比如也可以禁用默认构造函数、拷贝构造函数等
- 如果同时禁用了拷贝赋值和移动赋值,那么类的对象将完全不支持赋值操作
- delete声明必须放在函数的声明部分,不能和函数定义同时出现
- 对于单例类,通常会同时禁用拷贝构造、拷贝赋值、移动构造、移动赋值,确保单例的唯一性
通过合理使用delete关键字禁用不需要的赋值运算符,可以让类的设计更严谨,避免很多因为默认赋值行为带来的潜在问题,是C++类设计中非常实用的技巧。