Ініціалізація всіх елементів масиву до одного значення за замовчуванням у C ++?


248

Примітки C ++: Ініціалізація масиву містить хороший список щодо ініціалізації масивів. Я маю

int array[100] = {-1};

очікуючи, що він буде повним -1, але його немає, тільки перше значення є, а решта 0 змішані зі випадковими значеннями.

Код

int array[100] = {0};

працює просто чудово і встановлює кожен елемент 0.

Що мені тут не вистачає? Не можна його ініціалізувати, якщо значення не дорівнює нулю?

І 2: Чи ініціалізація за замовчуванням (як зазначено вище) швидше звичайного циклу через весь масив і присвоює значення чи робить це те ж саме?


1
Поведінка у С та С ++ різна. У C {0} - особливий випадок для ініціалізатора структури, проте AFAIK не для масивів. int масив [100] = {0} повинен бути таким же, як масив [100] = {[0] = 0}, який як побічний ефект скасує всі інші елементи. Компілятор змінного струму НЕ повинен вести себе так, як описано вище, натомість int масив [100] = {- 1} повинен встановити перший елемент на -1, а решту на 0 (без шуму). У C, якщо у вас є масив struct x [100], використання = {0} як ініціалізатора НЕ припустимо. Ви можете використовувати {{0}}, який буде ініціалізувати перший елемент і нулювати всі інші, в більшості випадків це буде те саме.
Фредрік Відлунд

1
@FredrikWidlund Це однаково в обох мовах. {0}не є особливим випадком для конструкцій і масивів. Правило полягає в тому, що елементи без ініціалізатора ініціалізуються так, ніби вони були 0для ініціалізатора. Якщо є вкладені агрегати (наприклад struct x array[100]), то ініціалізатори застосовуються до неагрегаторів у порядку "основний рядок"; підтяжки можуть додатково пропускати це. struct x array[100] = { 0 }діє в С; та дійсна в C ++ до тих пір, поки перший член struct Xприймає 0як ініціалізатор.
ММ

1
{ 0 }не є особливим для C, але набагато складніше визначити тип даних, який неможливо ініціалізувати з ним, оскільки немає конструкторів і, отже, немає можливості зупинитись 0від того, щоб неявно перетворити та призначити щось .
Левшенко

3
Проголосували за повторне відкриття, оскільки інше питання стосується C. Існує багато способів ініціалізації масиву C ++, недійсних у C.
xskxzr

1
Також проголосували за повторне відкриття - C та C ++ - це різні мови
Піт

Відповіді:


350

Використовуючи синтаксис, який ви використовували,

int array[100] = {-1};

говорить "встановити перший елемент на, -1а решта на 0", оскільки всі опущені елементи встановлені на 0.

У C ++, щоб встановити їх на всі -1, ви можете використовувати щось на зразок std::fill_n(від <algorithm>):

std::fill_n(array, 100, -1);

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


14
Це також відповіло на непряме запитання про те, як легко заповнити масив значеннями за замовчуванням. Дякую.
Мілан

7
@chessofnerd: не точно, #include <algorithm>це правильний заголовок, <vector>може або не може включати його опосередковано, що залежатиме від вашої реалізації.
Еван Теран

2
Не потрібно вдаватися до ініціалізації масиву під час виконання. Якщо вам справді потрібна ініціалізація статично, можливо використовувати різноманітні шаблони та варіативні послідовності для створення потрібної послідовності ints та розширення її в ініціалізатор масиву.
void-pointer

2
@ontherocks, немає, немає правильного способу використовувати один виклик, fill_nщоб заповнити весь 2D масив. Потрібно перевести цикл на один вимір, одночасно заповнивши інший.
Еван Теран

7
Це відповідь на якесь інше питання. std::fill_nне є ініціалізацією.
Бен Войгт

133

Існує розширення до компілятора gcc, який дозволяє синтаксису:

int array[100] = { [0 ... 99] = -1 };

Це встановило б усі елементи до -1.

Це відомо як "призначені ініціалізатори", див. Тут для отримання додаткової інформації.

Зверніть увагу: це не реалізовано для компілятора gcc c ++.


2
Дивовижно. Цей синтаксис також працює в кланге (тому його можна використовувати в iOS / Mac OS X).
Йосиф H

31

Сторінка, на яку ви пов’язали, вже дала відповідь у першій частині:

Якщо вказаний явний розмір масиву, але вказаний коротший список ініціалізації, неуточнені елементи встановлюються на нуль.

Не існує вбудованого способу ініціалізації всього масиву до якогось ненульового значення.

Що стосується того, що швидше, застосовується звичайне правило: "Метод, який дає компілятору найбільшу свободу, ймовірно, швидший".

int array[100] = {0};

просто каже компілятору "встановити ці 100 входів на нуль", які компілятор може вільно оптимізувати.

for (int i = 0; i < 100; ++i){
  array[i] = 0;
}

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

Нарешті, якщо ви хочете встановити масив на ненульове значення, вам слід (як мінімум, на C ++) використовувати std::fill:

std::fill(array, array+100, 42); // sets every value in the array to 42

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


5
Хороша відповідь. Зауважте, що в C ++ (не в C) ви можете робити масив int [100] = {}; і дайте компілятору найбільшу свободу :)
Йоханнес Шауб - ліб

1
погоджена, відмінна відповідь. Але для масиву фіксованого розміру він буде використовувати std :: fill_n :-P.
Еван Теран

12

У C ++ 11 є ще один (недосконалий) варіант:

std::array<int, 100> a;
a.fill(-1);

абоstd::fill(begin(a), end(a), -1)
doctorlai

9

За допомогою {} ви призначаєте елементи так, як вони оголошені; решта ініціалізується з 0.

Якщо немає необхідності = {}для ініціалізації, вміст не визначається.


8

Сторінка, на яку ви пов’язали, заявляє

Якщо вказаний явний розмір масиву, але вказаний коротший список ініціалізації, неуточнені елементи встановлюються на нуль.

Питання швидкості: Будь-які відмінності були б незначними для масивів, які є такими маленькими. Якщо ви працюєте з великими масивами, а швидкість набагато важливіша за розмір, ви можете мати масив const значень за замовчуванням (ініціалізований під час компіляції), а потім memcpyїх до масиву, який може змінюватися.


2
memcpy - це не дуже гарна ідея, оскільки це було б порівняно з просто встановленням значень, що мають пряму швидкість.
Еван Теран

1
Я не бачу необхідності в копії та const масиві: чому б не створити змінний масив в першу чергу з попередньо заповненими значеннями?
Йоханнес Шауб - ліб

Дякуємо за пояснення швидкості та як це зробити, якщо швидкість є проблемою з великим розміром масиву (який є в моєму випадку)
Мілан,

Список ініціалізатора робиться під час компіляції та завантажується під час виконання. Не потрібно копіювати речі.
Мартін Йорк

@litb, @Evan: Наприклад, gcc генерує динамічну ініціалізацію (багато Movs) навіть при ввімкненій оптимізації. Для великих масивів і жорстких вимог до продуктивності потрібно зробити init під час компіляції. memcpy, ймовірно, краще оптимізований для великих копій, ніж багато простих Movs.
laalto

4

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

#define DUP1( X ) ( X )
#define DUP2( X ) DUP1( X ), ( X )
#define DUP3( X ) DUP2( X ), ( X )
#define DUP4( X ) DUP3( X ), ( X )
#define DUP5( X ) DUP4( X ), ( X )
.
.
#define DUP100( X ) DUP99( X ), ( X )

#define DUPx( X, N ) DUP##N( X )
#define DUP( X, N ) DUPx( X, N )

Ініціалізувати масив до загального значення можна легко:

#define LIST_MAX 6
static unsigned char List[ LIST_MAX ]= { DUP( 123, LIST_MAX ) };

Примітка: DUPx введено для включення заміни макроконтролеру в параметрах на DUP


3

У випадку масиву однобайтових елементів ви можете використовувати memset для встановлення всіх елементів на одне значення.

Там приклад тут .


3

Використовуючи це std::array, ми можемо зробити це досить просто у C ++ 14. Це можна зробити лише в C ++ 11, але трохи складніше.

Наш інтерфейс - це розмір часу компіляції та значення за замовчуванням.

template<typename T>
constexpr auto make_array_n(std::integral_constant<std::size_t, 0>, T &&) {
    return std::array<std::decay_t<T>, 0>{};
}

template<std::size_t size, typename T>
constexpr auto make_array_n(std::integral_constant<std::size_t, size>, T && value) {
    return detail::make_array_n_impl<size>(std::forward<T>(value), std::make_index_sequence<size - 1>{});
}


template<std::size_t size, typename T>
constexpr auto make_array_n(T && value) {
    return make_array_n(std::integral_constant<std::size_t, size>{}, std::forward<T>(value));
}

Третя функція в основному для зручності, тому користувачеві не потрібно будувати std::integral_constant<std::size_t, size>самостійно, оскільки це досить багатослівна конструкція. Справжню роботу виконує одна з перших двох функцій.

Перше перевантаження досить просте: воно конструює std::arrayрозміром 0. Копіювання не потрібно, ми просто конструюємо його.

Друга перевантаження трохи складніше. Він передає значення, яке воно отримало як джерело, а також створює екземпляр make_index_sequenceі просто викликає якусь іншу функцію реалізації. Як виглядає ця функція?

namespace detail {

template<std::size_t size, typename T, std::size_t... indexes>
constexpr auto make_array_n_impl(T && value, std::index_sequence<indexes...>) {
    // Use the comma operator to expand the variadic pack
    // Move the last element in if possible. Order of evaluation is well-defined
    // for aggregate initialization, so there is no risk of copy-after-move
    return std::array<std::decay_t<T>, size>{ (static_cast<void>(indexes), value)..., std::forward<T>(value) };
}

}   // namespace detail

Це створює аргументи першого розміру - 1, копіюючи значення, яке ми передали. Тут ми використовуємо наші індекси змінних параметрів просто для розширення. У цьому пакеті розмір - 1 запис (як ми вказали при розробці make_index_sequence), і вони мають значення 0, 1, 2, 3, ..., розмір - 2. Однак ми не дбаємо про значення ( тому ми приводимо це до недійсного, щоб заглушити будь-які попередження компілятора). Розширення пакета параметрів розширює наш код приблизно таким чином (якщо вважати розмір == 4):

return std::array<std::decay_t<T>, 4>{ (static_cast<void>(0), value), (static_cast<void>(1), value), (static_cast<void>(2), value), std::forward<T>(value) };

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

Останній аргумент, той, до якого ми звертаємось std::forward, - незначна оптимізація. Якщо хтось передає тимчасовий рядок std :: і каже "зробіть масив із 5", ми хотіли б мати 4 копії та 1 хід, а не 5 копій. У std::forwardгарантує , що ми робимо це.

Повний код, включаючи заголовки та деякі тестові одиниці:

#include <array>
#include <type_traits>
#include <utility>

namespace detail {

template<std::size_t size, typename T, std::size_t... indexes>
constexpr auto make_array_n_impl(T && value, std::index_sequence<indexes...>) {
    // Use the comma operator to expand the variadic pack
    // Move the last element in if possible. Order of evaluation is well-defined
    // for aggregate initialization, so there is no risk of copy-after-move
    return std::array<std::decay_t<T>, size>{ (static_cast<void>(indexes), value)..., std::forward<T>(value) };
}

}   // namespace detail

template<typename T>
constexpr auto make_array_n(std::integral_constant<std::size_t, 0>, T &&) {
    return std::array<std::decay_t<T>, 0>{};
}

template<std::size_t size, typename T>
constexpr auto make_array_n(std::integral_constant<std::size_t, size>, T && value) {
    return detail::make_array_n_impl<size>(std::forward<T>(value), std::make_index_sequence<size - 1>{});
}

template<std::size_t size, typename T>
constexpr auto make_array_n(T && value) {
    return make_array_n(std::integral_constant<std::size_t, size>{}, std::forward<T>(value));
}



struct non_copyable {
    constexpr non_copyable() = default;
    constexpr non_copyable(non_copyable const &) = delete;
    constexpr non_copyable(non_copyable &&) = default;
};

int main() {
    constexpr auto array_n = make_array_n<6>(5);
    static_assert(std::is_same<std::decay_t<decltype(array_n)>::value_type, int>::value, "Incorrect type from make_array_n.");
    static_assert(array_n.size() == 6, "Incorrect size from make_array_n.");
    static_assert(array_n[3] == 5, "Incorrect values from make_array_n.");

    constexpr auto array_non_copyable = make_array_n<1>(non_copyable{});
    static_assert(array_non_copyable.size() == 1, "Incorrect array size of 1 for move-only types.");

    constexpr auto array_empty = make_array_n<0>(2);
    static_assert(array_empty.empty(), "Incorrect array size for empty array.");

    constexpr auto array_non_copyable_empty = make_array_n<0>(non_copyable{});
    static_assert(array_non_copyable_empty.empty(), "Incorrect array size for empty array of move-only.");
}

Ваш non_copyableтип фактично копіюється за допомогою operator=.
Герц

Я думаю, non_copy_constructibleбуло б точнішою назвою об’єкта. Однак у цьому коді ніде немає призначення, тому для цього прикладу це не має значення.
Девід Стоун

1

1) Коли ви використовуєте ініціалізатор для такої структури або масиву, невизначені значення по суті побудовані за замовчуванням. У випадку примітивного типу, наприклад ints, це означає, що вони будуть нульовими. Зауважте, що це застосовується рекурсивно: у вас може бути масив структур, що містить масиви, і якщо ви вкажете лише перше поле першої структури, то всі інші будуть ініціалізовані нулями та конструкторами за замовчуванням.

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


1) Ініціалізація за замовчуванням POD не відбувається тут. Використовуючи список, компілятор буде генерувати значення під час компіляції та розміщувати їх у спеціальному розділі асемблеї, який щойно завантажується як частина ініціалізації програми (наприклад, код). Тож вартість дорівнює нулю під час виконання.
Мартін Йорк

1
Я не бачу, де він помиляється? int a [100] = {}, безумовно, ініціалізується на всі 0, ігноруючи, де воно з'являється, і структура {int a; } b [100] = {}; занадто. "по суті побудовано за замовчуванням" => "побудовано значення", тхо. Але це не має значення у випадку ints, PODS або типів із оголошеними користувачем ctors. Це важливо лише для NON-Pods без оголошених користувачем ctors, наскільки я знаю. Але я не став би (!) Голосувати через це. у будь-якому разі, +1 для вас, щоб зробити це знову 0:)
Йоханнес Шауб - ліб

@Evan: Я визначив свою заяву "Коли ви використовуєте ініціалізатор ...", я не мав на увазі неініціалізовані значення. @Martin: Це може працювати для постійних, статичних або глобальних даних. Але я не бачу, як це могло б працювати з чимось на зразок: int test () {int i [10] = {0}; int v = i [0]; i [0] = 5; повернути v; } Компілятор краще ініціалізувати i [] до нулів щоразу, коли ви викликаєте test ().
Бужум

він може розмістити дані в статичний сегмент даних і зробити «я» посиланням на нього :)
Йоханнес Шауб - litb

Правда - технічно в цьому випадку воно також може повністю витіснити "i" і просто повернути 0. Але використання сегмента статичних даних для змінних даних було б небезпечно в багатопотокових середовищах. Справа, яку я намагався зробити у відповідь Мартіну, полягав у тому, що ви не можете повністю усунути вартість ініціалізації. Скопіюйте заздалегідь зроблений фрагмент із сегмента статичних даних, напевно, але це все-таки не безкоштовно.
Бужум


0

У мові програмування C ++ V4, Stroustrup рекомендує використовувати вектори або valarrays над вбудованими масивами. Коли ви створюєте їх, ви можете запросити їх до певного значення, наприклад:

valarray <int>seven7s=(7777777,7);

Ініціалізувати масив довжиною 7 членів з "7777777".

Це спосіб C ++ реалізації відповіді за допомогою структури даних C ++ замість масиву "звичайний старий C".

Я перейшов на використання valarray як спробу свого коду, щоб спробувати використовувати C ++ 'isms v. C'ism ....


Це другий найгірший приклад використання типу, який я коли-небудь бачив ...
Steazy

-3

Повинна бути стандартною функцією, але чомусь вона не входить до стандартних C, ані C ++ ...

#include <stdio.h>

 __asm__
 (
"    .global _arr;      "
"    .section .data;    "
"_arr: .fill 100, 1, 2; "
 );

extern char arr[];

int main() 
{
    int i;

    for(i = 0; i < 100; ++i) {
        printf("arr[%u] = %u.\n", i, arr[i]);
    }
}

У Fortran ви могли:

program main
    implicit none

    byte a(100)
    data a /100*2/
    integer i

    do i = 0, 100
        print *, a(i)
    end do
end

але він не має підписаних номерів ...

Чому C / C ++ не може просто реалізувати це. Невже так важко? Це так нерозумно, щоб писати це вручну, щоб досягти того ж результату ...

#include <stdio.h>
#include <stdint.h>

/* did I count it correctly? I'm not quite sure. */
uint8_t arr = {
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
    2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
};    

int main() 
{
    int i;

    for(i = 0; i < 100; ++i) {
        printf("arr[%u] = %u.\n", i, arr[i]);
    }
}

Що робити, якщо це був масив 1000,00 байт? Мені потрібно написати сценарій, щоб написати його для мене, або вдатися до хак із збіркою / тощо. Це нісенітниця.

Він ідеально портативний, немає причини, щоб він не був на мові.

Просто зламайте його так:

#include <stdio.h>
#include <stdint.h>

/* a byte array of 100 twos declared at compile time. */
uint8_t twos[] = {100:2};

int main()
{
    uint_fast32_t i;
    for (i = 0; i < 100; ++i) {
        printf("twos[%u] = %u.\n", i, twos[i]);
    }

    return 0;
}

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

#!/usr/bin/perl
use warnings;
use strict;

open my $inf, "<main.c";
open my $ouf, ">out.c";

my @lines = <$inf>;

foreach my $line (@lines) {
    if ($line =~ m/({(\d+):(\d+)})/) {
        printf ("$1, $2, $3");        
        my $lnew = "{" . "$3, "x($2 - 1) . $3 . "}";
        $line =~ s/{(\d+:\d+)}/$lnew/;
        printf $ouf $line;
    } else {
        printf $ouf $line;
    }
}

close($ouf);
close($inf);

ви друкуєте в циклі, чому ви не можете призначити його в циклі?
Абхінав Ганіял

1
присвоєння всередині циклу несе час виконання; тоді як жорстке кодування буфера вільне, оскільки буфер вже вбудований у двійковий файл, тому він не витрачає час на створення масиву з нуля кожного разу, коли програма запускається. ви маєте рацію, що друк у циклі не є загальною ідеєю, хоча краще додати всередині циклу, а потім надрукувати один раз, оскільки кожен виклик printf вимагає системного виклику, тоді як з'єднання рядків за допомогою купи / стека програми не робить. Оскільки розмір у цій програмі є непередаваним, найкраще побудувати цей масив під час компіляції, а не під час виконання.
Дмитро

"присвоєння всередині циклу вимагає накладних витрат" - Ви вкрай занижуєте оптимізатор.
Асу

Залежно від розміру масиву, gcc і clang будуть "жорстким кодом" або обманюють значення в, а з більшими масивами - безпосередньо саме memsetйого, навіть із масивом "hardcoded".
Асу
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.