4
Подписаться
1271
Подписчики

Вопросы по написанию стратегии на C++: Переработка памяти на C++

Создано: 2017-12-29 11:00:02, Обновлено: 2017-12-29 11:25:11
comments   0
hits   3111

Вопросы по написанию стратегии на C++: Переработка памяти на C++

Прежде чем писать стратегию на C++, нужно знать некоторые основы, не требуя знания в конфуцианском стиле, но, как минимум, знать эти правила. Ниже приведены ссылки.

  • #### C++ Memory Object Conflict (Конфликты по объектам памяти в C++)

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

Мы знаем, что C++ делит память на три логических области: куча, куча и статическая область хранения. Так как это так, то я называю объекты, расположенные между ними, кучами, кучами и статическими объектами. Что же отличает эти различные объекты памяти? Какие преимущества и недостатки есть у кучи и кучи? Как запретить создание кучи или кучи?

  • 1 Основные понятия

    Вначале рассмотрим . , обычно используется для хранения локальных переменных или объектов, таких как объекты, которые мы объявляем в определении функции подобными следующим утверждениям:

    Type stack_object ; 
    

    stack_object - это кусочный объект, жизнь которого начинается с точки определения и заканчивается, когда возвращается функция, в которой он находится.

    Кроме того, практически все временные объекты - это объекты паутины. Например, следующее определение функции:

    Type fun(Type object);
    

    Эта функция производит по меньшей мере два временных объекта, во-первых, аргументы передаются по значениям, поэтому будет вызвана функция копирования, чтобы создать временный объект object_copy1, который используется внутри функции, а не object, а object_copy1, естественно, object_copy1 - это кусочек объекта, который освобождается при возврате функции; также эта функция является возвращаемой величиной, и при возврате функции, если мы не учитываем оптимизацию возвращаемой величины ((NRV), то также будет создан временный объект object_copy2, который будет освобожден в течение некоторого времени после возвращения функции. Например, функция имеет следующий код:

    Type tt ,result ; //生成两个栈对象
    
    tt = fun(tt); //函数返回时,生成的是一个临时对象object_copy2
    

    Второй вышеприведенный вывод выполняется следующим образом: сначала функция fun возвращает временный объект object_copy2, а затем вызывает оператор присвоения

    tt = object_copy2 ; //调用赋值运算符
    

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

    Далее, посмотрите на стек. стек, также называемый свободной зоной хранения, он динамически распределяется в процессе выполнения программы, поэтому его самая большая особенность - динамичность. В C ++ все объекты стека создаются и уничтожаются программистом, поэтому, если плохо обрабатывать, возникают проблемы с памятью. Если вы распределяете объекты стека, но забываете освободить их, возникает утечка памяти; а если вы освобождаете объекты, но не устанавливаете соответствующую стрелку на NULL, эта стрелка называется подвесной стрелкой, и повторное использование этой стрелки приводит к незаконному доступу, что при серьезных случаях приводит к краху программы.

    Так как же распределять объекты стека в C++? Единственный способ это использовать new ((конечно, инструкция типа malloc также может быть использована для получения памяти стека в C-образе), когда используется new, то в стеке распределяется блок памяти и возвращается указатель, указывающий на объект стека.

    Возвращаясь к статическому хранилищу, все статические и глобальные объекты распределены в статическом хранилище. Что касается глобальных объектов, то они распределены до выполнения функции main (). Фактически, перед выполнением кода отображения в функции main (), будет вызвана функция _main (), созданная компилятором, а функция _main (), которая выполняет конструирование и инициализацию всех глобальных объектов, а перед окончанием функции main (), будет вызвана функция exit, созданная компилятором, чтобы освободить все глобальные объекты.

    void main(void)
    {
        ... // 显式代码
    }
    
    
    // 实际上转化为这样:
    
    
    void main(void)
    {
        _main(); //隐式代码,由编译器产生,用以构造所有全局对象
        ...      // 显式代码
        ...
        exit() ; // 隐式代码,由编译器产生,用以释放所有全局对象
    }
    

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

    Есть также статический объект, который является статическим членом класса. При рассмотрении этого вопроса возникают некоторые более сложные проблемы.

    Первая проблема - жизненный цикл статических членов класса, которые возникают с появлением первого объекта класса и исчезают в конце всей программы. То есть, существует ситуация, когда в программе мы определяем класс, в котором есть статический объект в качестве члена, но в процессе выполнения программы, если мы не создадим ни одного объекта этого класса, то не будет создано статического объекта, содержащегося в этом классе.

    Второй вопрос, который возникает, когда:

    class Base
    {
    public:
        static Type s_object ;
    }
    
    
    class Derived1 : public Base / / 公共继承
    {
    
    
        ... // other data 
    
    
    }
    
    
    class Derived2 : public Base / / 公共继承
    {
    
    
        ... // other data 
    
    
    }
    
    
    Base example ;
    
    
    Derivde1 example1 ;
    
    
    Derivde2 example2 ;
    
    
    example.s_object = …… ;
    
    
    example1.s_object = …… ; 
    
    
    example2.s_object = …… ; 
    

    Обратите внимание на три вышеприведенных высказывания, обозначенных как черные тела, и находятся ли они в одном и том же объекте s_object? Ответ - да, они действительно находятся в одном и том же объекте, и это звучит не так, верно? Но это так, вы можете написать простые строки кода и проверить это сами.

    Давайте подумаем, что происходит, когда мы передаем объект типа Derived1 функции, принимающей параметры типа Base без ссылки. Как же происходит вырезание? Полагаю, что теперь вы знаете, что это просто извлечение субобъекта из объекта типа Derived1, игнорируя все остальные данные, заданные Derived1, и передача этого субобъекта функции (в действительности, функция использует копию этого субобъекта).

    Все объекты производящих классов, которые наследуют тип Base, содержат субобъект типа Base (который является ключевым, к которому можно указать на объект Derived1 с помощью указателя типа Base, который, естественно, является многообразным ключевым), а все субобъекты и все объекты типа Base используют один и тот же s_object, естественно, все экземпляры классов во всей системе наследования, производящихся от типа Base, используют один и тот же s_object.

  • 2 Сравнение трех видов объектов памяти

    Преимущество кристалловых объектов в том, что они создаются и уничтожаются автоматически в нужное время, без заботы программиста; кроме того, кристалловые объекты создаются быстрее, чем нагромождаемые объекты, поскольку при распределении нагромождаемых объектов вызывается оператор new, который использует алгоритм поиска в пространстве памяти, а этот поиск может быть трудоемким, а создание кристалловых объектов не так уж сложно, он просто требует перемещения кристалловой стрелки. Однако следует отметить, что обычно кристалловые объекты имеют небольшую емкость, обычно 1 МБ к 2 МБ, поэтому большие объекты не подходят для распределения в кристаллах.

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

    Теперь посмотрим на статические объекты.

    В первую очередь это глобальные объекты. Глобальные объекты обеспечивают самый простой способ для межклассного и межфункционального общения, хотя и не очень элегантный. В целом, в полностью объектно-ориентированных языках, таких как C#, глобальные объекты отсутствуют, потому что они означают небезопасность и высокую сплоченность, а чрезмерное использование глобальных объектов в программе значительно снижает прочность, стабильность, поддерживаемость и многофункциональность программы.

    Второй - статический член класса. Как уже было сказано выше, все объекты базового класса и его производных классов имеют общий объект статического члена, поэтому такие статические члены, несомненно, являются хорошим выбором, когда требуется обмен данными или общение между этими классами или между этими объектами класса.

    Далее следует статический локальный объект, используемый в основном для сохранения промежуточного состояния, в котором он находится во время неоднократного вызова функции. Одним из наиболее ярких примеров является регрессивная функция, которую мы знаем как функцию, которая сама себя вызывает. Если в регрессивной функции определяется нестатический локальный объект, то при значительном количестве повторных вызовов возникают огромные издержки.

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

  • 3 Непредвиденные урожаи от использования клея

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

    Если мы запечатаем ресурс в запечатанном объекте и выполняем действия по освобождению ресурса в его запечатанном функции, то вероятность утечки ресурсов значительно снижается, потому что запечатанный объект может автоматически освободить ресурс, даже если произойдет аномалия в функции, в которой он находится. Фактический процесс таков: когда функция выбросит аномалию, произойдет то, что называется stack_unwinding (переворачивание стека), то есть стек разворачивается, и, поскольку запечатанный объект существует в естественном заклинании, в процессе переворачивания стека функция запечатанного объекта выполняется, освобождая его от запечатанного ресурса. Если только в процессе выполнения функции запечатывания эта аномалия не повторится снова, то это очень маловероятно, поэтому мы можем создать собственный запечатанный ресурс на основе запечатанного объекта.

  • 4 Запрет на создание кучи объектов

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

    Как же тогда запретить создание кучи объектов? Мы уже знаем, что единственный способ создания кучи объектов - это использование new-операции, если мы запретим использование new-операции. Далее, new-операция при выполнении вызывает оператор new, а оператор new может быть перезагружен.

    #include <stdlib.h> //需要用到C式内存分配函数
    
    
    class Resource ; //代表需要被封装的资源类
    
    
    class NoHashObject
    {
    private: 
        Resource* ptr ;//指向被封装的资源
    
    
        ... ... //其它数据成员
    
    
        void* operator new(size_t size) //非严格实现,仅作示意之用
        { 
            return malloc(size) ; 
        } 
    
    
        void operator delete(void* pp) //非严格实现,仅作示意之用
        { 
            free(pp) ; 
        } 
    
    
    public: 
        NoHashObject() 
        { 
            //此处可以获得需要封装的资源,并让ptr指针指向该资源
    
    
            ptr = new Resource() ; 
        } 
    
    
        ~NoHashObject() 
        { 
    
    
            delete ptr ; //释放封装的资源
        } 
    }; 
    

    Теперь NoHashObject - это класс, который запрещает объекты стека, если вы напишете следующий код:

    NoHashObject* fp = new NoHashObject (() ; // ошибка компиляции!

    delete fp ;

    Вышеприведенный код может вызвать ошибку компиляции. Теперь, когда вы знаете, как создать класс, запрещающий объекты стека, у вас, возможно, возникли такие же вопросы, как и у меня: неужели нельзя создать такой тип стека, если не изменить определение класса NoHashObject? Нет, есть способ, который я называю “насильственным взломом”.

    void main(void)
    {
        char* temp = new char[sizeof(NoHashObject)] ; 
    
    
        //强制类型转换,现在ptr是一个指向NoHashObject对象的指针
    
    
        NoHashObject* obj_ptr = (NoHashObject*)temp ; 
    
    
        temp = NULL ; //防止通过temp指针修改NoHashObject对象
    
    
        //再一次强制类型转换,让rp指针指向堆中NoHashObject对象的ptr成员
    
    
        Resource* rp = (Resource*)obj_ptr ; 
    
    
        //初始化obj_ptr指向的NoHashObject对象的ptr成员
    
    
        rp = new Resource() ; 
    
    
        //现在可以通过使用obj_ptr指针使用堆中的NoHashObject对象成员了
    
    
        ... ... 
    
    
        delete rp ;//释放资源
    
    
        temp = (char*)obj_ptr ; 
    
    
        obj_ptr = NULL ;//防止悬挂指针产生
    
    
        delete [] temp ;//释放NoHashObject对象所占的堆空间。
    
    
        } 
    

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

    Данные в блоке памяти неизменны, а тип - это очки, которые мы носим. Когда мы надеваем очки, мы интерпретируем данные в памяти с помощью соответствующих типов, так что разные интерпретации дают разную информацию.

    Так называемое принудительное преобразование типа - это, по сути, переодевание в новые очки и повторное просмотр того же фрагмента памяти.

    Следует также помнить, что разные компиляторы могут расположить данные о членах объекта по-разному, например, большинство компиляторов размещают членов указателя ptr NoHashObject в первых 4 байтах пространства объекта, чтобы гарантировать выполнение следующих действий по преобразованию следующего предложения так, как мы ожидаем:

    Resource* rp = (Resource*)obj_ptr ;

    Но не все компиляторы такие.

    Если мы можем запретить создание определенного типа кучи объектов, то можем ли мы создать класс, который не может создавать кучи объектов? Конечно, можно.

  • 5 Запрет на создание куриных объектов

    Как уже упоминалось ранее, при создании объекта ячейки перемещают верхнюю стрелку, чтобы вытащить из ячейки пространство соответствующего размера, а затем вызывают соответствующую конструкторскую функцию, чтобы сформировать объект ячейки, а когда функция возвращается, она вызовет свою дифференцирующую функцию, чтобы освободить этот объект, а затем отрегулирует верхнюю стрелку, чтобы вернуть этот блок ячейки. В этом процессе не требуется оператор new/delete, поэтому настройка оператора new/delete как частная не может достичь цели. Конечно, из приведенного выше вы, возможно, уже подумали: сделать конфигурацию функции компоновки или функции компоновки частной, чтобы система не могла вызвать функцию компоновки / дифференцировки, и, конечно, не могла создать объект в ячейке.

    Это действительно возможно, и я также планирую использовать эту схему. Но до этого, есть кое-что, что нужно учесть, то есть, если мы установим конструкторскую функцию как частную, то мы не сможем использовать new для непосредственного создания кучи объектов, потому что new будет также вызывать свою конструкторскую функцию после распределения пространства для объектов. Так что я собираюсь только настроить дизайнерскую функцию как частную.

    Если класс не предназначен для использования в качестве базового класса, обычно используется вариант, в котором дифференцированная функция объявляется частной.

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

    class NoStackObject
    {
    protected: 
    
    
        ~NoStackObject() { } 
    
    
    public: 
    
    
        void destroy() 
    
    
        { 
    
    
            delete this ;//调用保护析构函数
    
    
        } 
    }; 
    

    Затем можно использовать класс NoStackObject, который выглядит так:

    NoStackObject* hash_ptr = new NoStackObject() ;

    … … // обрабатывает объект, направленный на hash_ptr

    hash_ptr->destroy() ; Ну, может быть, это немного странно, что мы создаем объект с помощью new, но вместо того, чтобы удалить его с помощью delete, мы используем метод destroy. Очевидно, что пользователи не привыкли к этому странному использованию. Поэтому я решил, что конструкторская функция также должна быть закрыта или защищена.

    class NoStackObject
    {
    protected: 
    
    
        NoStackObject() { } 
    
    
        ~NoStackObject() { } 
    
    
    public: 
    
    
        static NoStackObject* creatInstance() 
        { 
            return new NoStackObject() ;//调用保护的构造函数
        } 
    
    
        void destroy() 
        { 
    
    
            delete this ;//调用保护的析构函数
    
    
        } 
    };
    

    Теперь можно использовать класс NoStackObject следующим образом:

    NoStackObject* hash_ptr = NoStackObject::creatInstance() ;

    … … // обрабатывает объект, направленный на hash_ptr

    hash_ptr->destroy() ;

    hash_ptr = NULL ; // предотвращает использование подвесных указателей

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

  • Необходимый метод утилизации мусора в C++

Многие программисты C или C++ возмущаются по поводу утилизации мусора, считая, что утилизация мусора, безусловно, будет менее эффективной, чем управление динамической памятью, и что во время утилизации программа обязательно остановится там, а если контролировать управление памятью, то время распределения и высвобождения будет стабильным и не приведет к остановке программы. И, наконец, многие программисты C/C++ твердо убеждены, что в C/C++ невозможно реализовать механизм утилизации мусора. Эти ошибочные взгляды были выдуманы из-за незнания алгоритмов утилизации мусора.

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

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

При утилизации мусора достаточно сканировать bss, data и используемое в данный момент пространство, чтобы найти количество указателей динамической памяти. Рекурсивное сканирование ссылки на память позволяет получить все динамические памяти, используемые в данный момент.

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

Смотреть онлайнHK Zhang

  • #### Почему, когда указателю присваивается адрес местной переменной, его жизненный цикл может быть продлен до конца всей программы?
  #include<stdio.h>
  int*fun(){
      int k = 12;
      return &k;
  }
  int main(){
      int *p = fun();    
      printf("%d\n", *p);

      getchar();
      return 0;
  }

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

csdn bbs