[C/C++安全编程]_[中级]_[如何避免出现悬垂指针]

场景

  1. 在开发C/C++程序时,通常情况下,如果需要传递数据给其他线程,那么传递的类型就需要创建在堆上。 即使用mallocnew来创建指针对象,而安全编程最需要回避的就是操作指针。指针对象是需要手动管理释放的,这也可能造成内存安全问题。 比如悬垂指针,对象释放后再使用。那么有什么办法可以避免出现悬垂指针呢?

说明

  1. C/C++的编译器在Rust出现以前还没考虑过代替工程师检查代码安全的问题。代码的安全完全靠C/C++开发者的自觉和水平。一个要求代码质量高的C++项目,开发完之后还需要各种静态分析工具Clang-Tidy,Cppcheck,动态分析工具Valgrind,AddressSanitizer等进行扫描才能去除90%的内存安全错误。相对Rustc的编译即检查麻烦很多,因为这些外部工具也是要安装的,如果是版本高了,还不支持开发所用的系统。

  2. C/C++的内存安全问题就是避免出现未定义行为,避免使用指针。其中未定义行为希望新版的标准能检测出来,不过那都是之后的事。通常C/C++的项目都是使用了不同的C++标准的,比如老项目也支持C++11标准,而新项目支持C++20都不奇怪。因为除了Windows平台上的编译器发展慢之外,C++abi新版还不兼容旧版的。

  3. 出现悬垂指针的情况也有多种,以下总结了几种可能发生悬垂指针的情况。 使用建议的编程方法能避免悬垂指针出现的几率。

  4. 悬垂指针: 一个指针指向的内存已经被释放(或不再有效),但指针本身依然保存着那个“无效”的内存地址,如果后续继续通过这个指针去访问或操作那块内存,就会导致未定义行为​,可能引发程序崩溃、数据损坏或其他难以调试的问题。

情况1: 数据传输到另一个线程。

  1. 普通情况下因为方法结束后栈对象会销毁,那么传递到另一个线程就需要new一个堆对象出来,传递给线程,之后等不需要的时候再进行delete。这种情况下用了指针就存在后续可能有悬垂指针的情况。 推荐做法: 不创建堆对象,通过std::move来把内存数据移动到threadfunction对象里。
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();
}
  1. 注意不要使用传递引用的方式,因为引用在工作线程后执行时会失效。
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: 多个线程共用一个变量

  1. 使用共享指针封装对象,不使用裸指针
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: 局部变量必须使用堆变量

  1. 比如malloc大的连续内存,牺牲一点性能,使用SafeArray[2],std::shared_ptrstd::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: 对象作为函数参数时

  1. 调用函数传参不要传递指针,传递引用或者共享指针, 共享指针参数与以用在多线程里。
void TestPassParam(A& a)
{
	cout << "TestPassParam" << endl;
	cout << a.str << endl;
}

情况5: 通过PostMessage传递指针时

  1. WTL/Win32编程时,可能需要通过PostMessage传递数据到主线程。这时候可以使用DispatchQueue类,参考[7],并使用lambda传递对象到主线程。
DispatchQueue::DispatchAsync(DispatchQueue::DispatchGetMainQueue(), new std::function<void()>([this,row]() {

		list_view_.RefreshCell(2, row);
	}));

例子

  1. 这里自定义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

参考

  1. std::shared_ptr

  2. 如何避免数组访问越界

  3. std::thread

  4. Lambda expressions since C++11

  5. std::function

  6. std::bind

  7. Win32实现Cocoa的dispatch_async到主线程的异步消息处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白行微

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值