本文主要是为了谈谈C++编程过程中内存的手动申请和释放问题,以及记录本人在释放内存时犯的错误。
首先先讲一下内存的分配和划分方式(转自https://www.cnblogs.com/ruixin-jia/p/5877492.html)
内存分配方式
内存分配方式有三种:
[1]从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
[2]在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
[3]从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
在编写程序是尽量避免大量地定义全局变量(一方面大量地全局变量会占用大量地计算机内存,另一方面,大量地定义全局变量会大致我们在变量名称的构思上耗费很多精力,毕竟不能重复嘛,而局部变量会由计算机自动释放内存,出了其作用域,变量名又可以被使用,方便很多)。
下面看一下,一段C/C++代码编译的程序具体的内存划分:
1、栈区(stack)— 程序运行时由编译器自动分配,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。程序结束时由编译器自动释放。
2、堆区(heap) — 在内存开辟另一块存储区域。一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。c中用malloc, calloc, realloc等分配内存的函数分配得到的就是在堆上,C++中则用new和delete来分配和释放,当然C++中可以兼用C中的方式,而纯c风格的不可以使用new和delete,这部分处理不好就会造成内存泄漏
3、全局区(静态区)(static)—编译器编译时即分配内存。全局变量和静态变量的存储是放在一块的。对于C语言初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。而C++则没有这个区别 - 程序结束后由系统释放
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放,如string str = “Hello World”;中“Hello World”就存放于此
5、程序代码区—存放函数体的二进制代码。
下面根据我自己碰到的问题具体分析一下:
下面的程序是剑指offer中的例1(被说删掉了一部分)。
#include<cstring>
#include<cstdio>
#include<stdlib.h>
class CMyString
{
public:
//构造函数中给了一个默认值,在实现中,没有参数,则默认使用改值
CMyString(char* pData = nullptr);
CMyString(const CMyString& str);//深拷贝构造函数
~CMyString(void);
CMyString& operator = (const CMyString& str);//运算符重载函数
void Print();
private:
char* m_pData;
};
CMyString::CMyString(char *pData)
{
if (pData == nullptr)
{
m_pData = new char[1];
m_pData[0] = '\0';
}
else
{
int length = strlen(pData);
m_pData = new char[length + 1];
strcpy(m_pData, pData);
}
}
CMyString::CMyString(const CMyString &str)
{
int length = strlen(str.m_pData);
m_pData = new char[length + 1];
strcpy(m_pData, str.m_pData);
}
CMyString::~CMyString()
{
delete[] m_pData;
}
CMyString& CMyString::operator = (const CMyString& str)
{
if (this == &str)
return *this;
delete[]m_pData;
m_pData = nullptr;
m_pData = new char[strlen(str.m_pData) + 1];
strcpy(m_pData, str.m_pData);
return *this;
}
// ====================测试代码====================
void CMyString::Print()
{
printf("%s", m_pData);
}
void Test1()
{
printf("Test1 begins:\n");
char* text = "Hello world";
CMyString str1(text);
CMyString str2;
str2 = str1;
printf("The expected result is: %s.\n", text);
printf("The actual result is: ");
str2.Print();
printf(".\n");
}
int main(int argc, char* argv[])
{
Test1();
system("pause");
return 0;
}
以上程序的运行结果如下:
正确地输出了str1和str2的值,利用赋值运算符完成了两个实例的相互赋值操作。
之后我就有一个疑问,就是拷贝函数不是可以直接将str1拷贝给str2吗,为什么还要多此一举用运算符重载?把重载函数删掉直接用拷贝函数行不行呢?于是我就将赋值运算符重载函数的声明(
CMyString& operator = (const CMyString& str);//运算符重载函数
)和定义删除,其它不改动的情况下运行了一遍,发现结果并不是很好,程序死了,运行结果如下:
虽然结果输出了,但程序不能终止,一直处于调试状态,寻找问题,发现是直接赋值造成的。
如果改成
结果正常输出:
为什么会这样?经过查找,得到以下结论:赋值重载和拷贝函数很像尤其是都使用“=”时,但拷贝构造函数实在实例化的时候才会被调用,如果实例化完之后再用“=”就不会调用拷贝构造函数了,此时就是运算符重载。
然后我就想为什么将赋值重载函数删除后直接赋值就不行了,是应为C++中本来就不可以将两个实例进行互相赋值吗?显然不是的,虽然程序执行有问题,但通过结果可以看得出,str2是已经通过“=”获得了str1的所有数据了的。
经过一通折腾,发现,只要我将析构函数中的delete []m_pData;删除程序就可以正常运行,但这样又会造成内存泄漏,显然不合适,但基本可以肯定是内存管理问题了。是str2 = str1这个操作将str2和str1的地址变得一样了?最后导致同一个地址的内存空间删除两次?于是查看各自的内存地址,发现并不一样,也就是说实例化后每个实例的内存地址是不一样的 。
没办法,只好查看构造函数,拷贝构造函数和析构函数的运行情况(在每个函数下输出其函数名),问题出现了,构造函数正常运行,拷贝构造函数没有运行,析构函数只运行了一次(应该是两次str1和str2)。导致这个结果的原因应该是:str2在实例化的时候没有参数传入,并没有通过构造函数为其在堆中分配资源,CMyString str2; 而str1在实例化的时候传入了参数text,在堆中分配了资源,所以执行析构函数的时候,str1有空间释放,而str2无空间释放,此时强行释放会导致计算机不知道释放哪块内存,最终程序就一直
总结一下:这次的问主要涉及到拷贝构造函数与赋值运算符重载的区别,内存空间的管理,以及再出现类似bug后调试的方法。