Переменные, ввод-вывод, операции
Немного о языке
На этом курсе будет рассматриваться язык C++, который был создан в 1980-х как расширение языка C, в котором не было доступно ООП (объектно-ориентированное программирование). Автор C++ — Бьёрн Страуструп.
Этот язык является:
компилируемым — по тексту программы создается исполняемый файл, который затем можно запустить
статически типизируемым — у каждой переменной есть свой тип, который определен еще в коде до компиляции
универсальным по сфере применения
а также инструментом для прямого управления памятью
Hello, world!
#include <iostream>
int main() {
std::cout << "Hello, world!\n";
}
В примере выше мы “подключаем” нужный модуль директивой #include
, затем в функции main
(с нее начинается исполнение программы) отправляем нужную строку в поток вывода.
Переменные
Программу можно рассматривать как последовательность действий, где точно известно следующее действие, если мы знаем состояние программы — значения всех переменных.
Переменная — объект определенного типа, у нее может быть определено значение. Тип переменной должен быть известен на этапе компиляции.
#include <iostream>
int main() {
= 0;
int number std::cout << "Our number is " << number << std::endl;
}
Программа выше создает переменную типа int
— целое число (integer) — инициализирует его нулем, а затем выводит его с подписью на стандартный поток ввода.
Способы создания переменной
int main() {
, b, c; // объявление без инициализации
int a= 10; // оператор присваивания
a
// вариант с одновременной инициализацией является предпочтительным
= 10; // создаем переменную со значением 10
int new_num_1 {10}; // альтернатива
int new_num_2}
Объявление без инициализации, как правило, не рекомендуется, так как в переменной может оказаться мусорное значение. Это происходит из–за низкоуровневости C++, компилятор которого выделяет, но не очищает память для переменной.
То есть всегда нужно указывать начальное значение переменной.
Типы переменных
Полный список встроенных типов
// #include <string> должен быть написан выше для корректной работы с std::string
char c = '1'; // символ
std::string s = "text"; // строка, не является встроенным типом, состоит из символов (char)
bool b = true; // boolean, булевая, логическая переменная, принимает значения false и true
int i = 42; // integer, целое число (как правило, 4 байта)
short int si = 17; // короткое целое (занимает 2 байта)
long li = 12321321312; // длинное целое (как правило, 8 байт)
float f = 2.71828; // дробное число с плавающей запятой (4 байта)
double d = 3.141592; // дробное число двойной точности (8 байт)
long double ld = 1e15; // длинное дробное (как правило, 16 байт)
Целые числа
Название типа | Размер в байтах на 64-bit системе Unix и минимальный размер по стандарту |
char |
\(1 (\geq1)\) |
short int |
\(2 (\geq2)\) |
int |
\(4 (\geq2)\) |
long int |
\(8 (\geq4)\) |
long long int |
\(8 (\geq8)\) |
Причем char
\(\leq\) short int
\(\leq\) int
\(\leq\) long int
\(\leq\) long long int
вне зависимости от архитектуры системы.
Подсчитаем границы множества возможных значений переменной типа char: \(-2^{7}\leq2^{7}-1\), то есть \(-128\leq var \leq 127\), так как переменная должна уметь принимать как отрицательные, так и положительные значения.
Важно отметить, что любой целый тип можно сделать беззнаковым, тогда бит, который обычно отводится на знак, удвоит количество принимаемых положительных значений. В этом случае: \(0 \leq var \leq 2^{8}-1=255\)
Область видимости переменной
Области видимости — тот участок кода, где переменная существует, как правило, выделяется фигурными скобками. Например, функция main
— области видимости большинства наших переменных.
// глобальная переменная -- доступна везде
bool global_flag = false;
int main() {
= false;
bool external_flag {
// это внутренний scope -- область видимости
// здесь external_flag доступен
= true;
bool internal_flag }
// здесь, за пределами скоупа internal_flag уже недоступен
// main.cpp: error: use of undeclared identifier 'internal_flag'
}
external_flag
и internal_flag
называются локальными, так как доступны только в своей области видимости (скоупе) — функции main
или скоупе{ }
. На предстоящих уроках мы узнаем, что множество конструкций в C++ используют { }
для обособления своей части кода — тела цикла или условия — для них также будет работать эта логика локальных переменных, который нельзя использовать извне.
Ввод-вывод
Мы уже сталкивались с выводом в консоль при написании самой первой программы, теперь же рассмотрим ввод и вывод чуть подробнее. Каждый исполняемый файл по умолчанию получает указание, откуда ему по умолчанию читать (стандартный поток ввода, std::cin
), а куда по умолчанию писать (стандартный поток вывода, std::cout
) данные в текстовом виде. При использовании чтения и записи с этими объектами форматирование для конкретного типа переменной произойдет автоматически.
Воспользуемся этими знаниями для создания простого диалогового бота:
#include <iostream>
#include <string>
int main() {
std::string name; // объявляем переменную name
std::cout << "What is your name?\n";
std::cin >> name; // считываем её значение с клавиатуры
std::cout << "Hello, " << name << "!\n";
// объявляем нужные нам переменные
// не инициализируем, потому что сразу будем в них читать значения
, height;
int agestd::cout << "What is your age?\n";
std::cin >> age;
std::cout << "How tall are you?" << std::endl;
std::cin >> height;
std::cout << "Hm, it seems " << name << " is " <<
<< "cm tall at " << age << " years old!\n";
height }
Стоит отметить, что подобное обращение к std::cin
читает данные до первого пробельного символа (переноса строки, знака табуляции или пробела — \n, \t, “ ”
соответственно), то есть при вводе имени из нескольких слов через пробел, в переменную name
будет прочитано только первое слово. Следующее слово имени будет прочитано как рост age
, что вызовет ошибку по время работы программы — runtime error
Операции
Арифметические операции
Работают стандартным образом.
при выполнении операций результат будет являться наибольшим из типов — например,
long long + int = long long
, а такжеfloat + double = double
операция взятия остатка —
%
, целочисленное деление применяется по умолчанию в целых типах (см. пример)
int main() {
= 7, b = 3;
int a = a / b; // 2
int q = a % b; // 1
int r
// целочисленное и точное деление
= 6, d = 4;
int c / d; // 1
c <float>(c) / d; // 1.5
static_cast1. * c / d; // 1.5
}
Стоит обратить внимание на две последние строки:
static_cast<T>(object)
приводит объект к типуT
, если это возможно1. * c / d
— сначала выполнится умножение (float * int = float
), затем точно деление, так как в нем участвуетfloat
:float / int = float
Если необходимо не создать новую, а изменить уже созданную переменную, есть сокращенный синтаксис:
int main() {
= 2;
int a += 5;
a std::cout << a << '\n';
/= 2; // целочисленное деление на 2
a }
Показанные выше операции являются бинарными — в них участвует 2 операнда — объекта, с которыми они взаимодействуют.
Есть и унарные операции, например, — инкремент и декремент — увеличение и уменьшение переменной на 1 соответственно.
#include <iostream>
int main() {
= 2;
int a ++a;
std::cout << a << '\n'; // 3
std::cout << ++a << '\n'; // 4
std::cout << a++ << '\n'; // 4
std::cout << a << '\n'; // 5
}
Почему в последней строки было выведено 4, а не 5? Такой инкремент (когда ++ стоит после переменной) выполняет операцию, но возвращает свое старое значение, до увеличения.
Задача о нахождении гипотенузы
Задача:
Даны два целых числа \(a\) и \(b\). Найдите гипотенузу прямоугольного треугольника с катетами \(a\) и \(b\).
Входные данные:
В двух строках вводятся два целых положительных числа, не превышающих \(1000\).
Выходные данные:
Выведите длину гипотенузы.
Решение
Формула для гипотенузы:
\[
c = \sqrt{a^2 + b^2}
\]
Пример кода на C++
#include <iostream>
#include <cmath>
int main() {
int a, b;
std::cin >> a >> b;
float hypot = std::sqrt(a * a + b * b);
std::cout << hypot << '\n';
}
Примечание:
Для вычисления квадратного корня используется функция std::sqrt
из библиотеки <cmath>
.
Составное форматирование
Как еще можно читать и выводить данные с потоков ввода и вывода? Оказывается, что можно использовать более низкоуровневый функционал (который был унаследован у языка C) — функции prinf
и scanf
. Идея этих функций заключается в наличие формата, по которому эта функция будет либо читать в, либо подставлять указанные переменные.
scanf
не принято использовать для работы со строками, для этого лучше подходит функция getline(stream, string)
, которая читает из консоли строку целиком, не обращая внимания на пробельные символы до переноса строки.
Пример выше, переписанный с использованием prinf
и scanf
%d
— целое число%f
— число с плавающей точкой%s
— строка
#include <iostream>
#include <string>
int main() {
std::string name; // объявляем переменную name
("What is your name?\n");
printf(std::cin, name);
getline
("Hello, %s!\n", name.c_str());
printf
// объявляем нужные нам переменные
// не инициализируем, потому что сразу будем в них читать значения
, height;
int age("What is your age?\n");
printf("%d", &age);
scanf("How tall are you?\n");
printf("%d", &height);
scanf
, month, year;
int day("%d-%d-%d", &day, &month, &year);
scanf
("Hm, it seems %s is %dcm tall at %d years olf!\n",
printf.c_str(), height, age);
name}
Задача о разрядах числа
Подумаем над задачей о нахождении цифры в разряде единиц для системы счисления с основанием \(d\). По определению позиционных систем счисления:
\(\overline{a_{1}a_{2}...a_{n}}_{10}=\underbrace{b_{1}\cdot d^{m} + b_{2} \cdot d^{m -1} +\cdot\cdot\cdot + }_{\vdots d} d^{m} \cdot d^{0}= \overline{b_{1}b_{2}...b_{m}}_d\)
Подобным разложением для получения цифр в другой системе счисления мы будем пользоваться часто.
Чтобы получить цифру \(d_{m}\), заметим, что все остальные слагаемые делятся на \(d\), поэтому ответом будет остаток от деления на \(d\). Для получения цифры в разряде \(m-1\) нужно сначала выполнить целочисленное деление на \(d\), а затем снова взять остаток.
Рассмотрим на примере более близкой нам, десятичной системы счисления:
#include <iostream>
#include <cmath>
int main() {
= 12345;
int num std::cout << num % 10 << '\n'; // 5
std::cout << num / 10 % 10 << '\n'; // 4
std::cout << num / 100 % 10 << '\n'; // 3
}
Представление целых чисел в памяти компьютера
Есть различные способы хранить знаковые целые числа в памяти. Один из них — sign-magnitude, то есть первый бит отведен на знак, остальные — на модуль. Это кажется более естественным, но только не для компьютера.
Например: \(-6\) в sign-magnitude будет представлено как 110
, где 1
— знак (отрицательный), а 10
— модуль.
Бинарное значение | Sign-magnitude | Беззнаковое |
---|---|---|
00000000 | 0 | 0 |
00000001 | 1 | 1 |
… | … | … |
01111111 | 127 | 127 |
10000000 | -0 | 128 |
10000001 | -1 | 129 |
… | … | … |
11111111 | -127 | 255 |
В sign-magnitude первый бит — знак, остальные — модуль. В беззнаковом представлении все биты — значение.
Двоично-дополнительный код — это способ представления отрицательных целых чисел в памяти компьютера. Он позволяет выполнять арифметические операции над знаковыми числами так же просто, как и над беззнаковыми.
Как получить двоично-дополнительное представление отрицательного числа:
- Записать абсолютное значение числа в двоичной системе, используя необходимое количество бит.
- Инвертировать все биты (заменить 0 на 1, а 1 на 0).
- Прибавить 1 к полученному числу.
Пример:
Получим представление числа \(-6\) в 4-битном двоично-дополнительном коде.
- \(+6\) в двоичном виде:
0110
- Инвертируем все биты:
1001
- Прибавляем 1:
1001 + 1 = 1010
Таким образом, \(-6\) в 4-битном двоично-дополнительном коде — это 1010
.
Проверка:
Бит | 1 | 0 | 1 | 0 |
---|---|---|---|---|
Значение | \(-8\) | \(4\) | \(2\) | \(1\) |
\(1010 = -8 + 0 + 2 + 0 = -6\)
Таблица 4-битных двоично-дополнительных чисел
Биты | Десятичное значение |
---|---|
0000 | 0 |
0001 | 1 |
0010 | 2 |
0011 | 3 |
0100 | 4 |
0101 | 5 |
0110 | 6 |
0111 | 7 |
1000 | -8 |
1001 | -7 |
1010 | -6 |
1011 | -5 |
1100 | -4 |
1101 | -3 |
1110 | -2 |
1111 | -1 |
Замечание:
Инвертирование и прибавление 1 — это универсальный способ получить противоположное число в двоично-дополнительном коде. Например, чтобы получить \(-n\), достаточно инвертировать все биты числа \(n\) и прибавить 1.