目录
异常处理
C语言异常处理机制:
- 终止程序
- 返回错误码
C++异常处理机制:
- try:测试异常
- throw:抛出异常
- catch:捕获异常
throw抛出异常时,先看当前函数栈帧是否有try-catch,若有就在当前栈帧捕获,若无就去上一层栈帧检查try-catch
#include <iostream>
using namespace std;
double div(double a, double b)
{
if (b == 0)
{
throw "除0错误";
}
return a / b;
}
void func()
{
double a, b;
cin >> a >> b;
try
{
div(a, b);
}
catch (const char* err) // catch 捕捉异常需要类型匹配
{
cout << err << endl;
}
catch(...) // 捕获任意类型异常
{
cout << "未知错误" << endl;
}
}
int main()
{
func();
return 0;
}
异常安全:最好不要在构造函数和析构函数抛出异常,可能造成对象构造不完整或资源泄露
C++异常的缺陷:
- 异常会导致执行流乱跳,提高我们跟踪调试和分析程序的难度
- C++没有垃圾回收机制,资源需要自己管理,容易造成内存泄漏、死锁等问题
针对异常处理容易造成的内存泄漏问题,C++设计了智能指针。
智能指针
智能指针设计意义:协助管理动态分配的内存,帮助我们自动释放new出来的内存,从而避免内存泄漏。
内存泄漏示例:
#include <iostream>
using namespace std;
void memory_leak_test1()
{
string* str = new string("hello world");
return;
// 没有释放new出来的内存,内存泄漏
}
void memory_leak_test2()
{
string* str = new string("hello world");
if (1)
{
return;
// 因某种情况或发生异常退出函数,delete没有正常释放内存,内存泄漏
}
delete str;
}
int main()
{
memory_leak_test1();
memory_leak_test2();
return 0;
}
智能指针设计原理:将我们分配的动态内存都交由有生命周期的对象来处理,在对象过期时,让它自动调用析构函数释放内存。
C++98 auto_ptr
auto_ptr:auto_ptr是C++98提供的智能指针模板,其定义了管理指针的对象,可以将new获得的空间地址赋给这个对象,当对象过期时调用析构函数来delete内存。
auto_ptr源码设计:
#pragma once
#include <iostream>
template<class T>
class AutoPtr
{
public:
AutoPtr()
: _ptr(nullptr)
{}
AutoPtr(T* ptr)
: _ptr(ptr)
{}
~AutoPtr()
{
delete _ptr;
std::cout << "auto_ptr delete: " << _ptr << std::endl;
}
AutoPtr(AutoPtr<T>& ap)
: _ptr(ap._ptr)
{
ap._ptr = nullptr;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
_ptr = ap._ptr;
ap._ptr = nullptr;
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
void auto_ptr_test()
{
AutoPtr<int> ap1(new int(1));
std::cout << *ap1 << std::endl;
*ap1 += 1;
AutoPtr<int> ap2 = ap1;
std::cout << *ap2 << std::endl;
++(*ap2);
AutoPtr<int> ap3(ap2);
std::cout << *ap3 << std::endl;
// auto_ptr缺陷
// ap1 和 ap2 在赋值和复制的时候被置空,但是程序员有可能再次调用ap1和ap2造成访问越界
}
// 输出结果
// 1
// 2
// 3
// auto_ptr delete: 00A17030
// auto_ptr delete: 00000000
// auto_ptr delete: 00000000
auto_ptr的重大缺陷:
- 复制或者赋值都会改变资源的所有权,导致原智能指针失效,但是程序员有可能继续访问原来的智能指针,引发访问越界
- 在STL容器中使用auto_ptr存在风险,因为容器内元素必须支持可复制、可赋值,这会引发第一个缺陷
- 不支持对象数组的内存管理
于是C++11之后不再使用auto_ptr,取而代之的是unique_ptr、shared_ptr/weak_ptr,这三种智能指针有各自的应用场景,都是在弥补auto_ptr的重大缺陷之后各自也存在缺陷,互相弥补以适应不同的应用场景。
C++11 unique_ptr
unique_ptr:unique_ptr为了弥补auto_ptr的缺陷,在auto_ptr的设计基础上进行了升级。
unique_ptr特性:
- 排他性:两个指针不能指向同一个资源
- 无法进行unique_ptr左值的赋值和构造,但支持unique_ptr右值的赋值和构造
- 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象
- 在STL容器中保存指针是安全的,支持对象数组的内存管理
unique_ptr源码设计:
#include <iostream>
template<class T>
class UniquePtr
{
public:
UniquePtr(T* ptr)
: _ptr(ptr)
{}
~UniquePtr()
{
delete _ptr;
std::cout << "unique_ptr delete: " << _ptr << std::endl;
}
UniquePtr(const UniquePtr<T>& up) = delete;
UniquePtr<T>& operator=(const UniquePtr<T>& up) = delete;
UniquePtr(UniquePtr<T>&& up)
{
_ptr = up._ptr;
up._ptr = nullptr;
}
UniquePtr<T>& operator=(UniquePtr<T>&& up)
{
_ptr = up._ptr;
up._ptr = nullptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
};
void unique_ptr_test()
{
UniquePtr<std::string> up1(new std::string("hello world"));
std::cout << *up1 << std::endl;
std::cout << up1[0] << std::endl;
UniquePtr<int> up2(new int[10]);
for (int i = 0; i < 10; ++i)
{
up2[i] = i;
}
for (int i = 0; i < 10; ++i)
{
std::cout << up2[i] << " ";
}
std::cout << std::endl;
UniquePtr<int> up3 = std::move(up2);
for (int i = 0; i < 10; ++i)
{
std::cout << up3[i] << " ";
}
std::cout << std::endl;
UniquePtr<int> up4(std::move(up3));
for (int i = 0; i < 10; ++i)
{
std::cout << up4[i] << " ";
}
std::cout << std::endl;
}
unique_ptr的排他性虽然解决了auto_ptr的复制赋值的风险,但是其本是也是一个缺陷,因为我们很多应用场景是需要让智能指针进行复制和赋值操作的,于是C++有了shared_ptr。
C++11 shared_ptr
shared_ptr:在unique_ptr的设计基础上进行升级,支持多个智能指针变量指向同一个资源,支持智能指针进行左值复制赋值。
shared_ptr设计原理:设计引用计数,记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数+1,当智能指针析构时,引用计数-1,当某个资源的引用计数为0时,代表已经没有指针指向这块资源,则释放这个内存资源。
为防止引用计数在多线程中容易因为资源共享而导致报错,引用计数在修改智能指针数量时加上了互斥锁。
shared_ptr源码设计:
#include <iostream>
#include <thread>
#include <mutex>
// 默认删除器,用于内置变量,对于自定义变量的可定制删除器
template<class T>
class DefaultDelete
{
public:
void operator()(T* ptr)
{
delete ptr;
}
};
template<class T, class D = DefaultDelete<T>>
class SharedPtr
{
public:
SharedPtr(T* ptr = nullptr)
: _ptr(ptr), _pCount(new int(1)), _pMtx(new std::mutex)
{}
~SharedPtr()
{
release();
}
void release()
{
bool flag = false;
_pMtx->lock();
if (--(*_pCount) == 0)
{
_del(_ptr);
delete _pCount;
flag = true;
}
_pMtx->unlock();
if (flag)
{
delete _pMtx;
}
}
SharedPtr(const SharedPtr<T>& sp)
: _ptr(sp._ptr), _pCount(sp._pCount), _pMtx(sp._pMtx), _del(sp._del)
{
_pMtx->lock();
++(*_pCount);
_pMtx->unlock();
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
if (_ptr != sp._ptr)
{
release();
_ptr = sp._ptr;
_pCount = sp._pCount;
_pMtx = sp._pMtx;
_del = sp._del;
_pMtx->lock();
++(*_pCount);
_pMtx->unlock();
}
return *this;
}
int use_count()const
{
return *_pCount;
}
T* get()const
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T& operator[](size_t pos)
{
return _ptr[pos];
}
private:
T* _ptr;
int* _pCount;
std::mutex* _pMtx;
D _del;
};
struct Date
{
int _year;
int _month;
int _day;
Date(int y, int m, int d)
: _year(y), _month(m), _day(d)
{}
friend std::ostream& operator<<(std::ostream& os, const Date& d);
};
std::ostream& operator<<(std::ostream& os, const Date& d)
{
os << d._year << "/" << d._month << "/" << d._day << std::endl;
return os;
}
void shared_ptr_test()
{
SharedPtr<Date> sp1(new Date(2023, 5, 20));
int n = 10000;
std::mutex mtx;
std::thread t1([&]()
{
for (int i = 0; i < n; ++i)
{
SharedPtr<Date> sp2(sp1);
mtx.lock();
sp2->_year++;
sp2->_month++;
sp2->_day++;
mtx.unlock();
}
});
std::thread t2([&]()
{
for (int i = 0; i < n; ++i)
{
SharedPtr<Date> sp3(sp1);
mtx.lock();
sp3->_year++;
sp3->_month++;
sp3->_day++;
mtx.unlock();
}
});
t1.join();
t2.join();
std::cout << sp1.use_count() << std::endl;
std::cout << sp1.get() << std::endl;
std::cout << sp1[0] << std::endl;
}
// 输出
// 1
// 006514D0
// 22023 / 20005 / 20020
shared_ptr的缺陷:当shared_ptr作为被管控对象的成员时,可能因为循环引用而造成资源泄露。即A类中有B类的智能指针,B类中有A类的智能指针,当它们交叉互相持有对方的管理对象时,就形成了循环引用。
class Girl;
class Boy
{
public:
Boy()
{
std::cout << "Boy 构造" << std::endl;
}
~Boy()
{
std::cout << "Boy 析构" << std::endl;
}
void set_girlfriend(SharedPtr<Girl> girlFriend)
{
_girlFriend = girlFriend;
}
private:
SharedPtr<Girl> _girlFriend;
};
class Girl
{
public:
Girl()
{
std::cout << "Girl 构造" << std::endl;
}
~Girl()
{
std::cout << "Girl 析构" << std::endl;
}
void set_boyfriend(SharedPtr<Boy> boyFriend)
{
_boyFriend = boyFriend;
}
private:
SharedPtr<Boy> _boyFriend;
};
void shared_ptr_test1()
{
SharedPtr<Boy> spBoy(new Boy);
SharedPtr<Girl> spGirl(new Girl);
// 循环引用
spBoy->set_girlfriend(spGirl);
spGirl->set_boyfriend(spBoy);
std::cout << spBoy.use_count() << std::endl;
std::cout << spGirl.use_count() << std::endl;
}
// 输出
// Boy 构造
// Girl 构造
// 2
// 2
// 因为循环引用,无法进行析构,造成内存泄漏
所以在使用shared_ptr的时候,要避免对象的交叉引用情况,而weak_ptr就是为了弥补shared_ptr的这个缺陷而出现。
C++11 weak_ptr
weak_ptr:weak_ptr是为了配合shared_ptr而设计的,目的是协助shared_ptr工作,它只可以从一个shared_ptr和另一个weak_ptr构造,它的构造和析构不会引起引用计数的增减,同时weak_ptr没有重载 * 和 -> ,但可以使用 lock 获得一个可用的shared_ptr对象。
weak_ptr源码设计:
#pragma once
#include "shared_ptr.h"
template<class T>
class WeakPtr
{
public:
WeakPtr()
: _ptr(nullptr)
{}
WeakPtr(const SharedPtr<T>& sp)
: _ptr(sp.get())
{}
WeakPtr<T>& operator=(const SharedPtr<T>& sp)
{
_ptr = sp.get();
return *this;
}
private:
T* _ptr;
};
class Girl;
class Boy
{
public:
Boy()
{
std::cout << "Boy 构造" << std::endl;
}
~Boy()
{
std::cout << "Boy 析构" << std::endl;
}
void set_girlfriend(SharedPtr<Girl> girlFriend)
{
_girlFriend = girlFriend;
}
private:
WeakPtr<Girl> _girlFriend;
};
class Girl
{
public:
Girl()
{
std::cout << "Girl 构造" << std::endl;
}
~Girl()
{
std::cout << "Girl 析构" << std::endl;
}
void set_boyfriend(SharedPtr<Boy> boyFriend)
{
_boyFriend = boyFriend;
}
private:
WeakPtr<Boy> _boyFriend;
};
void weak_ptr_test()
{
SharedPtr<Boy> spBoy(new Boy);
SharedPtr<Girl> spGirl(new Girl);
WeakPtr<Girl> wpGirl_1; // 定义空的弱指针
WeakPtr<Girl> wpGirl_2(spGirl); // 使用共享指针构造弱指针
wpGirl_1 = spGirl; // 允许共享指针赋值弱指针
std::cout << spBoy.use_count() << std::endl; // 1
std::cout << spGirl.use_count() << std::endl; // 1
spBoy->set_girlfriend(spGirl);
spGirl->set_boyfriend(spBoy);
std::cout << spBoy.use_count() << std::endl; // 1
std::cout << spGirl.use_count() << std::endl; // 1
}
// 修改Boy类和Girl类后的输出:
// Boy 构造
// Girl 构造
// 1
// 1
// 1
// 1
// Girl 析构
// Boy 析构
注:文中的源码设计相比与标准库进行了精简,保留了主要特征的功能,还有部分功能函数没有实现。