使用memmove/memcpy库函数拷贝内存时容易引发的异常
-
首先,我们来看一下C库函数memmove的原型,如下:
void memmove( void dest, const void* src, size_t n);
头文件:<string.h>
功能:由src所指内存区域复制n个字节到dest所指内存区域。
返回值:函数返回指向dest的指针。 -
其次,C库函数memcpy的原型如下:
void *memcpy(void *dest, const void *src, size_t n);
头文件:<string.h>
功能:由src所指内存区域复制n个字节到dest所指内存区域。
返回值:函数返回指向dest的指针。 -
然后,简单描述一下上述两个库函数的区别:
当内存发生局部重叠的时候,memmove保证拷贝的结果是正确的,memcpy不保证拷贝的结果的正确。
<1> 内存不重叠情况:
<2> 内存重叠的情况之一:
<3> 内存重叠的情况之二:
<4> 对于内存重叠两种情况,memmove和memcpy拷贝内存情况如下:
红色字体表示原源地址内存存储的数据内容,绿色字体表示原目的地址内存存储的数据内容,紫色字体代表用memmove函数内存拷贝后目的地址存储的正确结果,蓝色字体表示用memcpy函数内存拷贝后目的地址存储的有可能错误的结果。 -
言归正传,下面我们来探讨一下使用memmove/memcpy库函数拷贝内存时容易引发的异常
<1> 首先,先看下面的一段简单的代码:
#include <iostream>
#include <string.h>
class Base {
public:
int base_id = 0;
std::string base_name;
float * depths = nullptr;
Base() {}
virtual ~Base() {
delete[] depths;
depths = nullptr;
}
int GetBaseId() { return base_id; }
std::string GetBaseName() { return base_name; }
float * GetDepths() { return depths; }
virtual void ChangeBaseName() { base_name = "base name"; }
};
class BaseInfo : public Base {
public:
int baseinfo_id = 0;
std::string baseinfo_name;
Base() {}
~Base() {}
};
int main() {
BaseInfo * binfo1 = new BaseInfo[2];
binfo1[0].baseinfo_id = 1;
binfo1[0].baseinfo_name = "baseinfo 1";
binfo1[0].depths = new float[3] {0.4f, 0.7f, 0.3f};
binfo1[1].baseinfo_id = 2;
binfo1[1].baseinfo_name = "baseinfo 2";
binfo1[1].depths = new float[2] {0.9f, 0.7f};
BaseInfo * binfo2 = new BaseInfo[2];
memmove(binfo2, binfo1, 2 * sizeof(class BaseInfo));
delete[] binfo1;
binfo1 = nullptr;
delete[] binfo2;
binfo2 = nullptr;
return 0;
}
<2> 调试上面的这段代码,当程序运行到“delete[] binfo2;”这一行时,程序中断发生错误,抛出异常如下:
<3> 那么是什么原因引发了异常呢?
1" memmove函数是用来拷贝内存的,我首先想到的是【src源地址内存和dest目的地址的内存有重叠,导致重复释放同一块内存区域产生异常】,经过添加代码,验证两块内存并没有重叠部分,遂排除此种可能性。
2" 其次我想到的是【BaseInfo里的成员变量包含指针,两个BaseInfo对象的指针成员指向同一块内存区域,析构对象时导致重复释放同一块内存区域产生异常】。
回到代码中可以看出BaseInfo从基类Base继承了一个指针变量“depths”,看下图:depths(3)/depth(4)是通过拷贝了depths(1)/depths(2)的内存得来的,因此depths(1)的值等于depths(3)的值,即这两个数组指针指向内存中同一块内存区域(同理,depths(2)的值等于depths(4)的值。所以当我们delete[] baseinfo1时调用析构函数时第一次释放了depths(1)所指向的内存,再delete[] baseinfo2时调用析构函数时会第二次去释放depths(3)所指向的内存,重复释放内存导致异常。
3" 将成员depths注释掉以及其相关的接口改掉再编译调试代码发现还是会有同样的异常,我想到的是【会不会是因为派生类继承了基类的virtual虚函数,派生类的第一个成员是虚表,派生类对象的成员包含虚指针,在释放虚指针的时候导致重复释放而造成异常呢?】,后面经过验证发现类里面包含虚函数不会引发这个异常,稍后第5点讨论。
4" 引发异常的另一个原因有时候很难想到,原因是【BaseInfo的成员中包含std::string类型的变量】。我们下面来看一下string的原型,它用来表示字符串,但实质上它是一个类。string中含有一个m_data的指针变量,所以也会导致重复释放同一块内存的异常,原理同2"相似。
class String
{
public:
String(const char *str = NULL); // 普通构造
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operate =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
5" 最后关于【虚指针的释放】:
一个存在虚函数的类会有一个虚表,这个类的对象都包含一个成员即虚指针,这些虚指针都指向这个类的虚表,即同类的对象共享这个类的虚表。虚表相当于一个一维数组,它里面按顺序存放这个类里每个虚函数实现的地址。在构造函数中进行虚表的创建以及虚指针的初始化,而虚指针的释放我也没太弄清楚,不知道它是在什么时候如何释放的。
我的猜想是:
在构造A类的第一个对象时构造了这个类的虚表,并将这个对象的虚指针初始化为这个虚表,之后再构造A类对象时将它们的虚指针都初始化为此虚表,所有A类对象共享A类的虚表;在析构A类的对象时只是将虚指针置空,直到析构最后一个A类的对象时才将它的虚指针指向的虚表从内存中释放掉,所以上述的程序才没有导致重复释放内存的异常。(貌似有点道理,好像又有点牵强,哈哈)
我是个小神女,快来关注我吧~