C++ Course
  • Описание курса
  • Материалы лекций
  • Задания
  • Об авторе
  • Гайды

On this page

  • Немного о языке
  • Hello, world!
  • Переменные
    • Способы создания переменной
    • Типы переменных
    • Целые числа
    • Область видимости переменной
  • Ввод-вывод
  • Операции
    • Арифметические операции
  • Задача о нахождении гипотенузы
    • Решение
    • Пример кода на C++
    • Составное форматирование
  • Задача о разрядах числа
  • Представление целых чисел в памяти компьютера
    • Как получить двоично-дополнительное представление отрицательного числа:
    • Проверка:
    • Таблица 4-битных двоично-дополнительных чисел

Переменные, ввод-вывод, операции

Немного о языке

На этом курсе будет рассматриваться язык C++, который был создан в 1980-х как расширение языка C, в котором не было доступно ООП (объектно-ориентированное программирование). Автор C++ — Бьёрн Страуструп.

Этот язык является:

  • компилируемым — по тексту программы создается исполняемый файл, который затем можно запустить

  • статически типизируемым — у каждой переменной есть свой тип, который определен еще в коде до компиляции

  • универсальным по сфере применения

  • а также инструментом для прямого управления памятью

Hello, world!

#include <iostream>

int main() {
    std::cout << "Hello, world!\n";
}

В примере выше мы “подключаем” нужный модуль директивой #include , затем в функции main (с нее начинается исполнение программы) отправляем нужную строку в поток вывода.

Переменные

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

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

#include <iostream>


int main() {
    int number = 0;
    std::cout << "Our number is " << number << std::endl;
}

Программа выше создает переменную типа int — целое число (integer) — инициализирует его нулем, а затем выводит его с подписью на стандартный поток ввода.

Способы создания переменной

int main() {
    int a, b, c; // объявление без инициализации
    a = 10; // оператор присваивания
    
    // вариант с одновременной инициализацией является предпочтительным
    int new_num_1 = 10; // создаем переменную со значением 10
    int new_num_2{10}; // альтернатива
}
Note

Объявление без инициализации, как правило, не рекомендуется, так как в переменной может оказаться мусорное значение. Это происходит из–за низкоуровневости 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() {
    bool external_flag = false;
    {
        // это внутренний scope -- область видимости
        // здесь external_flag доступен
        bool internal_flag = true;
    }
    // здесь, за пределами скоупа 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";


    // объявляем нужные нам переменные
    // не инициализируем, потому что сразу будем в них читать значения
    int age, height;
    std::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 " << 
            height << "cm tall at " << age << " years old!\n";
}

Стоит отметить, что подобное обращение к std::cin читает данные до первого пробельного символа (переноса строки, знака табуляции или пробела — \n, \t, “ ” соответственно), то есть при вводе имени из нескольких слов через пробел, в переменную name будет прочитано только первое слово. Следующее слово имени будет прочитано как рост age , что вызовет ошибку по время работы программы — runtime error

Операции

Арифметические операции

Работают стандартным образом.

  • при выполнении операций результат будет являться наибольшим из типов — например, long long + int = long long, а также float + double = double

  • операция взятия остатка — %, целочисленное деление применяется по умолчанию в целых типах (см. пример)

int main() {
    int a = 7, b = 3;
    int q = a / b;  // 2
    int r = a % b;  // 1
    
    // целочисленное и точное деление
    int c = 6, d = 4;
    c / d; // 1
    static_cast<float>(c) / d; // 1.5
    1. * c / d; // 1.5
}

Стоит обратить внимание на две последние строки:

  1. static_cast<T>(object)  приводит объект к типу T , если это возможно

  2. 1. * c / d  — сначала выполнится умножение (float * int = float), затем точно деление, так как в нем участвует float : float / int = float

Если необходимо не создать новую, а изменить уже созданную переменную, есть сокращенный синтаксис:

int main() {
    int a = 2;
    a += 5;
    std::cout << a << '\n';
    a /= 2; // целочисленное деление на 2
}

Показанные выше операции являются бинарными — в них участвует 2 операнда — объекта, с которыми они взаимодействуют.

Есть и унарные операции, например, — инкремент и декремент — увеличение и уменьшение переменной на 1 соответственно.

#include <iostream>


int main() {
    int a = 2;
    ++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';
}
Note

Примечание:
Для вычисления квадратного корня используется функция 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
    printf("What is your name?\n");
    getline(std::cin, name);
    
    printf("Hello, %s!\n", name.c_str());


    // объявляем нужные нам переменные
    // не инициализируем, потому что сразу будем в них читать значения
    int age, height;
    printf("What is your age?\n");
    scanf("%d", &age);
    printf("How tall are you?\n");
    scanf("%d", &height);


    int day, month, year;
    scanf("%d-%d-%d", &day, &month, &year);    


    printf("Hm, it seems %s is %dcm tall at %d years olf!\n", 
            name.c_str(), height, age);
}

Задача о разрядах числа

Подумаем над задачей о нахождении цифры в разряде единиц для системы счисления с основанием \(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() {
    int num = 12345;
    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 первый бит — знак, остальные — модуль. В беззнаковом представлении все биты — значение.

Двоично-дополнительный код — это способ представления отрицательных целых чисел в памяти компьютера. Он позволяет выполнять арифметические операции над знаковыми числами так же просто, как и над беззнаковыми.

Как получить двоично-дополнительное представление отрицательного числа:

  1. Записать абсолютное значение числа в двоичной системе, используя необходимое количество бит.
  2. Инвертировать все биты (заменить 0 на 1, а 1 на 0).
  3. Прибавить 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.

Denis Bakin ©

Build on Quart Academic Website Template adapted by Dr. Gang He