MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ std::weak_ptr 的锁定操作

2022-10-184.9k 阅读

C++ std::weak_ptr 的锁定操作

理解 std::weak_ptr 的基本概念

在 C++ 的智能指针体系中,std::weak_ptr 是一种相对特殊的存在。与 std::shared_ptr 不同,std::weak_ptr 并不拥有对象的所有权,它的主要作用是观察 std::shared_ptr 所管理的对象。

当我们创建一个 std::shared_ptr 来管理某个对象时,这个对象的引用计数会增加。多个 std::shared_ptr 可以指向同一个对象,它们共同维护这个对象的生命周期,当引用计数降为 0 时,对象会被自动销毁。而 std::weak_ptr 可以从一个 std::shared_ptr 初始化得到,它不会影响对象的引用计数。

例如:

#include <memory>
#include <iostream>

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
    std::weak_ptr<int> weakPtr(sharedPtr);

    std::cout << "Shared pointer use count: " << sharedPtr.use_count() << std::endl;
    return 0;
}

在上述代码中,通过 std::make_shared 创建了一个 std::shared_ptr 指向一个 int 类型的对象。然后用这个 std::shared_ptr 初始化了一个 std::weak_ptr。此时,std::shared_ptr 的引用计数为 1,而 std::weak_ptr 不会增加引用计数。

std::weak_ptr 的锁定操作

std::weak_ptr 的锁定操作是通过 lock 成员函数来实现的。lock 函数尝试锁定 std::weak_ptr 所观察的对象,如果对象仍然存在(即对应的 std::shared_ptr 的引用计数大于 0),lock 函数会返回一个 std::shared_ptr,这个 std::shared_ptr 指向被观察的对象,并且会增加对象的引用计数。如果对象已经被销毁,lock 函数会返回一个空的 std::shared_ptr

下面是一个简单的示例,展示 lock 函数的基本使用:

#include <memory>
#include <iostream>

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
    std::weak_ptr<int> weakPtr(sharedPtr);

    std::shared_ptr<int> lockedPtr = weakPtr.lock();
    if (lockedPtr) {
        std::cout << "Locked successfully. Value: " << *lockedPtr << std::endl;
    } else {
        std::cout << "Failed to lock. Object may have been destroyed." << std::endl;
    }

    // 手动释放 sharedPtr
    sharedPtr.reset();

    lockedPtr = weakPtr.lock();
    if (lockedPtr) {
        std::cout << "Locked successfully. Value: " << *lockedPtr << std::endl;
    } else {
        std::cout << "Failed to lock. Object may have been destroyed." << std::endl;
    }

    return 0;
}

在上述代码中,首先从 std::weak_ptr 进行锁定,此时对象存在,所以锁定成功并输出对象的值。然后手动通过 reset 函数释放 std::shared_ptr,再次尝试锁定时,由于对象已经被销毁,锁定失败。

锁定操作在实际场景中的应用

  1. 解决循环引用问题 在 C++ 中,循环引用是使用智能指针时可能遇到的一个问题。例如,假设有两个类 AB,它们相互持有对方的 std::shared_ptr
#include <memory>
#include <iostream>

class B;

class A {
public:
    std::shared_ptr<B> bPtr;
    ~A() {
        std::cout << "A destroyed" << std::endl;
    }
};

class B {
public:
    std::shared_ptr<A> aPtr;
    ~B() {
        std::cout << "B destroyed" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->bPtr = b;
    b->aPtr = a;

    return 0;
}

在上述代码中,AB 相互持有对方的 std::shared_ptr,这会导致循环引用。当 main 函数结束时,ab 的引用计数都不会降为 0,因为它们相互引用,从而导致内存泄漏。

可以通过将其中一个指针改为 std::weak_ptr 来解决这个问题。例如,将 B 中的 aPtr 改为 std::weak_ptr

#include <memory>
#include <iostream>

class B;

class A {
public:
    std::shared_ptr<B> bPtr;
    ~A() {
        std::cout << "A destroyed" << std::endl;
    }
};

class B {
public:
    std::weak_ptr<A> aPtr;
    ~B() {
        std::cout << "B destroyed" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();

    a->bPtr = b;
    b->aPtr = a;

    return 0;
}

在这个修改后的代码中,B 不再拥有 A 的所有权,避免了循环引用。当 main 函数结束时,ab 的引用计数都能正确降为 0,对象会被正常销毁。

如果 B 类需要访问 A 类的成员,可以通过 std::weak_ptr 的锁定操作来实现:

#include <memory>
#include <iostream>

class B;

class A {
public:
    int value;
    std::shared_ptr<B> bPtr;
    A(int v) : value(v) {}
    ~A() {
        std::cout << "A destroyed" << std::endl;
    }
};

class B {
public:
    std::weak_ptr<A> aPtr;
    void printAPtrValue() {
        std::shared_ptr<A> lockedA = aPtr.lock();
        if (lockedA) {
            std::cout << "Value of A: " << lockedA->value << std::endl;
        } else {
            std::cout << "A has been destroyed." << std::endl;
        }
    }
    ~B() {
        std::cout << "B destroyed" << std::endl;
    }
};

int main() {
    std::shared_ptr<A> a = std::make_shared<A>(42);
    std::shared_ptr<B> b = std::make_shared<B>();

    a->bPtr = b;
    b->aPtr = a;

    b->printAPtrValue();

    return 0;
}

B 类的 printAPtrValue 函数中,通过 lock 操作尝试获取 Astd::shared_ptr,如果成功则可以访问 A 的成员。

  1. 缓存与延迟加载 std::weak_ptr 的锁定操作在缓存和延迟加载场景中也非常有用。假设我们有一个缓存系统,用于存储一些昂贵的对象。缓存中的对象由 std::shared_ptr 管理,当缓存中的对象长时间未被使用时,我们希望可以自动释放它以节省内存。
#include <memory>
#include <iostream>
#include <unordered_map>

class ExpensiveObject {
public:
    int data;
    ExpensiveObject(int d) : data(d) {
        std::cout << "ExpensiveObject created: " << data << std::endl;
    }
    ~ExpensiveObject() {
        std::cout << "ExpensiveObject destroyed: " << data << std::endl;
    }
};

class Cache {
private:
    std::unordered_map<int, std::weak_ptr<ExpensiveObject>> cache;
public:
    std::shared_ptr<ExpensiveObject> getObject(int key) {
        auto it = cache.find(key);
        if (it != cache.end()) {
            std::shared_ptr<ExpensiveObject> lockedObj = it->second.lock();
            if (lockedObj) {
                return lockedObj;
            } else {
                cache.erase(it);
            }
        }
        // 对象不存在,创建新对象并放入缓存
        std::shared_ptr<ExpensiveObject> newObj = std::make_shared<ExpensiveObject>(key);
        cache[key] = newObj;
        return newObj;
    }
};

int main() {
    Cache cache;
    std::shared_ptr<ExpensiveObject> obj1 = cache.getObject(1);
    std::shared_ptr<ExpensiveObject> obj2 = cache.getObject(1);

    // 模拟 obj1 不再使用
    obj1.reset();

    // 再次获取对象
    std::shared_ptr<ExpensiveObject> obj3 = cache.getObject(1);

    return 0;
}

在上述代码中,Cache 类使用 std::unordered_map 来存储 std::weak_ptr。当调用 getObject 方法时,首先尝试从缓存中获取对象并锁定。如果锁定成功,直接返回对象;如果锁定失败,说明对象已被销毁,从缓存中移除并重新创建对象。

锁定操作的线程安全性

在多线程环境下,std::weak_ptr 的锁定操作需要特别注意线程安全性。虽然 std::weak_ptr 本身的大部分操作(如构造、析构、赋值等)都是线程安全的,但 lock 函数的返回值可能会受到竞争条件的影响。

例如,考虑以下多线程场景:

#include <memory>
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
std::shared_ptr<int> sharedData;
std::weak_ptr<int> weakData;

void threadFunction() {
    std::unique_lock<std::mutex> lock(mtx);
    if (!sharedData) {
        sharedData = std::make_shared<int>(42);
        weakData = sharedData;
    }
    lock.unlock();

    std::shared_ptr<int> localPtr = weakData.lock();
    if (localPtr) {
        std::cout << "Thread got value: " << *localPtr << std::endl;
    } else {
        std::cout << "Thread failed to lock." << std::endl;
    }
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

    t1.join();
    t2.join();

    return 0;
}

在上述代码中,两个线程同时尝试获取 sharedData。如果 sharedData 为空,线程会创建它并更新 weakData。然后尝试从 weakData 锁定。然而,这里存在一个竞争条件。如果一个线程在更新 weakData 后但在另一个线程锁定之前,sharedData 被其他地方释放了,那么锁定可能会失败。

为了避免这种情况,需要使用互斥锁来保护对 sharedDataweakData 的操作:

#include <memory>
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
std::shared_ptr<int> sharedData;
std::weak_ptr<int> weakData;

void threadFunction() {
    std::unique_lock<std::mutex> lock(mtx);
    std::shared_ptr<int> localPtr;
    if (!sharedData) {
        sharedData = std::make_shared<int>(42);
        weakData = sharedData;
        localPtr = sharedData;
    } else {
        localPtr = weakData.lock();
    }
    lock.unlock();

    if (localPtr) {
        std::cout << "Thread got value: " << *localPtr << std::endl;
    } else {
        std::cout << "Thread failed to lock." << std::endl;
    }
}

int main() {
    std::thread t1(threadFunction);
    std::thread t2(threadFunction);

    t1.join();
    t2.join();

    return 0;
}

在这个改进后的代码中,通过互斥锁 mtx 确保了对 sharedDataweakData 的操作是线程安全的,避免了竞争条件导致的锁定失败问题。

锁定操作与对象生命周期管理

  1. 锁定操作对对象生命周期的影响 当通过 std::weak_ptrlock 函数成功获取到 std::shared_ptr 时,对象的引用计数会增加。这意味着对象的生命周期会被延长,直到所有指向该对象的 std::shared_ptr 的引用计数都降为 0 为止。

例如:

#include <memory>
#include <iostream>

void testLifetime() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
    std::weak_ptr<int> weakPtr(sharedPtr);

    {
        std::shared_ptr<int> lockedPtr = weakPtr.lock();
        if (lockedPtr) {
            std::cout << "Inner block: Locked successfully. Use count: " << lockedPtr.use_count() << std::endl;
        }
    } // lockedPtr 在此处超出作用域,引用计数减 1

    std::cout << "Outer block: Shared pointer use count: " << sharedPtr.use_count() << std::endl;
}

int main() {
    testLifetime();
    return 0;
}

在上述代码中,在内部块中通过 lock 函数获取 std::shared_ptr,此时对象的引用计数增加。当内部块结束,lockedPtr 超出作用域,引用计数减 1。外部块中 sharedPtr 的引用计数也会相应变化。

  1. 确保对象在锁定期间的有效性 在使用 std::weak_ptr 的锁定操作时,需要确保在锁定后的操作过程中,对象不会被意外销毁。这通常需要结合适当的作用域管理。

例如,假设有一个函数接受一个 std::weak_ptr 并进行一些操作:

#include <memory>
#include <iostream>

void processWeakPtr(std::weak_ptr<int> weakPtr) {
    std::shared_ptr<int> lockedPtr = weakPtr.lock();
    if (lockedPtr) {
        // 对 lockedPtr 进行操作
        *lockedPtr += 1;
        std::cout << "Processed value: " << *lockedPtr << std::endl;
    } else {
        std::cout << "Object has been destroyed." << std::endl;
    }
}

int main() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
    processWeakPtr(sharedPtr);

    return 0;
}

在上述代码中,processWeakPtr 函数从 std::weak_ptr 锁定对象,并在锁定成功后对对象进行操作。这样可以确保在操作过程中对象的有效性。

锁定操作与智能指针转换

  1. 从 std::weak_ptr 转换到 std::shared_ptr std::weak_ptrlock 函数本质上就是一种从 std::weak_ptrstd::shared_ptr 的转换操作。当对象存在时,lock 函数返回一个指向该对象的 std::shared_ptr,增加对象的引用计数。

例如:

#include <memory>
#include <iostream>

class Base {
public:
    virtual void print() {
        std::cout << "Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void print() override {
        std::cout << "Derived class" << std::endl;
    }
};

int main() {
    std::shared_ptr<Derived> derivedPtr = std::make_shared<Derived>();
    std::weak_ptr<Base> weakBasePtr(derivedPtr);

    std::shared_ptr<Base> basePtr = weakBasePtr.lock();
    if (basePtr) {
        basePtr->print();
    }

    return 0;
}

在上述代码中,std::weak_ptrstd::shared_ptr<Derived> 初始化,通过 lock 函数转换为 std::shared_ptr<Base>,并调用虚函数 print

  1. 动态类型转换与锁定操作的结合 在进行类型转换时,结合 std::weak_ptr 的锁定操作可以确保类型转换的安全性。例如,使用 std::dynamic_pointer_caststd::weak_ptr 结合:
#include <memory>
#include <iostream>

class Base {
public:
    virtual void print() {
        std::cout << "Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void print() override {
        std::cout << "Derived class" << std::endl;
    }
};

void processWeakPtr(std::weak_ptr<Base> weakBasePtr) {
    std::shared_ptr<Base> basePtr = weakBasePtr.lock();
    if (basePtr) {
        std::shared_ptr<Derived> derivedPtr = std::dynamic_pointer_cast<Derived>(basePtr);
        if (derivedPtr) {
            derivedPtr->print();
        } else {
            std::cout << "Failed to dynamic cast to Derived." << std::endl;
        }
    } else {
        std::cout << "Object has been destroyed." << std::endl;
    }
}

int main() {
    std::shared_ptr<Derived> derivedPtr = std::make_shared<Derived>();
    std::weak_ptr<Base> weakBasePtr(derivedPtr);

    processWeakPtr(weakBasePtr);

    return 0;
}

在上述代码中,processWeakPtr 函数首先从 std::weak_ptr 锁定对象,然后尝试将 std::shared_ptr<Base> 动态转换为 std::shared_ptr<Derived>。这样可以确保在对象存在的情况下进行安全的类型转换。

锁定操作在复杂数据结构中的应用

  1. 树形结构中的应用 在树形数据结构中,std::weak_ptr 的锁定操作可以用于避免循环引用,同时实现安全的节点访问。例如,考虑一个简单的二叉树结构:
#include <memory>
#include <iostream>

class TreeNode {
public:
    int value;
    std::shared_ptr<TreeNode> left;
    std::shared_ptr<TreeNode> right;
    std::weak_ptr<TreeNode> parent;

    TreeNode(int v) : value(v) {}

    void setParent(std::shared_ptr<TreeNode> p) {
        parent = p;
    }

    std::shared_ptr<TreeNode> getParent() {
        return parent.lock();
    }
};

void printPathToRoot(std::shared_ptr<TreeNode> node) {
    std::shared_ptr<TreeNode> current = node;
    while (current) {
        std::cout << current->value << " ";
        current = current->getParent();
    }
    std::cout << std::endl;
}

int main() {
    std::shared_ptr<TreeNode> root = std::make_shared<TreeNode>(1);
    std::shared_ptr<TreeNode> leftChild = std::make_shared<TreeNode>(2);
    std::shared_ptr<TreeNode> rightChild = std::make_shared<TreeNode>(3);

    root->left = leftChild;
    root->right = rightChild;

    leftChild->setParent(root);
    rightChild->setParent(root);

    printPathToRoot(leftChild);

    return 0;
}

在上述代码中,TreeNode 类包含一个 std::weak_ptr 指向父节点。通过 getParent 方法可以安全地获取父节点,避免了循环引用问题。

  1. 图结构中的应用 在图结构中,std::weak_ptr 也可以用于管理节点之间的关系,特别是在有向图中避免循环引用。例如,假设有一个简单的有向图节点类:
#include <memory>
#include <iostream>
#include <vector>

class GraphNode {
public:
    int value;
    std::vector<std::shared_ptr<GraphNode>> neighbors;
    std::vector<std::weak_ptr<GraphNode>> reverseNeighbors;

    GraphNode(int v) : value(v) {}

    void addNeighbor(std::shared_ptr<GraphNode> neighbor) {
        neighbors.push_back(neighbor);
        neighbor->reverseNeighbors.push_back(shared_from_this());
    }

    void printReverseNeighbors() {
        for (const auto& weakNeighbor : reverseNeighbors) {
            std::shared_ptr<GraphNode> lockedNeighbor = weakNeighbor.lock();
            if (lockedNeighbor) {
                std::cout << lockedNeighbor->value << " ";
            }
        }
        std::cout << std::endl;
    }
};

int main() {
    std::shared_ptr<GraphNode> node1 = std::make_shared<GraphNode>(1);
    std::shared_ptr<GraphNode> node2 = std::make_shared<GraphNode>(2);
    std::shared_ptr<GraphNode> node3 = std::make_shared<GraphNode>(3);

    node1->addNeighbor(node2);
    node2->addNeighbor(node3);

    node3->printReverseNeighbors();

    return 0;
}

在上述代码中,GraphNode 类通过 std::weak_ptr 来存储反向邻居,避免了循环引用。通过 printReverseNeighbors 方法可以安全地访问反向邻居节点。

通过以上详细的介绍和丰富的代码示例,相信你对 C++ std::weak_ptr 的锁定操作有了深入的理解,能够在实际编程中灵活运用它来解决各种问题,特别是在对象生命周期管理、避免循环引用以及多线程环境等方面。