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

C++智能指针的异常处理

2021-06-013.3k 阅读

C++智能指针基础回顾

在深入探讨C++智能指针的异常处理之前,我们先来简要回顾一下智能指针的基础知识。C++ 标准库提供了几种智能指针类型,主要包括 std::unique_ptrstd::shared_ptrstd::weak_ptr

std::unique_ptr

std::unique_ptr 是一种独占式智能指针,它负责管理一个对象的生命周期。当 std::unique_ptr 被销毁时,它所指向的对象也会被自动销毁。这种智能指针不能被复制,只能被移动。例如:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};

int main() {
    std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
    // 这里ptr独占MyClass对象的所有权
    return 0;
}

在上述代码中,std::make_unique 是 C++14 引入的函数,用于创建 std::unique_ptr 并分配对象。当 main 函数结束时,ptr 被销毁,从而自动调用 MyClass 的析构函数。

std::shared_ptr

std::shared_ptr 是一种共享式智能指针,多个 std::shared_ptr 可以指向同一个对象。它通过引用计数来管理对象的生命周期,当引用计数为 0 时,对象会被自动销毁。例如:

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};

int main() {
    std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
    std::shared_ptr<MyClass> ptr2 = ptr1;
    // ptr1和ptr2共享MyClass对象的所有权,引用计数为2
    return 0;
}

ptr1ptr2 都超出作用域时,引用计数降为 0,MyClass 对象被销毁。

std::weak_ptr

std::weak_ptr 是一种弱引用智能指针,它不增加对象的引用计数。std::weak_ptr 通常用于解决 std::shared_ptr 中的循环引用问题。例如:

#include <iostream>
#include <memory>

class B;

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

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

int main() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b = b;
    b->a = a;
    // 这里如果B中不是weak_ptr,会导致循环引用,对象无法销毁
    return 0;
}

在上述代码中,B 类中的 a 成员使用 std::weak_ptr,避免了 AB 之间的循环引用,使得对象可以正常销毁。

异常处理在C++中的重要性

在 C++ 编程中,异常处理是确保程序健壮性和可靠性的关键机制。异常可以用于处理运行时错误,例如内存分配失败、文件打开失败等。如果不妥善处理异常,程序可能会崩溃,导致数据丢失或其他严重后果。

传统的异常处理方式

在没有智能指针的时代,程序员通常使用手动内存管理结合 try - catch 块来处理异常。例如:

#include <iostream>
#include <exception>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};

int main() {
    MyClass* ptr = nullptr;
    try {
        ptr = new MyClass();
        // 可能会抛出异常的代码
        throw std::runtime_error("Some error occurred");
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
        if (ptr) {
            delete ptr;
        }
    }
    return 0;
}

在上述代码中,try 块中分配了 MyClass 对象,如果在后续代码中抛出异常,catch 块会捕获异常,并手动释放 ptr 所指向的内存。这种方式存在几个问题:

  1. 代码冗长:每次在 try 块中分配资源,都需要在 catch 块中手动释放,增加了代码量。
  2. 容易出错:如果忘记在 catch 块中释放资源,就会导致内存泄漏。

智能指针在异常处理中的优势

智能指针的出现极大地简化了异常处理中的资源管理。智能指针会在其生命周期结束时自动释放所管理的资源,无论是否发生异常。例如,使用 std::unique_ptr 重写上述代码:

#include <iostream>
#include <memory>
#include <exception>

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};

int main() {
    std::unique_ptr<MyClass> ptr;
    try {
        ptr = std::make_unique<MyClass>();
        throw std::runtime_error("Some error occurred");
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

在这个版本中,无论 try 块中是否抛出异常,ptr 在离开其作用域时都会自动释放 MyClass 对象,无需手动管理内存,代码更加简洁且不易出错。

std::unique_ptr的异常处理

异常安全保证

std::unique_ptr 提供了强异常安全保证。这意味着如果在 std::unique_ptr 的构造、赋值或销毁过程中抛出异常,程序状态不会发生改变,不会导致资源泄漏。例如:

#include <iostream>
#include <memory>
#include <exception>

class MyResource {
public:
    MyResource() { std::cout << "MyResource constructor" << std::endl; }
    ~MyResource() { std::cout << "MyResource destructor" << std::endl; }
};

class MyClass {
private:
    std::unique_ptr<MyResource> resource;
public:
    MyClass() {
        resource = std::make_unique<MyResource>();
        // 模拟可能抛出异常的操作
        throw std::runtime_error("Some error in MyClass constructor");
    }
};

int main() {
    try {
        MyClass obj;
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,MyClass 的构造函数中分配了 MyResource 对象并由 std::unique_ptr 管理。即使构造函数中抛出异常,std::unique_ptr 也会确保 MyResource 对象被正确销毁,不会发生资源泄漏。

异常处理场景下的移动语义

std::unique_ptr 的移动语义在异常处理场景中也非常有用。例如,考虑以下函数,它返回一个 std::unique_ptr

#include <iostream>
#include <memory>
#include <exception>

class MyResource {
public:
    MyResource() { std::cout << "MyResource constructor" << std::cout; }
    ~MyResource() { std::cout << "MyResource destructor" << std::cout; }
};

std::unique_ptr<MyResource> createResource() {
    std::unique_ptr<MyResource> ptr = std::make_unique<MyResource>();
    // 模拟可能抛出异常的操作
    throw std::runtime_error("Some error in createResource");
    return ptr;
}

int main() {
    try {
        std::unique_ptr<MyResource> result = createResource();
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

createResource 函数中,std::unique_ptr 可以安全地移动其管理的资源,即使函数因为异常而提前返回。这确保了资源的正确管理,不会因为异常而丢失。

std::shared_ptr的异常处理

引用计数与异常安全

std::shared_ptr 的异常安全依赖于其引用计数机制。在构造 std::shared_ptr 时,引用计数会增加;在销毁或赋值时,引用计数会相应减少。当引用计数为 0 时,所管理的对象会被自动销毁。例如:

#include <iostream>
#include <memory>
#include <exception>

class MyResource {
public:
    MyResource() { std::cout << "MyResource constructor" << std::endl; }
    ~MyResource() { std::cout << "MyResource destructor" << std::endl; }
};

void processResource() {
    std::shared_ptr<MyResource> ptr1 = std::make_shared<MyResource>();
    std::shared_ptr<MyResource> ptr2 = ptr1;
    // 模拟可能抛出异常的操作
    throw std::runtime_error("Some error in processResource");
}

int main() {
    try {
        processResource();
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,processResource 函数中创建了两个 std::shared_ptr 指向同一个 MyResource 对象。即使函数因为异常而提前返回,MyResource 对象的引用计数会在 ptr1ptr2 超出作用域时正确减少,当引用计数为 0 时,对象会被销毁,确保了异常安全。

自定义删除器与异常处理

std::shared_ptr 支持自定义删除器,这在处理一些特殊资源(如文件句柄、数据库连接等)时非常有用。在异常处理场景下,自定义删除器也能确保资源的正确释放。例如:

#include <iostream>
#include <memory>
#include <exception>
#include <cstdio>

void customDeleter(FILE* file) {
    if (file) {
        std::cout << "Custom deleter closing file" << std::endl;
        fclose(file);
    }
}

void processFile() {
    std::shared_ptr<FILE> filePtr(fopen("test.txt", "r"), customDeleter);
    if (!filePtr) {
        throw std::runtime_error("Failed to open file");
    }
    // 模拟文件处理操作
    // 可能会抛出异常
    throw std::runtime_error("Some error in file processing");
}

int main() {
    try {
        processFile();
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,std::shared_ptr 使用了自定义删除器 customDeleter 来关闭文件。即使在文件处理过程中抛出异常,std::shared_ptr 也会调用自定义删除器来正确关闭文件,确保资源不泄漏。

std::weak_ptr的异常处理

从std::weak_ptr提升为std::shared_ptr时的异常

std::weak_ptr 本身不管理资源的生命周期,但它可以通过 lock 方法提升为 std::shared_ptr。在提升过程中,如果所指向的对象已经被销毁(即 std::weak_ptr 已经过期),lock 方法会返回一个空的 std::shared_ptr。这种情况不会抛出异常,但需要程序员进行适当的检查。例如:

#include <iostream>
#include <memory>
#include <exception>

class MyResource {
public:
    MyResource() { std::cout << "MyResource constructor" << std::endl; }
    ~MyResource() { std::cout << "MyResource destructor" << std::endl; }
};

void testWeakPtr() {
    std::shared_ptr<MyResource> ptr1 = std::make_shared<MyResource>();
    std::weak_ptr<MyResource> weakPtr = ptr1;
    ptr1.reset();
    std::shared_ptr<MyResource> ptr2 = weakPtr.lock();
    if (!ptr2) {
        std::cerr << "Weak pointer has expired" << std::endl;
    }
}

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

在上述代码中,ptr1 释放了对 MyResource 对象的引用,此时 weakPtr 已经过期。调用 lock 方法返回一个空的 std::shared_ptr,程序通过检查 ptr2 是否为空来处理这种情况。

与异常处理结合的场景

虽然 std::weak_ptr 本身在异常处理方面没有直接的特殊机制,但在复杂的对象关系中,它可以帮助避免循环引用,从而间接保证异常安全。例如,在一个包含多个 std::shared_ptr 相互引用的场景中,如果存在循环引用,可能会导致对象在异常情况下无法正确销毁。使用 std::weak_ptr 可以打破这种循环引用,确保资源在异常发生时能够正常释放。例如:

#include <iostream>
#include <memory>
#include <exception>

class B;

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

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

void createAndThrow() {
    std::shared_ptr<A> a = std::make_shared<A>();
    std::shared_ptr<B> b = std::make_shared<B>();
    a->b = b;
    b->a = a;
    throw std::runtime_error("Some error in createAndThrow");
}

int main() {
    try {
        createAndThrow();
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,AB 之间通过 std::shared_ptrstd::weak_ptr 建立了关系,std::weak_ptr 避免了循环引用。当 createAndThrow 函数抛出异常时,ab 所指向的对象能够正常销毁,确保了异常安全。

智能指针异常处理的常见陷阱与最佳实践

避免悬空指针

在使用智能指针时,要注意避免产生悬空指针。例如,当一个 std::shared_ptr 管理的对象被销毁,但还有其他指向该对象的原始指针存在时,就会产生悬空指针。这在异常处理场景中可能会导致未定义行为。例如:

#include <iostream>
#include <memory>
#include <exception>

class MyResource {
public:
    MyResource() { std::cout << "MyResource constructor" << std::endl; }
    ~MyResource() { std::cout << "MyResource destructor" << std::endl; }
};

void badPractice() {
    std::shared_ptr<MyResource> ptr = std::make_shared<MyResource>();
    MyResource* rawPtr = ptr.get();
    ptr.reset();
    // 这里rawPtr成为悬空指针,如果在异常处理中使用它,会导致未定义行为
    try {
        // 模拟可能抛出异常的操作
        throw std::runtime_error("Some error in badPractice");
    } catch (const std::exception& e) {
        // 如果在这里使用rawPtr,会导致未定义行为
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
}

int main() {
    try {
        badPractice();
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

为了避免这种情况,尽量避免在智能指针存在的情况下使用原始指针。如果必须使用原始指针,要确保在智能指针销毁之前,原始指针已经不再使用。

异常处理中的资源泄漏检测

虽然智能指针大大减少了资源泄漏的风险,但在复杂的程序中,仍然可能存在资源泄漏的情况。可以使用一些工具来检测资源泄漏,例如 Valgrind 。Valgrind 是一个内存调试、内存泄漏检测以及性能分析的工具。以下是一个简单的使用 Valgrind 检测智能指针异常处理中资源泄漏的示例:

  1. 编写代码
#include <iostream>
#include <memory>
#include <exception>

class MyResource {
public:
    MyResource() { std::cout << "MyResource constructor" << std::endl; }
    ~MyResource() { std::cout << "MyResource destructor" << std::endl; }
};

void potentialLeak() {
    std::shared_ptr<MyResource> ptr;
    try {
        ptr = std::make_shared<MyResource>();
        throw std::runtime_error("Some error in potentialLeak");
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
        // 这里如果没有正确处理ptr,可能会导致资源泄漏
    }
}

int main() {
    try {
        potentialLeak();
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}
  1. 使用 Valgrind 检测: 编译代码后,使用以下命令运行 Valgrind:
valgrind --leak-check=full./a.out

Valgrind 会报告程序中是否存在内存泄漏。如果在异常处理中没有正确处理智能指针,Valgrind 会检测到资源泄漏并给出详细信息。

异常安全的设计原则

在设计使用智能指针的类和函数时,要遵循异常安全的设计原则。这包括确保构造函数、析构函数和赋值运算符都是异常安全的。例如,在构造函数中分配资源时,要确保如果构造过程中抛出异常,已经分配的资源能够正确释放。在析构函数中,要确保不会抛出异常,因为析构函数抛出异常可能会导致程序崩溃。例如:

#include <iostream>
#include <memory>
#include <exception>

class MyClass {
private:
    std::shared_ptr<int> data;
public:
    MyClass() {
        try {
            data = std::make_shared<int>(42);
            // 模拟可能抛出异常的操作
            throw std::runtime_error("Some error in MyClass constructor");
        } catch (...) {
            // 这里如果不处理异常,data会自动释放,确保异常安全
            std::cerr << "Exception caught during construction" << std::endl;
        }
    }
    ~MyClass() {
        // 析构函数中不应该抛出异常
        std::cout << "MyClass destructor" << std::endl;
    }
};

int main() {
    try {
        MyClass obj;
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}

在上述代码中,MyClass 的构造函数通过 try - catch 块确保了如果构造过程中抛出异常,data 所管理的资源会被正确释放。析构函数也遵循不抛出异常的原则,确保了异常安全。

总结

C++ 智能指针为异常处理中的资源管理提供了强大而便捷的方式。std::unique_ptrstd::shared_ptrstd::weak_ptr 各自具有不同的特性,在异常处理场景中发挥着重要作用。通过合理使用智能指针,遵循异常安全的设计原则,避免常见陷阱,并结合资源泄漏检测工具,可以编写出更加健壮、可靠的 C++ 程序。在实际编程中,要根据具体的需求选择合适的智能指针类型,并充分利用它们的优势来处理异常,确保程序在各种情况下都能正确运行。