Перед тем как написать стратегию 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 (конечно, также можно получить малок), который распределяет блок памяти в стеке и возвращает указатель к объекту.
Возвращаясь к статическому хранилищу; все статические объекты, все глобальные объекты распределены в статическом хранилище. Что касается глобальных объектов, то они распределены до выполнения функции main. Фактически, перед выполнением кода для отображения в функции main, выполняется вызов функции main, созданной компилятором, а функция main выполняет работу по созданию и инициализации всех глобальных объектов.
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 = …… ;
Обратите внимание, что три вышеупомянутые фразы, обозначенные как черные тела, и что они имеют в виду, являются одним и тем же объектом? Ответ: да, они действительно указывают на один и тот же объект, что звучит не так, не так ли? Но это правда, вы можете написать простые фрагменты кода и проверить сами.
Давайте представим, что когда мы передаем объект типа Derived1 к функции, которая принимает параметры типа Base без ссылки, происходит обрезание. Как же это происходит?
Все объекты класса-производителя, унаследованного от класса Base, содержат субобъект типа Base (это ключ, который можно указать на объект Derived1 с помощью указателя типа Base, который, естественно, также является полимодальным ключом), а все субобъекты и все объекты типа Base имеют один и тот же s_object, естественно, примеры классов, полученные от класса Base, будут иметь один и тот же s_object.
2 Сравнение трех видов объектов памяти
Преимущество кубических объектов заключается в том, что они автоматически генерируются и уничтожаются в нужное время, без необходимости беспокойства программистов; и создание кубических объектов обычно быстрее, чем создание объектов на стеке, поскольку при распределении объектов на стеке используется операция operator new, которая использует алгоритм поиска в памяти, который может быть очень трудоемким, и создание кубических объектов не так сложно, это просто требует передвижения купольного указателя. Однако следует иметь в виду, что обычно кубические пространства имеют относительно небольшую емкость, как правило, 1 МБ к 2 МБ, поэтому большие объекты не подходят для распределения кубиков.
Объекты стека, которые должны быть программированы как в момент их создания, так и в момент их уничтожения, т. е. программисты имеют полный контроль над жизнью объектов стека. Мы часто нуждаемся в таких объектах, например, мы хотим создать объект, доступный для нескольких функций, но не хотим, чтобы он был глобальным, тогда создание объекта стека в этот момент, несомненно, является хорошим вариантом, а затем передача указателей этого объекта стека между различными функциями, позволяет достичь обмена этим объектом. Кроме того, по сравнению с кубическим пространством, емкость стека намного больше.
В следующий раз мы рассмотрим статические объекты.
Во-первых, глобальные объекты. Глобальные объекты предоставляют самый простой способ для межклассного и межфункционального общения, хотя этот способ не элегантен. В целом, в полностью объектно-ориентированных языках глобальных объектов не существует, например, в C#, поскольку глобальные объекты означают небезопасность и высокую сплоченность.
Далее следуют статические члены класса, которые, как уже упоминалось выше, разделяются всеми объектами базового класса и его производных классов. Таким образом, такие статические члены являются хорошим выбором, когда требуется обмен данными или связь между этими классами или между этими объектами класса.
Далее - статические локальные объекты, которые в основном используются для хранения промежуточного состояния во время циклического вызова функции, на которой находится объект. Наиболее ярким примером является рекурсивная функция.
В дизайне рекурсионных функций можно использовать статические объекты для замены нестатических локальных объектов (т. е. хабов), что не только снижает затраты на создание и освобождение нестатических объектов при каждом рекурсионном вызове и возвращении, но и сохраняет промежуточное состояние для рекурсионных вызовов и доступно для всех уровней вызова.
3 Непредвиденный урожай с использованием предметов из глины
Ранее мы уже говорили, что объекты создаются в нужное время, а затем автоматически освобождаются в нужное время, то есть у них есть функция автоматического управления. Что же происходит, когда объекты автоматически освобождаются?
Объект х, когда он автоматически освобождается, вызовет свою собственную функцию развертывания. Если мы вкладываем ресурс в объект х, и выполняем действия по освобождению ресурса в функцию развертывания объекта х, то вероятность утечки ресурса значительно снижается, потому что объект х может автоматически освобождать ресурс, даже когда происходит аномалия в его функции. Фактический процесс таков: когда функция выбрасывает аномалию, происходит так называемое stack_unwinding (склад рекрутинга), то есть развертывается в стеке, и поскольку объект х, естественно, существует в стеке, поэтому в процессе рекрутинга функция развертывания объекта х выполняется, что приводит к освобождению ограниченного количества ресурсов.
4 Запрет на создание объектов на куче
Как уже упоминалось выше, если вы решили запретить создание объекта определенного типа стека, то вы можете создать свой собственный класс упаковки ресурсов, который может быть создан только в пакете, чтобы автоматически освободить ресурс упаковки в случае исключений.
Так как же запретить создание объекта стека? Мы уже знаем, что единственный способ создания объекта стека - это использовать new, если мы запрещаем использовать new не работает. Далее, new при выполнении операции вызывает оператор new, а operator 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 (()) ; // ошибка в компиляции!
удалить 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 байтах объекта, чтобы гарантировать выполнение преобразования следующего утверждения так, как мы ожидаем:
Ресурс* rp = (Ресурс*)obj_ptr ;
Однако не все компиляторы должны быть такими же.
Так как мы можем запретить создание объекта определенного типа набора, можно ли спроектировать класс, чтобы он не создавал объекта х?
5 Запрет на создание предметов из цинка
Как уже упоминалось ранее, при создании объекта кубик перемещается с купольным указателем, чтобы убрать пространство с подходящим размером кубика, а затем на этом пространстве напрямую вызывается соответствующая конструкторская функция, чтобы сформировать объект кубика, а когда функция возвращается, она вызовет свою дифференциальную функцию, чтобы освободить объект, а затем настроит купольный указатель, чтобы вернуть этот кубик в память. В этом процессе не требуется операция new/delete, поэтому настройка оператора new/delete на private не достигает цели.
Это действительно возможно, и я планирую использовать этот вариант. Но прежде чем это сделать, нужно иметь в виду, что если мы настроим конструкционную функцию на частную, то мы не сможем использовать new для прямого создания объекта на куче, потому что new также будет вызывать его конструкторную функцию после распределения пространства для объекта. Поэтому я планирую настроить только диаконструкторную функцию на частную.
Если класс не предназначен как базовый класс, обычно используется заявление его диалогической функции как частной.
Для того, чтобы ограничить объекты х, но не ограничивать наследование, мы можем объявить диалогическую функцию защищенной, так что оба варианта хороши.
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//调用保护析构函数
}
};
Затем вы можете использовать класс NoStackObject примерно так:
NoStackObject* hash_ptr = новый NoStackObject() ;
...... // выполнять операции с объектом, на который указывается hash_ptr
hash_ptr->destroy (()); Я не знаю. О, не кажется ли вам немного странным, что мы создаем объект с помощью new, но вместо того, чтобы удалить его с помощью delete, используем метод destroy. Очевидно, пользователи не привыкли к этому странному способу использования. Поэтому я решил также установить конструирующую функцию на private или protected. Это возвращается к вопросу, который я пытался избежать выше, а именно, каким образом можно создать объект без new?
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++ идея невозможности репатриировать мусор обычно основана на невозможности правильно сканировать все блоки памяти, которые могут быть еще использованы, однако кажущееся невозможным, на самом деле, несложно. Во-первых, с помощью сканирования данных из памяти, динамически распределенные в памяти и направленные на стек, легко идентифицируются, и если есть ошибки в идентификации, можно только использовать некоторые данные, не являющиеся указателями, как указатели, а не указатели как непоказатели. Таким образом, процесс репатриации мусора просто упускает возможность восстановления и убирает ошибки, которые не должны быть устранены.
При рециклировании нужно просто сканировать bss-сегмент, data-сегмент и используемое в данный момент пространство, чтобы найти количество потенциальных динамических указателей, и рекурсивное сканирование упомянутой памяти позволит получить все динамические показатели, которые используются в данный момент.
Если вы готовы к созданию хорошего мусороуборочного устройства для вашего проекта, то возможно увеличить скорость управления памятью или даже уменьшить общий расход памяти. Если вы заинтересованы, то вы можете найти статьи и библиотеки, которые уже доступны в Интернете, и это особенно важно для программиста.
Перевод:ХК Чжан
#include<stdio.h>
int*fun(){
int k = 12;
return &k;
}
int main(){
int *p = fun();
printf("%d\n", *p);
getchar();
return 0;
}
Это не только доступно, но и изменяется, но доступ непредсказуем. Адреса локальных переменных находятся в собственном стеке программы, и после окончания административного переменного значение остается, если не передать адрес локальной переменной в память другой переменной. Однако, если изменить его, это опасно, поскольку этот адрес может быть передан другой переменной программы, которая может быть вынуждена изменить его с помощью указателя, что может вызвать крах программы.