首先我有三个文件,
mystring.cpp ,用于实现功能。
#include <iostream>
#include "mystring.hpp"
// Default constructor
MyString::MyString()
{
data = 0;
length = 0;
}
MyString::MyString(int n)
{
data = new char [n];
length = n;
}
MyString::MyString(const char* str, int n)
{
length = n;
data = new char [n];
for (int i = 0; i < n; i++)
{
data[i] = str[i];
}
}
void MyString::trim(int n)
{
if(n < length)
{
int newlength = n;
char* newdata = new char [newlength];
for (int i = 0; i < newlength; i++)
{
newdata[i] = data[i];
}
delete[] data;
data = newdata;
length = newlength;
}
}
MyString::~MyString()
{
delete[] data;
}
void MyString::append(const MyString& rhs)
{
// Determine the length of the resulting
// string and allocate room for it.
int newlength = length + rhs.length;
char* newdata = new char [newlength];
// Copy the current string's data
for (int i = 0; i < length; i++)
{
newdata[i] = data[i];
}
// Copy the given string's data
for (int i = 0; i < rhs.length; i++)
{
newdata[i + length] = rhs.data[i];
}
// Now we must deallocate the original memory
// and update the member variables
delete[] data;
data = newdata;
length = newlength;
}
void MyString::print(char separator) const
{
for (int i = 0; i < length - 1; i++)
{
std::cout << data[i] << separator;
}
std::cout << data[length - 1] << std::endl;
}
第二个头文件是 mystring.hpp
#ifndef __mystring_hpp__
#define __mystring_hpp__
class MyString
{
public:
// Constructors
MyString();
MyString(int n);
MyString(const char* str, int n);
~MyString();
// Modify the current string by appending "rhs" to it
void append(const MyString& rhs);
// Trim the string such that it contains "n" characters.
// If "n" is larger than the current string's length,
// then do nothing.
void trim(int n);
// Prints this string by putting the separator
// character between each element in the data.
// By default, do not print anything.
void print(char separator = '\0') const;
// This is the destructor. It is automatically
// called when an object of this class is destroyed.
//~MyString(); //Implement!
// This is the assignment operator which is automatically
// called when an object of type MyString is assigned
// to another object of the same type. Technically, it
// does not have to return "MyString&" but we do so to
// allow chaining assignments such as: str1 = str2 = str3
//MyString& operator=(const MyString& rhs); //Implement!
// Copy constructor. Different from the assignment operator,
// this is called when an object is "being created" as a
// copy of another object.
//MyString(const MyString& rhs); //Implement!
private:
char* data;
int length;
};
#endif // __mystring_v1_hpp__
最后是main_assignment.cpp,其中包括main.cpp
#include "mystring.hpp"
int main()
{
MyString str1("ali", 3);
MyString str2("veli", 4);
str1 = str2;
return 0;
}
这里的问题是,我没有为MyString类重载赋值运算符,因此在* main_assignment.cpp *中,两个字符串应该指向相同的内存,并且当程序返回时,它应该尝试先释放两个中的一个但是当涉及到另一个时,它将尝试释放分配一些“未定义”内存的指针。
从我的角度来看,它应该崩溃但程序可以完美运行,为什么?
答案 0 :(得分:4)
当您delete
两次指针时,程序的行为为undefined。这意味着该标准未指定在这种情况下应发生的情况-特别是它未指定该程序应崩溃。它可能-或可能表现出预期的行为-或可能会继续运行,但行为方式异常,似乎与调用未定义行为的代码部分无关。
答案 1 :(得分:-2)
如@TypeIA所述,double free的行为在C ++规范中为未定义。
事实上,它has been implemented是GCC 1.17中的复活节彩蛋。但是,请注意,此操作仅针对“特定情况”。编译器需要非常非常确定在启动某些游戏之前会调用未定义的行为。
在大多数情况下,编译器不能确定是否启动复活节彩蛋。例如。双重释放通常不会像简化情况那样以一种方法实现。它们发生在从磁盘读取错误的文件后,或者用户以错误的顺序按下按钮等。这些信息在编译时根本不可用。
因此,任何普通的编译器都可能会编译与您在代码中编写的内容相对应的内容。 (但是,优化器可能会介入您的工作,并删除未使用的变量等。编写演示代码时有时需要考虑这一点。)
如果不是这种情况,您可能会担心程序中的任何错误都会向您的雇主发送电子邮件以辞职,并在Reddit上发布所有照片,然后格式化硬盘。
好吧,这是我对未定义行为的看法。也许不是很受欢迎,我也许值得赞扬一下。但是,I have debugged a lot和我在WinDbg的10年经验中,我会说事情通常是可以预测和调试的。
回到您原来的问题...
您在问题的标题中写道
取消分配已释放的对象不会产生编译器错误
如果您之前阅读过我的声明,那么合乎逻辑的是这将不是编译器错误,而是运行时错误。
我认为您的意思是运行时错误,因为您还说过:
从我的角度来看,它应该崩溃但程序可以完美运行
您当然没想到编译器会崩溃。
我已将您的代码复制/粘贴到Visual Studio 2017 C ++项目中。我在调试版本中运行了x86版本,并在Windows 7 SP1 x64上发布了版本-两次都崩溃了。
调试版本(在调试器中运行):
发布版本(不在调试器中运行)
在WinDbg中,您可以看到该类的析构函数调用free()
和free()
调用HeapFree()
和HeapFree()
检测到double free,并生成了{{1 }}例外。
0xc0000374
所以恕我直言,您的假设“它应该崩溃”是一个很好的假设-我也会假设它。并没有让我失望。
如何进行...
下次,请使用您的语言,以确保人们开始信任您。如果您混合使用编译时间和运行时,或者混合使用堆栈和堆,或者翻转物理RAM和虚拟内存,那不是一个好的起点。
要获得此类情况的帮助,您需要提出更好的问题。不要问C ++标准怎么说。在大多数情况下,它将是“未定义的”。
相反,发布确切的环境,可能比我更精确。给我们编译器版本号和编译器命令行选项。创建一个最小,完整,可行的示例供我们复制。显示您的调试技巧以及您的发展水平。
然后,有人可能会来给你一个答案,为什么它不会在您的情况下崩溃,例如:
0:000> *** Release Build, debugged when crashed
0:000> k
# ChildEBP RetAddr
00 0036f9d0 775bf8a9 ntdll!RtlReportCriticalFailure+0x57
01 0036f9e0 775bf989 ntdll!RtlpReportHeapFailure+0x21
02 0036fa14 7756d95c ntdll!RtlpLogHeapFailure+0xa1
03 0036fa44 0f64fddb ntdll!RtlFreeHeap+0x64
04 0036fa58 0f64fda8 ucrtbase!_free_base+0x1b
05 0036fa68 0137106d ucrtbase!free+0x18
06 (Inline) -------- DeallocateTwice!MyString::{dtor}+0x6 [c:\users\for example john\documents\visual studio 2017\projects\deallocatetwice\mystring.cpp @ 49]
07 0036faa0 01371277 DeallocateTwice!main+0x6d [c:\users\for example john\documents\visual studio 2017\projects\deallocatetwice\deallocatetwice.cpp @ 11]
08 (Inline) -------- DeallocateTwice!invoke_main+0x1c [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78]
09 0036fae8 74f0343d DeallocateTwice!__scrt_common_main_seh+0xfa [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
0a 0036faf4 77529802 kernel32!BaseThreadInitThunk+0xe
0b 0036fb34 775297d5 ntdll!__RtlUserThreadStart+0x70
0c 0036fb4c 00000000 ntdll!_RtlUserThreadStart+0x1b
)。