如何检查指向无效内存地址的 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
有很多缺点:
- 非常糟糕的编程设计。
- 不确定它是否适用于自己的进程。
- 不可移植,需要了解 Linux 和 macOS。
- 该函数比直接内存访问慢几个数量级。您的程序可能会变得非常慢。
- 内存可访问并不意味着指针有效。它可以指向其他完全随机的应用程序数据。你不能相信你读到的东西;显示调试是可以的,真正使用它不是。而且写字很危险
或者,您可能实际上需要的是Address Sanitizer。 Address 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。
祝你好运! :) 我希望在我附近的任何地方都看不到这些废话 :)
来源:
如评论中所述,没有规范的方法来检查原始指针是否指向有效分配的内存。任何依赖于底层编译器特定语义的代码都会产生脆弱的代码。
幸运的是,自 C++11 以来,C++ 标准库为我们提供了 3 种智能指针类型,我们可以使用它们来编写可以检查内存有效性的安全代码。查看智能指针的 documentation。
三种智能指针类型是std::unique_ptr
、std::shared_ptr
和std::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_ptr
和 weak_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;
}
有没有人告诉我如何检查我的指针是否指向无效的内存地址。
#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
有很多缺点:
- 非常糟糕的编程设计。
- 不确定它是否适用于自己的进程。
- 不可移植,需要了解 Linux 和 macOS。
- 该函数比直接内存访问慢几个数量级。您的程序可能会变得非常慢。
- 内存可访问并不意味着指针有效。它可以指向其他完全随机的应用程序数据。你不能相信你读到的东西;显示调试是可以的,真正使用它不是。而且写字很危险
或者,您可能实际上需要的是Address Sanitizer。 Address 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。
祝你好运! :) 我希望在我附近的任何地方都看不到这些废话 :)
来源:
如评论中所述,没有规范的方法来检查原始指针是否指向有效分配的内存。任何依赖于底层编译器特定语义的代码都会产生脆弱的代码。
幸运的是,自 C++11 以来,C++ 标准库为我们提供了 3 种智能指针类型,我们可以使用它们来编写可以检查内存有效性的安全代码。查看智能指针的 documentation。
三种智能指针类型是std::unique_ptr
、std::shared_ptr
和std::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_ptr
和 weak_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;
}