memset () або ініціалізація значення для обнулення структури?


78

У програмуванні API Win32 типово використовувати C structs з декількома полями. Зазвичай лише пара з них мають значущі значення, а всі інші повинні бути обнулені. Цього можна досягти одним із двох способів:

STRUCT theStruct;
memset( &theStruct, 0, sizeof( STRUCT ) );

або

STRUCT theStruct = {};

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

Чи є у нього якісь недоліки порівняно з першим варіантом? Який варіант використовувати і чому?


Цей спосіб відповіді [1] на наступне запитання видається більш корисним і простішим. [1]: stackoverflow.com/questions/4625212/class-initialization-list / ...
TheMatto

Відповіді:


96

Ці дві конструкції дуже різні за своїм значенням. Перший використовує memsetфункцію, яка призначена для встановлення буферу пам'яті певного значення . Другий, який ініціалізує об’єкт . Дозвольте пояснити це трохи коду:

Припустимо, у вас є структура, яка містить лише членів типів POD ("Звичайні старі дані" - див. Що таке типи POD у C ++? )

struct POD_OnlyStruct
{
    int a;
    char b;
};

POD_OnlyStruct t = {};  // OK

POD_OnlyStruct t;
memset(&t, 0, sizeof t);  // OK as well

У цьому випадку написання POD_OnlyStruct t = {}або POD_OnlyStruct t; memset(&t, 0, sizeof t)не має великої різниці, оскільки єдина різниця, яку ми маємо тут, - це байти вирівнювання, які встановлюються на нульове значення у випадку memsetвикористання. Оскільки ви зазвичай не маєте доступу до цих байтів, для вас немає різниці.

З іншого боку, оскільки ви позначили своє запитання як C ++, спробуймо ще один приклад із типами членів, відмінними від POD :

struct TestStruct
{
    int a;
    std::string b;
};

TestStruct t = {};  // OK

{
    TestStruct t1;
    memset(&t1, 0, sizeof t1);  // ruins member 'b' of our struct
}  // Application crashes here

У цьому випадку використання виразу like TestStruct t = {}- це добре, а використання memseton - призведе до аварії. Ось що трапляється, якщо ви використовуєте memset- створюється об’єкт типу TestStruct, таким чином створюється об’єкт типу std::string, оскільки він є членом нашої структури. Далі memsetвстановлює пам’ять, де bзнаходився об’єкт, на певне значення, скажімо нуль. Тепер, як тільки наш об'єкт TestStruct виходить за межі області його використання, він буде знищений, і коли черга прийде до його учасника, std::string bви побачите збій, оскільки всі внутрішні структури цього об'єкта були зруйновані memset.

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

Мій голос - використовувати memsetна об’єктах лише якщо це потрібно, а в усіх інших випадках використовувати ініціалізацію за замовчуваннямx = {} .


Привіт Dimity! У мене є структура, яка містить кілька членів, і я спробував перший варіант набору пам’яті: "struct stVar = {}". Але я отримую попередження "-Wmissing-field-initializers". Це проблема?
MayurK

1
У цьому випадку під POD ви маєте на увазі насправді тривіально конструктивний об'єкт (тобто об'єкт без c-tor, що надається користувачем)? Я не думаю, що це повинно обмежуватися POD.
Ал.Г.

Це не призведе до аварії: coliru.stacked-crooked.com/a/4b3dbf0b8761bc9b Це технічно невизначена поведінка, оскільки структуру тривіально не можна призначити (звідси попередження компілятора). Однак я сумніваюся, що існує якась загальна платформа, для якої обнулені байти є недійсним значенням std::string.
Kyle Strand

Я думаю, що ця відповідь застаріла. У C ++ 11 біти заповнення гарантовано if T is a (possibly cv-qualified) non-union class type, each non-static data member and each base-class subobject is zero-initialized and padding is initialized to zero bits;
Clément

29

Залежно від членів структури, два варіанти не обов'язково еквівалентні. memsetвстановить структуру на all-bit-zero, тоді як ініціалізація значення ініціалізує всі члени до значення zero. Стандарт C гарантує, що вони однакові лише для інтегральних типів, а не для значень із плаваючою комою чи покажчиків.

Крім того, деякі API вимагають, щоб для структури дійсно було встановлено значення all-bit-zero. Наприклад, API сокета Берклі використовує структури поліморфно, і тому важливо реально встановити всю структуру на нуль, а не лише видимі значення. Документація API повинна вказувати, чи справді структура повинна бути абсолютно бітовою, але це може бути недоліком.

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


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

3
Кілька старих процесорів до IEEE-754 мали дивні нулі з плаваючою частотою. Математика, яка не є 754, може ще повернутися, ви ніколи не знаєте, тому краще не писати ці помилки.
Ендрю Макгрегор,

1
Не має значення. Стандарт C не вказує, який формат плаваючої форми використовується. Отже, навіть якщо це працює зараз для IEEE 754, це може не спрацювати на іншій реалізації з плаваючою точкою (майбутнє чи минуле)
Жаба

3
Я здогадуюсь, що в наш час не так багато, оскільки IEEE був настільки поширеним, але раніше вони були більш поширеними. Я розумію, що реалізації програмного забезпечення FP є типовими прикладами, коли нуль не був абсолютно біт-нулем. Отже, ви, мабуть, не потрапите в біду, але все ж C не вимагає IEEE, тому, якщо нульова ініціалізація не є вузьким місцем, "безпечніший" спосіб насправді нічого не коштує.
JaakkoK

1
Ініціалізація кожного члена до нуля не призведе до нуля кожного члена, але ви пропустите байти заповнення. Тому мемсет - це ваш єдиний вибір.
fmuecke

11

Якщо ваша структура містить такі речі:

int a;
char b;
int c;

Тоді байти заповнення будуть вставлені між "b" і "c". memset () обнулить їх, в іншому випадку ні, тому буде 3 байти сміття (якщо ваші ints складають 32 біти). Якщо ви маєте намір використовувати свою структуру для читання / запису з файлу, це може бути важливо.


2
Здається, це неправда. З CppReference: "Якщо T не є об'єднаним типом класу, всі базові класи та нестатичні члени даних нульово ініціалізовані, а всі відступи ініційовані нульовими бітами. Конструктори, якщо такі є, ігноруються." en.cppreference.com/w/cpp/language/zero_initialization
Кайл Странд

Можливо, це стосується лише С, а не С ++.
syockit

7

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

Ви можете покластися на memsetнульову структуру після її використання.


6

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


while doing a memset would certainly not- не зовсім вірно. Насправді, на x86 та x64 перестановка float / double до нуля встановить його до нуля. Звичайно, це не входить до стандарту C / C ++, але він працює на найпопулярніших платформах.
sbk

2
sbk: наразі ... хто знає, яку реалізацію з плаваючою комою вони можуть почати використовувати. IEEE 754 не визначений для компілятора. Отже, навіть якщо це може працювати зараз, це просто щастя для вас, але пізніше може виникнути проблема.
Жаба

4

Ініціалізація значення, оскільки це може бути зроблено під час компіляції.
Також він правильно 0 ініціалізує всі типи POD.

Memset () виконується під час виконання.
Також використання memset () є підозрілим, якщо структура не є POD.
Не правильно ініціалізує (до нуля) нетипові типи.


3
Значення не ініціалізуються під час компіляції. Компілятор генерує стартовий код, який ініціює всі глобали під час запуску програми, таким чином, під час виконання. Для змінних стека ініціалізація виконується при введенні функції - знову під час виконання.
qrdl

@qrdl, залежить від компілятора та цілі. Для коду, що підтримує ПЗУ, значення іноді встановлюються під час компіляції.
Проф. Фалькен

2
@qrdl: Дозвольте мені переформулювати це. Ініціалізація значень може дозволити (за певних ситуацій) компілятору виконувати ініціалізацію під час компіляції (а не під час виконання). Таким чином, під час компіляції можна ініціалізувати лише глобальні глобальні інтерфейси.
Мартін Йорк,

@qrdl: Якщо на багатьох платформах "foo" є Int32_t статичного класу сховища, оператор часу виконання "foo = 0x12345678;" згенерує код для зберігання 0x12345678 у foo; цей код, швидше за все, міг би становити щонайменше десять байт, деяким мікроконтролерам знадобиться до 32 байт. Декларація "Int32_t foo = 0x12345678;" на багатьох платформах призведе до зв’язування змінної в сегменті ініціалізованих даних та додавання 4 байт до списку ініціалізації. У деяких системах "Int32_t foo;" буде на чотири байти дешевше, ніж "Int32_t foo = 0;", останній примушує foo до сегменту ініціалізованих даних.
supercat

3

У деяких компіляторах STRUCT theStruct = {};це перекладається memset( &theStruct, 0, sizeof( STRUCT ) );у виконуваний файл. Деякі функції C вже пов'язані, щоб виконати налаштування середовища виконання, тому компілятор має такі функції бібліотеки, як memset / memcpy, доступними для використання.


2
Це насправді мене нещодавно сильно покусало. Я працював над спеціальним фрагментом коду стиснення і ініціалізував деякі великі структури під час оголошення, struct something foo = { x, y, z }а cachegrind показав, що 70% "роботи" моєї програми було в memsetтому, що структури були обнулені при КОЖНОМУ виклику функції.
Джоді Брушон

-1

Якщо членів покажчика багато, і ви, ймовірно, додасте більше в майбутньому, це може допомогти використовувати memset. У поєднанні з відповідними assert(struct->member)викликами ви можете уникнути випадкових збоїв у спробі визначити поганий вказівник, який ви забули ініціалізувати. Але якщо ви не такі забудькуваті, як я, тоді ініціалізація учасників, мабуть, найкраща!

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


Як це допомагає у проведенні майбутніх перевірок? Якщо ви припускаєте, що клієнтський код не перекомпільований, це закінчиться викликом memsetіз неправильним розміром структури. Якщо клієнтський код перекомпілюється, йому знадобиться доступ до оновленого файлу заголовка з визначенням структури, щоб memsetініціалізація або значення працювала. (Клієнт і бібліотека повинні мати узгоджене уявлення про те, як представлений нульовий покажчик, однак, тому, якщо API рекомендує memset, його слід перевіряти проти all-bit-zero, а не проти NULL.)
jamesdlin

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