文章目录

  • 拷贝构造函数的基本概念
  • 定义
  • 语法
  • 何时使用拷贝构造函数
  • 示例代码
  • 运行结果
  • 注意事项
  • 拷贝赋值运算符的基本概念
  • 定义
  • 语法
  • 何时使用拷贝赋值运算符
  • 示例代码
  • 运行结果
  • 注意事项
  • 析构函数的基本概念
  • 定义
  • 语法
  • 何时调用析构函数
  • 示例代码
  • 运行结果
  • 注意事项
  • 三/五法则
  • 三法则 (Rule of Three)
  • 五法则 (Rule of Five)
  • 示例:实现三/五法则
  • 注意事项
  • 使用场景
  • 示例代码
  • 注意事项
  • 阻止拷贝
  • 方法 1:删除拷贝构造函数和拷贝赋值运算符
  • 方法 2:继承自 `std::noncopyable`
  • 为什么要阻止拷贝
  • “行为像值”的类
  • 实现“行为像值”的类的关键点:
  • 示例代码
  • 注意事项
  • 定义行为像指针的类
  • 关键特征
  • 实现策略
  • 示例代码
  • 注意事项
  • 交换操作
  • 交换操作的基本概念
  • 实现交换操作
  • 完整的示例代码
  • 代码说明


拷贝构造函数的基本概念

定义

拷贝构造函数是一种特殊的构造函数,用于创建一个新对象作为现有对象的副本。在 C++ 中,当对象以值传递的方式传入函数,或从函数返回时,或用一个对象初始化另一个对象时,拷贝构造函数会被调用。

语法

ClassName (const ClassName &old_obj);

何时使用拷贝构造函数

  1. 对象作为参数传递给函数(按值传递)
  2. 对象从函数返回(按值返回)
  3. 对象需要通过另一个对象进行初始化

示例代码

假设我们有一个简单的类 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();

它没有返回值,也不接受任何参数。

何时调用析构函数

析构函数会在以下情况被调用:

  1. 局部对象:当局部对象的作用域结束时(例如,函数执行完毕时)。
  2. 动态分配的对象:当使用 delete 操作符删除动态分配的对象时。
  3. 通过 delete[] 删除的对象数组:为数组中的每个对象调用。
  4. 程序结束:当程序结束时,为全局对象或静态对象调用。

示例代码

下面是一个简单的示例,演示如何定义和使用析构函数。

#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)

如果你的类需要显式定义或删除以下任何一个成员,则它可能需要显式定义或删除所有三个:

  1. 拷贝构造函数
  2. 拷贝赋值运算符
  3. 析构函数

这是因为这三个函数通常涉及资源的分配和释放。例如,如果你的类动态分配内存,则需要确保在拷贝对象时正确地复制这些资源,并在对象销毁时释放资源。

五法则 (Rule of Five)

随着 C++11 的引入,新增了两个成员函数,扩展了三法则,成为五法则:

  1. 拷贝构造函数
  2. 拷贝赋值运算符
  3. 析构函数
  4. 移动构造函数
  5. 移动赋值运算符

这两个新成员函数用于支持移动语义,这在处理大型资源时是非常有用的,因为它允许资源的所有权从一个对象转移到另一个,而不是进行昂贵的拷贝。

示例:实现三/五法则

假设我们有一个类 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++ 中,创建一个“行为像值”的类意味着该类的实例在被拷贝时表现得就像基本数据类型(如 intdouble)那样。这通常涉及到实现深拷贝,确保每个对象都有自己的数据副本,从而使得对象之间相互独立。

“行为像值”的类

实现“行为像值”的类的关键点:

  1. 深拷贝:在拷贝构造函数和拷贝赋值运算符中实现深拷贝,以确保复制对象的数据而非仅复制指针或引用。
  2. 资源管理:确保管理动态分配的内存,防止内存泄漏。
  3. 拷贝赋值运算符:遵循赋值时的“拷贝并交换”惯用法,以确保异常安全和代码简洁。
  4. 析构函数:释放对象拥有的资源。

示例代码

假设我们有一个简单的 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++ 中,定义一个“行为像指针”的类通常意味着这个类的实例在被拷贝时表现得就像指针一样。这种行为通常涉及到共享数据,而不是像值类型那样进行深拷贝。这种模式经常通过实现引用计数或使用智能指针来实现。

定义行为像指针的类

关键特征

  1. 共享数据:实例之间共享相同的数据。
  2. 引用计数:通常通过引用计数来管理共享数据的生命周期。
  3. 自管理资源:类负责管理其指向的资源,如分配和释放内存。

实现策略

  1. 使用智能指针:如 std::shared_ptr,它自动处理引用计数和资源管理。
  2. 手动引用计数:在类内部手动实现引用计数机制。

示例代码

假设我们有一个简单的 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 类的实例 s1s2 共享相同的字符串。通过使用 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)。
  • 执行交换后,obj1obj2 的内容互换。