Где хранятся значения программы, сколько места они занимают и что с ними можно делать
sizeofstatic_castconst, constexpr и enum вместо «магических чисел»g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe и сравните вывод со страницей. Раздел заканчивается тестом на портале; раздел 4 с первыми практическими работами на C++ открывается после теста.sizeofВ памяти компьютера — только байты. Одни и те же четыре байта могут быть целым числом, дробным числом или четырьмя буквами; как их понимать, решает тип. Тип значения определяет три вещи:
7 / 2 для целых и для дробных даёт разный результат (пункт 08).Основные (встроенные) типы C++:
| Тип | Что хранит | Байт в Windows x64 | Литерал |
|---|---|---|---|
bool | логическое значение | 1 | true, false |
char | код одного символа (маленькое целое) | 1 | 'A' |
short | целое | 2 | — |
int | целое — основной целый тип | 4 | 42 |
long | целое | 4 | 42L |
long long | целое | 8 | 42LL |
float | вещественное, около 7 значащих цифр | 4 | 3.5f |
double | вещественное, 15–16 значащих цифр — основной дробный тип | 8 | 3.5 |
long double | вещественное повышенной точности | 16 | 3.5L |
У целых типов есть беззнаковые варианты со словом unsigned: unsigned int, unsigned long long. Литерал беззнакового типа — с суффиксом u: 42u. Строки std::string из раздела 2 — не встроенный тип, а тип стандартной библиотеки (раздел 10).
Размер типа в байтах возвращает операция sizeof. Стандарт C++ задаёт только минимальные размеры, точные выбирает компилятор под систему, поэтому их проверяют, а не угадывают:
#include <iostream>
int main()
{
std::cout << "bool " << sizeof(bool) << '\n';
std::cout << "char " << sizeof(char) << '\n';
std::cout << "short " << sizeof(short) << '\n';
std::cout << "int " << sizeof(int) << '\n';
std::cout << "long " << sizeof(long) << '\n';
std::cout << "long long " << sizeof(long long) << '\n';
std::cout << "float " << sizeof(float) << '\n';
std::cout << "double " << sizeof(double) << '\n';
std::cout << "long double " << sizeof(long double) << '\n';
return 0;
}
bool 1
char 1
short 2
int 4
long 4
long long 8
float 4
double 8
long double 16
long в Windows — 4 байта, как int, даже в 64-битной программе. В Linux тот же long — 8 байт. Код, который рассчитывает на 8-байтовый long, в Windows молча теряет старшие разряды. Нужны большие целые — пишите long long, он 8 байт везде. По этой же причине Windows API объявляет свои типы с точным размером: DWORD — беззнаковое 32-битное, BYTE — 8-битное; они встретятся с раздела 14.Переменная — именованная область памяти, в которой хранится значение одного типа. Объявление задаёт тип и имя; инициализация — начальное значение в момент объявления. В C++ три равноправные записи:
int layers = 2; // копирующая инициализация — основная в курсе
int rooms(3); // в круглых скобках
int floors{5}; // в фигурных скобках — запрещает сужение (пункт 05)
int total{}; // пустые фигурные скобки — ноль
double width = 3.2, height = 2.7; // несколько переменных одного типа
Переменная без инициализации — не ноль: в ней лежит то, что осталось в памяти от прошлого использования. Чтение такой переменной — ошибка, и -Wall о ней предупреждает:
int count;
std::cout << count << '\n';
uninit.cpp:6:27: warning: 'count' is used uninitialized [-Wuninitialized]
Исключение — переменная, которую сразу заполняет ввод, как age в анкете раздела 2: её можно не инициализировать. Правило курса — объявлять переменную там, где у неё впервые появляется значение, а не списком в начале main.
Область видимости — часть программы, где имя переменной доступно. Переменная, объявленная в блоке { }, существует от объявления до закрывающей скобки этого блока; это локальная переменная. Переменная вне всех функций — глобальная, она видна во всём файле ниже объявления.
#include <iostream>
int total = 100; // глобальная: видна во всём файле
int main()
{
int count = 5; // локальная: видна до конца main
std::cout << total << ' ' << count << '\n';
{
int count = 7; // другая переменная, закрывает внешнюю
int extra = 1;
std::cout << count << ' ' << extra << '\n';
}
std::cout << count << '\n'; // снова внешняя count; extra здесь нет
return 0;
}
100 5
7 1
5
Внутренняя count — отдельная переменная, которая на время блока скрывает внешнюю; после блока внешняя снова видна со своим значением 5. Обращение к extra после блока — ошибка компиляции 'extra' was not declared in this scope: переменной там уже нет.
-Wall -Wextra, но читающий код путается — давайте переменным разные имена.В n битах помещается 2n разных значений. Беззнаковый тип отдаёт их все неотрицательным числам: от 0 до 2n − 1. Знаковый делит пополам: от −2n−1 до 2n−1 − 1. Для int (32 бита) это ±2,1 миллиарда, для unsigned int — от 0 до 4,29 миллиарда. Границы типов записаны в заголовке <climits>:
#include <iostream>
#include <climits>
int main()
{
std::cout << "short: " << SHRT_MIN << " .. " << SHRT_MAX << '\n';
std::cout << "int: " << INT_MIN << " .. " << INT_MAX << '\n';
std::cout << "unsigned int: 0 .. " << UINT_MAX << '\n';
std::cout << "long long: " << LLONG_MIN << " .. " << LLONG_MAX << '\n';
unsigned int counter = 0;
counter = counter - 1; // беззнаковое: по кругу, к максимуму
std::cout << "0 - 1 in unsigned int = " << counter << '\n';
short small = 32767;
small = small + 1; // 32768 не помещается в short
std::cout << "32767 + 1 in short = " << small << '\n';
return 0;
}
short: -32768 .. 32767
int: -2147483648 .. 2147483647
unsigned int: 0 .. 4294967295
long long: -9223372036854775808 .. 9223372036854775807
0 - 1 in unsigned int = 4294967295
32767 + 1 in short = -32768
Выход результата за диапазон — переполнение. Что при этом происходит, зависит от типа:
short выше, но при оптимизации компилятор вправе считать, что переполнения не бывает, и выбросить проверки, которые на нём держатся. Правило одно — не допускать.Если переполнение видно уже при компиляции, g++ предупреждает:
int big = 2147483647 + 1;
overflow.cpp:5:26: warning: integer overflow in expression of type 'int' results in '-2147483648' [-Woverflow]
Но значения, пришедшие с ввода, компилятор не видит. Тип выбирают заранее, по самому большому значению, которое может получиться: количество людей — int, население Земли или сумма в копейках — long long, размер файла в байтах — long long (файл бывает больше 2 ГБ).
int с unsigned int знаковое значение переводится в беззнаковое: −1 становится 4294967295. Поэтому -1 < 1u — ложь. Компилятор предупреждает:
signcmp.cpp:7:27: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare]
Беззнаковые типы в курсе — для флагов (пункт 11) и там, где их требует Windows API. Счётчики и величины — int и long long.float и double хранят число в виде мантиссы и порядка — как 1.5e3 в литерале, только в двоичной системе. Диапазон огромный (у double — до 10308), но значащих цифр ограниченное число: у float около 7, у double 15–16. Кроме того, большинство десятичных дробей в двоичной системе бесконечны, как 1/3 в десятичной, и хранятся с округлением.
#include <iostream>
#include <iomanip>
int main()
{
double sum = 0.1 + 0.2;
std::cout << sum << '\n';
std::cout << std::setprecision(17) << sum << '\n';
std::cout << (sum == 0.3) << '\n';
float big = 16777216.0f;
std::cout << std::setprecision(10) << big + 1.0f << '\n';
return 0;
}
0.3
0.30000000000000004
0
16777216
std::cout по умолчанию показывает 6 значащих цифр и округляет — поэтому первая строка выглядит точной. std::setprecision(n) из заголовка <iomanip> задаёт число значащих цифр вывода и действует до следующего изменения. С 17 цифрами видно, что сумма не равна 0.3, и сравнение даёт 0 — «ложь». В последней строке к 16 777 216 прибавили единицу, а число не изменилось: у float кончились значащие цифры.
== — только «разница меньше допуска», это будет в разделе 4. Деньги не хранят в double: копейки теряются при сложении тысяч сумм. Сумма хранится целым числом копеек в long long. Основной вещественный тип курса — double; float — только там, где его требует библиотека.static_castОперация выполняется над значениями одного типа. Если типы разные, компилятор сам приводит их к общему — это неявное преобразование. Правила, которые нужны на практике:
bool, char и short в выражении сначала превращаются в int: 'A' + 1 — это int 66;7.0 / 2 равно 3.5;int n = 3.9; даёт 3 — дробная часть отбрасывается, без округления.Явное преобразование записывается static_cast<тип>(выражение). Оно нужно, когда неявное делает не то, что требуется, и чтобы показать читателю: потеря данных здесь намеренная.
#include <iostream>
int main()
{
int apples = 7;
int people = 2;
double share1 = apples / people; // целое деление, потом в double
double share2 = static_cast<double>(apples) / people; // деление вещественное
std::cout << share1 << ' ' << share2 << '\n';
double price = 99.99;
int rubles = static_cast<int>(price); // дробная часть отбрасывается
std::cout << rubles << '\n';
char letter = 'A';
std::cout << letter + 1 << ' ' << static_cast<char>(letter + 1) << '\n';
std::cout << static_cast<int>(letter) << '\n';
std::cout << 7 / 2 << ' ' << 7.0 / 2 << ' ' << 1 / 3 * 3 << '\n';
return 0;
}
3 3.5
99
66 B
65
3 3.5 0
В share1 деление выполнено над двумя int, результат 3 — и только потом превращён в double. Приводить нужно операнд до деления, как в share2; static_cast<double>(apples / people) даст ту же тройку. char хранит код символа: 'A' — 65, letter + 1 — число 66, а приведённое обратно к char — буква B.
Инициализация в фигурных скобках запрещает сужение — преобразование с потерей данных:
int count{3.9};
narrowing.cpp:5:15: error: narrowing conversion of '3.8999999999999999e+0' from 'double' to 'int' [-Wnarrowing]
(double)apples или double(apples). Оно работает, но в курсе не используется: в разделах о Windows API такая запись молча превращает что угодно во что угодно, включая указатели, а static_cast разрешает только осмысленные преобразования и сразу видна при чтении.const и constexprЛитерал — константа без имени. Число 1.1 в середине формулы расхода краски из раздела 1 не говорит, что это запас 10 % на потери, а если запас изменится, придётся искать все места, где оно записано. Такие числа называют магическими. Вместо них заводят именованные константы:
| Запись | Значение известно | Пример |
|---|---|---|
const | к моменту объявления, в том числе при выполнении | const double area = PI * radius * radius; |
constexpr | уже при компиляции — только из литералов и других constexpr | constexpr int SECONDS_IN_HOUR = 60 * 60; |
Обе — переменные, которые после инициализации нельзя изменить. Имена констант в курсе пишутся заглавными буквами через подчёркивание.
#include <iostream>
constexpr double PI = 3.14159265358979; // значение известно при компиляции
const int DAYS_IN_WEEK = 7;
enum Weekday { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY };
enum class Access { READ = 1, WRITE = 2, EXECUTE = 4 };
int main()
{
double radius;
std::cout << "Radius: ";
std::cin >> radius;
const double area = PI * radius * radius; // значение известно только при выполнении
std::cout << "Area: " << area << '\n';
Weekday today = FRIDAY;
std::cout << today << ' ' << DAYS_IN_WEEK - today << '\n';
Access mode = Access::WRITE;
std::cout << static_cast<int>(mode) << '\n';
return 0;
}
Radius: 2
Area: 12.5664
4 3
2
Перечисления enum — в пункте 07. Константу нельзя изменить и нельзя оставить без значения — обе ошибки ловит компилятор:
const int LIMIT = 10;
LIMIT = 20; // error: assignment of read-only variable 'LIMIT'
const int MAX; // error: uninitialized 'const MAX' [-fpermissive]
const. Это не только защита от случайного присваивания: читая const double area, не нужно просматривать остаток функции, чтобы понять, какое значение у area в конце. constexpr — для настоящих констант задачи: коэффициентов, пределов, размеров массивов (раздел 8 — размер массива обязан быть известен при компиляции).#define против const; перечисления enumКонстанту можно задать и макросом: #define DAYS_IN_WEEK 7. Так пишут в старом коде на C и в заголовках Windows API. Разница — в том, кто обрабатывает запись (раздел 2, пункт 07):
#define | const / constexpr | |
|---|---|---|
| Обрабатывает | препроцессор, как текст | компилятор, как переменную |
| Тип | нет — подставляется текст | есть, проверяется |
| Область видимости | от строки #define до конца файла, во всех функциях | блок, как у переменной |
| Ошибки | сообщение указывает на место подстановки, имени макроса в нём может не быть | сообщение называет константу |
| В курсе | только условная компиляция | все именованные константы |
Константы Windows API — макросы: GENERIC_READ, INFINITE, FOREGROUND_RED. Причина — API написан на C, где constexpr нет. Пользоваться ими можно как обычными константами, а свои константы в курсе — только const и constexpr.
Перечисление enum — тип с набором именованных целых констант. Если значения не заданы, первая равна 0, каждая следующая на 1 больше: в примере пункта 06 FRIDAY — 4. Перечисление заменяет «коды» вида «1 — понедельник, 2 — вторник»: today = FRIDAY читается без комментария.
Вид перечисления два:
enum Weekday — простое: имена констант видны без префикса, и константа сама превращается в int, поэтому DAYS_IN_WEEK - today собирается;enum class Access — с областью видимости: имена пишутся с префиксом Access::WRITE, и в int значение превращается только явно.Access mode = Access::WRITE;
int code = mode;
enumclass.cpp:8:16: error: cannot convert 'Access' to 'int' in initialization
enum попадают в окружающую область видимости и могут столкнуться с чужими именами: в windows.h уже есть макросы ERROR, DELETE, IN, OUT, и перечисление с такой константой после подключения windows.h не соберётся. enum class от этого защищён, поэтому для новых перечислений в курсе — он.| Операция | Смысл | Пример | Результат |
|---|---|---|---|
+ - | сложение, вычитание | 7 - 2 | 5 |
* | умножение | 7 * 2 | 14 |
/ | деление; у двух целых — целая часть | 7 / 2, 7.0 / 2 | 3, 3.5 |
% | остаток от деления, только для целых | 7 % 2 | 1 |
унарный - | смена знака | -x |
Целочисленное деление и остаток — главные инструменты работы с разрядами и единицами измерения: n % 10 — последняя цифра числа, n / 10 — число без неё, n % 2 — 0 у чётного. Перевод секунд в часы, минуты и секунды:
#include <iostream>
int main()
{
int total_seconds;
std::cout << "Seconds: ";
std::cin >> total_seconds;
int hours = total_seconds / 3600;
int minutes = total_seconds % 3600 / 60;
int seconds = total_seconds % 60;
std::cout << hours << " h " << minutes << " min " << seconds << " s\n";
std::cout << -7 / 2 << ' ' << -7 % 2 << ' ' << 7 % -2 << '\n';
return 0;
}
Seconds: 7384
2 h 3 min 4 s
-3 -1 1
Целое деление отбрасывает дробную часть в сторону нуля: −7 / 2 = −3, а не −4. Знак остатка совпадает со знаком делимого: −7 % 2 = −1. Всегда верно равенство (a / b) * b + a % b == a.
0xC0000094 (STATUS_INTEGER_DIVIDE_BY_ZERO). Если ноль виден при компиляции, g++ предупреждает warning: division by zero [-Wdiv-by-zero], но делитель из ввода компилятор не видит — проверять его придётся ветвлением (раздел 4). Вещественное деление на ноль не аварийное: 1.0 / 0 даёт inf.++ и --Присваивание x = выражение вычисляет правую часть и записывает результат в переменную слева. Слева может стоять только то, у чего есть место в памяти: 5 = x и a + b = 3 — ошибки. Присваивание само является выражением со значением присвоенного, поэтому цепочка x = y = z = 10 выполняется справа налево.
Составное присваивание — сокращение: x += 5 значит x = x + 5. Так же работают -= *= /= %= и побитовые &= |= ^= <<= >>= (пункт 11).
Инкремент ++ увеличивает переменную на 1, декремент -- уменьшает. У каждого две формы, и разница видна, когда результат используется в выражении:
++a — сначала увеличить, значение выражения — новое;a++ — значение выражения — старое, увеличение после.#include <iostream>
int main()
{
int a = 5;
int b = a++; // b получает 5, затем a становится 6
int c = ++a; // a становится 7, затем c получает 7
std::cout << a << ' ' << b << ' ' << c << '\n';
int stock = 20;
stock -= 3; // stock = stock - 3
stock *= 2;
stock /= 5;
stock %= 4;
std::cout << stock << '\n';
int x, y, z;
x = y = z = 10; // присваивание выполняется справа налево
std::cout << x + y + z << '\n';
return 0;
}
7 5 7
2
30
stock: 20 − 3 = 17, × 2 = 34, / 5 = 6 (целое деление), % 4 = 2.
i++ + i++ не определён стандартом, разные компиляторы дают разное. g++ предупреждает:
seqpt.cpp:6:14: warning: operation on 'i' may be undefined [-Wsequence-point]
Правило курса: ++ и -- — отдельной инструкцией (count++;) или в заголовке цикла (раздел 5), а не внутри формул.Операции сравнения == != < <= > >= дают значение типа bool. Логические операции соединяют условия:
| Операция | Смысл | Истинно, когда |
|---|---|---|
&& | И | истинны оба операнда |
|| | ИЛИ | истинен хотя бы один |
! | НЕ | операнд ложен |
#include <iostream>
int main()
{
int age = 17;
int score = 82;
bool adult = age >= 18;
bool passed = score >= 70 && score <= 100;
std::cout << adult << ' ' << passed << '\n';
std::cout << std::boolalpha << adult << ' ' << passed << '\n';
std::cout << (adult || passed) << ' ' << !adult << '\n';
std::cout << (2 + 3 * 4 > 10 && age != 17) << '\n';
return 0;
}
0 1
false true
true true
false
bool выводится как 1 и 0; после std::boolalpha — словами. Условие «число в диапазоне от 70 до 100» записывается двумя сравнениями через &&. && и || вычисляются сокращённо: если левый операнд && ложен, правый не вычисляется — результат уже известен. В разделе 4 на этом строятся защитные условия вида «делитель не ноль и частное больше 5».
Приоритет определяет, какая операция выполняется раньше, когда скобок нет. Операции одного уровня выполняются слева направо, кроме унарных и присваиваний — те справа налево.
| Уровень | Операции |
|---|---|
| 1 (раньше всех) | (), a++ a--, static_cast |
| 2 | ++a --a, !, ~, унарный -, sizeof |
| 3 | * / % |
| 4 | + - |
| 5 | << >> — сдвиги и вывод cout |
| 6 | < <= > >= |
| 7 | == != |
| 8, 9, 10 | &, затем ^, затем | |
| 11 | && |
| 12 | || |
| 13 (позже всех) | = += -= *= /= %= &= |= ^= <<= >>= |
Запоминать таблицу целиком не нужно. Достаточно: арифметика раньше сравнения, сравнение раньше логики, присваивание последним. Во всех сомнительных местах ставятся скобки — они не замедляют программу.
0 < x < 10 — это (0 < x) < 10: сначала получается 0 или 1, и это всегда меньше 10. При x = 50 результат — истина. g++ предупреждает: comparisons like 'X<=Y<=Z' do not have their mathematical meaning [-Wparentheses]. Правильно — 0 < x && x < 10.std::cout << adult || passed; — << выполняется раньше ||: выводится только adult, а passed попадает в логическую операцию с потоком, результат которой отбрасывается. Эта строка собирается без единого предупреждения. Логическое выражение в выводе — всегда в скобках.= — присваивание, == — сравнение. Путаница между ними — самая частая ошибка раздела 4.Побитовые операции работают с отдельными битами целого числа. Каждый бит результата вычисляется из битов операндов в той же позиции.
| Операция | Бит результата равен 1, когда | Пример (4 бита) |
|---|---|---|
& — И | оба бита 1 | 0101 & 0011 = 0001 (5 & 3 = 1) |
| — ИЛИ | хотя бы один 1 | 0101 | 0011 = 0111 (5 | 3 = 7) |
^ — исключающее ИЛИ | биты разные | 0101 ^ 0011 = 0110 (5 ^ 3 = 6) |
~ — НЕ | бит был 0 | ~0101 = 1010 |
<< n — сдвиг влево | биты сдвигаются на n позиций к старшим; = умножение на 2n | 1 << 4 = 16 |
>> n — сдвиг вправо | к младшим; для неотрицательных = деление на 2n | 200 >> 3 = 25 |
Главное применение в системном программировании — флаги: несколько независимых признаков «да/нет» в одном целом числе, по биту на признак. Так устроены почти все параметры Windows API: права доступа к файлу, цвет символа в консоли, способ создания процесса. Флаг — константа, в которой установлен ровно один бит; набор флагов собирается операцией |.
#include <iostream>
#include <bitset>
const unsigned int FLAG_READ = 1u << 0; // 0001
const unsigned int FLAG_WRITE = 1u << 1; // 0010
const unsigned int FLAG_EXECUTE = 1u << 2; // 0100
const unsigned int FLAG_HIDDEN = 1u << 3; // 1000
int main()
{
unsigned int flags = FLAG_READ | FLAG_WRITE;
std::cout << std::bitset<4>(flags) << '\n';
flags |= FLAG_HIDDEN; // установить флаг
flags &= ~FLAG_WRITE; // снять флаг
flags ^= FLAG_EXECUTE; // переключить флаг
std::cout << std::bitset<4>(flags) << '\n';
std::cout << std::boolalpha;
std::cout << ((flags & FLAG_WRITE) != 0) << ' '
<< ((flags & FLAG_HIDDEN) != 0) << '\n';
std::cout << (5 & 3) << ' ' << (5 | 3) << ' ' << (5 ^ 3) << ' '
<< (1 << 4) << ' ' << (200 >> 3) << '\n';
return 0;
}
0011
1101
false true
1 7 6 16 25
std::bitset<4>(x) из заголовка <bitset> выводит 4 младших бита числа — удобно для проверки. Четыре приёма работы с флагами:
| Действие | Запись | Почему работает |
|---|---|---|
| установить | flags |= F; | ИЛИ с 1 даёт 1, с 0 — не меняет бит |
| снять | flags &= ~F; | ~F — все биты 1, кроме нужного; И с 0 даёт 0 |
| переключить | flags ^= F; | исключающее ИЛИ с 1 меняет бит на противоположный |
| проверить | (flags & F) != 0 | остаётся только нужный бит; не ноль — флаг установлен |
Та же запись в Windows API — красный яркий символ в консоли (раздел 15) и доступ к файлу на чтение и запись (раздел 17):
FOREGROUND_RED | FOREGROUND_INTENSITY
GENERIC_READ | GENERIC_WRITE
flags & FLAG_WRITE != 0 читается компилятором как flags & (FLAG_WRITE != 0): != по приоритету выше &. g++ предупреждает: suggest parentheses around comparison in operand of '&' [-Wparentheses]. Не путайте побитовые & | с логическими && ||: 1 & 2 равно 0, а 1 && 2 — истина. Флаги храните в беззнаковых типах: сдвиг влево отрицательного знакового числа в C++17 — неопределённое поведение, а с беззнаковыми сдвиги и ~ работают одинаково на любом компиляторе.sizeof. В Windows long — 4 байта, большие целые — long longdouble хранит 15–16 значащих цифр с округлением: 0.1 + 0.2 == 0.3 ложно, деньги — целыми копейкамиstatic_cast<double>(a) / bconst и constexpr; #define — только условная компиляция; перечисления — enum class0 < x < 10 — ошибка, логическое выражение в cout — в скобках|=, снять &= ~, переключить ^=, проверить (flags & F) != 0-Wall -Wextra и чтение предупреждений, макросы #define, переменные анкеты. Нужен для: раздела 4 — логические выражения в условиях и проверка делителя; разделов 5–6 — счётчики, ++, составное присваивание, разряды числа через / и %; раздела 8 — constexpr для размера массива; разделов 14–19 — типы Windows API с точным размером, флаги процессов, консоли, каналов и памяти.