C ++, освобождение уже освобождено не дает ошибку компилятора - PullRequest
0 голосов
/ 08 ноября 2018

Во-первых, у меня есть три файла,

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 * две строки должны указывать на одну и ту же память, а когда программа возвращается, она должна попытаться освободить одну из двух но когда дело доходит до другого, он попытается освободить указатель, который указывает на некоторую «неопределенную» память.

С моей точки зрения, должно произойти сбой, но программа работает безупречно, почему?

Ответы [ 2 ]

0 голосов
/ 08 ноября 2018

Как уже упоминалось @TypeIA, поведение двойного освобождения равно undefined в спецификации C ++.

И, фактически, это было реализовано как пасхальное яйцо в GCC 1.17. Однако обратите внимание, что это было сделано только для «особых случаев». Компилятор должен быть очень и очень уверен, что перед запуском некоторых игр вызывается неопределенное поведение.

В большинстве случаев компилятор не может быть уверен, что запускает пасхальное яйцо. Например. двойные освобождения обычно не реализуются ни в одном методе, как в упрощенном случае. Они случаются, например, после чтения неисправного файла с диска, или если пользователь нажимает кнопки в неправильном порядке и т. д. Эта информация просто недоступна во время компиляции.

Итак, любой нормальный компилятор, вероятно, скомпилирует что-то, что соответствует тому, что вы написали в своем коде. (Однако оптимизатор может помешать вам и удалить неиспользуемые переменные и т. Д. Это иногда необходимо учитывать при написании демонстрационного кода.)

Если бы это было не так, вам нужно было бы жить со страхом, что любая ошибка в вашей программе отправляет электронное письмо вашему работодателю, бросающему вашу работу, публикует все ваши фотографии в Reddit, а затем форматирует ваш жесткий диск.

Хорошо, это мое мнение о неопределенном поведении. Возможно, не очень популярный, и я могу заслужить некоторые отрицательные отзывы за сказанное выше. Тем не менее, Я много отладил , и из моего 10-летнего опыта работы с WinDbg я бы сказал, что в целом все достаточно предсказуемо и отлаживаемо.


Вернуться к исходной проблеме ...

В заголовке вопроса вы написали

освобождение уже освобождено не дает ошибку компилятора

Если вы читали мое заявление раньше, надеюсь, логично, что это будет не ошибка компилятора, а ошибка времени выполнения.

Полагаю, вы имели в виду ошибку времени выполнения, потому что вы также сказали:

С моей точки зрения, должно произойти сбой, но программа работает безупречно

И вы, конечно же, не ожидали сбоя компилятора.

Я скопировал / вставил ваш код в проект Visual Studio 2017 C ++. Я запускал версию x86 в отладочной сборке и сборку выпуска на Windows 7 с пакетом обновления 1 x64 - и он падал оба раза.

Отладочная сборка (запускается в отладчике):

Double free in debug build

Выпуск сборки (не запускается в отладчике):

Double free in release build

В 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

Так что ИМХО, ваше предположение "оно должно рухнуть" было хорошим предположением - я бы тоже это предположил. И это меня не разочаровало.


Как продолжить ...

В следующий раз будьте точны на своем языке, чтобы люди начали доверять вам. Если вы смешиваете время компиляции и время выполнения, или смешиваете стек и кучу, или переворачиваете физическую ОЗУ и виртуальную память, это не очень хорошая отправная точка.

Чтобы получить помощь в таких случаях, вам нужно задать лучшие вопросы. Не спрашивайте, что говорит стандарт C ++. В большинстве случаев это будет «неопределено».

Вместо этого опубликуйте точное окружение, потенциально даже более точное, чем я. Дайте нам номера версий компилятора и параметры командной строки компилятора. Создайте минимальный, полный, жизнеспособный пример для воспроизведения. Покажите свои навыки отладки и как далеко вы зашли.

Тогда кто-то может прийти и дать вам ответ, почему в вашем случае это не сработало, например:

  • вы проигнорировали исключение в отладчике (например, sxi в WinDbg).
  • вы активировали автоматический сбор аварийных дампов. Программа действительно потерпела крах, но вы этого не заметили.
  • вы отключили Windows Error Reporting.
  • вы подключили отладчик к неправильному процессу.
  • ...
0 голосов
/ 08 ноября 2018

Поведение программы при delete указателе дважды равно не определено . Это означает, что стандарт не определяет, что должно произойти в этом случае - в частности, он не определяет, что программа должна аварийно завершить работу. Это могло бы - или это могло бы вести себя как ожидалось - или это могло бы продолжаться, но вести себя неожиданными способами, которые, кажется, не имеют никакого отношения к части кода, где было вызвано неопределенное поведение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...