☰
C++ 取地址符相关的默认函数
2026/11/4 3:11:15 网站建设 项目流程

前言


这个标题的措辞比较含糊,需要先界定范围。C++ 里与「取地址符」和「默认函数」相关的知识其实分属三块:一是一元&运算符可以被重载;二是编译器为类隐式声明、也可以用= default显式恢复的特殊成员函数;三是 C++11 引入的std::addressof,它能在&被重载时仍然拿到对象的真实地址。三者常在同一段代码里相遇,因此放在一起讲。


典型误解有两个。第一,以为&obj永远返回对象地址——只要类重载了一元operator&,&obj调用的就是那个重载,返回值可以是任何东西。第二,以为写了~T() = default;和没写一样——只要析构函数被「用户声明」,编译器就不再隐式生成移动构造函数,这个差异会直接影响性能与可移动性。


本文先讲operator&的重载规则与std::addressof的必要性,再讲= default对特殊成员函数声明的影响,最后用一个资源包装类把两者串起来。代码以 C++17 为基准。


一、一元&可以被重载


C++ 允许重载一元取地址运算符,形式可以是成员函数,也可以是非成员函数:


// 适用于 C++17
#include <iostream>

class Tracked {
public:
Tracked* operator&() {
std::cout << "non-const address-of\n";
return this;
}
const Tracked* operator&() const {
std::cout << "const address-of\n";
return this;
}
};

int main() {
Tracked a;
const Tracked b;

Tracked* pa = &a; // 调用非 const 重载
const Tracked* pb = &b; // 调用 const 重载
std::cout << (pa == &a) << " " << (pb == &b) << "\n";
return 0;
}

成员形式的一元operator&没有参数,返回类型任意(通常写成指针)。非成员形式接收一个参数:


// 适用于 C++17
Tracked* operator&(Tracked& obj) { return &obj; } // 注意:内部再用 & 会递归

上面这行如果真这么写,函数体里的&obj又会调用operator&,形成无限递归。正确写法是借助std::addressof或reinterpret_cast,这正好引出下一节。


需要区分一元与二元:a & b是按位与,&a是取地址,两者是不同的运算符,可以分别重载。重载了一元&不会影响按位与,反之亦然。


二、std::addressof与它解决的问题


std::addressof定义在<memory>中,C++11 起可用,签名大致是:


// 适用于 C++11 及以上
template <class T>
T* addressof(T& arg) noexcept;

它返回arg的真实地址,即使T重载了一元operator&。主流实现通过编译器内建函数(如__builtin_addressof)完成,因此没有递归问题。


// 适用于 C++17
#include <memory>
#include <iostream>

class Tracked {
public:
Tracked* operator&() { return nullptr; } // 故意劫持,返回空指针
};

int main() {
Tracked a;
Tracked* hijacked = &a; // 调用重载,得到 nullptr
Tracked* real = std::addressof(a); // 绕过重载,拿到真实地址
std::cout << (hijacked == nullptr) << " " << (real != nullptr) << "\n";
return 0;
}

这个例子把重载写成返回nullptr,只是为了把差异放大到肉眼可见。结论是:泛型代码中需要真实地址时,一律用std::addressof。


为什么标准库关心这件事?因为std::vector、std::unique_ptr、std::function等模板内部需要取元素的地址来构造指针或做定位,如果元素的类型重载了&并返回了别的东西,直接用&就会得到错误结果。标准库实现因此在内部统一使用std::addressof。


那么,重载一元&合理吗?极少数场景有正当理由,例如代理对象(proxy)希望&proxy返回它所引用的真实对象地址,或者智能指针希望&sp返回内部指针。但这类设计会让代码对读者产生强烈误导,现代 C++ 更倾向于用显式的get()方法表达,而不是劫持运算符。除非有非常明确的理由,不要重载一元&。


三、= default与隐式声明的特殊成员函数


C++ 的类有六个特殊成员函数:默认构造、拷贝构造、拷贝赋值、移动构造、移动赋值、析构。编译器会在满足条件时隐式声明它们。= default的作用是「显式要求编译器生成默认实现」,= delete则是「明确禁用」。


// 适用于 C++17
#include <string>
#include <vector>

class Record {
public:
Record() = default;
Record(const Record&) = default;
Record& operator=(const Record&) = default;
Record(Record&&) = default;
Record& operator=(Record&&) = default;
~Record() = default;

private:
std::string name_;
std::vector<int> values_;
};

对一个只含std::string与std::vector的类,其实一个都不用写——编译器隐式生成的版本已经正确,这就是「零规则」(rule of zero)。= default在这里的价值是显式表达意图,同时保持函数「非用户提供」(not user-provided),从而不破坏类型的三 trivial 性与聚合性质。


关键细节在于「用户声明」与「用户提供」的区别:




写法是否用户声明是否用户提供对隐式移动的影响



不写否否正常隐式生成

~Record() = default;(类内首次声明)是否抑制隐式移动构造/赋值

~Record() {}是是抑制隐式移动,且析构非平凡

~Record();类外Record::~Record() = default;是是抑制隐式移动



也就是说,只要你在类内声明了析构函数(哪怕写成= default),编译器就不会再隐式声明移动构造函数和移动赋值运算符。此后对Record做std::move,实际调用的是拷贝构造——因为移动版本不存在,重载决议回退到const T&的拷贝构造。这是一个非常隐蔽的性能陷阱:代码看起来用了移动,实际发生了深拷贝。


如果确实需要自定义析构(例如要释放裸资源),就必须把移动构造和移动赋值也显式= default或自行实现,这就是「五规则」(rule of five)。更好的做法是把资源交给std::unique_ptr等 RAII 成员,让析构保持隐式,从而回到零规则。


= default还可以写在类外,但那时函数被视为「用户提供」,语义与类内首次声明不同。日常编码建议一律写在类内首次声明处。


C++20 起,= default还可用于默认化的比较运算符,例如bool operator==(const T&) const = default;与auto operator<=>(const T&) const = default;,编译器会按成员逐级生成比较。这属于 C++20 特性,使用前需确认工具链支持。


四、把两者放进一个例子


下面这个类同时涉及取地址重载与默认函数:它包装一个堆上的整数,提供显式的get(),并且不重载&——这是推荐做法。


// 适用于 C++17
#include <memory>
#include <iostream>

class IntHolder {
public:
IntHolder() = default;
explicit IntHolder(int value) : value_(new int(value)) {}

// 析构由 unique_ptr 负责,无需手写,保持零规则
int value() const { return value_ ? *value_ : 0; }
void reset(int value) { value_.reset(new int(value)); }

private:
std::unique_ptr<int> value_;
};

int main() {
IntHolder a(7);
IntHolder b = std::move(a); // 移动构造隐式可用
std::cout << b.value() << "\n";

IntHolder* p = std::addressof(b); // 即使将来重载 &,这行仍然正确
std::cout << (p != nullptr) << "\n";
return 0;
}

IntHolder没有声明析构、拷贝、移动中的任何一个,编译器隐式生成移动构造与移动赋值(因为成员unique_ptr可移动),拷贝构造被隐式删除(因为unique_ptr不可拷贝)。std::move(a)因此走的是移动构造,a进入「有效但未指定」状态,此后不应再读取它的值。


std::addressof(b)在这里与&b等价,因为IntHolder没有重载&;写成addressof是为了让代码在未来加入重载时仍然正确,也让读者明确知道这里要的是真实地址。


常见坑点


坑点一:以为&obj一定返回对象地址。


// ❌ 错误:类重载了 operator&,返回值可能不是 this
SomeProxy p;
void* addr = &p;

// ✅ 正确:需要真实地址时用 std::addressof
void* addr = std::addressof(p);

坑点二:在重载的operator&内部再用&,造成递归。


// ❌ 错误:函数体里的 &obj 再次调用自身
Tracked* operator&(Tracked& obj) { return &obj; }

// ✅ 正确:用 std::addressof 或内建手段
Tracked* operator&(Tracked& obj) { return std::addressof(obj); }

坑点三:随手重载一元&以「美化」接口。


// ❌ 错误:让 &obj 返回内部指针,泛型代码拿到错误地址
class Smart { public: int* operator&() { return raw_; } };

// ✅ 正确:提供显式的 get(),保持 & 的常规语义
class Smart { public: int* get() { return raw_; } };

坑点四:声明了= default析构,却以为移动仍然隐式生成。


// ❌ 错误:~T() = default 抑制了隐式移动,std::move 实际走拷贝
class T {
public:
~T() = default;
std::vector<int> data;
};

// ✅ 正确:需要移动就显式声明,或干脆不写析构
class T {
public:
T(T&&) = default;
T& operator=(T&&) = default;
~T() = default;
std::vector<int> data;
};

坑点五:把= default写在类外,误以为与类内等价。


// ❌ 错误:类外默认化属于「用户提供」,语义不同
class T { public: T(); };
T::T() = default;

// ✅ 正确:在类内首次声明处默认化
class T { public: T() = default; };

坑点六:用= delete禁用拷贝后忘记提供移动。


// ❌ 错误:拷贝被删除,移动也未声明,对象无法转移
class Handle {
public:
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
};

// ✅ 正确:同时声明移动操作
class Handle {
public:
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
Handle(Handle&&) = default;
Handle& operator=(Handle&&) = default;
};

坑点七:对const对象使用std::move期待移动。


// ❌ 错误:const 对象只能绑定到 const T&,退化为拷贝
const std::string s = "hello";
std::string t = std::move(s);

// ✅ 正确:去掉 const,或接受这是一次拷贝
std::string s = "hello";
std::string t = std::move(s);

总结




主题要点



一元&可重载,成员形式无参,非成员形式接收一个参数

std::addressofC++11,绕过重载拿真实地址,泛型代码应默认使用

= default显式要求编译器生成,类内首次声明处使用

用户声明析构会抑制隐式移动构造与移动赋值

零规则用 RAII 成员,不手写特殊成员函数

五规则一旦自定义析构/拷贝/移动之一,通常五个都要考虑



「取地址符」与「默认函数」看似不相关,实际都指向同一个原则:不要依赖隐式行为,也不要用运算符重载去制造惊喜。需要真实地址就写std::addressof,需要特殊成员函数就显式写出并想清楚它对其他成员的影响,代码的可预测性会好很多。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询