拷贝构造函数是一种特殊的构造函数,用于创建一个新对象作为现有对象的副本。在 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++ 中同样扮演着重要的角色,特别是在对象间赋值时。让我们详细探讨一下这个概念。
拷贝赋值运算符用于将一个对象的值复制到另一个已经存在的对象中。每个类都有一个拷贝赋值运算符,可以是显式定义的,也可以是编译器自动生成的。
对于类 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
析构函数被调用
三/五法则(Rule of Three/Five)是 C++ 编程中的一个重要原则,它涉及类的拷贝控制成员:拷贝构造函数、拷贝赋值运算符和析构函数。这个法则帮助程序员处理资源管理,特别是在涉及动态内存分配时。
如果你的类需要显式定义或删除以下任何一个成员,则它可能需要显式定义或删除所有三个:
这是因为这三个函数通常涉及资源的分配和释放。例如,如果你的类动态分配内存,则需要确保在拷贝对象时正确地复制这些资源,并在对象销毁时释放资源。
随着 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++ 中,阻止一个类被拷贝是一种常见的实践,尤其是对于那些管理独占资源的类。阻止拷贝可以确保对象的唯一性和资源管理的安全性。有两种主要方法来阻止类被拷贝:
在 C++11 及以后的版本中,最简单的方式是使用 =delete
关键字明确地删除拷贝构造函数和拷贝赋值运算符。
class NonCopyable {
public:
NonCopyable() = default; // 默认构造函数
// 删除拷贝构造函数和拷贝赋值运算符
NonCopyable(const NonCopyable&) = delete;
NonCopyable& operator=(const NonCopyable&) = delete;
// 允许移动构造函数和移动赋值运算符
NonCopyable(NonCopyable&&) = default;
NonCopyable& operator=(NonCopyable&&) = default;
};
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++ 中,定义一个“行为像指针”的类通常意味着这个类的实例在被拷贝时表现得就像指针一样。这种行为通常涉及到共享数据,而不是像值类型那样进行深拷贝。这种模式经常通过实现引用计数或使用智能指针来实现。
std::shared_ptr
,它自动处理引用计数和资源管理。假设我们有一个简单的 SharedString
类,该类使用 std::shared_ptr
来共享字符串数据:
#include <iostream>
#include <string>
#include <memory>
class SharedString {
private:
std::shared_ptr<std::string> data;
public:
SharedString(const char* str = "") : data(std::make_shared<std::string>(str)) {}
// 使用默认的拷贝构造函数、赋值运算符和析构函数
SharedString(const SharedString&) = default;
SharedString& operator=(const SharedString&) = default;
~SharedString() = default;
// 获取字符串
const std::string& get() const {
return *data;
}
// 其他成员函数...
};
int main() {
SharedString s1("Hello");
SharedString s2 = s1; // s1 和 s2 共享相同的数据
std::cout << "s1: " << s1.get() << ", s2: " << s2.get() << std::endl;
return 0;
}
在这个例子中,SharedString
类的实例 s1
和 s2
共享相同的字符串。通过使用 std::shared_ptr
,当最后一个 SharedString
实例被销毁时,字符串数据会被自动释放。
在 C++ 中,交换操作是一个重要的概念,尤其是在实现拷贝赋值运算符和移动构造函数时。正确的交换操作可以提高代码的效率和安全性。
交换操作的目的是交换两个对象的状态。在许多情况下,这比通过传统的拷贝更高效,特别是对于大型对象或资源密集型对象。
交换操作通常通过定义一个非成员函数 swap
来实现,该函数接受两个同类型对象的引用并交换它们的内部状态。
当然,我可以提供一个更完整的代码示例,展示如何在 C++ 类中实现交换操作。我们将创建一个简单的类 MyClass
,该类将包含一些基本的成员变量和一个交换操作的实现。
#include <iostream>
#include <utility> // For std::swap (C++11 and later)
class MyClass {
private:
int* data;
size_t size;
public:
// 构造函数
MyClass(size_t size) : size(size), data(new int[size]) {
for (size_t i = 0; i < size; ++i) {
data[i] = i; // 示例数据初始化
}
}
// 拷贝构造函数
MyClass(const MyClass& other) : size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
// 拷贝赋值运算符
MyClass& operator=(MyClass other) {
swap(*this, other);
return *this;
}
// 移动构造函数
MyClass(MyClass&& other) noexcept : MyClass() {
swap(*this, other);
}
// 移动赋值运算符
MyClass& operator=(MyClass&& other) noexcept {
swap(*this, other);
return *this;
}
// 析构函数
~MyClass() {
delete[] data;
}
// 交换成员函数
friend void swap(MyClass& first, MyClass& second) noexcept {
using std::swap;
swap(first.size, second.size);
swap(first.data, second.data);
}
// 用于演示的函数
void print() const {
for (size_t i = 0; i < size; ++i) {
std::cout << data[i] << ' ';
}
std::cout << std::endl;
}
};
int main() {
MyClass obj1(5);
MyClass obj2(10);
std::cout << "Original obj1: ";
obj1.print();
std::cout << "Original obj2: ";
obj2.print();
// 使用交换操作
swap(obj1, obj2);
std::cout << "Swapped obj1: ";
obj1.print();
std::cout << "Swapped obj2: ";
obj2.print();
return 0;
}
Original obj1: 0 1 2 3 4
Original obj2: 0 1 2 3 4 5 6 7 8 9
Swapped obj1: 0 1 2 3 4 5 6 7 8 9
Swapped obj2: 0 1 2 3 4
这个 MyClass
类包含一个动态分配的整型数组和一个表示数组大小的成员变量。
实现了构造函数、拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。
实现了一个 swap
函数,用于交换两个 MyClass
实例的内部状态。
在 main
函数中,创建了两个 MyClass
对象,并使用 swap
函数展示了交换操作的效果。
obj1
最初包含 5 个元素(从 0 到 4)。
obj2
最初包含 10 个元素(从 0 到 9)。
执行交换后,obj1
和 obj2
的内容互换。