Новые типы данных. Приведение типов указателей

Практический курс по C/C++: https://stepik.org/course/193691

Продолжаем изучение дополнительных возможностей языка С++ и это занятие начнем с рассмотрения некоторых дополнительных типов данных. Сразу отмечу, что в С++ присутствуют все стандартные (базовые) типы языка Си, но дополнительно введено еще несколько:

  • bool – булевый тип (1 байт), принимающий два состояния true/false;
  • wchar_t – расширенный символьный тип (2 байта в ОС Windows; 4 байта в ОС Linux);
  • char8_t – символьный тип (1 байт) для символов кодировки Unicode (UTF-8) (добавлен в стандарте С++20);
  • char16_t – символьный тип (2 байта) для символов кодировки Unicode (UTF-16);
  • char32_t – символьный тип (4 байта) для символов кодировки Unicode (UTF-32).

Наибольший интерес здесь представляет тип bool, которого не хватало в языке Си. Пользоваться им очень просто. Вначале объявляется переменная этого типа, например:

bool fl_print = false;

а, затем, можно изменить ее значение на true:

fl_print = true;

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

    if (fl_print) {
        std::cout << "Hi!" << std::endl;
    }

Вообще, значение false соответствует целому числу 0, а true – единице. Поэтому, запись вида:

fl_print = 1;

будет эквивалентна предыдущей. Тип bool и ключевые слова true/false введены для удобства написания и понимания программы. Принципиально нового на уровне машинных кодов ничего не появляется.

Следующий тип wchar_t используется для представления символов, коды которых выходят за пределы одного байта (типа char). Как мы знаем, это, например, могут быть символы кодировки Unicode. Если записать:

wchar_t wch;
wch = L'Я';

и вывести значение кода буквы ‘Я’:

std::cout << wch << std::endl;

то увидим число 1071. Это значение символа ‘Я’ в кодировке UTF8, т. к. текст программы сохранен в файле в этой кодировки. Причем, смотрите, если мы попытаемся этот же символ присвоить переменной типа char:

char ch = 'Я';

то компилятор выдаст предупреждение, что код символа выходит за диапазон типа char. А вот с латинскими символами таких проблем уже не возникает:

char ch = 'd';

так как их коды не превышают 127.

Еще одно важное отличие языка С++ от Си состоит в том, что символьные литералы, записанные в программе, приводятся компилятором к типу char, а не int, как это было в языке Си. Во всем остальном, работа с отдельными символами или их последовательностями остается прежней. Например, можно объявить строковый литерал следующим образом:

char str[] = "Привет мир!";

и вывести это сообщение в консоль:

std::cout << str << std::endl;
std::cout << sizeof(str) << std::endl;

Увидим строку «Привет мир!» и число 21. Почему размер массива равен 21, а не 11 по числу символов? Очевидно, что под русские символы здесь отводится два байта (кодировка UTF8), поэтому и размер становится больше.

Если мы далее запишем:

char ch = str[0];
printf("ch = %c\n", ch);

то увидим вывод не символа буквы ‘П’, а кракозябру. Но, если прописать:

wchar_t str[] = L"Привет мир!";
wchar_t ch = str[0];
 
std::cout << ch << std::endl;

то переменная ch будет содержать корректный код первого символа строки.

Обратите внимание, все эти изменения актуальны для кодировок, отличающихся от кодировки ASCII, где символы латинского и русского алфавитов умещались в диапазон [0; 255], то есть, в один байт. Если, например, используется кодовая страница Windows-1251, то символы можно по-прежнему обрабатывать переменными типа char.

Приведение типов указателей

Следующее важное отличие языка С++ от Си проявляется в приведении разных типов указателей друг к другу. Пусть, например, объявлены два следующих указателя:

int* ptr_i = 0L;
char* ptr_ch = 0L;

В языке Си, далее, мы могли бы записать команду:

ptr_ch = ptr_i;

и она бы скомпилировалась с предупреждением, что типы указателей не совпадают. Язык С++ работает строже. Если типы указателей не совпадают, то компиляция останавливается с сообщением об ошибке. Поэтому здесь нам нужно явно прописывать операцию приведения типов:

ptr_ch = (char *)ptr_i;

сообщая компилятору, что мы знаем, что делаем и адрес указателя ptr_i нужно присвоить указателю ptr_ch.

Та же самая ситуация возникает и с обобщенным указателем *void языка Си:

void* ptr_void = 0L;

Чтобы присвоить адрес, который хранится в ptr_void, нужно также явно прописывать операцию приведения типов:

ptr_i = (int *)ptr_void;

Напомню, что в языке Си использование указателей *void с другими типами даже не приводило к предупреждениям. В С++ ситуация изменилась кардинально. В частности, из-за этого при выделении памяти с помощью функции malloc() нужно дополнительно записывать приведение типов:

ptr_i = (int *)malloc(sizeof(int) * 5);

Далее, в С++ не принято использовать константу NULL для указания нулевого адреса. Пишется просто числовой литерал 0L. Начиная со стандарта С++11 был введен специальный «нулевой» указатель с ключевым словом nullptr:

char* ptr_ch = nullptr;
printf("%p\n", ptr_ch);

В данном случае видим все то же значение 0.

Если версия компилятора языка С++ позволяет использовать nullptr, то рекомендуется его применять для обозначения «нулевого» указателя.

В заключение этого занятия отмечу еще одно популярное нововведение в языке С++ - новую форму записи цикла for для перебора коллекций. Давайте я покажу его на конкретном примере. Пусть у нас имеется строка:

char msg[] = "I like C++ language";

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

for (char x : msg) 
    std::cout << x << " ";

После запуска увидим:

I   l i k e   C + +   l a n g u a g e

То есть, массив msg был последовательно (от начала до конца) перебран и на каждой итерации в переменную x помещался текущий символ. В теле цикла символы выводились через пробел. Видите, как удобно можно проходить по коллекциям целиком, используя новую форму записи цикла for. Причем, цикл проходит именно по массиву, а не по строке. Например, если указать больший размер массива:

char msg[30] = "I like C++ language";

то при выводе дополнительно увидим значение NUL в консоли, которые присутствуют в массиве msg. Это же касается и всех других коллекций, например, динамических массивов (std::vector) или связных списков (std::list) и многих других последовательностей библиотеки STL.

На следующем занятии мы продолжим знакомиться с дополнительными возможностями языка С++, которые качественно отличают его от языка Си.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме