§ 12. Базовые типы данных

Введение

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

02 Язык программирования C++ является строго типизированным. Это означает, что каждый элемент данных, который хранится в памяти, имеет свой тип. Типы данных C++, которые соответствуют форматам данных, с которыми работает процессор, называются примитивными. Различные типы могут иметь одинаковые форматы представления данных в памяти. Основные форматы включают в себя целые числа, числа с плавающей точкой (дробные) и байтовые массивы. Байтовый массив соответствует структуре оперативной памяти, которая сама представляет собой байтовый массив, а целые и дробные числа соответствуют формату регистров процессора.

03 В языке C++ тип данных определяет множество операций, которые можно с данными этих типов производить. Для байтовых массивов определены только операции копирования, перемещения и заполнения, для целых и дробных чисел в дополнение к ним определены арифметические операции, а для дробных чисел в дополнение к арифметическим и байтовых операциям определены тригонометрические, логарифмические и другие операции, которые имеют смысл только для дробных чисел (взятие дробной части, sin, cos, exp, log и т.д.). Большинство этих операций также реализованы в процессоре.

04 В рамках формата представления в памяти типы данных отличаются, как правило, только размером. Посколько размер зависит от архитектуры процессора, для которого пишется программа, то на разных процессорах размеры могут отличаться. В таблице перечислены примитивные типы данных C++, их формат представления в памяти и размер для архитектуры x86_64.

Примитивный типФормат представления в памятиРазмер для процессора архитектуры x86_64, байт
boolцелое число1
charцелое число1
shortцелое число2
intцелое число4
longцелое число8
long longцелое число8
floatчисло с плавающей точкой (дробное)4
doubleчисло с плавающей точкой (дробное)8
Примитивные типы данных в C++.

Константы и переменные

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

06 Для того чтобы создать переменную или константу, ее нужно сначала объявить, — то есть обозначить, что есть такая область памяти с таким именем, — а потом определить, — то есть загрузить в эту область конкретные данные. Как правило объявление и определение переменных делается совместно, а для констант они всегда делаются совместно, потому что константа не может не иметь значения. Ниже показаны примеры объявления и определения переменных и констант.

// объявить переменную с именем i, которая имеет тип int и определить ее равной 0
int i = 0;
// объявить переменную с именем j, которая имеет тип int и значение которой не определено
long j;
// объявить переменную с именем x, которая имеет тип float и определить ее равной числу 1.123
float x = 1.123;
Объявление и определение переменных и констант.

В коде выше 1.123 и 0 являются константами соответствующих типов, которые объявлены и определены неявно путем записи их непосредственных значений, и у них нет имен (можно считать, что сами значения являются их именами).

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

// объявить переменную с именем i, которая имеет тип int и определить ее равной размеру типа int
int i = sizeof(int);
Оператор sizeof.

Ввод/вывод переменных

08 Для того чтобы человек мог понять, какое значение присвоено той или иной переменной или константе, это значение нужно преобразовать в текстовый вид. Для этого в стандартной библиотеке языка C++ есть готовые функции, которые коллективно называются функциями ввода/вывода. Чтобы ими воспользоваться, необходимо подключить заголовочный файл iostream. В этом заголовочном файле доступны стандартный поток вывода std::cout, поток ввода std::cin, поток ошибок std::cerr и поток ошибок с буферизацией std::clog. Для вывода значения переменной используется оператор вывода <<, а для ввода оператор ввода >>. Пример программы и вывод показан ниже.

int x = 0;
std::cout << "Enter a number: ";
std::cin >> x;
std::cout << "You entered " << x << '\n';
Ввод/вывод в C++.

Написание программы и компиляция

09 Простейшая программа на C++ содержит только функцию main. Эта функция является точкой входа в программу, и она вызывается самой операционной системой при запуске программы. Также как и переменные, функции можно объявить, — то есть написать название, список типов аргументов и тип возвращаемого значения — и определить, — то есть написать тело функции. Объявление и определение функции main как правило совмещают, то есть пишут тело функции сразу после ее объявления.

10 У каждой функции C++ есть имя, тип возвращаемого значения и список типов аргументов. У функции main тип возвращаемого значения (тип данных которые функция возвращает в место вызова) равен int. Система интерпретирует его следующим образом: если main возвращает отличное от нуля число, то программа завершилась с ошибкой, а если она возвращает ноль, то программа завершилась корректно. Аргументы функции main можно опустить, если они не используются. Ниже показана программа, которая выводит на экран приветствие.

#include <iostream>               // подключение заголовочного файла с функциями ввода/вывода
int main() {                      // объявление и определение функции main
    std::cout << "Hello world\n"; // вывод на экран строки Hello world
    return 0;                     // возвращение значения 0 из фунции main
}                                 // (0 означает корректное завершение программы)
Hello world на C++.

Здесь в фигурных скобках находится тело функции main.

11 Для того чтобы превратить программу из текстового вида (который понятен человеку) в двоичный (который понятен процессору и операционной системе), используют программы, называемые компиляторами. Ниже показаны команды для компиляции программы для Linux.

КомандаОписание
g++ main.ccкомпиляция программы из файла main.cc
./a.outзапуск получившейся программы (файл a.out)
Компиляция и запуск программы.

Указатели

12 Указатель — это адрес переменной или константы, который хранится в отдельной переменной. Адрес является целым числом без знака и занимает 8 байт на 64-битных системах, 4 байта на 32-битных, 2 байта на 16-битных и 1 байт на 8-битных процессорах. Такое ограничение связано с тем, что адресация (загрузка переменной по сохраненному в регистре адресу) производится процессором, а значит размер адреса ограничен возможностями процессора.

13 Указатель является строго-типизированным. Это означает, что указатель на целое число и указатель на число с плавающей точкой имеют разные типы. Единственным отличием указателя от обычной целочисленной переменной является другой набор операций, который зависит от типа переменной, на которую этот адрес указывает. Для указателей определены операции сложения и вычитания. Когда вы прибавляете к указателю n, то адрес увеличивается на n, домноженное на размер типа переменной, на которую указывает адрес. Если предположить, что адрес указывал на начало массива, то после прибавления n он будет указывать на n-ый элемент массива.

int x = 0;                   // переменная x
int* px = &x;                // указатель на переменную x
int array[3] = {1,2,3};      // массив из трех элементов
int* parray = array;         // указатель на первый элемент массива
++parray;                    // указатель на второй элемент массива
std::cout << *parray << '\n' // вывод второго элемента массива
Примеры работы с указателями.

Особняком стоит указатель на void. Поскольку этот тип данных имеет нулевой размер, то операции сложения и вычитания для указателя на него не работают. Указатель void* полезен только для хранения адреса, что встречается редко.

14 Когда следует использовать указатели? В основном, их используют для итерации по элементам массива в высокопроизводительных вычислениях, когда эффективность работы программы является приоритетом. Также указатели используют для передачи аргументов в библиотечные функции.

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

16 Другая проблема заключается в динамическом выделении памяти и сохранении адреса выделенной памяти в указателе. В таком случае лучше всего использовать умные указатели, поскольку они позволяют контролировать время жизни объекта и избежать утечек памяти. Это также можно проверить с помощью санитайзеров.

Приведение типов

17 Несмотря на строгую типизацию, переменные одних типов можно преобразовывать к переменным других типов. Это называет приведением типов. Для этого существуют различные операции, наиболее часто используемая из них — static_cast. Эта операция приводит тип на этапе компиляции, позволяя компилятору проверить корректность операции.

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

19 С указателями ситуации сложнее. Приводить указатель от одного типа к другому безопасно, только если целевой тип имеет меньший или такой же размер. В общем случае приведение типов указателей не целесообразно — проще считать переменную и привести ее тип к нужному. В таком случае ограничения на размер нет.

20 Особняком опять стоит приведение указателя void* к другим типам. Целевой тип не может иметь размер меньше нуля, поэтому вам надо обладать информацией о выравнивании этого адреса, чтобы корректно преобразовать тип. Если вы получили указатель из вызова mmap, то он выровнен по размеру страницы памяти и его можно привести к любому типу.

int x = 123;
long y = static_cast<long>(x); // приведение int к long
int z = static_cast<int>(y); // приведение long к int (возможно усечение)
int x1 = -123;
unsigned int y1 = static_cast<unsigned int>(x1); // y1 = max - 123, см. дополнительный код
int z1 = static_cast<int>(y1); // z1 = -123
Приведение типов.

Задачи

Массивы1 балл

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

Локальные переменные1 балл

22 Объявите и определите произвольными значениями три локальных переменных разных примитивных типов. Выведите их адреса. Вычтите из каждого адреса адрес первой переменной и выведите результат. Для этого вам понадобится привести указатель к целочисленному типу с помощью reinterpret_cast.

Дополнительный код1 балл

23 Приведите максимальное значения типа unsigned int к типу int. Приведите максимальное значение типа int к типу unsigned int. Для определения максимального значения воспользуйтесь заголовочным файлом limits.

Указатели2 балла

24 Выведите побайтово значение переменной типа int со знаком плюс и минус. Для этого используйте приведение типов указателей.

Видео

Запись лекции 20.11.2021.