= default 与 = delete
定义与作用
C++11 引入了两个新的函数声明语法:
| 语法 | 含义 | 使用场景 |
|---|---|---|
= default | 显式要求编译器生成默认实现 | 恢复被抑制的默认特殊成员函数 |
= delete | 显式禁止使用该函数 | 阻止拷贝、阻止隐式转换、阻止特定重载 |
class NonCopyable {
public:
NonCopyable() = default; // 使用编译器生成的默认构造
NonCopyable(const NonCopyable&) = delete; // 禁止拷贝
NonCopyable& operator=(const NonCopyable&) = delete;
};
核心原理
特殊成员函数的默认生成规则
=delete 与 =default 的对比
| 特性 | = default | = delete |
|---|---|---|
| 作用 | 使用编译器默认行为 | 禁止使用 |
| 可修饰 | 特殊成员函数(构造/析构/拷贝/移动) | 任意函数 |
| 出现位置 | 类内声明 + 类外定义 | 声明处(必须) |
| 适用性 | 仅编译期可生成的函数 | 任何函数(包括自由函数) |
| 本质 | "请生成这个" | "不能用这个" |
完整示例
示例一:从 Rule of Three 到 Rule of Zero
场景说明:对比三种资源管理策略——手写所有函数、全部 = default、Rule of Zero。
#include <iostream>
#include <memory>
#include <vector>
#include <string>
// C++98 风格:手写所有特殊成员函数(繁琐易错)
class Course98 {
public:
Course98(std::string name) : name_(new std::string(std::move(name))) {
std::cout << "[98-构造] " << *name_ << std::endl;
}
~Course98() { delete name_; }
Course98(const Course98& other)
: name_(new std::string(*other.name_)) {
std::cout << "[98-拷贝]" << std::endl;
}
Course98& operator=(const Course98& other) {
if (this != &other) {
delete name_;
name_ = new std::string(*other.name_);
}
return *this;
}
// 没有移动语义——无法利用临时对象优化
const std::string& getName() const { return *name_; }
private:
std::string* name_; // 裸指针管理资源
};
// C++11 风格:全部 = default(但需要正确使用智能指针)
class Course11 {
public:
Course11(std::string name) : name_(std::make_unique<std::string>(std::move(name))) {
std::cout << "[11-构造] " << *name_ << std::endl;
}
// 有 unique_ptr 成员,拷贝操作被自动删除
// 移动操作自动生成(因为成员 unique_ptr 可移动)
const std::string& getName() const { return *name_; }
private:
std::unique_ptr<std::string> name_;
};
// Rule of Zero 风格:不声明任何特殊成员函数
class CourseModern {
public:
CourseModern(std::string name) : name_(std::move(name)) {
std::cout << "[Modern-构造] " << name_ << std::endl;
}
// 不需要任何特殊成员函数——编译器自动生成全部正确的
const std::string& getName() const { return name_; }
private:
std::string name_; // std::string 自身已正确管理资源
};
int main() {
std::cout << "===== C++98 风格(裸指针) =====" << std::endl;
{
Course98 c1("数据结构");
Course98 c2 = c1; // 深拷贝
std::cout << "c1: " << c1.getName() << std::endl;
std::cout << "c2: " << c2.getName() << std::endl;
}
std::cout << "\n===== C++11 风格(unique_ptr) =====" << std::endl;
{
Course11 c1("操作系统");
// Course11 c2 = c1; // 编译错误:unique_ptr 不可拷贝
Course11 c2 = std::move(c1); // 移动:c1 的资源转移给 c2
std::cout << "c2: " << c2.getName() << std::endl;
// c1 不再持有资源
}
std::cout << "\n===== Rule of Zero(std::string) =====" << std::endl;
{
CourseModern c1("计算机网络");
CourseModern c2 = c1; // 拷贝
CourseModern c3 = std::move(c2); // 移动
std::cout << "c1: " << c1.getName() << std::endl;
std::cout << "c3: " << c3.getName() << std::endl;
}
return 0;
}
预期输出:
===== C++98 风格(裸指针) =====
[98-构造] 数据结构
[98-拷贝]
c1: 数据结构
c2: 数据结构
===== C++11 风格(unique_ptr) =====
[11-构造] 操作系统
c2: 操作系统
===== Rule of Zero(std::string) =====
[Modern-构造] 计算机网络
c1: 计算机网络
c3: 计算机网络
逐段分析:
Course98使用裸指针,必须手写析构 + 拷贝构造 + 拷贝赋值(Rule of Three),且不能享受移动优化Course11使用unique_ptr——拷贝被自动删除,移动自动生成。但仍然需要写析构?不需要!unique_ptr自动释放CourseModern使用std::string——Rule of Zero:不声明任何特殊成员函数,编译器全自动生成,且全部正确- Rule of Zero 是最高目标:让每个成员自己管理资源,类本身不操心
示例二:=delete 的多种应用场景
#include <iostream>
// 场景 1:禁止拷贝
class DatabaseConnection {
public:
DatabaseConnection(const char* conn) {
std::cout << "连接数据库: " << conn << std::endl;
}
DatabaseConnection(const DatabaseConnection&) = delete;
DatabaseConnection& operator=(const DatabaseConnection&) = delete;
// 但允许移动(如果需要)
DatabaseConnection(DatabaseConnection&&) = default;
};
// 场景 2:禁止特定类型的隐式转换
void safe_int_only(int x) {
std::cout << "safe: " << x << std::endl;
}
void safe_int_only(double) = delete; // 禁止 double 重载
void safe_int_only(long) = delete; // 禁止 long 重载
// 场景 3:禁止堆上分配
class StackOnly {
public:
StackOnly() { std::cout << "StackOnly 构造" << std::endl; }
// 禁止 new
void* operator new(std::size_t) = delete;
};
// 场景 4:禁止特定模板实例化
template <typename T>
void process(T value) {
std::cout << "通用处理: " << value << std::endl;
}
template <>
void process<double>(double) = delete; // 禁止处理 double
int main() {
std::cout << "===== 场景 1: 禁止拷贝 =====" << std::endl;
DatabaseConnection db1("mysql://localhost");
// DatabaseConnection db2 = db1; // 编译错误
std::cout << "\n===== 场景 2: 禁止隐式转换 =====" << std::endl;
safe_int_only(42);
// safe_int_only(3.14); // 编译错误:double 版本被 delete
// safe_int_only(100L); // 编译错误:long 版本被 delete
std::cout << "\n===== 场景 3: 禁止堆分配 =====" << std::endl;
StackOnly so; // OK,栈上
// StackOnly* p = new StackOnly(); // 编译错误
std::cout << "\n===== 场景 4: 禁止特定模板实例化 =====" << std::endl;
process(42);
process(std::string("hello"));
// process(3.14); // 编译错误
return 0;
}
预期输出:
===== 场景 1: 禁止拷贝 =====
连接数据库: mysql://localhost
===== 场景 2: 禁止隐式转换 =====
safe: 42
===== 场景 3: 禁止堆分配 =====
StackOnly 构造
===== 场景 4: 禁止特定模板实例化 =====
通用处理: 42
通用处理: hello
逐段分析:
= delete可以应用于任意函数,不仅限于特殊成员函数- 删除
operator new可以强制对象只能在栈上创建 - 删除特定类型重载或模板特化,是更精确的"重载禁止"方案(优于 SFINAE 的可读性)
= delete与= default都可以在类外定义时使用(仅特殊成员函数)
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 移动操作被声明但拷贝操作未处理 | 拷贝被隐式删除 | 要么全部 default,要么全部自定义 |
= default 在类内时自动 inline | 影响编译/链接策略 | 若要不 inline,在类外 = default |
= delete 只写声明不写定义(C++98 风格) | 链接时错误而非编译时 | C++11 起用 = delete 直接编译时拒绝 |
模板中的 = delete 偏特化 | 语法限制 | 函数模板不支持偏特化,需用重载 + delete |
= default 在成员有 const/引用时 | 编译器无法生成 | 手写构造函数 |
常见面试问题
= default和= delete的作用和使用场景?——= default要求编译器生成默认实现(如恢复被用户声明抑制的移动操作)。= delete显式禁止使用(如禁用拷贝、阻止隐式转换)。Rule of Zero 和 Rule of Five 的关系?——Rule of Zero 是最高目标:让每个成员自行管理资源,类本身不声明任何特殊成员函数。当必须管理资源时才用 Rule of Five。
= delete和private(C++98)禁止拷贝的区别?——= delete在编译时就报错,错误信息明确;private可能延迟到链接时才报错,且友元和成员仍可访问。移动操作和拷贝操作的自动生成相互影响?——声明了移动操作,拷贝操作被隐式删除。声明了拷贝操作或析构函数,移动操作不自动生成。
哪些函数不能用
= default?——只有编译器可以生成的函数(默认构造、析构、拷贝/移动构造和赋值)才能= default。带参数的非特殊成员函数必须手写。
小结
= default显式要求编译器生成默认实现,= delete显式禁止函数使用- Rule of Zero 是现代 C++ 的最高准则:让成员自己管资源,类不声明特殊成员函数
= delete不仅是"禁止拷贝",还可禁止特定重载、特定模板实例化、特定转换- 编译器自动生成规则复杂——涉及移动时尤其容易出错,
= default让意图明确 - C++11 的
= delete优于 C++98 的 private + 不实现——编译时报错而非链接时