场景
- 在开发
C/C++
程序时,通常情况下,如果需要传递数据给其他线程,那么传递的类型就需要创建在堆上。 即使用malloc
和new
来创建指针对象,而安全编程最需要回避的就是操作指针。指针对象是需要手动管理释放的,这也可能造成内存安全问题。 比如悬垂指针,对象释放后再使用。那么有什么办法可以避免出现悬垂指针呢?
说明
-
C/C++
的编译器在Rust
出现以前还没考虑过代替工程师检查代码安全的问题。代码的安全完全靠C/C++
开发者的自觉和水平。一个要求代码质量高的C++
项目,开发完之后还需要各种静态分析工具Clang-Tidy,Cppcheck
,动态分析工具Valgrind,AddressSanitizer
等进行扫描才能去除90%
的内存安全错误。相对Rustc
的编译即检查麻烦很多,因为这些外部工具也是要安装的,如果是版本高了,还不支持开发所用的系统。 -
C/C++
的内存安全问题就是避免出现未定义行为,避免使用指针。其中未定义行为希望新版的标准能检测出来,不过那都是之后的事。通常C/C++
的项目都是使用了不同的C++
标准的,比如老项目也支持C++11
标准,而新项目支持C++20
都不奇怪。因为除了Windows
平台上的编译器发展慢之外,C++
的abi
新版还不兼容旧版的。 -
出现悬垂指针的情况也有多种,以下总结了几种可能发生悬垂指针的情况。 使用建议的编程方法能避免悬垂指针出现的几率。
-
悬垂指针: 一个指针指向的内存已经被释放(或不再有效),但指针本身依然保存着那个“无效”的内存地址,如果后续继续通过这个指针去访问或操作那块内存,就会导致未定义行为,可能引发程序崩溃、数据损坏或其他难以调试的问题。
情况1: 数据传输到另一个线程。
- 普通情况下因为方法结束后栈对象会销毁,那么传递到另一个线程就需要
new
一个堆对象出来,传递给线程,之后等不需要的时候再进行delete
。这种情况下用了指针就存在后续可能有悬垂指针的情况。 推荐做法: 不创建堆对象,通过std::move
来把内存数据移动到thread
的function
对象里。
void TestPassObjectRefData_1()
{
cout << "TestPassObjectRefData_1" << endl;
string str = "Hello";
thread t1(std::bind([](const std::string& s) {
std::cout << std::this_thread::get_id() << "=>" << s << std::endl;
},
std::move(str) // 移动捕获
));
// 1. 线程`func`函数没使用前字符串内存数据已经通过`std::bind`函数移走了。
// 2. 这种是传递数据到另一个线程最合适的方式.
cout << "str is empty: " << str << endl;
t1.join();
}
- 注意不要使用传递引用的方式,因为引用在工作线程后执行时会失效。
void TestPassObjectRefData_2()
{
cout << "void TestPassObjectRefData_2" << endl;
std::string str = "Hello";
auto func = [&str]() mutable {
std::string local_str = std::move(str); // 在 lambda 内部移动
std::cout << std::this_thread::get_id() << "=>" << local_str << std::endl;
};
// 1. 没执行`func`函数前,`str`数据还在;如果开启线程执行`func`,而线程在主线程后执行,那么引用`str`已经被销毁,已经是失效状态。
// 2. 不能用这种方式。
std::cout << std::this_thread::get_id() << "=> str is not empty: " << str << endl;
thread t1(func);
// 如果不是`join`,而是`detach`,那么可能会出现工作线程还没执行,方法就已经结束, `str`销毁,之后线程执行`move(str)`就会报错。
t1.join();
}
情况2: 多个线程共用一个变量
- 使用共享指针封装对象,不使用裸指针
void TestPassObjectForSharedRead()
{
cout << "TestPassObjectForSharedRead" << endl;
shared_ptr<void> sp(new string("world"));
vector<thread> ts;
for (int i = 0; i < 10; i++)
ts.emplace_back(bind(DoWork,sp));
for (int i = 0; i < 10; i++)
ts[i].join();
}
情况3: 局部变量必须使用堆变量
- 比如
malloc
大的连续内存,牺牲一点性能,使用SafeArray
[2],std::shared_ptr
或std::unique_ptr
封装。
class A {
public:
A(const string& str) {
this->str = str;
}
~A() {
cout << "~A" << endl;
}
string str;
};
#define NATIVE_FREE(a,name) shared_ptr<void> a##name(a,[](void* data){ free(data); cout << "call free" << endl; })
#define NATIVE_DELETE(a,name) using PType = remove_pointer_t<decltype(a)>;shared_ptr<PType> a##name(a,[](PType* data){ delete data;})
void TestLocalHeadObject()
{
cout << "TestLocalHeadObject" << endl;
auto kStr = "hello world!";
const int kMaxPath = 512;
auto buf_1 = malloc(kMaxPath); // 如果有void*指针
// 使用之前先用RAII方式封装,方法执行完后只执行一次释放, 避免悬垂指针。如果需要传递给其他线程,把它封装到类里,传递类。
SafeArray sf((uint8_t*)buf_1, kMaxPath);
sf.copy(sf, sf.total(), (uint8_t*)kStr, strlen(kStr));
// 因为已经用类来封装数组,所以不再使用`NATIVE_FREE`来控制生命周期。
// NATIVE_FREE(buf_1, buf_1);
auto buf_2 = new A("hello world"); // 如果有对象指针
// 使用之前先用RAII方式封装,方法执行完后只执行一次释放, 避免悬垂指针。如果需要传递给其他线程,把它封装到共享指针里,传递共享指针。
NATIVE_DELETE(buf_2,_sp);
// 不再直接使用指针`buf_1`,使用SafeArray
cout << "buf_1: " << string((char*)sf.begin(),sf.total()) << endl;
// 不再直接使用指针`buf_2`,使用智能指针
cout << "buf_2: " << buf_2_sp->str << endl;
}
情况4: 对象作为函数参数时
- 调用函数传参不要传递指针,传递引用或者共享指针, 共享指针参数与以用在多线程里。
void TestPassParam(A& a)
{
cout << "TestPassParam" << endl;
cout << a.str << endl;
}
情况5: 通过PostMessage
传递指针时
WTL/Win32
编程时,可能需要通过PostMessage
传递数据到主线程。这时候可以使用DispatchQueue
类,参考[7],并使用lambda
传递对象到主线程。
DispatchQueue::DispatchAsync(DispatchQueue::DispatchGetMainQueue(), new std::function<void()>([this,row]() {
list_view_.RefreshCell(2, row);
}));
例子
- 这里自定义
SafeArray
类增加了一个接受缓存和缓存大小的参数,这让它可以托管传输的缓存。
TestCppWildPointer.cpp
#include <iostream>
#include <string>
#include <thread>
#include <algorithm>
#include <assert.h>
#include <functional>
#include <vector>
using namespace std;
class SafeArray
{
public:
SafeArray(int size) {
buf_ = (uint8_t*)malloc(size+1);
if (buf_) {
memset(buf_, 0, size+1);
size_ = size;
}else {
throw "Error allocate size memory.";
}
}
SafeArray(uint8_t* buf, int bufSize) {
if(!buf || !bufSize)
throw "Error assign buf.";
buf_ = buf;
size_ = bufSize;
clear();
}
~SafeArray() {
free(buf_);
}
public:
uint8_t* begin() {
return buf_;
}
int total() {
return size_;
}
void clear() {
memset(buf_, 0, size_);
}
public:
uint8_t& at(int index) {
if (index < size_)
return *(buf_ + index);
string message("Index exceeds maximum limit!");
message.append(" -> ").append(to_string(index));
throw std::out_of_range(message);
}
uint8_t& operator [](int index) {
return at(index);
}
operator uint8_t*(){
return current();
}
uint8_t* current() {
if (index_ >= size_)
return NULL;
return buf_ + index_;
}
int remain() {
return size_ - index_;
}
int remain(int maxSize) {
return min(remain(), maxSize);
}
void reset() {
index_ = 0;
}
uint8_t* add(int number) {
if ((index_ + number) < 0)
return NULL;
if ((index_ + number) > size_)
return NULL;
index_ += number;
return buf_ + index_;
}
bool full() {
return (index_ + 1) == size_;
}
int copy(uint8_t* dest, int destSize, uint8_t* source, int sourceSize) {
if (!destSize || !sourceSize)
return 0;
auto lSize = remain();
if (!lSize)
return 0;
if (destSize > lSize)
destSize = lSize;
if (sourceSize > destSize)
sourceSize = destSize;
if (memcpy_s(dest, destSize, source, sourceSize) == 0) {
auto count = min(destSize, sourceSize);
return (add(count))?count:0;
}
return 0;
}
int index() {
return index_;
}
const char* c_str() {
if (index_ == 0)
return "";
return (const char*)buf_;
}
private:
uint8_t *buf_ = NULL;
int size_ = 0;
int index_ = 0;
};
void TestPassObjectRefData_1()
{
cout << "TestPassObjectRefData_1" << endl;
string str = "Hello";
thread t1(std::bind([](const std::string& s) {
std::cout << std::this_thread::get_id() << "=>" << s << std::endl;
},
std::move(str) // 移动捕获
));
// 1. 线程`func`函数没使用前字符串内存数据已经通过`std::bind`函数移走了。
// 2. 这种是传递数据到另一个线程最合适的方式.
cout << "str is empty: " << str << endl;
t1.join();
}
void TestPassObjectRefData_2()
{
cout << "void TestPassObjectRefData_2" << endl;
std::string str = "Hello";
auto func = [&str]() mutable {
std::string local_str = std::move(str); // 在 lambda 内部移动
std::cout << std::this_thread::get_id() << "=>" << local_str << std::endl;
};
// 1. 没执行`func`函数前,`str`数据还在;如果开启线程执行`func`,而线程在主线程后执行,那么引用`str`已经被销毁,已经是失效状态。
// 2. 不能用这种方式。
std::cout << std::this_thread::get_id() << "=> str is not empty: " << str << endl;
thread t1(func);
// 如果不是`join`,而是`detach`,那么可能会出现工作线程还没执行,方法就已经结束, `str`销毁,之后线程执行`move(str)`就会报错。
t1.join();
}
void DoWork(shared_ptr<void> data)
{
auto& str = *(string*)data.get();
cout << "DoWork " << " =>" << this_thread::get_id() << " =>" << str << endl;
}
void TestPassObjectForSharedRead()
{
cout << "TestPassObjectForSharedRead" << endl;
shared_ptr<void> sp(new string("world"));
vector<thread> ts;
for (int i = 0; i < 10; i++)
ts.emplace_back(bind(DoWork,sp));
for (int i = 0; i < 10; i++)
ts[i].join();
}
class A {
public:
A(const string& str) {
this->str = str;
}
~A() {
cout << "~A" << endl;
}
string str;
};
#define NATIVE_FREE(a,name) shared_ptr<void> a##name(a,[](void* data){ free(data); cout << "call free" << endl; })
#define NATIVE_DELETE(a,name) using PType = remove_pointer_t<decltype(a)>;shared_ptr<PType> a##name(a,[](PType* data){ delete data;})
void TestLocalHeadObject()
{
cout << "TestLocalHeadObject" << endl;
auto kStr = "hello world!";
const int kMaxPath = 512;
auto buf_1 = malloc(kMaxPath); // 如果有void*指针
// 使用之前先用RAII方式封装,方法执行完后只执行一次释放, 避免悬垂指针。如果需要传递给其他线程,把它封装到类里,传递类。
SafeArray sf((uint8_t*)buf_1, kMaxPath);
sf.copy(sf, sf.total(), (uint8_t*)kStr, strlen(kStr));
// 因为已经用类来封装数组,所以不再使用`NATIVE_FREE`来控制生命周期。
// NATIVE_FREE(buf_1, buf_1);
auto buf_2 = new A("hello world"); // 如果有对象指针
// 使用之前先用RAII方式封装,方法执行完后只执行一次释放, 避免悬垂指针。如果需要传递给其他线程,把它封装到共享指针里,传递共享指针。
NATIVE_DELETE(buf_2,_sp);
// 不再直接使用指针`buf_1`,使用SafeArray
cout << "buf_1: " << string((char*)sf.begin(),sf.total()) << endl;
// 不再直接使用指针`buf_2`,使用智能指针
cout << "buf_2: " << buf_2_sp->str << endl;
}
void TestPassParam(A& a)
{
cout << "TestPassParam" << endl;
cout << a.str << endl;
}
void TestWildPointer()
{
// 1. 数据传输到另一个线程。
// -- 普通情况下因为方法结束后栈对象会销毁,那么传递到另一个线程就需要`new`一个堆对象出来,传递给线程,之后等不需要的时候再进行`delete`。
// 这种情况下用了指针就存在后续可能有悬垂指针的情况。
// 推荐做法: 不创建堆对象,通过`std::move`来把内存数据移动到`thread`的`function`对象里。
TestPassObjectRefData_1();
// -- 使用引用的方法传递给`function`对象,当线程执行
TestPassObjectRefData_2();
// 2. 多个线程共用一个变量
// -- 使用共享指针封装对象,不使用裸指针
TestPassObjectForSharedRead();
// 3. 局部变量必须使用堆变量,比如`malloc`大的连续内存
// -- 牺牲一点性能,使用`std::shared_ptr`或`std::unique_ptr`。
TestLocalHeadObject();
// 4. 调用函数传参不要传递指针,传递引用或者共享指针, 共享指针参数与以用在多线程里。
A a("infoworld");
TestPassParam(a);
}
int main()
{
std::cout << "Hello World!\n";
TestWildPointer();
}
dispatch_queue.h
#include <Windows.h>
#include <WinUser.h>
#include <functional>
enum
{
WMC_DISPATCH_MAIN_QUEUE = WM_USER+1000
};
typedef struct DispatchQueueObject1
{
DWORD threadId;
HWND m_hwnd;
UINT msg;
}DispatchQueueObject;
class DispatchQueue
{
public:
static DWORD GetMainThreadId();
static void DispatchQueueInit(HWND hwnd);
static DispatchQueueObject* DispatchGetMainQueue();
static void FreeDispatchMainQueue(DispatchQueueObject* dqo);
static void DispatchAsync(DispatchQueueObject* queue,std::function<void()>* callback);
};
dispatch_queue.cpp
#include "tool/dispatch_queue.h"
static HWND gMainHwnd = NULL;
static DWORD gMainThreadId = 0;
DWORD DispatchQueue::GetMainThreadId()
{
return gMainThreadId;
}
void DispatchQueue::DispatchQueueInit(HWND hwnd)
{
gMainHwnd = hwnd;
gMainThreadId = GetCurrentThreadId();
}
void DispatchQueue::DispatchAsync(DispatchQueueObject* queue,std::function<void()>* callback)
{
if(queue->threadId){
::PostThreadMessage(queue->threadId,queue->msg,(WPARAM)callback,0);
}else{
::PostMessage(queue->m_hwnd,queue->msg,(WPARAM)callback,0);
}
FreeDispatchMainQueue(queue);
}
DispatchQueueObject* DispatchQueue::DispatchGetMainQueue()
{
DispatchQueueObject* object = (DispatchQueueObject*)malloc(sizeof(DispatchQueueObject));
memset(object,0,sizeof(DispatchQueueObject));
object->m_hwnd = gMainHwnd;
object->msg = WMC_DISPATCH_MAIN_QUEUE;
return object;
}
void DispatchQueue::FreeDispatchMainQueue(DispatchQueueObject* dqo)
{
free(dqo);
}
输出
Hello World!
TestPassObjectRefData_1
3040=>str is empty: Hello
void TestPassObjectRefData_2
23184=> str is not empty: Hello
25952=>Hello
TestPassObjectForSharedRead
DoWork =>25608DoWork =>DoWork DoWork => =>DoWork => =>DoWork world =>
24284DoWork => =>world23232DoWork =>14684 =>world5880 =>DoWork world19244 =>27300 =>
world =>
world26984 =>
world =>
world
DoWork =>1204 =>
world26308 =>world
TestLocalHeadObject
buf_1: hello world!
buf_2: hello world
~A
TestPassParam
infoworld
~A