C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。
栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。
全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。
常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。
代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。
说明:
从操作系统的本身来讲,以上存储区在内存中的分布是如下形式(从低地址到高地址):.text 段 —> .data 段 —> .bss 段 —> 堆 —> unused —> 栈 —> env
#include <iostream>
using namespace std;
int g_var = 0; //全局区,.bss
char *gp_var; // 全局区 .data
int main()
{
int var; //栈
char *p_var; //栈
char arr[] = "abc"; //数组arr在栈,"abc"在常量区
char *p_var1 = "123456"; //p_var1在栈,"123456"在常量区
static int s_var = 0; //s_var在静态存储区(.data)
p_var = (char *)malloc(10);//分配的10个字节在堆中
free(p_var);
return 0;
}
C++内存泄漏
内存泄漏是指程序的不正常操作导致分配的内存由于没有正常的释放而导致的该块内存不可用。常指堆内存的泄漏。使用 malloc
calloc
realloc
new
等分配内存时,使用完后需要调用相应的 free
delete
进行释放
char *p = (char *)malloc(10);
char *p1 = (char *)malloc(10);
p = np;
上面的程序是由于指针的重新赋值而导致的内存泄漏。
防止内存泄漏的方法
using namespace std;
class A { private: char *p; unsigned int p_size;
public: A(unsigned int n = 1) // 构造函数中分配内存空间 { p = new char[n]; p_size = n; }; ~A() // 析构函数中释放内存空间 { if (p != NULL) { delete[] p; // 删除字符数组 p = NULL; // 防止出现野指针 } }; char GetPointer() { return p; }; }; void fun() { A ex(100); char p = ex.GetPointer(); strcpy(p, “Test”); cout << p << endl; } int main() { fun(); return 0; }
以上的做法会在类对象进行复制的时候,程序会出现同一块内存空间释放两次的情况。如下所示
```cpp
void fun1()
{
A ex(100);
A ex1 = ex;
char *p = ex.GetPointer();
strcpy(p, "Test");
cout << p << endl;
}
对于 fun1 这个函数中定义的两个类的对象而言,在离开该函数的作用域时,会两次调用析构函数来释放空间,但是这两个对象指向的是同一块内存空间,所以导致同一块内存空间被释放两次,可以通过增加计数机制来避免这种情况
#include <iostream>
#include <cstring>
using namespace std;
class A
{
private:
char *p;
unsigned int p_size;
int *p_count; // 计数变量
public:
A(unsigned int n = 1) // 在构造函数中申请内存
{
p = new char[n];
p_size = n;
p_count = new int;
*p_count = 1;
cout << "count is : " << *p_count << endl;
};
A(const A &temp)
{
p = temp.p;
p_size = temp.p_size;
p_count = temp.p_count;
(*p_count)++; // 复制时,计数变量 +1
cout << "count is : " << *p_count << endl;
}
~A()
{
(*p_count)--; // 析构时,计数变量 -1
cout << "count is : " << *p_count << endl;
if (*p_count == 0) // 只有当计数变量为 0 的时候才会释放该块内存空间
{
cout << "buf is deleted" << endl;
if (p != NULL)
{
delete[] p; // 删除字符数组
p = NULL; // 防止出现野指针
if (p_count != NULL)
{
delete p_count;
p_count = NULL;
}
}
}
};
char *GetPointer()
{
return p;
};
};
void fun()
{
A ex(100);
char *p = ex.GetPointer();
strcpy(p, "Test");
cout << p << endl;
A ex1 = ex; // 此时计数变量会 +1
cout << "ex1.p = " << ex1.GetPointer() << endl;
}
int main()
{
fun();
return 0;
}
程序运行结果的倒数 2、3 行是调用两次析构函数时进行的操作,在第二次调用析构函数时,进行内存空间的释放,从而会有倒数第 1 行的输出结果。
内存泄漏工具的实现原理
valgrind 是一套 Linux 下,开放源代码(GPL V2)的仿真调试工具的集合,
valgrind 包括以下工具:
Memcheck:内存检查器(valgrind 应用最广泛的工具),能够发现开发中绝大多数内存错误的使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。
Callgrind:检查程序中函数调用过程中出现的问题。
Cachegrind:检查程序中缓存使用出现的问题。
Helgrind:检查多线程程序中出现的竞争问题。
Massif:检查程序中堆栈使用中出现的问题。
Extension:可以利用 core 提供的功能,自己编写特定的内存调试工具。
Memcheck 能够检测出内存问题,关键在于其建立了两个全局表:
Valid-Value 表:对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits ;对于 CPU 的每个寄存器,也有一个与之对应的 bit 向量。这些 bits 负责记录该字节或者寄存器值是否具有有效的、已初始化的值。
Valid-Address 表:对于进程整个地址空间中的每一个字节(byte),还有与之对应的 1 个 bit,负责记录该地址是否能够被读写。
检测原理:
当要读写内存中某个字节时,首先检查这个字节对应的 Valid-Address 表中对应的 bit。如果该 bit 显示该位置是无效位置,Memcheck 则报告读写错误。
内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节在 Valid-Value 表对应的 bits 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 Memcheck 会检查 Valid-Value 表对应的 bits,如果该值尚未初始化,则会报告使用未初始化内存错误。
valgrind的实现
valgrind用以检查内存,比方说对于如下命令,用来查看内存的泄漏
valgrind --tool=memcheck --leak-check=yes --log-file="logfilenew.out" --trace-children=yes --leak-check=full --show-leak-kinds=all -v ./application -n 5
valgrind的still reachable不算事内存泄漏
如果报告中有上图红框框所示意的部分的泄漏,证明存在内存的泄漏。
下图中,显示的是内存没有泄漏
valgrind --tool=memcheck --leak-check=yes --log-file="logflenew.out" --track-origins=yes --trace-children=yes --leak-check=full --show-leak-kinds=all -v ./demo