Попытка понять конструкторы по умолчанию и инициализацию членов

Я привык инициализировать переменные-члены в конструкторах классов, но решил проверить, устанавливаются ли значения по умолчанию конструкторами по умолчанию. Мои тесты проводились с Visual Studio 2022 с использованием языкового стандарта C++ 20. Результаты меня смутили:

#include <iostream>

class A
{
public:
    double r;
};

class B
{
public:
    B() = default;
    double r;
};

class C
{
public:
    C() {}
    double r;
};

int main()
{
    A a1;
    std::cout << a1.r << std::endl; // ERROR: uninitialized local variable 'a1' used

    A a2();
    std::cout << a2.r << std::endl; // ERROR: left of '.r' must have class/struct/union

    A* pa1 = new A;
    std::cout << pa1->r << std::endl; // output: -6.27744e+66

    A* pa2 = new A();
    std::cout << pa2->r << std::endl; // output: 0

    B b1;
    std::cout << b1.r << std::endl; // ERROR: uninitialized local variable 'b1' used

    B b2();
    std::cout << b2.r << std::endl; // ERROR: left of '.r' must have class/struct/union

    B* pb1 = new B;
    std::cout << pb1->r << std::endl; // output: -6.27744e+66

    B* pb2 = new B();
    std::cout << pb2->r << std::endl; // output: 0

    C c1;
    std::cout << c1.r << std::endl;  // output: -9.25596e+61

    C c2();
    std::cout << c2.r << std::endl; // ERROR: left of '.r' must have class/struct/union

    C* pc1 = new C;
    std::cout << pc1->r << std::endl; // output: -6.27744e+66

    C* pc2 = new C();
    std::cout << pc2->r << std::endl; // output: -6.27744e+66
}

Спасибо всем, кто может просветить меня.

Какая часть сбивает с толку? Есть 2 разные вещи - про унитилизированные значения и left of '.r' must have class/struct/union, что самое неприятное в разборе.

Yksisarvinen 19.03.2022 14:22
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
1
56
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

MyType name(); // This is treated as a function declaration
MyType name{}; // This is the correct way

A a2();, B b2(); и C c2(); также могут быть проанализированы как объявления функций, возвращающих A/B/C с пустым списком параметров. Эта интерпретация предпочтительнее, поэтому вы объявляете функции, а не переменные. Это также известно как проблема "самый неприятный разбор".

Ни один из конструкторов по умолчанию (включая неявный из A) не инициализирует r, поэтому он будет иметь неопределенное значение. Чтение этого значения приведет к неопределенному поведению.

Исключение составляют A* pa2 = new A(); и B* pb2 = new B();. Инициализатор () выполняет инициализация значения. Эффект инициализации значения в этих двух случаях заключается в том, что весь объект будет инициализирован нулями, потому что и A, и B имеют конструктор по умолчанию, который не предоставляется пользователем. (По умолчанию при первом объявлении не считается предоставленным пользователем.)

В случае C это неприменимо, потому что конструктор C по умолчанию предоставляется пользователем, и поэтому инициализация значения приведет только к инициализации по умолчанию, вызывая конструктор по умолчанию, который не инициализирует r.

Спасибо. «Самый неприятный вопрос разбора» назван правильно!

Keith M 20.03.2022 21:17
Ответ принят как подходящий

Давайте посмотрим, что происходит в каждом конкретном случае в вашем примере.

Дело 1

Здесь мы рассмотрим утверждения:

A a1; //this creates a variable named a1 of type A using the default constrcutor
std::cout << a1.r << std::endl; //this uses the uninitialized data member r which leads to undefined behavior

В приведенном выше фрагменте первый оператор создает переменную с именем a1 типа A, используя по умолчаниюA::A()синтезированный компилятором. Это означает, что элемент данных r будет инициализировано по умолчанию. А так как r встроенного типа, то у него будет неопределенное значение. Использование этой переменной неинициализированный, которую вы делаете, когда пишете второй оператор, показанный в приведенном выше фрагменте, — это неопределенное поведение.

Случай 2

Здесь мы рассмотрим утверждения:

A a2(); //this is a function declaration 
std::cout << a2.r << std::endl; //this is not valid since a2 is the name of a function

Первый оператор в приведенном выше фрагменте, объявляет, представляет собой функцию с именем a2, которая не принимает параметров и имеет возвращаемый тип A. То есть первый оператор на самом деле является объявление функции. Теперь во втором выражении вы пытаетесь получить доступ к члену данных r функции с именем a2, что не имеет никакого смысла, и, следовательно, вы получаете упомянутую ошибку.

Случай 3

Здесь мы рассмотрим утверждения:

A* pa1 = new A; 
std::cout << pa1->r << std::endl; // output: -6.27744e+66

Первый оператор в приведенном выше фрагменте имеет следующие эффекты:

  1. безымянный объект типа A создается в куче из-за new A использования конструктор по умолчаниюA::A()синтезированный компилятором. Более того, в результате мы получаем еще и указатель на этот безымянный объект.
  2. Затем указатель, который мы получили на шаге 1 выше, используется как инициализатор для pa1. То есть создается указатель на A с именем pa1 и инициализируется указателем на безымянный объект, который мы получили на шаге 1 выше.

Поскольку использовался конструктор по умолчанию (см. шаг 1), это означает, что элемент данных r безымянного объекта — инициализация по умолчанию. А поскольку член данных r имеет встроенный тип, это означает, что он имеет неопределенное значение. И использование этого неинициализированного члена данных r, которое вы делаете во втором операторе приведенного выше фрагмента кода, — это неопределенное поведение. Вот почему вы получаете какое-то мусорное значение в качестве вывода.

Случай 4

Здесь мы рассмотрим утверждения:

A* pa2 = new A();
std::cout << pa2->r << std::endl;

Первый оператор приведенного выше фрагмента имеет следующие эффекты:

  1. Безымянный объект типа A создается благодаря выражению new A(). Но на этот раз, поскольку вы использовали круглые скобки () и поскольку класс A не имеет предоставленного пользователем конструктора по умолчанию, это означает, что произойдет инициализация значения. По сути, это означает, что элемент данных r будет ноль инициализирован. Вот почему / как вы получаете вывод как 0 во втором утверждении приведенного выше фрагмента. Более того, в качестве результата возвращается указатель на этот безымянный объект.

  2. Далее создается указатель на A с именем pa2, который инициализируется с помощью указателя на безымянный объект, который мы получили на шаге 1 выше.


Точно то же самое происходит со следующими 4 операторами, относящимися к классу B. Поэтому я не буду обсуждать следующие 4 утверждения, относящиеся к классу B, так как мы не узнаем из них ничего нового. Для них произойдет то же самое, что и для предыдущих 4 утверждений, описанных выше.


Теперь рассмотрим утверждения, относящиеся к классу C. Мы не пропускаем эти 4 утверждения, потому что для класса C существует определяемый пользователем конструктор по умолчанию.

Заявление 5

Здесь мы рассмотрим утверждения:

C c1;
std::cout << c1.r << std::endl;

Первый оператор приведенного выше фрагмента кода создает переменную с именем c1 типа C, используя предоставленный пользователем конструктор по умолчаниюA::A(). Поскольку этот предоставленный пользователем конструктор по умолчанию ничего не делает, член данных r остается неинициализированным, и мы получаем то же поведение, что и для A a1;. То есть использование этой неинициализированной переменной, которую вы делаете во втором операторе, — это неопределенное поведение.

Заявление 6

Здесь мы рассмотрим утверждения:

C c2();
std::cout << c2.r << std::endl;

Первый оператор в приведенном выше фрагменте — это объявление функции. Таким образом, вы получите то же поведение/ошибку, что и для класса A.

Заявление 7

Здесь мы рассмотрим утверждения:

C* pc1 = new C;
std::cout << pc1->r << std::endl;

Первый оператор в приведенном выше фрагменте имеет следующие эффекты:

  1. Безымянный объект типа C создается в куче с использованием предоставленного пользователем конструктора по умолчанию A::A() из-за выражения new A. И поскольку пользователь предоставляет конструктор по умолчанию, ничего не делает, член данных r остается неинициализированным. Более того, в результате мы получаем указатель на этот безымянный объект.

  2. Далее создается указатель на C с именем pc1, который инициализируется pionter для безымянного объекта, который мы получили на шаге 1.

Теперь второй оператор в приведенном выше фрагменте использует неинициализированный элемент данных r, что является неопределенным поведением и объясняет, почему вы получаете какое-то мусорное значение в качестве вывода.

Заявление 8

Здесь мы рассмотрим утверждения:

C* pc2 = new C();
std::cout << pc2->r << std::endl;

Первый оператор приведенного выше фрагмента имеет следующие эффекты:

  1. Безымянный объект типа C создается в куче из-за new C(). Теперь, поскольку вы указали скобки (), это будет выполнять инициализацию значения. Но поскольку на этот раз у нас есть конструктор по умолчанию, предоставляемый пользователем, инициализация значения аналогична инициализации по умолчанию, которая будет выполняться с использованием конструктора по умолчанию, предоставляемого пользователем. И поскольку пользователь предоставляет конструктор по умолчанию, ничего не делает, элемент данных r останется неинициализированным. Более того, в результате мы получаем указатель на безымянный объект.

  2. Затем создается указатель на C с именем pc2, который инициализируется pionter для безымянного объекта, который мы получили на шаге 1 выше.

Теперь второй оператор в приведенном выше фрагменте использует неинициализированный элемент данных r, который является неопределенное поведение, и объясняет, почему вы получаете какое-то мусорное значение в качестве вывода.

Спасибо, что нашли время, чтобы объяснить так подробно. Это очень полезно.

Keith M 20.03.2022 21:14

@KeithM Пожалуйста, взгляните на Что мне делать, когда кто-то отвечает на мой вопрос?.

user17732522 20.03.2022 21:30

Другие вопросы по теме