拷贝构造函数的基本概念
定义
拷贝构造函数是一种特殊的构造函数,用于创建一个新对象作为现有对象的副本。在 C++ 中,当对象以值传递的方式传入函数,或从函数返回时,或用一个对象初始化另一个对象时,拷贝构造函数会被调用。
语法
ClassName (const ClassName &old_obj);
何时使用拷贝构造函数
- 对象作为参数传递给函数(按值传递)
- 对象从函数返回(按值返回)
- 对象需要通过另一个对象进行初始化
示例代码
假设我们有一个简单的类 Point,它有一个拷贝构造函数。
#include <iostream> using namespace std; class Point { private: int x, y; public: Point(int x1, int y1) { // 普通构造函数 x = x1; y = y1; } // 拷贝构造函数 Point(const Point &p2) { x = p2.x; y = p2.y; } int getX() { return x; } int getY() { return y; } }; int main() { Point p1(10, 15); // 普通构造函数被调用 Point p2 = p1; // 拷贝构造函数被调用 cout << "p1.x = " << p1.getX() << ", p1.y = " << p1.getY(); cout << "\np2.x = " << p2.getX() << ", p2.y = " << p2.getY(); return 0; }
运行结果
p1.x = 10, p1.y = 15 p2.x = 10, p2.y = 15
注意事项
如果你没有为类定义拷贝构造函数,C++ 编译器会自动生成一个默认的拷贝构造函数。
拷贝构造函数通常应该使用引用传递,以避免无限递归的拷贝操作。
当类中包含指针成员时,可能需要深度拷贝。这需要自定义拷贝构造函数来确保每个成员正确地被复制。
拷贝赋值运算符在 C++ 中同样扮演着重要的角色,特别是在对象间赋值时。让我们详细探讨一下这个概念。
拷贝赋值运算符的基本概念
定义
拷贝赋值运算符用于将一个对象的值复制到另一个已经存在的对象中。每个类都有一个拷贝赋值运算符,可以是显式定义的,也可以是编译器自动生成的。
语法
对于类 ClassName,拷贝赋值运算符通常定义为:
ClassName& operator=(const ClassName& other);
何时使用拷贝赋值运算符
当使用赋值操作符(=)将一个对象的值赋给另一个已经存在的对象时,就会调用拷贝赋值运算符。例如:
ClassName obj1, obj2; obj1 = obj2; // 这里调用了拷贝赋值运算符
示例代码
让我们以 Point 类为例,为其添加一个拷贝赋值运算符:
#include <iostream> using namespace std; class Point { private: int x, y; public: Point(int x1, int y1) { // 构造函数 x = x1; y = y1; } // 拷贝赋值运算符 Point& operator=(const Point &p) { x = p.x; y = p.y; return *this; } int getX() { return x; } int getY() { return y; } }; int main() { Point p1(10, 15); // 构造函数被调用 Point p2; // 默认构造函数被调用 p2 = p1; // 拷贝赋值运算符被调用 cout << "p2.x = " << p2.getX() << ", p2.y = " << p2.getY(); return 0; }
运行结果
p2.x = 10, p2.y = 15
注意事项
- 拷贝赋值运算符应该检查自赋值的情况。
- 与拷贝构造函数类似,当类中有指针成员时,需要考虑深拷贝。
- 拷贝赋值运算符通常返回一个指向当前对象的引用,以允许链式赋值。
析构函数在 C++ 中是一个基本概念,用于管理对象销毁时的资源释放和清理工作。下面是关于析构函数的详细讲解。
析构函数的基本概念
定义
析构函数是一个特殊的成员函数,当对象生命周期结束时被自动调用。它的主要作用是释放对象占用的资源,例如释放分配给对象的内存、关闭文件等。
语法
对于类 ClassName,其析构函数的定义如下:
~ClassName();
它没有返回值,也不接受任何参数。
何时调用析构函数
析构函数会在以下情况被调用:
- 局部对象:当局部对象的作用域结束时(例如,函数执行完毕时)。
- 动态分配的对象:当使用 delete 操作符删除动态分配的对象时。
- 通过 delete[] 删除的对象数组:为数组中的每个对象调用。
- 程序结束:当程序结束时,为全局对象或静态对象调用。
示例代码
下面是一个简单的示例,演示如何定义和使用析构函数。
#include <iostream> using namespace std; class Point { private: int x, y; public: Point(int x1, int y1) { // 构造函数 x = x1; y = y1; cout << "构造函数被调用" << endl; } ~Point() { // 析构函数 cout << "析构函数被调用" << endl; } int getX() { return x; } int getY() { return y; } }; void createPoint() { Point p(10, 15); // 构造函数被调用 cout << "Point created: " << p.getX() << ", " << p.getY() << endl; // 当createPoint函数结束时,p的析构函数被调用 } int main() { createPoint(); return 0; }
运行结果
构造函数被调用 Point created: 10, 15 析构函数被调用
注意事项
- 析构函数不能被显式调用;它由 C++ 运行时自动调用。
- 析构函数应该足够简单,避免在其中抛出异常。
- 当类包含动态分配的资源时(如指针),通常需要在析构函数中释放这些资源,以防止内存泄露。
三/五法则(Rule of Three/Five)是 C++ 编程中的一个重要原则,它涉及类的拷贝控制成员:拷贝构造函数、拷贝赋值运算符和析构函数。这个法则帮助程序员处理资源管理,特别是在涉及动态内存分配时。
三/五法则
三法则 (Rule of Three)
如果你的类需要显式定义或删除以下任何一个成员,则它可能需要显式定义或删除所有三个:
- 拷贝构造函数
- 拷贝赋值运算符
- 析构函数
这是因为这三个函数通常涉及资源的分配和释放。例如,如果你的类动态分配内存,则需要确保在拷贝对象时正确地复制这些资源,并在对象销毁时释放资源。
五法则 (Rule of Five)
随着 C++11 的引入,新增了两个成员函数,扩展了三法则,成为五法则:
- 拷贝构造函数
- 拷贝赋值运算符
- 析构函数
- 移动构造函数
- 移动赋值运算符
这两个新成员函数用于支持移动语义,这在处理大型资源时是非常有用的,因为它允许资源的所有权从一个对象转移到另一个,而不是进行昂贵的拷贝。
示例:实现三/五法则
假设我们有一个类 ResourceHolder,它管理一个动态分配的数组。
#include <algorithm> // std::swap #include <iostream> using namespace std; class ResourceHolder { private: int* data; size_t size; public: // 构造函数 ResourceHolder(size_t size): size(size), data(new int[size]) {} // 析构函数 ~ResourceHolder() { delete[] data; } // 拷贝构造函数 ResourceHolder(const ResourceHolder& other): size(other.size), data(new int[other.size]) { std::copy(other.data, other.data + size, data); } // 拷贝赋值运算符 ResourceHolder& operator=(ResourceHolder other) { swap(*this, other); return *this; } // 移动构造函数 ResourceHolder(ResourceHolder&& other) noexcept : data(nullptr), size(0) { swap(*this, other); } // 移动赋值运算符 ResourceHolder& operator=(ResourceHolder&& other) noexcept { if (this != &other) { delete[] data; data = nullptr; swap(*this, other); } return *this; } // 交换函数 friend void swap(ResourceHolder& first, ResourceHolder& second) noexcept { using std::swap; swap(first.size, second.size); swap(first.data, second.data); } // 其他成员函数... }; int main() { ResourceHolder r1(10); ResourceHolder r2 = r1; // 拷贝构造函数 ResourceHolder r3(15); r3 = std::move(r1); // 移动赋值运算符 // ... }
注意事项
使用这些规则可以帮助避免资源泄漏、双重释放等问题。
在实现移动构造函数和移动赋值运算符时,要注意处理自赋值情况,并确保符合异常安全的要求。
在 C++11 及更高版本中,=default 关键字的引入提供了一种简洁的方式来让编译器自动生成类的默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。使用 =default 可以显式地告诉编译器我们希望使用它的默认实现,而不是完全禁用这些特殊的成员函数。
使用场景
默认构造函数:当你希望类有一个默认构造函数,但不需要特别的实现时。
拷贝构造函数和拷贝赋值运算符:当你希望类能被正常拷贝,且默认的逐成员拷贝行为是适当的。
移动构造函数和移动赋值运算符:当你希望类支持移动语义,但不需要特别的移动逻辑。
析构函数:当你希望类有一个默认的析构函数,通常在没有动态分配的资源需要清理时使用。
—
示例代码
下面的示例演示了如何使用 =default:
#include <iostream> #include <vector> using namespace std; class MyClass { public: MyClass() = default; // 默认构造函数 ~MyClass() = default; // 默认析构函数 MyClass(const MyClass& other) = default; // 默认拷贝构造函数 MyClass(MyClass&& other) noexcept = default; // 默认移动构造函数 MyClass& operator=(const MyClass& other) = default; // 默认拷贝赋值运算符 MyClass& operator=(MyClass&& other) noexcept = default; // 默认移动赋值运算符 // 其他成员函数... }; int main() { MyClass obj1; // 调用默认构造函数 MyClass obj2 = obj1; // 调用默认拷贝构造函数 MyClass obj3 = std::move(obj1); // 调用默认移动构造函数 // ... }
注意事项
使用 =default 时,编译器生成的成员函数是公共的、非虚的、非显式的,且具有相同的异常规范。
如果类中有成员不可拷贝或不可移动,对应的拷贝或移动操作将被编译器删除。
使用 =default 声明的函数可以在类定义中(此时为内联的)或类定义外声明。
在 C++ 中,阻止一个类被拷贝是一种常见的实践,尤其是对于那些管理独占资源的类。阻止拷贝可以确保对象的唯一性和资源管理的安全性。有两种主要方法来阻止类被拷贝:
阻止拷贝
方法 1:删除拷贝构造函数和拷贝赋值运算符
在 C++11 及以后的版本中,最简单的方式是使用 =delete 关键字明确地删除拷贝构造函数和拷贝赋值运算符。
class NonCopyable { public: NonCopyable() = default; // 默认构造函数 // 删除拷贝构造函数和拷贝赋值运算符 NonCopyable(const NonCopyable&) = delete; NonCopyable& operator=(const NonCopyable&) = delete; // 允许移动构造函数和移动赋值运算符 NonCopyable(NonCopyable&&) = default; NonCopyable& operator=(NonCopyable&&) = default; };
方法 2:继承自 std::noncopyable
在 C++11 之前的版本中,或者在更喜欢这种方法的情况下,可以通过继承 boost::noncopyable 或自定义的非拷贝基类来实现。
#include <boost/noncopyable.hpp> class NonCopyable : private boost::noncopyable { // 类定义... };
或者自定义一个非拷贝基类:
class NonCopyable { protected: NonCopyable() = default; ~NonCopyable() = default; NonCopyable(const NonCopyable&) = delete; NonCopyable& operator=(const NonCopyable&) = delete; }; class MyClass : private NonCopyable { // 类定义... };
为什么要阻止拷贝
某些对象,比如文件句柄、数据库连接或者网络套接字,管理着不能简单复制的资源。在这些情况下,拷贝这样的对象可能会导致资源管理混乱(如多次释放同一资源),或者违反对象的唯一性约束。
通过阻止拷贝,你可以确保这类对象的实例保持唯一,并且避免了复制可能导致的问题。
在 C++ 中,创建一个“行为像值”的类意味着该类的实例在被拷贝时表现得就像基本数据类型(如 int、double)那样。这通常涉及到实现深拷贝,确保每个对象都有自己的数据副本,从而使得对象之间相互独立。
“行为像值”的类
实现“行为像值”的类的关键点:
- 深拷贝:在拷贝构造函数和拷贝赋值运算符中实现深拷贝,以确保复制对象的数据而非仅复制指针或引用。
- 资源管理:确保管理动态分配的内存,防止内存泄漏。
- 拷贝赋值运算符:遵循赋值时的“拷贝并交换”惯用法,以确保异常安全和代码简洁。
- 析构函数:释放对象拥有的资源。
示例代码
假设我们有一个简单的 String 类,它包含一个动态分配的字符数组:
#include <cstring> #include <algorithm> class String { private: char* data; size_t length; void freeData() { delete[] data; } public: // 构造函数 String(const char* str = "") : length(strlen(str)) { data = new char[length + 1]; std::copy(str, str + length, data); data[length] = '\0'; } // 拷贝构造函数 String(const String& other) : length(other.length) { data = new char[length + 1]; std::copy(other.data, other.data + length, data); data[length] = '\0'; } // 拷贝赋值运算符 String& operator=(String other) { swap(*this, other); return *this; } // 移动构造函数 String(String&& other) noexcept : data(nullptr), length(0) { swap(*this, other); } // 析构函数 ~String() { freeData(); } // 交换函数 friend void swap(String& first, String& second) noexcept { using std::swap; swap(first.length, second.length); swap(first.data, second.data); } // 其他成员函数... };
在这个例子中,String 类实现了深拷贝,确保每个 String 对象都独立拥有自己的字符数组。析构函数释放这些资源,而拷贝赋值运算符则使用“拷贝并交换”惯用法来确保异常安全。
注意事项
保深拷贝是必要的,并且能正确处理自赋值情况。
考虑异常安全性,特别是在处理资源分配和释放时。
为了提高效率,可以考虑实现移动构造函数和移动赋值运算符。
在 C++ 中,定义一个“行为像指针”的类通常意味着这个类的实例在被拷贝时表现得就像指针一样。这种行为通常涉及到共享数据,而不是像值类型那样进行深拷贝。这种模式经常通过实现引用计数或使用智能指针来实现。
c++拷贝控制(二)https://developer.aliyun.com/article/1437372?spm=a2c6h.13262185.profile.28.5bba685cuSQkDD