Когда бы вы использовали массив, а не вектор / строку?

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

Я заметил, что многие ответы на SO предлагают использовать векторы над массивами и строки над массивами char. Похоже, что это «правильный» способ программирования на C++.

При этом, когда все еще стоит использовать классический массив / char * (если вообще)?

Ответов (8)

Решение

При написании кода, который следует использовать в других проектах, в частности, если вы нацелены на специальные платформы (встроенные, игровые консоли и т. Д.), Где STL может отсутствовать.

Старые проекты или проекты с особыми требованиями могут не захотеть вводить зависимости от библиотек STL. Интерфейс, зависящий от массивов, char * или чего-либо еще, будет совместим с чем угодно, поскольку он является частью языка. Однако не гарантируется, что STL будет присутствовать во всех средах сборки.

Этот вопрос можно разделить на две части:

  1. Как мне управлять памятью для данных плоского массива?
  2. Как мне получить доступ к элементам плоского массива?

Я лично предпочитаю использовать std::vector для управления памятью, за исключением случаев, когда мне нужно поддерживать совместимость с кодом, который не использует STL (например, при взаимодействии с прямым кодом C ). Гораздо сложнее создать безопасный для исключений код с необработанными массивами, выделенными с помощью new или malloc (отчасти потому, что очень легко забыть, что вам нужно об этом беспокоиться). См. Любую статью о RAII для выяснения причин.

На практике std::vector реализован как плоский массив . Таким образом, всегда можно извлечь необработанный массив и использовать шаблоны доступа в стиле C. Обычно я начинаю с синтаксиса оператора индекса вектора. Для некоторых компиляторов при создании отладочной версии векторы обеспечивают автоматическую проверку границ . Это медленно (часто 10-кратное замедление для жестких циклов), но помогает находить определенные типы ошибок.

Если профилирование на определенной платформе указывает на то, что оператор [] является узким местом, я переключаюсь на прямой доступ к необработанному массиву. Интересно, что в зависимости от компилятора и ОС иногда может быть быстрее использовать вектор STL, чем необработанный массив .

Вот некоторые результаты простого тестового приложения. Он был скомпилирован с Visual Studio 2008 в 32-разрядном режиме выпуска с использованием оптимизации / O2 и запускался в Vista x64. Аналогичные результаты достигаются с 64-битным тестовым приложением.

Binary search...
           fill vector (for reference) :  0.27 s
                   array with ptr math :  0.38 s <-- C-style pointers lose
                  array with int index :  0.23 s <-- [] on raw array wins
            array with ptrdiff_t index :  0.24 s
                 vector with int index :  0.30 s  <-- small penalty for vector abstraction
           vector with ptrdiff_t index :  0.30 s

Counting memory (de)allocation...
                memset (for reference) :  2.85 s
      fill malloc-ed raw array with [] :  2.66 s
     fill malloc-ed raw array with ptr :  2.81 s
         fill new-ed raw array with [] :  2.64 s
        fill new-ed raw array with ptr :  2.65 s
                  fill vector as array :  3.06 s  \ something's slower 
                           fill vector :  3.05 s  / with vector!

NOT counting memory (de)allocation...
                memset (for reference) :  2.57 s
      fill malloc-ed raw array with [] :  2.86 s
     fill malloc-ed raw array with ptr :  2.60 s
         fill new-ed raw array with [] :  2.63 s
        fill new-ed raw array with ptr :  2.78 s
                  fill vector as array :  2.49 s \ after discounting the  
                           fill vector :  2.54 s / (de)allocation vector is faster!

Код:

#define WINDOWS_LEAN_AND_MEAN
#include <windows.h>
#include <string>
#include <vector>
#include <stdio.h>

using namespace std;

__int64 freq; // initialized in main
int const N = 1024*1024*1024/sizeof(int)/2; // 1/2 GB of data
int const nIter = 10;

class Timer {
public:
  Timer(char *name) : name(name) {
    QueryPerformanceCounter((LARGE_INTEGER*)&start);
  }
  ~Timer() {
    __int64 stop;
    QueryPerformanceCounter((LARGE_INTEGER*)&stop);
    printf("  %36s : % 4.2f s\n", name.c_str(), (stop - start)/double(freq));
  }
private:
  string const name;
  __int64 start;
};


template <typename Container, typename Index>
int binarySearch_indexed(Container sortedArray, Index first, Index last, int key) {
  while (first <= last) {
    Index mid = (first + last) / 2; // NOT safe if (first+last) is too big!
    if (key > sortedArray[mid])      first = mid + 1;
    else if (key < sortedArray[mid])  last = mid - 1; 
    else return mid;  
  }
  return 0; // Use "(Index)-1" in real code
}

int Dummy = -1;
int const *binarySearch_ptr(int const *first, int const *last, int key) {
  while (first <= last) {
    int const *mid = (int const *)(((unsigned __int64)first + (unsigned __int64)last) / 2);  
    if (key > *mid)      first = mid + 1;
    else if (key < *mid)  last = mid - 1; 
    else return mid;  
  }
  return &Dummy; // no NULL checks: don't do this for real
}

void timeFillWithAlloc() {
  printf("Counting memory (de)allocation...\n");
  { 
    Timer tt("memset (for reference)");
    int *data = (int*)malloc(N*sizeof(int));
    for (int it=0; it<nIter; it++) memset(data, 0, N*sizeof(int));
    free(data);
  }
  { 
    Timer tt("fill malloc-ed raw array with []");
    int *data = (int*)malloc(N*sizeof(int));
    for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
    free(data);
  }
  { 
    Timer tt("fill malloc-ed raw array with ptr");
    int *data = (int*)malloc(N*sizeof(int));
    for (int it=0; it<nIter; it++) {
    int *d = data;
    for (size_t i=0; i<N; i++) *d++ = (int)i;
    }
    free(data);
  }
  { 
    Timer tt("fill new-ed raw array with []");
    int *data = new int[N];
    for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
    delete [] data;
  }
  { 
    Timer tt("fill new-ed raw array with ptr");
    int *data = new int[N];
    for (int it=0; it<nIter; it++) {
    int *d = data;
    for (size_t i=0; i<N; i++) *d++ = (int)i;
    }
    delete [] data;
  }
  { 
    Timer tt("fill vector as array");
    vector<int> data(N); 
    for (int it=0; it<nIter; it++) {
      int *d = &data[0]; 
    for (size_t i=0; i<N; i++) *d++ = (int)i;
    }
  }
  { 
    Timer tt("fill vector");
    vector<int> data(N); 
    for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
  }
  printf("\n");
}

void timeFillNoAlloc() {
  printf("NOT counting memory (de)allocation...\n");

  { 
    int *data = (int*)malloc(N*sizeof(int));
    {
      Timer tt("memset (for reference)");
      for (int it=0; it<nIter; it++) memset(data, 0, N*sizeof(int));
    }
    free(data);
  }
  { 
    int *data = (int*)malloc(N*sizeof(int));
    {
      Timer tt("fill malloc-ed raw array with []");
      for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
    }
    free(data);
  }
  { 
    int *data = (int*)malloc(N*sizeof(int));
    {
      Timer tt("fill malloc-ed raw array with ptr");
      for (int it=0; it<nIter; it++) {
        int *d = data;
        for (size_t i=0; i<N; i++) *d++ = (int)i;
      }
    }
    free(data);
  }
  { 
    int *data = new int[N];
    {
      Timer tt("fill new-ed raw array with []");
      for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
    }
    delete [] data;
  }
  { 
    int *data = new int[N];
    {
      Timer tt("fill new-ed raw array with ptr");
      for (int it=0; it<nIter; it++) {
        int *d = data;
        for (size_t i=0; i<N; i++) *d++ = (int)i;
      }
    }
    delete [] data;
  }
  { 
    vector<int> data(N); 
    {
      Timer tt("fill vector as array");
      for (int it=0; it<nIter; it++) {
        int *d = &data[0]; 
        for (size_t i=0; i<N; i++) *d++ = (int)i;
      }
    }
  }
  { 
    vector<int> data(N); 
    {
      Timer tt("fill vector");
      for (int it=0; it<nIter; it++) for (size_t i=0; i<N; i++) data[i] = (int)i;
    }
  }
  printf("\n");
}

void timeBinarySearch() {
  printf("Binary search...\n");
  vector<int> data(N); 
  {
    Timer tt("fill vector (for reference)");
    for (size_t i=0; i<N; i++) data[i] = (int)i;
  }

  {
    Timer tt("array with ptr math");
    int sum = 0;
    for (int i=-1000000; i<1000000; i++) {
      sum += *binarySearch_ptr(&data[0], &data[0]+data.size(), i);
    }
  }
  {
    Timer tt("array with int index");
    int sum = 0;
    for (int i=-1000000; i<1000000; i++) {
      sum += data[binarySearch_indexed<int const *, int>(
        &data[0], 0, (int)data.size(), -1)];
    }
  }
  {
    Timer tt("array with ptrdiff_t index");
    int sum = 0;
    for (int i=-1000000; i<1000000; i++) {
      sum += data[binarySearch_indexed<int const *, ptrdiff_t>(
        &data[0], 0, (ptrdiff_t)data.size(), -1)];
    }
  }
  {
    Timer tt("vector with int index");
    int sum = 0;
    for (int i=-1000000; i<1000000; i++) {
      sum += data[binarySearch_indexed<vector<int> const &, int>(
        data, 0, (int)data.size(), -1)];
    }
  }
  {
    Timer tt("vector with ptrdiff_t index");
    int sum = 0;
    for (int i=-1000000; i<1000000; i++) {
      sum += data[binarySearch_indexed<vector<int> const &, ptrdiff_t>(
        data, 0, (ptrdiff_t)data.size(), -1)];
    }
  }

  printf("\n");
}

int main(int argc, char **argv)
{
  QueryPerformanceFrequency((LARGE_INTEGER*)&freq);

  timeBinarySearch();
  timeFillWithAlloc();
  timeFillNoAlloc();

  return 0;
}

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

Я вижу две причины:

  1. Совместимость (старый код без STL).
  2. Скорость. (Я сравнивал скорость использования векторного / двоичного_поиска и массива / рукописного двоичного поиска. Для последнего был получен один некрасивый код (с перераспределением памяти), но он был примерно в 1,2-1,5 раза быстрее первого, я использовал MS VC++ 8 )

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

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

Никогда.

Если необработанный массив кажется лучшим решением, чем вектор (по другим причинам, упомянутым здесь), я использую std::tr1 :: array или std::array в компиляторах C++ 11 (или boost :: array ). Он просто выполняет проверки, которые я бы сделал в любом случае, чтобы быть уверенным, и использование значения размера автоматически реализует DRY (например, я использую размер в циклах, чтобы будущие изменения объявления массива работали автоматически).

Это реализация массива «является» необработанным массивом с проверками и предоставленной константой размера в любом случае, поэтому легко получить код массива и во встроенном коде, потому что код на самом деле не «слишком умный» для любого компилятора. Что касается шаблонов поддержки компилятора, я бы скопировал заголовки boost в свой код, чтобы позволить мне использовать этот вместо необработанных массивов. Потому что явно слишком легко ошибиться с необработанными массивами. Необработанные массивы - это зло . Они подвержены ошибкам.

И он очень хорошо работает с алгоритмами STL (если есть).

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

Другая причина в том, что компилятор вообще не поддерживает шаблоны ...

Я работаю над общей библиотекой, которой нужен доступ к структурированным данным. Эти данные известны во время компиляции, поэтому для хранения данных используются массивы констант POD (простые старые данные) в файловой области.

Это заставляет компилятор и компоновщик помещать большую часть данных в раздел только для чтения, что дает два преимущества:

  • Его можно сопоставить с каталогом памяти с диска без запуска какого-либо специального кода инициализации.
  • Его можно разделить между процессами.

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

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

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