Практический курс по C/C++: https://stepik.org/course/193691
Продолжаем
рассматривать основные отличия языка С++ от Си. Следующим шагом будет логично
рассмотреть способы инициализации переменных в С++. Напомню, что в языке Си
инициализация записывается с помощью оператора ‘=’ при объявлении переменной.
Например:
int val = 0; // инициализация
int pow[] = {1, 2, 4, 8};
Если же символ ‘=’
используется после объявления переменной:
double d;
d = 5.78; // операция присваивания
то он уже
определяет операцию присваивания.
Эти две операции
нужно четко себе различать, так как они работают по-разному. Так вот, в языке
С++, чтобы четко, синтаксически отделить одну операцию от другой, введен новый
синтаксис и новые операции инициализации переменных. Помимо уже знакомого нам
оператора ‘=’ инициализацию можно выполнять, по крайней мере, еще двумя
распространенными способами:
short sh(10); // functional notation
double d(-4.37);
char ch{'b'}; // braced initialization
long lv{};
При выводе
значений этих переменных:
cout << sh << " " << d << endl;
cout << ch << " " << lv << endl;
увидим:
10
-4.37
b 0
То есть,
переменным были присвоены значения, указанные в скобках. Если же в скобках
ничего не прописано, то переменная инициализируется нулем.
Вообще, в языках
Си и С++ начальная инициализация переменных – это рекомендуемая практика.
Напомню, что все локальные переменные, размещаемые в стековом фрейме (их еще
называют автоматическими), могут принимать неопределенное начальное значение.
Конкретное поведение зависит от компилятора С++. Поэтому, чтобы не возникало
непредвиденных ошибок из-за неопределенного начального значения переменной, ее
следует сразу инициализировать. Именно для этого введены операторы круглых и
фигурных скобок.
Но здесь
возникает вопрос, чем отличаются эти два способа инициализации? На самом деле,
почти ничем, но при использовании фигурных скобок дополнительно происходит
проверка типа инициализируемых данных. Например, если у переменной lv в фигурных
скобках прописать вещественное число:
то компилятор
выдаст ошибку из-за несовпадения типов переменной lv (long) и числа 5.43 (double). Причем, тип double нельзя без
потерь привести к типу long. Но, если фигурные скобки заменить на
круглые:
то ошибки не
будет и переменная lv примет значение 5, то есть, будет
выполнено неявное приведение типа double к типу long.
Для полноты
картины приведу еще несколько примеров инициализации переменных указанными
способами:
int sum {2 + 3 + 4 + 5};
double p (1 * 2.3 * 4.5 - 1);
bool n_fl(false), t_fl(true);
То есть, в инициализаторе
можно прописывать любые допустимые конструкции языка С++.
Особенность модификатора const в С++
Я думаю, с
инициализацией переменных, в целом, все понятно, поэтому перейдем к следующему
отличию использования модификатора const. Как мы знаем
из курса по Си с помощью ключевого слова const можно
определять неизменяемые переменные, то есть, константы. Например, так:
const double pi {3.1415};
В С++ этот
модификатор используется с теми же целями, но есть одно небольшое отличие при
его использовании. Константная переменная должна быть инициализирована в момент
ее объявления. То есть, запись, которая была допустима в языке Си:
в С++ приведет к
ошибке при компиляции программы. Во всем остальном он работает так же, как и в
языке Си. И это не удивительно, так как ключевое слово const перешло в Си из
языка С++.
Вычисляемый тип (auto и decltype)
В языке С++,
начиная со стандарта С++11, ключевое слово auto радикально
изменило свое назначение. Если раньше в язык Си оно вводилось для явного
обозначения автоматических переменных, то есть, тех, что располагаются в
стековом фрейме, то теперь с его помощью можно объявлять переменные
вычисляемого типа. О чем здесь речь? Давайте я покажу это на конкретном
примере. С помощью ключевого слова auto мы можем объявлять
переменные следующим образом:
auto i = -100; // тип int
auto d = 76.98; // тип double
auto g = 0.55f; // тип float
auto h = 'f'; // тип char
Во всех случаях
компилятор сначала вычисляет тип данных, которым инициализируется переменная, а
затем, вместо auto подставляет
этот тип. В итоге получаем в приведенном примере типы: int, double, float и char.
Или, могут быть
более сложные выражения, например:
auto i = 1 + 2 - 10; // тип int
auto d = 1 + i + 3.0; // тип double
auto g = 55u; // тип unsigned int
auto h = (short)10 + 100000UL; // тип unsigned long
Здесь также
вначале компилятор вычисляет тип инициализируемых данных и только потом вместо
ключевого слова auto подставляет вычисленный тип.
Но, как всегда,
здесь есть ряд важных нюансов. Давайте объявим следующие три переменных:
int *ptr = nullptr;
int k;
int& lk = k;
и посмотрим,
какой тип будет вычисляться компилятором с их использованием:
auto t1 = k; // int
auto t2 = *ptr; // int
auto t3 = ptr; // int *
auto t4 = &ptr; // int **
auto t5 = lk; // int
Пока
использовались обычные переменные и указатели, все ожидаемо, но для ссылки
получили обычный тип int, а не ссылку.
Далее, если
перед типами прописать ключевое слово const:
const int *ptr = nullptr;
const int k = 0;
const int& lk = k;
то результат
будет следующим:
auto t1 = k; // int
auto t2 = *ptr; // int
auto t3 = ptr; // const int *
auto t4 = &ptr; // const int **
auto t5 = lk; // int
В целом,
ожидаемо. Компилятор старается найти наиболее общий тип при учете ссылок и
модификаторов. Поэтому везде, где это допустимо, ключевое слово const отбрасывается
при вычислении типа переменной. Очевидно, его нельзя отбросить для переменных t3 и t4, т. к. здесь
объявляются указатели через константный указатель ptr. Во всех
остальных случаях его можно не учитывать.
Однако если для
нас важно сохранить полную идентичность типа переменной, например, ссылку
оставить ссылкой, или не отбрасывать модификатор const, то при
объявлении переменных можно воспользоваться еще одним новым ключевым словом decltype следующим
образом:
decltype(k) var1 = 1; // const int
decltype(ptr) var2; // const int *
decltype(lk) var3 = k; // const int &
Конечно, здесь
возникает вопрос, зачем это нужно? Разве не проще явно объявлять типы
переменных, как это делалось до сих пор? На самом деле, есть ряд ситуаций,
когда использование auto несколько облегчает написание программного кода. Часто
это касается использования STL-коллекций со сложными шаблонными типами
данных. Например, в программе можно объявить двусвязный список, содержащий
числа, инициализировать его и перебрать обычным циклом for:
#include <iostream>
#include <list>
int main()
{
std::list<short> dg = {-3, -2, 0, 2, 3};
for(std::list<short>::iterator i = dg.begin(); i != dg.end(); ++i)
std::cout << *i << " ";
return 0;
}
Смотрите, какой тип
(std::list<short>::iterator) приходится
прописывать у переменной i. Это не очень удобно. Как раз в
подобных случаях проще прописать ключевое слово auto и компилятор
сам автоматически подставит нужный тип переменной:
for(auto i = dg.begin(); i != dg.end(); ++i)
std::cout << *i << " ";
И результат
будет абсолютно такой же.
Конечно,
злоупотреблять этим не стоит. Все же, явное обозначение чаще лучше неявного. И
ключевое слово auto имеет ряд ограничений. Например, его нельзя (в ряде
компиляторов) использовать при определении параметров функции:
Компилятор GCC выдает
предупреждение, но другие могут приводить к ошибке. Поэтому вместо auto следует
использовать или перегрузку функций, или определять шаблон функции.
Я не стану
дальше углубляться в эту тему. Общей информации для понимания ключевых слов auto и decltype пока достаточно.
В дальнейшем, когда в практике программирования вам действительно понадобится
их использовать, значит, вы доросли до такого уровня, когда легко сможете более
глубоко усвоить этот материал.
Практический курс по C/C++: https://stepik.org/course/193691