C++常见的内存错误和解决策略

ops/2024/10/9 2:17:32/

目录

1.未初始化指针 (Uninitialized Pointer)

2.内存分配未成功却使用了它

3.野指针 (Dangling Pointer)

4.内存泄漏 (Memory Leak)

5.重复释放内存 (Double Free)

6.内存越界访问 (Buffer Overflow)

7.错误的数组删除方式 (Mismatched Delete)

8.栈内存溢出 (Stack Overflow)

9.delete (void*)

10.Virtual destructor

11. 对象循环引用


1.未初始化指针 (Uninitialized Pointer)

  • 错误描述:内存分配成功后,未进行初始化就引用,可能导致错误的数据引用。
  • 解决策略:无论用何种方式创建数组或动态内存,都应为其赋初值,防止将未被初始化的内存作为右值使用。

常见的示例代码如下:

int* p;
*p = 10; //可能会导致内存访问错误

解决方案:在使用指针前,进行初始化。

int* p = nullptr;
p = new int;
*p = 10;

2.内存分配未成功却使用了它

  • 错误描述:在内存分配失败的情况下,程序仍然尝试使用未成功分配的内存。
  • 解决策略:在使用内存之前,应检查指针是否为NULL。如果指针是函数的参数,那么在函数的入口处用assert(p!=NULL)(头文件是assert.h)进行检查。如果是用mallocnew来申请内存,应该用if(p==NULL)进行防错处理。

C++之assert惯用法_c++ assert-CSDN博客

3.野指针 (Dangling Pointer)

  • 错误描述:内存释放后,程序仍然尝试使用已释放的内存,可能导致程序崩溃或不稳定。

  • 解决策略

    • 重新设计数据结构,从根本上解决对象管理的混乱局面。
    • 注意函数的return语句,不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
    • 使用freedelete释放了内存后,将指针设置为NULL,防止产生“野指针”。
    • 使用智能指针可以避免野指针问题,因为它们会在对象超出作用域时自动释放资源。

4.内存泄漏 (Memory Leak)

问题: 动态分配的内存在不需要时没有被正确释放,导致可用内存越来越少。

解决策略:

  • 智能指针: 使用 C++ 的智能指针(如 std::unique_ptr 和 std::shared_ptr)来管理动态内存。它们可以在超出作用域时自动释放内存。

  • 确保delete匹配new: 每个通过new分配的内存,都要在适当的时机调用delete释放。

  • 工具检查: 使用内存检测工具如 Valgrind 或 AddressSanitizer 来检测内存泄漏。

C++惯用法之RAII思想: 资源管理_raii 思想-CSDN博客

下面看个例子:

void MemoryLeakFunction()
{XXX_Class * pObj = new XXX_Class();pObj->DoSomething();return; 
}

下面这个场景,就是析构函数中并没有释放成员所指向的内存。这个我们就要注意了,一般当你构建一个类的时候,写析构函数一定要切记释放类成员关联的资源。

class MemoryLeakClass
{
public:MemoryLeakClass() { m_pObj = new XXX_ResourceClass;}void DoSomething(){m_pObj->DoSomething();}~MemoryLeakClass(){;}
private:XXX_ResourceClass* m_pObj;
};

boost或者C++ 11后,通过智能指针去进行包裹这个原始指针,这是一种RAII的思想, 在out of scope的时候,释放自己所包裹的原始指针指向的资源。将上述例子用unique_ptr改写一下。

void MemoryLeakFunction()
{std::unique_ptr<XXX_Class> pObj = make_unique<XXX_Class>();pObj->DoSomething();return; 
}

5.重复释放内存 (Double Free)

问题: 动态分配的内存被释放多次,这可能会导致程序崩溃或未定义行为。

解决策略:

  • 将指针设置为 nullptr: 在释放指针之后,将其设置为nullptr,以避免重复释放。

  • 检查指针状态: 释放内存前,检查指针是否为nullptr

int* p = new int(10);
delete p;
p = nullptr;  // 避免重复释放

6.内存越界访问 (Buffer Overflow)

问题: 动态分配的内存被超范围使用,可能导致数据损坏、程序崩溃等问题。

解决策略:

  • 谨慎使用指针和数组: 确保访问的内存区域是合法的。

  • 边界检查: 在处理动态数组时,始终执行边界检查。

  • 使用标准库容器: 使用如 std::vectorstd::string 等标准库容器,它们会自动管理边界检查。

7.错误的数组删除方式 (Mismatched Delete)

问题: 使用new[]分配的数组使用delete而不是delete[]释放,或反之。

解决策略:

  • 确保匹配使用new/deletenew[]/delete[]:数组需要使用delete[]来释放。

  • 使用 C++ 的智能指针(如 std::unique_ptr 和 std::shared_ptr)来管理动态数组内存

  • 手动编写获取内存的类,自动释放内存,如:

C++简单缓冲区类设计_c++ 缓存区设计-CSDN博客

8.栈内存溢出 (Stack Overflow)

问题: 过度使用栈内存,通常是由于递归调用过深或栈上分配的大型对象。

解决策略:

  • 减少递归深度: 优化递归算法,避免过深的递归调用。

  • 将大对象放在堆上: 如果对象太大,使用new将其分配到堆上而非栈上。

9.delete (void*)

因为C++的灵活性,有时候会将一个对象指针转换为void *,隐藏其类型。这种情况SDK比较常用,实际上返回的并不是SDK用的实际类型,而是一个没有类型的地址,当然有时候我们会为其亲切的取一个名字,比如叫做XXX_HANDLE

那么继续用上述为例MemoryLeakClass, SDK假设提供了下面三个接口:

  1. InitObj创建一个对象,并且返回一个PROGRAMER_HANDLE(即void *),对应用程序屏蔽其实际类型
  2. DoSomething 提供了一个功能去做一些事情,输入的参数,即为通过InitObj申请的对象
  3. 应用程序使用完毕后,一般需要释放SDK申请的对象,提供了FreeObj
typedef void * PROGRAMER_HANDLE;PROGRAMER_HANDLE InitObj()
{MemoryLeakClass* pObj = new MemoryLeakClass();return (PROGRAMER_HANDLE)pObj;
}void DoSomething(PROGRAMER_HANDLE pHandle)
{((MemoryLeakClass*)pHandle)->DoSomething();
}void FreeObj(void *pObj)
{delete pObj;
}

看到这里,也许有读者已经发现问题所在了。上述代码在调用FreeObj的时候,delete看到的是一个void *, 只会释放对象所占用的内存,但是并不会调用对象的析构函数,那么对象内部的m_pStr所指向的内存并没有被释放,从而会导致内存泄露。修改也是自然比较简单的:

void FreeObj(void *pObj)
{delete ((MemoryLeakClass*)pObj);
}

那么一般来说,最好由相对资深的程序员去进行SDK的开发,无论从设计和实现上面,都尽量避免了各种让人泪流满满的坑。

10.Virtual destructor

C++析构函数为什么要为虚函数?_析构函数虚函数-CSDN博客

现在大家来看看这个很容易犯错的场景, 一个很常用的多态场景。那么在调用delete pObj;会出现内存泄露吗?

class Father
{
public:virtual void DoSomething()
{std::cout << "Father DoSomething()" << std::endl;}
};class Child : public Father
{
public:Child(){std::cout << "Child()" << std::endl;m_pStr = new char[100];}~Child(){std::cout << "~Child()" << std::endl;delete[] m_pStr;}void DoSomething()
{std::cout << "Child DoSomething()" << std::endl;}
protected:char* m_pStr;
};void MemoryLeakVirualDestructor()
{Father * pObj = new Child;pObj->DoSomething();delete pObj;
}

会的,因为Father没有设置Virtual 析构函数,那么在调用delete pObj;的时候会直接调用Father的析构函数,而不会调用Child的析构函数,这就导致了Child中的m_pStr所指向的内存,并没有被释放,从而导致了内存泄露。

并不是绝对,当有这种使用场景的时候,最好是设置基类的析构函数为虚析构函数。修改如下:

class Father
{
public:virtual void DoSomething()
{std::cout << "Father DoSomething()" << std::endl;}virtual ~Father() { ; }
};class Child : public Father
{
public:Child(){std::cout << "Child()" << std::endl;m_pStr = new char[100];}virtual ~Child(){std::cout << "~Child()" << std::endl;delete[] m_pStr;}void DoSomething()
{std::cout << "Child DoSomething()" << std::endl;}
protected:char* m_pStr;
};

11. 对象循环引用

看下面例子,既然为了防止内存泄露,于是使用了智能指针shared_ptr;并且这个例子就是创建了一个双向链表,为了简单演示,只有两个节点作为演示,创建了链表后,对链表进行遍历。
那么这个例子会导致内存泄露吗?

struct Node
{Node(int iVal){m_iVal = iVal;}~Node(){std::cout << "~Node(): " << "Node Value: " << m_iVal << std::endl;}void PrintNode()
{std::cout << "Node Value: " << m_iVal << std::endl;}std::shared_ptr<Node> m_pPreNode;std::shared_ptr<Node> m_pNextNode;int m_iVal;
};void MemoryLeakLoopReference()
{std::shared_ptr<Node> pFirstNode = std::make_shared<Node>(100);std::shared_ptr<Node> pSecondNode = std::make_shared<Node>(200);pFirstNode->m_pNextNode = pSecondNode;pSecondNode->m_pPreNode = pFirstNode;//Iterate nodesauto pNode = pFirstNode;while (pNode){pNode->PrintNode();pNode = pNode->m_pNextNode;}
}

先来看看下图,是链表创建完成后的示意图。有点晕乎了,怎么一个双向链表画的这么复杂,黄色背景的均为智能指针或者智能指针的组成部分。其实根据双向链表的简单性和下图的复杂性,可以想到,智能指针的引入虽然提高了安全性,但是损失的是性能。所以往往安全性和性能是需要互相权衡的。 我们继续往下看,哪里内存泄露了呢?

如果函数退出,那么m_pFirstNodem_pNextNode作为栈上局部变量,智能指针本身调用自己的析构函数,给引用的对象引用计数减去1(shared_ptr本质采用引用计数,当引用计数为0的时候,才会删除对象)。此时如下图所示,可以看到智能指针的引用计数仍然为1, 这也就导致了这两个节点的实际内存,并没有被释放掉, 从而导致内存泄露。

你可以在函数返回前手动调用pFirstNode->m_pNextNode.reset();强制让引用计数减去1, 打破这个循环引用。
还是之前那句话,如果通过手动去控制难免会出现遗漏的情况, C++提供了weak_ptr

struct Node
{Node(int iVal){m_iVal = iVal;}~Node(){std::cout << "~Node(): " << "Node Value: " << m_iVal << std::endl;}void PrintNode()
{std::cout << "Node Value: " << m_iVal << std::endl;}std::shared_ptr<Node> m_pPreNode;std::weak_ptr<Node>    m_pNextNode;int m_iVal;
};void MemoryLeakLoopRefference()
{std::shared_ptr<Node> pFirstNode = std::make_shared<Node>(100);std::shared_ptr<Node> pSecondNode = std::make_shared<Node>(200);pFirstNode->m_pNextNode = pSecondNode;pSecondNode->m_pPreNode = pFirstNode;//Iterate nodesauto pNode = pFirstNode;while (pNode){pNode->PrintNode();    pNode = pNode->m_pNextNode.lock();}
}

看看使用了weak_ptr之后的链表结构如下图所示,weak_ptr只是对管理的对象做了一个弱引用,其并不会实际支配对象的释放与否,对象在引用计数为0的时候就进行了释放,而无需关心weak_ptrweak计数。注意shared_ptr本身也会对weak计数加1.

那么在函数退出后,当pSecondNode调用析构函数的时候,对象的引用计数减一,引用计数为0,释放第二个Node,在释放第二个Node的过程中又调用了m_pPreNode的析构函数,第一个Node对象的引用计数减1,再加上pFirstNode析构函数对第一个Node对象的引用计数也减去1,那么第一个Node对象的引用计数也为0,第一个Node对象也进行了释放。

如果将上述代码改为双向循环链表,去除那个循环遍历Node的代码,那么最后Node的内存会被释放吗?这个问题留给读者。

推荐阅读

C++智能指针的自定义销毁器(销毁策略)_c++指针销毁-CSDN博客


http://www.ppmy.cn/ops/122929.html

相关文章

CSS 效果:实现动态展示双箭头

最近写了一段 CSS 样式&#xff0c;虽然不难&#xff0c;但实现过程比较繁琐。这个效果结合了两个箭头&#xff0c;一个突出&#xff0c;一个内缩&#xff0c;非常适合用于步骤导航或选项卡切换等场景。样式不仅仅是静态的&#xff0c;还可以通过点击 click 或者 hover 事件&am…

Spring Boot中线程池使用

说明&#xff1a;在一些场景&#xff0c;如导入数据&#xff0c;批量插入数据库&#xff0c;使用常规方法&#xff0c;需要等待较长时间&#xff0c;而使用线程池可以提高效率。本文介绍如何在Spring Boot中使用线程池来批量插入数据。 搭建环境 首先&#xff0c;创建一个Spr…

路由交换实验指南

案例 01&#xff1a;部署使用 eNSP 平台实验需求&#xff1a; 安装华为 eNSP 网络模拟平台打开 eNSP 平台&#xff0c;新建拓扑并绘制网络能够成功启动交换机、计算机设备 实验步骤&#xff1a; 安装华为 eNSP 网络模拟平台启动安装程序 配置安装内容 防护墙允许 eNSP 程序的…

SpringCloud Alibaba - Eureka注册中心,Nacos配置中心

Eureka 1、创建服务端 server:port: 8761 # eureka 默认端口spring:application:name: eureka-server # 应用名称&#xff08;微服务中建议必须定义应用名称&#xff09; SpringBootApplication EnableEurekaServer // 开启eureka注册中心功能 public class EurekaServerAppli…

力扣206.反转链表

题目链接&#xff1a;206. 反转链表 - 力扣&#xff08;LeetCode&#xff09; 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5]输出&#xff1a;[5,4,3,2,1] 示例 2&#xff1a; …

map和set的使用

引言 序列式容器和关联式容器 序列式容器&#xff1a;逻辑结构为线性序列的数据结构&#xff0c;两个位置存储的值之间⼀般没有紧密的关联关系&#xff0c;例如&#xff1a;string、vector、list、deque、array. 关联式容器&#xff1a;关联式容器也是用来存储数据的&#x…

如何让70B参数的大型语言模型在资源有限的边缘设备上高效运行?

你有没有想过,像我们平时使用的智能手机、家里的智能音箱这样的小设备,也能运行那些参数量高达数十亿的大型语言模型(LLM)呢?这听起来像是天方夜谭,毕竟这些模型动辄需要巨大的算力和存储资源,但实际上,随着技术的发展,这个梦想正在变成现实。那么,问题来了,怎么在资…

Streamlit:用Python快速构建交互式Web应用

在传统的Web开发中&#xff0c;开发者常常需要编写大量的前端和后端代码&#xff0c;才能实现一个简单的交互式Web应用。Streamlit 通过简化这一过程&#xff0c;使得你只需要用Python编写代码&#xff0c;就能快速创建具有丰富交互功能的Web应用。本文将介绍如何使用Streamlit…