Предисловие
Java не похож на C ++, в отличие от рекламы. Машина для раскрутки Java хотела бы, чтобы вы поверили, что, поскольку Java имеет синтаксис, подобный C ++, языки схожи. Ничто не может быть дальше от правды. Эта дезинформация является частью причины, по которой Java-программисты переходят на C ++ и используют Java-подобный синтаксис, не понимая значения своего кода.
Вперед мы идем
Но я не могу понять, почему мы должны делать это таким образом. Я предполагаю, что это связано с эффективностью и скоростью, поскольку мы получаем прямой доступ к адресу памяти. Я прав?
На самом деле наоборот. Куча намного медленнее стека, потому что стек очень прост по сравнению с кучей. Переменные автоматического хранения (или переменные стека) имеют свои деструкторы, вызываемые после выхода из области видимости. Например:
{
std::string s;
}
// s is destroyed here
С другой стороны, если вы используете динамически размещаемый указатель, его деструктор должен вызываться вручную. delete
называет это деструктором для вас.
{
std::string* s = new std::string;
}
delete s; // destructor called
Это не имеет ничего общего с new
синтаксисом, распространенным в C # и Java. Они используются для совершенно разных целей.
Преимущества динамического распределения
1. Вам не нужно заранее знать размер массива
Одна из первых проблем, с которыми сталкиваются многие программисты на С ++, заключается в том, что когда они принимают произвольные данные от пользователей, вы можете выделить фиксированный размер только для переменной стека. Вы также не можете изменить размер массивов. Например:
char buffer[100];
std::cin >> buffer;
// bad input = buffer overflow
Конечно, если вы использовали std::string
вместо этого, std::string
внутренне изменяет размер себя, так что это не должно быть проблемой. Но, по сути, решением этой проблемы является динамическое распределение. Вы можете выделить динамическую память на основе ввода пользователя, например:
int * pointer;
std::cout << "How many items do you need?";
std::cin >> n;
pointer = new int[n];
Примечание : одна ошибка, которую допускают многие новички, - использование массивов переменной длины. Это расширение GNU, а также расширение в Clang, поскольку они отражают многие расширения GCC. Поэтому
int arr[n]
не следует полагаться на следующее.
Поскольку куча намного больше стека, можно произвольно выделить / перераспределить столько памяти, сколько ему нужно, в то время как у стека есть ограничение.
2. Массивы не указатели
Какую пользу вы спрашиваете? Ответ станет ясен, как только вы поймете путаницу / миф о массивах и указателях. Обычно считается, что они одинаковы, но это не так. Этот миф проистекает из того факта, что указатели могут быть подписаны так же, как массивы, и из-за того, что массивы распадаются на указатели на верхнем уровне в объявлении функции. Однако, когда массив распадается на указатель, указатель теряет свою sizeof
информацию. Таким образом, sizeof(pointer)
будет указан размер указателя в байтах, который обычно составляет 8 байтов в 64-битной системе.
Вы не можете назначать массивы, только инициализировать их. Например:
int arr[5] = {1, 2, 3, 4, 5}; // initialization
int arr[] = {1, 2, 3, 4, 5}; // The standard dictates that the size of the array
// be given by the amount of members in the initializer
arr = { 1, 2, 3, 4, 5 }; // ERROR
С другой стороны, вы можете делать что угодно с указателями. К сожалению, поскольку в Java и C # различие между указателями и массивами выполняется вручную, новички не понимают разницы.
3. Полиморфизм
Java и C # имеют средства, которые позволяют вам рассматривать объекты как другие, например, используя as
ключевое слово. Поэтому, если кто-то хочет рассматривать Entity
объект как Player
объект, это можно сделать. Player player = Entity as Player;
Это очень полезно, если вы собираетесь вызывать функции в однородном контейнере, который должен применяться только к определенному типу. Функциональность может быть достигнута аналогичным образом ниже:
std::vector<Base*> vector;
vector.push_back(&square);
vector.push_back(&triangle);
for (auto& e : vector)
{
auto test = dynamic_cast<Triangle*>(e); // I only care about triangles
if (!test) // not a triangle
e.GenericFunction();
else
e.TriangleOnlyMagic();
}
Так, скажем, если бы только у треугольников была функция Rotate, это было бы ошибкой компилятора, если бы вы попытались вызвать ее для всех объектов класса. Используя dynamic_cast
, вы можете моделироватьas
ключевое слово. Для ясности, если приведение не выполнено, возвращается неверный указатель. Таким образом, !test
это, по сути, сокращение для проверки, test
является ли NULL или недопустимый указатель, что означает, что приведение не удалось.
Преимущества автоматических переменных
После просмотра всех замечательных вещей, которые может сделать динамическое распределение, вы, вероятно, задаетесь вопросом, почему бы никому НЕ использовать динамическое распределение все время? Я уже говорил вам одну причину, куча медленная. И если вам не нужна вся эта память, вы не должны злоупотреблять ею. Итак, вот некоторые недостатки в произвольном порядке:
Это подвержено ошибкам. Ручное распределение памяти опасно, и вы склонны к утечкам. Если вы не разбираетесь в использовании отладчика илиvalgrind
(средством утечки памяти), вы можете вырвать волосы из головы. К счастью, идиомы RAII и умные указатели немного смягчают это, но вы должны быть знакомы с такими практиками, как Правило Трех и Правило Пяти. Здесь много информации, и новички, которые не знают или не заботятся, попадут в эту ловушку.
Это не обязательно. В отличие от Java и C #, где использование new
ключевого слова везде идиоматично , в C ++ его следует использовать только при необходимости. Общая фраза гласит: все выглядит как гвоздь, если у вас есть молоток. В то время как новички, начинающие с C ++, боятся указателей и учатся использовать переменные стека по привычке, программисты на Java и C # начинают с использования указателей, не понимая этого! Это буквально ступает не на ту ногу. Вы должны отказаться от всего, что вы знаете, потому что синтаксис - это одно, а изучение языка - это другое.
1. (N) RVO - Aka, (Named) Оптимизация возвращаемого значения
Одна оптимизация, которую делают многие компиляторы, это то, что называется оптимизацией elision и return value . Эти вещи могут устранить ненужные копии, которые полезны для очень больших объектов, таких как вектор, содержащий много элементов. Обычно обычной практикой является использование указателей для передачи прав собственности, а не копирование больших объектов для их перемещения . Это привело к появлению семантики перемещения и умных указателей. .
Если вы используете указатели, (N) RVO НЕ происходит. Более выгодно и менее подвержено ошибкам использовать преимущество (N) RVO, а не возвращать или передавать указатели, если вы беспокоитесь об оптимизации. Утечка ошибок может произойти, если вызывающая функция ответственна за delete
динамически размещенный объект и тому подобное. Может быть трудно отследить право собственности на объект, если указатели передаются как горячий картофель. Просто используйте переменные стека, потому что это проще и лучше.