C++,释放已经释放不会产生编译器错误

Posted

技术标签:

【中文标题】C++,释放已经释放不会产生编译器错误【英文标题】:C++,deallocating already deallocated does not give a compiler error 【发布时间】:2018-11-07 22:21:43 【问题描述】:

首先我有三个文件,

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.cpp 的 main_assignment.cpp

#include "mystring.hpp"

int main()

    MyString str1("ali", 3);
    MyString str2("veli", 4);

    str1 = str2;


    return 0;

这里的问题是,我没有为 MyString 类重载赋值运算符,所以在 *main_assignment.cpp * 中,两个字符串应该指向同一个内存,当程序返回时,它应该首先尝试释放两个字符串中的一个但是当涉及到另一个时,它会尝试释放一个指向一些“未定义”内存的指针。

在我看来,它应该崩溃但程序运行完美,为什么?

【问题讨论】:

因为它被命名为undefined行为是有原因的。 __mystring_hpp__ -- 不要使用前导双下划线作为标识符。带有前导双下划线的标识符保留给编译器实现。 离题,但有一些建议——您的trim 函数只需要更改length。真的不需要重新分配和复制相同的数据——让length控制字符串实际具有的字符数。 @user9679818:您要为哪个操作系统、哪个编译器、哪个构建模式和哪个平台编译?我复制/粘贴了您在 VS2017 中编译的代码,它在调试和发布版本 x86 中都崩溃了。 @trim 感谢您的建议 :) 【参考方案1】:

当你delete 一个指针两次时程序的行为是undefined。这意味着标准没有指定在这种情况下应该发生什么 - 特别是它没有指定程序应该崩溃。它可能 - 或者它的行为符合预期 - 或者它可能继续但以意想不到的方式运行,似乎与调用未定义行为的代码部分无关。

【讨论】:

【参考方案2】:

正如@TypeIA 已经提到的,双重释放的行为在 C++ 规范中是未定义的

事实上,has been implemented 在 GCC 1.17 中就像一个复活节彩蛋。但是,请注意,这仅适用于“特定情况”。编译器需要非常确定在启动某些游戏之前调用了未定义的行为。

在大多数情况下,编译器无法确定启动复活节彩蛋。例如。双重释放通常不会像您的简化案例那样以一种方法实现。它们发生例如从磁盘读取错误文件后,或者用户按错误顺序按下按钮等。这些信息在编译时根本不可用。

因此,任何普通的编译器都可能编译出与您在代码中编写的内容相对应的内容。 (但是,优化器可能会妨碍您并删除未使用的变量等。在编写演示代码时有时需要考虑这一点。)

如果不是这样,您将不得不担心程序中的任何错误都会向您的雇主发送一封电子邮件,辞去您的工作,将您的所有照片发布到 Reddit 上,然后格式化您的硬盘。

好的,这就是我对未定义行为的看法。也许不是很受欢迎,我可能应该因为上述事情而受到一些反对。但是,I have debugged a lot 以及我在 WinDbg 中 10 年的经验,我会说事情通常是相当可预测和可调试的。


回到你原来的问题...

在问题的标题中,你写了

解除分配已经解除分配不会产生编译器错误

如果您之前阅读过我的声明,希望这不是编译器错误而是运行时错误是合乎逻辑的。

我假设您的意思是运行时错误,因为您还说过:

在我看来,它应该会崩溃,但程序可以完美运行

而且你当然没想到编译器会崩溃。

我已将您的代码复制/粘贴到 Visual Studio 2017 C++ 项目中。我在 Windows 7 SP1 x64 的调试版本和发布版本中运行 x86 版本 - 两次都崩溃了。

调试构建(在调试器中运行):

发布构建(不在调试器中运行):

在WinDbg中可以看到调用free()free()的类的析构函数调用HeapFree()HeapFree()检测到双重释放,并产生0xc0000374异常。

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

所以恕我直言,您的假设“它应该崩溃”是一个很好的假设 - 我也会假设它。它并没有让我失望。


如何进行...

下一次,你的语言要准确,这样人们就会开始信任你。如果您混合编译时间和运行时,或者混合堆栈和堆,或者翻转物理 RAM 和虚拟内存,这不是一个好的起点。

要获得此类案例的帮助,您需要提出更好的问题。不要问 C++ 标准是怎么说的。在大多数情况下,它将是“未定义的”。

相反,发布确切的环境,可能比我做的更准确。给我们编译器版本号和编译器命令行选项。创建一个最小的、完整的、可行的示例供我们复制。展示您的调试技能以及您取得的成就。

然后,有人可能会来回答你为什么它没有在你的情况下崩溃,比如:

您忽略了调试器中的异常(如 WinDbg 中的 sxi)。 您已激活自动故障转储收集。程序实际上崩溃了,但你没有注意到。 您已禁用 Windows 错误报告。 您将调试器附加到了错误的进程。 ...

【讨论】:

以上是关于C++,释放已经释放不会产生编译器错误的主要内容,如果未能解决你的问题,请参考以下文章

确保释放向量的 C++ 技巧,但 GNU g++ 的编译问题

在 C 或 C++ 中释放内存 [重复]

用delphi开发应用程序会不会有内存泄露的问题?

C++核心编程

c++存储区

C++简易