Як члени класу C ++ ініціалізуються, якщо я не роблю це явно?


158

Припустимо , у мене є клас з приватними ЧЛЕНІВ ptr, name, pname, rname, crnameі age. Що станеться, якщо я сам не ініціалізую їх? Ось приклад:

class Example {
    private:
        int *ptr;
        string name;
        string *pname;
        string &rname;
        const string &crname;
        int age;

    public:
        Example() {}
};

І тоді я роблю:

int main() {
    Example ex;
}

Як члени ініціалізуються в ex? Що відбувається з покажчиками? Зробити stringта intотримати 0-intialized за допомогою конструкторів за замовчуванням string()та int()? Що з референтним членом? А як щодо посилань на const?

Що ще я повинен знати?

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

Я хотів би дізнатися це, щоб я міг писати кращі програми (без помилок). Будь-який відгук допоможе!


3
Рекомендації щодо книг див. У статті stackoverflow.com/questions/388242/…
Майк Сеймур

Майк, о, я маю на увазі розділ із якоїсь книги, яка це пояснює. Не ціла книга! :)
bodacydo

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

2
Скотт Мейєрс (популярний екс- провідний гуру C ++) заявляє в " Ефективній C ++" : "правила складні - занадто складні, щоб їх варто запам'ятати, на мою думку .... переконайтеся, що всі конструктори ініціалізують все в об'єкті". Тож, на його думку, найпростіший спосіб (спробувати) написати код «помилка» - це не намагатися запам'ятати правила (а насправді він не викладає їх у книзі), а чітко ініціалізувати все. Однак зауважте, що навіть якщо ви застосовуєте такий підхід у власному коді, ви можете працювати над проектами, написаними людьми, які цього не роблять, тому правила все ще можуть бути цінними.
Кайл Странд

2
@TylerMcHenry Які книги на C ++ ви вважаєте "хорошими"? Я прочитав кілька книг на C ++, але жодна з них не пояснила це повністю. Як зазначалося в моєму попередньому коментарі, Скотт Майєрс прямо відмовляється надавати повні правила в Ефективному С ++ . Я також читав « Ефективний сучасний C ++ » Майєрса , C ++ «Загальні знання» Деухерста та « Екскурсія по С ++» . На мою пам’ять, жоден з них не пояснив повних правил. Очевидно, я міг прочитати стандарт, але навряд чи вважав би це "хорошою книжкою"! : D І я думаю, що Stroustrup, ймовірно, пояснює це мовою програмування на C ++ .
Кайл Странд

Відповіді:


206

Замість явної ініціалізації ініціалізація членів у класах працює ідентично ініціалізації локальних змінних у функціях.

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

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

Для довідок (наприклад std::string&) не ініціалізувати їх незаконно , і ваш компілятор подасть скаргу та відмовиться складати такий код. Посилання завжди повинні бути ініціалізовані.

Отже, у вашому конкретному випадку, якщо вони явно не ініціалізовані:

    int *ptr;  // Contains junk
    string name;  // Empty string
    string *pname;  // Contains junk
    string &rname;  // Compile error
    const string &crname;  // Compile error
    int age;  // Contains junk

4
+1. Варто зазначити, що за суворим стандартним визначенням екземпляри примітивних типів поряд з різноманітними іншими речами (будь-які області зберігання) - всі вважаються об'єктами .
stinky472

7
"Якщо в класі об'єкта немає конструктора за замовчуванням, це буде помилка компіляції, якщо ви явно не ініціалізуєте його", це неправильно ! Якщо у класу немає конструктора за замовчуванням, йому надається конструктор за замовчуванням, який порожній.
Чарівник

19
@wiz Я думаю, що він буквально мав на увазі "якщо в об'єкта немає конструктора за замовчуванням", як у жодному згенерованому, що було б у випадку, якщо клас явно визначає будь-які конструктори, крім за замовчуванням (не буде створено жодного ctor за замовчуванням). Якщо ми станемо занадто педанатичними, ми, мабуть, заплутаємось більше, ніж допомога, і Тайлер добре відгукується про це у своїй відповіді до мене раніше.
stinky472

8
@ wiz-loz Я б сказав, що у fooнього є конструктор, це просто неявно. Але це справді аргумент семантики.
Тайлер Макенрі

4
Я трактую "конструктор за замовчуванням" як конструктор, який можна викликати без аргументів. Це буде той, який ви самі визначаєте, або неявно генерується компілятором. Тож відсутність цього означає, що не визначається вами і не породжується. Або так я це бачу.
5:00

28

Спочатку дозвольте мені пояснити, що таке список ініціалізаторів пам’яті . Список MEM-ініціалізатор- це розділений комами список MEM-ініціалізатор с, де кожен MEM-ініціалізатор є ім'я елемента з подальшим (, з подальшим виразом-лист , за яким слід ). Список виразів - це спосіб побудови члена. Наприклад, в

static const char s_str[] = "bodacydo";
class Example
{
private:
    int *ptr;
    string name;
    string *pname;
    string &rname;
    const string &crname;
    int age;

public:
    Example()
        : name(s_str, s_str + 8), rname(name), crname(name), age(-4)
    {
    }
};

ає-ініціалізатор-лист конструктора наданого користувача, який не-аргументи name(s_str, s_str + 8), rname(name), crname(name), age(-4). Цей MEM-ініціалізатор-списку означає , що nameелемент инициализируется в std::stringконструкторі , який приймає два вхідних ітераторів , то rnameелемент инициализируется з посиланням name, то crnameелемент инициализируется з константної-посиланням name, а ageелемент инициализируются зі значенням -4.

У кожного конструктора є свій список ініціалізаторів пам’яті , і члени можуть бути ініціалізовані лише у встановленому порядку (в основному порядку, в якому члени оголошуються в класі). Таким чином, члени Exampleможуть бути ініційовані лише в наступному порядку: ptr, name, pname, rname, crname, і age.

Якщо ви не вказуєте ініціалізатор пам’яті члена, стандарт C ++ говорить:

Якщо сутність є нестатичним членом даних ... типу класу ..., сутність ініціалізується за замовчуванням (8.5). ... Інакше сутність не ініціалізується.

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

Коли стандарт каже, що вони не ініціалізовані, це означає, що вони можуть мати будь-яке значення. Таким чином, оскільки вищезазначений код не ініціалізувався pname, він може бути чим завгодно.

Зауважте, що ви все одно повинні дотримуватися інших правил, таких як правило, згідно з яким посилання повинні завжди ініціалізуватися. Помилка компілятора не ініціалізувати посилання.


Це найкращий спосіб ініціалізації членів, коли ви хочете суворо розділити оголошення (in .h) та визначення (in .cpp), не показуючи занадто багато внутрішніх даних.
Матьє

12

Ви також можете ініціалізувати членів даних у точці, де ви їх оголосили:

class another_example{
public:
    another_example();
    ~another_example();
private:
    int m_iInteger=10;
    double m_dDouble=10.765;
};

Я користуюся цією формою досить ексклюзивно, хоча я читав, що деякі вважають це "поганою формою", можливо, тому, що вона була нещодавно представлена ​​- я думаю, що в C ++ 11. Для мене це більш логічно.

Ще одна корисна грань нових правил - це ініціалізація членів даних, які самі є класами. Наприклад, припустимо, що CDynamicStringце клас, який інкапсулює обробку рядків. У ньому є конструктор, який дозволяє вказати його початкове значення CDynamicString(wchat_t* pstrInitialString). Ви можете дуже добре використовувати цей клас як член даних всередині іншого класу - скажімо, клас, який інкапсулює значення реєстру Windows, яке в цьому випадку зберігає поштову адресу. Для "жорсткого коду" імені ключа реєстру, до якого пишеться, ви використовуєте дужки:

class Registry_Entry{
public:
    Registry_Entry();
    ~Registry_Entry();
    Commit();//Writes data to registry.
    Retrieve();//Reads data from registry;
private:
    CDynamicString m_cKeyName{L"Postal Address"};
    CDynamicString m_cAddress;
};

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


9

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

У глобальному екземплярі неініціалізовані скалярні члени будуть нульовими.

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

  • int *ptr; // неініціалізований покажчик (або нульовий, якщо глобальний)
  • string name; // конструктор викликається, ініціалізований порожнім рядком
  • string *pname; // неініціалізований покажчик (або нульовий, якщо глобальний)
  • string &rname; // помилка компіляції, якщо не вдалося ініціалізувати це
  • const string &crname; // помилка компіляції, якщо не вдалося ініціалізувати це
  • int age; // скалярне значення, неініціалізоване та випадкове (або нульове, якщо глобальне)

Я експериментував і, здається, string nameпорожній після ініціалізації класу на стеку. Ви абсолютно впевнені у своїй відповіді?
bodacydo

1
рядок матиме конструктор, який за замовчуванням надав порожній рядок - я поясню свою відповідь
Пол Діксон

@bodacydo: Павло правильно, але якщо ти дбаєш про таку поведінку, це ніколи не зашкодить бути явним. Занесіть його до списку ініціалізатора.
Стівен

Дякуємо за уточнення та пояснення!
bodacydo

2
Це не випадково! Випадковість занадто велике слово для цього! Якщо скалярні члени будуть випадковими, нам не знадобляться інші генератори випадкових чисел. Уявіть програму, яка аналізує дані "залишені файли" - як, наприклад, неповторні файли в пам'яті - дані далеко не випадкові. Це навіть не визначено! Зазвичай це важко визначити, тому що зазвичай ми не знаємо, що робить наша машина. Якщо ті "випадкові дані", які ви щойно оприлюднили, є єдиним зображенням вашого батька, ваша мати може навіть вважати образливим, якщо ви скажете, що його випадкові ...
slyy2048

5

Неініціалізовані нестатичні члени містять випадкові дані. Насправді вони просто матимуть значення місця пам'яті, якому вони призначені.

Звичайно для параметрів об'єкта (як string) конструктор об'єкта може зробити ініціалізацію за замовчуванням.

У вашому прикладі:

int *ptr; // will point to a random memory location
string name; // empty string (due to string's default costructor)
string *pname; // will point to a random memory location
string &rname; // it would't compile
const string &crname; // it would't compile
int age; // random value

2

Учасники з конструктором матимуть свій конструктор за замовчуванням, який буде викликаний для ініціалізації.

Ви не можете залежати від вмісту інших типів.


0

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

Отже, якщо у вас є рядок * pname, вказівник буде містити випадкові непотрібні. але для назви рядка конструктор за замовчуванням для рядка буде називатися, що дає вам порожню рядок. Щодо змінних вашого довідкового типу, я не впевнений, але це, ймовірно, буде посиланням на якийсь випадковий фрагмент пам'яті.


0

Це залежить від того, як побудований клас

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

Як простий приклад того, як важко:

main.cpp

#include <cassert>

int main() {
    struct C { int i; };

    // This syntax is called "default initialization"
    C a;
    // i undefined

    // This syntax is called "value initialization"
    C b{};
    assert(b.i == 0);
}

У ініціалізації за замовчуванням ви б почали з: https://en.cppreference.com/w/cpp/language/default_initialization, переходимо до частини "Ефекти ініціалізації за замовчуванням є" та запускаємо заяву:

  • "якщо T - це не-POD ": ні (визначення POD саме по собі є величезною заявою комутатора)
  • "якщо T - тип масиву": немає
  • "інакше нічого не робиться": тому воно залишається з невизначеним значенням

Потім, якщо хтось вирішить ініціалізувати значення, ми переходимо до https://en.cppreference.com/w/cpp/language/value_initialization "Ефекти ініціалізації значення є" та запускаємо заяву:

  • "якщо T - тип класу без конструктора за замовчуванням або з наданим користувачем або видаленим конструктором за замовчуванням": не так. Ви витратите 20 хвилин на Googling на ці умови:
    • у нас неявно визначений конструктор за замовчуванням (зокрема, тому, що не було визначено жодного іншого конструктора)
    • не надається користувачем (неявно визначено)
    • це не видалено ( = delete)
  • "якщо T - тип класу з конструктором за замовчуванням, який не надається ні видалено, ні видаляється": так
    • "об'єкт ініціалізований нулем, а потім він ініціалізований за замовчуванням, якщо він має нетривіальний конструктор за замовчуванням": немає нетривіального конструктора, просто нульовий ініціалізація. Визначення "нульової ініціалізації" принаймні є простим і робить те, що ви очікуєте: https://en.cppreference.com/w/cpp/language/zero_initialization

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

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