Чем инициализация отличается от присваивания
Перейти к содержимому

Чем инициализация отличается от присваивания

  • автор:

1.4 – Присваивание и инициализация переменных

В предыдущем уроке «1.3 – Знакомство с переменными в C++» мы рассмотрели, как определить переменную, которую мы сможем использовать для хранения значений. В этом уроке мы узнаем, как помещать значения в переменные, и как использовать эти значения.

Напоминаем, что следующий короткий фрагмент сначала выделяет память для одной целочисленной переменной с именем x , а затем выделяет память еще для двух целочисленных переменных с именами y и z :

int x; // определяем целочисленную переменную с именем x int y, z; // определяем две целочисленные переменные с именами y и z

Присваивание значения переменной

После того, как переменная была определена, вы можете присвоить ей значение (в отдельной инструкции) с помощью оператора = . Этот процесс для краткости называется копирующим присваиванием (или просто присваиванием).

int width; // определяем целочисленную переменную с именем width width = 5; // копирующее присваивание значения 5 в переменную width // переменная width теперь имеет значение 5

Копирующее присваивание названо так, потому что оно копирует значение с правой стороны оператора = в переменную с левой стороны оператора. Оператор = называется оператором присваивания.

Вот пример, где мы используем присваивание дважды:

#include int main() < int width; width = 5; // копирующее присваивание значения 5 в переменную width // переменная width теперь имеет значение 5 width = 7; // изменить значение, сохраненное в переменной width, на 7 // переменная width теперь имеет значение 7 return 0; >

Когда мы присваиваем значение 7 переменной width , значение 5, которое было там ранее, перезаписывается. Обычные переменные могут содержать только одно значение за раз.

Предупреждение

Одна из наиболее распространенных ошибок, которые делают новички, – это путать оператор присваивания ( = ) с оператором равенства ( == ). Присваивание ( = ) используется для присвоения значения переменной. Равенство ( == ) используется для проверки того, равны ли два операнда по значению.

Копирующая и прямая инициализации

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

Эти два шага можно совместить. При определении переменной вы также можете одновременно указать для нее начальное значение. Это называется инициализацией.

C++ поддерживает три основных способа инициализации переменных. Во-первых, мы можем выполнить копирующую инициализацию, используя знак равенства:

int width = 5; // копирующая инициализация значения 5 в переменную width

Подобно копирующему присваиванию, этот код копирует значение с правой стороны знака равно в переменную, создаваемую с левой стороны.

Во-вторых, мы можем выполнить прямую инициализацию с помощью скобок.

int width(5); // прямая инициализация значения 5 в переменную width

Для простых типов данных (например, целых чисел) копирующая и прямая инициализации, по сути, одинаковы. Различия между копирующей инициализацией и прямой инициализацией мы увидим в этой серии статей позже.

В-третьих, инициализация списком, которую мы рассмотрим в следующем разделе.

Инициализации списком

К сожалению, прямая инициализация с круглыми скобками не может использоваться для всех типов инициализации (таких как инициализация объекта списком данных). Чтобы обеспечить более согласованный механизм инициализации, существует инициализация списком (также иногда называемая унифицированной инициализацией или инициализацией с фигурными скобками), в которой используются фигурные скобки.

Инициализация списком бывает двух форм:

int width; // прямая инициализация списком значения 5 в переменную width (предпочтительно) int height = ; // копирующая инициализация списком значения 6 в переменную height

Эти две формы функционируют почти одинаково, но обычно предпочтительнее прямая форма.

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

int width<>; // инициализация значения значением 0

Инициализация списком имеет дополнительное преимущество, так как запрещает «сужающие» преобразования. Это означает, что если вы попытаетесь использовать инициализацию списком для инициализации переменной значением, которое она не может безопасно удерживать, компилятор выдаст предупреждение или ошибку. Например:

int width; // ошибка: не все значения типа double помещаются в int

В приведенном выше фрагменте мы пытаемся присвоить число ( 4.5 ), имеющее дробную часть (часть .5 ), целочисленной переменной (которая может содержать только числа без дробных частей). При копирующей и прямой инициализациях дробная часть будет отброшена, что приведет к инициализации переменной width значением 4. Однако при инициализации списком это приведет к тому, что компилятор выдаст ошибку (что, как правило, хорошо потому, что потеря данных редко бывает желательной). Преобразования, которые могут быть выполнены без потенциальной потери данных, в этом случае разрешены.

Лучшая практика

По возможности отдавайте предпочтение прямой инициализации списком.

Вопрос: C++ обеспечивает копирующую, прямую инициализации и инициализацию списком, а также копирующее присваивание. Существует ли прямое присваивание или присваивание списком?

Нет, C++ не поддерживает синтаксис прямого присваивания или присваивания списком.

Вопрос: Когда следует инициализировать с помощью , а когда с помощью <> ?

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

int x; // явная инициализация значением 0 std::cout 

Используйте инициализацию значения, если значение временное и будет заменено.

int x<>; // инициализация значения std::cin >> x; // мы немедленно заменяем это значение

Инициализируйте свои переменные

Инициализируйте свои переменные при создании. В конечном итоге вам могут встретиться случаи, когда вы захотите проигнорировать этот совет по определенной причине (например, критический для производительности фрагмент кода, который использует множество переменных), и это нормально, если выбор сделан сознательно.

Для более подробного обсуждения этой темы Бьёрн Страуструп (создатель C++) и Герб Саттер (эксперт по C ++) сами дают рекомендацию здесь.

Мы исследуем, что произойдет, если вы попытаетесь использовать переменную, для которой нет четко определенного значения, в уроке «1.6 – Неинициализированные переменные и неопределенное поведение».

Лучшая практика

Инициализируйте свои переменные при создании.

Инициализация нескольких переменных

В последнем разделе мы отметили, что можно определить несколько переменных одного типа в одной инструкции, разделив имена запятыми:

int a, b;

Мы также отметили, что лучше всего вообще избегать этого синтаксиса. Однако, поскольку вы можете столкнуться с чужим кодом, использующим этот стиль, всё же полезно поговорить об этом немного подробнее хотя бы, чтобы акцентировать внимание на некоторых причинам, по которым вам следует избегать его.

Вы можете инициализировать несколько переменных, определенных в одной строке:

int a = 5, b = 6; // копирующая инициализация int c(7), d(8); // прямая инициализация int e, f; // инициализация списком (предпочтительно)

К сожалению, здесь есть распространенная ошибка, которая может возникнуть, когда программист по ошибке пытается инициализировать обе переменные с помощью одной инструкции инициализации:

int a, b = 5; // неверно (a не инициализируется!) int a = 5, b = 5; // верно

В верхней инструкции переменная a будет оставлена неинициализированной, и компилятор может пожаловаться, а может и не пожаловаться. Если он не пожалуется, это отличный способ для вашей программы периодически давать сбои и выдавать случайные результаты. В ближайшее время мы подробнее поговорим о том, что произойдет, если вы будете использовать неинициализированные переменные.

Лучший способ запомнить, что это неправильно, – рассмотреть случай прямой инициализации или инициализации списком:

int a, b(5); int c, d;

Этот код делает более ясным, что значение 5 используется только для инициализации переменной b или d , а не a или c .

Небольшой тест

Вопрос 1

В чем разница между инициализацией и присваиванием?

Инициализация дает переменной начальное значение в момент ее создания. Присваивание дает переменной значение в какой-то момент после создания.

Вопрос 2

Какую форму инициализации следует использовать?

Прямая инициализация списком.

Инициализация vs Присваивания. Если ли разница?

Этот приём назван инициализацией. В отличие от присваивания, которое осуществляется в процессе выполнения программы, инициализация выполняется при выделении для переменной участка памяти.

Однако, мне кажется, что разницы (в современных компиляторах) нет. Ассемблерный код подтверждает мою догадку:

int a, b=2; a = 1;
1 2 3 4
mov w0, 2 str w0, [sp, 12] mov w0, 1 str w0, [sp, 8]
movl $2, -8(%rbp) movl $1, -4(%rbp)

Уже стало неактуально? А какой раньше генерировали компиляторы код, что была разница между инициализацией и последующим присваиванием?

94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
Ответы с готовыми решениями:

Инициализация копированием () и присваиванием =. В чем разница?
в чем разница инициализации в скобках от инициализации присваиванием? int a=5; int b(5); //в чем.

Какая разница между возвращением значения и присваивания значения?
Какая разница между возвращением значения и присваивания значения? Я всегда думал, что возвращение.

Инициализация в теле конструктора или в списке инициализации, есть ли разница в сгенерированном коде?
Инициализация в теле конструктора или в списке инициализации - большая ли разница в сгенерированном.

Эксперт CЭксперт С++

11126 / 6084 / 1663
Регистрация: 18.10.2014
Сообщений: 15,295

ЦитатаСообщение от Рыжий Лис Посмотреть сообщение

Однако, мне кажется, что разницы (в современных компиляторах) нет. Ассемблерный код подтверждает мою догадку:

Ваши примеры не подтверждают вообще ничего, ибо эксперименты с таким примитивным типом как int абсолютно не показательны. Однако, если уж на то пошло, в языке С разницы на уровне "ассемблерного кода" никогда и не было, независимо от того, современен компилятор или нет. Откуда ей вдруг взяться?

Но с чего бы это вдруг какой-то "ассемблерный код" стал влиять на актуальность инициализации? Инициализацию используют совсем не потому, что кто-то где-то надеется на лучший ассемблерный код. Наоборот, ассемблерный код не имеет никакого значения и никому не интересен в рамках этого вопроса.

Разница между инициализацией и присваиванием - важная концепция языкового уровня. Ее используют именно только ради ее концептуальной ценности. Но если вы не ощущаете этой ценности, то все равно: как вы собираетесь использовать присваивание с переменной типа const int ? Как вы собираетесь использовать присваивание с массивом?

3802 / 2354 / 413
Регистрация: 09.09.2017
Сообщений: 10,212

ЦитатаСообщение от Рыжий Лис Посмотреть сообщение

Однако, мне кажется, что разницы (в современных компиляторах) нет.

Для одной переменной действительно нет. Но если переменных куча, вместо поочередного присваивания будет заведен массив констант с дальнейшим распихиванием по соответствующим адресам переменных. Обычно это дает выигрыш в объеме кода и иногда - в скорости.

Просто Лис

Эксперт Python

5318 / 3332 / 1021
Регистрация: 17.05.2012
Сообщений: 9,765
Записей в блоге: 9
С вашей помощью смог написать пример, показывающий разницу:

1 2 3 4 5 6 7 8
void main() { char a[4] = {1,2,3,4}; char b[4]; b[0] = 1; b[1] = 2; b[2] = 3; b[3] = 4; }

x86-64 clang:

1 2 3 4 5 6 7 8 9 10 11 12 13
main: pushq %rbp movq %rsp, %rbp movl .L__const.main.a, %eax movl %eax, -4(%rbp) movb $1, -8(%rbp) movb $2, -7(%rbp) movb $3, -6(%rbp) movb $4, -5(%rbp) popq %rbp retq .L__const.main.a: .ascii "\001\002\003\004"

Для int компилятор тоже хитрит и записывает значения в память сразу кусками по 64 бита.

Я хотел услышать, что инициализация даёт возможность компилятору больше простора для оптимизации даже при опции -O0.

Эксперт CЭксперт С++

11126 / 6084 / 1663
Регистрация: 18.10.2014
Сообщений: 15,295

ЦитатаСообщение от Рыжий Лис Посмотреть сообщение

С вашей помощью смог написать пример, показывающий разницу:

Нет, не смогли. Скорее всего вы просто занимаетесь какой-то бессмыслицей, типа разглядывания кода, скомпилированного в отладочном режиме. И чтобы написать действительно показательный пример, нужно приложить определенные усилия.

Еще раз: ни в одном уважающем себя компиляторе никакой разницы не будет.

1 2 3 4 5 6 7 8
char a[5] = { 9, 7, 2, 0, 6 }; char b[5]; b[0] = 9; b[1] = 7; b[2] = 2; b[3] = 0; b[4] = 6;
1 2 3 4
mov DWORD PTR [rsp+6], 132873 mov BYTE PTR [rsp+10], 6 mov DWORD PTR [rsp+11], 132873 mov BYTE PTR [rsp+15], 6
1 2 3 4
mov byte ptr [rsp + 8], 6 mov dword ptr [rsp + 4], 132873 mov dword ptr [rsp + 11], 132873 mov byte ptr [rsp + 15], 6
1 2 3 4
mov dword ptr [rsp+38h],20709h mov byte ptr [rsp+3Ch],6 mov dword ptr [rsp+30h],20709h mov byte ptr [rsp+34h],6

Одинаково во всех случаях.
3802 / 2354 / 413
Регистрация: 09.09.2017
Сообщений: 10,212

ЦитатаСообщение от TheCalligrapher Посмотреть сообщение

Еще раз: ни в одном уважающем себя компиляторе никакой разницы не будет.

Естественно, разница будет. Инициализация глобальных переменных производится в одно и то же время при старте программы, где бы они ни были описаны, то есть компилятор имеет право инициализировать их все разом. А присваивания разбросаны по коду, причем в определенном порядке, то есть там такая оптимизация возможна не всегда.
Более того, если переменная объявлена глобальной и неинициализированной, произойдет сначала инициализация нулями, а уж потом, в коде, чем-то другим.
На примере AVR, так как его архитектура попроще:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
#if INIT char var[10] = {1,2,3,4,5,6,7,8,9,0}; #else char var[10]; #endif int main(){ #if !INIT var[0] = 0; var[1] = 1; var[2] = 2; var[3] = 3; var[4] = 4; var[5] = 5; var[6] = 6; var[7] = 7; var[8] = 8; var[9] = 9; #endif }
$ avr-gcc main.c -mmcu=atmega8 -gdwarf-2 -Os $ avr-objdump -S a.out
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
;=== зануляем глобальные переменные === 0000003a .do_clear_bss_loop>: 3a: 1d 92 st X+, r1 0000003c .do_clear_bss_start>: 3c: aa 36 cpi r26, 0x6A ; 106 3e: b2 07 cpc r27, r18 40: e1 f7 brne .-8 ; 0x3a 42: 02 d0 rcall .+4 ; 0x48 44: 19 c0 rjmp .+50 ; 0x78 ;=== собственно присваивание === var[0] = 0; 48: e0 e6 ldi r30, 0x60 ; 96 4a: f0 e0 ldi r31, 0x00 ; 0 4c: 10 82 st Z, r1 var[1] = 1; 4e: 81 e0 ldi r24, 0x01 ; 1 50: 81 83 std Z+1, r24 ; 0x01 var[2] = 2; 52: 82 e0 ldi r24, 0x02 ; 2 54: 82 83 std Z+2, r24 ; 0x02 var[3] = 3; 56: 83 e0 ldi r24, 0x03 ; 3 58: 83 83 std Z+3, r24 ; 0x03 var[4] = 4; 5a: 84 e0 ldi r24, 0x04 ; 4 5c: 84 83 std Z+4, r24 ; 0x04 var[5] = 5; 5e: 85 e0 ldi r24, 0x05 ; 5 60: 85 83 std Z+5, r24 ; 0x05 var[6] = 6; 62: 86 e0 ldi r24, 0x06 ; 6 64: 86 83 std Z+6, r24 ; 0x06 var[7] = 7; 66: 87 e0 ldi r24, 0x07 ; 7 68: 87 83 std Z+7, r24 ; 0x07 var[8] = 8; 6a: 88 e0 ldi r24, 0x08 ; 8 6c: 80 87 std Z+8, r24 ; 0x08 var[9] = 9; 6e: 89 e0 ldi r24, 0x09 ; 9 70: 81 87 std Z+9, r24 ; 0x09

А если использовать инициализацию:

$ avr-gcc main.c -mmcu=atmega8 -gdwarf-2 -Os -DINIT $ avr-objdump -S a.out
1 2 3 4 5 6 7 8 9 10 11 12 13 14
00000032 : 32: 10 e0 ldi r17, 0x00 ; 0 34: a0 e6 ldi r26, 0x60 ; 96 36: b0 e0 ldi r27, 0x00 ; 0 38: e8 e5 ldi r30, 0x58 ; 88 3a: f0 e0 ldi r31, 0x00 ; 0 3c: 02 c0 rjmp .+4 ; 0x42 3e: 05 90 lpm r0, Z+ 40: 0d 92 st X+, r0 42: aa 36 cpi r26, 0x6A ; 106 44: b1 07 cpc r27, r17 46: d9 f7 brne .-10 ; 0x3e 48: 02 d0 rcall .+4 ; 0x4e 4a: 04 c0 rjmp .+8 ; 0x54

Найти константы по адресам 0x58 в дизассемблерном файле с ходу не удалось, но вот они в собранном хексе:

:0A 0058 00 0102030405060708090071

Эксперт CЭксперт С++

11126 / 6084 / 1663
Регистрация: 18.10.2014
Сообщений: 15,295

ЦитатаСообщение от COKPOWEHEU Посмотреть сообщение

Инициализация глобальных переменных производится[. ]присваивания разбросаны по коду[. ]

Нет, конечно. Темой этого вопроса является разница в коде, вызванная локальной заменой инициализации на "эквивалентное" присваивание и обратно. Больше ничему.

Это, кстати, сразу же исключает из рассмотрения глобальные переменные, ибо для них в языке С это невозможно в принципе. Рассмотрение ограничено только локальными переменными, именно так как показал автор вопроса.

При этом всем понятно, что "присваивания, разбросанные по коду" никакого отношения к теме не имеют. Вопрос посвящен именно и только НЕ разбросанным по коду присваиваниям.

Просто Лис

Эксперт Python

5318 / 3332 / 1021
Регистрация: 17.05.2012
Сообщений: 9,765
Записей в блоге: 9

Вопрос скорее про особенности инициализации и что это даёт компилятору в отличии от присваивания. Думаю, ответ уже прозвучал: компилятор может оптимизировать "начальное заполнение памяти", так как ему известны местоположения всех переменных и их значения, поэтому он может заполнять их "большими кусками". Получается и меньше инструкций и быстрее работает.

3802 / 2354 / 413
Регистрация: 09.09.2017
Сообщений: 10,212

ЦитатаСообщение от TheCalligrapher Посмотреть сообщение

Это, кстати, сразу же исключает из рассмотрения глобальные переменные
Как это исключать наиболее яркий пример?

ЦитатаСообщение от TheCalligrapher Посмотреть сообщение

При этом всем понятно, что "присваивания, разбросанные по коду"

Даже если в моем коде внести объявление переменных внутрь тела main (разумеется, добавив volatile чтобы не соптимизировали в ноль), разница останется - копирование из флеша массивом против раздельной инициализации.

ЦитатаСообщение от Рыжий Лис Посмотреть сообщение

Думаю, ответ уже прозвучал: компилятор может оптимизировать "начальное заполнение памяти", так как ему известны местоположения всех переменных и их значения, поэтому он может заполнять их "большими кусками".

Не совсем так. Расположение в памяти ему известно в любом случае. Разница в том, что при присваивании порядок задан строго, а при инициализации - нет. Поэтому присваивать две переменные за раз компилятор не имеет права, а вот инициализировать - запросто.
Исключение - когда компилятор посчитал, что порядок ни на что не повлияет. И на самом деле это исключение случается даже чаще, чем обязательность порядка.

Эксперт CЭксперт С++

11126 / 6084 / 1663
Регистрация: 18.10.2014
Сообщений: 15,295

ЦитатаСообщение от Рыжий Лис Посмотреть сообщение

Думаю, ответ уже прозвучал: компилятор может оптимизировать "начальное заполнение памяти", так как ему известны местоположения всех переменных и их значения, поэтому он может заполнять их "большими кусками". Получается и меньше инструкций и быстрее работает.

Это утверждение не имеет смысла. Компилятору всегда известны местоположения всех переменных и их значения. Никакой разницы между инициализацией и присваиванием это, разумеется, не создает.

Еще раз: в тех ситуациях, когда существует выбор между инициализацией или присваиванием, нет абсолютно никакой разницы в генерируемом компиляторе коде при замене одного варианта на другой. "Заполнение" и там и там будет совершенно одинаковым, количество инструкции будет совершенно одинаковым и работать все будет совершенно одинаково. (См. правда, одно исключение ниже)

Это и есть тот ответ, который "уже прозвучал".

Добавлено через 8 минут

ЦитатаСообщение от COKPOWEHEU Посмотреть сообщение

разница останется - копирование из флеша массивом против раздельной инициализации.

Если ваш компилятор делает раздельную инициализацию там, где можно было инициализировать все оптом - то это не более чем побочный эффект либо каких-то выбранных вами установок компиляции, либо просто недоделка компилятора. AVR-GCC хорошо известен тем, что это довльно слабый компилятор в плане оптимизации и кодогенерации. Ваш пример как раз это демонстрирует.

Кстати, в AVR-GCC действительно есть известная неприятная недоделка, которая в ряде контекстов делает присваивание намного более предпочтительным, чем инициализация - это "частичная" инициализация больших агрегатов. Т.е. что-то вроде

char a[1024] = "Hello";

Язык требует, чтобы в такой ситуации весь хвост массива был заполнен нулями (хоть это нам может быть и не нужно).

GCC почему-то не соображает, что намного выгоднее выполнить это требование языка путем заполнения остатка массива нулями просто в коде, циклом. Это выгоднее и с точки зрения размера, и с точки зрения скорости кода. Но вместо этого GCC хранит образ всего 1024-байтного массива (состоящий в основном из нулей) в флэше и копирует его оттуда. Это плохо.

Но даже если GCC исправит эту недоделку, все равно стоит помнить, что во многих случаях именно ситуация "частичная инициализация больших агрегатов" - это та ситуация, когда, возможно, следует предпочитать присваивание, а не инициализацию. Ибо во многих случаях нам просто не нужно обнуление всего хвоста агрегата, как и в примере со строкой выше. Использование присваивания вместо инициализации может быть на порядок(ки) эффективнее.

3802 / 2354 / 413
Регистрация: 09.09.2017
Сообщений: 10,212

ЦитатаСообщение от TheCalligrapher Посмотреть сообщение

Если ваш компилятор делает раздельную инициализацию там, где можно было инициализировать все оптом - то это не более чем побочный эффект

Инициализацию да, присваивание - нет. Порядок операций это порядок операций.
Вот вам пример на х64, раз не доверяете AVR:

1 2 3 4 5 6 7 8 9 10 11 12 13 14
int main(){ volatile char var1[10] = {0,1,2,3,4,5,6,7,8,9}; volatile char var2[10]; var2[0]=0; var2[1]=1; var2[2]=2; var2[3]=3; var2[4]=4; var2[5]=5; var2[6]=6; var2[7]=7; var2[8]=8; var2[9]=9; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
volatile char var1[10] = {0,1,2,3,4,5,6,7,8,9}; 1040: 48 b8 00 01 02 03 04 movabs $0x706050403020100,%rax 1047: 05 06 07 104a: 48 89 44 24 e2 mov %rax,-0x1e(%rsp) var2[5]=5; var2[6]=6; var2[7]=7; var2[8]=8; var2[9]=9; } 104f: 31 c0 xor %eax,%eax volatile char var1[10] = {0,1,2,3,4,5,6,7,8,9}; 1051: 66 c7 44 24 ea 08 09 movw $0x908,-0x16(%rsp) var2[0]=0; 1058: c6 44 24 ec 00 movb $0x0,-0x14(%rsp) var2[1]=1; 105d: c6 44 24 ed 01 movb $0x1,-0x13(%rsp) var2[2]=2; 1062: c6 44 24 ee 02 movb $0x2,-0x12(%rsp) var2[3]=3; 1067: c6 44 24 ef 03 movb $0x3,-0x11(%rsp) var2[4]=4; 106c: c6 44 24 f0 04 movb $0x4,-0x10(%rsp) var2[5]=5; 1071: c6 44 24 f1 05 movb $0x5,-0xf(%rsp) var2[6]=6; 1076: c6 44 24 f2 06 movb $0x6,-0xe(%rsp) var2[7]=7; 107b: c6 44 24 f3 07 movb $0x7,-0xd(%rsp) var2[8]=8; 1080: c6 44 24 f4 08 movb $0x8,-0xc(%rsp) var2[9]=9; 1085: c6 44 24 f5 09 movb $0x9,-0xb(%rsp)

Как и следовало ожидать: инициализация идет сразу блоком, а присваивание поэлементно.
А вот если убрать volatile, то есть позволить компилятору больше свободы, разница пропадает.

200 / 236 / 33
Регистрация: 29.03.2019
Сообщений: 667

ЦитатаСообщение от Рыжий Лис Посмотреть сообщение

Вопрос скорее про особенности инициализации и что это даёт компилятору в отличии от присваивания.

Вопрос лишен смысла как такового. Инициализация есть абстракция уровня дизайна ЯП. Присваивание есть абстракция уровня реализации ЯП. Инициализация есть идея, присваивание есть один из методов реализации в том числе и этой идеи. Наличие фичи не "дает что-то компилятору", а просто обязывает строить компиляторы соблюдая некие соглашения, семантика которых описана в стандарте.

3802 / 2354 / 413
Регистрация: 09.09.2017
Сообщений: 10,212

ЦитатаСообщение от zeroalef Посмотреть сообщение

Инициализация есть идея, присваивание есть один из методов реализации в том числе и этой идеи.

Но не единственный, даже в рамках одного компилятора, о чем и речь. И прямым поэлементным присваиванием компилятор вынужден пользоваться, если ему не оставляют альтернатив. Если же ему разрешена оптимизация группы переменных, он, скорее всего, воспользуется групповым копированием.
Именно это наблюдается в моем примере: ключевое слово volatile запрещает оптимизацию доступа к данной переменной, поэтому во время присваивания компилятор вынужден работать с переменными по одной. А вот инициализация идет как бы вся одновременно перед началом выполнения кода, поэтому там оптимизировать можно, чем компилятор с радостью пользуется.
.
И пытаться уйти в философию, мол "все зависит от точки зрения" лишено смысла: от таких рассуждений код оптимальнее не станет, а вот от понимания подобных механизмов - вполне.

200 / 236 / 33
Регистрация: 29.03.2019
Сообщений: 667

ЦитатаСообщение от COKPOWEHEU Посмотреть сообщение

И пытаться уйти в философию, мол "все зависит от точки зрения" лишено смысла

Это не попытки. Это так и есть. Вот смотрите, в CS есть такие термины как инициализация, полиморфизм и т.д. Но как эти идеи будут реализованы в конкретном ЯП и вообще будут ли они в ЯП зависит от изначального проекта (дизайна) ЯП. Например, инициализация -- термин доступный для любого ЯП, содержащего в себе понятие "переменная" (variable). Но термин "присваивание" доступен только в императивных языках. В императивных ЯП, коим и является Си, инициализация осуществляется через присваивание. В функциональных такое понятие как "присаивание" отсутствует напрочь, но есть понятие "связывание" (binding) значения с переменной, которое с легкостью можно охарактеризовать в том числе и как инициализацию. Особенности и тонкости построения набора машинных инструкций, байткода нас не интересуют на данном этапе абстракции пока мы не засели за свой компилятор условного языка Oz. Главное что нам известно это то что при декларации некоторой переменной она 100% будет инициализирована определенными значениями. Это все что нам надо знать об инициализации.

Эксперт CЭксперт С++

11126 / 6084 / 1663
Регистрация: 18.10.2014
Сообщений: 15,295

ЦитатаСообщение от COKPOWEHEU Посмотреть сообщение

Инициализацию да, присваивание - нет. Порядок операций это порядок операций.

Ну ну ну. "Порядок операций" в языке С - это эфемерная воображаемая концепция, которая существует только для того, чтобы описывать поведение воображаемой Абстрактной С Машины. Никакого прямого отношения к генерации физического кода этот "порядок операций" не имеет.

В языке С есть только один реальной "порядок операций" - это правило "AS IF", т.е. наблюдаемое поведение программы (observable behavior). А именно: выполнение операций ввода-вывода и выполнение доступов к volatile объектам. Если не оговорено иначе, порядок этих операций в реальной программе должен совпадать со стандартной Абстрактной С Машиной. Больше никакого "порядка операций" физически не существует.

Вы это уже и сами должны были понять. Видите, как вы в своем последнем примере втихаря сделали все объекты volatile , в надежде, что никто не заметит? Хотя ни слова ни о каком volatile пока не шло, и к рассматриваемой теме совершенно непоказательный пример с volatile никакого отношения не имеет. Но без этого компилятор не хотел соглашаться с вашими "теориями" и вы занялись такой наивной подтасовкой ))

Точно таким же абстрактными концепциями, как "порядок операций", являются "инициализация" и "присваивание". И если в языке С++ между ними есть физически осязаемая разница, то в языке С такой разницы нет. Функционально-эквивалентная замена одного на другое в С порождает один и тот же код.

Присваивание и инициализация в Java

Java-университет

Основное предназначение компьютерных программ - обработка данных. Чтобы обработать данные нужно их как-то хранить. Предлагаю разобраться с тем, как происходит хранение данных.

Присваивание и инициализация в Java - 1

Переменные

  • Поля (fields) : переменные, объявленные в классе;
  • Локальные переменные (local variables) : переменные в методе или в блоке кода;
  • Параметры (parameters) : переменные в объявлении метода (в сигнатуре).
  • Тип переменной показывает, какие данные представляет данная переменная (т.е. какие данные может хранить). Как мы знаем, тип переменной может быть примитивным (primitives primitives) или объектным, не примитивными (Non-primitive). При объектных переменных их тип описывается определённым классом.
  • Название переменной должно быть с маленькой буквы, в camel case. Подробнее про именование можно прочитать в "Variables:Naming".

Объявление переменной (Declaration)

Итак, мы вспомнили, что такое переменная. Для того, чтобы с переменной начать работать нужно её объявить. Для начала, разберёмся с локальной переменной. Вместо IDE для удобства воспользуемся онлайн решением от tutorialspoint: Online IDE. Выполним в их online IDE вот такую простенькую программку:

 public class HelloWorld < public static void main(String []args)< int number; System.out.println(number); >> 

Итак, как видно, мы объявили локальную переменную с именем number и типом int . Нажимаем кнопку «Execute» и получаем ошибку:

 HelloWorld.java:5: error: variable number might not have been initialized System.out.println(number); 
  • Обращение к локальным переменным должно быть выполнено только после того, как они будут инициализированы;
  • Локальные переменные не имеют значений по умолчанию;
  • Проверка значений локальных переменных выполняется в момент компиляции.

Инициализация локальной переменной

Инициализация переменных одна из самых мудрёных тем в Java, т.к. очень тесно связана с работой с памятью, с реализацией JVM, спецификацией JVM и другими не менее страшными и хитрыми вещами. Но можно попробовать разобраться хоть в какой-то мере. Пойдём от простого к сложному. Чтобы инициализировать переменную воспользуемся оператором присваивания и изменим строчку в нашем прошлом коде:

 int number = 2; 

В таком варианте ошибок не будет и на экран выведется значение. Что же происходит в этом случае? Давайте попробуем порассуждать. Если мы хотим присвоить переменной какое-то значение, значит мы хотим, чтобы эта переменная хранила значение. Получается, что значение где-то должно храниться, но где? На диске? Но это очень медленно и может на нас накладывать ограничения. Получается, единственное, где мы можем быстро и эффективно хранить данные «здесь и сейчас» это память. Значит, нам нужно выделить в памяти какое-то место. Так и есть. При инициализации переменной под неё будет выделено место в памяти, отведённой java процессу, в рамках которого будет выполняться наша программа. Память, выделяемая java процессу, разделена на несколько областей или зон. В какой из них будет выделено место зависит от того, какого типа была объявлена переменная. Память разделяется на следующие разделы: Heap, Stack и Non-Heap. Начнём со стэковой памяти. Stack переводится как стопка (например, стопка книг). Представляет собой LIFO структуру данных (Last In, First Out). То есть как стопка книг. Когда мы добавляем в неё книги – мы кладём их сверху, а когда забираем – берём верхнюю (т.е. ту, которая добавлена самой последней). Итак, мы запускаем нашу программу. Как мы знаем, Java программу выполняет JVM, то есть виртуальная Java машина. JVM должна знать то, откуда должно начаться выполнение программы. Для этого мы объявляем main метод, который называется «точкой входа». Для выполнения в JVM создаётся основной поток (Thread). При создании потока ему выделяется свой стэк в памяти. Этот стэк состоит из фрэймов. При выполнении каждого нового метода в потоке под него будет выделен новый фрэйм и добавлен на вершину стэка (как новая книжка в стопке книг). Этот фрэйм будет содержит ссылки на объекты и примитивные типы. Да да, наш int будет храниться в стэке, т.к. int это примитивный тип. Прежде чем выделить фрэйм JVM должна понимать, что туда сохранять. Именно по этой причине мы получим ошибку «variable might not have been initialized», ведь если она не инициализирована, то JVM не сможет нам подготовить стэк. Поэтому при компиляции программы умный компилятор поможет нам не допустить ошибку и не сломать всё. (!) Для наглядности советую супер-пупер статью: "Java Stack and Heap: Java Memory Allocation Tutorial". В ней ссылаются на не менее крутое видео:

После завершения выполнения метода из стэка потока будут удаляться фрэймы, выделенные под эти методы, а вместе с ними и очищаться память, выделенная под этот фрэйм со всеми данными.

Инициализация локальных объектных переменных

Давайте опять изменим наш код на чуть более хитрый:

 public class HelloWorld < private int number = 2; public static void main(String []args)< HelloWorld object = new HelloWorld(); System.out.println(object.number); >> 

Что же тут будет происходить? Давайте ещё раз рассуждать. JVM узнает о том, откуда ей выполнять программу, т.е. она видит main метод. Она создаёт поток, под него выделяет память (потоку ведь надо где-то хранить данные, которые нужны для выполнения). В этом потоке выделяется фрэйм под метод main. Далее мы создаём объект HelloWorld. Этот объект уже создаётся не в стэке, а в хипе. Потому что object у нас не примитивный тип, а объектный. А в стэке будет храниться только ссылка на объект в хипе (мы ведь как-то должны обращаться к этому объекту). Далее в стэке метода main будут выделены фрэймы для выполнения метода println. После выполнения метода main будут уничтожены все фрэймы. При уничтожении фрэйма будут уничтожены все данные. Объект object не будет уничтожен сразу. Сначала на него будет уничтожена ссылка и таким образом на объект object больше никто ссылаться не будет и доступа больше к этому объекту в памяти будет не получить. Умная JVM имеет свой механизм для такого – сборщик мусора (garbage collector или сокращённо GC). Он то и удаляет из памяти такие объекты, на которые больше никто не ссылается. Данный процесс опять же был описан в ссылке, что была приведена выше. Там даже видео есть с объяснением.

Инициализация полей

Инициализация полей, указанных в классе происходит особым образом в зависимости от того, является ли поле статическим или нет. Если у поля стоит ключевое слово static, то данное поле относится к самому классу, а не слово static не указано, то данное поле относится к экземпляру класса. Давайте рассмотрим это на примере:

 public class HelloWorld < private int number; private static int count; public static void main(String []args)< HelloWorld object = new HelloWorld(); System.out.println(object.number); >> 

В данном примере, инициализация полей происходит в разное время. Поле number будет инициализировано после того, как будет создан объект object класса HelloWorld. А вот поле count будет инициализировано тогда, когда класс будет загружен виртуальной Java машиной. Загрузка классов – это отдельная тема, поэтому не будем сюда примешивать её. Просто стоит знать, что статические переменные инициализируются тогда, когда о классе становится известно при выполнении. Тут важнее другое и Вы уже это заметили. Мы нигде не указали значения, а оно работает. И действительно. Переменные, которые являются полями, если для них не указано значение, то они инициализируются значением по умолчанию. Для числовых значением это 0 или 0.0 для чисел с плавающей точкой. Для boolean это false. А для всех переменных объектных типов значение будет null (об этом мы ещё поговорим). Казалось бы, а почему так? А потому, что объекты создаются в Heap (в куче). Работа с данной областью выполняется в Runtime. И мы в runtime можем инициализировать эти переменные, в отличии от стэка, память под который должна быть подготовлена ещё до выполнения. Так устроена работа с памятью в Java. Но есть тут и ещё одна особенность. В этом маленьком кусочке затрагиваются разные уголки памяти. Как мы помним, в Stack памяти под метод main выделяется фрэйм. В этом фрэйме хранится ссылка (reference) на объект в Heap памяти. Но где тогда хранится count? Как мы помним, эта переменная инициализируется сразу, до создания объекта в хипе. Вот тут действительно хитрый вопрос. До Java 8 существовала область памяти, называемая PERMGEN. Начиная с Java 8 эта область претерпела изменения и называется METASPACE. По сути, статические переменные являются частью описания класса, т.е. его метаданными. Поэтому, логично, что хранится в хранилище метаданных, METASPACE. MetaSpace относится к той самой Non-Heap области памяти, является её частью. Важно ещё учитывать то, что учитывается порядок, в котором объявлены переменные. Например, в этом коде ошибка:

 public class HelloWorld < private static int b = a; private static int a = 1; public static void main(String []args)< System.out.println(b); >> 

Что такое null

Как было сказано выше, переменные объектных типов, если они являются полями класса, инициализируются значениями по умолчанию и таким значением по умолчанию является null. Но что же такое null в Java? Первое что важно помнить – примитивные типы не могут быть null. А всё потому, что null – это особенная ссылка (reference), которая не ссылается никуда, ни на какой объект. Поэтому, только объектная переменная может быть равна null. Второе, что важно понимать, что null – это ссылка, reference. Я reference тоже имеют свой вес. На эту тему можно почитать вопрос на stackoverflow: "Does null variable require space in memory".

Блоки инициализации

Рассматривая инициализацию переменных грех не рассмотреть блоки инициализации. Выглядит это следующим образом:

 public class HelloWorld < static < System.out.println("static block"); > < System.out.println("block"); >public HelloWorld () < System.out.println("Constructor"); >public static void main(String []args) < HelloWorld obj = new HelloWorld(); >> 

Порядок вывода будет: static block, block, Constructor. Как мы видим, блоки инициализации выполняются раньше, чем конструктор. И иногда это может быть удобным средством для инициализации.

чем отличается присваивание от инициализации?

Оставим, так сказать, за кадром массу проблем в вашем коде. Отличие в том, что во втором случае сначала создается "пустой" объект, а затем выполняется присваивание, а в первом — сразу создается нужный объект.

18 фев в 14:55

2 ответа 2

Сортировка: Сброс на вариант по умолчанию

Инициализация происходит один раз при вызове конструктора, присвоение - каждый раз при вызове оператора = .

На строке sample ob = input(); происходит инициализация (с использованием нежелательного синтаксиса copy initialization). Вместо этого во всех случаях следует использовать синтаксис direct list initialization sample ob; .

На строке ob = input(); происходит присваивание. Оно заканчивается плохо, так как оператор = для класса не переопределен надлежащим образом для корректного обращения с динамически выделенной памятью. Для любых не POD классов оператор = следует либо определять, либо запрещать. см правило 0/3/5

Отслеживать
ответ дан 18 фев в 15:10
user7860670 user7860670
29.6k 3 3 золотых знака 17 17 серебряных знаков 36 36 бронзовых знаков

Функция присваивания подразумевает аккуратно избавляться от текущего состояния объекта и присваивания нового.
Так как вы сами написали конструктор копирования, то оператор присваивания по-умолчанию не будет реалезован и вы должны сами его написать.
А-ля :

sample & sample :: operator = ( sample const & obj ) < // здесь и присутствует отличие от конструктора delete [ ] s ; s = new char[strlen(obj.s) + 1]; strcpy_s(s, strlen(obj.s) + 1, obj.s); cout

плохую функцию set можете удалить.

Отслеживать
ответ дан 18 фев в 19:19
17.1k 1 1 золотой знак 9 9 серебряных знаков 33 33 бронзовых знака

  • c++
  • функции
  • перегрузка-операторов
  • return
    Важное на Мете
Похожие

Подписаться на ленту

Лента вопроса

Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.

Дизайн сайта / логотип © 2023 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2023.10.27.43697

Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *