【高级程序设计语言C++】异常与智能指针

1. 异常

当我们编写程序时,可能会遇到各种错误和异常情况,例如除以零、访问无效的内存地址等。为了能够处理这些异常情况,C++提供了异常处理机制。

异常是程序在运行时发生的意外或错误情况。当异常发生时,程序会中断当前的执行流程,并跳转到异常处理代码块。异常处理代码块可以捕获并处理异常,从而使程序能够以一种控制的方式处理错误情况。

在C++中,我们使用try-catch语句来处理异常。try块中包含可能会抛出异常的代码,而catch块用于捕获并处理异常。当异常被抛出时,程序会跳转到最近的匹配的catch块,并执行其中的代码。

下面是一个简单的示例,演示了异常处理的基本用法:

#include <iostream>
#include <stdexcept>

int divide(int a, int b)
{
    if (b == 0)
        throw std::invalid_argument("除以零错误");

    return a / b;
}

int main()
{
    try
    {
        int result = divide(10, 0);
        std::cout << "结果: " << result << std::endl;
    }
    catch (const std::exception& e)
    {
        std::cout << "捕获到异常: " << e.what() << std::endl;
    }

    return 0;
}

在上面的代码中,divide()函数用于计算两个数的商。如果除数为0,我们会抛出一个std::invalid_argument异常。在main()函数中,我们使用try-catch块来捕获可能抛出的异常。如果异常被抛出,程序会跳转到catch块,并打印异常信息。

**这个例子中的异常处理机制使得我们可以在程序中处理各种错误情况,而不是简单地导致程序崩溃。**通过捕获和处理异常,我们可以提供更好的用户体验,并增加程序的可靠性和可维护性。

需要注意的是,异常处理应该在适当的地方进行,以确保程序能够正确处理异常并继续执行。同时,我们应该避免滥用异常,只在真正需要时才使用它们。另外,当使用异常处理时,应该谨慎处理资源的释放,以避免内存泄漏等问题。

当异常处理不在适当的地方进行时,可能会导致程序无法正确处理异常并继续执行。下面是一些反例,展示了异常处理不当的情况:

  1. 异常处理不在适当的地方进行:
#include <iostream>

void divide(int a, int b)
{
    if (b == 0)
        throw "除以零错误";

    std::cout << "结果: " << a / b << std::endl;
}

int main()
{
    try
    {
        divide(10, 0);
    }
    catch (const char* e)
    {
        std::cout << "捕获到异常: " << e << std::endl;
    }

    return 0;
}

在上面的代码中,divide()函数用于计算两个数的商。如果除数为0,我们会抛出一个字符串异常。在main()函数中,我们使用了try-catch块来捕获可能抛出的异常。然而,异常处理的位置不正确,导致程序无法正确处理异常并继续执行。在这个例子中,当异常发生时,divide()函数内部的cout语句不会执行,因此无法输出结果。

  1. 滥用异常:
#include <iostream>

int divide(int a, int b)
{
    if (b == 0)
        throw "除以零错误";

    return a / b;
}

int main()
{
    try
    {
        int result = divide(10, 2);
        std::cout << "结果: " << result << std::endl;
    }
    catch (const char* e)
    {
        std::cout << "捕获到异常: " << e << std::endl;
    }

    return 0;
}

**在上面的代码中,divide()函数用于计算两个数的商。如果除数为0,我们会抛出一个字符串异常。在main()函数中,我们使用了try-catch块来捕获可能抛出的异常。**然而,在这个例子中,我们滥用了异常,将其用作一种控制流程的手段。在实际情况下,除以零并不是一个异常情况,而是一种可以预料到的错误。因此,使用异常来处理这种情况并不合适。

  1. 资源释放问题:
#include <iostream>
#include <fstream>

void readFile()
{
    std::ifstream file("test.txt");
    if (!file)
        throw "无法打开文件";

    // 读取文件内容
    // ...

    file.close(); // 错误的资源释放方式
}

int main()
{
    try
    {
        readFile();
    }
    catch (const char* e)
    {
        std::cout << "捕获到异常: " << e << std::endl;
    }

    return 0;
}

在上面的代码中,readFile()函数用于读取文件内容。如果无法打开文件,我们会抛出一个字符串异常。然而,在这个例子中,我们没有正确处理资源的释放。当异常发生时,file对象不会被关闭,导致资源泄漏。

这些反例展示了异常处理不当的情况,**即异常处理不在适当的地方进行、滥用异常和未正确处理资源释放。**为了确保程序能够正确处理异常并继续执行,我们应该在适当的地方使用try-catch块来捕获和处理异常,并谨慎处理资源的释放,以避免内存泄漏等问题。

2. 智能指针

**当处理资源时,智能指针是一种常用的工具,它可以帮助我们管理资源的生命周期,确保资源在不再需要时被正确释放。**下面是一个简单的智能指针的实现,突出了RAII的作用和类似指针的操作:

template <typename T>
class SmartPtr
{
public:
    explicit SmartPtr(T* ptr = nullptr) : m_ptr(ptr) {}

    ~SmartPtr()
    {
        delete m_ptr;
    }

    /*SmartPtr(const SmartPtr& other)
    {
        m_ptr = new T(*other.m_ptr);
    }

    SmartPtr& operator=(const SmartPtr& other)
    {
        if (this != &other)
        {
            delete m_ptr;
            m_ptr = new T(*other.m_ptr);
        }
        return *this;
    }*/

    T& operator*() const
    {
        return *m_ptr;
    }

    T* operator->() const
    {
        return m_ptr;
    }

private:
    T* m_ptr;
};

上述代码定义了一个SmartPtr类,它使用模板来适应不同类型的资源。该类具有以下特点:

  1. 构造函数接受一个指针作为参数,初始化智能指针。当不再需要该指针时,智能指针会自动释放资源。
  2. 析构函数负责释放资源,确保资源在对象销毁时被正确释放。
  3. 重载了*和->操作符,使得智能指针的使用更类似于原始指针。

以下是一个示例,展示了如何使用SmartPtr类来管理动态分配的整数资源:

#include <iostream>

int main()
{
    SmartPtr<int> ptr(new int(42));

    std::cout << *ptr << std::endl;  // 输出: 42
    *ptr = 24;
    std::cout << *ptr << std::endl;  // 输出: 24

    return 0;
}

在上述示例中,我们创建了一个SmartPtr对象,并通过构造函数将一个动态分配的整数资源传递给它。我们可以通过*操作符解引用智能指针,并通过->操作符访问底层资源的成员。

**使用智能指针可以确保资源在不再需要时被正确释放,从而避免内存泄漏等问题。**希望这个简单的智能指针实现能够帮助你更好地理解智能指针的作用和RAII的概念。

以下是一个使用自己模拟写的智能指针SmartPtr的示例代码,展示了在异常使用不规范的场景下智能指针的作用:

#include <iostream>

template <typename T>
class SmartPtr
{
public:
    SmartPtr(T* ptr = nullptr) : m_ptr(ptr) {}

    ~SmartPtr()
    {
        delete m_ptr;
    }

    T& operator*()
    {
        return *m_ptr;
    }

    T* operator->()
    {
        return m_ptr;
    }

private:
    T* m_ptr;
};

class Resource
{
public:
    Resource()
    {
        std::cout << "Resource acquired." << std::endl;
    }

    ~Resource()
    {
        std::cout << "Resource released." << std::endl;
    }

    void DoSomething()
    {
        std::cout << "Doing something with the resource." << std::endl;
    }
};

void SomeFunction()
{
    SmartPtr<Resource> ptr(new Resource());

    // 在执行某些操作时可能发生异常
    // 这里模拟一个异常的情况
    throw std::runtime_error("An error occurred.");

    ptr->DoSomething();  // 潜在的资源泄漏
}

int main()
{
    try
    {
        SomeFunction();
    }
    catch (const std::exception& e)
    {
        std::cout << "Exception caught: " << e.what() << std::endl;
    }

    return 0;
}

在上述示例中,我们使用自己模拟写的智能指针SmartPtr来管理资源Resource。在SomeFunction函数中,我们通过SmartPtr创建了一个Resource对象,并在执行某些操作时可能发生异常。

img

当异常被捕获并跳转到catch块时,异常的处理会终止当前的函数执行,并继续执行异常处理代码。在这种情况下,即使SomeFunction函数没有完全执行完毕,但由于main函数的结束,整个程序即将退出,因此SomeFunction函数会被提前结束。

当函数结束时,局部变量(包括SmartPtr对象)的生命周期也会结束,它们的析构函数会被自动调用,从而释放资源。因此,即使在SomeFunction函数中发生了异常,SmartPtr对象的析构函数仍然会被调用,确保资源的释放。

2.1. auto_ptr

auto_ptr是C++98标准中提供的一个智能指针,用于管理动态分配的对象。它的特点是在拷贝构造和赋值操作时,会转移指针的所有权,因此只能有一个auto_ptr拥有该指针。在C++11标准中,auto_ptr已被弃用,推荐使用更安全和功能更丰富的unique_ptr、shared_ptr和weak_ptr。

下面是一个简单的模拟实现auto_ptr的示例代码:

template <typename T>
class AutoPtr
{
public:
    explicit AutoPtr(T* ptr = nullptr) : m_ptr(ptr) {}

    ~AutoPtr()
    {
        delete m_ptr;
    }

    AutoPtr(AutoPtr& other)
    {
        m_ptr = other.m_ptr;
        other.m_ptr = nullptr;
    }

    AutoPtr& operator=(AutoPtr& other)
    {
        if (this != &other)
        {
            delete m_ptr;
            m_ptr = other.m_ptr;
            other.m_ptr = nullptr;
        }
        return *this;
    }

    T& operator*()
    {
        return *m_ptr;
    }

    T* operator->()
    {
        return m_ptr;
    }

private:
    T* m_ptr;
};

在上述代码中,AutoPtr类模拟了auto_ptr的功能。它具有一个指向动态分配对象的指针m_ptr,在析构函数中释放资源。

拷贝构造函数和赋值操作符重载实现了指针所有权的转移。在拷贝构造函数中,将传入的AutoPtr对象的指针转移给当前对象,并将传入对象的指针设为nullptr,确保只有一个AutoPtr对象拥有该指针。赋值操作符重载也实现了相同的逻辑,并在赋值前释放当前对象的资源。

通过重载operator*和operator->,我们可以像使用指针一样使用AutoPtr对象,访问所管理的动态分配对象的成员。

需要注意的是,这只是一个简单的模拟实现,可能存在一些问题,例如在多线程环境下的安全性问题。在实际开发中,应该使用C++11标准中提供的更安全和功能更丰富的智能指针,如unique_ptr、shared_ptr和weak_ptr。

void TestAutoPtr()
{
    AutoPtr<int> ap1(new int(223));
    AutoPtr<int> ap2(ap1);

    (*ap2)++;
    (*ap1)++;

    cout << *ap2 << endl;
    cout << *ap1 << endl;
}

运行结果:

img

代码的运行是不正常的,下面通过调试的角度来看看为什么会这样?

img

img

img

2.2. unique_ptr

std::unique_ptr 是 C++ 标准库中的一个智能指针类模板,用于管理动态分配对象的所有权。它提供了独占式所有权,确保在任何时候只有一个 std::unique_ptr 对象可以拥有所管理的对象。

std::unique_ptr 的主要特点如下:

  1. 独占式所有权:std::unique_ptr 禁止拷贝构造和拷贝赋值操作,因此每个 std::unique_ptr 对象只能拥有一个动态分配对象的所有权。这样可以避免多个指针同时管理同一个对象,从而减少内存泄漏和资源竞争的风险。
  2. 自动释放:当 std::unique_ptr 对象超出作用域时,它会自动调用析构函数来释放所管理的对象。这意味着你不需要手动释放内存,从而避免了忘记释放或者多次释放的问题。
  3. 可以指定删除器:你可以使用自定义的删除器来指定在释放所管理的对象时使用的方式。删除器可以是函数指针、函数对象或者 Lambda 表达式,用于在释放对象之前执行一些额外的操作,比如释放资源、关闭文件等。
  4. 支持数组:除了管理单个对象,std::unique_ptr 也可以管理动态分配的数组。你可以使用 std::unique_ptr<T[]> 来创建一个 std::unique_ptr 对象,其中 T 是数组元素的类型。

使用 std::unique_ptr 的示例代码如下:

#include <memory>

int main() {
    // 创建一个 std::unique_ptr 对象,管理一个动态分配的整数
    std::unique_ptr<int> ptr(new int(42));

    // 使用 * 运算符解引用 std::unique_ptr 对象,获取所管理的对象的值
    int value = *ptr;
    std::cout << value << std::endl;

    // 使用 -> 运算符通过 std::unique_ptr 对象访问所管理对象的成员
    ptr->someMemberFunction();

    // 创建一个 std::unique_ptr 对象,管理动态分配的整型数组
    std::unique_ptr<int[]> arr(new int[5]);

    // 使用 [] 运算符访问 std::unique_ptr 对象所管理的数组元素
    arr[0] = 10;
    std::cout << arr[0] << std::endl;

    // std::unique_ptr 对象会在离开作用域时自动释放所管理的对象
    return 0;
}

总的来说,std::unique_ptr 是 C++ 标准库中一个非常有用的智能指针类模板,它提供了简单且安全的管理动态分配对象的方式,可以减少内存泄漏和资源竞争的风险。

下面是模拟实现的一个unique_ptr

template <typename T>
class UniquePtr
{
public:
    explicit UniquePtr(T* ptr = nullptr) : m_ptr(ptr) {}

    ~UniquePtr()
    {
        delete m_ptr;
    }

    UniquePtr(const UniquePtr& other) = delete;

    UniquePtr& operator=(UniquePtr& other) = delete;

    T& operator*()
    {
        return *m_ptr;
    }

    T* operator->()
    {
        return m_ptr;
    }

private:
    T* m_ptr;
};

在拷贝构造和赋值运算符重载加了delete关键字,禁止拷贝。

2.3. shared_ptr

auto_ptr是转移之后就不管了,然后置空导致会有问题。unique_ptr是禁止拷贝,只允许有一份。那如果我想要一个智能指针,它的功能是支持多个智能指针共享同一个对象的所有权。C++标准库里是有这样的一个智能指针的,叫做shared_ptr。下面就简单模拟写一个shared_ptr来认识一下库里shared_ptr的原理。

template <typename T>
class SharedPtr {
public:
    // 构造函数
    SharedPtr(T* ptr = nullptr) : m_ptr(ptr), m_refCount(new int(1)) {}

    // 拷贝构造函数
    SharedPtr(const SharedPtr& other) : m_ptr(other.m_ptr), m_refCount(other.m_refCount) {
        (*m_refCount)++;
    }

    // 析构函数
    ~SharedPtr() {
        (*m_refCount)--;
        if (*m_refCount == 0) {
            delete m_ptr;
            delete m_refCount;
        }
    }

    // 重载赋值运算符
    SharedPtr& operator=(const SharedPtr& other) {
        if (this != &other) {
            (*m_refCount)--;
            if (*m_refCount == 0) {
                delete m_ptr;
                delete m_refCount;
            }
            m_ptr = other.m_ptr;
            m_refCount = other.m_refCount;
            (*m_refCount)++;
        }
        return *this;
    }

    // 解引用运算符
    T& operator*() const {
        return *m_ptr;
    }

    // 成员访问运算符
    T* operator->() const {
        return m_ptr;
    }

private:
    T* m_ptr;         // 指向堆上对象的指针
    int* m_refCount;  // 对象的引用计数
};

所谓的多个智能指针共享一个对象,如图所示:

img

每当创建一个新的 SharedPtr 对象时,引用计数都会加一。当 SharedPtr 对象被销毁时,引用计数减一。当引用计数为零时,表示没有 SharedPtr 对象再引用该对象,可以安全地释放对象的内存。

imgimg

从调试的角度看变化:

imgimg

imgimg

可以看到,虽然有三个智能智能,但都是管同一块资源的,并且利用引用计数避免多次释放和内存泄漏的问题。

需要注意的是

标准库的shared_ptr所管理的资源并不是线程安全的,需要保证自行加锁。

2.4. 循环引用

来看这样一段代码

class B;  // 前向声明

class A
{
public:
    SharedPtr<B> ptrB;
};

class B
{
public:
    SharedPtr<A> ptrA;
};

int main()
{
    SharedPtr<A> a(new A());
    SharedPtr<B> b(new B());

    a->ptrB = b;
    b->ptrA = a;

    return 0;
}

上面的代码会引发循环引用,什么是循环引用呢?看图

img

img

2.5. weak_ptr

weak_ptr是C++11引入的智能指针,用于解决循环引用问题。它是shared_ptr的一个辅助类,允许我们在不增加引用计数的情况下观察和访问由shared_ptr管理的对象。

与shared_ptr不同,weak_ptr并不拥有对象的所有权,它只是对由shared_ptr管理的对象的一个弱引用。因此,weak_ptr不会增加对象的引用计数,也不会阻止对象的销毁。当对象被销毁后,weak_ptr会自动失效。

weak_ptr的主要作用是用来解决循环引用问题。循环引用指的是两个或多个对象之间相互持有对方的shared_ptr,导致它们的引用计数永远不会减为零,从而导致内存泄漏。通过使用weak_ptr,我们可以打破循环引用,避免内存泄漏。

下面是一个简单的weak_ptr

template<class T>
class weak_ptr
{
public:
    weak_ptr()
        :_ptr(nullptr)
    {}

    weak_ptr(const shared_ptr<T>& sp)
        :_ptr(sp.get())
    {}

    weak_ptr<T>& operator=(const shared_ptr<T>& sp)
    {
        _ptr = sp.get(); //获取sp的成员
        return *this;
    }

    // 像指针一样
    T& operator*()
    {
        return *_ptr;
    }

    T* operator->()
    {
        return _ptr;
    }
public:
    T* _ptr;
};

上面的代码实现的是一个简单的weak_ptr,跟标准库里的有很大的不同,有兴趣的同学可以自行去阅读C++官网的文档。

使用weakptr后,修改的代码如下:

class B;  // 前向声明

class A
{
public:
    WeakPtr<B> ptrB;
};

class B
{
public:
    WeakPtr<A> ptrA;
};

void Test()
{
    SharedPtr<A> a(new A());
    SharedPtr<B> b(new B());

    a->ptrB = b;
    b->ptrA = a;
}

这里就不会出现循环引用的场景了,如图所示:

img

img

weak_ptr其实扮演的角色就是帮shared_ptr擦屁股。

2.6. 定制删除器

定制删除器(Custom Deleter)是用于在智能指针释放资源时执行特定操作的函数或函数对象。它可以用来扩展智能指针的功能,以满足特定的需求。

我们可以用一个玩具小车的例子来解释定制删除器的概念。

假设我们有一个玩具小车,它有一个电池作为能源。当我们不再使用这个小车时,我们需要将电池取出并且关闭开关,以节省电池的能量。在这个例子中,玩具小车就是我们要管理的资源,电池就是这个资源的一部分,关闭开关就是我们要执行的特定操作。

在C++中,我们可以使用shared_ptr来管理玩具小车这个资源。我们可以通过定制删除器,在shared_ptr释放资源时执行关闭开关的操作。

下面是一个示例代码,演示了如何使用定制删除器关闭玩具小车的开关:

#include <iostream>
#include <memory>

class ToyCar {
public:
    ToyCar() {
        std::cout << "ToyCar constructed" << std::endl;
    }
    
    ~ToyCar() {
        std::cout << "ToyCar destructed" << std::endl;
    }
    
    void turnOn() {
        std::cout << "ToyCar turned on" << std::endl;
    }
    
    void turnOff() {
        std::cout << "ToyCar turned off" << std::endl;
    }
};

void closeSwitch(ToyCar* car) {
    car->turnOff();
}

int main() {
    std::shared_ptr<ToyCar> car(new ToyCar(), closeSwitch);
    
    car->turnOn();
    
    return 0;
}

在这个例子中,我们定义了一个ToyCar类,表示玩具小车。在ToyCar类中,我们有turnOn和turnOff两个成员函数,分别用于打开和关闭小车的开关。

我们使用shared_ptr来管理玩具小车这个资源,并通过定制删除器closeSwitch来执行关闭开关的操作。closeSwitch是一个普通函数,它接受一个ToyCar*参数,并在函数体中调用turnOff函数来关闭开关。

在main函数中,我们创建了一个shared_ptr对象car,它管理着一个ToyCar实例,并使用closeSwitch作为删除器。当car的引用计数为0时(即没有其他指针指向这个资源),删除器会被调用,关闭小车的开关。

通过定制删除器,我们可以在智能指针释放资源时执行特定的操作,从而扩展智能指针的功能。这在处理一些特殊资源时非常有用,比如文件、数据库连接等。

当使用智能指针管理资源时,可以通过定制删除器来指定在资源释放时执行的特定操作。定制删除器可以是函数、函数对象或者lambda表达式。

定制删除器可以通过两种方式指定:

  1. 通过函数指针或函数对象:可以将一个函数指针或者函数对象作为第二个参数传递给智能指针的构造函数。这个函数或者函数对象将在智能指针释放资源时被调用。
void myDeleter(ToyCar* car) {
    // 执行特定操作
}

std::shared_ptr<ToyCar> car(new ToyCar(), myDeleter);
  1. 通过lambda表达式:可以使用lambda表达式作为删除器。lambda表达式是一种匿名函数,可以直接在智能指针的构造函数中定义,并在其中编写特定操作的代码。
std::shared_ptr<ToyCar> car(new ToyCar(), [](ToyCar* car) {
    // 执行特定操作
});

在lambda表达式中,我们可以使用捕获列表来捕获外部变量,以便在删除器中使用。

下面是使用lambda表达式作为删除器的完整示例代码:

#include <iostream>
#include <memory>

class ToyCar {
public:
    ToyCar() {
        std::cout << "ToyCar constructed" << std::endl;
    }
    
    ~ToyCar() {
        std::cout << "ToyCar destructed" << std::endl;
    }
    
    void turnOn() {
        std::cout << "ToyCar turned on" << std::endl;
    }
    
    void turnOff() {
        std::cout << "ToyCar turned off" << std::endl;
    }
};

int main() {
    std::shared_ptr<ToyCar> car(new ToyCar(), [](ToyCar* car) {
        car->turnOff();
    });
    
    car->turnOn();
    
    return 0;
}

在这个例子中,我们使用lambda表达式作为删除器,关闭玩具小车的开关。lambda表达式接受一个ToyCar*参数,并在函数体中调用turnOff函数来关闭开关。

通过定制删除器,我们可以在智能指针释放资源时执行特定的操作,无论是使用函数指针、函数对象还是lambda表达式,都能够实现这个功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值