Як використовувати масиви в C ++?


480

C ++ успадковані масиви від C, де вони використовуються практично скрізь. C ++ надає абстракції, які простіше у використанні і менш схильні до помилок ( std::vector<T>оскільки C ++ 98 і std::array<T, n>так як C ++ 11 ), тому потреба в масивах не виникає настільки ж часто , як це робиться в С. Тим НЕ менше, коли ви читаєте спадщина коду або взаємодії з бібліотекою, написаною на C, ви повинні чітко зрозуміти, як працюють масиви.

Цей FAQ розділений на п'ять частин:

  1. масиви на рівні типу та елементи доступу
  2. створення та ініціалізація масиву
  3. призначення та передача параметрів
  4. багатовимірні масиви та масиви покажчиків
  5. поширені підводні камені при використанні масивів

Якщо вам здається, що у цьому поширеному запитанні чогось важливого не вистачає, напишіть відповідь і зв’яжіть його тут як додаткову частину.

У наступному тексті "масив" означає "масив C", а не шаблон класу std::array. Передбачається базове знання синтаксису декларатора C. Зауважте, що ручне використання newта, deleteяк показано нижче, надзвичайно небезпечно за винятком винятків, але це тема іншого FAQ .

(Примітка. Це означає, що це запис до C ++ FAQ Stack Overflow . Якщо ви хочете критикувати ідею надання поширених запитань у цій формі, то тут слід зробити публікацію про мета, яка почала все це . Відповіді на це питання відстежується в кімнаті чатів C ++ , де ідея FAQ задається в першу чергу, тому велику ймовірність отримати відповідь ті, хто придумав цю ідею.)


Вони були б ще кращими, якби вказівники завжди вказували на початок, а не десь на середині цілі, хоча ...
Дедуплікатор

Вам слід використовувати вектор STL, оскільки він забезпечує більшу гнучкість.
Моїз Садід

2
При сумісній доступності std::arrays, std::vectors і gsl::spans - я відверто очікував би на FAQ, як використовувати масиви в C ++, щоб сказати: "Наразі ви можете почати розглядати просто, ну, не використовувати їх".
einpoklum

Відповіді:


302

Масиви на рівні типу

Тип масиву позначається як , T[n]де Tце тип елемента і nє позитивним розмір , кількість елементів в масиві. Тип масиву - це тип продукту типу елемента та розміру. Якщо один або обидва ці інгредієнти відрізняються, ви отримуєте виразний тип:

#include <type_traits>

static_assert(!std::is_same<int[8], float[8]>::value, "distinct element type");
static_assert(!std::is_same<int[8],   int[9]>::value, "distinct size");

Зауважте, що розмір є частиною типу, тобто типи масивів різного розміру є несумісними типами, які абсолютно нічого спільного між собою не мають. sizeof(T[n])еквівалентно n * sizeof(T).

Розпад масиву-вказівника

Єдиний "зв'язок" між T[n]і T[m]полягає в тому, що обидва типи можуть неявно перетворюватися на T*, а результатом цього перетворення є вказівник на перший елемент масиву. Тобто, де завгодно а T*, ви можете вказати T[n], і компілятор мовчки надасть цей покажчик:

                  +---+---+---+---+---+---+---+---+
the_actual_array: |   |   |   |   |   |   |   |   |   int[8]
                  +---+---+---+---+---+---+---+---+
                    ^
                    |
                    |
                    |
                    |  pointer_to_the_first_element   int*

Ця конверсія відома як "розпад масиву до вказівника", і вона є головним джерелом плутанини. Розмір масиву втрачається в цьому процесі, оскільки він більше не є частиною типу ( T*). Pro: Забуття розміру масиву на рівні типу дозволяє вказівнику вказувати на перший елемент масиву будь-якого розміру. Con: Даючи вказівник на перший (або будь-який інший) елемент масиву, немає способу визначити, наскільки великий цей масив або де саме вказівник вказує відносно меж масиву. Покажчики надзвичайно дурні .

Масиви не є покажчиками

Компілятор мовчки генерує вказівник на перший елемент масиву, коли це буде корисним, тобто коли операція не завершиться на масиві, але буде успішною на покажчику. Це перетворення з масиву в покажчик тривіальне, оскільки отримане значення вказівника є просто адресою масиву. Зверніть увагу, що вказівник не зберігається як частина самого масиву (або деінде в пам'яті).Масив - не вказівник.

static_assert(!std::is_same<int[8], int*>::value, "an array is not a pointer");

Одним з важливих контекстів, коли масив не впадає в покажчик на його перший елемент, - це коли &оператор застосовується до нього. У цьому випадку &оператор видає вказівник на весь масив, а не просто вказівник на його перший елемент. Хоча в цьому випадку значення (адреси) однакові, вказівник на перший елемент масиву і вказівник на весь масив є абсолютно різними типами:

static_assert(!std::is_same<int*, int(*)[8]>::value, "distinct element type");

Наступне мистецтво ASCII пояснює цю відмінність:

      +-----------------------------------+
      | +---+---+---+---+---+---+---+---+ |
+---> | |   |   |   |   |   |   |   |   | | int[8]
|     | +---+---+---+---+---+---+---+---+ |
|     +---^-------------------------------+
|         |
|         |
|         |
|         |  pointer_to_the_first_element   int*
|
|  pointer_to_the_entire_array              int(*)[8]

Зверніть увагу, як вказівник на перший елемент вказує лише на одне ціле число (зображене як невелике поле), тоді як вказівник на весь масив вказує на масив з 8 цілих чисел (зображений як велике поле).

Така ж ситуація виникає в класах і, можливо, є більш очевидною. Вказівник на об’єкт і вказівник на його першого члена даних однакові значення (однакова адреса), проте вони є абсолютно різними типами.

Якщо вам невідомий синтаксис декларатора C, ключові дужки в цьому типі int(*)[8]є важливими:

  • int(*)[8] є вказівником на масив з 8 цілих чисел.
  • int*[8]це масив з 8 покажчиків, кожен елемент типу int*.

Доступ до елементів

C ++ забезпечує дві синтаксичні варіанти для доступу до окремих елементів масиву. Жоден з них не перевершує інших, і вам слід ознайомитись з обома.

Арифметика вказівника

Давши вказівник pна перший елемент масиву, вираз p+iдає вказівник на i-й елемент масиву. Після виведення цього покажчика можна отримати доступ до окремих елементів:

std::cout << *(x+3) << ", " << *(x+7) << std::endl;

Якщо xпозначає масив , тоді розпочнеться розпад масиву-вказівника, оскільки додавання масиву та цілого числа є безглуздим (немає операцій з плюсами на масивах), але додавання вказівника та цілого числа має сенс:

   +---+---+---+---+---+---+---+---+
x: |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
     |           |               |
x+0  |      x+3  |          x+7  |     int*

(Зауважте, що неявно генерований покажчик не має імені, тому я написав x+0 для того, щоб його ідентифікувати.)

Якщо, з іншого боку, xпозначає вказівник на перший (або будь-який інший) елемент масиву, то розпад масиву вказівник не потрібен, тому що вказівник, на який iбуде додано, вже існує:

   +---+---+---+---+---+---+---+---+
   |   |   |   |   |   |   |   |   |   int[8]
   +---+---+---+---+---+---+---+---+
     ^           ^               ^
     |           |               |
     |           |               |
   +-|-+         |               |
x: | | |    x+3  |          x+7  |     int*
   +---+

Зауважте, що у зображеному випадку x- це змінна вказівник (помітна за невеликим полем поруч x), але вона також може бути результатом функції, що повертає вказівник (або будь-який інший вираз типу T*).

Оператор індексації

Оскільки синтаксис *(x+i)трохи незграбний, C ++ надає альтернативний синтаксис x[i]:

std::cout << x[3] << ", " << x[7] << std::endl;

Через те, що додавання є комутативним, наступний код робить точно так само:

std::cout << 3[x] << ", " << 7[x] << std::endl;

Визначення оператора індексації призводить до такої цікавої еквівалентності:

&x[i]  ==  &*(x+i)  ==  x+i

Однак, &x[0]як правило, не рівнозначно x. Перший - вказівник, другий - масив. Тільки тоді, коли контекст запускає розпад масиву-вказівника, можна xі &x[0]використовувати його взаємозамінно. Наприклад:

T* p = &array[0];  // rewritten as &*(array+0), decay happens due to the addition
T* q = array;      // decay happens due to the assignment

У першому рядку компілятор виявляє призначення від вказівника до вказівника, що тривіально вдається. У другому рядку він виявляє призначення з масиву вказівнику. Оскільки це безглуздо (але призначення вказівника на сенс має сенс), розпад масиву-вказівника починається як завжди.

Діапазони

Масив типу T[n]містить nелементи, індексовані від 0до n-1; немає елемента n. І все ж, для підтримки напіввідкритих діапазонів (де початок є включним, а кінець виключним ), C ++ дозволяє обчислювати вказівник на (неіснуючий) n-й елемент, але забороняється перенапрягувати цей покажчик:

   +---+---+---+---+---+---+---+---+....
x: |   |   |   |   |   |   |   |   |   .   int[8]
   +---+---+---+---+---+---+---+---+....
     ^                               ^
     |                               |
     |                               |
     |                               |
x+0  |                          x+8  |     int*

Наприклад, якщо ви хочете сортувати масив, обидва наступні будуть однаково добре працювати:

std::sort(x + 0, x + n);
std::sort(&x[0], &x[0] + n);

Зауважте, що подавати &x[n]як другий аргумент незаконно, оскільки це еквівалентно &*(x+n), а *(x+n)підвираз технічно викликається невизначене поведінку в C ++ (але не в C99).

Також зауважте, що ви можете просто навести xяк перший аргумент. Це трохи надто лаконічно на мій смак, і це також робить виведення аргументів шаблону трохи складніше для компілятора, оскільки в цьому випадку перший аргумент - це масив, а другий аргумент - покажчик. (Знову починається розпад масиву-вказівника.)


Тут для довідки проілюстровані випадки, коли масив не розпадається на покажчик .
legends2k

@fredoverflow У частині доступу або діапазонів, можливо, варто згадати, що C-масиви працюють з циклами C ++ 11 для циклів.
gnzlbg

135

Програмісти часто плутають багатовимірні масиви з масивами покажчиків.

Багатовимірні масиви

Більшість програмістів знайомі з названими багатовимірними масивами, але багато хто не знає про те, що багатовимірний масив можна також створювати анонімно. Багатовимірні масиви часто називають "масивами масивів" або " істинними" багатовимірними масивами".

Названі багатовимірні масиви

Використовуючи названі багатовимірні масиви, всі розміри повинні бути відомі під час компіляції:

int H = read_int();
int W = read_int();

int connect_four[6][7];   // okay

int connect_four[H][7];   // ISO C++ forbids variable length array
int connect_four[6][W];   // ISO C++ forbids variable length array
int connect_four[H][W];   // ISO C++ forbids variable length array

Ось так виглядає в пам'яті названий багатовимірний масив:

              +---+---+---+---+---+---+---+
connect_four: |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+
              |   |   |   |   |   |   |   |
              +---+---+---+---+---+---+---+

Зауважте, що 2D-сітки, як описано вище, є лише корисними візуалізаціями. З точки зору C ++, пам'ять - це "плоска" послідовність байтів. Елементи багатовимірного масиву зберігаються в основному рядку. Тобто connect_four[0][6]і connect_four[1][0]є сусідами по пам’яті. Насправді connect_four[0][7]і connect_four[1][0]позначимо той самий елемент! Це означає, що ви можете приймати багатовимірні масиви і обробляти їх як великі одновимірні масиви:

int* p = &connect_four[0][0];
int* q = p + 42;
some_int_sequence_algorithm(p, q);

Анонімні багатовимірні масиви

Для анонімних багатовимірних масивів усі параметри, крім першого, повинні бути відомі під час компіляції:

int (*p)[7] = new int[6][7];   // okay
int (*p)[7] = new int[H][7];   // okay

int (*p)[W] = new int[6][W];   // ISO C++ forbids variable length array
int (*p)[W] = new int[H][W];   // ISO C++ forbids variable length array

Ось як виглядає анонімний багатовимірний масив у пам'яті:

              +---+---+---+---+---+---+---+
        +---> |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |     |   |   |   |   |   |   |   |
        |     +---+---+---+---+---+---+---+
        |
      +-|-+
   p: | | |
      +---+

Зауважте, що сам масив все ще виділяється як єдиний блок в пам'яті.

Масиви покажчиків

Ви можете подолати обмеження фіксованої ширини, ввівши інший рівень непрямості.

Названі масиви вказівників

Ось іменований масив з п'яти покажчиків, які ініціалізовані анонімними масивами різної довжини:

int* triangle[5];
for (int i = 0; i < 5; ++i)
{
    triangle[i] = new int[5 - i];
}

// ...

for (int i = 0; i < 5; ++i)
{
    delete[] triangle[i];
}

І ось як це виглядає в пам'яті:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
triangle: | | | | | | | | | | |
          +---+---+---+---+---+

Оскільки зараз кожен рядок виділяється індивідуально, перегляд 2D масивів як 1D масивів більше не працює.

Анонімні масиви покажчиків

Ось анонімний масив з 5 (або будь-якої іншої кількості) покажчиків, ініціалізованих з анонімними масивами різної довжини:

int n = calculate_five();   // or any other number
int** p = new int*[n];
for (int i = 0; i < n; ++i)
{
    p[i] = new int[n - i];
}

// ...

for (int i = 0; i < n; ++i)
{
    delete[] p[i];
}
delete[] p;   // note the extra delete[] !

І ось як це виглядає в пам'яті:

          +---+---+---+---+---+
          |   |   |   |   |   |
          +---+---+---+---+---+
            ^
            | +---+---+---+---+
            | |   |   |   |   |
            | +---+---+---+---+
            |   ^
            |   | +---+---+---+
            |   | |   |   |   |
            |   | +---+---+---+
            |   |   ^
            |   |   | +---+---+
            |   |   | |   |   |
            |   |   | +---+---+
            |   |   |   ^
            |   |   |   | +---+
            |   |   |   | |   |
            |   |   |   | +---+
            |   |   |   |   ^
            |   |   |   |   |
            |   |   |   |   |
          +-|-+-|-+-|-+-|-+-|-+
          | | | | | | | | | | |
          +---+---+---+---+---+
            ^
            |
            |
          +-|-+
       p: | | |
          +---+

Конверсії

Розпад масиву вказівник природно поширюється на масиви масивів та масивів покажчиків:

int array_of_arrays[6][7];
int (*pointer_to_array)[7] = array_of_arrays;

int* array_of_pointers[6];
int** pointer_to_pointer = array_of_pointers;

Однак неявного перетворення з T[h][w]на T**. Якщо таке неявне перетворення існувало, результатом було б вказівник на перший елемент масиву hпокажчиків на T(кожен вказує на перший елемент рядка в початковому 2D масиві), але цей масив вказівників не існує ніде в пам'ять поки. Якщо ви хочете такого перетворення, ви повинні створити та заповнити необхідний масив вказівника вручну:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = connect_four[i];
}

// ...

delete[] p;

Зауважте, що це створює подання вихідного багатовимірного масиву. Якщо вам потрібна копія, потрібно створити зайві масиви та скопіювати дані самостійно:

int connect_four[6][7];

int** p = new int*[6];
for (int i = 0; i < 6; ++i)
{
    p[i] = new int[7];
    std::copy(connect_four[i], connect_four[i + 1], p[i]);
}

// ...

for (int i = 0; i < 6; ++i)
{
    delete[] p[i];
}
delete[] p;

Як пропозиція: Ви повинні зазначити, що int connect_four[H][7];, int connect_four[6][W]; int connect_four[H][W];як int (*p)[W] = new int[6][W];і int (*p)[W] = new int[H][W];є дійсними твердженнями, коли Hі Wвідомі під час компіляції.
RobertS підтримує Моніку Селліо

88

Призначення

Без будь-якої конкретної причини масиви не можуть бути призначені один одному. Використовуйте std::copyзамість цього:

#include <algorithm>

// ...

int a[8] = {2, 3, 5, 7, 11, 13, 17, 19};
int b[8];
std::copy(a + 0, a + 8, b);

Це більш гнучко, ніж те, що може надати справжнє призначення масиву, оскільки можна копіювати фрагменти великих масивів у менші масиви. std::copyзазвичай спеціалізується на примітивних типах, щоб забезпечити максимальну продуктивність. Навряд чи це std::memcpyкраще. Якщо сумніваєтесь, міряйте.

Хоча ви не можете призначити масиви безпосередньо, ви можете призначити структури та класи, які містять членів масиву. Це пояснюється тим, що члени масиву копіюються в долоні оператором призначення, який надається компілятором за замовчуванням. Якщо ви визначаєте оператор присвоєння вручну для власних типів структури або класів, ви повинні перейти до ручного копіювання для членів масиву.

Параметр проходження

Масиви не можуть передаватися за значенням. Ви можете передати їх за вказівником або за посиланням.

Пройти повз вказівник

Оскільки самі масиви не можуть бути передані за значенням, зазвичай вказівник на їх перший елемент передається замість цього значення. Це часто називають "пройти повз". Оскільки розмір масиву неможливо отримати за допомогою цього вказівника, вам слід передати другий параметр, який вказує розмір масиву (класичне рішення C) або другий вказівник, що вказує після останнього елемента масиву (рішення ітератора C ++) :

#include <numeric>
#include <cstddef>

int sum(const int* p, std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

int sum(const int* p, const int* q)
{
    return std::accumulate(p, q, 0);
}

Як синтаксична альтернатива, ви також можете оголосити параметри як T p[], і це означає абсолютно те саме, що і T* p в контексті списків параметрів :

int sum(const int p[], std::size_t n)
{
    return std::accumulate(p, p + n, 0);
}

Ви можете вважати компілятор як перезапис T p[]лише T *p в контексті списків параметрів . Це спеціальне правило частково відповідає за всю плутанину щодо масивів та покажчиків. У будь-якому іншому контексті оголошення про щось як масив або як вказівник робить величезним зміну.

На жаль, ви також можете надати розмір у параметрі масиву, який компілятор мовчки ігнорує. Тобто наступні три підписи є рівнозначними, як вказують помилки компілятора:

int sum(const int* p, std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[], std::size_t n)

// error: redefinition of 'int sum(const int*, size_t)'
int sum(const int p[8], std::size_t n)   // the 8 has no meaning here

Перейти за посиланням

Масиви також можуть передаватися за посиланням:

int sum(const int (&a)[8])
{
    return std::accumulate(a + 0, a + 8, 0);
}

У цьому випадку розмір масиву є значним. Оскільки написання функції, яка приймає лише масиви з точно 8 елементів, малокорисна, програмісти зазвичай записують такі функції, як шаблони:

template <std::size_t n>
int sum(const int (&a)[n])
{
    return std::accumulate(a + 0, a + n, 0);
}

Зауважте, що ви можете викликати такий шаблон функції лише з фактичним масивом цілих чисел, а не з вказівником на ціле число. Розмір масиву автоматично виводиться, і для кожного розміру nінстанціюється інша функція від шаблону. Ви також можете написати досить корисні шаблони функцій, які абстрагуються як від типу елемента, так і від розміру.


2
Можливо, варто додати зауваження, що навіть tho в void foo(int a[3]) aдійсно схожий на те, що один передає масив за значенням, модифікація aвсередині fooбуде модифікувати вихідний масив. Це повинно бути зрозуміло, оскільки масиви неможливо скопіювати, але, можливо, варто посилити це.
gnzlbg

C ++ 20 маєranges::copy(a, b)
LF

int sum( int size_, int a[size_]);- від (я думаю) C99 і далі
Chef Gladiator

73

5. Поширені підводні камені при використанні масивів.

5.1 Провалля: довіряти небезпечному типу.

Гаразд, вам сказали або ви самі зрозуміли, що глобальні дані (змінні області простору імен, до яких можна отримати доступ поза блоком перекладу) - це Evil ™. Але чи знали ви, наскільки справді вони Evil ™? Розглянемо програму нижче, що складається з двох файлів [main.cpp] і [numbers.cpp]:

// [main.cpp]
#include <iostream>

extern int* numbers;

int main()
{
    using namespace std;
    for( int i = 0;  i < 42;  ++i )
    {
        cout << (i > 0? ", " : "") << numbers[i];
    }
    cout << endl;
}

// [numbers.cpp]
int numbers[42] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

У Windows 7 це компілюється і добре пов'язується як з MinGW g ++ 4.4.1, так і з Visual C ++ 10.0.

Оскільки типи не відповідають, програма виходить з ладу під час її запуску.

Діалогове вікно аварії Windows 7

Формальне пояснення: програма має не визначене поведінку (UB), і замість збоїв вона може просто зависати, а може й нічого не робити, або може надсилати загрозливі електронні листи президентам США, Росії, Індії, Китай та Швейцарія, і змушують назальні демони вилітати з носа.

На практиці пояснення: у main.cppмасиві трактується як вказівник, розміщений за тією ж адресою, що і масив. Для 32-бітного виконуваного файлу це означає, що перше intзначення в масиві трактується як вказівник. Тобто, в змінний містить або містить , як видається, . Це змушує програму отримати доступ до пам'яті внизу адресного простору, що умовно зарезервовано і викликає пастку. Результат: ви отримаєте збій.main.cppnumbers(int*)1

Укладачі повністю належать до своїх прав не діагностувати цю помилку, оскільки C ++ 11 §3.5 / 10 говорить про вимогу сумісних типів для декларацій,

[N3290 §3.5 / 10]
Порушення цього правила щодо ідентичності типу не потребує діагностики.

У цьому ж абзаці описано дозволену варіацію:

… Декларації для об’єкта масиву можуть визначати типи масивів, які відрізняються наявністю або відсутністю основного зв'язаного масиву (8.3.4).

Цей дозволений варіант не включає оголошення імені як масиву в одній одиниці перекладу, а як вказівника в іншому блоці перекладу.

5.2 Проблема: передчасна оптимізація ( memsetта друзі).

Ще не написано

5.3 Підводний камінь: Використання ідіоми C для отримання кількості елементів.

З глибоким досвідом роботи з C цілком природно писати…

#define N_ITEMS( array )   (sizeof( array )/sizeof( array[0] ))

Оскільки arrayрозкладається вказівник на перший елемент, де це потрібно, вираз sizeof(a)/sizeof(a[0])також можна записати як sizeof(a)/sizeof(*a). Це означає те саме, і як би це не було написано, це ідіома C для пошуку числових елементів масиву.

Основна проблема: ідіома С не є безпечною. Наприклад, код ...

#include <stdio.h>

#define N_ITEMS( array ) (sizeof( array )/sizeof( *array ))

void display( int const a[7] )
{
    int const   n = N_ITEMS( a );          // Oops.
    printf( "%d elements.\n", n );
}

int main()
{
    int const   moohaha[]   = {1, 2, 3, 4, 5, 6, 7};

    printf( "%d elements, calling display...\n", N_ITEMS( moohaha ) );
    display( moohaha );
}

передає вказівник на N_ITEMS, а тому, швидше за все, дає неправильний результат. Скомпільований як 32-бітний виконуваний файл у Windows 7, він створює…

7 елементів, виклик дисплея ...
1 елемент.

  1. Компілятор переписує int const a[7]просто int const a[].
  2. Компілятор переписується int const a[]на int const* a.
  3. N_ITEMS тому викликається вказівником.
  4. Для 32-бітного виконуваного файлу sizeof(array)(розмір вказівника) тоді 4.
  5. sizeof(*array)еквівалентний sizeof(int), що для 32-бітного виконуваного файлу також дорівнює 4.

Щоб виявити цю помилку під час виконання, ви можете зробити…

#include <assert.h>
#include <typeinfo>

#define N_ITEMS( array )       (                               \
    assert((                                                    \
        "N_ITEMS requires an actual array as argument",        \
        typeid( array ) != typeid( &*array )                    \
        )),                                                     \
    sizeof( array )/sizeof( *array )                            \
    )

7 елементів, виклик дисплея ...
Твердження не вдалося: ("N_ITEMS вимагає фактичного масиву як аргумент", typeid (a)! = Typeid (& * a)), файл runtime_detect ion.cpp, рядок 16

Ця програма попросила Runtime припинити її незвичним чином.
Для отримання додаткової інформації зверніться до служби підтримки програми.

Виявлення помилок під час виконання краще, ніж виявлення, але це витрачає небагато часу на процесор і, можливо, набагато більше часу програміста. Краще з виявленням під час компіляції! І якщо ви раді не підтримувати масиви локальних типів з C ++ 98, ви можете зробити це:

#include <stddef.h>

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

#define N_ITEMS( array )       n_items( array )

Складаючи це визначення, замінене в першій повній програмі, з g ++, я отримав…

M: \ count> g ++ compile_time_detection.cpp
compile_time_detection.cpp: У функції 'void display (const int *)':
compile_time_detection.cpp: 14: помилка: немає функції узгодження для виклику 'n_items (const int * &)'

M: \ count> _

Як це працює: масив передається за посиланням на n_items, і тому він не затухає, щоб вказувати на перший елемент, а функція може просто повернути кількість елементів, визначених типом.

З C ++ 11 ви можете використовувати це також для масивів локального типу, і це безпечна ідіома типу C ++ для пошуку кількості елементів масиву.

5,4 C ++ 11 & C ++ 14 підводний камінь: Використання constexprфункції розміру масиву.

З C ++ 11 і пізнішими версіями це природно, але як ви побачите небезпечне! - замінити функцію C ++ 03

typedef ptrdiff_t   Size;

template< class Type, Size n >
Size n_items( Type (&)[n] ) { return n; }

з

using Size = ptrdiff_t;

template< class Type, Size n >
constexpr auto n_items( Type (&)[n] ) -> Size { return n; }

де суттєвою зміною є використання constexpr, що дозволяє цій функції виробляти константу часу компіляції .

Наприклад, на відміну від функції C ++ 03, така константа часу компіляції може використовуватися для оголошення масиву того ж розміру, що й інший:

// Example 1
void foo()
{
    int const x[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 4};
    constexpr Size n = n_items( x );
    int y[n] = {};
    // Using y here.
}

Але врахуйте цей код за допомогою constexprверсії:

// Example 2
template< class Collection >
void foo( Collection const& c )
{
    constexpr int n = n_items( c );     // Not in C++14!
    // Use c here
}

auto main() -> int
{
    int x[42];
    foo( x );
}

Проблема: станом на липень 2015 року вищезгадані компіляції з MinGW-64 5.1.0 з -pedantic-errors, та, тестування з онлайн-компіляторами на gcc.godbolt.org/ , також з clang 3.0 та clang 3.2, але не з clang 3.3, 3.4. 1, 3.5.0, 3.5.1, 3.6 (rc1) або 3.7 (експериментальний). І важливо для платформи Windows, вона не компілюється з Visual C ++ 2015. Причиною є заява C ++ 11 / C ++ 14 про використання посилань у constexprвиразах:

C ++ 11 C ++ 14 $ 5,19 / 2 дев'ять го тиру

Умовний вираз e є виразом постійна сердечника , якщо тільки оцінки e, слідуючи правила абстрактної машини (1.9), буде оцінювати одне з наступних виразів:
        ⋮

  • ID-вираз , яке відноситься до елементу або змінних даних довідкового типу , якщо посилання не має попередню ініціалізацію і або
    • вона ініціалізується постійним виразом або
    • це нестатичний член даних об'єкта, час життя якого розпочався в рамках оцінки e;

Завжди можна написати більш багатослівний

// Example 3  --  limited

using Size = ptrdiff_t;

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = std::extent< decltype( c ) >::value;
    // Use c here
}

… Але це не вдається, коли Collectionце не необроблений масив.

Для роботи з колекціями, які можуть бути без масивів, потрібна можливість перезавантаження n_itemsфункції, але також для використання часу компіляції потрібно представлення часу компіляції розміру масиву. Класичне рішення C ++ 03, яке добре працює і в C ++ 11 і C ++ 14, полягає в тому, щоб функція повідомляла про свій результат не як значення, а через тип результату функції . Наприклад так:

// Example 4 - OK (not ideal, but portable and safe)

#include <array>
#include <stddef.h>

using Size = ptrdiff_t;

template< Size n >
struct Size_carrier
{
    char sizer[n];
};

template< class Type, Size n >
auto static_n_items( Type (&)[n] )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

template< class Type, size_t n >        // size_t for g++
auto static_n_items( std::array<Type, n> const& )
    -> Size_carrier<n>;
// No implementation, is used only at compile time.

#define STATIC_N_ITEMS( c ) \
    static_cast<Size>( sizeof( static_n_items( c ).sizer ) )

template< class Collection >
void foo( Collection const& c )
{
    constexpr Size n = STATIC_N_ITEMS( c );
    // Use c here
    (void) c;
}

auto main() -> int
{
    int x[42];
    std::array<int, 43> y;
    foo( x );
    foo( y );
}

Щодо вибору типу повернення для static_n_items: цей код не використовується, std::integral_constant оскільки std::integral_constantрезультат відображається безпосередньо як constexprзначення, повторно вводячи початкову проблему. Замість Size_carrierкласу можна дозволити функції безпосередньо повернути посилання на масив. Однак не всі знайомі з цим синтаксисом.

Про іменування: частина цього рішення проблеми constexpr-invalid-due-to-reference полягає в тому, щоб зробити вибір часу постійної компіляції явним.

Будемо сподіватися, що constexprвипуск "oops-there-was-a-reference" - буде зафіксовано за допомогою C ++ 17, але до цього часу макрос, як STATIC_N_ITEMSописано вище, забезпечує портативність, наприклад, до компіляторів clang і Visual C ++, зберігаючи тип безпека.

Пов’язано: макроси не поважають області застосування, тому, щоб уникнути зіткнень з іменами, може бути хорошою ідеєю використовувати префікс імені, наприклад MYLIB_STATIC_N_ITEMS.


1
+1 Великий тест кодування C: Я витратив 15 хвилин на VC ++ 10.0 та GCC 4.1.2, намагаючись виправити Segmentation fault... Я нарешті знайшов / зрозумів, прочитавши ваші пояснення! Будь ласка, напишіть свій розділ §5.2 :-) Ура
олибре

Добре. Один нит - тип повернення для countOf повинен бути size_t замість ptrdiff_t. Напевно, варто згадати, що в C ++ 11/14 він повинен бути constexpr і noexcept.
Ricky65

@ Ricky65: Дякую, що згадуєш C ++ 11 міркувань. Підтримка цих функцій запізнилася на отримання Visual C ++. Щодо того size_t, це не має жодних переваг, про які я знаю для сучасних платформ, але у нього є ряд проблем через неявні правила перетворення типу C і C ++. Тобто ptrdiff_tвикористовується дуже навмисно, щоб уникнути проблем з size_t. Однак слід пам’ятати, що g ++ має проблеми зі збігом розміру масиву до параметра шаблону, якщо це не є size_t(я не думаю, що ця проблема компілятора з не size_tважливою, але YMMV).
ура та хт. - Альф

@Alf. У стандартному робочому проекті (N3936) 8.3.4 я читаю - Обмежений масив - це ... "перетворений постійний вираз типу std :: size_t і його значення повинно бути більше нуля".
Ricky65

@ Ricky: Якщо ви маєте на увазі невідповідність, цього твердження немає в поточному стандарті C ++ 11, тому важко відгадати контекст, але суперечність (динамічно розподілений масив може бути пов'язаний 0, за C + +11 §5.3.4 / 7), ймовірно, не закінчиться в C ++ 14. Чернетки - це лише те, що чернетки. Якщо ви натомість запитуєте про те, на що посилається "його", це стосується оригінального виразу, а не перетвореного. Якщо ви з третьої сторони згадуєте про це, бо думаєте, що, можливо, таке речення означає, що потрібно використовувати size_tдля позначення розмірів масивів, звичайно, це не так.
ура та хт. - Альф

72

Створення та ініціалізація масиву

Як і в будь-якому іншому об'єкті C ++, масиви можуть зберігатися або безпосередньо в названих змінних (тоді розмір повинен бути константою часу компіляції; C ++ не підтримує VLA ), або вони можуть зберігатись анонімно у купі та отримувати доступ до них опосередковано через покажчики (лише тоді розмір можна обчислити під час виконання).

Автоматичні масиви

Автоматичні масиви (масиви, що живуть "у стеці") створюються кожного разу, коли потік управління проходить через визначення нестатичної змінної локального масиву:

void foo()
{
    int automatic_array[8];
}

Ініціалізація виконується у порядку зростання. Зауважте, що початкові значення залежать від типу елемента T:

  • Якщо Tє POD (як intу наведеному вище прикладі), ініціалізація не відбувається.
  • В іншому випадку конструктор за замовчуванням Tініціалізує всі елементи.
  • Якщо Tнемає доступного конструктора за замовчуванням, програма не компілюється.

Альтернативно, початкові значення можуть бути явно вказані в ініціалізаторі масиву , розділеному комами списком, оточеним фігурними дужками:

    int primes[8] = {2, 3, 5, 7, 11, 13, 17, 19};

Оскільки в цьому випадку кількість елементів ініціалізатора масиву дорівнює розміру масиву, то вказівка ​​розміру вручну є зайвою. Він може автоматично зробити висновок компілятором:

    int primes[] = {2, 3, 5, 7, 11, 13, 17, 19};   // size 8 is deduced

Також можна вказати розмір та надати ініціалізатор коротшого масиву:

    int fibonacci[50] = {0, 1, 1};   // 47 trailing zeros are deduced

У цьому випадку решта елементів ініціалізуються нулем . Зауважте, що C ++ дозволяє пустити ініціалізатор порожнього масиву (усі елементи ініціалізовані нулем), тоді як C89 - ні (принаймні одне значення потрібно). Також зауважте, що ініціалізатори масивів можуть використовуватися лише для ініціалізації масивів; пізніше їх не можна використовувати в завданнях.

Статичні масиви

Статичні масиви (масиви, що живуть "у сегменті даних") - це локальні змінні масиви, визначені змінними staticключового слова та масиву в області простору імен ("глобальні змінні"):

int global_static_array[8];

void foo()
{
    static int local_static_array[8];
}

(Зверніть увагу, що змінні в області простору імен неявно статичні. Додавання staticключового слова до їх визначення має зовсім інше, застаріле значення .)

Ось як статичні масиви поводяться по-різному від автоматичних масивів:

  • Статичні масиви без ініціалізатора масиву ініціалізуються нулем до будь-якої подальшої потенціальної ініціалізації.
  • Статичні масиви POD ініціалізуються рівно один раз , і початкові значення, як правило, записуються у виконуваний файл, і в цьому випадку під час виконання немає витрат на ініціалізацію. Однак це не завжди є найбільш ефективним для простору рішенням, і це не вимагається стандартом.
  • Статичні масиви без ПДД ініціалізуються вперше, коли потік управління проходить через їх визначення. У випадку локальних статичних масивів це може ніколи не статися, якщо функція ніколи не викликається.

(Жодне з перерахованого вище не стосується масивів. Ці правила однаково добре застосовуються і до інших видів статичних об'єктів.)

Масив даних членів

Учасники даних масиву створюються, коли створюється власний об'єкт. На жаль, C ++ 03 не дає можливості ініціалізувати масиви у списку ініціалізаторів членів , тому ініціалізація повинна бути підробленою з призначеннями:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        primes[0] = 2;
        primes[1] = 3;
        primes[2] = 5;
        // ...
    }
};

Крім того, ви можете визначити автоматичний масив у тілі конструктора і скопіювати елементи на:

class Foo
{
    int primes[8];

public:

    Foo()
    {
        int local_array[] = {2, 3, 5, 7, 11, 13, 17, 19};
        std::copy(local_array + 0, local_array + 8, primes + 0);
    }
};

У C ++ 0x масиви можна ініціалізувати у списку ініціалізаторів членів завдяки рівномірній ініціалізації :

class Foo
{
    int primes[8];

public:

    Foo() : primes { 2, 3, 5, 7, 11, 13, 17, 19 }
    {
    }
};

Це єдине рішення, яке працює з типами елементів, у яких немає конструктора за замовчуванням.

Динамічні масиви

Динамічні масиви не мають імен, тому єдиний засіб доступу до них - через покажчики. Оскільки у них немає імен, відтепер я буду називати їх "анонімними масивами".

У C створюються анонімні масиви через mallocта друзів. У C ++ анонімні масиви створюються за допомогою new T[size]синтаксису, який повертає вказівник на перший елемент анонімного масиву:

std::size_t size = compute_size_at_runtime();
int* p = new int[size];

Наступне мистецтво ASCII зображує макет пам'яті, якщо розмір обчислюється як 8 під час виконання:

             +---+---+---+---+---+---+---+---+
(anonymous)  |   |   |   |   |   |   |   |   |
             +---+---+---+---+---+---+---+---+
               ^
               |
               |
             +-|-+
          p: | | |                               int*
             +---+

Очевидно, що анонімним масивам потрібно більше пам'яті, ніж іменовані масиви, завдяки додатковому вказівнику, який потрібно зберігати окремо. (У вільному магазині також є додаткові накладні витрати.)

Зауважте, що тут не відбувається розпад масиву-вказівника. Хоча оцінка new int[size]насправді створює масив цілих чисел, результат виразу new int[size]- це вже вказівник на одне ціле число (перший елемент), а не масив цілих чисел або вказівник на масив цілих чисел невідомого розміру. Це було б неможливо, оскільки система статичного типу вимагає, щоб розміри масивів були константами часу компіляції. (Отже, я не коментував анонімний масив зі статичною інформацією про тип зображення.)

Щодо значень за замовчуванням для елементів, анонімні масиви поводяться аналогічно автоматичним масивам. Зазвичай анонімні масиви POD не ініціалізуються, але існує спеціальний синтаксис, який запускає ініціалізацію значень:

int* p = new int[some_computed_size]();

(Зверніть увагу на кінцеву пару дужок перед крапкою з комою.) Знову ж, C ++ 0x спрощує правила та дозволяє вказати початкові значення для анонімних масивів завдяки рівномірній ініціалізації:

int* p = new int[8] { 2, 3, 5, 7, 11, 13, 17, 19 };

Якщо ви працюєте з анонімним масивом, вам доведеться випустити його назад в систему:

delete[] p;

Ви повинні випустити кожен анонімний масив рівно один раз, а потім більше ніколи не торкатися його. Якщо не звільняти його, це призводить до витоку пам’яті (або загалом, залежно від типу елемента, витоку ресурсу), а спроба випустити його кілька разів призводить до невизначеної поведінки. Використання форми delete(або free) без масиву замість delete[]звільнення масиву також є невизначеним поведінкою .


2
Застарення staticвикористання в області простору імен було вилучено в C ++ 11.
legends2k

Оскільки newя є оператором, він, безумовно, може повернути все знайдений масив за посиланням. Тут просто немає сенсу ...
Дедуплікатор

@Deduplicator Ніхто не міг, оскільки історично newнабагато старший за посилання.
fredoverflow

@FredOverflow: Отже, є причина, по якій він не міг повернути посилання, він просто зовсім відрізняється від письмового пояснення.
Дедуплікатор

2
@Deduplicator Я не думаю, що існує посилання на масив невідомих меж. Принаймні g ++ відмовляється компілюватиint a[10]; int (&r)[] = a;
fredoverflow
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.