К содержимому
ИС30

Поиск по сайту

Конспекты, лабы, квизы, ЧаВо и страницы

Войти

Лекция 1. Типы и операторы

Идентификаторы и стиль кода, объявления и определения, встроенные типы и их размеры, беззнаковая арифметика, представление чисел, литералы, перечисления, операторы и приоритет, преобразования типов.

Первая содержательная лекция о языке. Здесь собраны «кирпичики» любой программы: как назвать переменную, какой тип ей выбрать, как записать число, что делают операторы и почему выражение a + b * c понимается не так, как его читает глаз. Почти все ошибки начинающих в C++ рождаются именно на этом уровне, поэтому разбираем подробно.

Главный приём

Читая любое выражение, сначала расставьте скобки по приоритету, и только потом считайте. Если сомневаетесь, добавьте скобки в свой код: компилятор от этого не станет медленнее, а читателю станет проще.

1. Программа и её первые строки

Классическая программа «Hello, world!» выводит строку в стандартный поток вывода:

hello-world.cpp
#include <iostream>
 
int main() {
    std::cout << "Hello world!\n";
 
    return 0;
}

Здесь важны три вещи: директива #include подключает объявления из стандартной библиотеки, std::cout — поток вывода, а main — точка входа, которую вызывает система при запуске.

2. Идентификаторы и стиль

Идентификатор — это имя переменной, типа, функции или другого элемента программы. Правила такие:

  • может содержать буквы (в том числе допустимый Unicode), цифры и символ _;
  • не начинается с цифры;
  • регистр важен: value и Value — разные имена;
  • не совпадает с ключевым словом языка, например int или return.

Правила стиля на курсе такие же, как в проекте: snake_case для переменных и пространств имён, PascalCase для функций и типов, kPascalCase для констант. Отступ — четыре пробела, фигурная скобка остаётся на той же строке, фигурные скобки ставим даже у однострочных if и циклов. Форматирует clang-format, за основу взят Google C++ Style Guide.

Почему это важно

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

3. Объявление и определение

Объявление сообщает компилятору имя и тип. Определение дополнительно создаёт объект и выделяет под него память:

extern int global_count;  // объявление: объект где-то в другом месте
int global_count = 0;     // определение
double radius = 1.23;     // определение
char grade = 'A';         // определение

Каждое определение является объявлением, но не наоборот.

3.1. Переменная без инициализатора

Если написать int value; внутри функции, переменная создастся, но её значение будет мусором — тем, что оказалось в памяти до этого. Чтение такого значения — неопределённое поведение (undefined behavior): программа может сделать что угодно, включая «работать правильно» на одном запуске и ломаться на другом.

Всегда инициализируйте

Пишите int value{}; или int value = 0;. Неинициализированная локальная переменная — самый частый источник «плавающих» багов у новичков.

4. Встроенные типы

Основные типы языка делятся на группы:

  • Целочисленные: bool, char, short, int, long, long long.
  • С плавающей точкой: float, double, long double.
  • Специальные: void (нет значения) и std::nullptr_t (тип литерала nullptr).
  • Символьные: wchar_t, char8_t, char16_t, char32_t для разных кодировок.

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

4.1. Модификаторы

Модификаторы меняют диапазон целочисленного типа:

short int small_value = -10;
long int large_value = 1'000'000L;
long long very_large_value = 9'000'000'000LL;
 
signed int temperature = -20;
unsigned int student_count = 30U;
unsigned long file_size = 4'000'000UL;
  • signed разрешает отрицательные значения (по умолчанию для большинства целых типов);
  • unsigned хранит только неотрицательные;
  • short и long задают минимальный гарантированный диапазон, а не точный размер;
  • слово int часто можно опустить: unsigned означает unsigned int.

4.2. Беззнаковая арифметика

Беззнаковые числа «заворачиваются»: арифметика идёт по модулю 2N2^N, где NN — число бит типа. Поэтому:

unsigned int zero = 0U;
unsigned int wrapped = zero - 1U;  // максимальное значение типа, а не -1

Смешивание знаковых и беззнаковых типов в сравнении коварно. Знаковое значение приводится к беззнаковому, и -1 становится огромным положительным числом:

bool result = -1 < 10U;  // false
unsigned не защищает от ошибок со знаком

Беззнаковый тип не спасает от отрицательных значений: он просто меняет правила их интерпретации. Для счётчиков и размеров используйте std::size_t, но помните про преобразования при сравнении с int.

4.3. Переполнение при вычислении середины

Классическая ошибка в бинарном поиске:

int middle = (left + right) / 2;          // может переполнить int
int safe_middle = left + (right - left) / 2;

Сумма left + right при больших индексах превышает диапазон int. Для знаковых типов переполнение — неопределённое поведение. Именно такой баг много лет жил в стандартной реализации бинарного поиска в библиотеке Java.

5. Размеры типов

Размеры встроенных типов зависят от платформы. Типичная раскладка для 64-битной Linux-системы с моделью данных LP64:

ТипРазмер
char1 байт
short2 байта
int4 байта
long8 байт
long long8 байт
float4 байта
double8 байт

На другой архитектуре, в другой ОС или у другого компилятора цифры могут отличаться. Гарантировано только то, что sizeof(char) == 1.

Узнать размер можно оператором sizeof. Результат имеет тип std::size_t:

std::cout << sizeof(int) << '\n';

Диапазоны значений типа доступны через std::numeric_limits из заголовка <limits>: методы lowest() и max() дают крайние конечные значения. Для double метод min() возвращает наименьшее положительное нормализованное число, а не самое отрицательное, поэтому не путайте их.

5.1. Типы фиксированной ширины

Заголовок <cstdint> объявляет типы с точно заданным размером: std::int8_t, std::uint16_t, std::int32_t, std::uint64_t и т. д. Число в имени — ширина в битах.

Используйте их, когда размер является частью контракта: формат файла, сетевой протокол, регистр устройства. Для обычных счётчиков и вычислений часто достаточно int.

6. Как хранятся числа

6.1. Знаковые целые

Для знаковых целых в современном C++ используется дополнительный код (two's complement). Чтобы понять, зачем он нужен, сравним три подхода на восьми битах.

  • Прямой код: старший бит хранит знак, остальные — модуль. Сложение разных знаков не работает напрямую, а ноль имеет два представления.
  • Обратный код: для отрицательного числа инвертируются все биты. Сложение требует циклического переноса, а ноль по-прежнему двойной.
  • Дополнительный код: для отрицательного −x-x берётся 2N−x2^N - x. Для 8 бит −5-5 записывается как 1111 1011. Сложение работает одной и той же схемой для любых знаков, ноль один, диапазон от −128-128 до 127127.

Переполнение знакового целого всё равно остаётся неопределённым поведением. Например, -INT_MIN не имеет допустимого результата.

6.2. Числа с плавающей точкой

Стандартный формат float — IEEE 754 binary32: 1 бит знака, 8 бит смещённой экспоненты и 23 бита мантиссы. Для нормализованного числа значение равно

(−1)s⋅(1.f)2⋅2e−127.(-1)^s \cdot (1.f)_2 \cdot 2^{e - 127}.

Посмотрим на 5,755{,}75. В двоичной записи это 101,112=1,01112⋅22101{,}11_2 = 1{,}0111_2 \cdot 2^2. Значит, знак s=0s = 0, экспонента e=2+127=129=100000012e = 2 + 127 = 129 = 10000001_2, мантисса f=0111 0000…02f = 0111\,0000\dots0_2 (ведущая единица не хранится).

Не все десятичные дроби точны

Число 0,10{,}1 в двоичной системе — бесконечная периодическая дробь. Поэтому 0.1 + 0.2 == 0.3 в типах с плавающей точкой ложно. Сравнивайте вещественные числа с допуском или используйте целые величины там, где это возможно.

7. Литералы

7.1. Целочисленные

int decimal = 162;
int octal = 0242;           // ведущий ноль: восьмеричная система
int hexadecimal = 0xA2;     // префикс 0x: шестнадцатеричная
int binary = 0b1010'0010;   // префикс 0b: двоичная
unsigned long population = 1'000'000UL;

Апостроф разделяет разряды и на значение не влияет. Суффиксы: U — беззнаковый, L — long, LL — long long. Ведущий ноль меняет систему счисления: 010 — это восемь, а не десять.

7.2. Вещественные

Без суффикса литерал имеет тип double. Суффикс F делает float, L — long double. Запись 1.5E6 означает 1,5⋅1061{,}5 \cdot 10^6.

7.3. Символы и строки

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

  • \n — перевод строки, \t — табуляция, \0 — нулевой символ;
  • \\ — сама обратная косая черта, \' и \" — кавычки.

'\n' — один символ, а не два. Строковый литерал вида "Hello" имеет тип массива const char[6]: пять символов и завершающий нулевой символ.

const char message[] = "Hello, world!\n";

7.4. Логический тип

bool принимает два значения: true и false. Результаты сравнений имеют именно этот тип:

bool is_positive = value > 0;

8. Перечисления

Перечисление задаёт набор именованных констант. Бывают два вида:

enum Color { kRed, kGreen, kBlue };        // обычное
enum class Direction { kLeft, kRight };    // «сильное»
Свойствоenumenum class
Имена элементовВидны в окружающей областиТолько через Direction::kLeft
Неявное преобразование в intЕстьНет, только явное

Рекомендуем enum class: меньше конфликтов имён и случайных преобразований.

9. Операторы

Основные группы операторов:

  • арифметические: +, -, *, /, %;
  • сравнения: ==, !=, <, <=, >, >=;
  • логические: &&, ||, !;
  • инкремент и декремент: ++, -- (префиксная и постфиксная формы);
  • побитовые: &, |, ^, ~, <<, >>;
  • присваивание: =, +=, -=, *=, /=, %=;
  • условный: ?:;
  • прочие: sizeof, явные приведения типов.

9.1. Приоритет

Приоритет определяет, какие операторы объединяются первыми. Сокращённая таблица, от высокого к низкому:

  1. постфиксные: a[k], f(), ., ->, a++, a--;
  2. унарные: ++a, --a, !, ~, +, -, *, &, sizeof;
  3. умножение, деление, остаток;
  4. сложение и вычитание;
  5. сдвиги;
  6. отношения <, <=, >, >=;
  7. равенство ==, !=;
  8. побитовое И;
  9. побитовое исключающее ИЛИ;
  10. побитовое ИЛИ;
  11. логическое И;
  12. логическое ИЛИ;
  13. условный оператор;
  14. присваивание;
  15. запятая.

Приоритет не задаёт порядок вычисления операндов. Для выражения f() + g() порядок вызовов не определён стандартом. Если выражение трудно читать, ставьте скобки.

9.2. Разбор выражения

Пусть a = 1, b = 2, c = 3, d = 1, все типа int. Разберём выражение

a + b * c << d || 25 != 32 && !c++

По приоритету оно эквивалентно

((a + (b * c)) << d) || ((25 != 32) && (!(c++)))

Левая часть даёт (1+2⋅3)⋅2=14(1 + 2 \cdot 3) \cdot 2 = 14. Поскольку || вычисляется с коротким замыканием и левая часть ненулевая, правая часть не вычисляется, и c++ не происходит. Итоговое значение — true.

Не полагайтесь на памятку

Этот пример полезен как упражнение, но в реальном коде такие выражения не пишут. Побочные эффекты внутри сложного условия (вроде c++ здесь) ломают чтение и легко ведут к ошибкам.

10. Преобразования типов

Компилятор сам приводит операнды к общему типу. Правила такие:

  • char и short сначала превращаются в int;
  • при смешивании целых и вещественных результат обычно вещественный;
  • при смешивании знаковых и беззнаковых значения приводятся к беззнаковому, что часто удивляет.

Явное преобразование пока записываем в C-стиле: (тип) выражение. В следующих лекциях появятся static_cast и другие более строгие формы.

int total = 7;
int count = 2;
double average = static_cast<double>(total) / count;  // 3.5
Деление целых

7 / 2 даёт 3, а не 3.5: оба операнда целые, поэтому и результат целый. Чтобы получить вещественный результат, приведите хотя бы один операнд к double.

11. Оператор sizeof

sizeof применяется к типу или к выражению и возвращает размер в байтах:

sizeof(int)
sizeof(double)
sizeof(variable)

Результат имеет тип std::size_t. Количество бит в байте задано константой CHAR_BIT из заголовка <climits>.

Частые ошибки

  • Чтение неинициализированной переменной.
  • Беззнаковая арифметика, которая «уходит в минус» и становится огромным числом.
  • Деление целых там, где ожидался вещественный результат.
  • Полагаться на порядок вычисления операндов в одном выражении.
  • Сравнение вещественных чисел через ==.

Мини-тренажёр

Ответы
  1. int x; std::cout << x; — неопределённое поведение: x не инициализирован.
  2. unsigned int a = 0; a -= 1; — a становится максимальным значением типа, а не −1-1.
  3. -1 < 10U — false, потому что −1-1 преобразуется в очень большое беззнаковое значение.
  4. 7 / 2 — 3; 7 / 2.0 — 3.5.
  5. 0.1 + 0.2 == 0.3 — false из-за точности двоичного представления.
  6. sizeof(char) — всегда 1.

Шпаргалка

ПонятиеСуть
ОбъявлениеСообщает имя и тип
ОпределениеОбъявление, которое создаёт объект
Беззнаковый типАрифметика по модулю 2N2^N
Дополнительный кодПредставление знаковых целых в современном C++
IEEE 754Формат float: знак, экспонента, мантисса
enum classПеречисление без неявного преобразования в int
ПриоритетПорядок группировки операторов, не порядок вычисления
sizeofРазмер в байтах, тип std::size_t