Массив: хранение объектов или ссылок

#c

#c

Вопрос:

У меня, как у разработчика Java, возник следующий вопрос по C .

Если у меня есть объекты типа A, и я хочу сохранить их коллекцию в массиве, то должен ли я просто хранить указатели на объекты или лучше хранить сам объект?

На мой взгляд, лучше хранить указатели, потому что: 1) Можно легко удалить объект, установив для его указателя значение null 2) Экономится место.

Комментарии:

1. зависит от использования и контейнера..

2. Я думаю, что под «ссылками» вы имеете в виду указатели. Ссылкам в C нельзя присвоить значение null, и вы в любом случае не можете определить массив ссылок.

3. контейнер — это массив, и объекты могут добавляться и удаляться во время выполнения

4. @Nerdtron: Однако вам все равно нужно было бы каким-то образом управлять временем жизни объекта, и общее использование памяти было бы больше (сами объекты плюс указатели в массиве).

5. При работе с динамическими массивами хранение указателей вместо значений «приводит к потере» указателя для каждого элемента массива, но «сохраняет» значение для каждого элемента в объеме массива. Таким образом, для больших объектов, где вы не можете надежно хранить capacity() и size() закрывать массивы указателей, все еще может быть экономия места.

Ответ №1:

Указатели или просто объекты?

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

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

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

 #include <vector>

struct foo {
  virtual void it() {}
};

struct bar : public foo {
  int a;
  virtual void it() {}
}; 

int main() {
  std::vector<foo> v;
  v.push_back(bar()); // not doing what you expected! (the temporary bar gets "made into" a foo before storing as a foo and your vector doesn't get a bar added)
  std::vector<foo*> v2;
  v2.push_back(new bar()); // Fine
}
  

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

Удаление из массивов или контейнеров.

Присвоение NULL не приводит к уменьшению количества указателей в вашем контейнере / массиве ( delete он также не обрабатывает), размер остается тем же, но теперь есть указатели, которые вы не можете юридически разыменовывать. Это делает остальную часть вашего кода более сложной в виде дополнительных инструкций if и запрещает такие вещи, как:

 // need to go out of our way to make sure there's no NULL here
std::for_each(v2.begin(),v2.end(), std::mem_fun(amp;foo::it));
  

Мне действительно не нравится идея разрешать NULL s в последовательностях указателей в целом, потому что вы быстро заканчиваете тем, что хороните всю реальную работу в последовательности условных операторов. Альтернативой является то, что std::vector предоставляет erase метод, который использует итератор, чтобы вы могли писать:

 v2.erase(v2.begin());
  

чтобы удалить первое или v2.begin() 1 для второго. Хотя в std::vector нет простого метода «стереть n-й элемент» из-за трудоемкости — если вы выполняете много операций стирания, то есть другие контейнеры, которые могут быть более подходящими.

Для массива вы можете имитировать стирание с помощью:

 #include <utility>
#include <iterator>
#include <algorithm>
#include <iostream>

int main() {
  int arr[] = {1,2,3,4};
  int len = sizeof(arr)/sizeof(*arr);
  std::copy(arr, arr len, std::ostream_iterator<int>(std::cout, " "));
  std::cout << std::endl;

  // remove 2nd element, without preserving order:
  std::swap(arr[1], arr[len-1]);
  len -= 1;
  std::copy(arr, arr len, std::ostream_iterator<int>(std::cout, " "));
  std::cout << std::endl;

  // and again, first element:
  std::swap(arr[0], arr[len-1]);
  len -= 1;
  std::copy(arr, arr len, std::ostream_iterator<int>(std::cout, " "));
  std::cout << std::endl;
}
  

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

Комментарии:

1. Также «удаление» объекта путем присвоения указателю значения null неестественно и добавляет ненужную сложность коду.

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

3. @kbok the но как бы вы удалили объект из массива?

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

5. @user695652 Если вы хотите удалить объекты из контейнера, то массив — это не то, что вам нужно.

Ответ №2:

Похоже, вы путаете ссылки с указателями. C имеет 3 распространенных способа представления дескрипторов объектов

  • Ссылки
  • Указатели
  • Значения

Исходя из Java, наиболее аналогичный способ — сделать это с помощью указателя. Вероятно, это то, что вы пытаетесь здесь сделать.

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

Ответ №3:

Это полностью зависит от того, что вы хотите сделать … но в некотором смысле вы заблуждаетесь.

Вам следует знать следующие вещи:

  1. Вы не можете присвоить ссылке значение NULL в C , хотя вы можете присвоить указателю значение NULL.
  2. Ссылка может быть сделана только на существующий объект — он должен инициализироваться как таковой.
  3. Ссылка не может быть изменена (хотя значение, на которое ссылается ссылка, может быть).
  4. Вы бы не экономили место, на самом деле вы бы использовали больше, поскольку вы используете объект и ссылку. Если вам нужно ссылаться на один и тот же объект несколько раз, вы экономите место, но с тем же успехом вы можете использовать указатель — он более гибкий в БОЛЬШИНСТВЕ (читай: не во всех) сценариев.
  5. Последнее важное замечание: контейнеры STL (vector, list и т.д.) Имеют семантику КОПИРОВАНИЯ — они не могут работать со ссылками. Они могут работать с указателями, но это усложняется, поэтому пока вы всегда должны использовать копируемые объекты в этих контейнерах и соглашаться с тем, что они будут скопированы, нравится вам это или нет. STL разработан так, чтобы быть эффективным и безопасным с семантикой копирования.

Надеюсь, это поможет! 🙂

PS (РЕДАКТИРОВАТЬ): Вы можете использовать некоторые новые функции в BOOST / TR1 (погуглите их) и создать контейнер / массив shared_ptr (интеллектуальные указатели для подсчета ссылок), который даст вам ощущение, аналогичное ссылкам Java и сборке мусора. Существует множество различий, но вам придется прочитать об этом самостоятельно — они являются отличной особенностью нового стандарта.

Комментарии:

1. «должен ли я просто хранить указатели на объекты или лучше хранить сам объект?»

2. В его первоначальном вопросе никогда не упоминался указатель, в нем везде упоминались ссылки, пытающиеся обрабатывать их так, как они использовались бы в Java, что невозможно. 😉

Ответ №4:

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

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

В отличие от Java, присвоение указателю значения null не освобождает объект, на который указано; вместо этого вы получаете утечку памяти, если на объект больше нет указателей. Если объект был создан с использованием new , то delete в какой-то момент он должен быть вызван. Наилучшими вариантами здесь являются хранение интеллектуальных указателей ( shared_ptr или, возможно unique_ptr , если таковые имеются) или использование контейнеров указателей Boost.

Ответ №5:

Вы не можете хранить ссылки в контейнере. Вместо этого вы могли бы хранить (голые) указатели, но это чревато ошибками и поэтому вызывает неодобрение.

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

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

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

Ответ №6:

Чтобы добавить к ответу aix:

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