场景
- 之前在如何避免出现悬垂指针[1]里说过如果避免悬垂指针的方法,即不传递和使用裸指针。 但是如果某些情况必须使用裸指针来操作,比如不得不用
FILE*
,HANDLE
这些标准库或系统的指针时,那如何避免出现悬垂指针或空指针?
说明
- 裸指针在使用智能指针封装后,不再使用裸指针,那么基本能杜绝悬垂指针的情况, 原理是使用
RAII
特性。 可以使用宏CLOSE_UNIQUE_FILE
来封装常用操作,避免写大量的冗余代码。注意,声明的宏同时把裸指针变量赋值为NULL
,这避免了裸指针再次调用出现的内存安全问题。
#define CLOSE_UNIQUE_FILE(a,name) std::unique_ptr<FILE, int(*)(FILE*)> name(a,fclose);a=NULL;
#define FREE_UNIQUE_BUFFER(a,name) std::unique_ptr<char, void(*)(void*)> name(buf, free);a=NULL;
#define DELETE_SHARE_CLASS(a,name) std::shared_ptr<std::remove_pointer_t<decltype(a)>> name(a, [](decltype(a) str) { delete str; });a=NULL;
void TestMalloc()
{
// 2. 有些`C`接口返回的`char*`类型;是通过`malloc`创建的.
auto buf = (char*)malloc(32);
FREE_UNIQUE_BUFFER(buf, spBuf);
}
void TestWin32HANDLE()
{
// 3. `Win32`对象
WIN32_FIND_DATA findFileData;
HANDLE hFind = FindFirstFile(L".\\*.*", &findFileData);
if (hFind == INVALID_HANDLE_VALUE)
return;
std::shared_ptr<void> spFind(hFind, ::FindClose);
}
void TestFILE()
{
// 1. 使用`fopen_s`
// -- 使用`unique_ptr`
FILE* file = NULL;
auto err = fopen_s(&file,"1.txt", "rb");
if (err != 0)
return;
CLOSE_UNIQUE_FILE(file, spFile);
}
-
把变量赋值为
NULL
,空指针调用非虚函数,而虚函数里不会访问它的成员变量时,这个行为是未定义,可能会崩溃,也可能不会。经验来看,很少会出现崩溃的情况。那么如何让空指针调用非虚函数一定会崩溃呢?让空指针解引用,这种方式一定会在运行时崩溃
。一般有两种方式实现。- 在
public
非虚函数的最前面增加判断this == nullptr
,如果为true
,那么抛出异常。可以声明的宏CHECK_NULL
。这种方式就是麻烦,要程序员自己记住。C++
的 避免内存安全问题的方式就是要程序员遵守规则。
#define CHECK_NULL() if(this == nullptr) throw std::runtime_error("Null pointer dereference!"); class A { public: A(const std::string& name) :name_(name) { } ~A() { std::cout << "~A" << std::endl; } virtual void sayNo() { std::cout << "No " << std::endl; } void sayBye() { // -- 不要使用断言`assert`,因为在`Release`模式可能会被优化掉。 CHECK_NULL(); std::cout << "Bye " << std::endl; } private: std::string name_; };
- 把方法声明为
virtual
,因为访问vtable
虚函数表需要this
解引用再调用方法。只要空指针this
解引用就一定崩溃。如上边的sayNo
方法。
- 在
-
避免悬垂指针和空指针的有效方法就是避免操作裸指针,创建后就不用了。
例子
- 这个例子再次细化了避免悬垂指针的方法。
#include <iostream>
#include <memory>
#include <assert.h>
#include <stdio.h>
#include <Windows.h>
// -- 不允许直接访问指针.
#define CLOSE_UNIQUE_FILE(a,name) std::unique_ptr<FILE, int(*)(FILE*)> name(a,fclose);a=NULL;
#define FREE_UNIQUE_BUFFER(a,name) std::unique_ptr<char, void(*)(void*)> name(buf, free);a=NULL;
#define DELETE_SHARE_CLASS(a,name) std::shared_ptr<std::remove_pointer_t<decltype(a)>> name(a, [](decltype(a) str) { delete str; });a=NULL;
#define CHECK_NULL() if(this == nullptr) throw std::runtime_error("Null pointer dereference!");
class A
{
public:
A(const std::string& name) :name_(name) {
}
~A() {
std::cout << "~A" << std::endl;
}
virtual void sayNo() {
std::cout << "No " << std::endl;
}
void say() {
std::cout << "hello " << name_.c_str() << "!" << std::endl;
}
void sayYes(){
std::cout << "Yes " << std::endl;
}
void sayBye() {
// -- 不要使用断言`assert`,因为在`Release`模式可能会被优化掉。
CHECK_NULL();
std::cout << "Bye " << std::endl;
}
private:
std::string name_;
};
void TestMalloc()
{
// 2. 有些`C`接口返回的`char*`类型;是通过`malloc`创建的.
auto buf = (char*)malloc(32);
FREE_UNIQUE_BUFFER(buf, spBuf);
}
void TestWin32HANDLE()
{
// 3. `Win32`对象
WIN32_FIND_DATA findFileData;
HANDLE hFind = FindFirstFile(L".\\*.*", &findFileData);
if (hFind == INVALID_HANDLE_VALUE)
return;
std::shared_ptr<void> spFind(hFind, ::FindClose);
}
void TestFILE()
{
// 1. 使用`fopen_s`
// -- 使用`unique_ptr`
FILE* file = NULL;
auto err = fopen_s(&file,"1.txt", "rb");
if (err != 0)
return;
CLOSE_UNIQUE_FILE(file, spFile);
}
void TestVoid()
{
auto a = new A("infoworld");
DELETE_SHARE_CLASS(a, spA);
spA->say();
// `NULL`指针调用非虚函数可能不崩溃,未定义行为。
// -- 可能会崩溃,可能不会。
a->sayYes();
// -- 但是调用成员变量就会崩溃,因为要对`this`解引用访问成员变量,即对`NULL`解引用必然崩溃。
//a->say();
// -- 方法1:调用虚函数需要查找虚函数表,解引用`this`也必然崩溃。
//a->sayNo();
// -- 方法2:使用宏来判断`this`是否为`NULL`
// 如果不捕抓,就会在运行时崩溃。
try {
a->sayBye();
}catch (const std::exception& e) {
std::cout << e.what() << std::endl;
}
}
int main()
{
std::cout << "Hello World!\n";
TestFILE();
TestMalloc();
TestWin32HANDLE();
TestVoid();
}
输出
Hello World!
hello infoworld!
Yes
Null pointer dereference!
~A