C++defaulted与deleted函数

2026-04-11 19:30:30 564阅读 0评论

defaulteddeleted:C++里那两个“不干活却很忙”的函数

写C++时,你有没有过这种时刻:类里啥都没写,编译器却悄悄给你塞了拷贝构造、赋值操作——结果某天对象被意外复制,程序在深夜core dump,而你翻了半小时代码才意识到,问题出在那个你根本没声明、更没定义的默认拷贝函数上?

这正是 defaulteddeleted 出场的理由。它们不是语法糖,也不是炫技工具;它们是C++11给程序员配发的“函数控制权移交协议”——让你明确告诉编译器:“这个函数,我接管了;它该干啥,或不该干啥,我说了算。”


默认函数,从来就不“默认”

很多人误以为 = default 是“让编译器生成默认实现”,其实更准确的理解是:它显式启用编译器自动生成的特殊成员函数,且仅当该函数本就符合隐式生成条件时才合法。
比如:

struct Widget {
    std::string name;
    int id = 0;
    Widget() = default;           // ✅ 合法:无用户自定义构造,编译器本就会生成
    Widget(const Widget&) = default; // ✅ 同理
};

但如果你加了自定义析构函数:

struct BadWidget {
    ~BadWidget() { /* 自定义清理 */ }
    Widget(const BadWidget&) = default; // ❌ 编译错误!
};

为什么?因为一旦你声明了析构函数(哪怕只是空实现),编译器就不再自动生成拷贝构造和拷贝赋值——这是C++98遗留的保守策略。= default 并不能“绕过规则”,它只是“申请启用”,而编译器会严格审核:你申请的函数,在当前类定义下是否仍满足隐式生成的前提条件? 不满足,直接报错。这不是bug,是安全阀。


= delete:不是禁用,而是“精准封印”

delete 最常被当作“禁止拷贝”的快捷键,比如:

class NonCopyable {
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
public:
    NonCopyable() = default;
};

但它的真正价值,远不止于此。delete 的核心能力,是参与重载决议,并在匹配阶段主动失败——这意味着它能干掉“危险但合法”的隐式转换。

举个真实场景:你写了一个接受 int 的构造函数,想支持 Widget w(42);,但又不希望 Widget w = 42; 这种隐式转换发生(毕竟后者会触发拷贝初始化,可能绕过你的资源管理逻辑):

class Widget {
public:
    explicit Widget(int x) : val(x) {}
    // 错误示范:只加 explicit,仍允许 Widget w = 42;(编译器会尝试隐式转换+拷贝)
    // 正确做法:
    Widget(double) = delete;  // 封印所有浮点数隐式转换
    Widget(const char*) = delete; // 封印字符串字面量误用
private:
    int val;
};

这里 = delete 不是“补漏”,而是主动设计接口边界:它让 Widget w = 3.14; 在编译期就失败,错误信息清晰指向“调用了已删除的函数”,比运行时断言或静默截断强得多。


组合拳:defaulted + deleted 解决真实矛盾

最典型的冲突场景:移动语义引入后,拷贝与移动的共存规则变得微妙。假设你有一个管理动态内存的类:

class Buffer {
    char* data_;
    size_t size_;
public:
    Buffer(Buffer&& other) noexcept 
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // 问题来了:拷贝构造要不要?如果删掉,用户无法拷贝;
    // 如果留着默认,编译器不会生成(因移动构造存在),但你也没写——结果连拷贝都不可用。
    Buffer(const Buffer&) = delete; // 明确拒绝拷贝
    Buffer& operator=(const Buffer&) = delete;

    // 但移动赋值呢?必须显式定义,否则默认生成的可能不安全
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }
};

注意:移动赋值不能 = default ——因为 = default 会生成逐成员移动,而 char* 的移动本质仍是浅拷贝,析构时双重释放风险极高。这里必须手写,= delete 则用于彻底关闭你不信任的操作路径。


写在最后:它们不是修饰符,是契约

defaulteddeleted 看似轻巧,实则是C++对“意图明确性”的一次郑重升级。
它们逼你直面一个问题:这个函数,是应该存在、不存在,还是以某种特定方式存在?

不写任何声明,靠编译器猜;写了 = default,是你确认“这个默认行为对我安全”;写了 = delete,是你宣告“这个行为在此上下文中没有意义,甚至有害”。

下次当你犹豫要不要删掉某个拷贝函数,或者纠结移动操作该不该手写时,别急着查文档——先问自己一句:我此刻,是在让编译器干活,还是在和它签一份责任契约?

文章版权声明:除非注明,否则均为Dark零点博客原创文章,转载或复制请以超链接形式并注明出处。

发表评论

快捷回复: 表情:
验证码
评论列表 (暂无评论,564人围观)

还没有评论,来说两句吧...

目录[+]