@HighMan

Можно ли выделить память определенного размера?

Предположим, в C есть некая структура:
typedef struct {
  int a;
  char buf[];
} BUFFER;

Ее размер может меняться, на что указывает buf[];

В С, можно было сделать просто:
BUFFER * buf = (BUFFER *) malloc(1024);

Соответственно, размер buf = 1024 - sizeof(int);
В силу ряда причин, мне не подходит вариант:
typedef struct {
  int a;
  char * buf;
}

Мне нужно, что бы структура "одним куском" занимала память.
Можно ли выделить определенный объем памяти через new под мою структуру?
Если можно, то как? И как потом эту память освободить?
  • Вопрос задан
  • 170 просмотров
Пригласить эксперта
Ответы на вопрос 5
@code_panik
В C++ можно, как и в C, выделить себе участок памяти, в котором будут жить объекты (в широком смысле, в том числе встроенного типа, напр. int). Чтобы начать жизнь объекта, его нужно разместить в памяти с соответствующим типу выравниванием. Если выравнивание, равное степени двойки, будет меньше требуемого для типа, то приведение с помощью static_cast типа указателя в выделенной памяти (void*, char*, unsigned char*) к указателю на тип объекта создает неопределенное поведение. Значит, чтобы безопасно использовать объект, размещенный в выделенной памяти, нужно убедиться, что он размещен (с помощью placement new) по выровненному адресу.

Пусть мы создаем основной буфер под объект вызовом new unsigned char[size]. Будет ли буфер иметь достаточное выравнивание, чтобы в начале его разместить BUFFER::a типа int? - Да, если мы зарезервируем сразу достаточно памяти,

In addition, if the new-expression is used to allocate an array of char, unsigned char, or std::byte (since C++17), it may request additional memory from the allocation function if necessary to guarantee correct alignment of objects of all types no larger than the requested array size, if one is later placed into the allocated array.


Выражения new, new[] автоматически определяют размер выделяемой памяти и передают его низкоуровневым функциям operator new, operator new[] соответственно. Память, выделенная new, освобождается вызовом delete, для new[] - delete[]. Поведение delete похоже на поведение new выше.

Значит, у нас есть всё необходимое чтобы реализовать свой класс буфера. Например,
#include <cstddef>
#include <iostream>
#include <cstring>

class Buffer {
public:
  using preamble_type = int;
  static constexpr std::size_t preamble_size = sizeof(preamble_type);

  Buffer(const preamble_type& value, std::size_t bufferSize) {
    buffer_ = new unsigned char[preamble_size + bufferSize];
    ::new(buffer_) preamble_type(value);
  }

  Buffer(const Buffer& other) = delete;
  Buffer& operator=(const Buffer& rhs) = delete;

  Buffer(Buffer&& other) noexcept
    : buffer_(other.buffer_) {
      other.buffer_ = nullptr;
    }

  Buffer& operator=(Buffer&& rhs) noexcept {
    std::swap(buffer_, rhs.buffer_);
    return *this;
  }

  virtual ~Buffer() {
    destroy();
  }

  preamble_type& preamble() noexcept {
    return const_cast<preamble_type&>(
        const_cast<const Buffer *>(this)->preamble()
        );        
  }

  const preamble_type& preamble() const noexcept {
    return *preambleData();
  }

  unsigned char* data() const noexcept {
    return buffer_ + preamble_size;
  }    

  void resize(std::size_t size) {        
    if (buffer_ != nullptr) {
      auto newBuffer = new unsigned char[preamble_size + size];
      ::new(newBuffer) preamble_type(std::move(preamble()));
      destroy();
      buffer_ = newBuffer;
    }
  }

private:
  preamble_type* preambleData() const noexcept {
    // return std::launder(reinterpret_cast<preamble_type*>(buffer_)); c++17
    return reinterpret_cast<preamble_type*>(buffer_);
  }

  void destroy() noexcept {
    preambleData()->~preamble_type();
    delete[] buffer_;
  }

  unsigned char* buffer_ = nullptr;
};

int main()
{
  using std::cout;
  using std::endl;

  const std::size_t bufferSize = 100;
  Buffer b(100, bufferSize);
  const char hello[] = "hello world!";
  memcpy(b.data(), hello, sizeof(hello));

  auto c = std::move(b);
  cout << c.preamble() << ' ' << c.data() << endl;

  b = Buffer(5, 20);
  memcpy(b.data(), hello, sizeof(hello));
  cout << b.preamble() << ' ' << b.data() << endl;
  return 0;
}


Из кода можно выкинуть всё, что касается preamble_type, если считать, что первое поле всегда будет int. С другой стороны, код можно сделать обобщенным с минимальными изменениями. Например,
template<typename PreambleT>
class Buffer {
public:
   using preamble_type = PreambleT;
   //...
};


Такой пример class Buffer, по-моему, скорее плохой, потому что только имитирует на C++ код, написанный на C. Так, мы не храним в классе размер буфера, поэтому нельзя определить операторы копирования. Детали реализации класса "утекают" в пользовательский код.
Пока мы явно не сформулировали и не поддерживаем инварианты класса. Какое состояние объекта мы можем считать пригодным для использования?
В частности, мы можем переместить содержимое буфера в другой объект конструктором перемещения, но потом к исходному буферу не можем применить resize. Даже если бы resize давал нам новый буфер, значение preamble потеряно. Если бы мы сохраняли preamble, тогда в конструкторе перемещения пришлось бы выделять память под новый буфер, но тогда этот конструктор уже не будет noexcept - плохо. Придется запретить перемещение?

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

Наверняка есть лучшее проектное решение, такое которое не будет имитировать реализацию на C. Возможно, имеет смысл портировать решение на уровне интерфейсов, переписывая полностью на C++ детали реализации.
Ответ написан
Комментировать
@dima20155
you don't choose c++. It chooses you
Вы можете использовать только так:
typedef struct {
  int a;
  char buf [10];
}

То есть размер должен быть известен на момент компиляции.
В ином случае вы можете написать аллокацию памяти для buf самостоятельно.
То есть аллоцировать кусок памяти и отдать его в конструктор структуры. А в конструкторе структуры разместить массив в куске памяти, который был аллоцирован ранее с каким-то отступом.

Вопрос зачем это вам нужно. Если это вопрос сериализации, то используйте готовое решение.
Ответ написан
@rPman
Если у тебя в итоге размер этого buf будет известен на этапе компиляции, то можно воспользоваться шаблонами
template<int SIZE> struct MyStruct {
  int a;
  char buf[SIZE];
};
...
auto x=new MyStruct<10>; // x - указатель на MyStruct
или
const int size=10;
auto x=new MyStruct<size>;
...
delete x;
Ответ написан
@nagolove
typedef struct {
  int a;
  char *buf;
} BUFFER;

BUFFER buffer_alloc(int num) {
  BUFFER *b = malloc(sizeof(*b) + num * sizeof(b->[0]));
  b->buf = (char*)b + sizeof(*b);
  return b;
}

BUFFER *bb = buffer_alloc(10);
bb->buf[9] = 1;
Ответ написан
Комментировать
@stanislav888
Контейнеры std::vector или std::array хранят массив одним куском. Указатель на первый элемент достаётся через std::vector::data() . Кастуете его до указателя на вашу структуру и работаете. В конце области видимости контейнер красиво удалится вместе со своим массивом.
Ещё можете использовать std::unique_ptr для создания и удаления любых структур в динамической памяти..
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы