如何检查指向无效内存地址的 C++ 指针?

How to check c++ pointer pointing to invalid memory address?

有没有人告诉我如何检查我的指针是否指向无效的内存地址。

#include<iostream>

class Node{
 public:
  int data;
  Node * next , * prev;
};

// Driver Code

int main () {

   Node * node = new Node{ 3 , nullptr , nullptr };
   Node * ptr = node;

   delete node;
   // here node gets deleted from memory and ptr pointing to invalid memory address

   if(ptr == nullptr)
      std::cout << "ptr is null \n";
   else std::cout << "ptr is not null !\n";
   return 0;
 }

 // OUTPUT : ptr is not null !

这里我有非常简单的代码,其中“node”在堆和指针“ptr”中分配内存 在此之后指向节点我删除了'node'并且'ptr'仍然指向'node'。所以问题是我如何检查'ptr'是否指向无效的内存地址。

Windows 上的调试器将使用 ReadProcessMemory and WriteProcessMemory 函数以安全的方式访问被调试程序的内存。如果内存不可访问,这些函数不会崩溃,而是 return 一个错误。

在调试器之外使用 ReadProcessMemory 有很多缺点:

  1. 非常糟糕的编程设计。
  2. 不确定它是否适用于自己的进程。
  3. 不可移植,需要了解 Linux 和 macOS。
  4. 该函数比直接内存访问慢几个数量级。您的程序可能会变得非常慢。
  5. 内存可访问并不意味着指针有效。它可以指向其他完全随机的应用程序数据。你不能相信你读到的东西;显示调试是可以的,真正使用它不是。而且写字很危险

或者,您可能实际上需要的是Address SanitizerAddress Sanitizer 是一个非常强大的 C++ 调试工具,由 Google 开发,目前内置在所有主要编译器中:GCC、Clang 和 MSVC。

Address Sanitizer 将在取消引用之前自动检查每个指针值的有效性。一个有效的指针意味着指向一个先前分配的但尚未释放的块。如果你有一个错误的指针,程序会停止并显示一条很好的诊断消息。

Valgrind 是一个类似的调试工具,但我会推荐 Address Sanitizer,因为它快得多,使用的内存更少,并且在所有平台上都可用。

是的,您可以检查指针是否指向特定大小的已分配内存。

但是不,您无法检查它是否指向“正确”的对象。但由于这不是您的问题,我假设您只关心取消引用指针是否会导致程序崩溃。

bool wouldDereferencingCauseCrash(int* ptr); //checks if ptr points to sizeof(int) 
                                             //allocated on heap bytes

int* a = new(int);
int* b = a;
wouldDereferencingCauseCrash(b); //returns false - b points to memory alocated for a
free(a);
wouldDereferencingCauseCrash(b); //returns true -  b points to freed memory chunk 
int* c = new(int);
wouldDereferencingCauseCrash(b); //returns ???? -  b points to either freed memory 
                                 //                or memory allocated for c

现在我们如何实现这个神秘的函数“wouldDereferencingCauseCrash”?

首先,基础知识。假设您使用的是 GCC 编译器,而 new() 实际上只是伪装的 malloc()(通常是这种情况)。

堆是一个连续的内存块。 Malloc() returns 一块内存,我们的目的看起来像这样:

//this is a simplification but you can deduce all these parameters from the original 

//struct contained in malloc.c

struct memory_chunk{

void* ptrToPreviousChunk; //we can deduce pointer to prev. allocated memory_chunk
void* ptrToNextChunk;     //we can deduce pointer to next allocated memory_chunk
void* ptrToChunk;         //this is what malloc() returns - usable memory.
int sizeOfChunk;          //we can deduce if this holds sizeof(int) bytes

};

现在,如果我们只是遍历所有已分配的块并找到我们的指针 - 我们知道它指向已分配的内存。如果 sizeOfChunk 也是 sizeof(int) - 我们知道它包含一个整数。瞧,取消引用这个指针不会导致崩溃。伟大的!但为了安全起见,不要尝试写入此内存,先复制它:)。

现在是难点:

1/ 根据您的编译器,malloc() 的工作方式可能不同,

2/ malloc() 有时不在堆上分配内存块,有时它使用 mmap() 映射它们(但通常只有当它们非常非常大时),

3/ new() 可能不基于 malloc()(不太可能),

4/ 我对此进行了相当大的简化,如果您有兴趣实施这些内容,请阅读资源。我建议改用 unique_ptr 或在某种地图中跟踪 allocations/deallocations。

祝你好运! :) 我希望在我附近的任何地方都看不到这些废话 :)

来源:

https://sourceware.org/glibc/wiki/MallocInternals

https://reversea.me/index.php/digging-into-malloc/

如评论中所述,没有规范的方法来检查原始指针是否指向有效分配的内存。任何依赖于底层编译器特定语义的代码都会产生脆弱的代码。

幸运的是,自 C++11 以来,C++ 标准库为我们提供了 3 种智能指针类型,我们可以使用它们来编写可以检查内存有效性的安全代码。查看智能指针的 documentation

三种智能指针类型是std::unique_ptrstd::shared_ptrstd::weak_ptr

  • unique_ptr 只允许底层分配内存的单一所有权。这意味着在程序执行期间的任何时候,都只能使用一个 unique_ptr 对象来访问内存。一旦所有权转移到另一个 unique_ptr 对象,旧对象就不能再用于访问底层内存。这种智能指针非常适合保存私有状态或实现移动语义 API.
  • shared_ptr 允许共享内存所有权。只要至少有一个 shared_ptr 对象持有对内存的访问权限,内存就不会被释放。这是使用引用计数实现的。每次复制 shared_ptr 时,引用计数都会增加。每次 shared_ptr 对象超出范围时,引用计数都会减少。一旦计数达到 0,内存就会被释放。
  • weak_ptr 也与 shared_ptr 一起用于共享内存所有权。但是,持有 weak_ptr 对象不会阻止内存被释放(分配 weak_ptr 不会增加引用计数)。您可能想知道为什么这是一件好事?最简单的例子是循环引用。它将在下面的代码中显示。

实际上像问题中的链表不应该使用 unique_ptr(除非每个节点都拥有一些私有状态),而应该使用 shared_ptrweak_ptr。在下面的示例代码中,我将展示所有三种类型的使用(unique_ptr 将用于数据 - 而不是列表)。

#include <memory>
#include <iostream>

class List;
// A small sample ilustrating the use of smart pointer
class Node {
    friend class List;
public:
    typedef std::shared_ptr<Node> ptr;

    ~Node() = default; // No need to handle releasing memory ourselves - the smart pointer will take care of it

    static ptr create_with_data(int data) {
        return ptr(new Node(data));
    }

    ptr next() {
        return next_;
    }

    ptr prev() {
        return prev_.lock(); // We need to upgrade the weak_ptr to shared_ptr to actually be able to access the data
                             // If we don't have a previous element od if it was deleted we will return nullptr
    }

    int data() const {
        return *data_;
    }

private:
    // We make the constructors private so we can only create shared pointers to Node
    Node() = default;
    Node(int data) {
        data_.reset(new int);
        *data_ = data;
    }
    // The data will be automatically released when Node is released.
    // This is obviously not needed for int but if we were dealing with more complex data
    // Then it would have come handy
    std::unique_ptr<int> data_;
    ptr next_; // Pointer to the next node in the list
               // If we are released so will be the next node unless someone else is using it
    std::weak_ptr<Node> prev_; // Pointer to the previous node in the list (We will however not prevent it from being released)
                               // If we were to hold a shared_ptr here we would have prevented the list from being freed.
                               // because the reference count of prev would never get to be 0
};

class List {
public:
    typedef std::shared_ptr<List> ptr;
    ~List() = default; // Once List is deleted all the elements in the list will be dleted automatically
                       // If however someone is still holding an element of the list it will not be deleted until they are done

    static ptr create() {
        return ptr(new List());
    }

    void append(Node::ptr next) {
        if(nullptr == head_) {
            head_ = next;
        }
        else {
            auto tail = head_;
            while(tail->next_) {
                tail = tail->next_;
            }
            tail->next_ = next;
            next->prev_ = tail; // This will not increment the reference count of tail as prev_ is a weak_ptr
        }
    }

    Node::ptr head() {
        return head_;
    }

    long head_use_count() const {
        return head_.use_count();
    }
private:
    Node::ptr head_;
};

int main(int, char const*[]) {

    auto list = List::create(); // List will go out of scope when main returns and all the list will be released
    auto node = Node::create_with_data(100); // This node will also live until the end of main.
    std::cout << "node reference count: " << node.use_count() <<std::endl;
    list->append(node); // node is now the head of the list and has a reference count of 2
    std::cout << "node reference count: " << node.use_count() <<std::endl;
    node.reset(); // Hey what is this? node is no longer valid in the scope of main but continues to live happily inside the list
                  // the head of the list has a reference count of 1
    std::cout << "node reference count: " << node.use_count() <<std::endl;

    if (nullptr != node) {
        std::cout << node->data() << std::endl;    
    }
    else {
        std::cout << "node is released in this scope we can access the data using head()" << std::endl;
        std::cout << "Head is: " << list->head()->data() << std::endl;
        // You may thin that the below line should print 1. However since we requested
        // a copy of the head using head() it is 2
        std::cout << "Head reference count: " << list->head().use_count() << std::endl;
        // To print the use count from the list we will use the function we created for this
        // It will print 1 as expected
        std::cout << "Head reference count: " << list->head_use_count() << std::endl;
    }
    // Lets add another node to the list and then release the but continue holding our node and see what happens
    node = Node::create_with_data(200);
    list->append(node);
    // Did the reference count of the head changed? No because prev_ is weak_ptr
    std::cout << "Head reference count: " << list->head_use_count() << std::endl;
    auto prev = node->prev();
    // Did the reference count of the head changed? Yes because the call to prev() locks the  previous element for us
    // And the previous of node is the head
    std::cout << "Head reference count: " << list->head_use_count() << std::endl;
    prev.reset(); // Let's release our holding of the head
    std::cout << "Head reference count: " << list->head_use_count() << std::endl;
    // Traverse the list
    {
        auto next = list->head();
        while(next) {
            std::cout << "List Item: " << next->data() << std::endl;
            next = next->next();
        }
    }
    // Here we still hold a reference to the second element of the list.
    // Let's release the list and see what happens
    list.reset();
    if (nullptr != list) {
        std::cout << "The head of the list is " << list->head()->data() << std::endl;
    }
    else {
        // We will get here
        std::cout << "The list is released" <<std::endl;
        // So the list is released but we still have a reference to the second item - let's check this
        if (nullptr != node) {
            std::cout << "The data is " << node->data() << std::endl;
            // What about the head - can we maybe access it using prev?
            auto head = node->prev();
            if (nullptr != head) { // We will not get here
                std::cout << "The value of head is " << head->data() << std::endl;
            }
            else {
                // We will get here
                std::cout << "We are detached from the list" << std::endl;
            }
        }
        else {
            std::cout << "This is unexpected" << std::endl;
        }
    }    
    return 0;
}

注意 您在代码中看到的对 reset() 的调用只是为了说明当您释放引用 a shared_ptr 时会发生什么。在大多数情况下,您不会直接调用 reset()

我不知道如何检查无效内存地址,但我有解决您问题的方法,您可以创建自己的指针。这是代码

#include<iostream>

// Add this class in you're code
template<class T>
class Pointer {
 public:
       Pointer (T* node){
          this->node = node;
       }

       // use obj.DeletePtr() instead of 'delete' keyword
     void DeletePtr () {
        delete node;
        node = nullptr;
     }

     // compare to nullptr
     bool operator == (nullptr_t ptr){
       return node == ptr;
     }

 private:
    T* node;
};

class Node{
  public:
   int data;
   Node * next , * prev; 
};

 int main () {
    Pointer ptr (new Node{3 , nullptr , nullptr}); // initialize pointer like this

    ptr.DeletePtr();

    if(ptr == nullptr)
     std::cout << "ptr is null \n";
    else 
     std::cout << "ptr is not null !\n";

    return 0;
 }

我平时从不直接调用delete,我用模板函数清理内存:

    template < typename T > void destroy ( T*& p ) {
        if (p)
            delete p;
        p = nullptr;
    }

    ....

    Anything* mypointer = new ..... ;
    ....
    destroy(mypointer) ;   // Implicit instanciation, mypointer is nullptr on exit.

这样,您将永远不会有一个带有无效指针的已销毁对象。更好的是,对 destroy 的调用是安全的,您可以在已经 nullptr 的指针上调用它而不会产生任何后果。

我最终得到了这个解决方案它可能会帮助遇到同样问题的人

  #include<iostream>
  
  class Node{
   public:
    int data;
    Node * next , * prev;
  };
  
  template<class T>
  void DeletePtr (T*** ptr) {
      T** auxiliary = &(**ptr);
      delete *auxiliary;
      **ptr = nullptr;
      *ptr = nullptr;
  }
  
  
  // Driver Code
  int main () {
  
     Node * node = new Node{ 3 , nullptr , nullptr };
     Node ** ptr = &node;
     DeletePtr(&ptr);
  
     if(ptr == nullptr && node == nullptr)
        std::cout << "ptr is null \n";
     else std::cout << "ptr is not null !\n";
     return 0;
   }