可以解决的问题:
| # | 问题 |
|---|---|
| 1 | 在栈上使用线程安全的智能指针 |
| 2 | 安全地使用 指向由其他智能指针所管理的数据 的指针 |
一个SmartPointer类的辅助类。
可以将满足以下所有条件的类型包装为一个智能对象:
- 该类型可被构造
- 该类型不继承自
SmartObject
可以指向任何由智能对象类的实例的智能指针。
它不同于标准库中的std::unique_ptr和std::shared_ptr,与std::weak_ptr有些许相似,下面我们来看看它们的区别:
| 指针 | 指针类型示例 | 可指向的数据 | 是否影响数据生命周期 |
|---|---|---|---|
| 原始指针 | T* |
栈和堆 |
否 |
| 唯一指针 | std::unique_ptr<T> |
堆 |
是 |
| 共享指针 | std::shared_ptr<T> |
堆 |
是 |
| 弱指针 | std::weak_ptr<T> |
堆 |
否 |
SmartPointer指针 |
SmartPointer<T> |
栈和堆 |
否 |
只能指向存储在堆内存上的数据。
std::unique_ptr所指向的数据的生命周期是由指针本身来控制的,当指针被销毁时,数据也会被销毁。
有点像Rust中的所有权规则。
只能指向存储在堆内存上的数据。
std::shared_ptr所指向的数据的生命周期是由引用计数来控制的,当引用计数为0时,数据才会被销毁。
只能指向存储在堆内存上的数据。
std::weak_ptr不会影响被指向的数据的生命周期,如同原始指针那般自由。只不过std::weak_ptr会提供expired接口供程序员判断该指针所指向的数据是否已失效(包括野指针)。
这是原始指针做不到的地方,它无法判断自己是否为野指针。
其原理是根据引用计数是否为0来判断被指向的数据是否已被释放。因此指向有效数据的std::weak_ptr一定只能通过std::shared_ptr来创建。
既能指向存储在栈内存上的数据,也能指向存储在堆内存上的数据。
SmartPointer不会影响被指向的数据的生命周期,如同原始指针那般自由。只不过SmartPointer会提供isNull接口供程序员判断该指针所指向的数据是否已失效(包括野指针)。
这是与std::weak_ptr的相似之处。
虽然SmartPointer可以指向存储在堆内存上的数据,但它不会影响被指向的数据的生命周期,因此它不会自动销毁被指向的数据。
考虑以下示例代码:
class Student : public SmartObject {
private:
string nameStr;
size_t age = 0;
public:
virtual ~Student() {
cout << this << "::" << __FUNCTION__ << endl;
}
const string& getName() const {
return nameStr;
}
void setName(const string& nameStr) {
this->nameStr = nameStr;
}
size_t getAge() const {
return age;
}
void setAge(size_t age) {
this->age = age;
}
};
void mainForThread(SmartPointer<Student> p_student) {
cout << "子线程id:" << this_thread::get_id() << endl;
while (p_student) {
// ==========================↓在以下区域delete则抛异常↓==========================
cout << "======子线程======" << endl;
// 由于Student类线程不安全,所以无法保证其对象操作的原子性
// cout << "姓名:" << p_student->getName() << endl;
// cout << "年龄:" << p_student->getAge() << endl;
// 有概率抛异常,情况为:子线程在while判断后,打印student信息前,主线程delete
try {
cout << "姓名:" << (*p_student).getName() << endl;
cout << "年龄:" << (*p_student).getAge() << endl;
// ======================↑在以上区域delete则抛异常↑==========================
}
catch (const exception& excp) {
cerr << excp.what() << endl;
}
}
cout << "内存已释放..." << endl;
}
void main() {
cout << "多线程测试开始" << endl;
cout << "主线程id:" << this_thread::get_id() << endl;
Student* p_student = new Student;
p_student->setName("田所浩二");
p_student->setAge(24);
thread thrd(mainForThread, SmartPointer<Student>(p_student));
this_thread::sleep_for(chrono::milliseconds(1000));
delete p_student;
thrd.join();
cout << "多线程测试结束" << endl;
}以下是运行两次的输出:
第一次
多线程测试开始
主线程id:1
子线程id:2
======子线程======
姓名:田所浩二
年龄:24
(此处省略重复输出的内容...)
======子线程======
姓名:田所浩二
年龄:24
======子线程======
姓名:0x1001d00::~Student田所浩二
年龄:24
======子线程======
姓名:田所浩二
年龄:内存已释放...
多线程测试结束
日期 / 时间:Oct 23 2024 / 19:00:19
文件:D:/Application Projects/CLion/SmartObject/SmartObject/SmartPointer.h 行:122
函数:operator*
错误信息:指针无效...第二次
多线程测试开始
主线程id:1
子线程id:2
======子线程======
姓名:田所浩二
年龄:24
(此处省略重复输出的内容...)
======子线程======
姓名:田所浩二
年龄:24
======子线程======
姓名:田所浩二
0x10e1d00年龄:::24~Student
======子线程======
姓名:田所浩二
年龄:24
内存已释放...
多线程测试结束原始指针最大的痛点在于:
原始指针无法判断自己是否为野指针。
而造成野指针的原因只有一个:
指针所指向的数据已被释放,但指针本身并不知情。
因此我们可以在数据的析构函数上做手脚,在自己被销毁时通知所有指向它的指针。
熟悉Qt的开发者们就知道这种功能只需要一个信号就可以解决。
要实现保障了功能的最小性能开销,就得手动实现一个简单的信号槽功能,也许都称不上信号槽,只能算一些简单的回调功能。
但为了保证线程安全,还是选择了现成的sigslot信号槽库。
由于sigslot提供的功能是线程安全的,因此只需要保证SmartPointer中修改原始指针的代码是线程安全的即可。
只需用一个互斥锁保护原始指针即可。
考虑到普通的互斥锁会在一个线程读取时会导致需要读取它的其他线程发生阻塞,因此可以将互斥锁替换为读写锁。
| # | 问题 | 描述 |
|---|---|---|
| 1 | 不可继承的类型 | 目前为止TypeWrapper对不可继承类型的支持不太友好。例如:不支持到原始类型的隐式类型转换;必须显式调用原始类型的方法等 |