上海建设厅焊工证查询网站,如何进行网站建设,网页设计素材网站有哪些,策划网站建设价格类与对象三大核心函数#xff1a;构造、析构、拷贝构造详解
一、引言
在C面向对象编程中#xff0c;构造函数、析构函数和拷贝构造函数被称为三大件#xff08;Rule of Three#xff09;。它们是类设计的基石#xff0c;决定了对象的创建、拷贝和销毁行为。…类与对象三大核心函数构造、析构、拷贝构造详解一、引言在C面向对象编程中构造函数、析构函数和拷贝构造函数被称为三大件Rule of Three。它们是类设计的基石决定了对象的创建、拷贝和销毁行为。本文将通过多个实际案例深入剖析这三大核心函数的作用、原理及使用技巧。二、构造函数对象诞生的第一步2.1 构造函数的多种形态构造函数是特殊的成员函数在创建对象时自动调用。让我们看一个日期类示例class Date { public: // 1. 全缺省构造函数最常用 Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; } // 2. 无参构造函数 Date() { _year 1; _month 1; _day 1; } // 3. 带参构造函数 Date(int year, int month, int day) { _year year; _month month; _day day; } private: int _year; int _month; int _day; };⚠️重要注意事项不要同时提供无参构造函数和全缺省构造函数会导致调用不明确创建无参对象时不要加括号Date d1;✅Date d2();❌会被解析为函数声明2.2 编译器自动生成的构造函数如果没有显式定义任何构造函数编译器会自动生成一个默认构造函数。但需要注意对内置类型int、指针等不做初始化对自定义类型成员会调用其自身的默认构造函数class MyQueue { public: // 编译器默认生成构造函数会自动调用Stack的构造函数 private: Stack pushst; // 会调用Stack的构造函数 Stack popst; // 会调用Stack的构造函数 };三、析构函数对象生命的终结者3.1 析构函数的作用析构函数在对象生命周期结束时自动调用用于清理资源。对于管理动态内存的类必须自定义析构函数。class Stack { public: // 构造函数 Stack(int n 4) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr _a) { perror(malloc申请空间失败); return; } _capacity n; _top 0; } // 析构函数 ~Stack() { cout ~Stack() endl; free(_a); // 释放动态内存 _a nullptr; // 防止野指针 _top _capacity 0; } private: STDataType* _a; // 动态数组指针 size_t _capacity; size_t _top; };3.2 析构函数的自动调用当类包含自定义类型成员时其析构函数会自动调用成员的析构函数class MyQueue { public: // 即使不写析构函数编译器生成的析构函数也会 // 1. 先执行函数体如果有 // 2. 再自动调用成员的析构函数先popst后pushst ~MyQueue() { // 可以在这里添加MyQueue特有的清理工作 } // 函数体结束后会自动调用popst.~Stack()和pushst.~Stack() private: Stack pushst; Stack popst; };四、拷贝构造函数深度复制的重要性4.1 拷贝构造函数的语法拷贝构造函数用于用一个已存在的对象初始化同类型的新对象class Date { public: // ✅ 正确的拷贝构造函数声明 Date(const Date d) // 必须传引用否则无限递归 { _year d._year; _month d._month; _day d._day; } // ❌ 错误的声明参数不是引用 // Date(Date d) // 会无限递归调用自身 };必须传引用的原因如果传值需要先拷贝参数对象而拷贝参数又需要调用拷贝构造函数形成无限递归。4.2 深拷贝 vs 浅拷贝这是理解拷贝构造函数的关键让我们通过栈类的例子来说明情况一使用默认拷贝构造浅拷贝Stack st1; st1.Push(1); st1.Push(2); // 使用编译器自动生成的拷贝构造函数 Stack st2 st1; // 浅拷贝st2._a 和 st1._a 指向同一块内存 // 问题st1和st2析构时都会释放同一块内存导致双重释放程序崩溃情况二实现自定义拷贝构造深拷贝class Stack { public: // 深拷贝构造函数 Stack(const Stack st) { // 1. 申请新内存 _a (STDataType*)malloc(sizeof(STDataType) * st._capacity); if (nullptr _a) { perror(malloc申请空间失败!!!); return; } // 2. 拷贝数据 memcpy(_a, st._a, sizeof(STDataType) * st._top); // 3. 拷贝其他成员 _top st._top; _capacity st._capacity; } };深拷贝的效果Stack st1; // st1._a → [内存块A] st1.Push(1); st1.Push(2); Stack st2 st1; // st2._a → [内存块B]新申请的内存 // 内存块B的内容和内存块A相同 // 现在st1和st2有各自独立的内存可以安全析构4.3 何时需要自定义拷贝构造函数遵循Rule of Three原则如果你需要自定义析构函数那么很可能也需要自定义拷贝构造函数和拷贝赋值运算符。需要自定义拷贝构造的典型场景类包含动态分配的内存类包含文件句柄、网络连接等需要特殊管理的资源类包含指针成员且不希望共享指针指向的资源五、拷贝构造函数的调用场景理解拷贝构造函数何时被调用非常重要class Date { public: Date(int year 1, int month 1, int day 1) : _year(year), _month(month), _day(day) {} Date(const Date d) { cout 调用拷贝构造函数 endl; _year d._year; _month d._month; _day d._day; } private: int _year, _month, _day; };场景1用同类型对象初始化Date d1(2024, 7, 5); Date d2(d1); // ✅ 拷贝构造 Date d3 d1; // ✅ 拷贝构造注意这是初始化不是赋值场景2函数传值参数void Func1(Date d) // 传值参数会调用拷贝构造 { d.Print(); } Date d1(2024, 7, 5); Func1(d1); // 调用拷贝构造函数创建形参d场景3函数返回局部对象有坑// ❌ 危险返回局部对象的引用 Date Func2() { Date tmp(2024, 7, 5); return tmp; // tmp是局部变量函数结束就销毁 } // 返回的是野引用 // ✅ 正确返回对象会调用拷贝构造 Date Func3() { Date tmp(2024, 7, 5); return tmp; // 编译器可能会优化RVO/NRVO但逻辑上应该调用拷贝构造 }六、组合类的拷贝构造当类包含其他自定义类型成员时拷贝构造函数会自动调用成员的拷贝构造函数class MyQueue { private: Stack pushst; Stack popst; }; MyQueue mq1; // 编译器自动生成的拷贝构造函数会 // 1. 调用pushst的拷贝构造函数 // 2. 调用popst的拷贝构造函数 MyQueue mq2 mq1;关键点如果Stack实现了深拷贝那么MyQueue的拷贝就是安全的如果Stack没有实现深拷贝那么MyQueue的拷贝也是浅拷贝七、综合示例完整的Stack类class Stack { public: // 构造函数 Stack(int n 4) : _a(nullptr) , _capacity(0) , _top(0) { _a (STDataType*)malloc(sizeof(STDataType) * n); if (_a) { _capacity n; _top 0; } } // 拷贝构造函数深拷贝 Stack(const Stack other) : _a(nullptr) , _capacity(0) , _top(0) { if (other._a other._capacity 0) { _a (STDataType*)malloc(sizeof(STDataType) * other._capacity); if (_a) { memcpy(_a, other._a, sizeof(STDataType) * other._top); _capacity other._capacity; _top other._top; } } } // 析构函数 ~Stack() { if (_a) { free(_a); _a nullptr; } _capacity _top 0; } // 入栈操作 void Push(STDataType x) { if (_top _capacity) { int newCapacity _capacity 0 ? 4 : _capacity * 2; STDataType* tmp (STDataType*)realloc(_a, sizeof(STDataType) * newCapacity); if (tmp) { _a tmp; _capacity newCapacity; } } _a[_top] x; } private: STDataType* _a; size_t _capacity; size_t _top; };八、总结与最佳实践1. 构造函数尽量使用初始化列表进行成员初始化优先使用全缺省构造函数替代无参和多个带参构造函数对于内置类型编译器生成的默认构造函数不会初始化2. 析构函数有动态资源就必须自定义析构函数遵循谁申请谁释放原则释放后将指针置为nullptr防止野指针3. 拷贝构造函数参数必须是const引用有动态资源必须实现深拷贝遵循Rule of Three定义了析构函数通常也需要定义拷贝构造和赋值运算符4. 现代C建议考虑使用智能指针管理动态内存使用std::vector等标准容器替代手动内存管理对于不可拷贝的类型使用 delete明确删除拷贝操作class NonCopyable { public: NonCopyable() default; ~NonCopyable() default; // 禁止拷贝 NonCopyable(const NonCopyable) delete; NonCopyable operator(const NonCopyable) delete; };九、思考题为什么拷贝构造函数的参数必须是引用如果传值会发生什么什么时候编译器会自动生成拷贝构造函数深拷贝和浅拷贝的区别是什么各自适用什么场景如何避免拷贝构造带来的性能开销通过本文的学习相信你已经掌握了C类设计中三大核心函数的关键要点。合理使用它们可以避免许多常见的内存错误和逻辑错误写出更安全、更健壮的代码。深入剖析C拷贝构造的四大核心问题一、为什么拷贝构造函数的参数必须是引用如果传值会发生什么1.1 问题的本质这是拷贝构造函数设计中最重要的理解点。让我们先看错误示例class Date { public: // ❌ 错误的拷贝构造函数声明 Date(Date d) // 参数是值传递 { _year d._year; _month d._month; _day d._day; } private: int _year, _month, _day; };1.2 无限递归的发生过程当调用这个拷贝构造函数时Date d1(2024, 7, 5); Date d2(d1); // 尝试用d1构造d2调用链分析编译器要创建d2需要调用Date::Date(Date d)参数d是传值的这意味着需要复制实参d1来创建形参d复制d1到d需要调用拷贝构造函数调用拷贝构造函数又需要传值参数…无限递归开始这个过程可以用流程图表示Date d2(d1) → 调用Date(Date d) → 需要复制d1到d → 调用Date(Date d) → 需要复制d1到d → ... ↑ ↓ └──────────────────────────────────────────────────────────────┘1.3 引用传递的解决方案class Date { public: // ✅ 正确的拷贝构造函数 Date(const Date d) // 传引用不产生副本 { _year d._year; _month d._month; _day d._day; } };引用传递的优势避免无限递归引用只是别名不需要拷贝提高效率避免不必要的数据复制支持const防止意外修改原对象1.4 编译器如何阻止错误如果你不小心写成传值形式编译器会报错// error: invalid constructor; you probably meant Date (const Date) Date(Date d); // 编译错误现代编译器能识别这个常见错误并给出提示。二、什么时候编译器会自动生成拷贝构造函数2.1 自动生成的时机编译器在以下所有条件都满足时会自动生成拷贝构造函数没有显式定义拷贝构造函数没有定义移动构造函数C11及以后没有定义移动赋值运算符C11及以后示例class SimpleClass { int x; double y; char z; public: SimpleClass() default; // 编译器会自动生成 // SimpleClass(const SimpleClass other) : x(other.x), y(other.y), z(other.z) {} };2.2 自动生成的拷贝构造函数做了什么自动生成的拷贝构造函数执行成员级别的拷贝class Example { private: int a; double b; std::string c; // 自定义类型 int* ptr; // 指针 public: // 编译器生成的拷贝构造函数相当于 // Example(const Example other) // : a(other.a), // 值拷贝 // b(other.b), // 值拷贝 // c(other.c), // 调用string的拷贝构造函数 // ptr(other.ptr) {} // ❌ 危险浅拷贝 };重要特点内置类型值拷贝包括指针只拷贝地址不拷贝指向的内容自定义类型调用该类型的拷贝构造函数2.3 需要手动定义拷贝构造的场景场景原因示例动态内存管理避免浅拷贝导致双重释放vector、string等容器文件/资源句柄避免重复关闭资源文件流、数据库连接唯一资源所有权确保资源不被共享独占指针、网络连接复杂对象图需要深拷贝整个结构树、图等数据结构2.4 C11后的变化class ModernClass { public: // 如果定义其中任何一个编译器不会自动生成拷贝构造 ~ModernClass(); // 析构函数 ModernClass(ModernClass); // 移动构造 ModernClass operator(ModernClass); // 移动赋值 // 但可以显式要求编译器生成 ModernClass(const ModernClass) default; // 显式默认 ModernClass operator(const ModernClass) delete; // 显式删除 };三、深拷贝和浅拷贝的区别是什么各自适用什么场景3.1 直观对比// 浅拷贝只拷贝指针不拷贝指向的内存 int* p1 new int[100]; int* p2 p1; // 浅拷贝p1和p2指向同一内存 // 深拷贝既拷贝指针也拷贝指向的内存 int* p3 new int[100]; int* p4 new int[100]; // 新申请内存 memcpy(p4, p3, 100 * sizeof(int)); // 拷贝内容3.2 类级别的对比示例class ShallowCopy { private: int* data; int size; public: // 编译器生成的拷贝构造执行浅拷贝 // 相当于data other.data; size other.size; }; class DeepCopy { private: int* data; int size; public: // 手动实现深拷贝 DeepCopy(const DeepCopy other) { size other.size; data new int[size]; // 申请新内存 for(int i 0; i size; i) data[i] other.data[i]; // 拷贝内容 } };3.3 内存布局对比浅拷贝的内存布局原对象: [指针:0x1000] → [数据区] 副本对象: [指针:0x1000] → 指向同一数据区 问题两次delete会崩溃深拷贝的内存布局原对象: [指针:0x1000] → [数据区] 副本对象: [指针:0x2000] → [新数据区内容与原数据区相同] 安全各自独立可单独释放3.4 适用场景对比浅拷贝适用场景场景原因示例只读共享多个对象读取同一数据配置信息、常量数据无资源管理不涉及动态内存简单的值类、POD类型引用计数配合智能指针使用shared_ptr管理的对象性能优先避免拷贝开销大对象的临时引用// 适合浅拷贝的类示例 class Point // 简单的值类型 { double x, y, z; public: // 可以用浅拷贝默认生成的即可 // 没有动态资源只有基本类型 };深拷贝适用场景场景原因示例动态内存避免双重释放自定义容器、字符串文件句柄需要独立副本文件流、数据库连接网络资源避免冲突socket、连接池线程安全需要独立状态线程局部存储// 需要深拷贝的类示例 class String { char* str; size_t length; public: String(const String other) { length other.length; str new char[length 1]; // 深拷贝 strcpy(str, other.str); } ~String() { delete[] str; } };3.5 混合策略实际开发中可以根据需要采用混合策略class SmartArray { private: int* data; size_t size; mutable int* cache; // 可共享的缓存 public: // 对data深拷贝对cache浅拷贝 SmartArray(const SmartArray other) { size other.size; data new int[size]; // 深拷贝核心数据 for(size_t i 0; i size; i) data[i] other.data[i]; cache other.cache; // 浅拷贝可共享的缓存 } };四、如何避免拷贝构造带来的性能开销4.1 避免不必要的拷贝技巧1使用引用传递// ❌ 传值会有拷贝开销 void processData(Data data); // ✅ 传const引用无拷贝开销 void processData(const Data data); // 如果不需要修改原对象优先使用const引用技巧2使用移动语义C11class BigData { int* hugeArray; size_t size; public: // 移动构造函数 BigData(BigData other) noexcept : hugeArray(other.hugeArray) // 转移资源 , size(other.size) { other.hugeArray nullptr; // 置空原对象 other.size 0; } // 移动赋值 BigData operator(BigData other) noexcept { if(this ! other) { delete[] hugeArray; // 释放已有资源 hugeArray other.hugeArray; // 转移资源 size other.size; other.hugeArray nullptr; other.size 0; } return *this; } };4.2 编译器优化技术返回值优化RVOData createData() { Data d; // 直接在返回位置构造 // ... 初始化d return d; // 可能被优化不调用拷贝构造 // 编译器优化为 // 直接在调用者的栈帧上构造对象 }命名返回值优化NRVOData createData(bool flag) { Data d1, d2; if(flag) return d1; // 可能被优化 else return d2; // 可能被优化 }4.3 延迟拷贝写时复制 - Copy on Writeclass COWString { private: struct Data { char* buffer; size_t refcount; // 引用计数 size_t length; }; Data* data; // 真正的拷贝只在需要修改时发生 void detach() { if(data >4.4 使用智能指针共享资源class SharedResource { private: class Impl { // 实际的资源数据 std::vectorint data; }; std::shared_ptrImpl pImpl; // 共享实现 public: // 拷贝构造只增加引用计数 SharedResource(const SharedResource other) : pImpl(other.pImpl) // 引用计数1 {} // 修改时如果需要独立副本 void modify() { if(pImpl.use_count() 1) // 有多个引用 { pImpl std::make_sharedImpl(*pImpl); // 深拷贝 } // 现在可以安全修改 } };4.5 设计模式应用原型模式class Prototype { public: virtual ~Prototype() default; virtual std::unique_ptrPrototype clone() const 0; }; class ConcretePrototype : public Prototype { HeavyData* data; // 昂贵的数据 public: // 延迟拷贝只有调用clone时才真正拷贝 std::unique_ptrPrototype clone() const override { auto copy std::make_uniqueConcretePrototype(); if(data) copy-data >4.6 实际性能对比// 测试不同策略的性能 class TestObject { std::vectorint data; // 大量数据 public: // 传统深拷贝 TestObject(const TestObject other) : data(other.data) // 立即拷贝所有数据 {} // 移动构造 TestObject(TestObject other) noexcept : data(std::move(other.data)) // 只转移所有权 {} // 惰性拷贝 void lazyCopyFrom(const TestObject other) { if(data.empty()) data other.data; // 需要时才拷贝 } };五、总结与最佳实践5.1 拷贝构造的核心要点参数必须是const引用避免无限递归编译器自动生成浅拷贝只适合无动态资源的简单类有资源必须深拷贝遵循Rule of Three/Five考虑性能优化引用传递、移动语义、延迟拷贝5.2 现代C最佳实践class ModernResource { private: std::unique_ptrData data; // 使用智能指针 std::shared_ptrCache cache; // 共享资源 public: // 默认行为 ModernResource() default; // 禁止拷贝 ModernResource(const ModernResource) delete; ModernResource operator(const ModernResource) delete; // 允许移动 ModernResource(ModernResource) default; ModernResource operator(ModernResource) default; // 显式拷贝接口 ModernResource clone() const { ModernResource copy; if(data) copy.data std::make_uniqueData(*data); // 按需深拷贝 copy.cache cache; // 共享缓存 return copy; } };5.3 决策流程图需要拷贝功能吗 ├── 是 → 有动态资源吗 │ ├── 是 → 实现深拷贝 │ │ ├── 性能敏感 → 考虑移动语义/COW │ │ └── 共享资源 → 使用智能指针 │ └── 否 → 使用默认拷贝 │ └── 否 → 删除拷贝构造 ├── 只移动 → 实现移动语义 └── 单例模式 → 私有化拷贝构造5.4 性能优化检查清单✅ 函数参数使用const T而不是T✅ 返回局部对象时依赖编译器优化RVO/NRVO✅ 对大对象实现移动语义✅ 考虑写时复制COW模式✅ 使用智能指针管理共享资源✅ 避免不必要的拷贝操作掌握这些技巧你就能在保证正确性的同时编写出高性能的C代码。