Когда бы вы использовали массив, а не вектор / строку?
Я начинающий программист на C++, поэтому научился использовать массивы, а не векторы (кажется, это общий способ делать что-то, а потом перейду к векторам).
Я заметил, что многие ответы на SO предлагают использовать векторы над массивами и строки над массивами char. Похоже, что это «правильный» способ программирования на C++.
При этом, когда все еще стоит использовать классический массив / char * (если вообще)?
Ответов (8)8
При написании кода, который следует использовать в других проектах, в частности, если вы нацелены на специальные платформы (встроенные, игровые консоли и т. Д.), Где STL может отсутствовать.
Старые проекты или проекты с особыми требованиями могут не захотеть вводить зависимости от библиотек STL. Интерфейс, зависящий от массивов, char * или чего-либо еще, будет совместим с чем угодно, поскольку он является частью языка. Однако не гарантируется, что STL будет присутствовать во всех средах сборки.
Этот вопрос можно разделить на две части:
- Как мне управлять памятью для данных плоского массива?
- Как мне получить доступ к элементам плоского массива?
Я лично предпочитаю использовать 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 к примитивным типам на этапе оптимизации лучше, чем запуск проекта с более трудным для чтения кодом.
Я вижу две причины:
- Совместимость (старый код без STL).
- Скорость. (Я сравнивал скорость использования векторного / двоичного_поиска и массива / рукописного двоичного поиска. Для последнего был получен один некрасивый код (с перераспределением памяти), но он был примерно в 1,2-1,5 раза быстрее первого, я использовал MS VC++ 8 )
Никогда.
Если необработанный массив кажется лучшим решением, чем вектор (по другим причинам, упомянутым здесь), я использую std::tr1 :: array или std::array в компиляторах C++ 11 (или boost :: array ). Он просто выполняет проверки, которые я бы сделал в любом случае, чтобы быть уверенным, и использование значения размера автоматически реализует DRY (например, я использую размер в циклах, чтобы будущие изменения объявления массива работали автоматически).
Это реализация массива «является» необработанным массивом с проверками и предоставленной константой размера в любом случае, поэтому легко получить код массива и во встроенном коде, потому что код на самом деле не «слишком умный» для любого компилятора. Что касается шаблонов поддержки компилятора, я бы скопировал заголовки boost в свой код, чтобы позволить мне использовать этот вместо необработанных массивов. Потому что явно слишком легко ошибиться с необработанными массивами. Необработанные массивы - это зло . Они подвержены ошибкам.
И он очень хорошо работает с алгоритмами STL (если есть).
Теперь есть два случая, когда вам нужно использовать необработанные массивы (обязательство): когда вы используете код только для C (не взаимодействуете с кодом C, но пишете часть кода только для C, например, библиотеку C). Но тогда это другой язык .
Другая причина в том, что компилятор вообще не поддерживает шаблоны ...
Я работаю над общей библиотекой, которой нужен доступ к структурированным данным. Эти данные известны во время компиляции, поэтому для хранения данных используются массивы констант POD (простые старые данные) в файловой области.
Это заставляет компилятор и компоновщик помещать большую часть данных в раздел только для чтения, что дает два преимущества:
- Его можно сопоставить с каталогом памяти с диска без запуска какого-либо специального кода инициализации.
- Его можно разделить между процессами.
Единственным исключением является то, что компилятор по-прежнему генерирует код инициализации для загрузки констант с плавающей запятой, поэтому любая структура, содержащая числа с плавающей запятой, попадает в секцию с возможностью записи. Я подозреваю, что это как-то связано с плавающими исключениями или режимами округления с плавающей запятой, но я не уверен, как проверить любую гипотезу.
Если бы я использовал для этого векторные и строковые объекты, то компилятор генерировал бы намного больше кода инициализации, который выполнялся бы всякий раз, когда загружалась моя разделяемая библиотека. Постоянные данные будут размещены в куче, поэтому они не будут использоваться совместно между процессами.
Если бы я прочитал данные из файла на диске, мне пришлось бы иметь дело с проверкой формата данных вместо того, чтобы компилятор C++ делал это за меня. Я также должен был бы управлять временем жизни данных в кодовой базе, в которую эти глобальные данные с самого начала были «встроены».