C++默认函数生成问题详解与解决方法
1. 默认函数生成规则演变
1.1 C++03 vs C++11 vs C++14 vs C++17 vs C++20
// C++03: 三大件(拷贝构造、拷贝赋值、析构)默认生成
class Cpp03Class {
public:
// 编译器自动生成:
// 1. Cpp03Class()
// 2. ~Cpp03Class()
// 3. Cpp03Class(const Cpp03Class&)
// 4. Cpp03Class& operator=(const Cpp03Class&)
};
// C++11: 增加了移动语义和=default/=delete
class Cpp11Class {
public:
// 编译器可能自动生成:
// 1. 默认构造函数
// 2. 析构函数
// 3. 拷贝构造函数
// 4. 拷贝赋值运算符
// 5. 移动构造函数(条件复杂)
// 6. 移动赋值运算符(条件复杂)
// 规则变化:用户声明析构函数会抑制移动操作生成
virtual ~Cpp11Class() = default; // 这会抑制移动操作生成
};
// C++17: 规则进一步调整
class Cpp17Class {
public:
// 主要变化:聚合类的默认构造函数生成规则变化
};
2. 六大特殊成员函数的生成规则
2.1 默认构造函数
class DefaultConstructor {
int x;
std::string s;
public:
// 情况1:没有任何构造函数 -> 编译器生成默认构造函数
// DefaultConstructor() {}
// 情况2:有用户声明的构造函数 -> 不生成默认构造函数
DefaultConstructor(int val) : x(val) {}
// 此时需要显式声明默认构造函数
DefaultConstructor() = default;
// 情况3:有const成员或引用成员需要初始化
const int y = 0; // 有默认成员初始化器 -> 可以生成默认构造函数
// int& ref; // 错误:引用必须初始化,默认构造函数无法生成
};
// 问题:依赖编译器生成可能导致未初始化
void test_default_ctor() {
DefaultConstructor dc1; // x未初始化!危险
std::cout << dc1.x; // 未定义行为
}
解决方法:
// 方案1:显式声明=default并初始化成员
class SafeDefault {
int x = 0; // 提供默认值
std::string s;
public:
SafeDefault() = default; // 现在安全了
SafeDefault(int val) : x(val) {}
};
// 方案2:删除默认构造函数
class NoDefault {
int x;
public:
NoDefault() = delete; // 禁止默认构造
NoDefault(int val) : x(val) {}
};
2.2 拷贝构造函数
class CopyConstructor {
int* data;
size_t size;
public:
CopyConstructor(size_t s) : size(s), data(new int[s]{}) {}
// 问题:编译器生成的拷贝构造函数是浅拷贝
// CopyConstructor(const CopyConstructor& other) :
// data(other.data), size(other.size) {}
// 这会导致双重释放!
~CopyConstructor() { delete[] data; }
};
// 正确做法:自定义拷贝构造函数
class ProperCopy {
int* data;
size_t size;
public:
ProperCopy(size_t s) : size(s), data(new int[s]{}) {}
// 自定义深拷贝
ProperCopy(const ProperCopy& other) :
size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + other.size, data);
}
~ProperCopy() { delete[] data; }
// 一旦自定义了拷贝构造函数,移动构造函数就不会自动生成
};
2.3 拷贝赋值运算符
class CopyAssignment {
std::vector<int> data;
std::string name;
public:
// 编译器生成的拷贝赋值运算符:
// CopyAssignment& operator=(const CopyAssignment& other) {
// data = other.data;
// name = other.name;
// return *this;
// }
// 对于vector和string,这通常是安全的
// 问题1:自赋值问题
CopyAssignment& operator=(const CopyAssignment& other) {
if (this == &other) return *this; // 自赋值检查
data = other.data;
name = other.name;
return *this;
}
// 问题2:异常安全问题
CopyAssignment& safe_assign(const CopyAssignment& other) {
// 拷贝和交换惯用法
CopyAssignment temp(other); // 可能抛异常
swap(temp); // 不会抛异常
return *this;
}
void swap(CopyAssignment& other) noexcept {
using std::swap;
swap(data, other.data);
swap(name, other.name);
}
};
2.4 移动构造函数和移动赋值运算符
class MoveOperations {
std::unique_ptr<int[]> data;
size_t size;
public:
MoveOperations(size_t s) : size(s), data(std::make_unique<int[]>(s)) {}
// 移动构造函数不会自动生成的条件:
// 1. 用户声明了拷贝操作(构造或赋值)
// 2. 用户声明了移动操作
// 3. 用户声明了析构函数
// 正确做法:显式声明移动操作
MoveOperations(MoveOperations&& other) noexcept
: data(std::move(other.data)), size(other.size) {
other.size = 0;
}
MoveOperations& operator=(MoveOperations&& other) noexcept {
if (this != &other) {
data = std::move(other.data);
size = other.size;
other.size = 0;
}
return *this;
}
// 注意:声明了移动操作会抑制拷贝操作的生成
// 需要显式声明拷贝操作(如果需要)
MoveOperations(const MoveOperations& other)
: size(other.size), data(std::make_unique<int[]>(other.size)) {
std::copy(other.data.get(), other.data.get() + size, data.get());
}
};
2.5 析构函数
class DestructorIssues {
int* data;
public:
DestructorIssues() : data(new int[100]) {}
// 问题:编译器生成的析构函数不会delete[]
// ~DestructorIssues() {} // 内存泄漏!
// 正确:自定义析构函数
~DestructorIssues() {
delete[] data;
}
// 重要:声明析构函数会抑制移动操作生成
// 如果需要移动操作,必须显式声明
};
// 虚析构函数问题
class Base {
public:
// 如果没有虚函数,不需要虚析构函数
~Base() = default; // 非虚
// 如果有虚函数,需要虚析构函数
virtual void foo() {}
virtual ~Base() = default; // 应该是虚的
};
class Derived : public Base {
public:
~Derived() override {
// 清理派生类资源
}
};
3. Rule of Zero/Three/Five
3.1 Rule of Zero(现代C++首选)
// Rule of Zero:让编译器自动生成所有特殊成员函数
class RuleOfZero {
// 使用管理资源的类(智能指针、容器等)
std::unique_ptr<int[]> data;
std::vector<int> items;
std::string name;
public:
// 不需要声明任何特殊成员函数
// 编译器会自动生成正确的拷贝/移动操作
RuleOfZero(std::string n, size_t count)
: name(std::move(n)), data(std::make_unique<int[]>(count)), items(count) {}
// 编译器生成的函数:
// 1. ~RuleOfZero()
// 2. RuleOfZero(const RuleOfZero&) // 调用成员的拷贝构造
// 3. RuleOfZero(RuleOfZero&&) // 调用成员的移动构造
// 4. operator=(const RuleOfZero&) // 调用成员的拷贝赋值
// 5. operator=(RuleOfZero&&) // 调用成员的移动赋值
};
3.2 Rule of Three(C++98传统)
// Rule of Three:如果需要自定义析构函数,也需要自定义拷贝构造和拷贝赋值
class RuleOfThree {
char* data;
size_t size;
public:
RuleOfThree(const char* str) : size(std::strlen(str)) {
data = new char[size + 1];
std::strcpy(data, str);
}
// 1. 自定义析构函数
~RuleOfThree() {
delete[] data;
}
// 2. 自定义拷贝构造函数
RuleOfThree(const RuleOfThree& other) : size(other.size) {
data = new char[size + 1];
std::strcpy(data, other.data);
}
// 3. 自定义拷贝赋值运算符
RuleOfThree& operator=(const RuleOfThree& other) {
if (this != &other) {
delete[] data; // 释放原有资源
size = other.size;
data = new char[size + 1];
std::strcpy(data, other.data);
}
return *this;
}
// 问题:没有移动操作,效率低
};
3.3 Rule of Five(C++11推荐)
// Rule of Five:自定义析构函数时,五个特殊成员函数都要考虑
class RuleOfFive {
int* data;
size_t size;
public:
RuleOfFive(size_t s = 0) : size(s), data(size ? new int[size]{} : nullptr) {}
// 1. 析构函数
~RuleOfFive() {
delete[] data;
}
// 2. 拷贝构造函数
RuleOfFive(const RuleOfFive& other) : size(other.size),
data(size ? new int[size] : nullptr) {
if (data) {
std::copy(other.data, other.data + size, data);
}
}
// 3. 拷贝赋值运算符(使用拷贝和交换惯用法)
RuleOfFive& operator=(RuleOfFive other) { // 按值传递!
swap(*this, other);
return *this;
}
// 4. 移动构造函数
RuleOfFive(RuleOfFive&& other) noexcept
: data(nullptr), size(0) {
swap(*this, other);
}
// 5. 移动赋值运算符(通过拷贝赋值处理)
// 已经通过operator=的按值传递版本处理了
friend void swap(RuleOfFive& first, RuleOfFive& second) noexcept {
using std::swap;
swap(first.data, second.data);
swap(first.size, second.size);
}
};
4. 常见问题与陷阱
4.1 问题:析构函数抑制移动操作
class Problematic {
std::vector<int> data;
public:
Problematic() = default;
// 用户声明的析构函数(即使是=default)
virtual ~Problematic() = default; // 这会抑制移动操作生成!
// 结果:编译器不会生成移动构造函数和移动赋值运算符
// 但是会生成拷贝操作
// 意外行为:
// Problematic p1;
// Problematic p2 = std::move(p1); // 调用拷贝构造函数,不是移动!
};
// 解决方法1:显式声明移动操作
class FixedWithExplicitMove {
std::vector<int> data;
public:
FixedWithExplicitMove() = default;
// 显式声明所有特殊成员函数
~FixedWithExplicitMove() = default;
FixedWithExplicitMove(const FixedWithExplicitMove&) = default;
FixedWithExplicitMove& operator=(const FixedWithExplicitMove&) = default;
// 关键:显式声明移动操作
FixedWithExplicitMove(FixedWithExplicitMove&&) = default;
FixedWithExplicitMove& operator=(FixedWithExplicitMove&&) = default;
};
// 解决方法2:使用final类(如果不是多态基类)
class FixedFinal final {
std::vector<int> data;
public:
// 如果不作为基类,不需要虚析构函数
~FixedFinal() = default; // 非虚析构函数不会抑制移动操作
};
4.2 问题:const成员阻止移动赋值
class ConstMemberProblem {
const int id; // const成员!
std::string name;
public:
ConstMemberProblem(int i, std::string n) : id(i), name(std::move(n)) {}
// 问题:const成员阻止默认移动赋值运算符生成
// 因为const成员不能被赋值
// 编译器不会生成:
// ConstMemberProblem& operator=(ConstMemberProblem&&);
// 但会生成拷贝赋值运算符?不会!因为const成员也不能被拷贝赋值
// ConstMemberProblem& operator=(const ConstMemberProblem&) = delete;
};
// 解决方法:重新设计,避免const数据成员
class ConstMemberSolution {
int id;
std::string name;
public:
ConstMemberSolution(int i, std::string n) : id(i), name(std::move(n)) {}
// 提供const访问器
int get_id() const { return id; }
// 现在可以生成所有特殊成员函数
// 或者使用=default显式声明
ConstMemberSolution() = default;
~ConstMemberSolution() = default;
ConstMemberSolution(const ConstMemberSolution&) = default;
ConstMemberSolution(ConstMemberSolution&&) = default;
ConstMemberSolution& operator=(const ConstMemberSolution&) = default;
ConstMemberSolution& operator=(ConstMemberSolution&&) = default;
};
4.3 问题:引用成员阻止赋值操作
class ReferenceMemberProblem {
int& ref; // 引用成员
std::string name;
public:
ReferenceMemberProblem(int& r, std::string n) : ref(r), name(std::move(n)) {}
// 问题:引用成员阻止默认拷贝/移动赋值运算符生成
// 因为引用一旦初始化就不能重新绑定
// 所有赋值运算符都被删除
};
// 解决方法1:使用指针代替引用
class ReferenceSolutionPointer {
int* ptr;
std::string name;
public:
ReferenceSolutionPointer(int& r, std::string n)
: ptr(&r), name(std::move(n)) {}
int& get() { return *ptr; }
const int& get() const { return *ptr; }
// 现在可以生成赋值运算符
// 但要注意指针的语义
};
// 解决方法2:使用std::reference_wrapper
#include <functional>
class ReferenceSolutionWrapper {
std::reference_wrapper<int> ref;
std::string name;
public:
ReferenceSolutionWrapper(int& r, std::string n)
: ref(r), name(std::move(n)) {}
int& get() { return ref.get(); }
const int& get() const { return ref.get(); }
// std::reference_wrapper是可赋值的
ReferenceSolutionWrapper& operator=(const ReferenceSolutionWrapper& other) {
ref = other.ref; // 重新绑定引用
name = other.name;
return *this;
}
};
4.4 问题:默认函数与继承
class Base {
protected:
std::string data;
public:
Base() = default;
virtual ~Base() = default; // 虚析构函数
// 声明虚析构函数抑制了移动操作生成
Base(const Base&) = default;
Base& operator=(const Base&) = default;
// 需要显式声明移动操作
Base(Base&&) = default;
Base& operator=(Base&&) = default;
};
class Derived : public Base {
std::vector<int> extra;
public:
// 问题:派生类的特殊成员函数
// 默认生成的派生类拷贝构造函数会调用基类的拷贝构造函数
// 默认生成的派生类移动构造函数会调用基类的移动构造函数(如果有)
// 但如果基类没有移动操作,派生类移动会退化为拷贝
Derived(Derived&& other)
: Base(std::move(other)), // 如果Base没有移动构造函数,调用拷贝构造函数
extra(std::move(other.extra)) {}
// 正确做法:确保基类有移动操作,或显式实现派生类移动操作
};
5. 高级技巧与模式
5.1 CRTP中的默认函数生成
template<typename Derived>
class CRTPBase {
protected:
~CRTPBase() = default; // 保护析构函数,防止切片
public:
// 提供通用的clone方法
Derived clone() const {
return Derived(static_cast<const Derived&>(*this));
}
// 注意:基类析构函数非虚,但CRTP是静态多态
};
class CRTPDerived : public CRTPBase<CRTPDerived> {
std::vector<int> data;
public:
CRTPDerived() = default;
// 需要显式定义拷贝构造函数,因为基类拷贝构造函数不可访问
CRTPDerived(const CRTPDerived& other)
: CRTPBase<CRTPDerived>(other), data(other.data) {}
// 同样需要移动构造函数
CRTPDerived(CRTPDerived&& other) noexcept
: CRTPBase<CRTPDerived>(std::move(other)),
data(std::move(other.data)) {}
// 赋值运算符
CRTPDerived& operator=(CRTPDerived other) {
swap(*this, other);
return *this;
}
friend void swap(CRTPDerived& a, CRTPDerived& b) noexcept {
using std::swap;
swap(a.data, b.data);
}
};
5.2 PIMPL模式中的默认函数
// 头文件
class PimplClass {
class Impl;
std::unique_ptr<Impl> pimpl;
public:
// 特殊成员函数需要用户声明
PimplClass();
~PimplClass(); // 需要在实现文件中定义,因为Impl是不完整类型
// 移动操作可以默认
PimplClass(PimplClass&&) noexcept = default;
PimplClass& operator=(PimplClass&&) noexcept = default;
// 拷贝操作需要自定义
PimplClass(const PimplClass&);
PimplClass& operator=(const PimplClass&);
// 交换操作
friend void swap(PimplClass& a, PimplClass& b) noexcept {
a.swap(b);
}
private:
void swap(PimplClass& other) noexcept {
using std::swap;
swap(pimpl, other.pimpl);
}
};
// 实现文件
class PimplClass::Impl {
// 实现细节
public:
std::vector<int> data;
std::string name;
Impl() = default;
~Impl() = default;
Impl(const Impl&) = default;
Impl(Impl&&) = default;
Impl& operator=(const Impl&) = default;
Impl& operator=(Impl&&) = default;
};
PimplClass::PimplClass() : pimpl(std::make_unique<Impl>()) {}
// 必须在Impl完整定义后定义
PimplClass::~PimplClass() = default;
PimplClass::PimplClass(const PimplClass& other)
: pimpl(other.pimpl ? std::make_unique<Impl>(*other.pimpl) : nullptr) {}
PimplClass& PimplClass::operator=(const PimplClass& other) {
if (this != &other) {
if (other.pimpl) {
if (pimpl) {
*pimpl = *other.pimpl; // 拷贝赋值
} else {
pimpl = std::make_unique<Impl>(*other.pimpl);
}
} else {
pimpl.reset();
}
}
return *this;
}
5.3 使用concept约束默认函数(C++20)
template<typename T>
concept MoveEnabled = std::is_move_constructible_v<T> &&
std::is_move_assignable_v<T>;
template<typename T>
concept CopyEnabled = std::is_copy_constructible_v<T> &&
std::is_copy_assignable_v<T>;
template<typename T>
class SafeContainer {
std::vector<T> data;
public:
SafeContainer() = default;
// 条件性生成特殊成员函数
SafeContainer(const SafeContainer&) requires CopyEnabled<T> = default;
SafeContainer& operator=(const SafeContainer&) requires CopyEnabled<T> = default;
SafeContainer(SafeContainer&&) requires MoveEnabled<T> = default;
SafeContainer& operator=(SafeContainer&&) requires MoveEnabled<T> = default;
// 析构函数总是生成
~SafeContainer() = default;
// 如果没有满足条件,构造函数被删除
};
6. 调试与检测技术
6.1 检测特殊成员函数的生成状态
#include <type_traits>
template<typename T>
void check_special_members() {
std::cout << "Type: " << typeid(T).name() << "\n";
std::cout << "Default constructible: "
<< std::is_default_constructible_v<T> << "\n";
std::cout << "Copy constructible: "
<< std::is_copy_constructible_v<T> << "\n";
std::cout << "Move constructible: "
<< std::is_move_constructible_v<T> << "\n";
std::cout << "Copy assignable: "
<< std::is_copy_assignable_v<T> << "\n";
std::cout << "Move assignable: "
<< std::is_move_assignable_v<T> << "\n";
std::cout << "Destructible: "
<< std::is_destructible_v<T> << "\n";
std::cout << "Trivially destructible: "
<< std::is_trivially_destructible_v<T> << "\n";
std::cout << "------------------------\n";
}
// 测试类
class TestClass {
public:
TestClass() = default;
~TestClass() = default;
// 注释/取消注释以下行来测试不同情况
// TestClass(const TestClass&) = delete;
// TestClass(TestClass&&) = delete;
};
void test_detection() {
check_special_members<TestClass>();
}
6.2 使用静态断言确保正确性
template<typename T>
class ResourceWrapper {
T* resource;
public:
ResourceWrapper() : resource(nullptr) {}
explicit ResourceWrapper(T* res) : resource(res) {}
~ResourceWrapper() {
delete resource;
}
// 确保资源管理类不可拷贝
ResourceWrapper(const ResourceWrapper&) = delete;
ResourceWrapper& operator=(const ResourceWrapper&) = delete;
// 但允许移动
ResourceWrapper(ResourceWrapper&& other) noexcept
: resource(other.resource) {
other.resource = nullptr;
}
ResourceWrapper& operator=(ResourceWrapper&& other) noexcept {
if (this != &other) {
delete resource;
resource = other.resource;
other.resource = nullptr;
}
return *this;
}
// 静态断言确保移动操作是noexcept
static_assert(noexcept(ResourceWrapper(std::declval<ResourceWrapper&&>())),
"Move constructor must be noexcept");
static_assert(noexcept(std::declval<ResourceWrapper&>() =
std::declval<ResourceWrapper&&>()),
"Move assignment must be noexcept");
};
7. 最佳实践总结
7.1 现代C++默认函数生成指南
// 1. 优先使用Rule of Zero
class ModernClass {
std::vector<int> data; // 管理资源
std::unique_ptr<Database> db; // 管理资源
std::string name; // 管理资源
// 不要声明任何特殊成员函数
// 让编译器自动生成正确的版本
public:
ModernClass(std::string n) : name(std::move(n)) {}
// 编译器生成:
// 1. 默认构造函数(因为成员都有默认初始化或默认构造函数)
// 2. 析构函数
// 3. 拷贝操作(深拷贝,调用成员的拷贝操作)
// 4. 移动操作(高效移动,调用成员的移动操作)
};
// 2. 需要自定义行为时使用Rule of Five
class CustomBehavior {
int* raw_ptr;
size_t size;
public:
explicit CustomBehavior(size_t sz) : size(sz), raw_ptr(new int[sz]{}) {}
// 必须自定义析构函数
~CustomBehavior() { delete[] raw_ptr; }
// 一旦自定义析构函数,必须考虑所有五个特殊成员函数
CustomBehavior(const CustomBehavior& other)
: size(other.size), raw_ptr(new int[other.size]) {
std::copy(other.raw_ptr, other.raw_ptr + size, raw_ptr);
}
CustomBehavior& operator=(CustomBehavior other) {
swap(*this, other);
return *this;
}
CustomBehavior(CustomBehavior&& other) noexcept
: raw_ptr(nullptr), size(0) {
swap(*this, other);
}
// 移动赋值通过拷贝赋值处理
friend void swap(CustomBehavior& a, CustomBehavior& b) noexcept {
using std::swap;
swap(a.raw_ptr, b.raw_ptr);
swap(a.size, b.size);
}
};
// 3. 使用=default显式声明意图
class ExplicitIntent {
std::vector<int> data;
public:
// 明确声明所有特殊成员函数
ExplicitIntent() = default;
~ExplicitIntent() = default;
// 明确拷贝语义
ExplicitIntent(const ExplicitIntent&) = default;
ExplicitIntent& operator=(const ExplicitIntent&) = default;
// 明确移动语义(即使有析构函数)
ExplicitIntent(ExplicitIntent&&) = default;
ExplicitIntent& operator=(ExplicitIntent&&) = default;
// 添加其他构造函数
ExplicitIntent(std::initializer_list<int> init) : data(init) {}
};
// 4. 使用=delete禁止不需要的操作
class NonCopyable {
std::unique_ptr<int> resource;
public:
NonCopyable() = default;
// 允许移动
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
// 禁止拷贝
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
};
// 5. 对于多态基类,使用虚析构函数并显式声明移动操作
class PolymorphicBase {
protected:
std::vector<int> base_data;
public:
PolymorphicBase() = default;
virtual ~PolymorphicBase() = default; // 虚析构函数
// 显式声明所有特殊成员函数
PolymorphicBase(const PolymorphicBase&) = default;
PolymorphicBase& operator=(const PolymorphicBase&) = default;
// 关键:显式声明移动操作
PolymorphicBase(PolymorphicBase&&) = default;
PolymorphicBase& operator=(PolymorphicBase&&) = default;
virtual void do_something() = 0;
};
7.2 决策流程图
开始设计类
│
├── 类管理资源吗?
│ ├── 否 → 使用Rule of Zero(让编译器生成一切)
│ └── 是 →
│ ├── 资源可以被标准库类型管理吗?
│ │ ├── 是 → 使用标准库类型,回到Rule of Zero
│ │ └── 否 → 需要自定义资源管理
│ │ ├── 需要拷贝语义吗?
│ │ │ ├── 是 → 实现Rule of Five
│ │ │ └── 否 → 仅实现移动语义(删除拷贝操作)
│ │ └── 是继承层次的一部分吗?
│ │ ├── 是 → 确保基类有虚析构函数和移动操作
│ │ └── 否 → 不需要虚析构函数
│ └── 类有const/引用成员吗?
│ ├── 是 → 重新设计或自定义赋值操作
│ └── 否 → 正常实现
│
├── 类需要多态吗?
│ ├── 是 →
│ │ ├── 基类:虚析构函数 + 显式声明移动操作
│ │ └── 派生类:确保正确处理基类部分
│ └── 否 → 不需要虚析构函数
│
└── 最终检查:
├── 所有特殊成员函数都正确声明了吗?
├── 移动操作是noexcept吗?
├── 自赋值安全吗?
└── 异常安全吗?
通过遵循这些最佳实践和理解默认函数生成的规则,可以避免许多常见的内存管理错误和性能问题,编写出更安全、更高效的C++代码。
1万+

被折叠的 条评论
为什么被折叠?



